diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-29 22:55:49 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-29 22:55:49 +0100 |
| commit | 49df948de25d28601469e9ca993f33530989ebd0 (patch) | |
| tree | 291aae0f57d0d7d12a27eb28038e43f942d6b991 /src/client/games | |
| parent | v2.4.0: key stored credentials by certificate fingerprint (diff) | |
| download | zagan-bet-49df948de25d28601469e9ca993f33530989ebd0.tar.gz zagan-bet-49df948de25d28601469e9ca993f33530989ebd0.zip | |
v3.0.0: cursor-driven roulette felt with splits and corners; drop schema migrations; require 80x24
Diffstat (limited to '')
| -rw-r--r-- | src/client/games/manager.rs | 17 | ||||
| -rw-r--r-- | src/client/games/roulette.rs | 346 |
2 files changed, 297 insertions, 66 deletions
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index b20ff44..bb2a8b5 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs | |||
| @@ -55,7 +55,7 @@ impl GameManager { | |||
| 55 | ); | 55 | ); |
| 56 | data.insert("chip".to_string(), "10".to_string()); | 56 | data.insert("chip".to_string(), "10".to_string()); |
| 57 | data.insert("status".to_string(), "place".to_string()); | 57 | data.insert("status".to_string(), "place".to_string()); |
| 58 | data.insert("number_input".to_string(), String::new()); | 58 | data.insert("cursor".to_string(), "lattice:0:3".to_string()); |
| 59 | 0 | 59 | 0 |
| 60 | } else if game_type == "blackjack" { | 60 | } else if game_type == "blackjack" { |
| 61 | data.insert("status".to_string(), "ready".to_string()); | 61 | data.insert("status".to_string(), "ready".to_string()); |
| @@ -234,11 +234,19 @@ impl GameManager { | |||
| 234 | } | 234 | } |
| 235 | } | 235 | } |
| 236 | 236 | ||
| 237 | pub fn set_roulette_number_input(&mut self, value: &str) { | 237 | pub fn roulette_cursor(&self) -> crate::client::games::roulette::Cursor { |
| 238 | self.current_session | ||
| 239 | .as_ref() | ||
| 240 | .and_then(|session| session.data.get("cursor")) | ||
| 241 | .map(|raw| crate::client::games::roulette::Cursor::parse(raw)) | ||
| 242 | .unwrap_or_default() | ||
| 243 | } | ||
| 244 | |||
| 245 | pub fn set_roulette_cursor(&mut self, cursor: &crate::client::games::roulette::Cursor) { | ||
| 238 | if let Some(session) = self.current_session.as_mut() { | 246 | if let Some(session) = self.current_session.as_mut() { |
| 239 | session | 247 | session |
| 240 | .data | 248 | .data |
| 241 | .insert("number_input".to_string(), value.to_string()); | 249 | .insert("cursor".to_string(), cursor.encoded()); |
| 242 | } | 250 | } |
| 243 | } | 251 | } |
| 244 | 252 | ||
| @@ -274,9 +282,6 @@ impl GameManager { | |||
| 274 | session | 282 | session |
| 275 | .data | 283 | .data |
| 276 | .insert("status".to_string(), "result".to_string()); | 284 | .insert("status".to_string(), "result".to_string()); |
| 277 | session | ||
| 278 | .data | ||
| 279 | .insert("number_input".to_string(), String::new()); | ||
| 280 | } | 285 | } |
| 281 | 286 | ||
| 282 | pub fn apply_blackjack_state(&mut self, state: BlackjackState) { | 287 | pub fn apply_blackjack_state(&mut self, state: BlackjackState) { |
diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs index e80eca8..3cb1c58 100644 --- a/src/client/games/roulette.rs +++ b/src/client/games/roulette.rs | |||
| @@ -1,10 +1,178 @@ | |||
| 1 | use super::traits::{Game, GameState}; | 1 | use super::traits::{Game, GameState}; |
| 2 | use crate::roulette::{is_even, is_odd, Bets}; | 2 | use crate::roulette::{is_even, is_odd, BetTarget, Bets, Outside}; |
| 3 | 3 | ||
| 4 | pub const ROULETTE_BOARD_LINES: usize = 8; | 4 | pub const ROULETTE_BOARD_LINES: usize = 12; |
| 5 | |||
| 6 | /// Width of one number cell on the felt. | ||
| 7 | const CELL: usize = 5; | ||
| 8 | const NUM_COLUMNS: usize = 12; | ||
| 9 | const NUM_WIDTH: usize = CELL * NUM_COLUMNS; | ||
| 10 | const THIRD_WIDTH: usize = NUM_WIDTH / 3; | ||
| 11 | const OUTSIDE_WIDTH: usize = NUM_WIDTH / 6; | ||
| 12 | const OUTSIDE_ORDER: [Outside; 6] = [ | ||
| 13 | Outside::Low, | ||
| 14 | Outside::Even, | ||
| 15 | Outside::Red, | ||
| 16 | Outside::Black, | ||
| 17 | Outside::Odd, | ||
| 18 | Outside::High, | ||
| 19 | ]; | ||
| 5 | 20 | ||
| 6 | pub struct RouletteGame; | 21 | pub struct RouletteGame; |
| 7 | 22 | ||
| 23 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 24 | pub enum Direction { | ||
| 25 | Up, | ||
| 26 | Down, | ||
| 27 | Left, | ||
| 28 | Right, | ||
| 29 | } | ||
| 30 | |||
| 31 | /// Where the betting cursor is. | ||
| 32 | /// | ||
| 33 | /// Over the numbers the cursor sits on a *lattice point*, the way a chip sits | ||
| 34 | /// between numbers on a real felt: an inner point covers four numbers (corner), | ||
| 35 | /// an edge point covers two (split), and an outer corner is a straight number. | ||
| 36 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| 37 | pub enum Cursor { | ||
| 38 | Zero, | ||
| 39 | Lattice { x: usize, y: usize }, | ||
| 40 | Dozen(usize), | ||
| 41 | Column(usize), | ||
| 42 | Outside(usize), | ||
| 43 | } | ||
| 44 | |||
| 45 | impl Default for Cursor { | ||
| 46 | fn default() -> Self { | ||
| 47 | // Bottom-left number (1). | ||
| 48 | Cursor::Lattice { x: 0, y: 3 } | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | impl Cursor { | ||
| 53 | pub fn parse(raw: &str) -> Self { | ||
| 54 | let parts: Vec<&str> = raw.split(':').collect(); | ||
| 55 | match parts.as_slice() { | ||
| 56 | ["zero"] => Cursor::Zero, | ||
| 57 | ["lattice", x, y] => match (x.parse::<usize>(), y.parse::<usize>()) { | ||
| 58 | (Ok(x), Ok(y)) if x <= NUM_COLUMNS && y <= 3 => Cursor::Lattice { x, y }, | ||
| 59 | _ => Cursor::default(), | ||
| 60 | }, | ||
| 61 | ["dozen", index] => parse_index(index).map(Cursor::Dozen).unwrap_or_default(), | ||
| 62 | ["column", index] => parse_index(index).map(Cursor::Column).unwrap_or_default(), | ||
| 63 | ["outside", index] => match index.parse::<usize>() { | ||
| 64 | Ok(index) if index < OUTSIDE_ORDER.len() => Cursor::Outside(index), | ||
| 65 | _ => Cursor::default(), | ||
| 66 | }, | ||
| 67 | _ => Cursor::default(), | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | pub fn encoded(&self) -> String { | ||
| 72 | match self { | ||
| 73 | Cursor::Zero => "zero".to_string(), | ||
| 74 | Cursor::Lattice { x, y } => format!("lattice:{x}:{y}"), | ||
| 75 | Cursor::Dozen(index) => format!("dozen:{index}"), | ||
| 76 | Cursor::Column(index) => format!("column:{index}"), | ||
| 77 | Cursor::Outside(index) => format!("outside:{index}"), | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | pub fn target(&self) -> Option<BetTarget> { | ||
| 82 | match self { | ||
| 83 | Cursor::Zero => Some(BetTarget::Straight { number: 0 }), | ||
| 84 | Cursor::Lattice { x, y } => lattice_target(*x, *y), | ||
| 85 | Cursor::Dozen(index) => Some(BetTarget::Dozen { | ||
| 86 | index: *index as u8, | ||
| 87 | }), | ||
| 88 | Cursor::Column(index) => Some(BetTarget::Column { | ||
| 89 | index: *index as u8, | ||
| 90 | }), | ||
| 91 | Cursor::Outside(index) => Some(BetTarget::Outside { | ||
| 92 | kind: OUTSIDE_ORDER[*index], | ||
| 93 | }), | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | /// Numbers the cursor would cover, for highlighting the felt. | ||
| 98 | pub fn numbers(&self) -> Vec<u8> { | ||
| 99 | self.target().map(|target| target.numbers()).unwrap_or_default() | ||
| 100 | } | ||
| 101 | |||
| 102 | pub fn moved(&self, direction: Direction) -> Cursor { | ||
| 103 | match (*self, direction) { | ||
| 104 | (Cursor::Zero, Direction::Down) => Cursor::Lattice { x: 0, y: 0 }, | ||
| 105 | (Cursor::Zero, _) => Cursor::Zero, | ||
| 106 | |||
| 107 | (Cursor::Lattice { x, y }, Direction::Left) => Cursor::Lattice { | ||
| 108 | x: x.saturating_sub(1), | ||
| 109 | y, | ||
| 110 | }, | ||
| 111 | (Cursor::Lattice { x, y }, Direction::Right) => Cursor::Lattice { | ||
| 112 | x: (x + 1).min(NUM_COLUMNS), | ||
| 113 | y, | ||
| 114 | }, | ||
| 115 | (Cursor::Lattice { x: _, y: 0 }, Direction::Up) => Cursor::Zero, | ||
| 116 | (Cursor::Lattice { x, y }, Direction::Up) => Cursor::Lattice { x, y: y - 1 }, | ||
| 117 | (Cursor::Lattice { x, y: 3 }, Direction::Down) => Cursor::Dozen((x / 4).min(2)), | ||
| 118 | (Cursor::Lattice { x, y }, Direction::Down) => Cursor::Lattice { x, y: y + 1 }, | ||
| 119 | |||
| 120 | (Cursor::Dozen(index), Direction::Left) => Cursor::Dozen(index.saturating_sub(1)), | ||
| 121 | (Cursor::Dozen(index), Direction::Right) => Cursor::Dozen((index + 1).min(2)), | ||
| 122 | (Cursor::Dozen(index), Direction::Up) => Cursor::Lattice { | ||
| 123 | x: 4 * index + 1, | ||
| 124 | y: 3, | ||
| 125 | }, | ||
| 126 | (Cursor::Dozen(index), Direction::Down) => Cursor::Column(index), | ||
| 127 | |||
| 128 | (Cursor::Column(index), Direction::Left) => Cursor::Column(index.saturating_sub(1)), | ||
| 129 | (Cursor::Column(index), Direction::Right) => Cursor::Column((index + 1).min(2)), | ||
| 130 | (Cursor::Column(index), Direction::Up) => Cursor::Dozen(index), | ||
| 131 | (Cursor::Column(index), Direction::Down) => Cursor::Outside(index * 2), | ||
| 132 | |||
| 133 | (Cursor::Outside(index), Direction::Left) => { | ||
| 134 | Cursor::Outside(index.saturating_sub(1)) | ||
| 135 | } | ||
| 136 | (Cursor::Outside(index), Direction::Right) => { | ||
| 137 | Cursor::Outside((index + 1).min(OUTSIDE_ORDER.len() - 1)) | ||
| 138 | } | ||
| 139 | (Cursor::Outside(index), Direction::Up) => Cursor::Column(index / 2), | ||
| 140 | (Cursor::Outside(index), Direction::Down) => Cursor::Outside(index), | ||
| 141 | } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | fn parse_index(raw: &str) -> Option<usize> { | ||
| 146 | raw.parse::<usize>().ok().filter(|index| *index < 3) | ||
| 147 | } | ||
| 148 | |||
| 149 | /// Numbers under a lattice point: 1 (outer corner), 2 (edge) or 4 (inner). | ||
| 150 | fn lattice_target(x: usize, y: usize) -> Option<BetTarget> { | ||
| 151 | let cell = |column: isize, row: isize| -> Option<u8> { | ||
| 152 | if !(0..NUM_COLUMNS as isize).contains(&column) || !(0..3isize).contains(&row) { | ||
| 153 | return None; | ||
| 154 | } | ||
| 155 | Some((3 * column + (3 - row)) as u8) | ||
| 156 | }; | ||
| 157 | |||
| 158 | let x = x as isize; | ||
| 159 | let y = y as isize; | ||
| 160 | let mut numbers: Vec<u8> = [(x - 1, y - 1), (x, y - 1), (x - 1, y), (x, y)] | ||
| 161 | .into_iter() | ||
| 162 | .filter_map(|(column, row)| cell(column, row)) | ||
| 163 | .collect(); | ||
| 164 | numbers.sort_unstable(); | ||
| 165 | |||
| 166 | match numbers.as_slice() { | ||
| 167 | [number] => Some(BetTarget::Straight { number: *number }), | ||
| 168 | [a, b] => Some(BetTarget::Split { a: *a, b: *b }), | ||
| 169 | [a, b, c, d] => Some(BetTarget::Corner { | ||
| 170 | numbers: [*a, *b, *c, *d], | ||
| 171 | }), | ||
| 172 | _ => None, | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 8 | impl RouletteGame { | 176 | impl RouletteGame { |
| 9 | pub fn new() -> Self { | 177 | pub fn new() -> Self { |
| 10 | Self | 178 | Self |
| @@ -14,40 +182,99 @@ impl RouletteGame { | |||
| 14 | state | 182 | state |
| 15 | .data | 183 | .data |
| 16 | .get("bets") | 184 | .get("bets") |
| 17 | .and_then(|raw| serde_json::from_str(raw).ok()) | 185 | .and_then(|raw| Bets::from_json(raw).ok()) |
| 18 | .unwrap_or_default() | 186 | .unwrap_or_default() |
| 19 | } | 187 | } |
| 20 | 188 | ||
| 21 | fn bets_line(bets: &Bets) -> String { | 189 | fn cursor(state: &GameState) -> Cursor { |
| 22 | let mut parts = Vec::new(); | 190 | state |
| 23 | if bets.red > 0 { | 191 | .data |
| 24 | parts.push(format!("RED {}", bets.red)); | 192 | .get("cursor") |
| 25 | } | 193 | .map(|raw| Cursor::parse(raw)) |
| 26 | if bets.black > 0 { | 194 | .unwrap_or_default() |
| 27 | parts.push(format!("BLACK {}", bets.black)); | 195 | } |
| 28 | } | ||
| 29 | if bets.even > 0 { | ||
| 30 | parts.push(format!("EVEN {}", bets.even)); | ||
| 31 | } | ||
| 32 | if bets.odd > 0 { | ||
| 33 | parts.push(format!("ODD {}", bets.odd)); | ||
| 34 | } | ||
| 35 | for bet in &bets.numbers { | ||
| 36 | parts.push(format!("#{} {}", bet.number, bet.amount)); | ||
| 37 | } | ||
| 38 | 196 | ||
| 39 | let list = if parts.is_empty() { | 197 | fn zero_cell(cursor: Cursor) -> String { |
| 40 | "(none)".to_string() | 198 | let cell = if cursor == Cursor::Zero { |
| 199 | "[ 0 ]".to_string() | ||
| 41 | } else { | 200 | } else { |
| 42 | parts.join(" ") | 201 | format!("{:^CELL$}", 0) |
| 43 | }; | 202 | }; |
| 44 | format!("Bets: {} Total: {} PLN", list, bets.total()) | 203 | format!("{cell:<NUM_WIDTH$}") |
| 204 | } | ||
| 205 | |||
| 206 | fn number_row(row: usize, covered: &[u8]) -> String { | ||
| 207 | (0..NUM_COLUMNS) | ||
| 208 | .map(|column| { | ||
| 209 | let number = (3 * column + (3 - row)) as u8; | ||
| 210 | if covered.contains(&number) { | ||
| 211 | format!("[{number:^3}]") | ||
| 212 | } else { | ||
| 213 | format!("{number:^CELL$}") | ||
| 214 | } | ||
| 215 | }) | ||
| 216 | .collect() | ||
| 217 | } | ||
| 218 | |||
| 219 | fn wide_cell(label: &str, width: usize, active: bool) -> String { | ||
| 220 | if active { | ||
| 221 | format!("[{:^inner$}]", label, inner = width - 2) | ||
| 222 | } else { | ||
| 223 | format!("{label:^width$}") | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | fn dozens_row(cursor: Cursor) -> String { | ||
| 228 | ["1st 12", "2nd 12", "3rd 12"] | ||
| 229 | .iter() | ||
| 230 | .enumerate() | ||
| 231 | .map(|(index, label)| { | ||
| 232 | Self::wide_cell(label, THIRD_WIDTH, cursor == Cursor::Dozen(index)) | ||
| 233 | }) | ||
| 234 | .collect() | ||
| 235 | } | ||
| 236 | |||
| 237 | fn columns_row(cursor: Cursor) -> String { | ||
| 238 | ["col 1 2:1", "col 2 2:1", "col 3 2:1"] | ||
| 239 | .iter() | ||
| 240 | .enumerate() | ||
| 241 | .map(|(index, label)| { | ||
| 242 | Self::wide_cell(label, THIRD_WIDTH, cursor == Cursor::Column(index)) | ||
| 243 | }) | ||
| 244 | .collect() | ||
| 245 | } | ||
| 246 | |||
| 247 | fn outside_row(cursor: Cursor) -> String { | ||
| 248 | OUTSIDE_ORDER | ||
| 249 | .iter() | ||
| 250 | .enumerate() | ||
| 251 | .map(|(index, kind)| { | ||
| 252 | Self::wide_cell(kind.label(), OUTSIDE_WIDTH, cursor == Cursor::Outside(index)) | ||
| 253 | }) | ||
| 254 | .collect() | ||
| 45 | } | 255 | } |
| 46 | 256 | ||
| 47 | fn result_lines(state: &GameState) -> (String, String) { | 257 | fn bets_line(bets: &Bets) -> String { |
| 258 | let mut parts: Vec<String> = bets | ||
| 259 | .chips | ||
| 260 | .iter() | ||
| 261 | .filter(|chip| chip.amount > 0) | ||
| 262 | .map(|chip| format!("{} {}", chip.target.label(), chip.amount)) | ||
| 263 | .collect(); | ||
| 264 | if parts.is_empty() { | ||
| 265 | parts.push("(none)".to_string()); | ||
| 266 | } | ||
| 267 | let mut list = parts.join(" "); | ||
| 268 | if list.chars().count() > 40 { | ||
| 269 | list = list.chars().take(39).collect::<String>() + "…"; | ||
| 270 | } | ||
| 271 | format!("Bets: {list} Total: {} PLN", bets.total()) | ||
| 272 | } | ||
| 273 | |||
| 274 | fn status_line(state: &GameState) -> String { | ||
| 48 | if let Some(error) = state.data.get("error") { | 275 | if let Some(error) = state.data.get("error") { |
| 49 | if !error.is_empty() { | 276 | if !error.is_empty() { |
| 50 | return (format!("ERROR: {error}"), String::new()); | 277 | return format!("ERROR: {error}"); |
| 51 | } | 278 | } |
| 52 | } | 279 | } |
| 53 | 280 | ||
| @@ -77,15 +304,16 @@ impl RouletteGame { | |||
| 77 | .get("winnings") | 304 | .get("winnings") |
| 78 | .map(String::as_str) | 305 | .map(String::as_str) |
| 79 | .unwrap_or("0"); | 306 | .unwrap_or("0"); |
| 80 | let result = if winnings == "0" { | 307 | let stake = state.bet_amount; |
| 81 | "No win.".to_string() | 308 | if winnings == "0" { |
| 309 | format!("No win at {number} {color}{parity}") | ||
| 82 | } else { | 310 | } else { |
| 83 | format!("Won {winnings} PLN") | 311 | let profit = winnings.parse::<u64>().unwrap_or(0).saturating_sub(stake); |
| 84 | }; | 312 | format!("Won {winnings} PLN at {number} {color}{parity} (+{profit})") |
| 85 | (format!("Winning: {number} {color}{parity}"), result) | 313 | } |
| 86 | } | 314 | } |
| 87 | Some("spinning") => ("Spinning...".to_string(), String::new()), | 315 | Some("spinning") => "Spinning...".to_string(), |
| 88 | _ => ("Place bets, then SPACE to spin.".to_string(), String::new()), | 316 | _ => "Arrows move, ENTER places the chip, SPACE spins.".to_string(), |
| 89 | } | 317 | } |
| 90 | } | 318 | } |
| 91 | } | 319 | } |
| @@ -99,27 +327,25 @@ impl Game for RouletteGame { | |||
| 99 | let bets = Self::bets(state); | 327 | let bets = Self::bets(state); |
| 100 | let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); | 328 | let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); |
| 101 | let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); | 329 | let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); |
| 102 | let input = state | 330 | let cursor = Self::cursor(state); |
| 103 | .data | 331 | let covered = cursor.numbers(); |
| 104 | .get("number_input") | ||
| 105 | .map(String::as_str) | ||
| 106 | .unwrap_or(""); | ||
| 107 | let input_line = if input.is_empty() { | ||
| 108 | String::new() | ||
| 109 | } else { | ||
| 110 | format!("Number: {input}_") | ||
| 111 | }; | ||
| 112 | let (status, result) = Self::result_lines(state); | ||
| 113 | 332 | ||
| 114 | format!( | 333 | let lines = [ |
| 115 | "=== ROULETTE ===\r\nBalance: {} PLN Chip: {} PLN\r\n{}\r\n{}\r\n{}\r\n{}\r\n[R]ed [B]lack [E]ven [O]dd [#]Number [C]lear\r\n[+/-] Chip [SPACE] Spin [Q] Quit", | 334 | "=== ROULETTE ===".to_string(), |
| 116 | balance, | 335 | format!("Balance: {balance} PLN Chip: {chip} PLN"), |
| 117 | chip, | 336 | Self::zero_cell(cursor), |
| 337 | Self::number_row(0, &covered), | ||
| 338 | Self::number_row(1, &covered), | ||
| 339 | Self::number_row(2, &covered), | ||
| 340 | Self::dozens_row(cursor), | ||
| 341 | Self::columns_row(cursor), | ||
| 342 | Self::outside_row(cursor), | ||
| 118 | Self::bets_line(&bets), | 343 | Self::bets_line(&bets), |
| 119 | input_line, | 344 | Self::status_line(state), |
| 120 | status, | 345 | "[ARROWS] Move [ENTER] Bet [C]lear [+/-] Chip [SPACE] Spin [Q] Quit" |
| 121 | result | 346 | .to_string(), |
| 122 | ) | 347 | ]; |
| 348 | lines.join("\r\n") | ||
| 123 | } | 349 | } |
| 124 | 350 | ||
| 125 | fn render_lines(&self, _state: &GameState) -> usize { | 351 | fn render_lines(&self, _state: &GameState) -> usize { |
| @@ -129,20 +355,20 @@ impl Game for RouletteGame { | |||
| 129 | fn get_help(&self) -> String { | 355 | fn get_help(&self) -> String { |
| 130 | "ROULETTE (European)\n\ | 356 | "ROULETTE (European)\n\ |
| 131 | ==================\n\ | 357 | ==================\n\ |
| 132 | 37 pockets: 0 plus 1-36. 0 is green and loses all outside bets.\n\ | 358 | 37 pockets: 0 plus 1-36. 0 is green and loses every outside bet.\n\ |
| 133 | \n\ | 359 | \n\ |
| 134 | Betting:\n\ | 360 | Betting with the cursor:\n\ |
| 135 | R / B / E / O - add the current chip to red/black/even/odd\n\ | 361 | arrows - move over the felt; between numbers picks a split or corner\n\ |
| 136 | digits + Enter - add the current chip to that number\n\ | 362 | ENTER - place the current chip where the cursor is\n\ |
| 137 | C - clear all bets\n\ | 363 | C - clear all bets\n\ |
| 138 | + or = / - - change the chip size\n\ | 364 | + or = / - - change the chip size\n\ |
| 139 | SPACE - spin when at least one bet is placed\n\ | 365 | SPACE - spin when at least one bet is placed\n\ |
| 140 | Q - exit\n\ | 366 | Q - exit\n\ |
| 141 | \n\ | 367 | \n\ |
| 142 | Payouts:\n\ | 368 | Payouts (returned per unit staked):\n\ |
| 143 | straight number: 35:1 (36x returned)\n\ | 369 | straight 36x, split 18x, corner 9x\n\ |
| 144 | red/black: 1:1 (2x returned)\n\ | 370 | dozen 3x, column 3x\n\ |
| 145 | even/odd: 1:1 (2x returned)\n\ | 371 | red/black, even/odd, 1-18/19-36: 2x\n\ |
| 146 | Multiple bets are settled independently and added together." | 372 | Multiple bets are settled independently and added together." |
| 147 | .to_string() | 373 | .to_string() |
| 148 | } | 374 | } |
