summaryrefslogtreecommitdiffstats
path: root/src/server/games/blackjack.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/server/games/blackjack.rs274
1 files changed, 274 insertions, 0 deletions
diff --git a/src/server/games/blackjack.rs b/src/server/games/blackjack.rs
new file mode 100644
index 0000000..9505fed
--- /dev/null
+++ b/src/server/games/blackjack.rs
@@ -0,0 +1,274 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use crate::blackjack::{
3 card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState,
4};
5use rand::seq::SliceRandom;
6use rand::thread_rng;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10pub struct BlackjackGame {
11 fixed_deck: Option<Vec<String>>,
12}
13
14impl BlackjackGame {
15 pub fn new() -> Self {
16 Self { fixed_deck: None }
17 }
18
19 pub fn with_deck(deck: Vec<String>) -> Self {
20 Self {
21 fixed_deck: Some(deck),
22 }
23 }
24
25 fn shoe(&self) -> Vec<String> {
26 if let Some(deck) = &self.fixed_deck {
27 return deck.clone();
28 }
29 let mut shoe = fresh_shoe();
30 shoe.shuffle(&mut thread_rng());
31 shoe
32 }
33
34 pub fn deal(&self, bet: u64) -> BlackjackHand {
35 let mut hand = BlackjackHand::new(self.shoe(), bet);
36 if is_blackjack(&hand.player) || is_blackjack(&hand.dealer) {
37 hand.settle();
38 }
39 hand
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct BlackjackHand {
45 pub shoe: Vec<String>,
46 pub player: Vec<String>,
47 pub dealer: Vec<String>,
48 pub phase: String,
49 pub base_bet: u64,
50 pub bet: u64,
51 pub result: String,
52 pub winnings: u64,
53}
54
55impl BlackjackHand {
56 fn new(shoe: Vec<String>, bet: u64) -> Self {
57 let mut hand = Self {
58 shoe,
59 player: Vec::new(),
60 dealer: Vec::new(),
61 phase: "player".to_string(),
62 base_bet: bet,
63 bet,
64 result: "playing".to_string(),
65 winnings: 0,
66 };
67
68 let card = hand.draw();
69 hand.player.push(card);
70 let card = hand.draw();
71 hand.dealer.push(card);
72 let card = hand.draw();
73 hand.player.push(card);
74 let card = hand.draw();
75 hand.dealer.push(card);
76
77 hand
78 }
79
80 fn draw(&mut self) -> String {
81 self.shoe.remove(0)
82 }
83
84 pub fn player_total(&self) -> u8 {
85 hand_total(&self.player)
86 }
87
88 pub fn dealer_total(&self) -> u8 {
89 hand_total(&self.dealer)
90 }
91
92 pub fn is_done(&self) -> bool {
93 self.phase == "done"
94 }
95
96 pub fn hit(&mut self) -> Result<(), String> {
97 if self.is_done() {
98 return Err("Hand is over".to_string());
99 }
100 let card = self.draw();
101 self.player.push(card);
102 let total = self.player_total();
103 if total > 21 {
104 self.settle();
105 } else if total == 21 {
106 self.dealer_play();
107 self.settle();
108 }
109 Ok(())
110 }
111
112 pub fn stand(&mut self) {
113 if self.is_done() {
114 return;
115 }
116 self.dealer_play();
117 self.settle();
118 }
119
120 /// Caller must debit the extra `base_bet` before calling this.
121 pub fn double(&mut self) -> Result<(), String> {
122 if self.is_done() {
123 return Err("Hand is over".to_string());
124 }
125 if self.player.len() != 2 {
126 return Err("Double is only allowed on the first two cards".to_string());
127 }
128 self.bet = self.bet.saturating_add(self.base_bet);
129 let card = self.draw();
130 self.player.push(card);
131 self.dealer_play();
132 self.settle();
133 Ok(())
134 }
135
136 pub fn cancel(&mut self) {
137 self.phase = "done".to_string();
138 self.result = "cancelled".to_string();
139 self.winnings = 0;
140 }
141
142 fn dealer_play(&mut self) {
143 while self.dealer_total() < 17 {
144 let card = self.draw();
145 self.dealer.push(card);
146 }
147 }
148
149 fn settle(&mut self) {
150 if self.is_done() {
151 return;
152 }
153 self.phase = "done".to_string();
154
155 let player = self.player_total();
156 let dealer = self.dealer_total();
157 let player_blackjack = is_blackjack(&self.player);
158 let dealer_blackjack = is_blackjack(&self.dealer);
159
160 self.result = if player > 21 {
161 "bust".to_string()
162 } else if player_blackjack && !dealer_blackjack {
163 "blackjack".to_string()
164 } else if dealer_blackjack && !player_blackjack {
165 "lose".to_string()
166 } else if dealer > 21 {
167 "win".to_string()
168 } else if player == dealer {
169 "push".to_string()
170 } else if player > dealer {
171 "win".to_string()
172 } else {
173 "lose".to_string()
174 };
175
176 self.winnings = match self.result.as_str() {
177 "push" => self.bet,
178 "blackjack" => self.bet + (self.bet * 3) / 2,
179 "win" => self.bet * 2,
180 _ => 0,
181 };
182 }
183
184 pub fn state(&self, session_id: &str, balance: i64) -> BlackjackState {
185 let player_total = self.player_total();
186 let (dealer, dealer_total, actions) = if self.is_done() {
187 (self.dealer.clone(), self.dealer_total(), Vec::new())
188 } else {
189 let visible = self
190 .dealer
191 .first()
192 .map(|card| card_value(card))
193 .unwrap_or(0);
194 let mut actions = vec!["hit".to_string(), "stand".to_string()];
195 if self.player.len() == 2 {
196 actions.push("double".to_string());
197 }
198 (
199 vec![
200 self.dealer.first().cloned().unwrap_or_default(),
201 "?".to_string(),
202 ],
203 visible,
204 actions,
205 )
206 };
207
208 BlackjackState {
209 session_id: session_id.to_string(),
210 phase: self.phase.clone(),
211 player: self.player.clone(),
212 dealer,
213 player_total,
214 dealer_total,
215 bet: self.bet,
216 base_bet: self.base_bet,
217 balance,
218 actions,
219 result: self.result.clone(),
220 winnings: self.winnings,
221 }
222 }
223
224 pub fn to_json(&self) -> String {
225 serde_json::to_string(self).unwrap_or_default()
226 }
227
228 pub fn from_json(raw: &str) -> Result<Self, serde_json::Error> {
229 serde_json::from_str(raw)
230 }
231}
232
233impl Game for BlackjackGame {
234 fn game_type(&self) -> &'static str {
235 "blackjack"
236 }
237
238 fn get_name(&self) -> &'static str {
239 "Blackjack"
240 }
241
242 fn get_description(&self) -> &'static str {
243 "Blackjack: hit, stand or double. Dealer stands on all 17s."
244 }
245
246 fn prepare_state(&self, _username: &str, bet_amount: u64) -> GameState {
247 let mut data = HashMap::new();
248 data.insert("status".to_string(), "ready".to_string());
249 GameState {
250 session_id: String::new(),
251 game_type: "blackjack".to_string(),
252 bet_amount,
253 phase: GamePhase::WaitingToStart,
254 data,
255 }
256 }
257
258 fn process_action(
259 &self,
260 _state: &GameState,
261 _action: &str,
262 _params: &[&str],
263 ) -> Result<GameState, GameError> {
264 Err(GameError::InvalidAction(
265 "Blackjack uses BLACKJACK_ACTION".to_string(),
266 ))
267 }
268
269 fn calculate_result(&self, _state: &GameState) -> Result<GameResult, GameError> {
270 Err(GameError::InternalError(
271 "Blackjack settles through BLACKJACK_ACTION".to_string(),
272 ))
273 }
274}