diff options
Diffstat (limited to '')
| -rw-r--r-- | src/client/core.rs | 232 |
1 files changed, 134 insertions, 98 deletions
diff --git a/src/client/core.rs b/src/client/core.rs index 7e43fb7..3e3126a 100644 --- a/src/client/core.rs +++ b/src/client/core.rs | |||
| @@ -4,7 +4,7 @@ use crate::client::games::response::{ | |||
| 4 | parse_roulette_response, parse_spin_response, | 4 | parse_roulette_response, parse_spin_response, |
| 5 | }; | 5 | }; |
| 6 | use crate::client::games::roulette::Direction; | 6 | use crate::client::games::roulette::Direction; |
| 7 | use crate::client::view::{Command, RouletteView, View}; | 7 | use crate::client::view::{BlackjackView, Command, CrashView, RouletteView, SlotsView, View}; |
| 8 | use crate::client::ui::{ | 8 | use crate::client::ui::{ |
| 9 | command_names, completion_lines, display_width, help_lines, history_lines, | 9 | command_names, completion_lines, display_width, help_lines, history_lines, |
| 10 | input_cursor_column, parse_bet_arg, record_history, | 10 | input_cursor_column, parse_bet_arg, record_history, |
| @@ -38,7 +38,7 @@ pub enum InteractiveCommand { | |||
| 38 | 38 | ||
| 39 | /// High-level actions produced by the client core. The terminal adapter is | 39 | /// High-level actions produced by the client core. The terminal adapter is |
| 40 | /// responsible for turning these into crossterm calls and network writes. | 40 | /// responsible for turning these into crossterm calls and network writes. |
| 41 | #[derive(Debug, Clone, PartialEq, Eq)] | 41 | #[derive(Debug, Clone, PartialEq)] |
| 42 | pub enum Effect { | 42 | pub enum Effect { |
| 43 | /// Append lines to the scrolling transcript. | 43 | /// Append lines to the scrolling transcript. |
| 44 | Print(Vec<String>), | 44 | Print(Vec<String>), |
| @@ -213,7 +213,9 @@ impl ClientCore { | |||
| 213 | self.balance = Some(result.balance); | 213 | self.balance = Some(result.balance); |
| 214 | let winning = result.number; | 214 | let winning = result.number; |
| 215 | self.game.apply_roulette_result(result); | 215 | self.game.apply_roulette_result(result); |
| 216 | let view = self.view(); | 216 | let Some(view) = self.view() else { |
| 217 | return Vec::new(); | ||
| 218 | }; | ||
| 217 | vec![Effect::AnimateRoulette { winning, view }] | 219 | vec![Effect::AnimateRoulette { winning, view }] |
| 218 | } | 220 | } |
| 219 | Err(message) => { | 221 | Err(message) => { |
| @@ -340,22 +342,10 @@ impl ClientCore { | |||
| 340 | 342 | ||
| 341 | match key { | 343 | match key { |
| 342 | Key::CtrlC => vec![Effect::Quit], | 344 | Key::CtrlC => vec![Effect::Quit], |
| 343 | Key::Esc | Key::Char('q') => { | 345 | Key::Esc | Key::Char('q') => self.command(Command::Leave), |
| 344 | self.game.end_game(); | 346 | Key::Char(' ') => self.command(Command::Spin), |
| 345 | vec![ | 347 | Key::Char('+') | Key::Char('=') => self.command(Command::AdjustBet(10)), |
| 346 | Effect::ClearRegion, | 348 | Key::Char('-') => self.command(Command::AdjustBet(-10)), |
| 347 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 348 | ] | ||
| 349 | } | ||
| 350 | Key::Char(' ') => self.spin(), | ||
| 351 | Key::Char('+') | Key::Char('=') => { | ||
| 352 | let _ = self.game.adjust_bet(10); | ||
| 353 | self.board_effects() | ||
| 354 | } | ||
| 355 | Key::Char('-') => { | ||
| 356 | let _ = self.game.adjust_bet(-10); | ||
| 357 | self.board_effects() | ||
| 358 | } | ||
| 359 | _ => Vec::new(), | 349 | _ => Vec::new(), |
| 360 | } | 350 | } |
| 361 | } | 351 | } |
| @@ -393,7 +383,46 @@ impl ClientCore { | |||
| 393 | 383 | ||
| 394 | /// Apply a semantic command. Both frontends drive the core through this. | 384 | /// Apply a semantic command. Both frontends drive the core through this. |
| 395 | pub fn command(&mut self, command: Command) -> Vec<Effect> { | 385 | pub fn command(&mut self, command: Command) -> Vec<Effect> { |
| 386 | let game = self.current_game_type().map(str::to_string); | ||
| 387 | |||
| 396 | match command { | 388 | match command { |
| 389 | Command::SetBet(bet) => { | ||
| 390 | if game.as_deref() == Some("roulette") { | ||
| 391 | return self.command(Command::SetChip(bet)); | ||
| 392 | } | ||
| 393 | if self.game.set_bet(bet).is_ok() { | ||
| 394 | self.board_effects() | ||
| 395 | } else { | ||
| 396 | Vec::new() | ||
| 397 | } | ||
| 398 | } | ||
| 399 | Command::AdjustBet(delta) => { | ||
| 400 | if game.as_deref() == Some("roulette") { | ||
| 401 | return self.command(Command::AdjustChip(delta)); | ||
| 402 | } | ||
| 403 | if self.game.adjust_bet(delta).is_ok() { | ||
| 404 | self.board_effects() | ||
| 405 | } else { | ||
| 406 | Vec::new() | ||
| 407 | } | ||
| 408 | } | ||
| 409 | Command::Leave => self.leave_current_game(), | ||
| 410 | Command::Spin => match game.as_deref() { | ||
| 411 | Some("slots") => self.spin(), | ||
| 412 | Some("roulette") => self.spin_roulette(), | ||
| 413 | _ => Vec::new(), | ||
| 414 | }, | ||
| 415 | Command::Deal => self.start_blackjack(), | ||
| 416 | Command::Hit => self.blackjack_action("hit"), | ||
| 417 | Command::Stand => self.blackjack_action("stand"), | ||
| 418 | Command::Double => self.blackjack_action("double"), | ||
| 419 | Command::Start => self.start_crash(), | ||
| 420 | Command::CashOut => self.cash_out_crash(), | ||
| 421 | Command::Forfeit => match game.as_deref() { | ||
| 422 | Some("blackjack") => self.forfeit_blackjack(), | ||
| 423 | Some("crash") => self.forfeit_crash(), | ||
| 424 | _ => Vec::new(), | ||
| 425 | }, | ||
| 397 | Command::MoveCursor(direction) => { | 426 | Command::MoveCursor(direction) => { |
| 398 | let cursor = self.game.roulette_cursor().moved(direction); | 427 | let cursor = self.game.roulette_cursor().moved(direction); |
| 399 | self.game.set_roulette_cursor(&cursor); | 428 | self.game.set_roulette_cursor(&cursor); |
| @@ -418,16 +447,67 @@ impl ClientCore { | |||
| 418 | self.game.set_roulette_chip(chip); | 447 | self.game.set_roulette_chip(chip); |
| 419 | self.board_effects() | 448 | self.board_effects() |
| 420 | } | 449 | } |
| 421 | Command::Spin => self.spin_roulette(), | 450 | } |
| 422 | Command::Leave => { | 451 | } |
| 423 | self.crash_started = None; | 452 | |
| 424 | self.game.end_game(); | 453 | fn current_game_type(&self) -> Option<&str> { |
| 425 | vec![ | 454 | self.game |
| 426 | Effect::ClearRegion, | 455 | .get_current_state() |
| 427 | Effect::Print(vec!["Game exited.".to_string()]), | 456 | .map(|state| state.game_type.as_str()) |
| 428 | ] | 457 | } |
| 458 | |||
| 459 | fn leave_current_game(&mut self) -> Vec<Effect> { | ||
| 460 | match self.current_game_type().map(str::to_string).as_deref() { | ||
| 461 | Some("blackjack") => { | ||
| 462 | if self | ||
| 463 | .game | ||
| 464 | .blackjack_state() | ||
| 465 | .is_some_and(|hand| hand.phase == "player") | ||
| 466 | { | ||
| 467 | return self.forfeit_blackjack(); | ||
| 468 | } | ||
| 429 | } | 469 | } |
| 470 | Some("crash") => { | ||
| 471 | if self.game.crash_status() == "running" { | ||
| 472 | return self.forfeit_crash(); | ||
| 473 | } | ||
| 474 | } | ||
| 475 | _ => {} | ||
| 430 | } | 476 | } |
| 477 | self.exit_game() | ||
| 478 | } | ||
| 479 | |||
| 480 | fn exit_game(&mut self) -> Vec<Effect> { | ||
| 481 | self.crash_started = None; | ||
| 482 | self.game.end_game(); | ||
| 483 | vec![ | ||
| 484 | Effect::ClearRegion, | ||
| 485 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 486 | ] | ||
| 487 | } | ||
| 488 | |||
| 489 | fn forfeit_blackjack(&mut self) -> Vec<Effect> { | ||
| 490 | let Some(hand) = self | ||
| 491 | .game | ||
| 492 | .blackjack_state() | ||
| 493 | .filter(|hand| hand.phase == "player") | ||
| 494 | else { | ||
| 495 | return self.exit_game(); | ||
| 496 | }; | ||
| 497 | self.game.set_blackjack_status("forfeiting"); | ||
| 498 | self.pending = Some(Pending::BlackjackForfeit); | ||
| 499 | vec![Effect::SendLine(format!( | ||
| 500 | "BLACKJACK_ACTION {} forfeit", | ||
| 501 | hand.session_id | ||
| 502 | ))] | ||
| 503 | } | ||
| 504 | |||
| 505 | fn forfeit_crash(&mut self) -> Vec<Effect> { | ||
| 506 | let Some(session) = self.game.crash_session_id() else { | ||
| 507 | return self.exit_game(); | ||
| 508 | }; | ||
| 509 | self.pending = Some(Pending::CrashForfeit); | ||
| 510 | vec![Effect::SendLine(format!("CRASH_FORFEIT {session}"))] | ||
| 431 | } | 511 | } |
| 432 | 512 | ||
| 433 | fn place_roulette_chip_on(&mut self, target: BetTarget) -> Vec<Effect> { | 513 | fn place_roulette_chip_on(&mut self, target: BetTarget) -> Vec<Effect> { |
| @@ -482,46 +562,23 @@ impl ClientCore { | |||
| 482 | .cloned() | 562 | .cloned() |
| 483 | .unwrap_or_else(|| "ready".to_string()); | 563 | .unwrap_or_else(|| "ready".to_string()); |
| 484 | let hand = self.game.blackjack_state(); | 564 | let hand = self.game.blackjack_state(); |
| 565 | let player = hand.as_ref().is_some_and(|hand| hand.phase == "player"); | ||
| 566 | let can_double = hand.as_ref().is_some_and(|hand| { | ||
| 567 | hand.phase == "player" && hand.actions.iter().any(|action| action == "double") | ||
| 568 | }); | ||
| 485 | 569 | ||
| 486 | match key { | 570 | match key { |
| 487 | Key::CtrlC => vec![Effect::Quit], | 571 | Key::CtrlC => vec![Effect::Quit], |
| 488 | Key::Char('q') => { | 572 | Key::Char('q') => self.command(Command::Leave), |
| 489 | if let Some(hand) = hand.filter(|hand| hand.phase == "player") { | 573 | Key::Char(' ') => self.command(Command::Deal), |
| 490 | self.game.set_blackjack_status("forfeiting"); | 574 | Key::Char('h') if player => self.command(Command::Hit), |
| 491 | self.pending = Some(Pending::BlackjackForfeit); | 575 | Key::Char('s') if player => self.command(Command::Stand), |
| 492 | vec![Effect::SendLine(format!( | 576 | Key::Char('d') if can_double => self.command(Command::Double), |
| 493 | "BLACKJACK_ACTION {} forfeit", | ||
| 494 | hand.session_id | ||
| 495 | ))] | ||
| 496 | } else { | ||
| 497 | self.game.end_game(); | ||
| 498 | vec![ | ||
| 499 | Effect::ClearRegion, | ||
| 500 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 501 | ] | ||
| 502 | } | ||
| 503 | } | ||
| 504 | Key::Char(' ') => self.start_blackjack(), | ||
| 505 | Key::Char('h') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => { | ||
| 506 | self.blackjack_action("hit") | ||
| 507 | } | ||
| 508 | Key::Char('s') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => { | ||
| 509 | self.blackjack_action("stand") | ||
| 510 | } | ||
| 511 | Key::Char('d') | ||
| 512 | if hand.as_ref().is_some_and(|hand| { | ||
| 513 | hand.phase == "player" && hand.actions.iter().any(|action| action == "double") | ||
| 514 | }) => | ||
| 515 | { | ||
| 516 | self.blackjack_action("double") | ||
| 517 | } | ||
| 518 | Key::Char('+') | Key::Char('=') if matches!(status.as_str(), "ready" | "done") => { | 577 | Key::Char('+') | Key::Char('=') if matches!(status.as_str(), "ready" | "done") => { |
| 519 | let _ = self.game.adjust_bet(10); | 578 | self.command(Command::AdjustBet(10)) |
| 520 | self.board_effects() | ||
| 521 | } | 579 | } |
| 522 | Key::Char('-') if matches!(status.as_str(), "ready" | "done") => { | 580 | Key::Char('-') if matches!(status.as_str(), "ready" | "done") => { |
| 523 | let _ = self.game.adjust_bet(-10); | 581 | self.command(Command::AdjustBet(-10)) |
| 524 | self.board_effects() | ||
| 525 | } | 582 | } |
| 526 | _ => Vec::new(), | 583 | _ => Vec::new(), |
| 527 | } | 584 | } |
| @@ -643,32 +700,15 @@ impl ClientCore { | |||
| 643 | 700 | ||
| 644 | match key { | 701 | match key { |
| 645 | Key::CtrlC => vec![Effect::Quit], | 702 | Key::CtrlC => vec![Effect::Quit], |
| 646 | Key::Char('q') => { | 703 | Key::Char('q') => self.command(Command::Leave), |
| 647 | if running { | 704 | Key::Char(' ') if !running && self.pending.is_none() => self.command(Command::Start), |
| 648 | let Some(session) = self.game.crash_session_id() else { | 705 | Key::Char('c') if running && self.pending.is_none() => { |
| 649 | return Vec::new(); | 706 | self.command(Command::CashOut) |
| 650 | }; | ||
| 651 | self.pending = Some(Pending::CrashForfeit); | ||
| 652 | vec![Effect::SendLine(format!("CRASH_FORFEIT {session}"))] | ||
| 653 | } else { | ||
| 654 | self.crash_started = None; | ||
| 655 | self.game.end_game(); | ||
| 656 | vec![ | ||
| 657 | Effect::ClearRegion, | ||
| 658 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 659 | ] | ||
| 660 | } | ||
| 661 | } | 707 | } |
| 662 | Key::Char(' ') if !running && self.pending.is_none() => self.start_crash(), | ||
| 663 | Key::Char('c') if running && self.pending.is_none() => self.cash_out_crash(), | ||
| 664 | Key::Char('+') | Key::Char('=') if !running => { | 708 | Key::Char('+') | Key::Char('=') if !running => { |
| 665 | let _ = self.game.adjust_bet(10); | 709 | self.command(Command::AdjustBet(10)) |
| 666 | self.board_effects() | ||
| 667 | } | ||
| 668 | Key::Char('-') if !running => { | ||
| 669 | let _ = self.game.adjust_bet(-10); | ||
| 670 | self.board_effects() | ||
| 671 | } | 710 | } |
| 711 | Key::Char('-') if !running => self.command(Command::AdjustBet(-10)), | ||
| 672 | _ => Vec::new(), | 712 | _ => Vec::new(), |
| 673 | } | 713 | } |
| 674 | } | 714 | } |
| @@ -948,24 +988,20 @@ impl ClientCore { | |||
| 948 | effects | 988 | effects |
| 949 | } | 989 | } |
| 950 | 990 | ||
| 951 | fn board_lines(&self) -> Option<Vec<String>> { | ||
| 952 | let game = self.game.get_current_game()?; | ||
| 953 | let state = self.game.get_current_state()?; | ||
| 954 | Some(game.render(state).lines().map(str::to_string).collect()) | ||
| 955 | } | ||
| 956 | |||
| 957 | /// Snapshot of the current screen for a frontend to draw. | 991 | /// Snapshot of the current screen for a frontend to draw. |
| 958 | pub fn view(&self) -> View { | 992 | pub fn view(&self) -> Option<View> { |
| 959 | if let Some(state) = self.game.get_current_state() { | 993 | let state = self.game.get_current_state()?; |
| 960 | if state.game_type == "roulette" { | 994 | Some(match state.game_type.as_str() { |
| 961 | return View::Roulette(RouletteView::from_state(state)); | 995 | "slots" => View::Slots(SlotsView::from_state(state)), |
| 962 | } | 996 | "roulette" => View::Roulette(RouletteView::from_state(state)), |
| 963 | } | 997 | "blackjack" => View::Blackjack(BlackjackView::from_state(state)), |
| 964 | View::Text(self.board_lines().unwrap_or_default()) | 998 | "crash" => View::Crash(CrashView::from_state(state)), |
| 999 | _ => return None, | ||
| 1000 | }) | ||
| 965 | } | 1001 | } |
| 966 | 1002 | ||
| 967 | fn board_effects(&self) -> Vec<Effect> { | 1003 | fn board_effects(&self) -> Vec<Effect> { |
| 968 | vec![Effect::Render(self.view())] | 1004 | self.view().map(Effect::Render).into_iter().collect() |
| 969 | } | 1005 | } |
| 970 | 1006 | ||
| 971 | fn prompt_effects(&self) -> Vec<Effect> { | 1007 | fn prompt_effects(&self) -> Vec<Effect> { |
