use cardpack::prelude::{BasicPile, Pip};
use serde::{Deserialize, Serialize};
use crate::error::GfError;
use crate::game::action::PlayerAction;
use crate::player::{AskEntry, Player, PlayerView};
use crate::rules::{GameVariant, GoFishRules};
#[cfg(feature = "history")]
use crate::history::{GameRecord, TurnRecord};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GamePhase {
WaitingForAsk,
WaitingForDraw,
BookCompleted,
GameOver,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GameEvent {
Asked {
asker: usize,
target: usize,
rank: String,
},
Gave {
from: usize,
to: usize,
rank: String,
count: usize,
},
GoFish {
player: usize,
},
Drew {
player: usize,
matched: bool,
},
Book {
player: usize,
rank: String,
},
GameOver {
winner: Option<usize>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GameState {
pub phase: GamePhase,
pub current_player: usize,
pub players: Vec<PlayerView>,
pub draw_pile_size: usize,
pub last_event: Option<GameEvent>,
pub winner: Option<usize>,
pub ask_log: Vec<AskEntry>,
}
pub struct Game {
variant: GameVariant,
players: Vec<Player>,
draw_pile: BasicPile,
current_player: usize,
phase: GamePhase,
last_asked_rank: Option<Pip>,
last_event: Option<GameEvent>,
winner: Option<usize>,
ask_log: Vec<AskEntry>,
#[cfg(feature = "history")]
pending_turn_player: usize,
#[cfg(feature = "history")]
pending_turn_events: Vec<GameEvent>,
#[cfg(feature = "history")]
pending_turn_actions: Vec<PlayerAction>,
#[cfg(feature = "history")]
history: GameRecord,
}
impl Game {
pub fn new(variant: GameVariant, players: Vec<Player>) -> Result<Self, GfError> {
let draw_pile = variant.rules().deck();
Self::setup(variant, players, draw_pile)
}
#[cfg(feature = "history")]
pub(crate) fn new_with_deck(
variant: GameVariant,
players: Vec<Player>,
draw_pile: cardpack::prelude::BasicPile,
) -> Result<Self, GfError> {
Self::setup(variant, players, draw_pile)
}
fn setup(
variant: GameVariant,
mut players: Vec<Player>,
mut draw_pile: cardpack::prelude::BasicPile,
) -> Result<Self, GfError> {
let rules = variant.rules();
let player_count = players.len();
if player_count < rules.min_players() {
return Err(GfError::NotEnoughPlayers);
}
if player_count > rules.max_players() {
return Err(GfError::TooManyPlayers);
}
#[cfg(feature = "history")]
let history = {
let player_names: Vec<String> = players.iter().map(|p| p.name.clone()).collect();
let mut record = GameRecord::new(variant.rules().name(), player_names);
record.initial_draw_pile = Some(draw_pile.clone());
record
};
let hand_size = rules.initial_hand_size(player_count);
for _ in 0..hand_size {
for player in &mut players {
if let Some(card) = draw_pile.draw_first() {
player.receive_card(card);
}
}
}
for player in &mut players {
Self::collect_books_for_player(player, rules);
}
let mut game = Self {
variant,
players,
draw_pile,
current_player: 0,
phase: GamePhase::WaitingForAsk,
last_asked_rank: None,
last_event: None,
winner: None,
ask_log: Vec::new(),
#[cfg(feature = "history")]
pending_turn_player: 0,
#[cfg(feature = "history")]
pending_turn_events: Vec::new(),
#[cfg(feature = "history")]
pending_turn_actions: Vec::new(),
#[cfg(feature = "history")]
history,
};
game.ensure_current_player_can_ask();
Ok(game)
}
pub fn act(&mut self, action: PlayerAction) -> Result<GameEvent, GfError> {
if self.phase == GamePhase::GameOver {
return Err(GfError::GameAlreadyOver);
}
match action {
PlayerAction::Ask { target, rank } => self.handle_ask(target, rank),
PlayerAction::Draw => self.handle_draw(),
}
}
pub fn state(&self) -> Result<GameState, GfError> {
let players = PlayerView::from_perspective(&self.players, self.current_player)?;
Ok(GameState {
phase: self.phase.clone(),
current_player: self.current_player,
players,
draw_pile_size: self.draw_pile.len(),
last_event: self.last_event.clone(),
winner: self.winner,
ask_log: self.ask_log.clone(),
})
}
pub fn state_as_observer(&self, observer: usize) -> Result<GameState, GfError> {
let players = PlayerView::from_perspective(&self.players, observer)?;
Ok(GameState {
phase: self.phase.clone(),
current_player: self.current_player,
players,
draw_pile_size: self.draw_pile.len(),
last_event: self.last_event.clone(),
winner: self.winner,
ask_log: self.ask_log.clone(),
})
}
#[must_use]
pub fn current_player(&self) -> usize {
self.current_player
}
#[must_use]
pub fn phase(&self) -> &GamePhase {
&self.phase
}
#[must_use]
pub fn is_over(&self) -> bool {
self.phase == GamePhase::GameOver
}
#[cfg(feature = "history")]
pub fn record(&self) -> GameRecord {
let mut record = self.history.clone();
if !self.pending_turn_events.is_empty() {
let books: Vec<usize> = self.players.iter().map(Player::book_count).collect();
record.turns.push(TurnRecord {
player: self.pending_turn_player,
events: self.pending_turn_events.clone(),
books_after_turn: books,
actions: if self.pending_turn_actions.is_empty() {
None
} else {
Some(self.pending_turn_actions.clone())
},
});
}
record
}
fn handle_ask(&mut self, target: usize, rank: Pip) -> Result<GameEvent, GfError> {
if self.phase != GamePhase::WaitingForAsk && self.phase != GamePhase::BookCompleted {
return Err(GfError::OutOfTurn);
}
let cp = self.current_player;
if target == cp || target >= self.players.len() {
return Err(GfError::InvalidTarget);
}
if !self
.variant
.rules()
.is_valid_ask(self.players[cp].hand(), &rank)
{
return Err(GfError::InvalidAsk);
}
#[cfg(feature = "history")]
self.pending_turn_actions
.push(PlayerAction::Ask { target, rank });
self.ask_log.push(AskEntry {
asker: cp,
rank: rank.index.to_string(),
});
let asked_event = GameEvent::Asked {
asker: cp,
target,
rank: rank.index.to_string(),
};
self.last_event = Some(asked_event.clone());
#[cfg(feature = "history")]
self.push_event(&asked_event);
let transferred = self.players[target].give_cards_of_rank(&rank);
if transferred.is_empty() {
let event = GameEvent::GoFish { player: cp };
self.last_event = Some(event.clone());
#[cfg(feature = "history")]
self.push_event(&event);
self.phase = GamePhase::WaitingForDraw;
self.last_asked_rank = Some(rank);
return Ok(event);
}
let count = transferred.len();
for card in transferred {
self.players[cp].receive_card(card);
}
let gave_event = GameEvent::Gave {
from: target,
to: cp,
rank: rank.index.to_string(),
count,
};
self.last_event = Some(gave_event.clone());
#[cfg(feature = "history")]
self.push_event(&gave_event);
let rules = self.variant.rules();
let last_event = if Self::check_and_collect_book(&mut self.players, cp, &rank, rules) {
let book_event = GameEvent::Book {
player: cp,
rank: rank.index.to_string(),
};
self.last_event = Some(book_event.clone());
#[cfg(feature = "history")]
self.push_event(&book_event);
book_event
} else {
gave_event
};
self.phase = match &last_event {
GameEvent::Book { .. } => GamePhase::BookCompleted,
_ => GamePhase::WaitingForAsk,
};
self.ensure_current_player_can_ask();
if let Some(over_event) = self.check_win_condition() {
return Ok(over_event);
}
Ok(last_event)
}
fn handle_draw(&mut self) -> Result<GameEvent, GfError> {
if self.phase != GamePhase::WaitingForDraw {
return Err(GfError::OutOfTurn);
}
#[cfg(feature = "history")]
self.pending_turn_actions.push(PlayerAction::Draw);
let asked_rank = self.last_asked_rank.take();
let cp = self.current_player;
if self.draw_pile.is_empty() {
let drew_event = GameEvent::Drew {
player: cp,
matched: false,
};
self.last_event = Some(drew_event.clone());
#[cfg(feature = "history")]
self.push_event(&drew_event);
self.advance_turn();
self.phase = GamePhase::WaitingForAsk;
self.ensure_current_player_can_ask();
if let Some(over_event) = self.check_win_condition() {
return Ok(over_event);
}
return Ok(drew_event);
}
let Some(card) = self.draw_pile.draw_first() else {
let drew_event = GameEvent::Drew {
player: cp,
matched: false,
};
self.last_event = Some(drew_event.clone());
#[cfg(feature = "history")]
self.push_event(&drew_event);
self.advance_turn();
self.phase = GamePhase::WaitingForAsk;
self.ensure_current_player_can_ask();
if let Some(over_event) = self.check_win_condition() {
return Ok(over_event);
}
return Ok(drew_event);
};
let drawn_rank = card.rank;
let matched = asked_rank == Some(drawn_rank);
self.players[cp].receive_card(card);
let drew_event = GameEvent::Drew {
player: cp,
matched,
};
self.last_event = Some(drew_event.clone());
#[cfg(feature = "history")]
self.push_event(&drew_event);
let rules = self.variant.rules();
let book_formed = Self::check_and_collect_book(&mut self.players, cp, &drawn_rank, rules);
let last_event = if book_formed {
let book_event = GameEvent::Book {
player: cp,
rank: drawn_rank.index.to_string(),
};
self.last_event = Some(book_event.clone());
#[cfg(feature = "history")]
self.push_event(&book_event);
if matched {
self.phase = GamePhase::BookCompleted;
} else {
self.advance_turn();
self.phase = GamePhase::WaitingForAsk;
}
book_event
} else if matched {
self.phase = GamePhase::WaitingForAsk;
drew_event
} else {
self.advance_turn();
self.phase = GamePhase::WaitingForAsk;
drew_event
};
self.ensure_current_player_can_ask();
if let Some(over_event) = self.check_win_condition() {
return Ok(over_event);
}
Ok(last_event)
}
#[mutants::skip] fn replenish_until_has_cards(&mut self, player_index: usize) -> bool {
if player_index >= self.players.len() {
return false;
}
let max_draws = self.draw_pile.len() + 1;
for _ in 0..max_draws {
if !self.players[player_index].hand_is_empty() {
return true;
}
if self.draw_pile.is_empty() {
return false;
}
if let Some(card) = self.draw_pile.draw_first() {
let r = card.rank;
self.players[player_index].receive_card(card);
let rules = self.variant.rules();
Self::check_and_collect_book(&mut self.players, player_index, &r, rules);
}
}
!self.players[player_index].hand_is_empty()
}
fn advance_turn(&mut self) {
#[cfg(feature = "history")]
self.flush_turn();
let count = self.players.len();
if count == 0 {
return;
}
let start = self.current_player;
let mut next = (start + 1) % count;
for _ in 0..count {
if !self.players[next].hand_is_empty() {
self.current_player = next;
#[cfg(feature = "history")]
{
self.pending_turn_player = next;
}
return;
}
if self.replenish_until_has_cards(next) {
self.current_player = next;
#[cfg(feature = "history")]
{
self.pending_turn_player = next;
}
return;
}
next = (next + 1) % count;
}
self.current_player = next;
#[cfg(feature = "history")]
{
self.pending_turn_player = next;
}
}
fn ensure_current_player_can_ask(&mut self) {
if self.phase != GamePhase::WaitingForAsk && self.phase != GamePhase::BookCompleted {
return;
}
let cp = self.current_player;
if self.players[cp].hand_is_empty() {
if !self.replenish_until_has_cards(cp) {
self.advance_turn();
}
}
}
#[cfg(feature = "history")]
fn push_event(&mut self, event: &GameEvent) {
self.pending_turn_events.push(event.clone());
}
#[cfg(feature = "history")]
fn flush_turn(&mut self) {
if self.pending_turn_events.is_empty() {
return;
}
let books: Vec<usize> = self.players.iter().map(Player::book_count).collect();
self.history.turns.push(TurnRecord {
player: self.pending_turn_player,
events: std::mem::take(&mut self.pending_turn_events),
books_after_turn: books,
actions: if self.pending_turn_actions.is_empty() {
None
} else {
Some(std::mem::take(&mut self.pending_turn_actions))
},
});
}
fn check_and_collect_book(
players: &mut [Player],
player: usize,
rank: &Pip,
rules: &dyn GoFishRules,
) -> bool {
if player >= players.len() {
return false;
}
let cards_of_rank = players[player].cards_of_rank(rank);
if rules.is_book(&cards_of_rank) {
let book = players[player].give_cards_of_rank(rank);
players[player].add_book(book);
return true;
}
false
}
fn collect_books_for_player(player: &mut Player, rules: &dyn GoFishRules) {
let ranks: Vec<Pip> = player.held_ranks();
for rank in ranks {
let cards = player.cards_of_rank(&rank);
if rules.is_book(&cards) {
let book = player.give_cards_of_rank(&rank);
player.add_book(book);
}
}
}
fn no_productive_ask_exists(&self) -> bool {
for (i, player) in self.players.iter().enumerate() {
if player.hand_is_empty() {
continue;
}
for rank in player.held_ranks() {
let other_has = self
.players
.iter()
.enumerate()
.any(|(j, other)| j != i && other.has_rank(&rank));
if other_has {
return false; }
}
}
true }
fn check_win_condition(&mut self) -> Option<GameEvent> {
if !self.draw_pile.is_empty() {
return None;
}
let all_hands_empty = self.players.iter().all(Player::hand_is_empty);
let deadlocked = !all_hands_empty && self.no_productive_ask_exists();
if !all_hands_empty && !deadlocked {
return None;
}
let max_books = self
.players
.iter()
.map(Player::book_count)
.max()
.unwrap_or(0);
let winners: Vec<usize> = self
.players
.iter()
.enumerate()
.filter(|(_, p)| p.book_count() == max_books)
.map(|(i, _)| i)
.collect();
let winner = if winners.len() == 1 {
winners.first().copied()
} else {
None
};
self.winner = winner;
self.phase = GamePhase::GameOver;
let event = GameEvent::GameOver { winner };
self.last_event = Some(event.clone());
#[cfg(feature = "history")]
{
if !self.pending_turn_events.is_empty() {
self.pending_turn_events.push(event.clone());
}
self.flush_turn();
self.history.winner = winner;
}
Some(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_player_game() -> Game {
let players = vec![Player::new("Alice"), Player::new("Bob")];
Game::new(GameVariant::Standard, players).unwrap()
}
#[test]
fn test_game_new_valid_player_count() {
let game = two_player_game();
assert_eq!(game.current_player(), 0);
assert_eq!(*game.phase(), GamePhase::WaitingForAsk);
assert!(!game.is_over());
}
#[test]
fn test_game_new_not_enough_players() {
let result = Game::new(GameVariant::Standard, vec![Player::new("Solo")]);
assert!(matches!(result, Err(GfError::NotEnoughPlayers)));
}
#[test]
fn test_game_new_too_many_players() {
let players: Vec<Player> = (0..9).map(|i| Player::new(format!("P{i}"))).collect();
let result = Game::new(GameVariant::Standard, players);
assert!(matches!(result, Err(GfError::TooManyPlayers)));
}
#[test]
fn test_game_state_snapshot() {
let game = two_player_game();
let state = game.state().unwrap();
assert_eq!(state.phase, GamePhase::WaitingForAsk);
assert_eq!(state.current_player, 0);
assert_eq!(state.players.len(), 2);
assert!(state.last_event.is_none());
assert!(state.winner.is_none());
assert!(state.ask_log.is_empty());
}
#[test]
fn test_game_act_draw_when_waiting_for_ask_is_error() {
let mut game = two_player_game();
let result = game.act(PlayerAction::Draw);
assert_eq!(result, Err(GfError::OutOfTurn));
}
#[test]
fn test_game_already_over_error() {
let mut game = two_player_game();
game.phase = GamePhase::GameOver;
let result = game.act(PlayerAction::Draw);
assert_eq!(result, Err(GfError::GameAlreadyOver));
}
#[test]
fn test_game_invalid_target_self() {
let mut game = two_player_game();
let rank = game.players[0].held_ranks().into_iter().next().unwrap();
let result = game.act(PlayerAction::Ask { target: 0, rank });
assert_eq!(result, Err(GfError::InvalidTarget));
}
#[test]
fn test_game_invalid_target_out_of_range() {
let mut game = two_player_game();
let rank = game.players[0].held_ranks().into_iter().next().unwrap();
let result = game.act(PlayerAction::Ask { target: 99, rank });
assert_eq!(result, Err(GfError::InvalidTarget));
}
#[test]
fn test_game_invalid_ask_player_lacks_rank() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
let held: std::collections::HashSet<cardpack::prelude::Pip> =
game.players[0].held_ranks().into_iter().collect();
let all_ranks = [
FrenchBasicCard::ACE_SPADES.rank,
FrenchBasicCard::KING_SPADES.rank,
FrenchBasicCard::QUEEN_SPADES.rank,
FrenchBasicCard::JACK_SPADES.rank,
FrenchBasicCard::TEN_SPADES.rank,
FrenchBasicCard::NINE_SPADES.rank,
FrenchBasicCard::EIGHT_SPADES.rank,
FrenchBasicCard::SEVEN_SPADES.rank,
FrenchBasicCard::SIX_SPADES.rank,
FrenchBasicCard::FIVE_SPADES.rank,
FrenchBasicCard::FOUR_SPADES.rank,
FrenchBasicCard::TREY_SPADES.rank,
FrenchBasicCard::DEUCE_SPADES.rank,
];
if let Some(&rank) = all_ranks.iter().find(|r| !held.contains(r)) {
let result = game.act(PlayerAction::Ask { target: 1, rank });
assert_eq!(result, Err(GfError::InvalidAsk));
}
}
#[test]
fn test_game_ask_when_waiting_for_draw_is_error() {
let mut game = two_player_game();
game.phase = GamePhase::WaitingForDraw;
let rank = game.players[0].held_ranks().into_iter().next().unwrap();
let result = game.act(PlayerAction::Ask { target: 1, rank });
assert_eq!(result, Err(GfError::OutOfTurn));
}
#[test]
fn test_game_initial_hands_dealt() {
let game = two_player_game();
assert!(game.draw_pile.len() <= 52 - 14);
}
#[test]
fn test_game_is_over_false_initially() {
let game = two_player_game();
assert!(!game.is_over());
}
#[test]
fn test_game_draw_pile_reduced_after_deal() {
let game = two_player_game();
assert!(game.draw_pile.len() <= 38);
}
fn clear_hand(player: &mut Player) {
for rank in player.held_ranks() {
player.give_cards_of_rank(&rank);
}
}
fn three_player_game() -> Game {
let players: Vec<Player> = (0..3).map(|i| Player::new(format!("P{i}"))).collect();
Game::new(GameVariant::Standard, players).unwrap()
}
#[test]
fn test_handle_ask_produces_book_when_four_of_a_rank() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.players[0].receive_card(FrenchBasicCard::ACE_SPADES);
game.players[0].receive_card(FrenchBasicCard::ACE_HEARTS);
game.players[0].receive_card(FrenchBasicCard::ACE_DIAMONDS);
game.players[1].receive_card(FrenchBasicCard::ACE_CLUBS);
game.players[1].receive_card(FrenchBasicCard::KING_SPADES);
let ace_rank = FrenchBasicCard::ACE_SPADES.rank;
let result = game
.act(PlayerAction::Ask {
target: 1,
rank: ace_rank,
})
.unwrap();
assert!(
matches!(result, GameEvent::Book { .. }),
"expected Book event, got {result:?}"
);
assert_eq!(game.phase, GamePhase::BookCompleted);
}
#[test]
fn test_handle_draw_sets_matched_true_when_rank_matches() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
game.phase = GamePhase::WaitingForDraw;
game.last_asked_rank = Some(FrenchBasicCard::ACE_CLUBS.rank);
game.draw_pile = BasicPile::from(vec![
FrenchBasicCard::ACE_CLUBS,
FrenchBasicCard::KING_SPADES,
]);
let result = game.act(PlayerAction::Draw).unwrap();
assert!(
matches!(result, GameEvent::Drew { matched: true, .. }),
"expected matched=true, got {result:?}"
);
}
#[test]
fn test_handle_draw_sets_matched_false_when_rank_differs() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.players[0].receive_card(FrenchBasicCard::ACE_SPADES);
game.players[1].receive_card(FrenchBasicCard::ACE_HEARTS);
game.phase = GamePhase::WaitingForDraw;
game.last_asked_rank = Some(FrenchBasicCard::ACE_CLUBS.rank);
game.draw_pile = BasicPile::from(vec![FrenchBasicCard::KING_SPADES]);
let result = game.act(PlayerAction::Draw).unwrap();
assert!(
matches!(result, GameEvent::Drew { matched: false, .. }),
"expected matched=false, got {result:?}"
);
}
#[test]
fn test_handle_draw_forms_book_on_non_matching_draw() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.players[0].receive_card(FrenchBasicCard::FOUR_SPADES);
game.players[0].receive_card(FrenchBasicCard::FOUR_HEARTS);
game.players[0].receive_card(FrenchBasicCard::FOUR_DIAMONDS);
game.players[0].receive_card(FrenchBasicCard::NINE_SPADES);
game.players[1].receive_card(FrenchBasicCard::ACE_SPADES);
game.players[1].receive_card(FrenchBasicCard::NINE_HEARTS);
game.phase = GamePhase::WaitingForDraw;
game.last_asked_rank = Some(FrenchBasicCard::NINE_SPADES.rank);
game.draw_pile = BasicPile::from(vec![
FrenchBasicCard::FOUR_CLUBS,
FrenchBasicCard::KING_SPADES,
]);
let result = game.act(PlayerAction::Draw).unwrap();
match result {
GameEvent::Book { player, ref rank } => {
assert_eq!(player, 0, "book must be credited to player 0");
assert_eq!(rank, "4", "book must be of rank 4");
}
other => panic!("expected Book event, got {other:?}"),
}
assert_eq!(
game.players[0].book_count(),
1,
"player 0 should hold 1 book"
);
assert_eq!(
game.current_player(),
1,
"turn must advance on non-matching draw even when a book forms"
);
assert_eq!(*game.phase(), GamePhase::WaitingForAsk);
}
#[test]
fn test_replenish_invalid_player_index_returns_false() {
let mut game = two_player_game();
assert!(!game.replenish_until_has_cards(99));
}
#[test]
fn test_replenish_player_with_cards_returns_true_immediately() {
let mut game = two_player_game();
assert!(game.replenish_until_has_cards(0));
}
#[test]
fn test_replenish_empty_hand_empty_pile_returns_false() {
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
game.draw_pile = BasicPile::default();
assert!(!game.replenish_until_has_cards(0));
}
#[test]
fn test_replenish_empty_hand_non_empty_pile_returns_true() {
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
assert!(!game.draw_pile.is_empty());
let result = game.replenish_until_has_cards(0);
assert!(result);
assert!(!game.players[0].hand_is_empty());
}
#[test]
fn test_advance_turn_advances_to_next_player_with_cards() {
let mut game = two_player_game();
game.current_player = 0;
game.advance_turn();
assert_eq!(game.current_player, 1);
}
#[test]
fn test_advance_turn_wraps_around_with_modulo() {
let mut game = three_player_game();
game.current_player = 0;
clear_hand(&mut game.players[1]);
game.draw_pile = BasicPile::default();
game.advance_turn();
assert_eq!(game.current_player, 2);
}
#[test]
fn test_advance_turn_initial_next_wraps_from_last_player() {
let mut game = three_player_game();
game.current_player = 2;
game.advance_turn();
assert_eq!(game.current_player, 0);
}
#[test]
fn test_advance_turn_skips_empty_handed_player_when_pile_empty() {
let mut game = three_player_game();
game.current_player = 0;
clear_hand(&mut game.players[1]);
game.draw_pile = BasicPile::default();
game.advance_turn();
assert_eq!(game.current_player, 2);
assert!(!game.players[2].hand_is_empty());
}
#[test]
fn test_ensure_current_player_can_ask_no_op_in_waiting_for_draw_phase() {
let mut game = two_player_game();
game.phase = GamePhase::WaitingForDraw;
clear_hand(&mut game.players[0]);
game.ensure_current_player_can_ask();
assert_eq!(game.current_player, 0);
assert!(game.players[0].hand_is_empty());
}
#[test]
fn test_ensure_current_player_can_ask_replenishes_from_pile() {
let mut game = two_player_game();
game.phase = GamePhase::WaitingForAsk;
clear_hand(&mut game.players[0]);
assert!(!game.draw_pile.is_empty());
game.ensure_current_player_can_ask();
assert_eq!(game.current_player, 0);
assert!(!game.players[0].hand_is_empty());
}
#[test]
fn test_ensure_current_player_can_ask_advances_when_pile_empty() {
let mut game = two_player_game();
game.phase = GamePhase::WaitingForAsk;
clear_hand(&mut game.players[0]);
game.draw_pile = BasicPile::default();
game.ensure_current_player_can_ask();
assert_eq!(game.current_player, 1);
}
#[test]
fn test_collect_books_for_player_removes_complete_books() {
use crate::rules::StandardRules;
use cardpack::prelude::FrenchBasicCard;
let mut player = Player::new("Alice");
player.receive_card(FrenchBasicCard::ACE_SPADES);
player.receive_card(FrenchBasicCard::ACE_HEARTS);
player.receive_card(FrenchBasicCard::ACE_DIAMONDS);
player.receive_card(FrenchBasicCard::ACE_CLUBS);
player.receive_card(FrenchBasicCard::KING_SPADES);
Game::collect_books_for_player(&mut player, &StandardRules);
assert_eq!(player.book_count(), 1);
assert_eq!(player.hand_size(), 1);
}
#[test]
fn test_collect_books_for_player_no_op_for_mixed_hand() {
use crate::rules::StandardRules;
use cardpack::prelude::FrenchBasicCard;
let mut player = Player::new("Bob");
player.receive_card(FrenchBasicCard::ACE_SPADES);
player.receive_card(FrenchBasicCard::KING_SPADES);
player.receive_card(FrenchBasicCard::QUEEN_SPADES);
Game::collect_books_for_player(&mut player, &StandardRules);
assert_eq!(player.book_count(), 0);
assert_eq!(player.hand_size(), 3);
}
#[test]
fn test_no_productive_ask_exists_false_when_rank_overlap() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.players[0].receive_card(FrenchBasicCard::ACE_SPADES);
game.players[1].receive_card(FrenchBasicCard::ACE_HEARTS);
assert!(!game.no_productive_ask_exists());
}
#[test]
fn test_no_productive_ask_exists_true_when_no_overlap() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.players[0].receive_card(FrenchBasicCard::ACE_SPADES);
game.players[1].receive_card(FrenchBasicCard::KING_HEARTS);
assert!(game.no_productive_ask_exists());
}
#[test]
fn test_no_productive_ask_exists_true_when_all_hands_empty() {
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
assert!(game.no_productive_ask_exists());
}
#[test]
fn test_check_win_condition_returns_none_when_pile_not_empty() {
let mut game = two_player_game();
assert!(!game.draw_pile.is_empty());
assert!(game.check_win_condition().is_none());
}
#[test]
fn test_check_win_condition_returns_none_when_productive_ask_exists() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.players[0].receive_card(FrenchBasicCard::ACE_SPADES);
game.players[1].receive_card(FrenchBasicCard::ACE_HEARTS);
game.draw_pile = BasicPile::default();
assert!(game.check_win_condition().is_none());
}
#[test]
fn test_check_win_condition_game_over_when_all_hands_empty() {
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.draw_pile = BasicPile::default();
let result = game.check_win_condition();
assert!(result.is_some());
assert!(matches!(result.unwrap(), GameEvent::GameOver { .. }));
assert_eq!(game.phase, GamePhase::GameOver);
}
#[test]
fn test_check_win_condition_winner_has_most_books() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.draw_pile = BasicPile::default();
game.players[1].add_book(BasicPile::from(vec![
FrenchBasicCard::ACE_SPADES,
FrenchBasicCard::ACE_HEARTS,
FrenchBasicCard::ACE_DIAMONDS,
FrenchBasicCard::ACE_CLUBS,
]));
let result = game.check_win_condition().unwrap();
assert!(
matches!(result, GameEvent::GameOver { winner: Some(1) }),
"expected winner=Some(1), got {result:?}"
);
}
#[test]
fn test_check_win_condition_tie_gives_no_winner() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.draw_pile = BasicPile::default();
let book = BasicPile::from(vec![
FrenchBasicCard::ACE_SPADES,
FrenchBasicCard::ACE_HEARTS,
FrenchBasicCard::ACE_DIAMONDS,
FrenchBasicCard::ACE_CLUBS,
]);
game.players[0].add_book(book.clone());
game.players[1].add_book(book);
let result = game.check_win_condition().unwrap();
assert!(
matches!(result, GameEvent::GameOver { winner: None }),
"expected winner=None on tie, got {result:?}"
);
}
#[test]
fn test_check_win_condition_deadlock_triggers_game_over() {
use cardpack::prelude::FrenchBasicCard;
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.players[0].receive_card(FrenchBasicCard::ACE_SPADES);
game.players[1].receive_card(FrenchBasicCard::KING_HEARTS);
game.draw_pile = BasicPile::default();
let result = game.check_win_condition();
assert!(result.is_some());
assert!(matches!(result.unwrap(), GameEvent::GameOver { .. }));
}
#[cfg(feature = "history")]
#[test]
fn test_check_win_condition_gameover_appended_to_pending_turn() {
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.draw_pile = BasicPile::default();
game.pending_turn_events
.push(GameEvent::GoFish { player: 0 });
game.check_win_condition();
let last_turn = game.history.turns.last().expect("expected a flushed turn");
assert!(
last_turn
.events
.iter()
.any(|e| matches!(e, GameEvent::GameOver { .. })),
"GameOver must be in the last turn when pending events existed: {last_turn:?}"
);
}
#[cfg(feature = "history")]
#[test]
fn test_check_win_condition_no_synthetic_turn_when_pending_empty() {
let mut game = two_player_game();
clear_hand(&mut game.players[0]);
clear_hand(&mut game.players[1]);
game.draw_pile = BasicPile::default();
assert!(game.pending_turn_events.is_empty());
let turns_before = game.history.turns.len();
game.check_win_condition();
assert_eq!(
game.history.turns.len(),
turns_before,
"no synthetic GameOver-only turn should be added when pending events were empty"
);
}
#[test]
fn test_custom_is_valid_ask_honored_by_engine() {
use crate::game::action::PlayerAction;
use crate::player::Player;
use crate::rules::{GameVariant, GoFishRules};
use cardpack::prelude::{BasicPile, FrenchBasicCard, Pip};
struct AnyAskValid;
impl GoFishRules for AnyAskValid {
fn name(&self) -> &'static str {
"AnyAskValid"
}
fn deck(&self) -> BasicPile {
BasicPile::from(vec![
FrenchBasicCard::ACE_SPADES,
FrenchBasicCard::KING_SPADES,
FrenchBasicCard::ACE_HEARTS,
FrenchBasicCard::KING_HEARTS,
FrenchBasicCard::QUEEN_SPADES,
FrenchBasicCard::QUEEN_HEARTS,
])
}
fn book_size(&self) -> usize {
4
}
fn initial_hand_size(&self, _: usize) -> usize {
2
}
fn min_players(&self) -> usize {
2
}
fn max_players(&self) -> usize {
4
}
fn is_valid_ask(&self, _hand: &BasicPile, _rank: &Pip) -> bool {
true
}
fn is_book(&self, cards: &BasicPile) -> bool {
if cards.len() != 4 {
return false;
}
let first = match cards.v().first() {
Some(c) => c.rank,
None => return false,
};
cards.v().iter().all(|c| c.rank == first)
}
}
let players = vec![Player::new("A"), Player::new("B")];
let mut game = Game::new(GameVariant::Custom(Box::new(AnyAskValid)), players).unwrap();
let king_rank = FrenchBasicCard::KING_SPADES.rank;
let result = game.act(PlayerAction::Ask {
target: 1,
rank: king_rank,
});
assert!(
result.is_ok(),
"custom is_valid_ask (always true) must permit asking for unheld rank, got: {result:?}"
);
}
#[test]
fn test_custom_is_book_honored_by_engine() {
use crate::game::action::PlayerAction;
use crate::player::Player;
use crate::rules::{GameVariant, GoFishRules};
use cardpack::prelude::{BasicPile, FrenchBasicCard, Pip};
struct PairIsBook;
impl GoFishRules for PairIsBook {
fn name(&self) -> &'static str {
"PairIsBook"
}
fn deck(&self) -> BasicPile {
BasicPile::from(vec![
FrenchBasicCard::ACE_SPADES,
FrenchBasicCard::ACE_HEARTS,
FrenchBasicCard::KING_SPADES,
FrenchBasicCard::KING_HEARTS,
FrenchBasicCard::QUEEN_SPADES,
FrenchBasicCard::QUEEN_HEARTS,
])
}
fn book_size(&self) -> usize {
2
}
fn initial_hand_size(&self, _: usize) -> usize {
2
}
fn min_players(&self) -> usize {
2
}
fn max_players(&self) -> usize {
4
}
fn is_valid_ask(&self, hand: &BasicPile, rank: &Pip) -> bool {
hand.iter().any(|c| &c.rank == rank)
}
fn is_book(&self, cards: &BasicPile) -> bool {
if cards.len() != 2 {
return false;
}
match (cards.v().first(), cards.v().get(1)) {
(Some(a), Some(b)) => a.rank == b.rank,
_ => false,
}
}
}
let players = vec![Player::new("A"), Player::new("B")];
let mut game = Game::new(GameVariant::Custom(Box::new(PairIsBook)), players).unwrap();
let ace_rank = FrenchBasicCard::ACE_SPADES.rank;
let event = game
.act(PlayerAction::Ask {
target: 1,
rank: ace_rank,
})
.unwrap();
assert!(
matches!(event, GameEvent::Book { .. }),
"custom is_book (pair=book) must collect a 2-card book, got: {event:?}"
);
}
}