diff options
Diffstat (limited to '')
| -rw-r--r-- | src/bin/client/repl.rs | 13 | ||||
| -rw-r--r-- | src/client/core.rs | 109 | ||||
| -rw-r--r-- | src/client/games/roulette.rs | 141 | ||||
| -rw-r--r-- | src/client/mod.rs | 2 | ||||
| -rw-r--r-- | src/client/tui.rs | 18 | ||||
| -rw-r--r-- | src/client/view.rs | 103 |
6 files changed, 248 insertions, 138 deletions
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index 1debff1..c13fc58 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -209,13 +209,14 @@ impl ClientState { | |||
| 209 | println!("{}", rendered); | 209 | println!("{}", rendered); |
| 210 | } | 210 | } |
| 211 | } | 211 | } |
| 212 | Effect::ReplaceRegion(lines) => self.draw_region(&lines), | 212 | Effect::Render(view) => { |
| 213 | Effect::AnimateRoulette { | 213 | let lines = zagan_bet::client::tui::render_view(&view); |
| 214 | winning, | 214 | self.draw_region(&lines); |
| 215 | final_lines, | 215 | } |
| 216 | } => { | 216 | Effect::AnimateRoulette { winning, view } => { |
| 217 | self.animate_wheel(winning); | 217 | self.animate_wheel(winning); |
| 218 | self.draw_region(&final_lines); | 218 | let lines = zagan_bet::client::tui::render_view(&view); |
| 219 | self.draw_region(&lines); | ||
| 219 | while crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) { | 220 | while crossterm::event::poll(std::time::Duration::ZERO).unwrap_or(false) { |
| 220 | let _ = crossterm::event::read(); | 221 | let _ = crossterm::event::read(); |
| 221 | } | 222 | } |
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> { |
diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs index b3a8d9c..ec9573c 100644 --- a/src/client/games/roulette.rs +++ b/src/client/games/roulette.rs | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | use super::traits::{Game, GameState}; | 1 | use super::traits::{Game, GameState}; |
| 2 | use crate::client::view::{RoulettePhase, RouletteView}; | ||
| 2 | use crate::roulette::{BetTarget, Bets, Outside, is_even, is_odd}; | 3 | use crate::roulette::{BetTarget, Bets, Outside, is_even, is_odd}; |
| 3 | 4 | ||
| 4 | pub const ROULETTE_BOARD_LINES: usize = 12; | 5 | pub const ROULETTE_BOARD_LINES: usize = 12; |
| @@ -232,22 +233,6 @@ impl RouletteGame { | |||
| 232 | Self | 233 | Self |
| 233 | } | 234 | } |
| 234 | 235 | ||
| 235 | fn bets(state: &GameState) -> Bets { | ||
| 236 | state | ||
| 237 | .data | ||
| 238 | .get("bets") | ||
| 239 | .and_then(|raw| Bets::from_json(raw).ok()) | ||
| 240 | .unwrap_or_default() | ||
| 241 | } | ||
| 242 | |||
| 243 | fn cursor(state: &GameState) -> Cursor { | ||
| 244 | state | ||
| 245 | .data | ||
| 246 | .get("cursor") | ||
| 247 | .map(|raw| Cursor::parse(raw)) | ||
| 248 | .unwrap_or_default() | ||
| 249 | } | ||
| 250 | |||
| 251 | fn zero_cell(cursor: Cursor) -> String { | 236 | fn zero_cell(cursor: Cursor) -> String { |
| 252 | let cell = if cursor == Cursor::Zero { | 237 | let cell = if cursor == Cursor::Zero { |
| 253 | "[ 0 ]".to_string() | 238 | "[ 0 ]".to_string() |
| @@ -329,92 +314,82 @@ impl RouletteGame { | |||
| 329 | format!("Bets: {list} Total: {} Kr", bets.total()) | 314 | format!("Bets: {list} Total: {} Kr", bets.total()) |
| 330 | } | 315 | } |
| 331 | 316 | ||
| 332 | fn status_line(state: &GameState) -> String { | 317 | fn status_line(view: &RouletteView) -> String { |
| 333 | if let Some(error) = state.data.get("error") { | 318 | if let Some(error) = &view.error { |
| 334 | if !error.is_empty() { | 319 | return format!("ERROR: {error}"); |
| 335 | return format!("ERROR: {error}"); | ||
| 336 | } | ||
| 337 | } | 320 | } |
| 338 | 321 | ||
| 339 | match state.data.get("status").map(String::as_str) { | 322 | match view.phase { |
| 340 | Some("result") => { | 323 | RoulettePhase::Result => { |
| 341 | let number = state.data.get("number").map(String::as_str).unwrap_or("?"); | 324 | let number = view.number.unwrap_or(0); |
| 342 | let color = state | 325 | let color = view |
| 343 | .data | 326 | .color |
| 344 | .get("color") | 327 | .as_deref() |
| 345 | .map(|value| value.to_uppercase()) | 328 | .map(str::to_uppercase) |
| 346 | .unwrap_or_default(); | 329 | .unwrap_or_default(); |
| 347 | let parity = number | 330 | let parity = if is_even(number) { |
| 348 | .parse::<u8>() | 331 | " EVEN" |
| 349 | .ok() | 332 | } else if is_odd(number) { |
| 350 | .map(|number| { | 333 | " ODD" |
| 351 | if is_even(number) { | ||
| 352 | " EVEN" | ||
| 353 | } else if is_odd(number) { | ||
| 354 | " ODD" | ||
| 355 | } else { | ||
| 356 | "" | ||
| 357 | } | ||
| 358 | }) | ||
| 359 | .unwrap_or(""); | ||
| 360 | let winnings = state | ||
| 361 | .data | ||
| 362 | .get("winnings") | ||
| 363 | .map(String::as_str) | ||
| 364 | .unwrap_or("0"); | ||
| 365 | let stake = state.bet_amount; | ||
| 366 | if winnings == "0" { | ||
| 367 | format!("No win at {number} {color}{parity}") | ||
| 368 | } else { | 334 | } else { |
| 369 | let profit = winnings.parse::<u64>().unwrap_or(0).saturating_sub(stake); | 335 | "" |
| 370 | format!("Won {winnings} Kr at {number} {color}{parity} (+{profit})") | 336 | }; |
| 371 | } | 337 | match view.winnings.unwrap_or(0) { |
| 372 | } | 338 | 0 => format!("No win at {number} {color}{parity}"), |
| 373 | Some("spinning") => "Spinning...".to_string(), | 339 | winnings => { |
| 374 | _ => { | 340 | let profit = winnings.saturating_sub(view.bet_amount); |
| 375 | let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); | 341 | format!("Won {winnings} Kr at {number} {color}{parity} (+{profit})") |
| 376 | match Self::cursor(state).target() { | 342 | } |
| 377 | Some(target) => format!( | ||
| 378 | "Bet: {} · pays {} · ENTER places {} Kr", | ||
| 379 | target.label(), | ||
| 380 | target.return_label(), | ||
| 381 | chip | ||
| 382 | ), | ||
| 383 | None => "Arrows move, ENTER places the chip, SPACE spins.".to_string(), | ||
| 384 | } | 343 | } |
| 385 | } | 344 | } |
| 345 | RoulettePhase::Spinning => "Spinning...".to_string(), | ||
| 346 | RoulettePhase::Placing => match view.cursor.target() { | ||
| 347 | Some(target) => format!( | ||
| 348 | "Bet: {} · pays {} · ENTER places {} Kr", | ||
| 349 | target.label(), | ||
| 350 | target.return_label(), | ||
| 351 | view.chip | ||
| 352 | ), | ||
| 353 | None => "Arrows move, ENTER places the chip, SPACE spins.".to_string(), | ||
| 354 | }, | ||
| 386 | } | 355 | } |
| 387 | } | 356 | } |
| 388 | } | ||
| 389 | |||
| 390 | impl Game for RouletteGame { | ||
| 391 | fn get_name(&self) -> &'static str { | ||
| 392 | "Roulette" | ||
| 393 | } | ||
| 394 | 357 | ||
| 395 | fn render(&self, state: &GameState) -> String { | 358 | /// Render a roulette view to terminal lines. The GUI draws the same view |
| 396 | let bets = Self::bets(state); | 359 | /// with a painter instead. |
| 397 | let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); | 360 | pub fn render_view(view: &RouletteView) -> String { |
| 398 | let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); | 361 | let covered = view.cursor.numbers(); |
| 399 | let cursor = Self::cursor(state); | 362 | let balance = view |
| 400 | let covered = cursor.numbers(); | 363 | .balance |
| 364 | .map(|balance| balance.to_string()) | ||
| 365 | .unwrap_or_else(|| "?".to_string()); | ||
| 401 | 366 | ||
| 402 | let lines = [ | 367 | let lines = [ |
| 403 | "=== ROULETTE ===".to_string(), | 368 | "=== ROULETTE ===".to_string(), |
| 404 | format!("Balance: {balance} Kr Chip: {chip} Kr"), | 369 | format!("Balance: {balance} Kr Chip: {} Kr", view.chip), |
| 405 | Self::zero_cell(cursor), | 370 | Self::zero_cell(view.cursor), |
| 406 | Self::number_row(0, &covered), | 371 | Self::number_row(0, &covered), |
| 407 | Self::number_row(1, &covered), | 372 | Self::number_row(1, &covered), |
| 408 | Self::number_row(2, &covered), | 373 | Self::number_row(2, &covered), |
| 409 | Self::dozens_row(cursor), | 374 | Self::dozens_row(view.cursor), |
| 410 | Self::columns_row(cursor), | 375 | Self::columns_row(view.cursor), |
| 411 | Self::outside_row(cursor), | 376 | Self::outside_row(view.cursor), |
| 412 | Self::bets_line(&bets), | 377 | Self::bets_line(&view.bets), |
| 413 | Self::status_line(state), | 378 | Self::status_line(view), |
| 414 | "[ARROWS] Move [ENTER] Bet [C]lear [+/-] Chip [SPACE] Spin [Q] Quit".to_string(), | 379 | "[ARROWS] Move [ENTER] Bet [C]lear [+/-] Chip [SPACE] Spin [Q] Quit".to_string(), |
| 415 | ]; | 380 | ]; |
| 416 | lines.join("\r\n") | 381 | lines.join("\r\n") |
| 417 | } | 382 | } |
| 383 | } | ||
| 384 | |||
| 385 | impl Game for RouletteGame { | ||
| 386 | fn get_name(&self) -> &'static str { | ||
| 387 | "Roulette" | ||
| 388 | } | ||
| 389 | |||
| 390 | fn render(&self, state: &GameState) -> String { | ||
| 391 | Self::render_view(&RouletteView::from_state(state)) | ||
| 392 | } | ||
| 418 | 393 | ||
| 419 | fn render_lines(&self, _state: &GameState) -> usize { | 394 | fn render_lines(&self, _state: &GameState) -> usize { |
| 420 | ROULETTE_BOARD_LINES | 395 | ROULETTE_BOARD_LINES |
diff --git a/src/client/mod.rs b/src/client/mod.rs index 6841be3..a79a460 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs | |||
| @@ -2,4 +2,6 @@ pub mod core; | |||
| 2 | pub mod database; | 2 | pub mod database; |
| 3 | pub mod games; | 3 | pub mod games; |
| 4 | pub mod paths; | 4 | pub mod paths; |
| 5 | pub mod tui; | ||
| 5 | pub mod ui; | 6 | pub mod ui; |
| 7 | pub mod view; | ||
diff --git a/src/client/tui.rs b/src/client/tui.rs new file mode 100644 index 0000000..9a4e4da --- /dev/null +++ b/src/client/tui.rs | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | //! Text rendering for the terminal adapter. | ||
| 2 | //! | ||
| 3 | //! This is the TUI's half of the `View` boundary: the GUI renders the same | ||
| 4 | //! data with a painter instead of strings. | ||
| 5 | |||
| 6 | use crate::client::games::roulette::RouletteGame; | ||
| 7 | use crate::client::view::View; | ||
| 8 | |||
| 9 | /// Render any view to terminal lines. | ||
| 10 | pub fn render_view(view: &View) -> Vec<String> { | ||
| 11 | match view { | ||
| 12 | View::Roulette(roulette) => RouletteGame::render_view(roulette) | ||
| 13 | .lines() | ||
| 14 | .map(str::to_string) | ||
| 15 | .collect(), | ||
| 16 | View::Text(lines) => lines.clone(), | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/src/client/view.rs b/src/client/view.rs new file mode 100644 index 0000000..3683ac2 --- /dev/null +++ b/src/client/view.rs | |||
| @@ -0,0 +1,103 @@ | |||
| 1 | //! What a frontend draws and what it can ask the core to do. | ||
| 2 | //! | ||
| 3 | //! The core owns state and navigation; adapters map input to `Command` and | ||
| 4 | //! render `View`. Terminal text lives in [`crate::client::tui`], not here. | ||
| 5 | |||
| 6 | use crate::client::games::roulette::{Cursor, Direction}; | ||
| 7 | use crate::client::games::traits::GameState; | ||
| 8 | use crate::roulette::{BetTarget, Bets}; | ||
| 9 | |||
| 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 11 | pub enum RoulettePhase { | ||
| 12 | Placing, | ||
| 13 | Spinning, | ||
| 14 | Result, | ||
| 15 | } | ||
| 16 | |||
| 17 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 18 | pub struct RouletteView { | ||
| 19 | pub bet_amount: u64, | ||
| 20 | pub balance: Option<i64>, | ||
| 21 | pub chip: u64, | ||
| 22 | pub bets: Bets, | ||
| 23 | pub cursor: Cursor, | ||
| 24 | pub phase: RoulettePhase, | ||
| 25 | pub number: Option<u8>, | ||
| 26 | pub color: Option<String>, | ||
| 27 | pub winnings: Option<u64>, | ||
| 28 | pub error: Option<String>, | ||
| 29 | } | ||
| 30 | |||
| 31 | impl RouletteView { | ||
| 32 | pub fn from_state(state: &GameState) -> Self { | ||
| 33 | let bets = state | ||
| 34 | .data | ||
| 35 | .get("bets") | ||
| 36 | .and_then(|raw| Bets::from_json(raw).ok()) | ||
| 37 | .unwrap_or_default(); | ||
| 38 | let chip = state | ||
| 39 | .data | ||
| 40 | .get("chip") | ||
| 41 | .and_then(|value| value.parse().ok()) | ||
| 42 | .unwrap_or(10); | ||
| 43 | let cursor = state | ||
| 44 | .data | ||
| 45 | .get("cursor") | ||
| 46 | .map(|raw| Cursor::parse(raw)) | ||
| 47 | .unwrap_or_default(); | ||
| 48 | let phase = match state.data.get("status").map(String::as_str) { | ||
| 49 | Some("spinning") => RoulettePhase::Spinning, | ||
| 50 | Some("result") => RoulettePhase::Result, | ||
| 51 | _ => RoulettePhase::Placing, | ||
| 52 | }; | ||
| 53 | let number = state.data.get("number").and_then(|value| value.parse().ok()); | ||
| 54 | let color = state.data.get("color").cloned(); | ||
| 55 | let winnings = state | ||
| 56 | .data | ||
| 57 | .get("winnings") | ||
| 58 | .and_then(|value| value.parse().ok()); | ||
| 59 | let balance = state | ||
| 60 | .data | ||
| 61 | .get("balance") | ||
| 62 | .and_then(|value| value.parse().ok()); | ||
| 63 | let error = state | ||
| 64 | .data | ||
| 65 | .get("error") | ||
| 66 | .cloned() | ||
| 67 | .filter(|message| !message.is_empty()); | ||
| 68 | |||
| 69 | Self { | ||
| 70 | bet_amount: state.bet_amount, | ||
| 71 | balance, | ||
| 72 | chip, | ||
| 73 | bets, | ||
| 74 | cursor, | ||
| 75 | phase, | ||
| 76 | number, | ||
| 77 | color, | ||
| 78 | winnings, | ||
| 79 | error, | ||
| 80 | } | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | /// Everything a frontend can draw. `Text` is the bridge for games that have | ||
| 85 | /// not been ported to a structured view yet. | ||
| 86 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 87 | pub enum View { | ||
| 88 | Roulette(RouletteView), | ||
| 89 | Text(Vec<String>), | ||
| 90 | } | ||
| 91 | |||
| 92 | /// Semantic input. Frontends turn keys and clicks into these. | ||
| 93 | #[derive(Debug, Clone, PartialEq, Eq)] | ||
| 94 | pub enum Command { | ||
| 95 | MoveCursor(Direction), | ||
| 96 | PlaceBet, | ||
| 97 | PlaceBetAt(BetTarget), | ||
| 98 | ClearBets, | ||
| 99 | SetChip(u64), | ||
| 100 | AdjustChip(i64), | ||
| 101 | Spin, | ||
| 102 | Leave, | ||
| 103 | } | ||
