use crate::error::GfError;
use crate::game::Game;
use crate::player::Player;
use crate::rules::GameVariant;
use super::record::{GameCollection, GameRecord};
fn parse_variant(name: &str) -> Result<GameVariant, GfError> {
match name {
"Standard Go Fish" => Ok(GameVariant::Standard),
"Happy Families" => Ok(GameVariant::HappyFamilies),
"Quartet" => Ok(GameVariant::Quartet),
other => Err(GfError::ParseError(format!(
"unknown game variant: {other:?}"
))),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ReplayResult {
pub game_id: String,
pub is_consistent: bool,
pub final_books: Vec<usize>,
pub mismatch_at_turn: Option<usize>,
}
impl GameRecord {
pub fn replay(&self) -> Result<ReplayResult, GfError> {
if self.turns.is_empty() {
return Ok(ReplayResult {
game_id: self.id.clone(),
is_consistent: true,
final_books: vec![],
mismatch_at_turn: None,
});
}
let all_actions = self
.turns
.iter()
.map(|turn| turn.actions.clone().ok_or(GfError::NoReplayData))
.collect::<Result<Vec<_>, _>>()?;
let draw_pile = self
.initial_draw_pile
.clone()
.ok_or(GfError::NoReplayData)?;
let variant = parse_variant(&self.variant)?;
let players: Vec<Player> = self
.players
.iter()
.map(|name| Player::new(name.clone()))
.collect();
let mut game = Game::new_with_deck(variant, players, draw_pile)?;
for (_, actions) in self.turns.iter().zip(all_actions) {
for action in actions {
game.act(action)?;
}
}
let final_record = game.record();
for (i, (stored, replayed)) in self.turns.iter().zip(final_record.turns.iter()).enumerate()
{
if replayed.books_after_turn != stored.books_after_turn {
return Ok(ReplayResult {
game_id: self.id.clone(),
is_consistent: false,
final_books: replayed.books_after_turn.clone(),
mismatch_at_turn: Some(i),
});
}
}
if final_record.turns.len() != self.turns.len() {
let mismatch_at_turn = self.turns.len().min(final_record.turns.len());
let final_books = final_record
.turns
.last()
.map(|t| t.books_after_turn.clone())
.unwrap_or_default();
return Ok(ReplayResult {
game_id: self.id.clone(),
is_consistent: false,
final_books,
mismatch_at_turn: Some(mismatch_at_turn),
});
}
let is_consistent = final_record.winner == self.winner;
let final_books = final_record
.turns
.last()
.map(|t| t.books_after_turn.clone())
.unwrap_or_default();
Ok(ReplayResult {
game_id: self.id.clone(),
is_consistent,
final_books,
mismatch_at_turn: None,
})
}
}
impl GameCollection {
pub fn replay_all(&self) -> Vec<Result<ReplayResult, GfError>> {
self.games.iter().map(GameRecord::replay).collect()
}
}
#[cfg(test)]
mod tests {
use crate::bot::BotProfile;
use crate::error::GfError;
use crate::game::{Game, GamePhase, PlayerAction};
use crate::history::record::{GameCollection, GameRecord, TurnRecord};
use crate::player::Player;
use crate::rules::GameVariant;
fn play_to_completion() -> GameRecord {
let profiles = [BotProfile::basic("Alice"), BotProfile::basic("Bob")];
let players = vec![
Player::new("Alice".to_string()),
Player::new("Bob".to_string()),
];
let mut game = Game::new(GameVariant::Standard, players).unwrap();
while !game.is_over() {
let state = game.state().unwrap();
let cp = state.current_player;
let action = match state.phase {
GamePhase::WaitingForDraw => PlayerAction::Draw,
GamePhase::GameOver => break,
_ => {
let hand = state
.players
.iter()
.find(|v| v.index == cp)
.and_then(|v| v.hand.as_ref())
.cloned()
.unwrap_or_default();
profiles[cp % profiles.len()].decide(&hand, &state.players, &state.ask_log)
}
};
game.act(action).unwrap();
}
game.record()
}
#[test]
fn test_replay_no_actions_returns_error() {
let mut record = GameRecord::new(
"Standard Go Fish",
vec!["Alice".to_string(), "Bob".to_string()],
);
record.turns.push(TurnRecord {
player: 0,
events: vec![],
books_after_turn: vec![0, 0],
actions: None,
});
let result = record.replay();
assert!(
matches!(result, Err(GfError::NoReplayData)),
"expected Err(NoReplayData), got {result:?}"
);
}
#[test]
fn test_replay_consistent_game() {
let record = play_to_completion();
assert!(
record.turns.iter().all(|t| t.actions.is_some()),
"all turns must have stored actions"
);
assert!(
record.initial_draw_pile.is_some(),
"record must have initial_draw_pile for replay"
);
let result = record
.replay()
.expect("replay of a completed game must succeed");
assert!(
result.is_consistent,
"replay must be consistent; mismatch at turn: {:?}",
result.mismatch_at_turn
);
assert!(result.mismatch_at_turn.is_none());
}
#[test]
fn test_replay_all_returns_vec() {
let record = play_to_completion();
let mut col = GameCollection::new();
col.push(record);
let results = col.replay_all();
assert_eq!(results.len(), 1);
let result = results[0].as_ref().expect("replay must succeed");
assert!(
result.is_consistent,
"replayed game must be consistent; mismatch at turn: {:?}",
result.mismatch_at_turn
);
}
}