Skip to main content

cardpack/funky/types/
hands.rs

1use crate::preludes::funky::{BCardType, BuffoonCard, MPip};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(
6    Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize,
7)]
8pub enum HandType {
9    #[default]
10    HighCard,
11    Pair,
12    TwoPair,
13    ThreeOfAKind,
14    Straight,
15    Flush,
16    FullHouse,
17    FourOfAKind,
18    StraightFlush,
19    RoyalFlush,
20    FiveOfAKind,
21    FlushHouse,
22    FlushFive,
23}
24
25#[derive(
26    Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize,
27)]
28pub struct PokerHand {
29    pub hand_type: HandType,
30    pub level: usize,
31    pub chips: usize,
32    pub mult: usize,
33    pub times_played: usize,
34}
35
36impl PokerHand {
37    #[must_use]
38    pub fn new(hand_type: HandType, chips: usize, mult: usize) -> Self {
39        Self {
40            hand_type,
41            level: 1,
42            chips,
43            mult,
44            times_played: 0,
45        }
46    }
47}
48
49#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
50pub struct PokerHands {
51    pub hands: HashMap<HandType, PokerHand>,
52}
53
54impl PokerHands {
55    #[must_use]
56    pub fn new() -> Self {
57        let mut hands = HashMap::new();
58        hands.insert(HandType::HighCard, PokerHand::new(HandType::HighCard, 5, 1));
59        hands.insert(HandType::Pair, PokerHand::new(HandType::Pair, 10, 2));
60        hands.insert(HandType::TwoPair, PokerHand::new(HandType::TwoPair, 20, 2));
61        hands.insert(
62            HandType::ThreeOfAKind,
63            PokerHand::new(HandType::ThreeOfAKind, 30, 3),
64        );
65        hands.insert(
66            HandType::Straight,
67            PokerHand::new(HandType::Straight, 30, 4),
68        );
69        hands.insert(HandType::Flush, PokerHand::new(HandType::Flush, 35, 4));
70        hands.insert(
71            HandType::FullHouse,
72            PokerHand::new(HandType::FullHouse, 40, 4),
73        );
74        hands.insert(
75            HandType::FourOfAKind,
76            PokerHand::new(HandType::FourOfAKind, 50, 7),
77        );
78        hands.insert(
79            HandType::StraightFlush,
80            PokerHand::new(HandType::StraightFlush, 100, 8),
81        );
82        hands.insert(
83            HandType::FiveOfAKind,
84            PokerHand::new(HandType::FiveOfAKind, 120, 12),
85        );
86        hands.insert(
87            HandType::FlushHouse,
88            PokerHand::new(HandType::FlushHouse, 140, 14),
89        );
90        hands.insert(
91            HandType::FlushFive,
92            PokerHand::new(HandType::FlushFive, 160, 16),
93        );
94        Self { hands }
95    }
96
97    #[must_use]
98    pub fn get(&self, hand_type: &HandType) -> Option<&PokerHand> {
99        self.hands.get(hand_type)
100    }
101
102    pub fn get_mut(&mut self, hand_type: &HandType) -> Option<&mut PokerHand> {
103        self.hands.get_mut(hand_type)
104    }
105
106    pub fn increment(&mut self, planet_card: BuffoonCard) {
107        if planet_card.card_type == BCardType::Planet {
108            if let MPip::ChipsMultPlusOnHand(chips, mult, hand_type) = planet_card.enhancement {
109                if let Some(poker_hand) = self.get_mut(&hand_type) {
110                    poker_hand.chips += chips;
111                    poker_hand.mult += mult;
112                    poker_hand.level += 1;
113                }
114            }
115        }
116    }
117
118    pub fn play_hand(&mut self, hand_type: &HandType) {
119        if let Some(poker_hand) = self.get_mut(hand_type) {
120            poker_hand.times_played += 1;
121        }
122    }
123
124    /// The per-level chips/mult each hand gains — Balatro's level-up table.
125    ///
126    /// Kept here rather than read off the planet cards because the planet data is
127    /// not a clean per-hand map (two planets both target `FlushFive`, and
128    /// `FlushHouse` has none), so this is the one source of truth a whole-table
129    /// level-up can trust. Reconciling the planet consts against it is EPIC-01
130    /// Story 3 debt, out of scope here.
131    const LEVEL_UP: [(HandType, usize, usize); 12] = [
132        (HandType::HighCard, 10, 1),
133        (HandType::Pair, 15, 1),
134        (HandType::TwoPair, 20, 1),
135        (HandType::ThreeOfAKind, 20, 2),
136        (HandType::Straight, 30, 3),
137        (HandType::Flush, 15, 2),
138        (HandType::FullHouse, 25, 2),
139        (HandType::FourOfAKind, 30, 3),
140        (HandType::StraightFlush, 40, 4),
141        (HandType::FiveOfAKind, 35, 3),
142        (HandType::FlushHouse, 40, 4),
143        (HandType::FlushFive, 50, 3),
144    ];
145
146    /// Level **every** poker hand up by one — Black Hole's effect. Each hand
147    /// gains its canonical per-level chips/mult and one level, exactly as its
148    /// planet card would, but for all twelve at once.
149    pub fn increment_all(&mut self) {
150        for (hand_type, chips, mult) in Self::LEVEL_UP {
151            if let Some(poker_hand) = self.get_mut(&hand_type) {
152                poker_hand.chips += chips;
153                poker_hand.mult += mult;
154                poker_hand.level += 1;
155            }
156        }
157    }
158}
159
160impl Default for PokerHands {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166#[cfg(test)]
167#[allow(non_snake_case, unused_imports)]
168mod funky__types__hands_tests {
169    use super::*;
170    use crate::funky::decks::planet;
171    use crate::preludes::funky::*;
172    use rstest::rstest;
173
174    #[test]
175    fn increment() {
176        let mut hands = PokerHands::default();
177        let expected = PokerHand {
178            hand_type: HandType::HighCard,
179            level: 2,
180            chips: 15,
181            mult: 2,
182            times_played: 0,
183        };
184
185        hands.increment(planet::card::PLUTO);
186
187        assert_eq!(hands.get(&HandType::HighCard).unwrap(), &expected);
188    }
189
190    #[test]
191    fn increment_all__levels_every_hand_by_one() {
192        let mut hands = PokerHands::default();
193        hands.increment_all();
194
195        // HighCard: level 1→2, chips 5→15 (+10), mult 1→2 (+1).
196        let high = hands.get(&HandType::HighCard).unwrap();
197        assert_eq!((high.level, high.chips, high.mult), (2, 15, 2));
198
199        // Every hand type gained exactly one level.
200        for hand_type in [
201            HandType::Pair,
202            HandType::TwoPair,
203            HandType::ThreeOfAKind,
204            HandType::Straight,
205            HandType::Flush,
206            HandType::FullHouse,
207            HandType::FourOfAKind,
208            HandType::StraightFlush,
209            HandType::FiveOfAKind,
210            HandType::FlushHouse,
211            HandType::FlushFive,
212        ] {
213            assert_eq!(hands.get(&hand_type).unwrap().level, 2, "{hand_type:?}");
214        }
215    }
216
217    #[test]
218    fn play_hand() {
219        let mut hands = PokerHands::default();
220        let expected = PokerHand {
221            hand_type: HandType::HighCard,
222            level: 1,
223            chips: 5,
224            mult: 1,
225            times_played: 1,
226        };
227
228        hands.play_hand(&HandType::HighCard);
229
230        assert_eq!(hands.get(&HandType::HighCard).unwrap(), &expected);
231    }
232
233    #[test]
234    fn hand_type__serde_round_trip() {
235        for hand_type in [
236            HandType::HighCard,
237            HandType::Pair,
238            HandType::TwoPair,
239            HandType::ThreeOfAKind,
240            HandType::Straight,
241            HandType::Flush,
242            HandType::FullHouse,
243            HandType::FourOfAKind,
244            HandType::StraightFlush,
245            HandType::RoyalFlush,
246            HandType::FiveOfAKind,
247            HandType::FlushHouse,
248            HandType::FlushFive,
249        ] {
250            let json = serde_json::to_string(&hand_type).unwrap();
251
252            assert_eq!(serde_json::from_str::<HandType>(&json).unwrap(), hand_type);
253        }
254    }
255
256    /// Pins the wire format: a consumer parsing this JSON breaks if it changes.
257    #[test]
258    fn hand_type__serde_format() {
259        assert_eq!(
260            serde_json::to_string(&HandType::FlushFive).unwrap(),
261            r#""FlushFive""#
262        );
263    }
264}