//! 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 split (17:1 plus the stake). pub const SPLIT_RETURN: u64 = 18; /// Total return on a winning corner (8:1 plus the stake). pub const CORNER_RETURN: u64 = 9; /// Total return on a winning dozen or column (2:1 plus the stake). pub const THIRD_RETURN: u64 = 3; /// 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, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Outside { Low, High, Even, Odd, Red, Black, } impl Outside { pub fn wins(self, number: u8) -> bool { match self { Outside::Low => (1..=18).contains(&number), Outside::High => (19..=36).contains(&number), Outside::Even => is_even(number), Outside::Odd => is_odd(number), Outside::Red => is_red(number), Outside::Black => is_black(number), } } pub fn label(self) -> &'static str { match self { Outside::Low => "1-18", Outside::High => "19-36", Outside::Even => "EVEN", Outside::Odd => "ODD", Outside::Red => "RED", Outside::Black => "BLACK", } } } /// One place on the felt where a chip can sit. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "bet", rename_all = "snake_case")] pub enum BetTarget { Straight { number: u8 }, Split { a: u8, b: u8 }, Corner { numbers: [u8; 4] }, /// 0 = 1-12, 1 = 13-24, 2 = 25-36. Dozen { index: u8 }, /// 0 = 1,4,...,34; 1 = 2,5,...,35; 2 = 3,6,...,36. Column { index: u8 }, Outside { kind: Outside }, } impl BetTarget { /// Numbers this target covers on the number grid (empty for outside bets). pub fn numbers(&self) -> Vec { match self { BetTarget::Straight { number } => vec![*number], BetTarget::Split { a, b } => vec![*a, *b], BetTarget::Corner { numbers } => numbers.to_vec(), _ => Vec::new(), } } /// Total returned per unit staked, or 0 when the bet loses. pub fn return_multiplier(&self, number: u8) -> u64 { match self { BetTarget::Straight { number: straight } => { u64::from(*straight == number) * STRAIGHT_RETURN } BetTarget::Split { a, b } => u64::from(*a == number || *b == number) * SPLIT_RETURN, BetTarget::Corner { numbers } => { u64::from(numbers.contains(&number)) * CORNER_RETURN } BetTarget::Dozen { index } => { let wins = number != 0 && (number - 1) / 12 == *index; u64::from(wins) * THIRD_RETURN } BetTarget::Column { index } => { let wins = match index { 0 => number % 3 == 1, 1 => number % 3 == 2, 2 => number != 0 && number % 3 == 0, _ => false, }; u64::from(wins) * THIRD_RETURN } BetTarget::Outside { kind } => u64::from(kind.wins(number)) * EVEN_MONEY_RETURN, } } /// Total return for a winning version of this bet. pub fn return_label(&self) -> &'static str { match self { BetTarget::Straight { .. } => "36x", BetTarget::Split { .. } => "18x", BetTarget::Corner { .. } => "9x", BetTarget::Dozen { .. } | BetTarget::Column { .. } => "3x", BetTarget::Outside { .. } => "2x", } } pub fn label(&self) -> String { match self { BetTarget::Straight { number } => number.to_string(), BetTarget::Split { a, b } => format!("{a}/{b}"), BetTarget::Corner { numbers } => numbers .iter() .map(u8::to_string) .collect::>() .join("/"), BetTarget::Dozen { index } => match index { 0 => "1st12".to_string(), 1 => "2nd12".to_string(), _ => "3rd12".to_string(), }, BetTarget::Column { index } => format!("col{}", index + 1), BetTarget::Outside { kind } => kind.label().to_string(), } } fn validates(&self) -> bool { match self { BetTarget::Straight { number } => *number <= 36, BetTarget::Split { a, b } => *a <= 36 && *b <= 36 && a != b, BetTarget::Corner { numbers } => { numbers.iter().all(|number| *number <= 36) && numbers .iter() .enumerate() .all(|(index, number)| !numbers[..index].contains(number)) } BetTarget::Dozen { index } | BetTarget::Column { index } => *index < 3, BetTarget::Outside { .. } => true, } } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Chip { pub target: BetTarget, pub amount: u64, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Bets { #[serde(default)] pub chips: Vec, } impl Bets { pub fn total(&self) -> u64 { self.chips .iter() .fold(0u64, |total, chip| total.saturating_add(chip.amount)) } pub fn is_empty(&self) -> bool { self.chips.iter().all(|chip| chip.amount == 0) } pub fn amount_on(&self, target: &BetTarget) -> u64 { self.chips .iter() .filter(|chip| &chip.target == target) .fold(0u64, |total, chip| total.saturating_add(chip.amount)) } /// Put a chip on a target, merging with an existing one. pub fn add(&mut self, target: BetTarget, amount: u64) { if let Some(chip) = self.chips.iter_mut().find(|chip| chip.target == target) { chip.amount = chip.amount.saturating_add(amount); } else if amount > 0 { self.chips.push(Chip { target, amount }); } } /// Total returned for a winning number, including returned stakes. pub fn winnings(&self, number: u8) -> u64 { self.chips.iter().fold(0u64, |total, chip| { total.saturating_add( chip.amount .saturating_mul(chip.target.return_multiplier(number)), ) }) } 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 chip in &self.chips { if !chip.target.validates() { return Err(format!("Invalid bet: {}", chip.target.label())); } if chip.amount == 0 { return Err("A bet amount must be positive".to_string()); } } Ok(()) } pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap_or_else(|_| "{\"chips\":[]}".to_string()) } pub fn from_json(raw: &str) -> Result { serde_json::from_str(raw) } }