Skip to main content

gin_rummy_engine/
view.rs

1//! The [`View`]: what one seat may legally see of a round
2//!
3//! [`Round`] exposes the whole position — both hands and the stock order —
4//! and leaves information hygiene to its consumers.  This module is that
5//! hygiene: a [`View`] borrows the round privately and re-exposes only the
6//! whitelist of legally visible information, paired with the per-seat
7//! [`Knowledge`] that the driver accumulates and that no round snapshot can
8//! recover (which discards the opponent took, what they declined, whether
9//! this turn's draw is forced from the stock).
10
11use gin_rummy::{Card, Hand, Meld, Melds, Phase, Player, Round, Rules, best_melds, deadwood};
12
13/// What a seat has learned beyond the public table state
14///
15/// Owned and updated by the driver as actions are applied; a `Round`
16/// snapshot alone cannot reconstruct it.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub(crate) struct Knowledge {
19    /// Cards known to be in the opponent's hand: taken from the pile and
20    /// not yet shed or laid off
21    pub(crate) opponent_known: Hand,
22    /// Every card the opponent has discarded, whether or not it is still
23    /// in the pile
24    pub(crate) opponent_shed: Hand,
25    /// Upcards the opponent declined during the upcard phase
26    pub(crate) opponent_passed: Hand,
27    /// The card this seat took from the pile this turn, which may not be
28    /// shed until the next turn
29    pub(crate) taken_discard: Option<Card>,
30    /// The next draw must come from the stock (both players passed the
31    /// upcard)
32    pub(crate) forced_stock: bool,
33}
34
35/// What one seat may legally see of a round
36///
37/// The wrapped [`Round`] is private, and these accessors are the whitelist:
38/// the opponent's hand and the stock order are structurally unreachable.
39pub struct View<'a> {
40    round: &'a Round,
41    seat: Player,
42    know: &'a Knowledge,
43    /// Running game totals, this seat's first
44    scores: [u16; 2],
45}
46
47impl<'a> View<'a> {
48    pub(crate) const fn new(
49        round: &'a Round,
50        seat: Player,
51        know: &'a Knowledge,
52        scores: [u16; 2],
53    ) -> Self {
54        Self {
55            round,
56            seat,
57            know,
58            scores,
59        }
60    }
61
62    /// The seat this view belongs to
63    #[must_use]
64    pub const fn seat(&self) -> Player {
65        self.seat
66    }
67
68    /// The running game totals, this seat's first: `[mine, theirs]`
69    ///
70    /// Note the order: seat-relative, unlike [`Table::scores`], which is
71    /// indexed by [`Player`].  Both totals sit on the scoreboard in plain
72    /// sight, so they are part of the legal whitelist.  `[0, 0]` for a
73    /// standalone round played outside a [`Game`](gin_rummy::Game); the
74    /// target to win is
75    /// [`rules().game_target`](gin_rummy::Rules::game_target).
76    ///
77    /// [`Table::scores`]: crate::Table::scores
78    #[must_use]
79    pub const fn game_scores(&self) -> [u16; 2] {
80        self.scores
81    }
82
83    /// The scoring rules of the round
84    #[must_use]
85    pub const fn rules(&self) -> &'a Rules {
86        self.round.rules()
87    }
88
89    /// The dealer of the round
90    #[must_use]
91    pub const fn dealer(&self) -> Player {
92        self.round.dealer()
93    }
94
95    /// The knock limit in effect
96    #[must_use]
97    pub const fn knock_limit(&self) -> u8 {
98        self.round.knock_limit()
99    }
100
101    /// The current phase of the round
102    #[must_use]
103    pub const fn phase(&self) -> Phase {
104        self.round.phase()
105    }
106
107    /// The discard pile, oldest first — the last card is the top
108    #[must_use]
109    pub fn discard_pile(&self) -> &'a [Card] {
110        self.round.discard_pile()
111    }
112
113    /// The top of the discard pile
114    #[must_use]
115    pub fn upcard(&self) -> Option<Card> {
116        self.round.discard_pile().last().copied()
117    }
118
119    /// How many cards remain in the stock — the order is never visible
120    #[must_use]
121    pub fn stock_len(&self) -> usize {
122        self.round.stock().len()
123    }
124
125    /// The knocker's spread, extended by any layoffs so far
126    ///
127    /// Empty before a knock.  Enumeration order gives the meld indices that
128    /// [`Layoff::meld`](crate::Layoff::meld) addresses.
129    pub fn spread(&self) -> impl Iterator<Item = Meld> + 'a {
130        self.round.spread()
131    }
132
133    /// The knocker, if the round has reached a knock
134    #[must_use]
135    pub const fn knocker(&self) -> Option<Player> {
136        self.round.knocker()
137    }
138
139    /// This seat's hand
140    #[must_use]
141    pub const fn hand(&self) -> Hand {
142        self.round.hand(self.seat)
143    }
144
145    /// The card this seat took from the pile this turn, which may not be
146    /// shed until the next turn
147    #[must_use]
148    pub const fn taken_discard(&self) -> Option<Card> {
149        self.know.taken_discard
150    }
151
152    /// Whether taking the top of the discard pile is available
153    ///
154    /// True at the upcard offer and on a normal draw, false on the forced
155    /// stock draw after both players pass the upcard.
156    #[must_use]
157    pub const fn can_take_discard(&self) -> bool {
158        match self.round.phase() {
159            Phase::Upcard => true,
160            Phase::Draw => !self.know.forced_stock,
161            Phase::Discard | Phase::Layoff | Phase::Finished => false,
162        }
163    }
164
165    /// Cards known to be in the opponent's hand: taken from the pile and
166    /// not yet shed or laid off
167    #[must_use]
168    pub const fn opponent_known(&self) -> Hand {
169        self.know.opponent_known
170    }
171
172    /// Every card the opponent has discarded, whether or not it is still in
173    /// the pile
174    #[must_use]
175    pub const fn opponent_shed(&self) -> Hand {
176        self.know.opponent_shed
177    }
178
179    /// Upcards the opponent declined during the upcard phase
180    #[must_use]
181    pub const fn opponent_passed(&self) -> Hand {
182        self.know.opponent_passed
183    }
184
185    /// How many cards the opponent holds — 10, or 11 mid-turn
186    #[must_use]
187    pub const fn opponent_hand_len(&self) -> usize {
188        self.round.hand(self.seat.opponent()).len()
189    }
190
191    /// The cards this seat cannot locate: the whole deck minus its own
192    /// hand, the discard pile, the opponent's known cards, and the spread
193    ///
194    /// Exactly the cards a determinizing bot must distribute between the
195    /// stock and the hidden part of the opponent's hand.  Until a knock
196    /// reveals the spread,
197    /// `unseen().len() == stock_len() + opponent_hand_len() -
198    /// opponent_known().len()`; afterwards the spread also counts as seen.
199    #[must_use]
200    pub fn unseen(&self) -> Hand {
201        let seen = self
202            .round
203            .discard_pile()
204            .iter()
205            .fold(self.hand() | self.know.opponent_known, |acc, &card| {
206                acc | card.into()
207            });
208        let seen = self
209            .round
210            .spread()
211            .fold(seen, |acc, meld| acc | meld.cards());
212        Hand::ALL - seen
213    }
214
215    /// The minimum deadwood of this seat's hand
216    #[must_use]
217    pub fn deadwood(&self) -> u8 {
218        deadwood(self.hand())
219    }
220
221    /// A deadwood-minimizing arrangement of this seat's hand
222    #[must_use]
223    pub fn best_melds(&self) -> Melds {
224        best_melds(self.hand())
225    }
226}