From f07221b4e1d8aa464a44fe566884ccc0565406e9 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 16:17:19 +0100 Subject: v3.2.0: straight and split bets anywhere on the felt; add history command --- src/client/games/response.rs | 20 ++++++ src/client/games/roulette.rs | 141 +++++++++++++++++++++++++++++++------------ 2 files changed, 123 insertions(+), 38 deletions(-) (limited to 'src/client/games') diff --git a/src/client/games/response.rs b/src/client/games/response.rs index 88df459..060ad44 100644 --- a/src/client/games/response.rs +++ b/src/client/games/response.rs @@ -184,3 +184,23 @@ pub fn parse_crash_response(response: &str) -> Result Result, String> { + let response = response.trim(); + if response.is_empty() { + return Err("No response from server".to_string()); + } + if let Some(message) = response.strip_prefix("ERROR") { + let message = message.trim(); + return Err(if message.is_empty() { + "Server error".to_string() + } else { + message.to_string() + }); + } + + let raw = response + .strip_prefix("HISTORY ") + .ok_or_else(|| format!("Unexpected server response: {response}"))?; + serde_json::from_str(raw).map_err(|error| format!("Invalid history: {error}")) +} diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs index 96f8791..b3a8d9c 100644 --- a/src/client/games/roulette.rs +++ b/src/client/games/roulette.rs @@ -30,9 +30,16 @@ pub enum Direction { /// Where the betting cursor is. /// -/// Over the numbers the cursor sits on a *lattice point*, the way a chip sits -/// between numbers on a real felt: an inner point covers four numbers (corner), -/// an edge point covers two (split), and an outer corner is a straight number. +/// Over the numbers the cursor walks a grid twice as fine as the felt itself: +/// odd coordinates are the centre of a number cell and even coordinates are the +/// line between cells, exactly where a chip sits on a real felt. +/// +/// ```text +/// odd,odd the number straight +/// even,odd line between columns split of the two numbers +/// odd,even line between rows split of the two numbers +/// even,even corner of four cells corner +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Cursor { Zero, @@ -42,10 +49,14 @@ pub enum Cursor { Outside(usize), } +/// Fine-grid bounds: 12 columns and 3 rows of cells, plus the lines between. +const FELT_X_MAX: usize = 2 * NUM_COLUMNS - 1; +const FELT_Y_MAX: usize = 2 * 3 - 1; + impl Default for Cursor { fn default() -> Self { - // Bottom-left number (1). - Cursor::Lattice { x: 0, y: 3 } + // Centre of the bottom-left number (1). + Cursor::Lattice { x: 1, y: 5 } } } @@ -55,7 +66,9 @@ impl Cursor { match parts.as_slice() { ["zero"] => Cursor::Zero, ["lattice", x, y] => match (x.parse::(), y.parse::()) { - (Ok(x), Ok(y)) if x <= NUM_COLUMNS && y <= 3 => Cursor::Lattice { x, y }, + (Ok(x), Ok(y)) if (1..=FELT_X_MAX).contains(&x) && (1..=FELT_Y_MAX).contains(&y) => { + Cursor::Lattice { x, y } + } _ => Cursor::default(), }, ["dozen", index] => parse_index(index).map(Cursor::Dozen).unwrap_or_default(), @@ -81,7 +94,7 @@ impl Cursor { pub fn target(&self) -> Option { match self { Cursor::Zero => Some(BetTarget::Straight { number: 0 }), - Cursor::Lattice { x, y } => lattice_target(*x, *y), + Cursor::Lattice { x, y } => felt_target(*x, *y), Cursor::Dozen(index) => Some(BetTarget::Dozen { index: *index as u8, }), @@ -103,27 +116,29 @@ impl Cursor { pub fn moved(&self, direction: Direction) -> Cursor { match (*self, direction) { - (Cursor::Zero, Direction::Down) => Cursor::Lattice { x: 0, y: 0 }, + (Cursor::Zero, Direction::Down) => Cursor::Lattice { x: 1, y: 1 }, (Cursor::Zero, _) => Cursor::Zero, (Cursor::Lattice { x, y }, Direction::Left) => Cursor::Lattice { - x: x.saturating_sub(1), + x: if x > 1 { x - 1 } else { x }, y, }, (Cursor::Lattice { x, y }, Direction::Right) => Cursor::Lattice { - x: (x + 1).min(NUM_COLUMNS), + x: (x + 1).min(FELT_X_MAX), y, }, - (Cursor::Lattice { x: _, y: 0 }, Direction::Up) => Cursor::Zero, + (Cursor::Lattice { x: _, y: 1 }, Direction::Up) => Cursor::Zero, (Cursor::Lattice { x, y }, Direction::Up) => Cursor::Lattice { x, y: y - 1 }, - (Cursor::Lattice { x, y: 3 }, Direction::Down) => Cursor::Dozen((x / 4).min(2)), + (Cursor::Lattice { x, y: FELT_Y_MAX }, Direction::Down) => { + Cursor::Dozen((x.saturating_sub(1) / 8).min(2)) + } (Cursor::Lattice { x, y }, Direction::Down) => Cursor::Lattice { x, y: y + 1 }, (Cursor::Dozen(index), Direction::Left) => Cursor::Dozen(index.saturating_sub(1)), (Cursor::Dozen(index), Direction::Right) => Cursor::Dozen((index + 1).min(2)), (Cursor::Dozen(index), Direction::Up) => Cursor::Lattice { - x: 4 * index + 1, - y: 3, + x: 8 * index + 1, + y: FELT_Y_MAX, }, (Cursor::Dozen(index), Direction::Down) => Cursor::Column(index), @@ -146,30 +161,69 @@ fn parse_index(raw: &str) -> Option { raw.parse::().ok().filter(|index| *index < 3) } -/// Numbers under a lattice point: 1 (outer corner), 2 (edge) or 4 (inner). -fn lattice_target(x: usize, y: usize) -> Option { - let cell = |column: isize, row: isize| -> Option { - if !(0..NUM_COLUMNS as isize).contains(&column) || !(0..3isize).contains(&row) { - return None; +/// Number at a felt cell (column 0-11, row 0-2 counted from the top). +fn cell_number(column: usize, row: usize) -> Option { + if column >= NUM_COLUMNS || row >= 3 { + return None; + } + Some((3 * column + (3 - row)) as u8) +} + +/// Bet under a point of the fine grid. +fn felt_target(x: usize, y: usize) -> Option { + if !(1..=FELT_X_MAX).contains(&x) || !(1..=FELT_Y_MAX).contains(&y) { + return None; + } + + let column = (x - 1) / 2; + let row = (y - 1) / 2; + + match (x % 2, y % 2) { + // Centre of a cell: straight. + (1, 1) => cell_number(column, row).map(|number| BetTarget::Straight { number }), + // Vertical line: the two cells left and right of it. + (0, 1) => { + let left = cell_number(x / 2 - 1, row); + let right = cell_number(x / 2, row); + match (left, right) { + (Some(a), Some(b)) => Some(BetTarget::Split { + a: a.min(b), + b: a.max(b), + }), + _ => None, + } + } + // Horizontal line: the two cells above and below it. + (1, 0) => { + let above = cell_number(column, y / 2 - 1); + let below = cell_number(column, y / 2); + match (above, below) { + (Some(a), Some(b)) => Some(BetTarget::Split { + a: a.min(b), + b: a.max(b), + }), + _ => None, + } + } + // Crossing: the four cells around it. + _ => { + let mut numbers: Vec = [ + (x / 2 - 1, y / 2 - 1), + (x / 2, y / 2 - 1), + (x / 2 - 1, y / 2), + (x / 2, y / 2), + ] + .into_iter() + .filter_map(|(column, row)| cell_number(column, row)) + .collect(); + numbers.sort_unstable(); + match numbers.as_slice() { + [a, b, c, d] => Some(BetTarget::Corner { + numbers: [*a, *b, *c, *d], + }), + _ => None, + } } - Some((3 * column + (3 - row)) as u8) - }; - - let x = x as isize; - let y = y as isize; - let mut numbers: Vec = [(x - 1, y - 1), (x, y - 1), (x - 1, y), (x, y)] - .into_iter() - .filter_map(|(column, row)| cell(column, row)) - .collect(); - numbers.sort_unstable(); - - match numbers.as_slice() { - [number] => Some(BetTarget::Straight { number: *number }), - [a, b] => Some(BetTarget::Split { a: *a, b: *b }), - [a, b, c, d] => Some(BetTarget::Corner { - numbers: [*a, *b, *c, *d], - }), - _ => None, } } @@ -317,7 +371,18 @@ impl RouletteGame { } } Some("spinning") => "Spinning...".to_string(), - _ => "Arrows move, ENTER places the chip, SPACE spins.".to_string(), + _ => { + let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); + match Self::cursor(state).target() { + Some(target) => format!( + "Bet: {} · pays {} · ENTER places {} Kr", + target.label(), + target.return_label(), + chip + ), + None => "Arrows move, ENTER places the chip, SPACE spins.".to_string(), + } + } } } } -- cgit v1.2.3