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]
68 pub const fn scores(mut self, scores: [u16; 2]) -> Self {
69 self.scores = scores;
70 self
71 }
72
73 #[cfg(feature = "rand")]
75 #[must_use]
76 pub fn deal(
77 rules: gin_rummy::Rules,
78 dealer: Player,
79 rng: &mut (impl rand::Rng + ?Sized),
80 ) -> Self {
81 Self::new(Round::deal(rules, dealer, rng))
82 }
83
84 #[must_use]
86 pub const fn round(&self) -> &Round {
87 &self.round
88 }
89
90 #[must_use]
92 pub const fn turn(&self) -> Option<Player> {
93 self.round.turn()
94 }
95
96 #[must_use]
98 pub const fn view(&self, seat: Player) -> View<'_> {
99 let scores = [
100 self.scores[seat as usize],
101 self.scores[seat.opponent() as usize],
102 ];
103 View::new(&self.round, seat, &self.knowledge[seat as usize], scores)
104 }
105
106 pub fn step(
119 &mut self,
120 strategy: &mut dyn Strategy,
121 ) -> Result<Option<RoundResult>, EngineError> {
122 let Some(seat) = self.round.turn() else {
123 return Ok(self.round.result());
124 };
125 let reject = |source| EngineError::IllegalAction { seat, source };
126
127 match self.round.phase() {
128 Phase::Upcard => {
129 let top = self.round.discard_pile().last().copied();
130 match strategy.offer_upcard(&self.view(seat)) {
131 UpcardAction::Take => {
132 let card = self.round.take_discard().map_err(reject)?;
133 self.knowledge[seat as usize].taken_discard = Some(card);
134 self.knowledge[seat.opponent() as usize]
135 .opponent_known
136 .insert(card);
137 }
138 UpcardAction::Pass => {
139 self.round.pass().map_err(reject)?;
140 if let Some(card) = top {
141 self.knowledge[seat.opponent() as usize]
142 .opponent_passed
143 .insert(card);
144 }
145 if self.round.phase() == Phase::Draw {
147 self.knowledge[self.round.non_dealer() as usize].forced_stock = true;
148 }
149 }
150 }
151 }
152 Phase::Draw => {
153 let action = if self.knowledge[seat as usize].forced_stock {
154 DrawAction::Stock
155 } else {
156 strategy.choose_draw(&self.view(seat))
157 };
158 match action {
159 DrawAction::Stock => {
160 self.round.draw_stock().map_err(reject)?;
161 self.knowledge[seat as usize].forced_stock = false;
162 }
163 DrawAction::TakeDiscard => {
164 let card = self.round.take_discard().map_err(reject)?;
165 self.knowledge[seat as usize].taken_discard = Some(card);
166 self.knowledge[seat.opponent() as usize]
167 .opponent_known
168 .insert(card);
169 }
170 }
171 }
172 Phase::Discard => {
173 let shed = match strategy.play_turn(&self.view(seat)) {
174 TurnAction::Discard(card) => {
175 self.round.discard(card).map_err(reject)?;
176 Some(card)
177 }
178 TurnAction::Knock { discard, melds } => {
179 self.round.knock(discard, melds).map_err(reject)?;
180 Some(discard)
181 }
182 TurnAction::BigGin(melds) => {
183 self.round.declare_big_gin(melds).map_err(reject)?;
184 None
185 }
186 };
187 self.knowledge[seat as usize].taken_discard = None;
188 if let Some(card) = shed {
189 let observer = &mut self.knowledge[seat.opponent() as usize];
190 observer.opponent_known.remove(card);
191 observer.opponent_shed.insert(card);
192 }
193 }
194 Phase::Layoff => match strategy.choose_layoff(&self.view(seat)) {
195 Some(layoff) => {
196 self.round
197 .lay_off(layoff.card, layoff.meld)
198 .map_err(reject)?;
199 self.knowledge[seat.opponent() as usize]
201 .opponent_known
202 .remove(layoff.card);
203 }
204 None => {
205 self.round.finish_layoffs().map_err(reject)?;
206 }
207 },
208 Phase::Finished => {}
209 }
210 Ok(self.round.result())
211 }
212
213 pub fn play(&mut self, strategies: [&mut dyn Strategy; 2]) -> Result<RoundResult, EngineError> {
219 loop {
220 let Some(seat) = self.round.turn() else {
221 return Ok(self.round.result().expect("a turnless round is finished"));
222 };
223 if let Some(result) = self.step(&mut *strategies[seat as usize])? {
224 return Ok(result);
225 }
226 }
227 }
228}
229
230pub fn play_round(
237 round: Round,
238 strategies: [&mut dyn Strategy; 2],
239) -> Result<RoundResult, EngineError> {
240 Table::new(round).play(strategies)
241}
242
243#[cfg(feature = "rand")]
250pub fn play_game(
251 game: &mut gin_rummy::Game,
252 strategies: [&mut dyn Strategy; 2],
253 rng: &mut (impl rand::Rng + ?Sized),
254) -> Result<gin_rummy::FinalScore, EngineError> {
255 let [one, two] = strategies;
256 while !game.is_over() {
257 let scores = [game.score(Player::One), game.score(Player::Two)];
258 let mut table = Table::new(game.deal(rng)).scores(scores);
259 let result = table.play([&mut *one, &mut *two])?;
260 game.record(result)
261 .expect("a result produced by the round it was dealt for records cleanly");
262 }
263 Ok(game.final_score().expect("a game that is over settles"))
264}