gin_rummy_engine/
driver.rs1use crate::view::Knowledge;
9use crate::{DrawAction, Strategy, TurnAction, UpcardAction, View};
10use gin_rummy::round::RoundError;
11use gin_rummy::{Phase, Player, Round, RoundResult};
12use thiserror::Error;
13
14#[derive(Debug, Error)]
16#[non_exhaustive]
17pub enum EngineError {
18 #[error("{seat} chose an illegal action")]
20 IllegalAction {
21 seat: Player,
23 #[source]
25 source: RoundError,
26 },
27}
28
29#[derive(Debug, Clone)]
35pub struct Table {
36 round: Round,
37 knowledge: [Knowledge; 2],
38 scores: [u16; 2],
39}
40
41impl Table {
42 #[must_use]
52 pub fn new(round: Round) -> Self {
53 debug_assert_eq!(round.phase(), Phase::Upcard);
54 Self {
55 round,
56 knowledge: [Knowledge::default(); 2],
57 scores: [0; 2],
58 }
59 }
60
61 #[must_use]
67 pub const fn scores(mut self, scores: [u16; 2]) -> Self {
68 self.scores = scores;
69 self
70 }
71
72 #[cfg(feature = "rand")]
74 #[must_use]
75 pub fn deal(
76 rules: gin_rummy::Rules,
77 dealer: Player,
78 rng: &mut (impl rand::Rng + ?Sized),
79 ) -> Self {
80 Self::new(Round::deal(rules, dealer, rng))
81 }
82
83 #[must_use]
85 pub const fn round(&self) -> &Round {
86 &self.round
87 }
88
89 #[must_use]
91 pub const fn turn(&self) -> Option<Player> {
92 self.round.turn()
93 }
94
95 #[must_use]
97 pub const fn view(&self, seat: Player) -> View<'_> {
98 let scores = [
99 self.scores[seat as usize],
100 self.scores[seat.opponent() as usize],
101 ];
102 View::new(&self.round, seat, &self.knowledge[seat as usize], scores)
103 }
104
105 pub fn step(
118 &mut self,
119 strategy: &mut dyn Strategy,
120 ) -> Result<Option<RoundResult>, EngineError> {
121 let Some(seat) = self.round.turn() else {
122 return Ok(self.round.result());
123 };
124 let reject = |source| EngineError::IllegalAction { seat, source };
125
126 match self.round.phase() {
127 Phase::Upcard => {
128 let top = self.round.discard_pile().last().copied();
129 match strategy.offer_upcard(&self.view(seat)) {
130 UpcardAction::Take => {
131 let card = self.round.take_discard().map_err(reject)?;
132 self.knowledge[seat as usize].taken_discard = Some(card);
133 self.knowledge[seat.opponent() as usize]
134 .opponent_known
135 .insert(card);
136 }
137 UpcardAction::Pass => {
138 self.round.pass().map_err(reject)?;
139 if let Some(card) = top {
140 self.knowledge[seat.opponent() as usize]
141 .opponent_passed
142 .insert(card);
143 }
144 if self.round.phase() == Phase::Draw {
146 self.knowledge[self.round.non_dealer() as usize].forced_stock = true;
147 }
148 }
149 }
150 }
151 Phase::Draw => {
152 let action = if self.knowledge[seat as usize].forced_stock {
153 DrawAction::Stock
154 } else {
155 strategy.choose_draw(&self.view(seat))
156 };
157 match action {
158 DrawAction::Stock => {
159 self.round.draw_stock().map_err(reject)?;
160 self.knowledge[seat as usize].forced_stock = false;
161 }
162 DrawAction::TakeDiscard => {
163 let card = self.round.take_discard().map_err(reject)?;
164 self.knowledge[seat as usize].taken_discard = Some(card);
165 self.knowledge[seat.opponent() as usize]
166 .opponent_known
167 .insert(card);
168 }
169 }
170 }
171 Phase::Discard => {
172 let shed = match strategy.play_turn(&self.view(seat)) {
173 TurnAction::Discard(card) => {
174 self.round.discard(card).map_err(reject)?;
175 Some(card)
176 }
177 TurnAction::Knock { discard, melds } => {
178 self.round.knock(discard, melds).map_err(reject)?;
179 Some(discard)
180 }
181 TurnAction::BigGin(melds) => {
182 self.round.declare_big_gin(melds).map_err(reject)?;
183 None
184 }
185 };
186 self.knowledge[seat as usize].taken_discard = None;
187 if let Some(card) = shed {
188 let observer = &mut self.knowledge[seat.opponent() as usize];
189 observer.opponent_known.remove(card);
190 observer.opponent_shed.insert(card);
191 }
192 }
193 Phase::Layoff => match strategy.choose_layoff(&self.view(seat)) {
194 Some(layoff) => {
195 self.round
196 .lay_off(layoff.card, layoff.meld)
197 .map_err(reject)?;
198 self.knowledge[seat.opponent() as usize]
200 .opponent_known
201 .remove(layoff.card);
202 }
203 None => {
204 self.round.finish_layoffs().map_err(reject)?;
205 }
206 },
207 Phase::Finished => {}
208 }
209 Ok(self.round.result())
210 }
211
212 pub fn play(&mut self, strategies: [&mut dyn Strategy; 2]) -> Result<RoundResult, EngineError> {
218 loop {
219 let Some(seat) = self.round.turn() else {
220 return Ok(self.round.result().expect("a turnless round is finished"));
221 };
222 if let Some(result) = self.step(&mut *strategies[seat as usize])? {
223 return Ok(result);
224 }
225 }
226 }
227}
228
229pub fn play_round(
236 round: Round,
237 strategies: [&mut dyn Strategy; 2],
238) -> Result<RoundResult, EngineError> {
239 Table::new(round).play(strategies)
240}
241
242#[cfg(feature = "rand")]
249pub fn play_game(
250 game: &mut gin_rummy::Game,
251 strategies: [&mut dyn Strategy; 2],
252 rng: &mut (impl rand::Rng + ?Sized),
253) -> Result<gin_rummy::FinalScore, EngineError> {
254 let [one, two] = strategies;
255 while !game.is_over() {
256 let scores = [game.score(Player::One), game.score(Player::Two)];
257 let mut table = Table::new(game.deal(rng)).scores(scores);
258 let result = table.play([&mut *one, &mut *two])?;
259 game.record(result)
260 .expect("a result produced by the round it was dealt for records cleanly");
261 }
262 Ok(game.final_score().expect("a game that is over settles"))
263}