summaryrefslogtreecommitdiffstats
path: root/src/client/core.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-29 17:18:34 +0100
committersalmonik <salmonik@tuta.io>2026-10-29 17:18:34 +0100
commitd9f1fe513e6f7696809d3a9b4d44ca485fe99c97 (patch)
tree9da99200fdfe004a47056d10340b3f59c7ccc52e /src/client/core.rs
parentv2.0.0: reset plays audit to a generic per-game schema (diff)
downloadzagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.tar.gz
zagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.zip
v2.1.0: add European roulette with split bets and wheel animation
Diffstat (limited to '')
-rw-r--r--src/client/core.rs183
1 files changed, 182 insertions, 1 deletions
diff --git a/src/client/core.rs b/src/client/core.rs
index a88991b..e005f2a 100644
--- a/src/client/core.rs
+++ b/src/client/core.rs
@@ -1,9 +1,10 @@
1use crate::client::games::manager::GameManager; 1use crate::client::games::manager::GameManager;
2use crate::client::games::response::parse_spin_response; 2use crate::client::games::response::{parse_roulette_response, parse_spin_response};
3use crate::client::ui::{ 3use crate::client::ui::{
4 command_names, completion_lines, display_width, help_lines, input_cursor_column, 4 command_names, completion_lines, display_width, help_lines, input_cursor_column,
5 parse_bet_arg, record_history, 5 parse_bet_arg, record_history,
6}; 6};
7use crate::roulette::{Bets, NumberBet};
7 8
8#[derive(Debug, Clone, Copy, PartialEq, Eq)] 9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Key { 10pub enum Key {
@@ -33,6 +34,11 @@ pub enum Effect {
33 Print(Vec<String>), 34 Print(Vec<String>),
34 /// Replace the game board region in place. 35 /// Replace the game board region in place.
35 ReplaceRegion(Vec<String>), 36 ReplaceRegion(Vec<String>),
37 /// Run the roulette wheel animation, then draw the final board lines.
38 AnimateRoulette {
39 winning: u8,
40 final_lines: Vec<String>,
41 },
36 /// Remove the game board region. 42 /// Remove the game board region.
37 ClearRegion, 43 ClearRegion,
38 /// Redraw the command prompt. `text` is the input without the `> ` prefix. 44 /// Redraw the command prompt. `text` is the input without the `> ` prefix.
@@ -54,11 +60,20 @@ pub enum Effect {
54#[derive(Debug, Clone, Copy, PartialEq, Eq)] 60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum Pending { 61enum Pending {
56 Spin, 62 Spin,
63 RouletteSpin,
57 Ping, 64 Ping,
58 Echo, 65 Echo,
59 Info, 66 Info,
60} 67}
61 68
69#[derive(Debug, Clone, Copy)]
70enum RouletteSide {
71 Red,
72 Black,
73 Even,
74 Odd,
75}
76
62pub struct ClientCore { 77pub struct ClientCore {
63 input: String, 78 input: String,
64 history: Vec<String>, 79 history: Vec<String>,
@@ -160,6 +175,22 @@ impl ClientCore {
160 self.board_effects() 175 self.board_effects()
161 } 176 }
162 }, 177 },
178 Some(Pending::RouletteSpin) => match parse_roulette_response(reply) {
179 Ok(result) => {
180 self.balance = Some(result.balance);
181 let winning = result.number;
182 self.game.apply_roulette_result(result);
183 let final_lines = self.board_lines().unwrap_or_default();
184 vec![Effect::AnimateRoulette {
185 winning,
186 final_lines,
187 }]
188 }
189 Err(message) => {
190 self.game.set_error(message);
191 self.board_effects()
192 }
193 },
163 Some(Pending::Ping) => { 194 Some(Pending::Ping) => {
164 if reply.trim() == "PONG" { 195 if reply.trim() == "PONG" {
165 print_line("Pong") 196 print_line("Pong")
@@ -194,6 +225,15 @@ impl ClientCore {
194 } 225 }
195 226
196 fn handle_game_key(&mut self, key: Key) -> Vec<Effect> { 227 fn handle_game_key(&mut self, key: Key) -> Vec<Effect> {
228 if self
229 .game
230 .get_current_state()
231 .map(|state| state.game_type.as_str())
232 == Some("roulette")
233 {
234 return self.handle_roulette_key(key);
235 }
236
197 match key { 237 match key {
198 Key::CtrlC => vec![Effect::Quit], 238 Key::CtrlC => vec![Effect::Quit],
199 Key::Esc | Key::Char('q') => { 239 Key::Esc | Key::Char('q') => {
@@ -230,6 +270,147 @@ impl ClientCore {
230 effects 270 effects
231 } 271 }
232 272
273 fn handle_roulette_key(&mut self, key: Key) -> Vec<Effect> {
274 match key {
275 Key::CtrlC => vec![Effect::Quit],
276 Key::Char('q') => {
277 self.game.end_game();
278 vec![
279 Effect::ClearRegion,
280 Effect::Print(vec!["Game exited.".to_string()]),
281 ]
282 }
283 Key::Esc => {
284 self.game.set_roulette_number_input("");
285 self.board_effects()
286 }
287 Key::Char(' ') => self.spin_roulette(),
288 Key::Char('r') => self.add_roulette_side(RouletteSide::Red),
289 Key::Char('b') => self.add_roulette_side(RouletteSide::Black),
290 Key::Char('e') => self.add_roulette_side(RouletteSide::Even),
291 Key::Char('o') => self.add_roulette_side(RouletteSide::Odd),
292 Key::Char('c') => {
293 self.game.set_roulette_bets(&Bets::default());
294 self.game.set_roulette_number_input("");
295 self.game.set_roulette_status("place");
296 self.board_effects()
297 }
298 Key::Char('+') | Key::Char('=') => {
299 let chip = self.game.roulette_chip().saturating_add(10);
300 self.game.set_roulette_chip(chip);
301 self.board_effects()
302 }
303 Key::Char('-') => {
304 let chip = self.game.roulette_chip().saturating_sub(10).max(10);
305 self.game.set_roulette_chip(chip);
306 self.board_effects()
307 }
308 Key::Char(c) if c.is_ascii_digit() => {
309 let mut input = self.roulette_input();
310 if input.len() < 2 {
311 input.push(c);
312 self.game.set_roulette_number_input(&input);
313 }
314 self.board_effects()
315 }
316 Key::Backspace => {
317 let mut input = self.roulette_input();
318 input.pop();
319 self.game.set_roulette_number_input(&input);
320 self.board_effects()
321 }
322 Key::Enter => {
323 let input = self.roulette_input();
324 if !input.is_empty() {
325 match input.parse::<u8>() {
326 Ok(number) if number <= 36 => {
327 self.add_roulette_number(number);
328 self.game.set_roulette_number_input("");
329 }
330 _ => {
331 self.game.set_roulette_number_input("");
332 self.game.set_error("Number must be 0-36");
333 }
334 }
335 }
336 self.board_effects()
337 }
338 _ => Vec::new(),
339 }
340 }
341
342 fn add_roulette_side(&mut self, side: RouletteSide) -> Vec<Effect> {
343 let chip = self.game.roulette_chip();
344 let mut bets = self.game.roulette_bets();
345 match side {
346 RouletteSide::Red => bets.red = bets.red.saturating_add(chip),
347 RouletteSide::Black => bets.black = bets.black.saturating_add(chip),
348 RouletteSide::Even => bets.even = bets.even.saturating_add(chip),
349 RouletteSide::Odd => bets.odd = bets.odd.saturating_add(chip),
350 }
351 self.place_roulette_bets(bets)
352 }
353
354 fn add_roulette_number(&mut self, number: u8) -> Vec<Effect> {
355 let chip = self.game.roulette_chip();
356 let mut bets = self.game.roulette_bets();
357 if let Some(existing) = bets.numbers.iter_mut().find(|bet| bet.number == number) {
358 existing.amount = existing.amount.saturating_add(chip);
359 } else {
360 bets.numbers.push(NumberBet {
361 number,
362 amount: chip,
363 });
364 }
365 self.place_roulette_bets(bets)
366 }
367
368 fn place_roulette_bets(&mut self, bets: Bets) -> Vec<Effect> {
369 if let Some(balance) = self.balance {
370 if bets.total() > balance.max(0) as u64 {
371 self.game.set_error("Insufficient balance");
372 return self.board_effects();
373 }
374 }
375
376 self.game.set_roulette_bets(&bets);
377 self.game.set_roulette_status("place");
378 self.board_effects()
379 }
380
381 fn spin_roulette(&mut self) -> Vec<Effect> {
382 let bets = self.game.roulette_bets();
383 if bets.is_empty() {
384 self.game.set_error("Place at least one bet");
385 return self.board_effects();
386 }
387 if let Some(balance) = self.balance {
388 if bets.total() > balance.max(0) as u64 {
389 self.game.set_error("Insufficient balance");
390 return self.board_effects();
391 }
392 }
393 let Some(request) = self.game.spin_request() else {
394 return Vec::new();
395 };
396 if self.game.begin_spin().is_err() {
397 return Vec::new();
398 }
399 self.pending = Some(Pending::RouletteSpin);
400
401 let mut effects = self.board_effects();
402 effects.push(Effect::SendLine(request));
403 effects
404 }
405
406 fn roulette_input(&self) -> String {
407 self.game
408 .get_current_state()
409 .and_then(|state| state.data.get("number_input"))
410 .cloned()
411 .unwrap_or_default()
412 }
413
233 fn submit(&mut self) -> Vec<Effect> { 414 fn submit(&mut self) -> Vec<Effect> {
234 let line = self.input.trim().to_string(); 415 let line = self.input.trim().to_string();
235 self.input.clear(); 416 self.input.clear();