summaryrefslogtreecommitdiffstats
path: root/src/roulette.rs
blob: b59f92e53b5d7c1e2f59a65ce3ece1b1bf4f6dfa (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! European roulette rules shared by the server and client.

use serde::{Deserialize, Serialize};

/// European wheel clockwise from zero.
pub const WHEEL: [u8; 37] = [
    0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5,
    24, 16, 33, 1, 20, 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26,
];

pub const RED_NUMBERS: [u8; 18] = [
    1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36,
];

/// Total return on a winning straight (35:1 plus the stake).
pub const STRAIGHT_RETURN: u64 = 36;
/// Total return on a winning outside bet (1:1 plus the stake).
pub const EVEN_MONEY_RETURN: u64 = 2;

pub fn is_red(number: u8) -> bool {
    RED_NUMBERS.contains(&number)
}

pub fn is_black(number: u8) -> bool {
    number != 0 && !is_red(number)
}

pub fn color(number: u8) -> &'static str {
    if number == 0 {
        "green"
    } else if is_red(number) {
        "red"
    } else {
        "black"
    }
}

pub fn is_even(number: u8) -> bool {
    number != 0 && number % 2 == 0
}

pub fn is_odd(number: u8) -> bool {
    number != 0 && number % 2 == 1
}

pub fn rtp() -> f64 {
    36.0 / 37.0
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NumberBet {
    pub number: u8,
    pub amount: u64,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bets {
    #[serde(default)]
    pub red: u64,
    #[serde(default)]
    pub black: u64,
    #[serde(default)]
    pub even: u64,
    #[serde(default)]
    pub odd: u64,
    #[serde(default)]
    pub numbers: Vec<NumberBet>,
}

impl Bets {
    pub fn total(&self) -> u64 {
        let mut total = self
            .red
            .saturating_add(self.black)
            .saturating_add(self.even)
            .saturating_add(self.odd);
        for bet in &self.numbers {
            total = total.saturating_add(bet.amount);
        }
        total
    }

    pub fn is_empty(&self) -> bool {
        self.total() == 0
    }

    /// Total returned for a winning number, including returned stakes.
    pub fn winnings(&self, number: u8) -> u64 {
        let mut winnings = 0u64;

        if number != 0 {
            if is_red(number) {
                winnings = winnings.saturating_add(self.red.saturating_mul(EVEN_MONEY_RETURN));
            } else {
                winnings =
                    winnings.saturating_add(self.black.saturating_mul(EVEN_MONEY_RETURN));
            }
            if is_even(number) {
                winnings = winnings.saturating_add(self.even.saturating_mul(EVEN_MONEY_RETURN));
            } else {
                winnings = winnings.saturating_add(self.odd.saturating_mul(EVEN_MONEY_RETURN));
            }
        }

        for bet in &self.numbers {
            if bet.number == number {
                winnings = winnings
                    .saturating_add(bet.amount.saturating_mul(STRAIGHT_RETURN));
            }
        }

        winnings
    }

    pub fn validate(&self, balance: u64) -> Result<(), String> {
        if self.is_empty() {
            return Err("Place at least one bet".to_string());
        }
        if self.total() > balance {
            return Err("Insufficient balance".to_string());
        }
        for bet in &self.numbers {
            if bet.number > 36 {
                return Err(format!("Invalid number: {}", bet.number));
            }
            if bet.amount == 0 {
                return Err("A bet amount must be positive".to_string());
            }
        }
        Ok(())
    }
}