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}