diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-29 17:18:34 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-29 17:18:34 +0100 |
| commit | d9f1fe513e6f7696809d3a9b4d44ca485fe99c97 (patch) | |
| tree | 9da99200fdfe004a47056d10340b3f59c7ccc52e /src/client/core.rs | |
| parent | v2.0.0: reset plays audit to a generic per-game schema (diff) | |
| download | zagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.tar.gz zagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.zip | |
v2.1.0: add European roulette with split bets and wheel animation
Diffstat (limited to 'src/client/core.rs')
| -rw-r--r-- | src/client/core.rs | 183 |
1 files changed, 182 insertions, 1 deletions
diff --git a/src/client/core.rs b/src/client/core.rs index a88991b..e005f2a 100644 --- a/src/client/core.rs +++ b/src/client/core.rs | |||
| @@ -1,9 +1,10 @@ | |||
| 1 | use crate::client::games::manager::GameManager; | 1 | use crate::client::games::manager::GameManager; |
| 2 | use crate::client::games::response::parse_spin_response; | 2 | use crate::client::games::response::{parse_roulette_response, parse_spin_response}; |
| 3 | use crate::client::ui::{ | 3 | use crate::client::ui::{ |
| 4 | command_names, completion_lines, display_width, help_lines, input_cursor_column, | 4 | command_names, completion_lines, display_width, help_lines, input_cursor_column, |
| 5 | parse_bet_arg, record_history, | 5 | parse_bet_arg, record_history, |
| 6 | }; | 6 | }; |
| 7 | use crate::roulette::{Bets, NumberBet}; | ||
| 7 | 8 | ||
| 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 9 | pub enum Key { | 10 | pub enum Key { |
| @@ -33,6 +34,11 @@ pub enum Effect { | |||
| 33 | Print(Vec<String>), | 34 | Print(Vec<String>), |
| 34 | /// Replace the game board region in place. | 35 | /// Replace the game board region in place. |
| 35 | ReplaceRegion(Vec<String>), | 36 | ReplaceRegion(Vec<String>), |
| 37 | /// Run the roulette wheel animation, then draw the final board lines. | ||
| 38 | AnimateRoulette { | ||
| 39 | winning: u8, | ||
| 40 | final_lines: Vec<String>, | ||
| 41 | }, | ||
| 36 | /// Remove the game board region. | 42 | /// Remove the game board region. |
| 37 | ClearRegion, | 43 | ClearRegion, |
| 38 | /// Redraw the command prompt. `text` is the input without the `> ` prefix. | 44 | /// Redraw the command prompt. `text` is the input without the `> ` prefix. |
| @@ -54,11 +60,20 @@ pub enum Effect { | |||
| 54 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 60 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 55 | enum Pending { | 61 | enum Pending { |
| 56 | Spin, | 62 | Spin, |
| 63 | RouletteSpin, | ||
| 57 | Ping, | 64 | Ping, |
| 58 | Echo, | 65 | Echo, |
| 59 | Info, | 66 | Info, |
| 60 | } | 67 | } |
| 61 | 68 | ||
| 69 | #[derive(Debug, Clone, Copy)] | ||
| 70 | enum RouletteSide { | ||
| 71 | Red, | ||
| 72 | Black, | ||
| 73 | Even, | ||
| 74 | Odd, | ||
| 75 | } | ||
| 76 | |||
| 62 | pub struct ClientCore { | 77 | pub struct ClientCore { |
| 63 | input: String, | 78 | input: String, |
| 64 | history: Vec<String>, | 79 | history: Vec<String>, |
| @@ -160,6 +175,22 @@ impl ClientCore { | |||
| 160 | self.board_effects() | 175 | self.board_effects() |
| 161 | } | 176 | } |
| 162 | }, | 177 | }, |
| 178 | Some(Pending::RouletteSpin) => match parse_roulette_response(reply) { | ||
| 179 | Ok(result) => { | ||
| 180 | self.balance = Some(result.balance); | ||
| 181 | let winning = result.number; | ||
| 182 | self.game.apply_roulette_result(result); | ||
| 183 | let final_lines = self.board_lines().unwrap_or_default(); | ||
| 184 | vec![Effect::AnimateRoulette { | ||
| 185 | winning, | ||
| 186 | final_lines, | ||
| 187 | }] | ||
| 188 | } | ||
| 189 | Err(message) => { | ||
| 190 | self.game.set_error(message); | ||
| 191 | self.board_effects() | ||
| 192 | } | ||
| 193 | }, | ||
| 163 | Some(Pending::Ping) => { | 194 | Some(Pending::Ping) => { |
| 164 | if reply.trim() == "PONG" { | 195 | if reply.trim() == "PONG" { |
| 165 | print_line("Pong") | 196 | print_line("Pong") |
| @@ -194,6 +225,15 @@ impl ClientCore { | |||
| 194 | } | 225 | } |
| 195 | 226 | ||
| 196 | fn handle_game_key(&mut self, key: Key) -> Vec<Effect> { | 227 | fn handle_game_key(&mut self, key: Key) -> Vec<Effect> { |
| 228 | if self | ||
| 229 | .game | ||
| 230 | .get_current_state() | ||
| 231 | .map(|state| state.game_type.as_str()) | ||
| 232 | == Some("roulette") | ||
| 233 | { | ||
| 234 | return self.handle_roulette_key(key); | ||
| 235 | } | ||
| 236 | |||
| 197 | match key { | 237 | match key { |
| 198 | Key::CtrlC => vec![Effect::Quit], | 238 | Key::CtrlC => vec![Effect::Quit], |
| 199 | Key::Esc | Key::Char('q') => { | 239 | Key::Esc | Key::Char('q') => { |
| @@ -230,6 +270,147 @@ impl ClientCore { | |||
| 230 | effects | 270 | effects |
| 231 | } | 271 | } |
| 232 | 272 | ||
| 273 | fn handle_roulette_key(&mut self, key: Key) -> Vec<Effect> { | ||
| 274 | match key { | ||
| 275 | Key::CtrlC => vec![Effect::Quit], | ||
| 276 | Key::Char('q') => { | ||
| 277 | self.game.end_game(); | ||
| 278 | vec![ | ||
| 279 | Effect::ClearRegion, | ||
| 280 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 281 | ] | ||
| 282 | } | ||
| 283 | Key::Esc => { | ||
| 284 | self.game.set_roulette_number_input(""); | ||
| 285 | self.board_effects() | ||
| 286 | } | ||
| 287 | Key::Char(' ') => self.spin_roulette(), | ||
| 288 | Key::Char('r') => self.add_roulette_side(RouletteSide::Red), | ||
| 289 | Key::Char('b') => self.add_roulette_side(RouletteSide::Black), | ||
| 290 | Key::Char('e') => self.add_roulette_side(RouletteSide::Even), | ||
| 291 | Key::Char('o') => self.add_roulette_side(RouletteSide::Odd), | ||
| 292 | Key::Char('c') => { | ||
| 293 | self.game.set_roulette_bets(&Bets::default()); | ||
| 294 | self.game.set_roulette_number_input(""); | ||
| 295 | self.game.set_roulette_status("place"); | ||
| 296 | self.board_effects() | ||
| 297 | } | ||
| 298 | Key::Char('+') | Key::Char('=') => { | ||
| 299 | let chip = self.game.roulette_chip().saturating_add(10); | ||
| 300 | self.game.set_roulette_chip(chip); | ||
| 301 | self.board_effects() | ||
| 302 | } | ||
| 303 | Key::Char('-') => { | ||
| 304 | let chip = self.game.roulette_chip().saturating_sub(10).max(10); | ||
| 305 | self.game.set_roulette_chip(chip); | ||
| 306 | self.board_effects() | ||
| 307 | } | ||
| 308 | Key::Char(c) if c.is_ascii_digit() => { | ||
| 309 | let mut input = self.roulette_input(); | ||
| 310 | if input.len() < 2 { | ||
| 311 | input.push(c); | ||
| 312 | self.game.set_roulette_number_input(&input); | ||
| 313 | } | ||
| 314 | self.board_effects() | ||
| 315 | } | ||
| 316 | Key::Backspace => { | ||
| 317 | let mut input = self.roulette_input(); | ||
| 318 | input.pop(); | ||
| 319 | self.game.set_roulette_number_input(&input); | ||
| 320 | self.board_effects() | ||
| 321 | } | ||
| 322 | Key::Enter => { | ||
| 323 | let input = self.roulette_input(); | ||
| 324 | if !input.is_empty() { | ||
| 325 | match input.parse::<u8>() { | ||
| 326 | Ok(number) if number <= 36 => { | ||
| 327 | self.add_roulette_number(number); | ||
| 328 | self.game.set_roulette_number_input(""); | ||
| 329 | } | ||
| 330 | _ => { | ||
| 331 | self.game.set_roulette_number_input(""); | ||
| 332 | self.game.set_error("Number must be 0-36"); | ||
| 333 | } | ||
| 334 | } | ||
| 335 | } | ||
| 336 | self.board_effects() | ||
| 337 | } | ||
| 338 | _ => Vec::new(), | ||
| 339 | } | ||
| 340 | } | ||
| 341 | |||
| 342 | fn add_roulette_side(&mut self, side: RouletteSide) -> Vec<Effect> { | ||
| 343 | let chip = self.game.roulette_chip(); | ||
| 344 | let mut bets = self.game.roulette_bets(); | ||
| 345 | match side { | ||
| 346 | RouletteSide::Red => bets.red = bets.red.saturating_add(chip), | ||
| 347 | RouletteSide::Black => bets.black = bets.black.saturating_add(chip), | ||
| 348 | RouletteSide::Even => bets.even = bets.even.saturating_add(chip), | ||
| 349 | RouletteSide::Odd => bets.odd = bets.odd.saturating_add(chip), | ||
| 350 | } | ||
| 351 | self.place_roulette_bets(bets) | ||
| 352 | } | ||
| 353 | |||
| 354 | fn add_roulette_number(&mut self, number: u8) -> Vec<Effect> { | ||
| 355 | let chip = self.game.roulette_chip(); | ||
| 356 | let mut bets = self.game.roulette_bets(); | ||
| 357 | if let Some(existing) = bets.numbers.iter_mut().find(|bet| bet.number == number) { | ||
| 358 | existing.amount = existing.amount.saturating_add(chip); | ||
| 359 | } else { | ||
| 360 | bets.numbers.push(NumberBet { | ||
| 361 | number, | ||
| 362 | amount: chip, | ||
| 363 | }); | ||
| 364 | } | ||
| 365 | self.place_roulette_bets(bets) | ||
| 366 | } | ||
| 367 | |||
| 368 | fn place_roulette_bets(&mut self, bets: Bets) -> Vec<Effect> { | ||
| 369 | if let Some(balance) = self.balance { | ||
| 370 | if bets.total() > balance.max(0) as u64 { | ||
| 371 | self.game.set_error("Insufficient balance"); | ||
| 372 | return self.board_effects(); | ||
| 373 | } | ||
| 374 | } | ||
| 375 | |||
| 376 | self.game.set_roulette_bets(&bets); | ||
| 377 | self.game.set_roulette_status("place"); | ||
| 378 | self.board_effects() | ||
| 379 | } | ||
| 380 | |||
| 381 | fn spin_roulette(&mut self) -> Vec<Effect> { | ||
| 382 | let bets = self.game.roulette_bets(); | ||
| 383 | if bets.is_empty() { | ||
| 384 | self.game.set_error("Place at least one bet"); | ||
| 385 | return self.board_effects(); | ||
| 386 | } | ||
| 387 | if let Some(balance) = self.balance { | ||
| 388 | if bets.total() > balance.max(0) as u64 { | ||
| 389 | self.game.set_error("Insufficient balance"); | ||
| 390 | return self.board_effects(); | ||
| 391 | } | ||
| 392 | } | ||
| 393 | let Some(request) = self.game.spin_request() else { | ||
| 394 | return Vec::new(); | ||
| 395 | }; | ||
| 396 | if self.game.begin_spin().is_err() { | ||
| 397 | return Vec::new(); | ||
| 398 | } | ||
| 399 | self.pending = Some(Pending::RouletteSpin); | ||
| 400 | |||
| 401 | let mut effects = self.board_effects(); | ||
| 402 | effects.push(Effect::SendLine(request)); | ||
| 403 | effects | ||
| 404 | } | ||
| 405 | |||
| 406 | fn roulette_input(&self) -> String { | ||
| 407 | self.game | ||
| 408 | .get_current_state() | ||
| 409 | .and_then(|state| state.data.get("number_input")) | ||
| 410 | .cloned() | ||
| 411 | .unwrap_or_default() | ||
| 412 | } | ||
| 413 | |||
| 233 | fn submit(&mut self) -> Vec<Effect> { | 414 | fn submit(&mut self) -> Vec<Effect> { |
| 234 | let line = self.input.trim().to_string(); | 415 | let line = self.input.trim().to_string(); |
| 235 | self.input.clear(); | 416 | self.input.clear(); |
