use crate::view::Knowledge;
use crate::{DrawAction, Strategy, TurnAction, UpcardAction, View};
use gin_rummy::round::RoundError;
use gin_rummy::{Phase, Player, Round, RoundResult};
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum EngineError {
#[error("{seat} chose an illegal action")]
IllegalAction {
seat: Player,
#[source]
source: RoundError,
},
}
#[derive(Debug, Clone)]
pub struct Table {
round: Round,
knowledge: [Knowledge; 2],
}
impl Table {
#[must_use]
pub fn new(round: Round) -> Self {
debug_assert_eq!(round.phase(), Phase::Upcard);
Self {
round,
knowledge: [Knowledge::default(); 2],
}
}
#[cfg(feature = "rand")]
#[must_use]
pub fn deal(
rules: gin_rummy::Rules,
dealer: Player,
rng: &mut (impl rand::Rng + ?Sized),
) -> Self {
Self::new(Round::deal(rules, dealer, rng))
}
#[must_use]
pub const fn round(&self) -> &Round {
&self.round
}
#[must_use]
pub const fn turn(&self) -> Option<Player> {
self.round.turn()
}
#[must_use]
pub const fn view(&self, seat: Player) -> View<'_> {
View::new(&self.round, seat, &self.knowledge[seat as usize])
}
pub fn step(
&mut self,
strategy: &mut dyn Strategy,
) -> Result<Option<RoundResult>, EngineError> {
let Some(seat) = self.round.turn() else {
return Ok(self.round.result());
};
let reject = |source| EngineError::IllegalAction { seat, source };
match self.round.phase() {
Phase::Upcard => {
let top = self.round.discard_pile().last().copied();
match strategy.offer_upcard(&self.view(seat)) {
UpcardAction::Take => {
let card = self.round.take_discard().map_err(reject)?;
self.knowledge[seat as usize].taken_discard = Some(card);
self.knowledge[seat.opponent() as usize]
.opponent_known
.insert(card);
}
UpcardAction::Pass => {
self.round.pass().map_err(reject)?;
if let Some(card) = top {
self.knowledge[seat.opponent() as usize]
.opponent_passed
.insert(card);
}
if self.round.phase() == Phase::Draw {
self.knowledge[self.round.non_dealer() as usize].forced_stock = true;
}
}
}
}
Phase::Draw => {
let action = if self.knowledge[seat as usize].forced_stock {
DrawAction::Stock
} else {
strategy.choose_draw(&self.view(seat))
};
match action {
DrawAction::Stock => {
self.round.draw_stock().map_err(reject)?;
self.knowledge[seat as usize].forced_stock = false;
}
DrawAction::TakeDiscard => {
let card = self.round.take_discard().map_err(reject)?;
self.knowledge[seat as usize].taken_discard = Some(card);
self.knowledge[seat.opponent() as usize]
.opponent_known
.insert(card);
}
}
}
Phase::Discard => {
let shed = match strategy.play_turn(&self.view(seat)) {
TurnAction::Discard(card) => {
self.round.discard(card).map_err(reject)?;
Some(card)
}
TurnAction::Knock { discard, melds } => {
self.round.knock(discard, melds).map_err(reject)?;
Some(discard)
}
TurnAction::BigGin(melds) => {
self.round.declare_big_gin(melds).map_err(reject)?;
None
}
};
self.knowledge[seat as usize].taken_discard = None;
if let Some(card) = shed {
let observer = &mut self.knowledge[seat.opponent() as usize];
observer.opponent_known.remove(card);
observer.opponent_shed.insert(card);
}
}
Phase::Layoff => match strategy.choose_layoff(&self.view(seat)) {
Some(layoff) => {
self.round
.lay_off(layoff.card, layoff.meld)
.map_err(reject)?;
self.knowledge[seat.opponent() as usize]
.opponent_known
.remove(layoff.card);
}
None => {
self.round.finish_layoffs().map_err(reject)?;
}
},
Phase::Finished => {}
}
Ok(self.round.result())
}
pub fn play(&mut self, strategies: [&mut dyn Strategy; 2]) -> Result<RoundResult, EngineError> {
loop {
let Some(seat) = self.round.turn() else {
return Ok(self.round.result().expect("a turnless round is finished"));
};
if let Some(result) = self.step(&mut *strategies[seat as usize])? {
return Ok(result);
}
}
}
}
pub fn play_round(
round: Round,
strategies: [&mut dyn Strategy; 2],
) -> Result<RoundResult, EngineError> {
Table::new(round).play(strategies)
}
#[cfg(feature = "rand")]
pub fn play_game(
game: &mut gin_rummy::Game,
strategies: [&mut dyn Strategy; 2],
rng: &mut (impl rand::Rng + ?Sized),
) -> Result<gin_rummy::FinalScore, EngineError> {
let [one, two] = strategies;
while !game.is_over() {
let mut table = Table::new(game.deal(rng));
let result = table.play([&mut *one, &mut *two])?;
game.record(result)
.expect("a result produced by the round it was dealt for records cleanly");
}
Ok(game.final_score().expect("a game that is over settles"))
}