From d9f1fe513e6f7696809d3a9b4d44ca485fe99c97 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 17:18:34 +0100 Subject: v2.1.0: add European roulette with split bets and wheel animation --- src/roulette.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/roulette.rs (limited to 'src/roulette.rs') diff --git a/src/roulette.rs b/src/roulette.rs new file mode 100644 index 0000000..b59f92e --- /dev/null +++ b/src/roulette.rs @@ -0,0 +1,132 @@ +//! European roulette rules shared by the server and client. + +use serde::{Deserialize, Serialize}; + +/// European wheel clockwise from zero. +pub const WHEEL: [u8; 37] = [ + 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, + 24, 16, 33, 1, 20, 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26, +]; + +pub const RED_NUMBERS: [u8; 18] = [ + 1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36, +]; + +/// Total return on a winning straight (35:1 plus the stake). +pub const STRAIGHT_RETURN: u64 = 36; +/// Total return on a winning outside bet (1:1 plus the stake). +pub const EVEN_MONEY_RETURN: u64 = 2; + +pub fn is_red(number: u8) -> bool { + RED_NUMBERS.contains(&number) +} + +pub fn is_black(number: u8) -> bool { + number != 0 && !is_red(number) +} + +pub fn color(number: u8) -> &'static str { + if number == 0 { + "green" + } else if is_red(number) { + "red" + } else { + "black" + } +} + +pub fn is_even(number: u8) -> bool { + number != 0 && number % 2 == 0 +} + +pub fn is_odd(number: u8) -> bool { + number != 0 && number % 2 == 1 +} + +pub fn rtp() -> f64 { + 36.0 / 37.0 +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NumberBet { + pub number: u8, + pub amount: u64, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Bets { + #[serde(default)] + pub red: u64, + #[serde(default)] + pub black: u64, + #[serde(default)] + pub even: u64, + #[serde(default)] + pub odd: u64, + #[serde(default)] + pub numbers: Vec, +} + +impl Bets { + pub fn total(&self) -> u64 { + let mut total = self + .red + .saturating_add(self.black) + .saturating_add(self.even) + .saturating_add(self.odd); + for bet in &self.numbers { + total = total.saturating_add(bet.amount); + } + total + } + + pub fn is_empty(&self) -> bool { + self.total() == 0 + } + + /// Total returned for a winning number, including returned stakes. + pub fn winnings(&self, number: u8) -> u64 { + let mut winnings = 0u64; + + if number != 0 { + if is_red(number) { + winnings = winnings.saturating_add(self.red.saturating_mul(EVEN_MONEY_RETURN)); + } else { + winnings = + winnings.saturating_add(self.black.saturating_mul(EVEN_MONEY_RETURN)); + } + if is_even(number) { + winnings = winnings.saturating_add(self.even.saturating_mul(EVEN_MONEY_RETURN)); + } else { + winnings = winnings.saturating_add(self.odd.saturating_mul(EVEN_MONEY_RETURN)); + } + } + + for bet in &self.numbers { + if bet.number == number { + winnings = winnings + .saturating_add(bet.amount.saturating_mul(STRAIGHT_RETURN)); + } + } + + winnings + } + + pub fn validate(&self, balance: u64) -> Result<(), String> { + if self.is_empty() { + return Err("Place at least one bet".to_string()); + } + if self.total() > balance { + return Err("Insufficient balance".to_string()); + } + for bet in &self.numbers { + if bet.number > 36 { + return Err(format!("Invalid number: {}", bet.number)); + } + if bet.amount == 0 { + return Err("A bet amount must be positive".to_string()); + } + } + Ok(()) + } +} -- cgit v1.2.3