diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-29 18:22:40 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-29 18:22:40 +0100 |
| commit | b5a52fc0312afdf5d5fbe53275dc88d6d881c2ff (patch) | |
| tree | d12e3c7f6c23ae1698199d16ee763551edfd8c12 /src/client | |
| parent | v2.1.2: ignore ANSI when measuring board height (diff) | |
| download | zagan-bet-b5a52fc0312afdf5d5fbe53275dc88d6d881c2ff.tar.gz zagan-bet-b5a52fc0312afdf5d5fbe53275dc88d6d881c2ff.zip | |
v2.2.0: add blackjack with reserved bets and hit/stand/double
Diffstat (limited to '')
| -rw-r--r-- | src/client/core.rs | 154 | ||||
| -rw-r--r-- | src/client/games/blackjack.rs | 148 | ||||
| -rw-r--r-- | src/client/games/manager.rs | 49 | ||||
| -rw-r--r-- | src/client/games/mod.rs | 1 | ||||
| -rw-r--r-- | src/client/games/response.rs | 22 |
5 files changed, 369 insertions, 5 deletions
diff --git a/src/client/core.rs b/src/client/core.rs index e005f2a..6e9c28b 100644 --- a/src/client/core.rs +++ b/src/client/core.rs | |||
| @@ -1,5 +1,7 @@ | |||
| 1 | use crate::client::games::manager::GameManager; | 1 | use crate::client::games::manager::GameManager; |
| 2 | use crate::client::games::response::{parse_roulette_response, parse_spin_response}; | 2 | use crate::client::games::response::{ |
| 3 | parse_blackjack_response, parse_roulette_response, parse_spin_response, | ||
| 4 | }; | ||
| 3 | use crate::client::ui::{ | 5 | use crate::client::ui::{ |
| 4 | command_names, completion_lines, display_width, help_lines, input_cursor_column, | 6 | command_names, completion_lines, display_width, help_lines, input_cursor_column, |
| 5 | parse_bet_arg, record_history, | 7 | parse_bet_arg, record_history, |
| @@ -61,6 +63,9 @@ pub enum Effect { | |||
| 61 | enum Pending { | 63 | enum Pending { |
| 62 | Spin, | 64 | Spin, |
| 63 | RouletteSpin, | 65 | RouletteSpin, |
| 66 | BlackjackStart, | ||
| 67 | BlackjackAction, | ||
| 68 | BlackjackCancel, | ||
| 64 | Ping, | 69 | Ping, |
| 65 | Echo, | 70 | Echo, |
| 66 | Info, | 71 | Info, |
| @@ -191,6 +196,32 @@ impl ClientCore { | |||
| 191 | self.board_effects() | 196 | self.board_effects() |
| 192 | } | 197 | } |
| 193 | }, | 198 | }, |
| 199 | Some(Pending::BlackjackStart) | Some(Pending::BlackjackAction) => { | ||
| 200 | match parse_blackjack_response(reply) { | ||
| 201 | Ok(state) => { | ||
| 202 | self.balance = Some(state.balance); | ||
| 203 | self.game.apply_blackjack_state(state); | ||
| 204 | self.board_effects() | ||
| 205 | } | ||
| 206 | Err(message) => { | ||
| 207 | self.game.set_error(message); | ||
| 208 | self.board_effects() | ||
| 209 | } | ||
| 210 | } | ||
| 211 | } | ||
| 212 | Some(Pending::BlackjackCancel) => match parse_blackjack_response(reply) { | ||
| 213 | Ok(_) => { | ||
| 214 | self.game.end_game(); | ||
| 215 | vec![ | ||
| 216 | Effect::ClearRegion, | ||
| 217 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 218 | ] | ||
| 219 | } | ||
| 220 | Err(message) => { | ||
| 221 | self.game.set_error(message); | ||
| 222 | self.board_effects() | ||
| 223 | } | ||
| 224 | }, | ||
| 194 | Some(Pending::Ping) => { | 225 | Some(Pending::Ping) => { |
| 195 | if reply.trim() == "PONG" { | 226 | if reply.trim() == "PONG" { |
| 196 | print_line("Pong") | 227 | print_line("Pong") |
| @@ -225,14 +256,16 @@ impl ClientCore { | |||
| 225 | } | 256 | } |
| 226 | 257 | ||
| 227 | fn handle_game_key(&mut self, key: Key) -> Vec<Effect> { | 258 | fn handle_game_key(&mut self, key: Key) -> Vec<Effect> { |
| 228 | if self | 259 | let game_type = self |
| 229 | .game | 260 | .game |
| 230 | .get_current_state() | 261 | .get_current_state() |
| 231 | .map(|state| state.game_type.as_str()) | 262 | .map(|state| state.game_type.as_str()); |
| 232 | == Some("roulette") | 263 | if game_type == Some("roulette") { |
| 233 | { | ||
| 234 | return self.handle_roulette_key(key); | 264 | return self.handle_roulette_key(key); |
| 235 | } | 265 | } |
| 266 | if game_type == Some("blackjack") { | ||
| 267 | return self.handle_blackjack_key(key); | ||
| 268 | } | ||
| 236 | 269 | ||
| 237 | match key { | 270 | match key { |
| 238 | Key::CtrlC => vec![Effect::Quit], | 271 | Key::CtrlC => vec![Effect::Quit], |
| @@ -411,6 +444,117 @@ impl ClientCore { | |||
| 411 | .unwrap_or_default() | 444 | .unwrap_or_default() |
| 412 | } | 445 | } |
| 413 | 446 | ||
| 447 | fn handle_blackjack_key(&mut self, key: Key) -> Vec<Effect> { | ||
| 448 | let status = self | ||
| 449 | .game | ||
| 450 | .get_current_state() | ||
| 451 | .and_then(|state| state.data.get("status")) | ||
| 452 | .cloned() | ||
| 453 | .unwrap_or_else(|| "ready".to_string()); | ||
| 454 | let hand = self.game.blackjack_state(); | ||
| 455 | |||
| 456 | match key { | ||
| 457 | Key::CtrlC => vec![Effect::Quit], | ||
| 458 | Key::Char('q') => { | ||
| 459 | if let Some(hand) = hand.filter(|hand| hand.phase == "player") { | ||
| 460 | self.game.set_blackjack_status("cancelling"); | ||
| 461 | self.pending = Some(Pending::BlackjackCancel); | ||
| 462 | vec![Effect::SendLine(format!( | ||
| 463 | "BLACKJACK_ACTION {} cancel", | ||
| 464 | hand.session_id | ||
| 465 | ))] | ||
| 466 | } else { | ||
| 467 | self.game.end_game(); | ||
| 468 | vec![ | ||
| 469 | Effect::ClearRegion, | ||
| 470 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 471 | ] | ||
| 472 | } | ||
| 473 | } | ||
| 474 | Key::Char(' ') => self.start_blackjack(), | ||
| 475 | Key::Char('h') | ||
| 476 | if hand.as_ref().is_some_and(|hand| hand.phase == "player") => | ||
| 477 | { | ||
| 478 | self.blackjack_action("hit") | ||
| 479 | } | ||
| 480 | Key::Char('s') | ||
| 481 | if hand.as_ref().is_some_and(|hand| hand.phase == "player") => | ||
| 482 | { | ||
| 483 | self.blackjack_action("stand") | ||
| 484 | } | ||
| 485 | Key::Char('d') | ||
| 486 | if hand.as_ref().is_some_and(|hand| { | ||
| 487 | hand.phase == "player" | ||
| 488 | && hand.actions.iter().any(|action| action == "double") | ||
| 489 | }) => | ||
| 490 | { | ||
| 491 | self.blackjack_action("double") | ||
| 492 | } | ||
| 493 | Key::Char('+') | Key::Char('=') | ||
| 494 | if matches!(status.as_str(), "ready" | "done") => | ||
| 495 | { | ||
| 496 | let _ = self.game.adjust_bet(10); | ||
| 497 | self.board_effects() | ||
| 498 | } | ||
| 499 | Key::Char('-') if matches!(status.as_str(), "ready" | "done") => { | ||
| 500 | let _ = self.game.adjust_bet(-10); | ||
| 501 | self.board_effects() | ||
| 502 | } | ||
| 503 | _ => Vec::new(), | ||
| 504 | } | ||
| 505 | } | ||
| 506 | |||
| 507 | fn start_blackjack(&mut self) -> Vec<Effect> { | ||
| 508 | let status = self | ||
| 509 | .game | ||
| 510 | .get_current_state() | ||
| 511 | .and_then(|state| state.data.get("status")) | ||
| 512 | .cloned() | ||
| 513 | .unwrap_or_else(|| "ready".to_string()); | ||
| 514 | if !matches!(status.as_str(), "ready" | "done") { | ||
| 515 | return Vec::new(); | ||
| 516 | } | ||
| 517 | let Some(request) = self.game.spin_request() else { | ||
| 518 | return Vec::new(); | ||
| 519 | }; | ||
| 520 | if let Some(balance) = self.balance { | ||
| 521 | let bet = self | ||
| 522 | .game | ||
| 523 | .get_current_state() | ||
| 524 | .map(|state| state.bet_amount) | ||
| 525 | .unwrap_or(0); | ||
| 526 | if bet as i64 > balance { | ||
| 527 | self.game.set_error("Insufficient balance"); | ||
| 528 | return self.board_effects(); | ||
| 529 | } | ||
| 530 | } | ||
| 531 | |||
| 532 | self.game.set_blackjack_status("dealing"); | ||
| 533 | self.pending = Some(Pending::BlackjackStart); | ||
| 534 | let mut effects = self.board_effects(); | ||
| 535 | effects.push(Effect::SendLine(request)); | ||
| 536 | effects | ||
| 537 | } | ||
| 538 | |||
| 539 | fn blackjack_action(&mut self, action: &str) -> Vec<Effect> { | ||
| 540 | let Some(hand) = self.game.blackjack_state() else { | ||
| 541 | return Vec::new(); | ||
| 542 | }; | ||
| 543 | if hand.phase != "player" { | ||
| 544 | return Vec::new(); | ||
| 545 | } | ||
| 546 | if action == "double" | ||
| 547 | && !hand.actions.iter().any(|available| available == "double") | ||
| 548 | { | ||
| 549 | return Vec::new(); | ||
| 550 | } | ||
| 551 | self.pending = Some(Pending::BlackjackAction); | ||
| 552 | vec![Effect::SendLine(format!( | ||
| 553 | "BLACKJACK_ACTION {} {}", | ||
| 554 | hand.session_id, action | ||
| 555 | ))] | ||
| 556 | } | ||
| 557 | |||
| 414 | fn submit(&mut self) -> Vec<Effect> { | 558 | fn submit(&mut self) -> Vec<Effect> { |
| 415 | let line = self.input.trim().to_string(); | 559 | let line = self.input.trim().to_string(); |
| 416 | self.input.clear(); | 560 | self.input.clear(); |
diff --git a/src/client/games/blackjack.rs b/src/client/games/blackjack.rs new file mode 100644 index 0000000..a34eb63 --- /dev/null +++ b/src/client/games/blackjack.rs | |||
| @@ -0,0 +1,148 @@ | |||
| 1 | use super::traits::{Game, GameState}; | ||
| 2 | use crate::blackjack::{card_label, BlackjackState}; | ||
| 3 | |||
| 4 | pub const BLACKJACK_BOARD_LINES: usize = 6; | ||
| 5 | |||
| 6 | pub struct BlackjackGame; | ||
| 7 | |||
| 8 | impl BlackjackGame { | ||
| 9 | pub fn new() -> Self { | ||
| 10 | Self | ||
| 11 | } | ||
| 12 | |||
| 13 | fn state(state: &GameState) -> Option<BlackjackState> { | ||
| 14 | state | ||
| 15 | .data | ||
| 16 | .get("blackjack") | ||
| 17 | .and_then(|raw| serde_json::from_str(raw).ok()) | ||
| 18 | } | ||
| 19 | |||
| 20 | fn cards(cards: &[String]) -> String { | ||
| 21 | cards | ||
| 22 | .iter() | ||
| 23 | .map(|card| { | ||
| 24 | if card == "?" { | ||
| 25 | "[ ? ]".to_string() | ||
| 26 | } else { | ||
| 27 | format!("[{}]", card_label(card)) | ||
| 28 | } | ||
| 29 | }) | ||
| 30 | .collect::<Vec<_>>() | ||
| 31 | .join(" ") | ||
| 32 | } | ||
| 33 | |||
| 34 | fn result_text(result: &str, winnings: u64) -> String { | ||
| 35 | match result { | ||
| 36 | "blackjack" => format!("BLACKJACK! Won {winnings} PLN"), | ||
| 37 | "win" => format!("Win! Won {winnings} PLN"), | ||
| 38 | "push" => "Push. Bet returned.".to_string(), | ||
| 39 | "lose" => "Dealer wins.".to_string(), | ||
| 40 | "bust" => "Bust.".to_string(), | ||
| 41 | "cancelled" => "Cancelled.".to_string(), | ||
| 42 | _ => "Hand complete.".to_string(), | ||
| 43 | } | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 47 | impl Game for BlackjackGame { | ||
| 48 | fn get_name(&self) -> &'static str { | ||
| 49 | "Blackjack" | ||
| 50 | } | ||
| 51 | |||
| 52 | fn render(&self, state: &GameState) -> String { | ||
| 53 | let status = state | ||
| 54 | .data | ||
| 55 | .get("status") | ||
| 56 | .map(String::as_str) | ||
| 57 | .unwrap_or("ready"); | ||
| 58 | let balance = state | ||
| 59 | .data | ||
| 60 | .get("balance") | ||
| 61 | .map(String::as_str) | ||
| 62 | .unwrap_or("?"); | ||
| 63 | let hand = Self::state(state); | ||
| 64 | |||
| 65 | if let Some(error) = state.data.get("error") { | ||
| 66 | let bet = hand.as_ref().map(|hand| hand.bet).unwrap_or(state.bet_amount); | ||
| 67 | return format!( | ||
| 68 | "=== BLACKJACK ===\r\nBalance: {balance} PLN Bet: {bet} PLN\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit" | ||
| 69 | ); | ||
| 70 | } | ||
| 71 | |||
| 72 | let (dealer, dealer_total, player, player_total, bet, text, controls) = | ||
| 73 | if let Some(hand) = &hand { | ||
| 74 | let bet = if status == "player" { | ||
| 75 | hand.bet | ||
| 76 | } else { | ||
| 77 | state.bet_amount | ||
| 78 | }; | ||
| 79 | let text = match status { | ||
| 80 | "player" => { | ||
| 81 | if hand.actions.iter().any(|action| action == "double") { | ||
| 82 | "Hit, stand or double.".to_string() | ||
| 83 | } else { | ||
| 84 | "Hit or stand.".to_string() | ||
| 85 | } | ||
| 86 | } | ||
| 87 | "dealing" => "Dealing...".to_string(), | ||
| 88 | "cancelling" => "Cancelling...".to_string(), | ||
| 89 | "done" => Self::result_text(&hand.result, hand.winnings), | ||
| 90 | _ => "Press [SPACE] to deal.".to_string(), | ||
| 91 | }; | ||
| 92 | let controls = if status == "player" { | ||
| 93 | if hand.actions.iter().any(|action| action == "double") { | ||
| 94 | "[H]it [S]tand [D]ouble [Q]uit".to_string() | ||
| 95 | } else { | ||
| 96 | "[H]it [S]tand [Q]uit".to_string() | ||
| 97 | } | ||
| 98 | } else { | ||
| 99 | "[SPACE] New hand [+/-] Bet [Q] Quit".to_string() | ||
| 100 | }; | ||
| 101 | ( | ||
| 102 | Self::cards(&hand.dealer), | ||
| 103 | hand.dealer_total, | ||
| 104 | Self::cards(&hand.player), | ||
| 105 | hand.player_total, | ||
| 106 | bet, | ||
| 107 | text, | ||
| 108 | controls, | ||
| 109 | ) | ||
| 110 | } else { | ||
| 111 | ( | ||
| 112 | "—".to_string(), | ||
| 113 | 0, | ||
| 114 | "—".to_string(), | ||
| 115 | 0, | ||
| 116 | state.bet_amount, | ||
| 117 | "Press [SPACE] to deal.".to_string(), | ||
| 118 | "[SPACE] Deal [+/-] Bet [Q] Quit".to_string(), | ||
| 119 | ) | ||
| 120 | }; | ||
| 121 | |||
| 122 | format!( | ||
| 123 | "=== BLACKJACK ===\r\nBalance: {} PLN Bet: {} PLN\r\nDealer: {} {}\r\nYou: {} {}\r\n{}\r\n{}", | ||
| 124 | balance, bet, dealer, dealer_total, player, player_total, text, controls | ||
| 125 | ) | ||
| 126 | } | ||
| 127 | |||
| 128 | fn render_lines(&self, _state: &GameState) -> usize { | ||
| 129 | BLACKJACK_BOARD_LINES | ||
| 130 | } | ||
| 131 | |||
| 132 | fn get_help(&self) -> String { | ||
| 133 | "BLACKJACK\n\ | ||
| 134 | =========\n\ | ||
| 135 | Six-deck shoe, fresh for every hand.\n\ | ||
| 136 | Dealer stands on all 17s.\n\ | ||
| 137 | Blackjack pays 3:2.\n\ | ||
| 138 | Double is allowed on the first two cards.\n\ | ||
| 139 | \n\ | ||
| 140 | H - hit\n\ | ||
| 141 | S - stand\n\ | ||
| 142 | D - double (when available)\n\ | ||
| 143 | SPACE - deal a new hand\n\ | ||
| 144 | +/- - change the next hand's bet\n\ | ||
| 145 | Q - leave the table" | ||
| 146 | .to_string() | ||
| 147 | } | ||
| 148 | } | ||
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index 02e9451..4e0218b 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs | |||
| @@ -1,7 +1,9 @@ | |||
| 1 | use super::blackjack::BlackjackGame; | ||
| 1 | use super::response::{RouletteResult, SpinResult}; | 2 | use super::response::{RouletteResult, SpinResult}; |
| 2 | use super::roulette::RouletteGame; | 3 | use super::roulette::RouletteGame; |
| 3 | use super::slots::SlotsGame; | 4 | use super::slots::SlotsGame; |
| 4 | use super::traits::{Game, GameError, GamePhase, GameState}; | 5 | use super::traits::{Game, GameError, GamePhase, GameState}; |
| 6 | use crate::blackjack::BlackjackState; | ||
| 5 | use crate::roulette::Bets; | 7 | use crate::roulette::Bets; |
| 6 | use std::collections::HashMap; | 8 | use std::collections::HashMap; |
| 7 | 9 | ||
| @@ -15,6 +17,7 @@ impl GameManager { | |||
| 15 | let mut games: HashMap<String, Box<dyn Game>> = HashMap::new(); | 17 | let mut games: HashMap<String, Box<dyn Game>> = HashMap::new(); |
| 16 | games.insert("slots".to_string(), Box::new(SlotsGame::new())); | 18 | games.insert("slots".to_string(), Box::new(SlotsGame::new())); |
| 17 | games.insert("roulette".to_string(), Box::new(RouletteGame::new())); | 19 | games.insert("roulette".to_string(), Box::new(RouletteGame::new())); |
| 20 | games.insert("blackjack".to_string(), Box::new(BlackjackGame::new())); | ||
| 18 | 21 | ||
| 19 | Self { | 22 | Self { |
| 20 | current_session: None, | 23 | current_session: None, |
| @@ -51,6 +54,9 @@ impl GameManager { | |||
| 51 | data.insert("status".to_string(), "place".to_string()); | 54 | data.insert("status".to_string(), "place".to_string()); |
| 52 | data.insert("number_input".to_string(), String::new()); | 55 | data.insert("number_input".to_string(), String::new()); |
| 53 | 0 | 56 | 0 |
| 57 | } else if game_type == "blackjack" { | ||
| 58 | data.insert("status".to_string(), "ready".to_string()); | ||
| 59 | 10 | ||
| 54 | } else { | 60 | } else { |
| 55 | 10 | 61 | 10 |
| 56 | }; | 62 | }; |
| @@ -260,6 +266,49 @@ impl GameManager { | |||
| 260 | .insert("number_input".to_string(), String::new()); | 266 | .insert("number_input".to_string(), String::new()); |
| 261 | } | 267 | } |
| 262 | 268 | ||
| 269 | pub fn apply_blackjack_state(&mut self, state: BlackjackState) { | ||
| 270 | let Some(session) = self.current_session.as_mut() else { | ||
| 271 | return; | ||
| 272 | }; | ||
| 273 | session.phase = if state.phase == "player" { | ||
| 274 | GamePhase::Playing | ||
| 275 | } else { | ||
| 276 | GamePhase::Finished | ||
| 277 | }; | ||
| 278 | session.bet_amount = state.base_bet; | ||
| 279 | session.data.remove("error"); | ||
| 280 | session | ||
| 281 | .data | ||
| 282 | .insert("balance".to_string(), state.balance.to_string()); | ||
| 283 | session.data.insert( | ||
| 284 | "status".to_string(), | ||
| 285 | if state.phase == "player" { | ||
| 286 | "player".to_string() | ||
| 287 | } else { | ||
| 288 | "done".to_string() | ||
| 289 | }, | ||
| 290 | ); | ||
| 291 | session.data.insert( | ||
| 292 | "blackjack".to_string(), | ||
| 293 | serde_json::to_string(&state).unwrap_or_default(), | ||
| 294 | ); | ||
| 295 | } | ||
| 296 | |||
| 297 | pub fn blackjack_state(&self) -> Option<BlackjackState> { | ||
| 298 | self.current_session | ||
| 299 | .as_ref() | ||
| 300 | .and_then(|session| session.data.get("blackjack")) | ||
| 301 | .and_then(|raw| serde_json::from_str(raw).ok()) | ||
| 302 | } | ||
| 303 | |||
| 304 | pub fn set_blackjack_status(&mut self, status: &str) { | ||
| 305 | if let Some(session) = self.current_session.as_mut() { | ||
| 306 | session | ||
| 307 | .data | ||
| 308 | .insert("status".to_string(), status.to_string()); | ||
| 309 | } | ||
| 310 | } | ||
| 311 | |||
| 263 | pub fn get_current_game(&self) -> Option<&dyn Game> { | 312 | pub fn get_current_game(&self) -> Option<&dyn Game> { |
| 264 | self.current_session | 313 | self.current_session |
| 265 | .as_ref() | 314 | .as_ref() |
diff --git a/src/client/games/mod.rs b/src/client/games/mod.rs index 131ffbb..40ba32a 100644 --- a/src/client/games/mod.rs +++ b/src/client/games/mod.rs | |||
| @@ -1,3 +1,4 @@ | |||
| 1 | pub mod blackjack; | ||
| 1 | pub mod manager; | 2 | pub mod manager; |
| 2 | pub mod response; | 3 | pub mod response; |
| 3 | pub mod roulette; | 4 | pub mod roulette; |
diff --git a/src/client/games/response.rs b/src/client/games/response.rs index 73080dd..d9883f7 100644 --- a/src/client/games/response.rs +++ b/src/client/games/response.rs | |||
| @@ -141,3 +141,25 @@ pub fn parse_roulette_response(response: &str) -> Result<RouletteResult, String> | |||
| 141 | bets, | 141 | bets, |
| 142 | }) | 142 | }) |
| 143 | } | 143 | } |
| 144 | |||
| 145 | pub fn parse_blackjack_response( | ||
| 146 | response: &str, | ||
| 147 | ) -> Result<crate::blackjack::BlackjackState, String> { | ||
| 148 | let response = response.trim(); | ||
| 149 | if response.is_empty() { | ||
| 150 | return Err("No response from server".to_string()); | ||
| 151 | } | ||
| 152 | if let Some(message) = response.strip_prefix("ERROR") { | ||
| 153 | let message = message.trim(); | ||
| 154 | return Err(if message.is_empty() { | ||
| 155 | "Server error".to_string() | ||
| 156 | } else { | ||
| 157 | message.to_string() | ||
| 158 | }); | ||
| 159 | } | ||
| 160 | |||
| 161 | let raw = response | ||
| 162 | .strip_prefix("BLACKJACK_STATE ") | ||
| 163 | .ok_or_else(|| format!("Unexpected server response: {response}"))?; | ||
| 164 | serde_json::from_str(raw).map_err(|error| format!("Invalid blackjack state: {error}")) | ||
| 165 | } | ||
