summaryrefslogtreecommitdiffstats
path: root/src/client/core.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/client/core.rs232
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};
6use crate::client::games::roulette::Direction; 6use crate::client::games::roulette::Direction;
7use crate::client::view::{Command, RouletteView, View}; 7use crate::client::view::{BlackjackView, Command, CrashView, RouletteView, SlotsView, View};
8use crate::client::ui::{ 8use 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)]
42pub enum Effect { 42pub 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> {