Skip to main content

server/domain/scoring/
score_sheet.rs

1use constants::*;
2use itertools::*;
3use serde::{Deserialize, Serialize};
4
5use super::constants::*;
6use crate::{
7    display::format_vec,
8    domain::{
9        Card, Crib, GoStatus, Hand, Play, PlayState, Points, ScoreItem, ScoreKind, StarterCut,
10        Value,
11    },
12};
13
14/// A collection of scoring items accumulated for a hand, crib, or play phase.
15///
16/// `ScoreSheet` records individual scoring items (`ScoreItem`) and provides
17/// utility methods to calculate totals and construct common scoring scenarios
18/// such as pegging points, hand scoring, and crib scoring.
19#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
20pub struct ScoreSheet(Vec<ScoreItem>);
21
22impl ScoreSheet {
23    /// Adds a scoring event to this sheet and returns the updated sheet.
24    ///
25    /// # Parameters
26    /// - `kind`: The type of score (e.g., pair, run, fifteen).
27    /// - `cards`: The cards contributing to this scoring event.
28    /// - `points`: The number of points awarded.
29    #[must_use]
30    pub fn add_event(mut self, kind: ScoreKind, cards: &[Card], points: Points) -> Self {
31        let event = ScoreItem::new(kind, Vec::from(cards), points);
32        self.0.push(event);
33        self
34    }
35
36    /// Conditionally adds a scoring event if `condition` is true.
37    ///
38    /// # Parameters
39    /// - `condition`: Whether to record the scoring event.
40    /// - `kind`: The type of score.
41    /// - `cards`: The cards contributing to this scoring event.
42    /// - `points`: The number of points awarded.
43    #[must_use]
44    pub fn add_event_if(
45        mut self,
46        condition: bool,
47        kind: ScoreKind,
48        cards: &[Card],
49        points: Points,
50    ) -> Self {
51        if condition {
52            let event = ScoreItem::new(kind, Vec::from(cards), points);
53            self.0.push(event);
54        }
55        self
56    }
57
58    /// Returns the total points accumulated in this sheet.
59    #[must_use]
60    pub fn points(&self) -> Points {
61        self.0.iter().map(ScoreItem::points).sum()
62    }
63
64    /// Returns an immutable reference to the underlying list of scoring items.
65    #[must_use]
66    pub fn items(&self) -> &Vec<ScoreItem> {
67        &self.0
68    }
69
70    /// `ScoreSheet` constructor returning a `ScoreSheet` for the starter-card
71    ///  “his heels” bonus, if applicable.
72    ///
73    /// # Parameters
74    /// - `cut`: The starter card.
75    #[must_use]
76    pub fn his_heels(cut: Card) -> Self {
77        Self::default().add_event_if(
78            cut.is_jack(),
79            ScoreKind::HisHeels,
80            &[cut],
81            Points::from(SCORE_HIS_HEELS),
82        )
83    }
84
85    /// `ScoreSheet` constructor returning a `ScoreSheet` for the most recent
86    /// `Play` of a card in the current play state.
87    ///
88    /// # Parameters
89    /// - `play_state`: The current play state to evaluate.
90    #[must_use]
91    pub fn play_card(play_state: &PlayState) -> Self {
92        Self::default()
93            .play_card_fifteens(play_state)
94            .play_card_pairs(play_state)
95            .play_card_runs(play_state)
96            .play_card_31(play_state)
97            .play_last_card(play_state)
98    }
99
100    fn play_card_fifteens(self, play_state: &PlayState) -> Self {
101        let cards = play_state
102            .current_plays()
103            .iter()
104            .map(|p| p.card())
105            .collect::<Vec<_>>();
106
107        self.add_event_if(
108            play_state.running_total() == Value::from(15),
109            ScoreKind::Fifteen,
110            cards.as_slice(),
111            Points::from(SCORE_FIFTEEN),
112        )
113    }
114
115    fn play_card_pairs(self, play_state: &PlayState) -> Self {
116        let cards = play_state
117            .current_plays()
118            .iter()
119            .rev()
120            .map(Play::card)
121            .collect::<Vec<_>>();
122
123        let pair_info = cards.split_first().and_then(|(first, rest)| {
124            let same_face = |card: &&Card| card.face() == first.face();
125            let count = 1 + rest.iter().take_while(same_face).count();
126            match count {
127                2 => Some((ScoreKind::Pair, SCORE_PAIR)),
128                3 => Some((ScoreKind::Triplet, SCORE_ROYAL_PAIR)),
129                4 => Some((ScoreKind::Quadruplet, SCORE_DOUBLE_ROYAL_PAIR)),
130                _ => None,
131            }
132            .map(|(kind, pts)| (kind, pts, count))
133        });
134
135        match pair_info {
136            Some((kind, points, count)) => self.add_event(kind, &cards[..count], points.into()),
137            None => self,
138        }
139    }
140
141    fn play_card_runs(self, play_state: &PlayState) -> Self {
142        let cards = play_state
143            .current_plays()
144            .iter()
145            .rev()
146            .map(Play::card)
147            .collect::<Vec<_>>();
148
149        let longest_run = (MINIMUM_RUN_LENGTH..=cards.len())
150            .rev()
151            .find(|&len| {
152                let slice = &cards[..len];
153                let mut ranks: Vec<_> = slice.iter().map(|c| c.rank()).collect();
154                ranks.sort_unstable();
155                ranks.windows(2).all(|w| w[1] == w[0] + 1)
156            })
157            .map(|len| {
158                let mut run = cards[..len].to_vec();
159                run.sort_by_key(|c| c.rank());
160                (run, Points::from(len))
161            });
162
163        if let Some((cards, points)) = longest_run {
164            self.add_event(ScoreKind::Run, cards.as_slice(), points)
165        } else {
166            self
167        }
168    }
169
170    fn play_card_31(self, play_state: &PlayState) -> Self {
171        let is_31 = play_state.running_total() == PLAY_TARGET.into();
172
173        if is_31 {
174            let cards = play_state
175                .current_plays()
176                .iter()
177                .map(|p| p.card())
178                .collect::<Vec<_>>();
179            self.add_event(ScoreKind::ThirtyOne, &cards, SCORE_THIRTY_ONE.into())
180        } else {
181            self
182        }
183    }
184
185    fn play_last_card(self, play_state: &PlayState) -> Self {
186        let is_finished = play_state.is_finished();
187        let is_31 = play_state.running_total() == PLAY_TARGET.into();
188
189        if is_finished && !is_31 {
190            // Adding last card to the ScoreSheet for go enables
191            // player's go to be distinguished (and hence correctly notified!)
192            let last_card = play_state.current_plays().last().map(|p| p.card());
193            self.add_event(
194                ScoreKind::LastCard,
195                &last_card.into_iter().collect::<Vec<_>>(),
196                SCORE_GO.into(),
197            )
198        } else {
199            self
200        }
201    }
202
203    /// `ScoreSheet` constructor returning a `ScoreSheet` for the most recent
204    /// `Go` declaration.
205    ///
206    /// # Parameters
207    /// - `play_state`: The current play state to evaluate.
208    #[must_use]
209    pub fn go(play_state: &PlayState) -> Self {
210        Self::default().go_last_card(play_state)
211    }
212
213    fn go_last_card(self, play_state: &PlayState) -> Self {
214        // Adding last card to the ScoreSheet for go enables
215        // player's go to be distinguished (and hence correctly notified!)
216        let last_card = play_state.current_plays().last().map(|p| p.card());
217
218        self.add_event_if(
219            play_state.go_status() != &GoStatus::NotCalled,
220            ScoreKind::LastCard,
221            &last_card.into_iter().collect::<Vec<_>>(),
222            Points::from(SCORE_GO),
223        )
224    }
225
226    /// `ScoreSheet` constructor returning a `ScoreSheet` for the given hand and starter cut.
227    ///
228    /// # Parameters
229    /// - `hand`: The player’s hand.
230    /// - `cut`: The starter card.
231    #[must_use]
232    pub fn hand(hand: &Hand, cut: StarterCut) -> Self {
233        let mut all = hand.clone();
234        all.add(cut);
235
236        Self::default()
237            .fifteens(all.as_ref())
238            .pairs(all.as_ref())
239            .runs(all.as_ref())
240            .flush(hand.as_ref(), cut, 4)
241            .nobs(hand.as_ref(), cut)
242    }
243
244    /// `ScoreSheet` constructor returning a `ScoreSheet` for the crib and starter cut.
245    ///
246    /// # Parameters
247    /// - `crib`: The crib cards.
248    /// - `cut`: The starter card.
249    #[must_use]
250    pub fn crib(crib: &Crib, cut: StarterCut) -> Self {
251        let mut all = crib.clone();
252        all.add(cut);
253
254        Self::default()
255            .fifteens(all.as_ref())
256            .pairs(all.as_ref())
257            .runs(all.as_ref())
258            .flush(crib.as_ref(), cut, 5)
259            .nobs(crib.as_ref(), cut)
260    }
261
262    fn fifteens(self, cards: &[Card]) -> Self {
263        (2..=cards.len())
264            .flat_map(|n| cards.iter().combinations(n))
265            .filter(|combo| combo.iter().map(|c| c.value()).sum::<Value>() == 15.into())
266            .fold(self, |acc, combo| {
267                let combo_cards: Vec<Card> = combo.iter().copied().copied().collect();
268                acc.add_event(ScoreKind::Fifteen, &combo_cards, SCORE_FIFTEEN.into())
269            })
270    }
271
272    fn pairs(self, cards: &[Card]) -> Self {
273        cards
274            .iter()
275            .copied()
276            .combinations(2)
277            .filter(|pair| pair[0].face() == pair[1].face())
278            .fold(self, |acc, pair| {
279                acc.add_event(ScoreKind::Pair, &pair, SCORE_PAIR.into())
280            })
281    }
282
283    fn runs(self, cards: &[Card]) -> Self {
284        let mut scores = Vec::default();
285
286        let mut cards = Vec::from(cards);
287        cards.sort_by_key(|c| c.rank());
288
289        for len in (MINIMUM_RUN_LENGTH..=cards.len()).rev() {
290            let mut points = Points::default();
291
292            for combination in cards.iter().combinations(len) {
293                let differences = combination
294                    .windows(2)
295                    .map(|cs| cs[1].rank() - cs[0].rank())
296                    .collect::<Vec<_>>();
297
298                let sequential = differences.iter().all(|d| *d == 1);
299                if sequential {
300                    let combination = combination.into_iter().cloned().collect::<Vec<_>>();
301                    points = Points::from(combination.len());
302                    scores.push((combination, points))
303                }
304            }
305
306            if points != Points::default() {
307                break;
308            }
309        }
310
311        scores
312            .into_iter()
313            .fold(self, |acc, e| acc.add_event(ScoreKind::Run, &e.0, e.1))
314    }
315
316    fn flush(self, cards: &[Card], cut: StarterCut, constraint: usize) -> Self {
317        let mut all = Vec::from(cards);
318        all.push(cut);
319
320        let all_same_suit = |cs: &[Card]| cs.iter().map(|c| c.suit()).all_equal();
321
322        let points_if_flush = |cs: &[Card]| {
323            (cs.len() >= constraint)
324                .then_some(all_same_suit(cs).then(|| Points::from(cs.len())))
325                .flatten()
326        };
327
328        if let Some(points) = points_if_flush(&all) {
329            self.add_event(ScoreKind::Flush, &all, points)
330        } else if let Some(points) = points_if_flush(cards) {
331            self.add_event(ScoreKind::Flush, cards, points)
332        } else {
333            self
334        }
335    }
336
337    fn nobs(self, cards: &[Card], cut: StarterCut) -> Self {
338        let mut matched = cards
339            .iter()
340            .filter(|c| c.is_jack() && c.suit() == cut.suit());
341
342        if let Some(card) = matched.next() {
343            self.add_event(ScoreKind::Nobs, &[*card], SCORE_NOBS.into())
344        } else {
345            self
346        }
347    }
348}
349
350impl std::fmt::Display for ScoreSheet {
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        let events = format_vec(self.0.as_slice());
353        events.fmt(f)
354    }
355}
356
357#[cfg(test)]
358mod test {
359    use std::str::FromStr;
360
361    use super::*;
362    use crate::domain::{
363        Card, Hand, PLAYER0, PLAYER1,
364        test::domain_macros::{card, hand},
365    };
366
367    #[test]
368    fn impossible_pairs_will_return_0() {
369        let hand1 = hand!("AHADAH2C");
370        let hand2 = hand!("ACAS2H2D");
371
372        let mut play_state = PlayState::new(PLAYER0)
373            .with_pending_plays(PLAYER0, hand1.as_ref())
374            .with_pending_plays(PLAYER1, hand2.as_ref());
375        let _ = play_state.play(card!("AH"));
376        let _ = play_state.play(card!("AC"));
377        let _ = play_state.play(card!("AD"));
378        let _ = play_state.play(card!("AS"));
379        let _ = play_state.play(card!("AH"));
380
381        assert_eq!(
382            ScoreSheet::play_card(&play_state).points(),
383            Points::default()
384        );
385    }
386}