summaryrefslogtreecommitdiffstats
path: root/src/client/games
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/games')
-rw-r--r--src/client/games/blackjack.rs59
-rw-r--r--src/client/games/crash.rs67
-rw-r--r--src/client/games/slots.rs125
3 files changed, 116 insertions, 135 deletions
diff --git a/src/client/games/blackjack.rs b/src/client/games/blackjack.rs
index 8fa5693..e8f251f 100644
--- a/src/client/games/blackjack.rs
+++ b/src/client/games/blackjack.rs
@@ -1,5 +1,6 @@
1use super::traits::{Game, GameState}; 1use super::traits::{Game, GameState};
2use crate::blackjack::{BlackjackState, card_label}; 2use crate::blackjack::card_label;
3use crate::client::view::BlackjackView;
3 4
4pub const BLACKJACK_BOARD_LINES: usize = 6; 5pub const BLACKJACK_BOARD_LINES: usize = 6;
5 6
@@ -10,13 +11,6 @@ impl BlackjackGame {
10 Self 11 Self
11 } 12 }
12 13
13 fn state(state: &GameState) -> Option<BlackjackState> {
14 state
15 .data
16 .get("blackjack")
17 .and_then(|raw| serde_json::from_str(raw).ok())
18 }
19
20 fn cards(cards: &[String]) -> String { 14 fn cards(cards: &[String]) -> String {
21 cards 15 cards
22 .iter() 16 .iter()
@@ -43,40 +37,29 @@ impl BlackjackGame {
43 _ => "Hand complete.".to_string(), 37 _ => "Hand complete.".to_string(),
44 } 38 }
45 } 39 }
46}
47 40
48impl Game for BlackjackGame { 41 /// Render a blackjack view to terminal lines. The GUI paints the same view.
49 fn get_name(&self) -> &'static str { 42 pub fn render_view(view: &BlackjackView) -> String {
50 "Blackjack" 43 let balance = view
51 } 44 .balance
52 45 .map(|balance| balance.to_string())
53 fn render(&self, state: &GameState) -> String { 46 .unwrap_or_else(|| "?".to_string());
54 let status = state
55 .data
56 .get("status")
57 .map(String::as_str)
58 .unwrap_or("ready");
59 let balance = state.data.get("balance").map(String::as_str).unwrap_or("?");
60 let hand = Self::state(state);
61 47
62 if let Some(error) = state.data.get("error") { 48 if let Some(error) = &view.error {
63 let bet = hand 49 let bet = view.hand.as_ref().map(|hand| hand.bet).unwrap_or(view.bet);
64 .as_ref()
65 .map(|hand| hand.bet)
66 .unwrap_or(state.bet_amount);
67 return format!( 50 return format!(
68 "=== BLACKJACK ===\r\nBalance: {balance} Kr Bet: {bet} Kr\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit" 51 "=== BLACKJACK ===\r\nBalance: {balance} Kr Bet: {bet} Kr\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit"
69 ); 52 );
70 } 53 }
71 54
72 let (dealer, dealer_total, player, player_total, bet, text, controls) = 55 let (dealer, dealer_total, player, player_total, bet, text, controls) =
73 if let Some(hand) = &hand { 56 if let Some(hand) = &view.hand {
74 let bet = if status == "player" { 57 let bet = if view.status == "player" {
75 hand.bet 58 hand.bet
76 } else { 59 } else {
77 state.bet_amount 60 view.bet
78 }; 61 };
79 let text = match status { 62 let text = match view.status.as_str() {
80 "player" => { 63 "player" => {
81 if hand.actions.iter().any(|action| action == "double") { 64 if hand.actions.iter().any(|action| action == "double") {
82 "Hit, stand or double.".to_string() 65 "Hit, stand or double.".to_string()
@@ -89,7 +72,7 @@ impl Game for BlackjackGame {
89 "done" => Self::result_text(&hand.result, hand.winnings), 72 "done" => Self::result_text(&hand.result, hand.winnings),
90 _ => "Press [SPACE] to deal.".to_string(), 73 _ => "Press [SPACE] to deal.".to_string(),
91 }; 74 };
92 let controls = if status == "player" { 75 let controls = if view.status == "player" {
93 if hand.actions.iter().any(|action| action == "double") { 76 if hand.actions.iter().any(|action| action == "double") {
94 "[H]it [S]tand [D]ouble [Q] Forfeit".to_string() 77 "[H]it [S]tand [D]ouble [Q] Forfeit".to_string()
95 } else { 78 } else {
@@ -113,7 +96,7 @@ impl Game for BlackjackGame {
113 0, 96 0,
114 "—".to_string(), 97 "—".to_string(),
115 0, 98 0,
116 state.bet_amount, 99 view.bet,
117 "Press [SPACE] to deal.".to_string(), 100 "Press [SPACE] to deal.".to_string(),
118 "[SPACE] Deal [+/-] Bet [Q] Quit".to_string(), 101 "[SPACE] Deal [+/-] Bet [Q] Quit".to_string(),
119 ) 102 )
@@ -124,6 +107,16 @@ impl Game for BlackjackGame {
124 balance, bet, dealer, dealer_total, player, player_total, text, controls 107 balance, bet, dealer, dealer_total, player, player_total, text, controls
125 ) 108 )
126 } 109 }
110}
111
112impl Game for BlackjackGame {
113 fn get_name(&self) -> &'static str {
114 "Blackjack"
115 }
116
117 fn render(&self, state: &GameState) -> String {
118 Self::render_view(&BlackjackView::from_state(state))
119 }
127 120
128 fn render_lines(&self, _state: &GameState) -> usize { 121 fn render_lines(&self, _state: &GameState) -> usize {
129 BLACKJACK_BOARD_LINES 122 BLACKJACK_BOARD_LINES
diff --git a/src/client/games/crash.rs b/src/client/games/crash.rs
index 383e807..affb642 100644
--- a/src/client/games/crash.rs
+++ b/src/client/games/crash.rs
@@ -1,4 +1,5 @@
1use super::traits::{Game, GameState}; 1use super::traits::{Game, GameState};
2use crate::client::view::CrashView;
2 3
3pub const CRASH_BOARD_LINES: usize = 6; 4pub const CRASH_BOARD_LINES: usize = 6;
4 5
@@ -9,67 +10,67 @@ impl CrashGame {
9 Self 10 Self
10 } 11 }
11 12
12 fn value<'a>(state: &'a GameState, key: &str) -> Option<&'a str> { 13 fn status_text(view: &CrashView) -> String {
13 state.data.get(key).map(String::as_str) 14 match view.status.as_str() {
14 }
15
16 fn status_text(state: &GameState) -> String {
17 let status = Self::value(state, "status").unwrap_or("ready");
18 match status {
19 "starting" => "Starting...".to_string(), 15 "starting" => "Starting...".to_string(),
20 "running" => "Rising... cash out before it crashes.".to_string(), 16 "running" => "Rising... cash out before it crashes.".to_string(),
21 "cashed" => format!( 17 "cashed" => format!(
22 "Cashed out at {}x Won {} Kr", 18 "Cashed out at {:.2}x Won {} Kr",
23 Self::value(state, "multiplier").unwrap_or("1.00"), 19 view.multiplier, view.payout
24 Self::value(state, "payout").unwrap_or("0")
25 ), 20 ),
26 "crashed" => format!( 21 "crashed" => format!(
27 "CRASHED at {}x Bet lost", 22 "CRASHED at {}x Bet lost",
28 Self::value(state, "crash_point").unwrap_or("?") 23 view.crash_point
24 .map(|point| format!("{point:.2}"))
25 .unwrap_or_else(|| "?".to_string())
29 ), 26 ),
30 "forfeit" => "Forfeited. Bet lost.".to_string(), 27 "forfeit" => "Forfeited. Bet lost.".to_string(),
31 _ => "Press [SPACE] to start.".to_string(), 28 _ => "Press [SPACE] to start.".to_string(),
32 } 29 }
33 } 30 }
34 31
35 fn controls(state: &GameState) -> &'static str { 32 fn controls(view: &CrashView) -> &'static str {
36 if Self::value(state, "status") == Some("running") { 33 if view.status == "running" {
37 "[C] Cash out [Q] Forfeit" 34 "[C] Cash out [Q] Forfeit"
38 } else { 35 } else {
39 "[SPACE] Play [+/=/-] Bet [Q] Quit" 36 "[SPACE] Play [+/=/-] Bet [Q] Quit"
40 } 37 }
41 } 38 }
42}
43 39
44impl Game for CrashGame { 40 /// Render a crash view to terminal lines. The GUI paints the same view.
45 fn get_name(&self) -> &'static str { 41 pub fn render_view(view: &CrashView) -> String {
46 "Crash" 42 let balance = view
47 } 43 .balance
48 44 .map(|balance| balance.to_string())
49 fn render(&self, state: &GameState) -> String { 45 .unwrap_or_else(|| "?".to_string());
50 let balance = Self::value(state, "balance").unwrap_or("?");
51 let multiplier = Self::value(state, "multiplier").unwrap_or("1.00");
52 let potential = Self::value(state, "potential")
53 .map(str::to_string)
54 .unwrap_or_else(|| state.bet_amount.to_string());
55 46
56 if let Some(error) = Self::value(state, "error") { 47 if let Some(error) = &view.error {
57 return format!( 48 return format!(
58 "=== CRASH ===\r\nBalance: {balance} Kr Bet: {} Kr\r\n\r\nERROR: {error}\r\n\r\n[SPACE] Play [+/=/-] Bet [Q] Quit", 49 "=== CRASH ===\r\nBalance: {balance} Kr Bet: {} Kr\r\n\r\nERROR: {error}\r\n\r\n[SPACE] Play [+/=/-] Bet [Q] Quit",
59 state.bet_amount 50 view.bet
60 ); 51 );
61 } 52 }
62 53
63 format!( 54 format!(
64 "=== CRASH ===\r\nBalance: {} Kr Bet: {} Kr\r\nMultiplier: {}x\r\nPotential: {} Kr\r\n{}\r\n{}", 55 "=== CRASH ===\r\nBalance: {} Kr Bet: {} Kr\r\nMultiplier: {:.2}x\r\nPotential: {} Kr\r\n{}\r\n{}",
65 balance, 56 balance,
66 state.bet_amount, 57 view.bet,
67 multiplier, 58 view.multiplier,
68 potential, 59 view.potential,
69 Self::status_text(state), 60 Self::status_text(view),
70 Self::controls(state) 61 Self::controls(view)
71 ) 62 )
72 } 63 }
64}
65
66impl Game for CrashGame {
67 fn get_name(&self) -> &'static str {
68 "Crash"
69 }
70
71 fn render(&self, state: &GameState) -> String {
72 Self::render_view(&CrashView::from_state(state))
73 }
73 74
74 fn render_lines(&self, _state: &GameState) -> usize { 75 fn render_lines(&self, _state: &GameState) -> usize {
75 CRASH_BOARD_LINES 76 CRASH_BOARD_LINES
diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs
index 5bffc4c..3aad319 100644
--- a/src/client/games/slots.rs
+++ b/src/client/games/slots.rs
@@ -1,4 +1,5 @@
1use super::traits::{Game, GamePhase, GameState}; 1use super::traits::{Game, GameState};
2use crate::client::view::{SlotsPhase, SlotsView};
2use crate::slots::{LINE_NAMES, REELS, ROWS, paytable_lines}; 3use crate::slots::{LINE_NAMES, REELS, ROWS, paytable_lines};
3 4
4/// Height of the game board. It must never depend on the spin result. 5/// Height of the game board. It must never depend on the spin result.
@@ -28,105 +29,91 @@ impl SlotsGame {
28 } 29 }
29 } 30 }
30 31
31 fn highlighted_cells(state: &GameState) -> Vec<usize> { 32 fn render_grid(reels: &[char], winning: &[usize]) -> [String; ROWS] {
32 state
33 .data
34 .get("win_cells")
35 .map(|raw| {
36 raw.split(',')
37 .filter_map(|value| value.trim().parse::<usize>().ok())
38 .collect()
39 })
40 .unwrap_or_default()
41 }
42
43 fn render_grid(&self, reels: &str, winning: &[usize]) -> [String; ROWS] {
44 let chars: Vec<char> = reels.chars().collect();
45 std::array::from_fn(|row| { 33 std::array::from_fn(|row| {
46 (0..REELS) 34 (0..REELS)
47 .map(|col| Self::cell(&chars, row, col, winning)) 35 .map(|col| Self::cell(reels, row, col, winning))
48 .collect::<Vec<_>>() 36 .collect::<Vec<_>>()
49 .join(" ") 37 .join(" ")
50 }) 38 })
51 } 39 }
52 40
53 fn render_status(&self, state: &GameState) -> String { 41 fn render_status(view: &SlotsView) -> String {
54 if let Some(error) = state.data.get("error") { 42 if let Some(error) = &view.error {
55 return format!("ERROR: {error}"); 43 return format!("ERROR: {error}");
56 } 44 }
57 if state.phase == GamePhase::Playing { 45
58 return "Spinning...".to_string(); 46 match view.phase {
59 } 47 SlotsPhase::Spinning => "Spinning...".to_string(),
60 if state.phase == GamePhase::Finished { 48 SlotsPhase::Result => {
61 let winnings = state 49 let winnings = view.winnings.unwrap_or(0);
62 .data 50 if winnings == 0 {
63 .get("winnings") 51 return "No win.".to_string();
64 .map(String::as_str) 52 }
65 .unwrap_or("0"); 53 match view.win_type.as_str() {
66 if winnings != "0" { 54 "mega" => format!("MEGA WIN! {winnings} Kr"),
67 return match state.data.get("win_type").map(String::as_str) { 55 "big" => format!("BIG WIN! {winnings} Kr"),
68 Some("mega") => format!("MEGA WIN! {winnings} Kr"),
69 Some("big") => format!("BIG WIN! {winnings} Kr"),
70 _ => format!("Won {winnings} Kr"), 56 _ => format!("Won {winnings} Kr"),
71 }; 57 }
72 } 58 }
73 return "No win.".to_string(); 59 SlotsPhase::Ready => "Press [SPACE] to spin!".to_string(),
74 } 60 }
75 "Press [SPACE] to spin!".to_string()
76 } 61 }
77 62
78 fn render_win_lines(&self, state: &GameState) -> String { 63 fn render_win_lines(view: &SlotsView) -> String {
79 if state.phase != GamePhase::Finished { 64 if view.phase != SlotsPhase::Result || view.win_lines.is_empty() {
80 return String::new(); 65 return String::new();
81 } 66 }
82 match state.data.get("win_lines").map(String::as_str) {
83 Some(lines) if !lines.is_empty() && lines != "-" => {
84 let named: Vec<String> = lines
85 .split(',')
86 .filter_map(|raw| raw.trim().parse::<usize>().ok())
87 .map(|line| {
88 let name = line
89 .checked_sub(1)
90 .and_then(|index| LINE_NAMES.get(index))
91 .copied();
92 match name {
93 Some(name) => format!("{line} ({name})"),
94 None => line.to_string(),
95 }
96 })
97 .collect();
98 format!("Winning lines: {}", named.join(", "))
99 }
100 _ => String::new(),
101 }
102 }
103}
104 67
105impl Game for SlotsGame { 68 let named: Vec<String> = view
106 fn get_name(&self) -> &'static str { 69 .win_lines
107 "Slots" 70 .iter()
71 .map(|line| {
72 let name = line
73 .checked_sub(1)
74 .and_then(|index| LINE_NAMES.get(index))
75 .copied();
76 match name {
77 Some(name) => format!("{line} ({name})"),
78 None => line.to_string(),
79 }
80 })
81 .collect();
82 format!("Winning lines: {}", named.join(", "))
108 } 83 }
109 84
110 fn render(&self, state: &GameState) -> String { 85 /// Render a slots view to terminal lines. The GUI paints the same view.
111 let reels = state.data.get("reels").cloned().unwrap_or_default(); 86 pub fn render_view(view: &SlotsView) -> String {
112 let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); 87 let balance = view
113 let winning = Self::highlighted_cells(state); 88 .balance
114 let grid = self.render_grid(&reels, &winning); 89 .map(|balance| balance.to_string())
90 .unwrap_or_else(|| "?".to_string());
91 let grid = Self::render_grid(&view.reels, &view.win_cells);
115 let paytable = paytable_lines(); 92 let paytable = paytable_lines();
116 93
117 format!( 94 format!(
118 "=== SLOTS ===\r\nBet: {} Kr Balance: {} Kr\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit", 95 "=== SLOTS ===\r\nBet: {} Kr Balance: {} Kr\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit",
119 state.bet_amount, 96 view.bet,
120 balance, 97 balance,
121 grid[0], 98 grid[0],
122 grid[1], 99 grid[1],
123 grid[2], 100 grid[2],
124 paytable[0], 101 paytable[0],
125 paytable[1], 102 paytable[1],
126 self.render_status(state), 103 Self::render_status(view),
127 self.render_win_lines(state) 104 Self::render_win_lines(view)
128 ) 105 )
129 } 106 }
107}
108
109impl Game for SlotsGame {
110 fn get_name(&self) -> &'static str {
111 "Slots"
112 }
113
114 fn render(&self, state: &GameState) -> String {
115 Self::render_view(&SlotsView::from_state(state))
116 }
130 117
131 fn render_lines(&self, _state: &GameState) -> usize { 118 fn render_lines(&self, _state: &GameState) -> usize {
132 SLOT_BOARD_LINES 119 SLOT_BOARD_LINES