summaryrefslogtreecommitdiffstats
path: root/src/client
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-29 17:18:34 +0100
committersalmonik <salmonik@tuta.io>2026-10-29 17:18:34 +0100
commitd9f1fe513e6f7696809d3a9b4d44ca485fe99c97 (patch)
tree9da99200fdfe004a47056d10340b3f59c7ccc52e /src/client
parentv2.0.0: reset plays audit to a generic per-game schema (diff)
downloadzagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.tar.gz
zagan-bet-d9f1fe513e6f7696809d3a9b4d44ca485fe99c97.zip
v2.1.0: add European roulette with split bets and wheel animation
Diffstat (limited to '')
-rw-r--r--src/client/core.rs183
-rw-r--r--src/client/games/manager.rs131
-rw-r--r--src/client/games/mod.rs1
-rw-r--r--src/client/games/response.rs64
-rw-r--r--src/client/games/roulette.rs157
5 files changed, 532 insertions, 4 deletions
diff --git a/src/client/core.rs b/src/client/core.rs
index a88991b..e005f2a 100644
--- a/src/client/core.rs
+++ b/src/client/core.rs
@@ -1,9 +1,10 @@
1use crate::client::games::manager::GameManager; 1use crate::client::games::manager::GameManager;
2use crate::client::games::response::parse_spin_response; 2use crate::client::games::response::{parse_roulette_response, parse_spin_response};
3use crate::client::ui::{ 3use crate::client::ui::{
4 command_names, completion_lines, display_width, help_lines, input_cursor_column, 4 command_names, completion_lines, display_width, help_lines, input_cursor_column,
5 parse_bet_arg, record_history, 5 parse_bet_arg, record_history,
6}; 6};
7use crate::roulette::{Bets, NumberBet};
7 8
8#[derive(Debug, Clone, Copy, PartialEq, Eq)] 9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Key { 10pub enum Key {
@@ -33,6 +34,11 @@ pub enum Effect {
33 Print(Vec<String>), 34 Print(Vec<String>),
34 /// Replace the game board region in place. 35 /// Replace the game board region in place.
35 ReplaceRegion(Vec<String>), 36 ReplaceRegion(Vec<String>),
37 /// Run the roulette wheel animation, then draw the final board lines.
38 AnimateRoulette {
39 winning: u8,
40 final_lines: Vec<String>,
41 },
36 /// Remove the game board region. 42 /// Remove the game board region.
37 ClearRegion, 43 ClearRegion,
38 /// Redraw the command prompt. `text` is the input without the `> ` prefix. 44 /// Redraw the command prompt. `text` is the input without the `> ` prefix.
@@ -54,11 +60,20 @@ pub enum Effect {
54#[derive(Debug, Clone, Copy, PartialEq, Eq)] 60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55enum Pending { 61enum Pending {
56 Spin, 62 Spin,
63 RouletteSpin,
57 Ping, 64 Ping,
58 Echo, 65 Echo,
59 Info, 66 Info,
60} 67}
61 68
69#[derive(Debug, Clone, Copy)]
70enum RouletteSide {
71 Red,
72 Black,
73 Even,
74 Odd,
75}
76
62pub struct ClientCore { 77pub struct ClientCore {
63 input: String, 78 input: String,
64 history: Vec<String>, 79 history: Vec<String>,
@@ -160,6 +175,22 @@ impl ClientCore {
160 self.board_effects() 175 self.board_effects()
161 } 176 }
162 }, 177 },
178 Some(Pending::RouletteSpin) => match parse_roulette_response(reply) {
179 Ok(result) => {
180 self.balance = Some(result.balance);
181 let winning = result.number;
182 self.game.apply_roulette_result(result);
183 let final_lines = self.board_lines().unwrap_or_default();
184 vec![Effect::AnimateRoulette {
185 winning,
186 final_lines,
187 }]
188 }
189 Err(message) => {
190 self.game.set_error(message);
191 self.board_effects()
192 }
193 },
163 Some(Pending::Ping) => { 194 Some(Pending::Ping) => {
164 if reply.trim() == "PONG" { 195 if reply.trim() == "PONG" {
165 print_line("Pong") 196 print_line("Pong")
@@ -194,6 +225,15 @@ impl ClientCore {
194 } 225 }
195 226
196 fn handle_game_key(&mut self, key: Key) -> Vec<Effect> { 227 fn handle_game_key(&mut self, key: Key) -> Vec<Effect> {
228 if self
229 .game
230 .get_current_state()
231 .map(|state| state.game_type.as_str())
232 == Some("roulette")
233 {
234 return self.handle_roulette_key(key);
235 }
236
197 match key { 237 match key {
198 Key::CtrlC => vec![Effect::Quit], 238 Key::CtrlC => vec![Effect::Quit],
199 Key::Esc | Key::Char('q') => { 239 Key::Esc | Key::Char('q') => {
@@ -230,6 +270,147 @@ impl ClientCore {
230 effects 270 effects
231 } 271 }
232 272
273 fn handle_roulette_key(&mut self, key: Key) -> Vec<Effect> {
274 match key {
275 Key::CtrlC => vec![Effect::Quit],
276 Key::Char('q') => {
277 self.game.end_game();
278 vec![
279 Effect::ClearRegion,
280 Effect::Print(vec!["Game exited.".to_string()]),
281 ]
282 }
283 Key::Esc => {
284 self.game.set_roulette_number_input("");
285 self.board_effects()
286 }
287 Key::Char(' ') => self.spin_roulette(),
288 Key::Char('r') => self.add_roulette_side(RouletteSide::Red),
289 Key::Char('b') => self.add_roulette_side(RouletteSide::Black),
290 Key::Char('e') => self.add_roulette_side(RouletteSide::Even),
291 Key::Char('o') => self.add_roulette_side(RouletteSide::Odd),
292 Key::Char('c') => {
293 self.game.set_roulette_bets(&Bets::default());
294 self.game.set_roulette_number_input("");
295 self.game.set_roulette_status("place");
296 self.board_effects()
297 }
298 Key::Char('+') | Key::Char('=') => {
299 let chip = self.game.roulette_chip().saturating_add(10);
300 self.game.set_roulette_chip(chip);
301 self.board_effects()
302 }
303 Key::Char('-') => {
304 let chip = self.game.roulette_chip().saturating_sub(10).max(10);
305 self.game.set_roulette_chip(chip);
306 self.board_effects()
307 }
308 Key::Char(c) if c.is_ascii_digit() => {
309 let mut input = self.roulette_input();
310 if input.len() < 2 {
311 input.push(c);
312 self.game.set_roulette_number_input(&input);
313 }
314 self.board_effects()
315 }
316 Key::Backspace => {
317 let mut input = self.roulette_input();
318 input.pop();
319 self.game.set_roulette_number_input(&input);
320 self.board_effects()
321 }
322 Key::Enter => {
323 let input = self.roulette_input();
324 if !input.is_empty() {
325 match input.parse::<u8>() {
326 Ok(number) if number <= 36 => {
327 self.add_roulette_number(number);
328 self.game.set_roulette_number_input("");
329 }
330 _ => {
331 self.game.set_roulette_number_input("");
332 self.game.set_error("Number must be 0-36");
333 }
334 }
335 }
336 self.board_effects()
337 }
338 _ => Vec::new(),
339 }
340 }
341
342 fn add_roulette_side(&mut self, side: RouletteSide) -> Vec<Effect> {
343 let chip = self.game.roulette_chip();
344 let mut bets = self.game.roulette_bets();
345 match side {
346 RouletteSide::Red => bets.red = bets.red.saturating_add(chip),
347 RouletteSide::Black => bets.black = bets.black.saturating_add(chip),
348 RouletteSide::Even => bets.even = bets.even.saturating_add(chip),
349 RouletteSide::Odd => bets.odd = bets.odd.saturating_add(chip),
350 }
351 self.place_roulette_bets(bets)
352 }
353
354 fn add_roulette_number(&mut self, number: u8) -> Vec<Effect> {
355 let chip = self.game.roulette_chip();
356 let mut bets = self.game.roulette_bets();
357 if let Some(existing) = bets.numbers.iter_mut().find(|bet| bet.number == number) {
358 existing.amount = existing.amount.saturating_add(chip);
359 } else {
360 bets.numbers.push(NumberBet {
361 number,
362 amount: chip,
363 });
364 }
365 self.place_roulette_bets(bets)
366 }
367
368 fn place_roulette_bets(&mut self, bets: Bets) -> Vec<Effect> {
369 if let Some(balance) = self.balance {
370 if bets.total() > balance.max(0) as u64 {
371 self.game.set_error("Insufficient balance");
372 return self.board_effects();
373 }
374 }
375
376 self.game.set_roulette_bets(&bets);
377 self.game.set_roulette_status("place");
378 self.board_effects()
379 }
380
381 fn spin_roulette(&mut self) -> Vec<Effect> {
382 let bets = self.game.roulette_bets();
383 if bets.is_empty() {
384 self.game.set_error("Place at least one bet");
385 return self.board_effects();
386 }
387 if let Some(balance) = self.balance {
388 if bets.total() > balance.max(0) as u64 {
389 self.game.set_error("Insufficient balance");
390 return self.board_effects();
391 }
392 }
393 let Some(request) = self.game.spin_request() else {
394 return Vec::new();
395 };
396 if self.game.begin_spin().is_err() {
397 return Vec::new();
398 }
399 self.pending = Some(Pending::RouletteSpin);
400
401 let mut effects = self.board_effects();
402 effects.push(Effect::SendLine(request));
403 effects
404 }
405
406 fn roulette_input(&self) -> String {
407 self.game
408 .get_current_state()
409 .and_then(|state| state.data.get("number_input"))
410 .cloned()
411 .unwrap_or_default()
412 }
413
233 fn submit(&mut self) -> Vec<Effect> { 414 fn submit(&mut self) -> Vec<Effect> {
234 let line = self.input.trim().to_string(); 415 let line = self.input.trim().to_string();
235 self.input.clear(); 416 self.input.clear();
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs
index 168c49d..02e9451 100644
--- a/src/client/games/manager.rs
+++ b/src/client/games/manager.rs
@@ -1,6 +1,8 @@
1use super::response::SpinResult; 1use super::response::{RouletteResult, SpinResult};
2use super::roulette::RouletteGame;
2use super::slots::SlotsGame; 3use super::slots::SlotsGame;
3use super::traits::{Game, GameError, GamePhase, GameState}; 4use super::traits::{Game, GameError, GamePhase, GameState};
5use crate::roulette::Bets;
4use std::collections::HashMap; 6use std::collections::HashMap;
5 7
6pub struct GameManager { 8pub struct GameManager {
@@ -12,6 +14,7 @@ impl GameManager {
12 pub fn new() -> Self { 14 pub fn new() -> Self {
13 let mut games: HashMap<String, Box<dyn Game>> = HashMap::new(); 15 let mut games: HashMap<String, Box<dyn Game>> = HashMap::new();
14 games.insert("slots".to_string(), Box::new(SlotsGame::new())); 16 games.insert("slots".to_string(), Box::new(SlotsGame::new()));
17 games.insert("roulette".to_string(), Box::new(RouletteGame::new()));
15 18
16 Self { 19 Self {
17 current_session: None, 20 current_session: None,
@@ -38,11 +41,25 @@ impl GameManager {
38 return Err(GameError::InvalidAction); 41 return Err(GameError::InvalidAction);
39 } 42 }
40 43
44 let mut data = HashMap::new();
45 let bet_amount = if game_type == "roulette" {
46 data.insert(
47 "bets".to_string(),
48 serde_json::to_string(&Bets::default()).unwrap_or_else(|_| "{}".to_string()),
49 );
50 data.insert("chip".to_string(), "10".to_string());
51 data.insert("status".to_string(), "place".to_string());
52 data.insert("number_input".to_string(), String::new());
53 0
54 } else {
55 10
56 };
57
41 self.current_session = Some(GameState { 58 self.current_session = Some(GameState {
42 game_type: game_type.to_string(), 59 game_type: game_type.to_string(),
43 bet_amount: 10, 60 bet_amount,
44 phase: GamePhase::WaitingToStart, 61 phase: GamePhase::WaitingToStart,
45 data: HashMap::new(), 62 data,
46 }); 63 });
47 64
48 Ok(()) 65 Ok(())
@@ -54,6 +71,17 @@ impl GameManager {
54 if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { 71 if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished {
55 return None; 72 return None;
56 } 73 }
74 if session.game_type == "roulette" {
75 let bets = session
76 .data
77 .get("bets")
78 .cloned()
79 .unwrap_or_else(|| "{}".to_string());
80 return Some(format!(
81 "PLAY_START roulette {} {}",
82 session.bet_amount, bets
83 ));
84 }
57 Some(format!( 85 Some(format!(
58 "PLAY_START {} {}", 86 "PLAY_START {} {}",
59 session.game_type, session.bet_amount 87 session.game_type, session.bet_amount
@@ -78,6 +106,13 @@ impl GameManager {
78 session.data.remove("win_lines"); 106 session.data.remove("win_lines");
79 session.data.remove("win_cells"); 107 session.data.remove("win_cells");
80 session.data.remove("winnings"); 108 session.data.remove("winnings");
109 session.data.remove("number");
110 session.data.remove("color");
111 if session.game_type == "roulette" {
112 session
113 .data
114 .insert("status".to_string(), "spinning".to_string());
115 }
81 Ok(()) 116 Ok(())
82 } 117 }
83 118
@@ -130,6 +165,13 @@ impl GameManager {
130 session.data.remove("win_lines"); 165 session.data.remove("win_lines");
131 session.data.remove("win_cells"); 166 session.data.remove("win_cells");
132 session.data.remove("winnings"); 167 session.data.remove("winnings");
168 session.data.remove("number");
169 session.data.remove("color");
170 if session.game_type == "roulette" {
171 session
172 .data
173 .insert("status".to_string(), "place".to_string());
174 }
133 session.data.insert("error".to_string(), message.into()); 175 session.data.insert("error".to_string(), message.into());
134 } 176 }
135 177
@@ -141,6 +183,83 @@ impl GameManager {
141 } 183 }
142 } 184 }
143 185
186 pub fn roulette_bets(&self) -> Bets {
187 self.current_session
188 .as_ref()
189 .and_then(|session| session.data.get("bets"))
190 .and_then(|raw| serde_json::from_str(raw).ok())
191 .unwrap_or_default()
192 }
193
194 pub fn set_roulette_bets(&mut self, bets: &Bets) {
195 if let Some(session) = self.current_session.as_mut() {
196 session.data.insert(
197 "bets".to_string(),
198 serde_json::to_string(bets).unwrap_or_else(|_| "{}".to_string()),
199 );
200 session.bet_amount = bets.total();
201 }
202 }
203
204 pub fn roulette_chip(&self) -> u64 {
205 self.current_session
206 .as_ref()
207 .and_then(|session| session.data.get("chip"))
208 .and_then(|value| value.parse().ok())
209 .unwrap_or(10)
210 }
211
212 pub fn set_roulette_chip(&mut self, chip: u64) {
213 if let Some(session) = self.current_session.as_mut() {
214 session.data.insert("chip".to_string(), chip.to_string());
215 }
216 }
217
218 pub fn set_roulette_number_input(&mut self, value: &str) {
219 if let Some(session) = self.current_session.as_mut() {
220 session
221 .data
222 .insert("number_input".to_string(), value.to_string());
223 }
224 }
225
226 pub fn set_roulette_status(&mut self, status: &str) {
227 if let Some(session) = self.current_session.as_mut() {
228 session
229 .data
230 .insert("status".to_string(), status.to_string());
231 if status != "error" {
232 session.data.remove("error");
233 }
234 }
235 }
236
237 pub fn apply_roulette_result(&mut self, result: RouletteResult) {
238 let Some(session) = self.current_session.as_mut() else {
239 return;
240 };
241 session.phase = GamePhase::Finished;
242 session.bet_amount = result.bet_amount;
243 session.data.remove("error");
244 session.data.insert("bets".to_string(), result.bets);
245 session
246 .data
247 .insert("number".to_string(), result.number.to_string());
248 session.data.insert("color".to_string(), result.color);
249 session
250 .data
251 .insert("winnings".to_string(), result.winnings.to_string());
252 session
253 .data
254 .insert("balance".to_string(), result.balance.to_string());
255 session
256 .data
257 .insert("status".to_string(), "result".to_string());
258 session
259 .data
260 .insert("number_input".to_string(), String::new());
261 }
262
144 pub fn get_current_game(&self) -> Option<&dyn Game> { 263 pub fn get_current_game(&self) -> Option<&dyn Game> {
145 self.current_session 264 self.current_session
146 .as_ref() 265 .as_ref()
@@ -167,6 +286,12 @@ impl GameManager {
167 if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished { 286 if session.phase != GamePhase::WaitingToStart && session.phase != GamePhase::Finished {
168 return Err(GameError::InvalidAction); 287 return Err(GameError::InvalidAction);
169 } 288 }
289 if session.game_type == "roulette" {
290 session
291 .data
292 .insert("chip".to_string(), bet.max(10).to_string());
293 return Ok(bet.max(10));
294 }
170 session.bet_amount = bet.max(10); 295 session.bet_amount = bet.max(10);
171 Ok(session.bet_amount) 296 Ok(session.bet_amount)
172 } 297 }
diff --git a/src/client/games/mod.rs b/src/client/games/mod.rs
index 63cc579..131ffbb 100644
--- a/src/client/games/mod.rs
+++ b/src/client/games/mod.rs
@@ -1,4 +1,5 @@
1pub mod manager; 1pub mod manager;
2pub mod response; 2pub mod response;
3pub mod roulette;
3pub mod slots; 4pub mod slots;
4pub mod traits; 5pub mod traits;
diff --git a/src/client/games/response.rs b/src/client/games/response.rs
index d7f9d45..73080dd 100644
--- a/src/client/games/response.rs
+++ b/src/client/games/response.rs
@@ -77,3 +77,67 @@ fn parse_indices(raw: &str) -> Vec<usize> {
77 .filter_map(|value| value.trim().parse::<usize>().ok()) 77 .filter_map(|value| value.trim().parse::<usize>().ok())
78 .collect() 78 .collect()
79} 79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct RouletteResult {
83 pub session_id: String,
84 pub winnings: u64,
85 pub bet_amount: u64,
86 pub balance: i64,
87 pub number: u8,
88 pub color: String,
89 pub bets: String,
90}
91
92pub fn parse_roulette_response(response: &str) -> Result<RouletteResult, String> {
93 let response = response.trim();
94 if response.is_empty() {
95 return Err("No response from server".to_string());
96 }
97 if let Some(message) = response.strip_prefix("ERROR") {
98 let message = message.trim();
99 return Err(if message.is_empty() {
100 "Server error".to_string()
101 } else {
102 message.to_string()
103 });
104 }
105
106 let parts: Vec<&str> = response.split_whitespace().collect();
107 if parts.first() != Some(&"ROULETTE_RESULT") || parts.len() < 8 {
108 return Err(format!("Unexpected server response: {response}"));
109 }
110
111 let winnings = parts[2]
112 .parse::<u64>()
113 .map_err(|_| format!("Invalid winnings from server: {}", parts[2]))?;
114 let bet_amount = parts[3]
115 .parse::<u64>()
116 .map_err(|_| format!("Invalid bet from server: {}", parts[3]))?;
117 let balance = parts[4]
118 .parse::<i64>()
119 .map_err(|_| format!("Invalid balance from server: {}", parts[4]))?;
120 let number = parts[5]
121 .parse::<u8>()
122 .map_err(|_| format!("Invalid roulette number: {}", parts[5]))?;
123 if number > 36 {
124 return Err(format!("Invalid roulette number: {number}"));
125 }
126 let color = parts[6].to_string();
127 if !matches!(color.as_str(), "green" | "red" | "black") {
128 return Err(format!("Invalid roulette color: {color}"));
129 }
130 let bets = parts[7].to_string();
131 let _: crate::roulette::Bets = serde_json::from_str(&bets)
132 .map_err(|error| format!("Invalid roulette bets: {error}"))?;
133
134 Ok(RouletteResult {
135 session_id: parts[1].to_string(),
136 winnings,
137 bet_amount,
138 balance,
139 number,
140 color,
141 bets,
142 })
143}
diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs
new file mode 100644
index 0000000..3379317
--- /dev/null
+++ b/src/client/games/roulette.rs
@@ -0,0 +1,157 @@
1use super::traits::{Game, GameState};
2use crate::roulette::{is_even, is_odd, Bets};
3
4pub const ROULETTE_BOARD_LINES: usize = 8;
5
6pub struct RouletteGame;
7
8impl RouletteGame {
9 pub fn new() -> Self {
10 Self
11 }
12
13 fn bets(state: &GameState) -> Bets {
14 state
15 .data
16 .get("bets")
17 .and_then(|raw| serde_json::from_str(raw).ok())
18 .unwrap_or_default()
19 }
20
21 fn bets_line(bets: &Bets) -> String {
22 let mut parts = Vec::new();
23 if bets.red > 0 {
24 parts.push(format!("RED {}", bets.red));
25 }
26 if bets.black > 0 {
27 parts.push(format!("BLACK {}", bets.black));
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
39 let list = if parts.is_empty() {
40 "(none)".to_string()
41 } else {
42 parts.join(" ")
43 };
44 format!("Bets: {} Total: {} PLN", list, bets.total())
45 }
46
47 fn result_lines(state: &GameState) -> (String, String) {
48 if let Some(error) = state.data.get("error") {
49 if !error.is_empty() {
50 return (format!("ERROR: {error}"), String::new());
51 }
52 }
53
54 match state.data.get("status").map(String::as_str) {
55 Some("result") => {
56 let number = state
57 .data
58 .get("number")
59 .map(String::as_str)
60 .unwrap_or("?");
61 let color = state
62 .data
63 .get("color")
64 .map(|value| value.to_uppercase())
65 .unwrap_or_default();
66 let parity = number
67 .parse::<u8>()
68 .ok()
69 .map(|number| {
70 if is_even(number) {
71 " EVEN"
72 } else if is_odd(number) {
73 " ODD"
74 } else {
75 ""
76 }
77 })
78 .unwrap_or("");
79 let winnings = state
80 .data
81 .get("winnings")
82 .map(String::as_str)
83 .unwrap_or("0");
84 let result = if winnings == "0" {
85 "No win.".to_string()
86 } else {
87 format!("Won {winnings} PLN")
88 };
89 (format!("Winning: {number} {color}{parity}"), result)
90 }
91 Some("spinning") => ("Spinning...".to_string(), String::new()),
92 _ => ("Place bets, then SPACE to spin.".to_string(), String::new()),
93 }
94 }
95}
96
97impl Game for RouletteGame {
98 fn get_name(&self) -> &'static str {
99 "Roulette"
100 }
101
102 fn render(&self, state: &GameState) -> String {
103 let bets = Self::bets(state);
104 let chip = state.data.get("chip").map(String::as_str).unwrap_or("10");
105 let balance = state
106 .data
107 .get("balance")
108 .map(String::as_str)
109 .unwrap_or("?");
110 let input = state
111 .data
112 .get("number_input")
113 .map(String::as_str)
114 .unwrap_or("");
115 let input_line = if input.is_empty() {
116 String::new()
117 } else {
118 format!("Number: {input}_")
119 };
120 let (status, result) = Self::result_lines(state);
121
122 format!(
123 "=== 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",
124 balance,
125 chip,
126 Self::bets_line(&bets),
127 input_line,
128 status,
129 result
130 )
131 }
132
133 fn render_lines(&self, _state: &GameState) -> usize {
134 ROULETTE_BOARD_LINES
135 }
136
137 fn get_help(&self) -> String {
138 "ROULETTE (European)\n\
139 ==================\n\
140 37 pockets: 0 plus 1-36. 0 is green and loses all outside bets.\n\
141 \n\
142 Betting:\n\
143 R / B / E / O - add the current chip to red/black/even/odd\n\
144 digits + Enter - add the current chip to that number\n\
145 C - clear all bets\n\
146 + or = / - - change the chip size\n\
147 SPACE - spin when at least one bet is placed\n\
148 Q - exit\n\
149 \n\
150 Payouts:\n\
151 straight number: 35:1 (36x returned)\n\
152 red/black: 1:1 (2x returned)\n\
153 even/odd: 1:1 (2x returned)\n\
154 Multiple bets are settled independently and added together."
155 .to_string()
156 }
157}