diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-01 17:58:03 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-01 17:58:03 +0100 |
| commit | 46202ee2e398ffd49febe6684f5c6a8648da600d (patch) | |
| tree | 00631532b6b4805ddc8a728246787651dd7185f2 /src/client/core.rs | |
| parent | docs: document DOWNLOAD_URL and the XDG reset path (diff) | |
| download | zagan-bet-46202ee2e398ffd49febe6684f5c6a8648da600d.tar.gz zagan-bet-46202ee2e398ffd49febe6684f5c6a8648da600d.zip | |
v4.0.1: introduce Command/View and port roulette to the shared boundary
Diffstat (limited to 'src/client/core.rs')
| -rw-r--r-- | src/client/core.rs | 109 |
1 files changed, 60 insertions, 49 deletions
diff --git a/src/client/core.rs b/src/client/core.rs index 0b2e683..7e43fb7 100644 --- a/src/client/core.rs +++ b/src/client/core.rs | |||
| @@ -4,13 +4,14 @@ use crate::client::games::response::{ | |||
| 4 | parse_roulette_response, parse_spin_response, | 4 | parse_roulette_response, parse_spin_response, |
| 5 | }; | 5 | }; |
| 6 | use crate::client::games::roulette::Direction; | 6 | use crate::client::games::roulette::Direction; |
| 7 | use crate::client::view::{Command, RouletteView, View}; | ||
| 7 | use crate::client::ui::{ | 8 | use crate::client::ui::{ |
| 8 | command_names, completion_lines, display_width, help_lines, history_lines, | 9 | command_names, completion_lines, display_width, help_lines, history_lines, |
| 9 | input_cursor_column, parse_bet_arg, record_history, | 10 | input_cursor_column, parse_bet_arg, record_history, |
| 10 | }; | 11 | }; |
| 11 | use crate::crash::{CrashState, multiplier_at}; | 12 | use crate::crash::{CrashState, multiplier_at}; |
| 12 | use crate::history::MAX_HISTORY; | 13 | use crate::history::MAX_HISTORY; |
| 13 | use crate::roulette::Bets; | 14 | use crate::roulette::{BetTarget, Bets}; |
| 14 | use std::time::{Duration, Instant}; | 15 | use std::time::{Duration, Instant}; |
| 15 | 16 | ||
| 16 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| @@ -41,13 +42,10 @@ pub enum InteractiveCommand { | |||
| 41 | pub enum Effect { | 42 | pub enum Effect { |
| 42 | /// Append lines to the scrolling transcript. | 43 | /// Append lines to the scrolling transcript. |
| 43 | Print(Vec<String>), | 44 | Print(Vec<String>), |
| 44 | /// Replace the game board region in place. | 45 | /// Draw the current view in the board region. |
| 45 | ReplaceRegion(Vec<String>), | 46 | Render(View), |
| 46 | /// Run the roulette wheel animation, then draw the final board lines. | 47 | /// Run the roulette wheel animation, then draw the final view. |
| 47 | AnimateRoulette { | 48 | AnimateRoulette { winning: u8, view: View }, |
| 48 | winning: u8, | ||
| 49 | final_lines: Vec<String>, | ||
| 50 | }, | ||
| 51 | /// Remove the game board region. | 49 | /// Remove the game board region. |
| 52 | ClearRegion, | 50 | ClearRegion, |
| 53 | /// Redraw the command prompt. `text` is the input without the `> ` prefix. | 51 | /// Redraw the command prompt. `text` is the input without the `> ` prefix. |
| @@ -215,11 +213,8 @@ impl ClientCore { | |||
| 215 | self.balance = Some(result.balance); | 213 | self.balance = Some(result.balance); |
| 216 | let winning = result.number; | 214 | let winning = result.number; |
| 217 | self.game.apply_roulette_result(result); | 215 | self.game.apply_roulette_result(result); |
| 218 | let final_lines = self.board_lines().unwrap_or_default(); | 216 | let view = self.view(); |
| 219 | vec![Effect::AnimateRoulette { | 217 | vec![Effect::AnimateRoulette { winning, view }] |
| 220 | winning, | ||
| 221 | final_lines, | ||
| 222 | }] | ||
| 223 | } | 218 | } |
| 224 | Err(message) => { | 219 | Err(message) => { |
| 225 | self.game.set_error(message); | 220 | self.game.set_error(message); |
| @@ -380,53 +375,62 @@ impl ClientCore { | |||
| 380 | } | 375 | } |
| 381 | 376 | ||
| 382 | fn handle_roulette_key(&mut self, key: Key) -> Vec<Effect> { | 377 | fn handle_roulette_key(&mut self, key: Key) -> Vec<Effect> { |
| 383 | let direction = match key { | ||
| 384 | Key::Up => Some(Direction::Up), | ||
| 385 | Key::Down => Some(Direction::Down), | ||
| 386 | Key::Left => Some(Direction::Left), | ||
| 387 | Key::Right => Some(Direction::Right), | ||
| 388 | _ => None, | ||
| 389 | }; | ||
| 390 | if let Some(direction) = direction { | ||
| 391 | let cursor = self.game.roulette_cursor().moved(direction); | ||
| 392 | self.game.set_roulette_cursor(&cursor); | ||
| 393 | return self.board_effects(); | ||
| 394 | } | ||
| 395 | |||
| 396 | match key { | 378 | match key { |
| 397 | Key::CtrlC => vec![Effect::Quit], | 379 | Key::CtrlC => vec![Effect::Quit], |
| 398 | Key::Char('q') => { | 380 | Key::Up => self.command(Command::MoveCursor(Direction::Up)), |
| 399 | self.game.end_game(); | 381 | Key::Down => self.command(Command::MoveCursor(Direction::Down)), |
| 400 | vec![ | 382 | Key::Left => self.command(Command::MoveCursor(Direction::Left)), |
| 401 | Effect::ClearRegion, | 383 | Key::Right => self.command(Command::MoveCursor(Direction::Right)), |
| 402 | Effect::Print(vec!["Game exited.".to_string()]), | 384 | Key::Enter => self.command(Command::PlaceBet), |
| 403 | ] | 385 | Key::Char(' ') => self.command(Command::Spin), |
| 386 | Key::Char('c') => self.command(Command::ClearBets), | ||
| 387 | Key::Char('+') | Key::Char('=') => self.command(Command::AdjustChip(10)), | ||
| 388 | Key::Char('-') => self.command(Command::AdjustChip(-10)), | ||
| 389 | Key::Char('q') => self.command(Command::Leave), | ||
| 390 | _ => Vec::new(), | ||
| 391 | } | ||
| 392 | } | ||
| 393 | |||
| 394 | /// Apply a semantic command. Both frontends drive the core through this. | ||
| 395 | pub fn command(&mut self, command: Command) -> Vec<Effect> { | ||
| 396 | match command { | ||
| 397 | Command::MoveCursor(direction) => { | ||
| 398 | let cursor = self.game.roulette_cursor().moved(direction); | ||
| 399 | self.game.set_roulette_cursor(&cursor); | ||
| 400 | self.board_effects() | ||
| 404 | } | 401 | } |
| 405 | Key::Char(' ') => self.spin_roulette(), | 402 | Command::PlaceBet => match self.game.roulette_cursor().target() { |
| 406 | Key::Char('c') => { | 403 | Some(target) => self.place_roulette_chip_on(target), |
| 404 | None => Vec::new(), | ||
| 405 | }, | ||
| 406 | Command::PlaceBetAt(target) => self.place_roulette_chip_on(target), | ||
| 407 | Command::ClearBets => { | ||
| 407 | self.game.set_roulette_bets(&Bets::default()); | 408 | self.game.set_roulette_bets(&Bets::default()); |
| 408 | self.game.set_roulette_status("place"); | 409 | self.game.set_roulette_status("place"); |
| 409 | self.board_effects() | 410 | self.board_effects() |
| 410 | } | 411 | } |
| 411 | Key::Char('+') | Key::Char('=') => { | 412 | Command::SetChip(chip) => { |
| 412 | let chip = self.game.roulette_chip().saturating_add(10); | 413 | self.game.set_roulette_chip(chip.max(10)); |
| 413 | self.game.set_roulette_chip(chip); | ||
| 414 | self.board_effects() | 414 | self.board_effects() |
| 415 | } | 415 | } |
| 416 | Key::Char('-') => { | 416 | Command::AdjustChip(delta) => { |
| 417 | let chip = self.game.roulette_chip().saturating_sub(10).max(10); | 417 | let chip = (self.game.roulette_chip() as i64 + delta).max(10) as u64; |
| 418 | self.game.set_roulette_chip(chip); | 418 | self.game.set_roulette_chip(chip); |
| 419 | self.board_effects() | 419 | self.board_effects() |
| 420 | } | 420 | } |
| 421 | Key::Enter => self.place_roulette_chip(), | 421 | Command::Spin => self.spin_roulette(), |
| 422 | _ => Vec::new(), | 422 | Command::Leave => { |
| 423 | self.crash_started = None; | ||
| 424 | self.game.end_game(); | ||
| 425 | vec![ | ||
| 426 | Effect::ClearRegion, | ||
| 427 | Effect::Print(vec!["Game exited.".to_string()]), | ||
| 428 | ] | ||
| 429 | } | ||
| 423 | } | 430 | } |
| 424 | } | 431 | } |
| 425 | 432 | ||
| 426 | fn place_roulette_chip(&mut self) -> Vec<Effect> { | 433 | fn place_roulette_chip_on(&mut self, target: BetTarget) -> Vec<Effect> { |
| 427 | let Some(target) = self.game.roulette_cursor().target() else { | ||
| 428 | return Vec::new(); | ||
| 429 | }; | ||
| 430 | let mut bets = self.game.roulette_bets(); | 434 | let mut bets = self.game.roulette_bets(); |
| 431 | bets.add(target, self.game.roulette_chip()); | 435 | bets.add(target, self.game.roulette_chip()); |
| 432 | self.place_roulette_bets(bets) | 436 | self.place_roulette_bets(bets) |
| @@ -950,11 +954,18 @@ impl ClientCore { | |||
| 950 | Some(game.render(state).lines().map(str::to_string).collect()) | 954 | Some(game.render(state).lines().map(str::to_string).collect()) |
| 951 | } | 955 | } |
| 952 | 956 | ||
| 957 | /// Snapshot of the current screen for a frontend to draw. | ||
| 958 | pub fn view(&self) -> View { | ||
| 959 | if let Some(state) = self.game.get_current_state() { | ||
| 960 | if state.game_type == "roulette" { | ||
| 961 | return View::Roulette(RouletteView::from_state(state)); | ||
| 962 | } | ||
| 963 | } | ||
| 964 | View::Text(self.board_lines().unwrap_or_default()) | ||
| 965 | } | ||
| 966 | |||
| 953 | fn board_effects(&self) -> Vec<Effect> { | 967 | fn board_effects(&self) -> Vec<Effect> { |
| 954 | self.board_lines() | 968 | vec![Effect::Render(self.view())] |
| 955 | .map(Effect::ReplaceRegion) | ||
| 956 | .into_iter() | ||
| 957 | .collect() | ||
| 958 | } | 969 | } |
| 959 | 970 | ||
| 960 | fn prompt_effects(&self) -> Vec<Effect> { | 971 | fn prompt_effects(&self) -> Vec<Effect> { |
