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    /// This seat's lead in the game score: its running total minus the
69    /// opponent's, in points
70    ///
71    /// Positive when ahead in the game, negative when behind.  Always zero
72    /// for a standalone round played outside a [`Game`](gin_rummy::Game),
73    /// which has no scoreboard.  The game score is public — both players
74    /// see it — so it is part of the legal whitelist.  The target to win
75    /// is [`rules().game_target`](gin_rummy::Rules::game_target).
76    #[must_use]
77    pub const fn game_margin(&self) -> i32 {
78        self.scores[0] as i32 - self.scores[1] as i32
79    }
80
81    /// The running game totals, this seat's first: `[mine, theirs]`
82    ///
83    /// Note the order: seat-relative, unlike [`Table::scores`], which is
84    /// indexed by [`Player`].  Both totals sit on the scoreboard in plain
85    /// sight, so they are part of the legal whitelist.  `[0, 0]` for a
86    /// standalone round played outside a [`Game`](gin_rummy::Game); the
87    /// target to win is
88    /// [`rules().game_target`](gin_rummy::Rules::game_target).
89    ///
90    /// [`Table::scores`]: crate::Table::scores
91    #[must_use]
92    pub const fn game_scores(&self) -> [u16; 2] {
93        self.scores
94    }
95
96    /// The scoring rules of the round
97    #[must_use]
98    pub const fn rules(&self) -> &'a Rules {
99        self.round.rules()
100    }
101
102    /// The dealer of the round
103    #[must_use]
104    pub const fn dealer(&self) -> Player {
105        self.round.dealer()
106    }
107
108    /// The knock limit in effect
109    #[must_use]
110    pub const fn knock_limit(&self) -> u8 {
111        self.round.knock_limit()
112    }
113
114    /// The current phase of the round
115    #[must_use]
116    pub const fn phase(&self) -> Phase {
117        self.round.phase()
118    }
119
120    /// The discard pile, oldest first — the last card is the top
121    #[must_use]
122    pub fn discard_pile(&self) -> &'a [Card] {
123        self.round.discard_pile()
124    }
125
126    /// The top of the discard pile
127    #[must_use]
128    pub fn upcard(&self) -> Option<Card> {
129        self.round.discard_pile().last().copied()
130    }
131
132    /// How many cards remain in the stock — the order is never visible
133    #[must_use]
134    pub fn stock_len(&self) -> usize {
135        self.round.stock().len()
136    }
137
138    /// The knocker's spread, extended by any layoffs so far
139    ///
140    /// Empty before a knock.  Enumeration order gives the meld indices that
141    /// [`Layoff::meld`](crate::Layoff::meld) addresses.
142    pub fn spread(&self) -> impl Iterator<Item = Meld> + 'a {
143        self.round.spread()
144    }
145
146    /// The knocker, if the round has reached a knock
147    #[must_use]
148    pub const fn knocker(&self) -> Option<Player> {
149        self.round.knocker()
150    }
151
152    /// This seat's hand
153    #[must_use]
154    pub const fn hand(&self) -> Hand {
155        self.round.hand(self.seat)
156    }
157
158    /// The card this seat took from the pile this turn, which may not be
159    /// shed until the next turn
160    #[must_use]
161    pub const fn taken_discard(&self) -> Option<Card> {
162        self.know.taken_discard
163    }
164
165    /// Whether taking the top of the discard pile is available
166    ///
167    /// True at the upcard offer and on a normal draw, false on the forced
168    /// stock draw after both players pass the upcard.
169    #[must_use]
170    pub const fn can_take_discard(&self) -> bool {
171        match self.round.phase() {
172            Phase::Upcard => true,
173            Phase::Draw => !self.know.forced_stock,
174            Phase::Discard | Phase::Layoff | Phase::Finished => false,
175        }
176    }
177
178    /// Cards known to be in the opponent's hand: taken from the pile and
179    /// not yet shed or laid off
180    #[must_use]
181    pub const fn opponent_known(&self) -> Hand {
182        self.know.opponent_known
183    }
184
185    /// Every card the opponent has discarded, whether or not it is still in
186    /// the pile
187    #[must_use]
188    pub const fn opponent_shed(&self) -> Hand {
189        self.know.opponent_shed
190    }
191
192    /// Upcards the opponent declined during the upcard phase
193    #[must_use]
194    pub const fn opponent_passed(&self) -> Hand {
195        self.know.opponent_passed
196    }
197
198    /// How many cards the opponent holds — 10, or 11 mid-turn
199    #[must_use]
200    pub const fn opponent_hand_len(&self) -> usize {
201        self.round.hand(self.seat.opponent()).len()
202    }
203
204    /// The cards this seat cannot locate: the whole deck minus its own
205    /// hand, the discard pile, the opponent's known cards, and the spread
206    ///
207    /// Exactly the cards a determinizing bot must distribute between the
208    /// stock and the hidden part of the opponent's hand.  Until a knock
209    /// reveals the spread,
210    /// `unseen().len() == stock_len() + opponent_hand_len() -
211    /// opponent_known().len()`; afterwards the spread also counts as seen.
212    #[must_use]
213    pub fn unseen(&self) -> Hand {
214        let seen = self
215            .round
216            .discard_pile()
217            .iter()
218            .fold(self.hand() | self.know.opponent_known, |acc, &card| {
219                acc | card.into()
220            });
221        let seen = self
222            .round
223            .spread()
224            .fold(seen, |acc, meld| acc | meld.cards());
225        Hand::ALL - seen
226    }
227
228    /// The minimum deadwood of this seat's hand
229    #[must_use]
230    pub fn deadwood(&self) -> u8 {
231        deadwood(self.hand())
232    }
233
234    /// A deadwood-minimizing arrangement of this seat's hand
235    #[must_use]
236    pub fn best_melds(&self) -> Melds {
237        best_melds(self.hand())
238    }
239}