summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/client/core.rs151
-rw-r--r--src/client/tui.rs2
-rw-r--r--src/client/view.rs21
3 files changed, 137 insertions, 37 deletions
diff --git a/src/client/core.rs b/src/client/core.rs
index 3e3126a..37ba916 100644
--- a/src/client/core.rs
+++ b/src/client/core.rs
@@ -4,13 +4,15 @@ 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::{BlackjackView, Command, CrashView, RouletteView, SlotsView, View}; 7use crate::client::view::{
8 BlackjackView, Command, CrashView, HistoryView, LobbyView, RouletteView, SlotsView, View,
9};
8use crate::client::ui::{ 10use crate::client::ui::{
9 command_names, completion_lines, display_width, help_lines, history_lines, 11 command_names, completion_lines, display_width, help_lines, history_lines,
10 input_cursor_column, parse_bet_arg, record_history, 12 input_cursor_column, parse_bet_arg, record_history,
11}; 13};
12use crate::crash::{CrashState, multiplier_at}; 14use crate::crash::{CrashState, multiplier_at};
13use crate::history::MAX_HISTORY; 15use crate::history::{PlayRecord, MAX_HISTORY};
14use crate::roulette::{BetTarget, Bets}; 16use crate::roulette::{BetTarget, Bets};
15use std::time::{Duration, Instant}; 17use std::time::{Duration, Instant};
16 18
@@ -80,6 +82,12 @@ enum Pending {
80 Info, 82 Info,
81} 83}
82 84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86enum Screen {
87 Lobby,
88 History,
89}
90
83pub struct ClientCore { 91pub struct ClientCore {
84 input: String, 92 input: String,
85 history: Vec<String>, 93 history: Vec<String>,
@@ -93,6 +101,10 @@ pub struct ClientCore {
93 fingerprint: Option<String>, 101 fingerprint: Option<String>,
94 /// Local anchor for the crash multiplier, set when a round starts. 102 /// Local anchor for the crash multiplier, set when a round starts.
95 crash_started: Option<Instant>, 103 crash_started: Option<Instant>,
104 /// App screen shown when no game is open.
105 screen: Screen,
106 /// Last plays fetched with `history`.
107 play_history: Vec<PlayRecord>,
96} 108}
97 109
98impl ClientCore { 110impl ClientCore {
@@ -109,6 +121,8 @@ impl ClientCore {
109 host: None, 121 host: None,
110 fingerprint: None, 122 fingerprint: None,
111 crash_started: None, 123 crash_started: None,
124 screen: Screen::Lobby,
125 play_history: Vec::new(),
112 } 126 }
113 } 127 }
114 128
@@ -118,6 +132,7 @@ impl ClientCore {
118 self.logged_in = None; 132 self.logged_in = None;
119 self.pending = None; 133 self.pending = None;
120 self.crash_started = None; 134 self.crash_started = None;
135 self.screen = Screen::Lobby;
121 } 136 }
122 } 137 }
123 138
@@ -291,7 +306,10 @@ impl ClientCore {
291 } 306 }
292 }, 307 },
293 Some(Pending::History) => match parse_history_response(reply) { 308 Some(Pending::History) => match parse_history_response(reply) {
294 Ok(records) => vec![Effect::Print(history_lines(&records))], 309 Ok(records) => {
310 self.play_history = records.clone();
311 vec![Effect::Print(history_lines(&records))]
312 }
295 Err(message) => print_line(message), 313 Err(message) => print_line(message),
296 }, 314 },
297 Some(Pending::Ping) => { 315 Some(Pending::Ping) => {
@@ -386,6 +404,17 @@ impl ClientCore {
386 let game = self.current_game_type().map(str::to_string); 404 let game = self.current_game_type().map(str::to_string);
387 405
388 match command { 406 match command {
407 Command::Play { game, bet } => self.play_game(&game, bet),
408 Command::History { limit } => self.load_history(limit),
409 Command::ShowLobby => {
410 self.screen = Screen::Lobby;
411 Vec::new()
412 }
413 Command::Logout => {
414 self.logged_in = None;
415 self.screen = Screen::Lobby;
416 print_line("Logged out")
417 }
389 Command::SetBet(bet) => { 418 Command::SetBet(bet) => {
390 if game.as_deref() == Some("roulette") { 419 if game.as_deref() == Some("roulette") {
391 return self.command(Command::SetChip(bet)); 420 return self.command(Command::SetChip(bet));
@@ -479,6 +508,7 @@ impl ClientCore {
479 508
480 fn exit_game(&mut self) -> Vec<Effect> { 509 fn exit_game(&mut self) -> Vec<Effect> {
481 self.crash_started = None; 510 self.crash_started = None;
511 self.screen = Screen::Lobby;
482 self.game.end_game(); 512 self.game.end_game();
483 vec![ 513 vec![
484 Effect::ClearRegion, 514 Effect::ClearRegion,
@@ -850,30 +880,18 @@ impl ClientCore {
850 } 880 }
851 } 881 }
852 882
853 fn cmd_play(&mut self, parts: &[&str]) -> Vec<Effect> { 883 fn play_game(&mut self, game_type: &str, bet: Option<u64>) -> Vec<Effect> {
854 if !self.connected { 884 if !self.connected {
855 return print_line("Not connected. Use 'connect <ip>:<port>' first."); 885 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
856 } 886 }
857 if self.logged_in.is_none() { 887 if self.logged_in.is_none() {
858 return print_line("Not logged in. Use 'login' first."); 888 return print_line("Not logged in. Use 'login' first.");
859 } 889 }
860 if parts.len() < 2 {
861 return self.available_games(Some("Usage: play <game> [bet]"));
862 }
863
864 let bet = match parts.get(2) {
865 Some(raw) => match parse_bet_arg(raw) {
866 Ok(bet) => Some(bet),
867 Err(message) => return print_line(message),
868 },
869 None => None,
870 };
871
872 if self.game.is_in_game() { 890 if self.game.is_in_game() {
873 return print_line("Already in a game. Use 'quit' to exit current game."); 891 return print_line("Already in a game. Use 'quit' to exit current game.");
874 } 892 }
875 893
876 match self.game.enter_game(parts[1]) { 894 match self.game.enter_game(game_type) {
877 Ok(()) => { 895 Ok(()) => {
878 if let Some(bet) = bet { 896 if let Some(bet) = bet {
879 let _ = self.game.set_bet(bet); 897 let _ = self.game.set_bet(bet);
@@ -891,6 +909,52 @@ impl ClientCore {
891 } 909 }
892 } 910 }
893 911
912 fn load_history(&mut self, limit: u32) -> Vec<Effect> {
913 if !self.connected {
914 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
915 }
916 if self.logged_in.is_none() {
917 return print_line("Not logged in. Use 'login' first.");
918 }
919 if !(1..=MAX_HISTORY).contains(&limit) {
920 return print_line(format!("Usage: history [1-{MAX_HISTORY}]"));
921 }
922
923 self.screen = Screen::History;
924 self.pending = Some(Pending::History);
925 let request = if limit == crate::history::DEFAULT_HISTORY {
926 "HISTORY".to_string()
927 } else {
928 format!("HISTORY {limit}")
929 };
930 vec![Effect::SendLine(request)]
931 }
932
933 fn cmd_play(&mut self, parts: &[&str]) -> Vec<Effect> {
934 if !self.connected {
935 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
936 }
937 if self.logged_in.is_none() {
938 return print_line("Not logged in. Use 'login' first.");
939 }
940 if parts.len() < 2 {
941 return self.available_games(Some("Usage: play <game> [bet]"));
942 }
943
944 let bet = match parts.get(2) {
945 Some(raw) => match parse_bet_arg(raw) {
946 Ok(bet) => Some(bet),
947 Err(message) => return print_line(message),
948 },
949 None => None,
950 };
951
952 self.command(Command::Play {
953 game: parts[1].to_string(),
954 bet,
955 })
956 }
957
894 fn available_games(&self, usage: Option<&str>) -> Vec<Effect> { 958 fn available_games(&self, usage: Option<&str>) -> Vec<Effect> {
895 let mut lines = vec!["Available games:".to_string()]; 959 let mut lines = vec!["Available games:".to_string()];
896 for (game_type, name) in self.game.get_available_games() { 960 for (game_type, name) in self.game.get_available_games() {
@@ -904,23 +968,15 @@ impl ClientCore {
904 } 968 }
905 969
906 fn cmd_history(&mut self, parts: &[&str]) -> Vec<Effect> { 970 fn cmd_history(&mut self, parts: &[&str]) -> Vec<Effect> {
907 if !self.connected { 971 let limit = match parts.get(1) {
908 return print_line("Not connected. Use 'connect <ip>:<port>' first.");
909 }
910 if self.logged_in.is_none() {
911 return print_line("Not logged in. Use 'login' first.");
912 }
913
914 let request = match parts.get(1) {
915 Some(raw) => match raw.parse::<u32>() { 972 Some(raw) => match raw.parse::<u32>() {
916 Ok(count) if (1..=MAX_HISTORY).contains(&count) => format!("HISTORY {count}"), 973 Ok(count) => count,
917 _ => return print_line(format!("Usage: history [1-{MAX_HISTORY}]")), 974 Err(_) => return print_line(format!("Usage: history [1-{MAX_HISTORY}]")),
918 }, 975 },
919 None => "HISTORY".to_string(), 976 None => crate::history::DEFAULT_HISTORY,
920 }; 977 };
921 978
922 self.pending = Some(Pending::History); 979 self.command(Command::History { limit })
923 vec![Effect::SendLine(request)]
924 } 980 }
925 981
926 fn cmd_info(&mut self) -> Vec<Effect> { 982 fn cmd_info(&mut self) -> Vec<Effect> {
@@ -990,17 +1046,38 @@ impl ClientCore {
990 1046
991 /// Snapshot of the current screen for a frontend to draw. 1047 /// Snapshot of the current screen for a frontend to draw.
992 pub fn view(&self) -> Option<View> { 1048 pub fn view(&self) -> Option<View> {
993 let state = self.game.get_current_state()?; 1049 if let Some(state) = self.game.get_current_state() {
994 Some(match state.game_type.as_str() { 1050 return Some(match state.game_type.as_str() {
995 "slots" => View::Slots(SlotsView::from_state(state)), 1051 "slots" => View::Slots(SlotsView::from_state(state)),
996 "roulette" => View::Roulette(RouletteView::from_state(state)), 1052 "roulette" => View::Roulette(RouletteView::from_state(state)),
997 "blackjack" => View::Blackjack(BlackjackView::from_state(state)), 1053 "blackjack" => View::Blackjack(BlackjackView::from_state(state)),
998 "crash" => View::Crash(CrashView::from_state(state)), 1054 "crash" => View::Crash(CrashView::from_state(state)),
999 _ => return None, 1055 _ => return None,
1056 });
1057 }
1058
1059 Some(match self.screen {
1060 Screen::History => View::History(HistoryView {
1061 records: self.play_history.clone(),
1062 }),
1063 Screen::Lobby => View::Lobby(LobbyView {
1064 connected: self.connected,
1065 user: self.logged_in.clone(),
1066 balance: self.balance,
1067 games: self
1068 .game
1069 .get_available_games()
1070 .into_iter()
1071 .map(|(game, name)| (game.to_string(), name.to_string()))
1072 .collect(),
1073 }),
1000 }) 1074 })
1001 } 1075 }
1002 1076
1003 fn board_effects(&self) -> Vec<Effect> { 1077 fn board_effects(&self) -> Vec<Effect> {
1078 if !self.is_in_game() {
1079 return Vec::new();
1080 }
1004 self.view().map(Effect::Render).into_iter().collect() 1081 self.view().map(Effect::Render).into_iter().collect()
1005 } 1082 }
1006 1083
diff --git a/src/client/tui.rs b/src/client/tui.rs
index ddea5e2..9853edf 100644
--- a/src/client/tui.rs
+++ b/src/client/tui.rs
@@ -16,6 +16,8 @@ pub fn render_view(view: &View) -> Vec<String> {
16 View::Roulette(roulette) => RouletteGame::render_view(roulette), 16 View::Roulette(roulette) => RouletteGame::render_view(roulette),
17 View::Blackjack(blackjack) => BlackjackGame::render_view(blackjack), 17 View::Blackjack(blackjack) => BlackjackGame::render_view(blackjack),
18 View::Crash(crash) => CrashGame::render_view(crash), 18 View::Crash(crash) => CrashGame::render_view(crash),
19 // The terminal adapter keeps its command prompt for these screens.
20 View::Lobby(_) | View::History(_) => String::new(),
19 }; 21 };
20 rendered.lines().map(str::to_string).collect() 22 rendered.lines().map(str::to_string).collect()
21} 23}
diff --git a/src/client/view.rs b/src/client/view.rs
index 77efec6..903403c 100644
--- a/src/client/view.rs
+++ b/src/client/view.rs
@@ -241,9 +241,26 @@ impl CrashView {
241 } 241 }
242} 242}
243 243
244/// The lobby: who is logged in, the balance, and what can be played.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct LobbyView {
247 pub connected: bool,
248 pub user: Option<String>,
249 pub balance: Option<i64>,
250 pub games: Vec<(String, String)>,
251}
252
253/// The play log, already parsed.
254#[derive(Debug, Clone, PartialEq)]
255pub struct HistoryView {
256 pub records: Vec<crate::history::PlayRecord>,
257}
258
244/// Everything a frontend can draw. 259/// Everything a frontend can draw.
245#[derive(Debug, Clone, PartialEq)] 260#[derive(Debug, Clone, PartialEq)]
246pub enum View { 261pub enum View {
262 Lobby(LobbyView),
263 History(HistoryView),
247 Slots(SlotsView), 264 Slots(SlotsView),
248 Roulette(RouletteView), 265 Roulette(RouletteView),
249 Blackjack(BlackjackView), 266 Blackjack(BlackjackView),
@@ -253,6 +270,10 @@ pub enum View {
253/// Semantic input. Frontends turn keys and clicks into these. 270/// Semantic input. Frontends turn keys and clicks into these.
254#[derive(Debug, Clone, PartialEq, Eq)] 271#[derive(Debug, Clone, PartialEq, Eq)]
255pub enum Command { 272pub enum Command {
273 Play { game: String, bet: Option<u64> },
274 History { limit: u32 },
275 ShowLobby,
276 Logout,
256 SetBet(u64), 277 SetBet(u64),
257 AdjustBet(i64), 278 AdjustBet(i64),
258 Leave, 279 Leave,