Skip to main content

casino/
slots.rs

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}