Skip to main content

ceelo_core/
lib.rs

1//! This library is meant to be an "engine" for simulating the Cee-Lo Game. The game's mechanics aren't very simple, so I wrote this library to make sure I got it right and not mix game logic with implementation details.
2
3use std::{io::Write, rc::Rc};
4
5use either::Either;
6use rand::random_range;
7
8/// Stand in for the information known at the time the player or bank will make a bet.
9pub enum WagerData {
10    PlayerWager,
11    BankWager,
12}
13
14/// Types implenting this Trait will be able to make an informed wager.
15pub trait Wager {
16    fn wager(&self, data: WagerData) -> u32;
17}
18
19#[derive(Eq, Hash, PartialEq, Clone, Debug)]
20pub struct Player {
21    name: String,
22}
23
24/// Identifier for Players in a  Cee-Lo Game.
25impl Player {
26    /// Introduces named players
27    #[must_use]
28    pub const fn new(name: String) -> Self {
29        Self { name }
30    }
31}
32
33/// Stores the amount of money a player has in addition to their betting strategy.
34#[derive(Clone)]
35pub struct PlayerData {
36    points: u32,
37    wager: Rc<dyn Wager>,
38}
39
40impl PlayerData {
41    /// Initialises a some new player data using the betting strategy `wager` and initial amount of money `points`.
42    pub const fn new(wager: Rc<dyn Wager>, points: u32) -> Self {
43        Self { points, wager }
44    }
45}
46
47impl std::fmt::Debug for PlayerData {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("PlayerData")
50            .field("points", &self.points)
51            .field(
52                "wager",
53                &"Wager is `Rc<dyn Wager>` and therefore not `Debug`able",
54            )
55            .finish()
56    }
57}
58
59pub type Bet = u32;
60/// This helper function helps to keep score of all of the bets.
61// return type is (faded_bank_bet, player_left_over_bet)
62#[must_use]
63pub const fn fade_bet(bank_bet: u32, player_bet: u32) -> (u32, u32) {
64    let leftovers = bank_bet.saturating_sub(player_bet);
65
66    (
67        leftovers,
68        if leftovers == 0 { bank_bet } else { player_bet },
69    )
70}
71
72/// In Cee-Lo, it is necessary for the bank to make an initial bet through the use of it's `wager` function. The other players will then in order choose some portion of the bank's bet to "cover". We accomplish this by calling the players' `wager` functions. Once the bank's bet is completely "covered", no more betting can occur until the round ends.
73#[must_use]
74pub fn fade_bets(players: Vec<(Player, PlayerData)>) -> Vec<(Player, PlayerData, Bet)> {
75    // call bank and other players' wager function progressively "updating" wagers
76    let (bank, players) = {
77        let mut players = players;
78        (players.remove(0), players)
79    };
80    let original_bank_wager = bank.1.wager.wager(WagerData::BankWager);
81    let mut bank_wager = original_bank_wager;
82    let players: Vec<(Player, PlayerData, Bet)> = players
83        .into_iter()
84        .map(|(player, player_data)| {
85            let faded_wager = fade_bet(bank_wager, player_data.wager.wager(WagerData::PlayerWager));
86            bank_wager = faded_wager.0;
87            (player, player_data, faded_wager.1)
88        })
89        .collect();
90    let mut new_players = players;
91    let (bank, bank_data) = bank;
92    new_players.insert(0, (bank, bank_data, original_bank_wager));
93    new_players
94}
95
96/// Helper function that rolls 3d6 and gives the result.
97#[must_use]
98pub fn roll_dice() -> (u8, u8, u8) {
99    (
100        random_range(1..6 + 1),
101        random_range(1..6 + 1),
102        random_range(1..6 + 1),
103    )
104}
105
106/// This struct is necessary because whenever a valid roll occurs in Cee-Lo, we can evaluate it:
107/// - `X-X-X`, `4-5-6` and `X-X-6` are all instant wins if rolled
108/// - Finally, `X-X-Y`, where 6 != Y != 1, is setting a point of Y
109/// - `1-2-3` and `X-X-1` are instant losses
110///
111/// This struct represents these three cases.
112#[derive(PartialEq, Debug, Eq, Clone)]
113pub enum Rolls {
114    InstantWin { multiplier: u8 },
115    Point { point: u8, other: u8 }, // point is never 6 because that's instant win tho
116    Loss,
117}
118
119/// Type representing the need to Reroll the dice in hopes of a valid combination.
120#[derive(Debug)]
121pub struct Reroll;
122
123/// This Trait impl indicated that not all dice rolls are valid. In fact, the only valid dice rolls are of the form:
124/// - Triples `X-X-X`
125/// - Doubles `X-X-Y`
126/// - Four-Five-Six `4-5-6`
127/// - One-Two-Three `1-2-3`
128///
129/// In Cee-Lo if one of these combinations isn't rolled the player must reroll.
130impl TryFrom<(u8, u8, u8)> for Rolls {
131    type Error = Reroll;
132
133    fn try_from(value: (u8, u8, u8)) -> Result<Self, Self::Error> {
134        match (value.0 == value.1, value.1 == value.2) {
135            (true, true) => Ok(Self::InstantWin {
136                multiplier: if value.0 == 1 { 5 } else { 3 },
137            }),
138            (true, false) => {
139                if value.2 == 6 {
140                    Ok(Self::InstantWin { multiplier: 1 })
141                } else if value.2 == 1 {
142                    Ok(Self::Loss)
143                } else {
144                    Ok(Self::Point {
145                        point: value.2,
146                        other: value.0,
147                    })
148                }
149            }
150            (false, true) => {
151                if value.0 == 6 {
152                    Ok(Self::InstantWin { multiplier: 1 })
153                } else if value.0 == 1 {
154                    Ok(Self::Loss)
155                } else {
156                    Ok(Self::Point {
157                        point: value.0,
158                        other: value.1,
159                    })
160                }
161            }
162            (false, false) => {
163                if value.0 == value.2 {
164                    if value.1 == 6 {
165                        Ok(Self::InstantWin { multiplier: 1 })
166                    } else if value.1 == 1 {
167                        Ok(Self::Loss)
168                    } else {
169                        Ok(Self::Point {
170                            point: value.1,
171                            other: value.0,
172                        })
173                    }
174                } else {
175                    let p = [value.0, value.1, value.2];
176                    if p.contains(&4) && p.contains(&5) && p.contains(&6) {
177                        Ok(Self::InstantWin { multiplier: 2 })
178                    } else if p.contains(&1) && p.contains(&2) && p.contains(&3) {
179                        Ok(Self::Loss)
180                    } else {
181                        Err(Reroll)
182                    }
183                }
184            }
185        }
186    }
187}
188
189/// Using recursion to our advantage to always get a valid roll. This will run forever if you are particularly unlucky.
190#[must_use]
191pub fn always_roll() -> ((u8, u8, u8), Rolls) {
192    let mut raw = roll_dice();
193    loop {
194        if let Ok(res) = Rolls::try_from(raw) {
195            return (raw, res);
196        }
197        raw = roll_dice();
198    }
199}
200
201/// All players roll the dice in this phase. Both the raw dice result and "Quality" are stored for later.
202#[must_use]
203pub fn everyone_roll(
204    players: Vec<(Player, PlayerData, Bet)>,
205) -> Vec<(Player, PlayerData, Bet, (u8, u8, u8), Rolls)> {
206    let mut rolls = vec![];
207    for (player, player_data, wager) in players {
208        let (roll_raw, roll) = always_roll();
209        rolls.push((player, player_data, wager, roll_raw, roll));
210    }
211    rolls
212}
213
214/// Struct for storing if the Bank or Player had a higher quality dice roll than each other and how. In Cee-Lo, payouts can be up to 5 times the initial bet.
215#[derive(Clone)]
216pub enum WinOrLose {
217    InstWin(u8),
218    Win,
219    Tie,
220    Lose(u8),
221}
222
223/// Helper function to determine how the players in the game won or lost
224// it's sufficent to track the players besides the bank
225#[must_use]
226pub fn wins_and_losses(
227    players: Vec<(Player, PlayerData, Bet, (u8, u8, u8), Rolls)>,
228) -> Vec<(
229    Player,
230    PlayerData,
231    Bet,
232    (u8, u8, u8),
233    Rolls,
234    Option<WinOrLose>,
235)> {
236    let (bank, players) = {
237        let mut players = players;
238        (players.remove(0), players)
239    };
240
241    match bank.4 {
242        // bank gets all the money
243        Rolls::InstantWin { multiplier } => vec![players
244            .into_iter()
245            .map(|(player, pd, b, rr, r)| (player, pd, b, rr, r, Some(WinOrLose::Lose(multiplier))))
246            .collect::<Vec<(
247                Player,
248                PlayerData,
249                Bet,
250                (u8, u8, u8),
251                Rolls,
252                Option<WinOrLose>,
253            )>>()]
254        .into_iter()
255        .fold(std::iter::empty().collect(), |_, x| {
256            let mut unpushed = x;
257            let (bpl, bpd, bbet, brr, br) = &bank;
258
259            unpushed.insert(0, (bpl.clone(), bpd.clone(), *bbet, *brr, br.clone(), None));
260            unpushed
261        }),
262        Rolls::Point {
263            point: bank_point,
264            other: _,
265        } => {
266            let mut win_and_losses = Vec::new();
267            for (player, pd, b, rr, player_roll) in players {
268                match player_roll {
269                    Rolls::InstantWin { multiplier } => {
270                        win_and_losses.push((
271                            player,
272                            pd,
273                            b,
274                            rr,
275                            player_roll,
276                            Some(WinOrLose::InstWin(multiplier)),
277                        ));
278                    }
279                    Rolls::Point {
280                        point: player_point,
281                        other: _,
282                    } => match player_point.cmp(&bank_point) {
283                        std::cmp::Ordering::Less => {
284                            win_and_losses.push((
285                                player,
286                                pd,
287                                b,
288                                rr,
289                                player_roll,
290                                Some(WinOrLose::Lose(1)),
291                            ));
292                        }
293                        std::cmp::Ordering::Equal => {
294                            win_and_losses.push((
295                                player,
296                                pd,
297                                b,
298                                rr,
299                                player_roll,
300                                Some(WinOrLose::Tie),
301                            ));
302                        }
303                        std::cmp::Ordering::Greater => {
304                            win_and_losses.push((
305                                player,
306                                pd,
307                                b,
308                                rr,
309                                player_roll,
310                                Some(WinOrLose::Win),
311                            ));
312                        }
313                    },
314                    Rolls::Loss => {
315                        win_and_losses.push((
316                            player,
317                            pd,
318                            b,
319                            rr,
320                            player_roll,
321                            Some(WinOrLose::Lose(1)),
322                        ));
323                    }
324                }
325            }
326            let (pl, pd, bet, rr, rolls) = bank;
327            let new_bank = (pl, pd, bet, rr, rolls, None);
328            win_and_losses.insert(0, new_bank);
329            win_and_losses
330        }
331        // bank pays out in this situation
332        Rolls::Loss => {
333            let mut handled: Vec<(
334                Player,
335                PlayerData,
336                u32,
337                (u8, u8, u8),
338                Rolls,
339                Option<WinOrLose>,
340            )> = players
341                .into_iter()
342                .map(|(player, pd, b, rr, r)| (player, pd, b, rr, r, Some(WinOrLose::InstWin(1))))
343                .collect();
344            let (pl, pd, bet, rr, rolls) = bank;
345            let new_bank = (pl, pd, bet, rr, rolls, Some(WinOrLose::Lose(1)));
346            handled.insert(0, new_bank);
347            handled
348        }
349    }
350}
351
352impl WinOrLose {
353    /// Helper predicate to seperate winners from losers. I decided it would be more fair to have the bank take money from losers first and then give to winners. This way, there's a lower likelihood that the bank will run out of money to pay out winners.
354    #[must_use]
355    pub const fn is_win(&self) -> bool {
356        match self {
357            Self::InstWin(_) | Self::Win | Self::Tie => true,
358            Self::Lose(_) => false,
359        }
360    }
361}
362
363/// Seperates the bank from the players as they don't work the same way as regular players when paying out bets, since it revolves around them.
364#[must_use]
365pub fn wins_and_losses_no_bank(
366    players: Vec<(
367        Player,
368        PlayerData,
369        u32,
370        (u8, u8, u8),
371        Rolls,
372        Option<WinOrLose>,
373    )>,
374) -> (
375    (
376        Player,
377        PlayerData,
378        u32,
379        (u8, u8, u8),
380        Rolls,
381        Option<WinOrLose>,
382    ),
383    Vec<(Player, PlayerData, u32, (u8, u8, u8), Rolls, WinOrLose)>,
384) {
385    let mut the_players = players;
386    let bank = the_players.remove(0);
387    let some_players = the_players
388        .into_iter()
389        .filter_map(|(pl, pd, bet, rr, roll, won_or_lost)| {
390            won_or_lost.map(|wol| (pl, pd, bet, rr, roll, wol))
391        })
392        .collect();
393    (bank, some_players)
394}
395
396/// In this stage, players exchange money and a new bank is chosen if need be.
397#[must_use]
398pub fn payout(
399    players: Vec<(
400        Player,
401        PlayerData,
402        Bet,
403        (u8, u8, u8),
404        Rolls,
405        Option<WinOrLose>,
406    )>,
407) -> Vec<(Player, PlayerData, Bet, (u8, u8, u8), Rolls)> {
408    let (mut bank, the_players) = wins_and_losses_no_bank(players);
409    let losers_then_winners = {
410        let (mut losers, mut winners): (
411            Vec<(Player, PlayerData, u32, (u8, u8, u8), Rolls, WinOrLose)>,
412            _,
413        ) = the_players
414            .into_iter()
415            .partition(|player| player.5.is_win());
416
417        losers.append(&mut winners);
418        losers
419    };
420    let mut upd_players = vec![];
421    let mut winners = 0usize;
422    let mut instant_won = None;
423    for (player, pd, bet, rr, roll, win_or_lose) in losers_then_winners {
424        match win_or_lose {
425            WinOrLose::InstWin(mult) => {
426                let (bank_leftover, player_left_over) =
427                    fade_bet(bank.1.points, bet.saturating_mul(mult.into()));
428                bank.1.points = bank_leftover;
429                let new_player = (
430                    player.clone(),
431                    PlayerData {
432                        points: pd.points.saturating_add(player_left_over),
433                        wager: pd.wager,
434                    },
435                    bet,
436                    rr,
437                    roll,
438                );
439                instant_won = Some(new_player.clone());
440                upd_players.push(new_player);
441            }
442            WinOrLose::Win => {
443                winners = winners.saturating_add(1);
444                let (bank_leftover, player_leftover) = fade_bet(bank.1.points, bet);
445                bank.1.points = bank_leftover;
446                upd_players.push((
447                    player,
448                    PlayerData {
449                        points: pd.points.saturating_add(player_leftover),
450                        wager: pd.wager,
451                    },
452                    bet,
453                    rr,
454                    roll,
455                ));
456            }
457            WinOrLose::Tie => upd_players.push((player, pd, bet, rr, roll)),
458            WinOrLose::Lose(mult) => {
459                let (player_leftover, player_loss) =
460                    fade_bet(pd.points, bet.saturating_mul(mult.into()));
461                bank.1.points = bank.1.points.saturating_add(player_loss);
462
463                upd_players.push((
464                    player,
465                    PlayerData {
466                        points: player_leftover,
467                        wager: pd.wager,
468                    },
469                    bet,
470                    rr,
471                    roll,
472                ));
473            }
474        }
475    }
476    let new_bank = {
477        let (bank_player, bank_player_data, bbet, brr, brolls, ..) = bank;
478        (bank_player, bank_player_data, bbet, brr, brolls)
479    };
480
481    if let Some(next_bank) = instant_won {
482        upd_players.push(new_bank);
483        // remove from front until front = next_bank if names match
484        loop {
485            let front = upd_players.remove(0);
486            // make player the bank
487            if front.0 == next_bank.0 {
488                upd_players.insert(0, front);
489                break;
490            }
491            upd_players.push(front);
492        }
493    } else if winners == upd_players.len().saturating_add(1) || bank.5.is_some() {
494        upd_players.push(new_bank);
495    } else {
496        upd_players.insert(0, new_bank);
497    }
498    upd_players
499}
500
501/// Returns the only player standing or multiple players.
502#[must_use]
503pub fn winner(
504    players: Vec<(Player, PlayerData, Bet, (u8, u8, u8), Rolls)>,
505) -> Either<
506    (Player, PlayerData, Bet, (u8, u8, u8), Rolls),
507    Vec<(Player, PlayerData, Bet, (u8, u8, u8), Rolls)>,
508> {
509    match players {
510        mut lone if lone.len() == 1 => Either::Left(lone.remove(0)),
511        multiple => either::Right(multiple),
512    }
513}
514
515/// Simple Alias for the history of the game.
516pub type History = Vec<Vec<(Player, PlayerData, Bet, (u8, u8, u8), Rolls)>>;
517
518/// This is the game loop that decides the winner of the game. Returns the winning player and the game history.
519#[must_use]
520pub fn play_until_winner(
521    mut players: Vec<(Player, PlayerData)>,
522) -> ((Player, PlayerData, Bet, (u8, u8, u8), Rolls), History) {
523    let mut history = vec![];
524    loop {
525        let payed = payout(wins_and_losses(everyone_roll(fade_bets(players.clone()))));
526        history.push(payed.clone());
527        // for (player, pd, bet, (r1, r2, r3), roll) in payed.clone() {
528        //     println!("<{player}> <{pd}> <{bet}> <{roll}> <({r1}, {r2}, {r3})>");
529        // }
530        let maybe_winner = winner(payed);
531        let mut the_winner: Option<(Player, PlayerData, Bet, (u8, u8, u8), Rolls)> = None;
532        match maybe_winner {
533            Either::Left(winner) => the_winner = Some(winner),
534            Either::Right(not_winners) => {
535                // println!("no winner");
536                players = not_winners
537                    .into_iter()
538                    .map(|(player, pd, ..)| (player, pd))
539                    .collect();
540                players.retain(|player| player.1.points != 0);
541            }
542        }
543        if let Some(winner) = the_winner {
544            return (winner, history);
545        }
546    }
547}
548
549/// Wager struct for betting arbitrary amounts of money.
550pub struct WagerN(
551    /// Amount of money being bet each round.
552    pub u32,
553);
554
555impl Wager for WagerN {
556    /// Simply wagers the amount of money specified by `WagerN`
557    fn wager(&self, _: WagerData) -> u32 {
558        self.0
559    }
560}
561
562/// Basic implementation of players wagering at a console.
563pub struct WagerConsole;
564impl Wager for WagerConsole {
565    /// Wagers by parsing user input. Guarantees user input using loops.
566    fn wager(&self, _data: WagerData) -> u32 {
567        loop {
568            let mut buffer = String::new();
569            print!("Wager Here => ");
570            let _ = std::io::stdout().flush();
571            if std::io::stdin().read_line(&mut buffer).is_ok() {
572                if let Ok(i) = buffer.trim().parse::<u32>() {
573                    return i;
574                }
575                println!("Failed to parse");
576            }
577        }
578    }
579}
580
581/// Implements wagers for all basic functions that return a `u32`. Example:
582/// ```
583/// fn random_wager(_: WagerData) -> u32 {
584///     rand::random_range(0..500)
585/// }
586///
587/// fn main() {
588/// let pdata = PlayerData::new(Rc::new(random_wager), 300);
589/// }
590/// ```
591impl<T: Fn(WagerData) -> u32> Wager for T {
592    /// Simply calls the function on `WagerData`.
593    fn wager(&self, data: WagerData) -> u32 {
594        self(data)
595    }
596}
597
598#[must_use]
599pub const fn add(left: u64, right: u64) -> u64 {
600    left.saturating_add(right)
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn it_works() {
609        let result = add(2, 2);
610        assert_eq!(result, 4);
611    }
612}