1use core::fmt;
2use std::{
3 io::{stdout, Write},
4 thread::sleep,
5 time::Duration,
6};
7
8use anyhow::Result;
9use colored::*;
10use crossterm::{cursor, terminal, QueueableCommand};
11use inquire::Select;
12use itertools::Itertools;
13use rand::{
14 distr::{weighted::WeightedIndex, Distribution},
15 rng, Rng,
16};
17use rust_decimal::Decimal;
18
19use crate::{money::Money, Casino};
20
21pub fn play_slots() -> Result<()> {
22 let mut casino = Casino::from_filesystem()?;
23
24 let options = [
25 Money::from_major(1),
26 Money::from_major(5),
27 Money::from_major(10),
28 Money::from_major(25),
29 Money::from_major(100),
30 Money::from_major(500),
31 Money::from_major(1_000),
32 Money::from_major(5_000),
33 Money::from_major(25_000),
34 Money::from_major(100_000),
35 ]
36 .iter()
37 .filter(|&m| *m <= casino.bankroll)
38 .map(|m| PriceTier::new(*m))
39 .collect();
40
41 let bet_selection = Select::new(
42 format!("Which slot machine to use? (you have {}) ", casino.bankroll).as_str(),
43 options,
44 )
45 .prompt()
46 .unwrap();
47 let bet_amount = bet_selection.cost;
48
49 casino.bankroll -= bet_amount;
50 casino.stats.slots.record_pull(bet_amount);
51
52 casino.save();
53 println!(
54 "{}",
55 format!("* You insert your money into the {bet_amount} slot machine.").dimmed()
56 );
57 println!("You now have {} in the bank", casino.bankroll);
58 sleep(Duration::from_millis(600));
59 println!("{}", "* You pull the arm of the slot machine.".dimmed());
60 sleep(Duration::from_millis(600));
61 println!("{}", "* The wheels start spinning.".dimmed());
62
63 let slot_machine = SlotMachine::new_with_default_symbols(bet_selection.multiplier);
64
65 let mut rng = rng();
66 let mut position = 0.0;
67 let mut velocity = rng.random_range(20.0..40.0);
68 let accel = rng.random_range(-10.0..-5.0);
69
70 let mut stdout = stdout();
71
72 let mut selected: Vec<&Symbol> = slot_machine.pull();
73
74 while velocity > 0.0 {
75 if position >= 1.0 {
76 selected = slot_machine.pull();
77 position -= 1.0;
78 }
79
80 stdout.queue(cursor::SavePosition).unwrap();
81
82 stdout
83 .write_all(
84 format!(
85 "▶ {}{}{}{}{} ◀",
86 selected[0], selected[1], selected[2], selected[3], selected[4]
87 )
88 .as_bytes(),
89 )
90 .unwrap();
91
92 stdout.queue(cursor::RestorePosition).unwrap();
93 stdout.flush().unwrap();
94
95 sleep(Duration::from_millis(16));
96
97 velocity += accel * (16.0 / 1000.0);
98 position += velocity * (16.0 / 1000.0);
99
100 stdout.queue(cursor::RestorePosition).unwrap();
101 stdout
102 .queue(terminal::Clear(terminal::ClearType::FromCursorDown))
103 .unwrap();
104 }
105
106 stdout
107 .write_all(
108 format!(
109 "▶ {}{}{}{}{} ◀",
110 selected[0], selected[1], selected[2], selected[3], selected[4]
111 )
112 .as_bytes(),
113 )
114 .unwrap();
115 println!();
116
117 let mut total_payout = Money::ZERO;
118
119 let pay_table = slot_machine.payout(selected);
120
121 if !pay_table.is_empty() {
122 println!();
123 }
124
125 for entry in pay_table.iter() {
126 println!(" {} × {} = {}", entry.symbol, entry.count, entry.payout);
127 total_payout += entry.payout;
128 }
129
130 println!();
131 println!("Payout: {total_payout}");
132
133 casino.bankroll += total_payout;
134 casino.stats.update_bankroll(casino.bankroll);
135
136 if total_payout > Money::ZERO {
137 casino.stats.slots.record_win(total_payout);
138 }
139
140 casino.check_for_mister_green();
141
142 casino.save();
143
144 Ok(())
145}
146
147type Symbol = char;
148type Weight = u32;
149
150#[derive(Clone, Debug)]
151pub struct SlotMachine {
152 multiplier: f32,
153 weights: Vec<(Symbol, Weight)>,
154 distribution: WeightedIndex<Weight>,
155}
156
157impl SlotMachine {
158 pub fn new_with_default_symbols(multiplier: f32) -> Self {
159 let symbols = vec![
160 ('🍋', 30),
161 ('🍒', 30),
162 ('🍊', 30),
163 ('🍉', 30),
164 ('🔔', 20),
165 ('🍌', 20),
166 ('🍫', 10),
167 ('💰', 2),
168 ('💎', 1),
169 ];
170 let weights: Vec<u32> = symbols.iter().map(|s| s.1).collect();
171 Self {
172 multiplier,
173 weights: symbols,
174 distribution: WeightedIndex::new(weights).unwrap(),
175 }
176 }
177
178 pub fn add_symbol(&mut self, symbol: char, weight: Weight) {
179 self.weights.push((symbol, weight));
180 self.distribution = WeightedIndex::new(self.weights.iter().map(|i| i.1)).unwrap();
181 }
182
183 pub fn payout(&self, symbols: Vec<&Symbol>) -> Vec<PayTableEntry> {
184 let mut entries = vec![];
185
186 let counts = symbols.iter().counts();
187
188 for (symbol, count) in counts.iter() {
189 if *count >= 3 {
190 let sym: char = ***symbol;
191 let sym_weight = self.weights.iter().find(|(s, _w)| s == &sym).unwrap().1;
192 let sym_value = (self.multiplier * 120.0 / sym_weight as f32) as i64;
193 let sym_payout = Money::from_major(sym_value * (count - 2) as i64);
194
195 entries.push(PayTableEntry::new(sym, *count, sym_payout));
196 }
197 }
198
199 entries
200 }
201
202 pub fn pull(&self) -> Vec<&Symbol> {
203 let mut rng = rng();
204 let samples: Vec<usize> = self
205 .distribution
206 .clone()
207 .sample_iter(&mut rng)
208 .take(5)
209 .collect();
210 samples.iter().map(|i| &self.weights[*i].0).collect()
211 }
212}
213
214pub struct SlotMachineOutput {
215 pub entries: Vec<PayTableEntry>,
216}
217
218impl SlotMachineOutput {}
219
220pub struct PayTableEntry {
221 pub symbol: Symbol,
222 pub count: usize,
223 pub payout: Money,
224}
225
226impl PayTableEntry {
227 pub fn new(symbol: Symbol, count: usize, payout: Money) -> Self {
228 Self {
229 symbol,
230 count,
231 payout,
232 }
233 }
234}
235
236struct PriceTier {
237 pub cost: Money,
238 pub multiplier: f32,
239}
240
241impl PriceTier {
242 pub fn new(cost: Money) -> Self {
243 let mult: Decimal = cost.into();
244 Self {
245 cost,
246 multiplier: mult.try_into().unwrap(),
247 }
248 }
249}
250
251impl fmt::Display for PriceTier {
252 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253 write!(f, "{} per pull", self.cost)
254 }
255}
256
257#[cfg(test)]
258mod test {
259 use rust_decimal::Decimal;
260
261 use crate::{
262 money::Money,
263 slots::{PayTableEntry, SlotMachine},
264 };
265
266 #[test]
267 fn test_symbols_return_to_player() {
268 let slot_machine = SlotMachine::new_with_default_symbols(1.0);
269
270 let mut total_player_payment = Money::ZERO;
271 let mut total_player_return = Money::ZERO;
272
273 for _i in 1..10_000 {
274 total_player_payment += Money::from_major(1);
275
276 let payout: Vec<PayTableEntry> = slot_machine.payout(slot_machine.pull());
277
278 for pay_table_entry in payout.iter() {
279 total_player_return += pay_table_entry.payout;
280 }
281 }
282
283 let total_return: Decimal = total_player_return.into();
284 let total_payment: Decimal = total_player_payment.into();
285 let rtp_ratio: f32 = (total_return / total_payment).try_into().unwrap();
286
287 assert!(
288 rtp_ratio >= 0.80,
289 "Return-to-player ratio is {rtp_ratio}, which should be higher than 0.80"
290 );
291 assert!(
292 rtp_ratio < 1.0,
293 "Return-to-player ratio is {rtp_ratio}, which should be less than 1.0"
294 );
295 }
296}