summaryrefslogtreecommitdiffstats
path: root/src/roulette.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/roulette.rs')
-rw-r--r--src/roulette.rs215
1 files changed, 169 insertions, 46 deletions
diff --git a/src/roulette.rs b/src/roulette.rs
index 426ecc3..e7b3aad 100644
--- a/src/roulette.rs
+++ b/src/roulette.rs
@@ -14,6 +14,12 @@ pub const RED_NUMBERS: [u8; 18] = [
14 14
15/// Total return on a winning straight (35:1 plus the stake). 15/// Total return on a winning straight (35:1 plus the stake).
16pub const STRAIGHT_RETURN: u64 = 36; 16pub const STRAIGHT_RETURN: u64 = 36;
17/// Total return on a winning split (17:1 plus the stake).
18pub const SPLIT_RETURN: u64 = 18;
19/// Total return on a winning corner (8:1 plus the stake).
20pub const CORNER_RETURN: u64 = 9;
21/// Total return on a winning dozen or column (2:1 plus the stake).
22pub const THIRD_RETURN: u64 = 3;
17/// Total return on a winning outside bet (1:1 plus the stake). 23/// Total return on a winning outside bet (1:1 plus the stake).
18pub const EVEN_MONEY_RETURN: u64 = 2; 24pub const EVEN_MONEY_RETURN: u64 = 2;
19 25
@@ -47,67 +53,176 @@ pub fn rtp() -> f64 {
47 36.0 / 37.0 53 36.0 / 37.0
48} 54}
49 55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum Outside {
59 Low,
60 High,
61 Even,
62 Odd,
63 Red,
64 Black,
65}
66
67impl Outside {
68 pub fn wins(self, number: u8) -> bool {
69 match self {
70 Outside::Low => (1..=18).contains(&number),
71 Outside::High => (19..=36).contains(&number),
72 Outside::Even => is_even(number),
73 Outside::Odd => is_odd(number),
74 Outside::Red => is_red(number),
75 Outside::Black => is_black(number),
76 }
77 }
78
79 pub fn label(self) -> &'static str {
80 match self {
81 Outside::Low => "1-18",
82 Outside::High => "19-36",
83 Outside::Even => "EVEN",
84 Outside::Odd => "ODD",
85 Outside::Red => "RED",
86 Outside::Black => "BLACK",
87 }
88 }
89}
90
91/// One place on the felt where a chip can sit.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(tag = "bet", rename_all = "snake_case")]
94pub enum BetTarget {
95 Straight { number: u8 },
96 Split { a: u8, b: u8 },
97 Corner { numbers: [u8; 4] },
98 /// 0 = 1-12, 1 = 13-24, 2 = 25-36.
99 Dozen { index: u8 },
100 /// 0 = 1,4,...,34; 1 = 2,5,...,35; 2 = 3,6,...,36.
101 Column { index: u8 },
102 Outside { kind: Outside },
103}
104
105impl BetTarget {
106 /// Numbers this target covers on the number grid (empty for outside bets).
107 pub fn numbers(&self) -> Vec<u8> {
108 match self {
109 BetTarget::Straight { number } => vec![*number],
110 BetTarget::Split { a, b } => vec![*a, *b],
111 BetTarget::Corner { numbers } => numbers.to_vec(),
112 _ => Vec::new(),
113 }
114 }
115
116 /// Total returned per unit staked, or 0 when the bet loses.
117 pub fn return_multiplier(&self, number: u8) -> u64 {
118 match self {
119 BetTarget::Straight { number: straight } => {
120 u64::from(*straight == number) * STRAIGHT_RETURN
121 }
122 BetTarget::Split { a, b } => u64::from(*a == number || *b == number) * SPLIT_RETURN,
123 BetTarget::Corner { numbers } => {
124 u64::from(numbers.contains(&number)) * CORNER_RETURN
125 }
126 BetTarget::Dozen { index } => {
127 let wins = number != 0 && (number - 1) / 12 == *index;
128 u64::from(wins) * THIRD_RETURN
129 }
130 BetTarget::Column { index } => {
131 let wins = match index {
132 0 => number % 3 == 1,
133 1 => number % 3 == 2,
134 2 => number != 0 && number % 3 == 0,
135 _ => false,
136 };
137 u64::from(wins) * THIRD_RETURN
138 }
139 BetTarget::Outside { kind } => u64::from(kind.wins(number)) * EVEN_MONEY_RETURN,
140 }
141 }
142
143 pub fn label(&self) -> String {
144 match self {
145 BetTarget::Straight { number } => number.to_string(),
146 BetTarget::Split { a, b } => format!("{a}/{b}"),
147 BetTarget::Corner { numbers } => numbers
148 .iter()
149 .map(u8::to_string)
150 .collect::<Vec<_>>()
151 .join("/"),
152 BetTarget::Dozen { index } => match index {
153 0 => "1st12".to_string(),
154 1 => "2nd12".to_string(),
155 _ => "3rd12".to_string(),
156 },
157 BetTarget::Column { index } => format!("col{}", index + 1),
158 BetTarget::Outside { kind } => kind.label().to_string(),
159 }
160 }
161
162 fn validates(&self) -> bool {
163 match self {
164 BetTarget::Straight { number } => *number <= 36,
165 BetTarget::Split { a, b } => *a <= 36 && *b <= 36 && a != b,
166 BetTarget::Corner { numbers } => {
167 numbers.iter().all(|number| *number <= 36)
168 && numbers
169 .iter()
170 .enumerate()
171 .all(|(index, number)| !numbers[..index].contains(number))
172 }
173 BetTarget::Dozen { index } | BetTarget::Column { index } => *index < 3,
174 BetTarget::Outside { .. } => true,
175 }
176 }
177}
178
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct NumberBet { 180pub struct Chip {
52 pub number: u8, 181 pub target: BetTarget,
53 pub amount: u64, 182 pub amount: u64,
54} 183}
55 184
56#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] 185#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub struct Bets { 186pub struct Bets {
58 #[serde(default)] 187 #[serde(default)]
59 pub red: u64, 188 pub chips: Vec<Chip>,
60 #[serde(default)]
61 pub black: u64,
62 #[serde(default)]
63 pub even: u64,
64 #[serde(default)]
65 pub odd: u64,
66 #[serde(default)]
67 pub numbers: Vec<NumberBet>,
68} 189}
69 190
70impl Bets { 191impl Bets {
71 pub fn total(&self) -> u64 { 192 pub fn total(&self) -> u64 {
72 let mut total = self 193 self.chips
73 .red 194 .iter()
74 .saturating_add(self.black) 195 .fold(0u64, |total, chip| total.saturating_add(chip.amount))
75 .saturating_add(self.even)
76 .saturating_add(self.odd);
77 for bet in &self.numbers {
78 total = total.saturating_add(bet.amount);
79 }
80 total
81 } 196 }
82 197
83 pub fn is_empty(&self) -> bool { 198 pub fn is_empty(&self) -> bool {
84 self.total() == 0 199 self.chips.iter().all(|chip| chip.amount == 0)
85 } 200 }
86 201
87 /// Total returned for a winning number, including returned stakes. 202 pub fn amount_on(&self, target: &BetTarget) -> u64 {
88 pub fn winnings(&self, number: u8) -> u64 { 203 self.chips
89 let mut winnings = 0u64; 204 .iter()
90 205 .filter(|chip| &chip.target == target)
91 if number != 0 { 206 .fold(0u64, |total, chip| total.saturating_add(chip.amount))
92 if is_red(number) { 207 }
93 winnings = winnings.saturating_add(self.red.saturating_mul(EVEN_MONEY_RETURN));
94 } else {
95 winnings = winnings.saturating_add(self.black.saturating_mul(EVEN_MONEY_RETURN));
96 }
97 if is_even(number) {
98 winnings = winnings.saturating_add(self.even.saturating_mul(EVEN_MONEY_RETURN));
99 } else {
100 winnings = winnings.saturating_add(self.odd.saturating_mul(EVEN_MONEY_RETURN));
101 }
102 }
103 208
104 for bet in &self.numbers { 209 /// Put a chip on a target, merging with an existing one.
105 if bet.number == number { 210 pub fn add(&mut self, target: BetTarget, amount: u64) {
106 winnings = winnings.saturating_add(bet.amount.saturating_mul(STRAIGHT_RETURN)); 211 if let Some(chip) = self.chips.iter_mut().find(|chip| chip.target == target) {
107 } 212 chip.amount = chip.amount.saturating_add(amount);
213 } else if amount > 0 {
214 self.chips.push(Chip { target, amount });
108 } 215 }
216 }
109 217
110 winnings 218 /// Total returned for a winning number, including returned stakes.
219 pub fn winnings(&self, number: u8) -> u64 {
220 self.chips.iter().fold(0u64, |total, chip| {
221 total.saturating_add(
222 chip.amount
223 .saturating_mul(chip.target.return_multiplier(number)),
224 )
225 })
111 } 226 }
112 227
113 pub fn validate(&self, balance: u64) -> Result<(), String> { 228 pub fn validate(&self, balance: u64) -> Result<(), String> {
@@ -117,14 +232,22 @@ impl Bets {
117 if self.total() > balance { 232 if self.total() > balance {
118 return Err("Insufficient balance".to_string()); 233 return Err("Insufficient balance".to_string());
119 } 234 }
120 for bet in &self.numbers { 235 for chip in &self.chips {
121 if bet.number > 36 { 236 if !chip.target.validates() {
122 return Err(format!("Invalid number: {}", bet.number)); 237 return Err(format!("Invalid bet: {}", chip.target.label()));
123 } 238 }
124 if bet.amount == 0 { 239 if chip.amount == 0 {
125 return Err("A bet amount must be positive".to_string()); 240 return Err("A bet amount must be positive".to_string());
126 } 241 }
127 } 242 }
128 Ok(()) 243 Ok(())
129 } 244 }
245
246 pub fn to_json(&self) -> String {
247 serde_json::to_string(self).unwrap_or_else(|_| "{\"chips\":[]}".to_string())
248 }
249
250 pub fn from_json(raw: &str) -> Result<Self, serde_json::Error> {
251 serde_json::from_str(raw)
252 }
130} 253}