summaryrefslogtreecommitdiffstats
path: root/src/client/core.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/core.rs')
-rw-r--r--src/client/core.rs208
1 files changed, 6 insertions, 202 deletions
diff --git a/src/client/core.rs b/src/client/core.rs
index 98f109f..280eb77 100644
--- a/src/client/core.rs
+++ b/src/client/core.rs
@@ -3,32 +3,15 @@ use crate::client::games::response::{
3 parse_blackjack_response, parse_crash_response, parse_history_response, 3 parse_blackjack_response, parse_crash_response, parse_history_response,
4 parse_roulette_response, parse_spin_response, 4 parse_roulette_response, parse_spin_response,
5}; 5};
6use crate::client::games::roulette::Direction;
7use crate::client::view::{ 6use crate::client::view::{
8 BlackjackView, Command, CrashView, HistoryView, LobbyView, RouletteView, SlotsView, View, 7 BlackjackView, Command, CrashView, HistoryView, LobbyView, RouletteView, SlotsView, View,
9}; 8};
10use crate::client::ui::{ 9use crate::client::ui::{help_lines, history_lines, parse_bet_arg};
11 command_names, completion_lines, display_width, help_lines, history_lines,
12 input_cursor_column, parse_bet_arg, record_history,
13};
14use crate::crash::{CrashState, multiplier_at}; 10use crate::crash::{CrashState, multiplier_at};
15use crate::history::{PlayRecord, MAX_HISTORY}; 11use crate::history::{PlayRecord, MAX_HISTORY};
16use crate::roulette::{BetTarget, Bets}; 12use crate::roulette::{BetTarget, Bets};
17use std::time::{Duration, Instant}; 13use std::time::{Duration, Instant};
18 14
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Key {
21 Char(char),
22 Enter,
23 Backspace,
24 Up,
25 Down,
26 Left,
27 Right,
28 Tab,
29 Esc,
30 CtrlC,
31}
32 15
33#[derive(Debug, Clone, PartialEq, Eq)] 16#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum InteractiveCommand { 17pub enum InteractiveCommand {
@@ -50,8 +33,6 @@ pub enum Effect {
50 AnimateRoulette { winning: u8, view: View }, 33 AnimateRoulette { winning: u8, view: View },
51 /// Remove the game board region. 34 /// Remove the game board region.
52 ClearRegion, 35 ClearRegion,
53 /// Redraw the command prompt. `text` is the input without the `> ` prefix.
54 SetPrompt { text: String, cursor_col: u16 },
55 /// Send one protocol line and feed the reply to `handle_reply`. 36 /// Send one protocol line and feed the reply to `handle_reply`.
56 SendLine(String), 37 SendLine(String),
57 /// Delegate an interactive prompt flow to the terminal adapter. 38 /// Delegate an interactive prompt flow to the terminal adapter.
@@ -91,9 +72,6 @@ enum Screen {
91} 72}
92 73
93pub struct ClientCore { 74pub struct ClientCore {
94 input: String,
95 history: Vec<String>,
96 history_index: usize,
97 game: GameManager, 75 game: GameManager,
98 connected: bool, 76 connected: bool,
99 logged_in: Option<String>, 77 logged_in: Option<String>,
@@ -114,9 +92,6 @@ pub struct ClientCore {
114impl ClientCore { 92impl ClientCore {
115 pub fn new() -> Self { 93 pub fn new() -> Self {
116 Self { 94 Self {
117 input: String::new(),
118 history: Vec::new(),
119 history_index: 0,
120 game: GameManager::new(), 95 game: GameManager::new(),
121 connected: false, 96 connected: false,
122 logged_in: None, 97 logged_in: None,
@@ -184,40 +159,6 @@ impl ClientCore {
184 self.board_effects() 159 self.board_effects()
185 } 160 }
186 161
187 pub fn prompt_effect(&self) -> Option<Effect> {
188 if self.is_in_game() {
189 return None;
190 }
191 Some(Effect::SetPrompt {
192 text: self.input.clone(),
193 cursor_col: input_cursor_column(2, display_width(&self.input)),
194 })
195 }
196
197 pub fn handle_key(&mut self, key: Key) -> Vec<Effect> {
198 if self.game.is_in_game() {
199 return self.handle_game_key(key);
200 }
201
202 match key {
203 Key::CtrlC => vec![Effect::Quit],
204 Key::Enter => self.submit(),
205 Key::Backspace => {
206 self.input.pop();
207 self.prompt_effects()
208 }
209 Key::Char(c) => {
210 self.input.push(c);
211 self.prompt_effects()
212 }
213 Key::Up => self.history_previous(),
214 Key::Down => self.history_next(),
215 Key::Left | Key::Right => Vec::new(),
216 Key::Tab => self.complete(),
217 Key::Esc => Vec::new(),
218 }
219 }
220
221 pub fn handle_reply(&mut self, reply: &str) -> Vec<Effect> { 162 pub fn handle_reply(&mut self, reply: &str) -> Vec<Effect> {
222 match self.pending.take() { 163 match self.pending.take() {
223 Some(Pending::Spin) => match parse_spin_response(reply) { 164 Some(Pending::Spin) => match parse_spin_response(reply) {
@@ -370,31 +311,6 @@ impl ClientCore {
370 } 311 }
371 } 312 }
372 313
373 fn handle_game_key(&mut self, key: Key) -> Vec<Effect> {
374 let game_type = self
375 .game
376 .get_current_state()
377 .map(|state| state.game_type.as_str());
378 if game_type == Some("roulette") {
379 return self.handle_roulette_key(key);
380 }
381 if game_type == Some("blackjack") {
382 return self.handle_blackjack_key(key);
383 }
384 if game_type == Some("crash") {
385 return self.handle_crash_key(key);
386 }
387
388 match key {
389 Key::CtrlC => vec![Effect::Quit],
390 Key::Esc | Key::Char('q') => self.command(Command::Leave),
391 Key::Char(' ') => self.command(Command::Spin),
392 Key::Char('+') | Key::Char('=') => self.command(Command::AdjustBet(10)),
393 Key::Char('-') => self.command(Command::AdjustBet(-10)),
394 _ => Vec::new(),
395 }
396 }
397
398 fn spin(&mut self) -> Vec<Effect> { 314 fn spin(&mut self) -> Vec<Effect> {
399 let Some(request) = self.game.spin_request() else { 315 let Some(request) = self.game.spin_request() else {
400 return Vec::new(); 316 return Vec::new();
@@ -409,23 +325,6 @@ impl ClientCore {
409 effects 325 effects
410 } 326 }
411 327
412 fn handle_roulette_key(&mut self, key: Key) -> Vec<Effect> {
413 match key {
414 Key::CtrlC => vec![Effect::Quit],
415 Key::Up => self.command(Command::MoveCursor(Direction::Up)),
416 Key::Down => self.command(Command::MoveCursor(Direction::Down)),
417 Key::Left => self.command(Command::MoveCursor(Direction::Left)),
418 Key::Right => self.command(Command::MoveCursor(Direction::Right)),
419 Key::Enter => self.command(Command::PlaceBet),
420 Key::Char(' ') => self.command(Command::Spin),
421 Key::Char('c') => self.command(Command::ClearBets),
422 Key::Char('+') | Key::Char('=') => self.command(Command::AdjustChip(10)),
423 Key::Char('-') => self.command(Command::AdjustChip(-10)),
424 Key::Char('q') => self.command(Command::Leave),
425 _ => Vec::new(),
426 }
427 }
428
429 /// Apply a semantic command. Both frontends drive the core through this. 328 /// Apply a semantic command. Both frontends drive the core through this.
430 pub fn command(&mut self, command: Command) -> Vec<Effect> { 329 pub fn command(&mut self, command: Command) -> Vec<Effect> {
431 let game = self.current_game_type().map(str::to_string); 330 let game = self.current_game_type().map(str::to_string);
@@ -523,7 +422,7 @@ impl ClientCore {
523 } 422 }
524 } 423 }
525 424
526 fn current_game_type(&self) -> Option<&str> { 425 pub fn current_game_type(&self) -> Option<&str> {
527 self.game 426 self.game
528 .get_current_state() 427 .get_current_state()
529 .map(|state| state.game_type.as_str()) 428 .map(|state| state.game_type.as_str())
@@ -628,36 +527,6 @@ impl ClientCore {
628 effects 527 effects
629 } 528 }
630 529
631 fn handle_blackjack_key(&mut self, key: Key) -> Vec<Effect> {
632 let status = self
633 .game
634 .get_current_state()
635 .and_then(|state| state.data.get("status"))
636 .cloned()
637 .unwrap_or_else(|| "ready".to_string());
638 let hand = self.game.blackjack_state();
639 let player = hand.as_ref().is_some_and(|hand| hand.phase == "player");
640 let can_double = hand.as_ref().is_some_and(|hand| {
641 hand.phase == "player" && hand.actions.iter().any(|action| action == "double")
642 });
643
644 match key {
645 Key::CtrlC => vec![Effect::Quit],
646 Key::Char('q') => self.command(Command::Leave),
647 Key::Char(' ') => self.command(Command::Deal),
648 Key::Char('h') if player => self.command(Command::Hit),
649 Key::Char('s') if player => self.command(Command::Stand),
650 Key::Char('d') if can_double => self.command(Command::Double),
651 Key::Char('+') | Key::Char('=') if matches!(status.as_str(), "ready" | "done") => {
652 self.command(Command::AdjustBet(10))
653 }
654 Key::Char('-') if matches!(status.as_str(), "ready" | "done") => {
655 self.command(Command::AdjustBet(-10))
656 }
657 _ => Vec::new(),
658 }
659 }
660
661 fn start_blackjack(&mut self) -> Vec<Effect> { 530 fn start_blackjack(&mut self) -> Vec<Effect> {
662 let status = self 531 let status = self
663 .game 532 .game
@@ -768,25 +637,6 @@ impl ClientCore {
768 self.board_effects() 637 self.board_effects()
769 } 638 }
770 639
771 fn handle_crash_key(&mut self, key: Key) -> Vec<Effect> {
772 let status = self.game.crash_status();
773 let running = status == "running";
774
775 match key {
776 Key::CtrlC => vec![Effect::Quit],
777 Key::Char('q') => self.command(Command::Leave),
778 Key::Char(' ') if !running && self.pending.is_none() => self.command(Command::Start),
779 Key::Char('c') if running && self.pending.is_none() => {
780 self.command(Command::CashOut)
781 }
782 Key::Char('+') | Key::Char('=') if !running => {
783 self.command(Command::AdjustBet(10))
784 }
785 Key::Char('-') if !running => self.command(Command::AdjustBet(-10)),
786 _ => Vec::new(),
787 }
788 }
789
790 fn start_crash(&mut self) -> Vec<Effect> { 640 fn start_crash(&mut self) -> Vec<Effect> {
791 let status = self.game.crash_status(); 641 let status = self.game.crash_status();
792 if !matches!(status.as_str(), "ready" | "cashed" | "crashed" | "forfeit") { 642 if !matches!(status.as_str(), "ready" | "cashed" | "crashed" | "forfeit") {
@@ -822,16 +672,14 @@ impl ClientCore {
822 vec![Effect::SendLine(format!("CRASH_CASHOUT {session}"))] 672 vec![Effect::SendLine(format!("CRASH_CASHOUT {session}"))]
823 } 673 }
824 674
825 fn submit(&mut self) -> Vec<Effect> { 675 /// Run one command line. The interactive frontend owns the buffer and the
826 let line = self.input.trim().to_string(); 676 /// history; the core only sees the finished line.
827 self.input.clear(); 677 pub fn submit_line(&mut self, line: &str) -> Vec<Effect> {
678 let line = line.trim().to_string();
828 if line.is_empty() { 679 if line.is_empty() {
829 return Vec::new(); 680 return Vec::new();
830 } 681 }
831 682
832 record_history(&mut self.history, &line);
833 self.history_index = self.history.len();
834
835 let parts: Vec<&str> = line.splitn(3, ' ').collect(); 683 let parts: Vec<&str> = line.splitn(3, ' ').collect();
836 match parts[0] { 684 match parts[0] {
837 "play" => self.cmd_play(&parts), 685 "play" => self.cmd_play(&parts),
@@ -1047,47 +895,6 @@ impl ClientCore {
1047 effects 895 effects
1048 } 896 }
1049 897
1050 fn history_previous(&mut self) -> Vec<Effect> {
1051 if self.history_index == 0 {
1052 return Vec::new();
1053 }
1054 self.history_index -= 1;
1055 self.input = self.history[self.history_index].clone();
1056 self.prompt_effects()
1057 }
1058
1059 fn history_next(&mut self) -> Vec<Effect> {
1060 if self.history_index >= self.history.len() {
1061 return Vec::new();
1062 }
1063 self.history_index += 1;
1064 self.input = if self.history_index == self.history.len() {
1065 String::new()
1066 } else {
1067 self.history[self.history_index].clone()
1068 };
1069 self.prompt_effects()
1070 }
1071
1072 fn complete(&mut self) -> Vec<Effect> {
1073 let prefix = self.input.trim();
1074 let matches: Vec<&'static str> = command_names()
1075 .into_iter()
1076 .filter(|command| command.starts_with(prefix))
1077 .collect();
1078
1079 let mut effects = Vec::new();
1080 if matches.len() == 1 {
1081 self.input = matches[0].to_string();
1082 } else if matches.len() > 1 {
1083 effects.push(Effect::Print(completion_lines(&matches, 76)));
1084 }
1085 if let Some(prompt) = self.prompt_effect() {
1086 effects.push(prompt);
1087 }
1088 effects
1089 }
1090
1091 /// Snapshot of the current screen for a frontend to draw. 898 /// Snapshot of the current screen for a frontend to draw.
1092 pub fn view(&self) -> Option<View> { 899 pub fn view(&self) -> Option<View> {
1093 if let Some(state) = self.game.get_current_state() { 900 if let Some(state) = self.game.get_current_state() {
@@ -1126,9 +933,6 @@ impl ClientCore {
1126 self.view().map(Effect::Render).into_iter().collect() 933 self.view().map(Effect::Render).into_iter().collect()
1127 } 934 }
1128 935
1129 fn prompt_effects(&self) -> Vec<Effect> {
1130 self.prompt_effect().into_iter().collect()
1131 }
1132} 936}
1133 937
1134fn auth_result(reply: &str) -> Result<(), String> { 938fn auth_result(reply: &str) -> Result<(), String> {