summaryrefslogtreecommitdiffstats
path: root/src/client/core.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/client/core.rs251
1 files changed, 57 insertions, 194 deletions
diff --git a/src/client/core.rs b/src/client/core.rs
index 280eb77..21c1cb2 100644
--- a/src/client/core.rs
+++ b/src/client/core.rs
@@ -6,20 +6,13 @@ use crate::client::games::response::{
6use crate::client::view::{ 6use crate::client::view::{
7 BlackjackView, Command, CrashView, HistoryView, LobbyView, RouletteView, SlotsView, View, 7 BlackjackView, Command, CrashView, HistoryView, LobbyView, RouletteView, SlotsView, View,
8}; 8};
9use crate::client::ui::{help_lines, history_lines, parse_bet_arg}; 9use crate::client::ui::{game_lines, history_lines};
10use crate::crash::{CrashState, multiplier_at}; 10use crate::crash::{CrashState, multiplier_at};
11use crate::history::{PlayRecord, MAX_HISTORY}; 11use crate::history::{PlayRecord, MAX_HISTORY};
12use crate::roulette::{BetTarget, Bets}; 12use crate::roulette::{BetTarget, Bets};
13use std::time::{Duration, Instant}; 13use std::time::{Duration, Instant};
14 14
15 15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum InteractiveCommand {
18 Login,
19 Register,
20 SetCred,
21 Connect(Option<String>),
22}
23 16
24/// High-level actions produced by the client core. The terminal adapter is 17/// High-level actions produced by the client core. The terminal adapter is
25/// responsible for turning these into crossterm calls and network writes. 18/// responsible for turning these into crossterm calls and network writes.
@@ -29,20 +22,12 @@ pub enum Effect {
29 Print(Vec<String>), 22 Print(Vec<String>),
30 /// Draw the current view in the board region. 23 /// Draw the current view in the board region.
31 Render(View), 24 Render(View),
32 /// Run the roulette wheel animation, then draw the final view.
33 AnimateRoulette { winning: u8, view: View },
34 /// Remove the game board region. 25 /// Remove the game board region.
35 ClearRegion, 26 ClearRegion,
36 /// Send one protocol line and feed the reply to `handle_reply`. 27 /// Send one protocol line and feed the reply to `handle_reply`.
37 SendLine(String), 28 SendLine(String),
38 /// Delegate an interactive prompt flow to the terminal adapter.
39 Interactive(InteractiveCommand),
40 /// Drop the connection. 29 /// Drop the connection.
41 Disconnect, 30 Disconnect,
42 /// Let the adapter read the host database.
43 ListHosts,
44 /// Let the adapter delete a host from the database.
45 ForgetHost(String),
46 /// Leave the program. 31 /// Leave the program.
47 Quit, 32 Quit,
48} 33}
@@ -175,12 +160,8 @@ impl ClientCore {
175 Some(Pending::RouletteSpin) => match parse_roulette_response(reply) { 160 Some(Pending::RouletteSpin) => match parse_roulette_response(reply) {
176 Ok(result) => { 161 Ok(result) => {
177 self.balance = Some(result.balance); 162 self.balance = Some(result.balance);
178 let winning = result.number;
179 self.game.apply_roulette_result(result); 163 self.game.apply_roulette_result(result);
180 let Some(view) = self.view() else { 164 self.board_effects()
181 return Vec::new();
182 };
183 vec![Effect::AnimateRoulette { winning, view }]
184 } 165 }
185 Err(message) => { 166 Err(message) => {
186 self.game.set_error(message); 167 self.game.set_error(message);
@@ -331,22 +312,41 @@ impl ClientCore {
331 312
332 match command { 313 match command {
333 Command::Login { user, credential } => { 314 Command::Login { user, credential } => {
334 if !self.connected { 315 if !self.connected {
335 return print_line("Not connected. Use 'connect <ip>:<port>' first."); 316 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
336 } 317 }
337 self.login_error = None; 318 self.login_error = None;
338 self.pending = Some(Pending::Login { user: user.clone() }); 319 self.pending = Some(Pending::Login { user: user.clone() });
339 vec![Effect::SendLine(format!("AUTH {user} {credential}"))] 320 vec![Effect::SendLine(format!("AUTH {user} {credential}"))]
340 } 321 }
341 Command::Register { user, credential } => { 322 Command::Register { user, credential } => {
342 if !self.connected { 323 if !self.connected {
343 return print_line("Not connected. Use 'connect <ip>:<port>' first."); 324 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
344 } 325 }
345 self.login_error = None; 326 self.login_error = None;
346 self.pending = Some(Pending::Register); 327 self.pending = Some(Pending::Register);
347 vec![Effect::SendLine(format!("REGISTER {user} {credential}"))] 328 vec![Effect::SendLine(format!("REGISTER {user} {credential}"))]
348 } 329 }
349 Command::Play { game, bet } => self.play_game(&game, bet), 330 Command::Games => vec![Effect::Print(game_lines(&self.games()))],
331 Command::Ping => {
332 if !self.connected {
333 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
334 }
335 self.pending = Some(Pending::Ping);
336 vec![Effect::SendLine("PING".to_string())]
337 }
338 Command::Echo(message) => {
339 if !self.connected {
340 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
341 }
342 if message.trim().is_empty() {
343 return print_line("Usage: send <message>");
344 }
345 self.pending = Some(Pending::Echo);
346 vec![Effect::SendLine(message)]
347 }
348 Command::Info => self.info_effects(),
349 Command::Play { game, bet } => self.play_game(&game, bet),
350 Command::History { limit } => self.load_history(limit), 350 Command::History { limit } => self.load_history(limit),
351 Command::ShowLobby => { 351 Command::ShowLobby => {
352 self.screen = Screen::Lobby; 352 self.screen = Screen::Lobby;
@@ -672,106 +672,6 @@ impl ClientCore {
672 vec![Effect::SendLine(format!("CRASH_CASHOUT {session}"))] 672 vec![Effect::SendLine(format!("CRASH_CASHOUT {session}"))]
673 } 673 }
674 674
675 /// Run one command line. The interactive frontend owns the buffer and the
676 /// history; the core only sees the finished line.
677 pub fn submit_line(&mut self, line: &str) -> Vec<Effect> {
678 let line = line.trim().to_string();
679 if line.is_empty() {
680 return Vec::new();
681 }
682
683 let parts: Vec<&str> = line.splitn(3, ' ').collect();
684 match parts[0] {
685 "play" => self.cmd_play(&parts),
686 "games" => self.available_games(None),
687 "help" => vec![Effect::Print(help_lines())],
688 "connect" => {
689 if self.connected {
690 print_line("Already connected. Disconnect first.")
691 } else {
692 let target = parts.get(1).map(|value| value.to_string());
693 vec![Effect::Interactive(InteractiveCommand::Connect(target))]
694 }
695 }
696 "login" => {
697 if !self.connected {
698 print_line("Not connected. Use 'connect <ip>:<port>' first.")
699 } else if self.logged_in.is_some() {
700 print_line(format!(
701 "Already logged in as {}",
702 self.logged_in.as_deref().unwrap_or("?")
703 ))
704 } else {
705 vec![Effect::Interactive(InteractiveCommand::Login)]
706 }
707 }
708 "register" => {
709 if !self.connected {
710 print_line("Not connected. Use 'connect <ip>:<port>' first.")
711 } else {
712 vec![Effect::Interactive(InteractiveCommand::Register)]
713 }
714 }
715 "setcred" => {
716 if self.host.is_none() {
717 print_line("No host connected. Use 'connect <ip>:<port>' first.")
718 } else {
719 vec![Effect::Interactive(InteractiveCommand::SetCred)]
720 }
721 }
722 "logout" => {
723 self.logged_in = None;
724 print_line("Logged out")
725 }
726 "disconnect" => {
727 self.connected = false;
728 self.logged_in = None;
729 self.pending = None;
730 self.crash_started = None;
731 self.game.end_game();
732 vec![
733 Effect::ClearRegion,
734 Effect::Disconnect,
735 Effect::Print(vec!["Disconnected".to_string()]),
736 ]
737 }
738 "ping" => {
739 if !self.connected {
740 print_line("Not connected. Use 'connect <ip>:<port>' first.")
741 } else {
742 self.pending = Some(Pending::Ping);
743 vec![Effect::SendLine("PING".to_string())]
744 }
745 }
746 "send" => {
747 if !self.connected {
748 print_line("Not connected. Use 'connect <ip>:<port>' first.")
749 } else if parts.len() < 2 {
750 print_line("Usage: send <message>")
751 } else {
752 let message = parts[1..].join(" ");
753 self.pending = Some(Pending::Echo);
754 vec![Effect::SendLine(message)]
755 }
756 }
757 "info" => self.cmd_info(),
758 "history" => self.cmd_history(&parts),
759 "hosts" => vec![Effect::ListHosts],
760 "forget-host" => {
761 let target = parts
762 .get(1)
763 .map(|value| value.to_string())
764 .or_else(|| self.host.clone());
765 match target {
766 Some(target) => vec![Effect::ForgetHost(target)],
767 None => print_line("Usage: forget-host <ip>:<port> (or connect first)"),
768 }
769 }
770 "quit" => vec![Effect::Quit],
771 _ => vec![Effect::Print(help_lines())],
772 }
773 }
774
775 fn play_game(&mut self, game_type: &str, bet: Option<u64>) -> Vec<Effect> { 675 fn play_game(&mut self, game_type: &str, bet: Option<u64>) -> Vec<Effect> {
776 if !self.connected { 676 if !self.connected {
777 return print_line("Not connected. Use 'connect <ip>:<port>' first."); 677 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
@@ -822,56 +722,7 @@ impl ClientCore {
822 vec![Effect::SendLine(request)] 722 vec![Effect::SendLine(request)]
823 } 723 }
824 724
825 fn cmd_play(&mut self, parts: &[&str]) -> Vec<Effect> { 725 fn info_effects(&mut self) -> Vec<Effect> {
826 if !self.connected {
827 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
828 }
829 if self.logged_in.is_none() {
830 return print_line("Not logged in. Use 'login' first.");
831 }
832 if parts.len() < 2 {
833 return self.available_games(Some("Usage: play <game> [bet]"));
834 }
835
836 let bet = match parts.get(2) {
837 Some(raw) => match parse_bet_arg(raw) {
838 Ok(bet) => Some(bet),
839 Err(message) => return print_line(message),
840 },
841 None => None,
842 };
843
844 self.command(Command::Play {
845 game: parts[1].to_string(),
846 bet,
847 })
848 }
849
850 fn available_games(&self, usage: Option<&str>) -> Vec<Effect> {
851 let mut lines = vec!["Available games:".to_string()];
852 for (game_type, name) in self.game.get_available_games() {
853 lines.push(format!(" - {} ({})", game_type, name));
854 }
855 if let Some(usage) = usage {
856 lines.push(String::new());
857 lines.push(usage.to_string());
858 }
859 vec![Effect::Print(lines)]
860 }
861
862 fn cmd_history(&mut self, parts: &[&str]) -> Vec<Effect> {
863 let limit = match parts.get(1) {
864 Some(raw) => match raw.parse::<u32>() {
865 Ok(count) => count,
866 Err(_) => return print_line(format!("Usage: history [1-{MAX_HISTORY}]")),
867 },
868 None => crate::history::DEFAULT_HISTORY,
869 };
870
871 self.command(Command::History { limit })
872 }
873
874 fn cmd_info(&mut self) -> Vec<Effect> {
875 if !self.connected { 726 if !self.connected {
876 return print_line("Not connected to any host."); 727 return print_line("Not connected to any host.");
877 } 728 }
@@ -895,7 +746,24 @@ impl ClientCore {
895 effects 746 effects
896 } 747 }
897 748
898 /// Snapshot of the current screen for a frontend to draw. 749 /// The games a frontend can offer.
750 pub fn games(&self) -> Vec<(String, String)> {
751 self.game
752 .get_available_games()
753 .into_iter()
754 .map(|(game, name)| (game.to_string(), name.to_string()))
755 .collect()
756 }
757
758 /// Reset connection-dependent state after the adapter closed the socket.
759 ///
760 /// The transport belongs to the frontend, so it tells the core when the
761 /// link is gone. What to say about it is the frontend's business.
762 pub fn reset_after_disconnect(&mut self) {
763 self.set_connected(false);
764 self.game.end_game();
765 }
766
899 pub fn view(&self) -> Option<View> { 767 pub fn view(&self) -> Option<View> {
900 if let Some(state) = self.game.get_current_state() { 768 if let Some(state) = self.game.get_current_state() {
901 return Some(match state.game_type.as_str() { 769 return Some(match state.game_type.as_str() {
@@ -915,12 +783,7 @@ impl ClientCore {
915 connected: self.connected, 783 connected: self.connected,
916 user: self.logged_in.clone(), 784 user: self.logged_in.clone(),
917 balance: self.balance, 785 balance: self.balance,
918 games: self 786 games: self.games(),
919 .game
920 .get_available_games()
921 .into_iter()
922 .map(|(game, name)| (game.to_string(), name.to_string()))
923 .collect(),
924 login_error: self.login_error.clone(), 787 login_error: self.login_error.clone(),
925 }), 788 }),
926 }) 789 })