1#![allow(clippy::missing_docs_in_private_items)]
4
5use std::io::{self, Write};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use bjrs::{Card, DoubleOption, Game, GameOptions, GameState, Hand, HandStatus, Suit};
9
10fn main() {
11 println!("Blackjack CLI example (type 'q' to quit)");
12
13 let seed = SystemTime::now()
14 .duration_since(UNIX_EPOCH)
15 .unwrap_or_default()
16 .as_secs();
17 let options = GameOptions::default();
18 let game = Game::new(options, seed);
19
20 let player_id = game.join(500);
21
22 loop {
23 let money = game.get_money(player_id).unwrap_or(0);
24 if money == 0 {
25 println!("You are out of money. Game over.");
26 break;
27 }
28
29 if game.check_and_reshuffle() == Ok(true) {
30 println!("Shoe reshuffled.");
31 }
32
33 game.start_betting();
34
35 let Some(bet) = prompt_usize(&format!("Bet amount (1-{money}, 0 to quit): ")) else {
36 break;
37 };
38
39 if bet == 0 {
40 println!("Goodbye.");
41 break;
42 }
43
44 if let Err(err) = game.bet(player_id, bet) {
45 println!("Bet error: {err:?}");
46 continue;
47 }
48
49 if let Err(err) = game.deal() {
50 println!("Deal error: {err:?}");
51 game.clear_round();
52 continue;
53 }
54
55 if game.is_insurance_offered() {
56 println!("Dealer shows an Ace. Insurance offered.");
57 match prompt_line("Take insurance? (y/n): ").as_str() {
58 "y" | "yes" => match game.take_insurance(player_id) {
59 Ok(amount) => println!("Insurance bet placed: {amount}"),
60 Err(err) => println!("Insurance error: {err:?}"),
61 },
62 _ => {
63 if let Err(err) = game.decline_insurance(player_id) {
64 println!("Insurance error: {err:?}");
65 }
66 }
67 }
68
69 match game.finish_insurance() {
70 Ok(true) => {
71 println!("Dealer has blackjack.");
72 }
73 Ok(false) => {}
74 Err(err) => {
75 println!("Insurance finish error: {err:?}");
76 }
77 }
78 }
79
80 if *game.state.lock() == GameState::PlayerTurn && game.current_player().is_none() {
82 *game.state.lock() = GameState::DealerTurn;
83 }
84
85 while *game.state.lock() == GameState::PlayerTurn {
86 print_table(&game, player_id);
87
88 println!("{}", format_actions(&game, player_id));
89 let action = prompt_line("Action: ");
90 let turn = game.current_turn();
91
92 let result = match action.as_str() {
93 "h" | "hit" => game.hit(player_id, turn.hand_index).map(|_| ()),
94 "s" | "stand" => game.stand(player_id, turn.hand_index),
95 "d" | "double" => game.double_down(player_id, turn.hand_index).map(|_| ()),
96 "p" | "split" => game.split(player_id, turn.hand_index),
97 "u" | "surrender" => game.surrender(player_id, turn.hand_index).map(|_| ()),
98 "q" | "quit" => return,
99 _ => {
100 println!("Unknown action.");
101 continue;
102 }
103 };
104
105 if let Err(err) = result {
106 println!("Action error: {err:?}");
107 }
108 }
109
110 if *game.state.lock() == GameState::DealerTurn {
111 match game.dealer_play() {
112 Ok(drawn) => {
113 if !drawn.is_empty() {
114 println!("Dealer draws {} card(s).", drawn.len());
115 }
116 }
117 Err(err) => println!("Dealer error: {err:?}"),
118 }
119 }
120
121 if *game.state.lock() == GameState::RoundOver {
122 match game.showdown() {
123 Ok(result) => {
124 print_table_final(&game, player_id);
125 println!("Round complete.");
126 for player in result.players {
127 if player.player_id == player_id {
128 println!("Payout: {} (net {})", player.total_payout, player.net);
129 if player.insurance_bet > 0 {
130 println!("Insurance payout: {}", player.insurance_payout);
131 }
132 }
133 }
134 }
135 Err(err) => println!("Showdown error: {err:?}"),
136 }
137 }
138
139 game.clear_round();
140
141 }
143}
144
145fn prompt_line(prompt: &str) -> String {
146 print!("{prompt}");
147 let _ = io::stdout().flush();
148
149 let mut input = String::new();
150 if io::stdin().read_line(&mut input).is_err() {
151 return String::new();
152 }
153 input.trim().to_lowercase()
154}
155
156fn prompt_usize(prompt: &str) -> Option<usize> {
157 loop {
158 let input = prompt_line(prompt);
159 if input == "q" || input == "quit" {
160 return None;
161 }
162 match input.parse::<usize>() {
163 Ok(value) => return Some(value),
164 Err(_) => println!("Please enter a number."),
165 }
166 }
167}
168
169fn print_table(game: &Game, player_id: u8) {
170 let remaining = game.cards_remaining();
171 println!("\nShoe: {remaining} cards remaining");
172
173 let dealer = game.get_dealer_hand();
174 let dealer_view = format_dealer(&dealer);
175 let dealer_value = if dealer.is_hole_revealed() {
176 dealer.value()
177 } else {
178 dealer.visible_value()
179 };
180 println!("\nDealer: {dealer_view} (value {dealer_value})");
181
182 let hands = game.get_hands(player_id).unwrap_or_default();
183 let turn = game.current_turn();
184 for (index, hand) in hands.iter().enumerate() {
185 let marker = if index == turn.hand_index { "*" } else { " " };
186 println!(
187 "{} Hand {}: {} | value {} | bet {} | {:?}",
188 marker,
189 index,
190 format_hand(hand),
191 hand.value(),
192 hand.bet(),
193 hand.status()
194 );
195 }
196 println!();
197}
198
199fn print_table_final(game: &Game, player_id: u8) {
200 let remaining = game.cards_remaining();
201 println!("\nShoe: {remaining} cards remaining");
202
203 let dealer = game.get_dealer_hand();
204 println!(
205 "\nDealer: {} (value {})",
206 format_dealer(&dealer),
207 dealer.value()
208 );
209
210 let hands = game.get_hands(player_id).unwrap_or_default();
211 for (index, hand) in hands.iter().enumerate() {
212 println!(
213 "Hand {}: {} | value {} | bet {} | {:?}",
214 index,
215 format_hand(hand),
216 hand.value(),
217 hand.bet(),
218 hand.status()
219 );
220 }
221 println!();
222}
223
224fn format_actions(game: &Game, player_id: u8) -> String {
225 let availability = available_actions(game, player_id);
226 let mut parts = Vec::new();
227 parts.push(format_action("hit", "h", availability.hit));
228 parts.push(format_action("stand", "s", availability.stand));
229 parts.push(format_action("double", "d", availability.double));
230 parts.push(format_action("split", "p", availability.split));
231 parts.push(format_action("surrender", "u", availability.surrender));
232 format!("Actions: {}", parts.join(" "))
233}
234
235fn format_action(label: &str, key: &str, allowed: bool) -> String {
236 let text = format!("[{key}]{label}");
237 if allowed {
238 colorize(&text, "32")
239 } else {
240 colorize(&text, "90")
241 }
242}
243
244fn colorize(text: &str, code: &str) -> String {
245 format!("\u{1b}[{code}m{text}\u{1b}[0m")
246}
247
248struct ActionAvailability {
249 hit: bool,
250 stand: bool,
251 double: bool,
252 split: bool,
253 surrender: bool,
254}
255
256fn available_actions(game: &Game, player_id: u8) -> ActionAvailability {
257 if *game.state.lock() != GameState::PlayerTurn {
258 return ActionAvailability {
259 hit: false,
260 stand: false,
261 double: false,
262 split: false,
263 surrender: false,
264 };
265 }
266
267 if game.current_player() != Some(player_id) {
268 return ActionAvailability {
269 hit: false,
270 stand: false,
271 double: false,
272 split: false,
273 surrender: false,
274 };
275 }
276
277 let hands = game.get_hands(player_id).unwrap_or_default();
278 let turn = game.current_turn();
279 let Some(hand) = hands.get(turn.hand_index) else {
280 return ActionAvailability {
281 hit: false,
282 stand: false,
283 double: false,
284 split: false,
285 surrender: false,
286 };
287 };
288
289 if hand.status() != HandStatus::Active {
290 return ActionAvailability {
291 hit: false,
292 stand: false,
293 double: false,
294 split: false,
295 surrender: false,
296 };
297 }
298
299 let money = game.get_money(player_id).unwrap_or(0);
300 let bet = hand.bet();
301 let has_funds_for_double = money >= bet;
302 let has_funds_for_split = money >= bet;
303
304 let can_double_value = match game.options.double {
305 DoubleOption::Any => true,
306 DoubleOption::NineOrTen => hand.value() == 9 || hand.value() == 10,
307 DoubleOption::NineThrough11 => (9..=11).contains(&hand.value()),
308 DoubleOption::NineThrough15 => (9..=15).contains(&hand.value()),
309 DoubleOption::None => false,
310 _ => panic!("unhandled double option"),
311 };
312
313 let can_double = hand.len() == 2
314 && (!hand.is_from_split() || game.options.double_after_split)
315 && can_double_value
316 && has_funds_for_double;
317
318 let is_ace = hand.cards().first().is_some_and(|c| c.rank == 1);
319 let max_splits_reached = hands.len() > game.options.split as usize;
320 let can_split = hand.can_split()
321 && !max_splits_reached
322 && has_funds_for_split
323 && !(is_ace && hand.is_from_split() && game.options.split_aces_only_once);
324
325 let can_surrender = game.options.surrender && hand.len() == 2 && !hand.is_from_split();
326
327 ActionAvailability {
328 hit: true,
329 stand: true,
330 double: can_double,
331 split: can_split,
332 surrender: can_surrender,
333 }
334}
335
336fn format_dealer(dealer: &bjrs::DealerHand) -> String {
337 if dealer.cards().is_empty() {
338 return "(no cards)".to_string();
339 }
340
341 if dealer.is_hole_revealed() {
342 dealer
343 .cards()
344 .iter()
345 .map(format_card)
346 .collect::<Vec<_>>()
347 .join(" ")
348 } else {
349 let mut parts = Vec::new();
350 if let Some(card) = dealer.up_card() {
351 parts.push(format_card(card));
352 }
353 if dealer.len() > 1 {
354 parts.push("??".to_string());
355 }
356 parts.join(" ")
357 }
358}
359
360fn format_hand(hand: &Hand) -> String {
361 if hand.is_empty() {
362 return "(empty)".to_string();
363 }
364 hand.cards()
365 .iter()
366 .map(format_card)
367 .collect::<Vec<_>>()
368 .join(" ")
369}
370
371fn format_card(card: &Card) -> String {
372 let (suit, color_code) = match card.suit {
373 Suit::Hearts => ("H", "31"),
374 Suit::Diamonds => ("D", "31"),
375 Suit::Clubs => ("C", "32"),
376 Suit::Spades => ("S", "34"),
377 };
378
379 let (rank, is_face) = match card.rank {
380 1 => ("A".to_string(), true),
381 11 => ("J".to_string(), true),
382 12 => ("Q".to_string(), true),
383 13 => ("K".to_string(), true),
384 _ => (card.rank.to_string(), false),
385 };
386
387 let colored_rank = if is_face {
388 colorize(&rank, color_code)
389 } else {
390 rank
391 };
392 let colored_suit = colorize(suit, color_code);
393 format!("{colored_rank}{colored_suit}")
394}