use gfcore::prelude::{Game, GameEvent, GamePhase, GameVariant, Player, PlayerAction};
struct BotCounters {
counters: Vec<usize>,
}
impl BotCounters {
fn new(player_count: usize) -> Self {
Self {
counters: vec![0; player_count],
}
}
fn choose_rank(
&mut self,
player_idx: usize,
hand: &cardpack::prelude::BasicPile,
) -> Option<cardpack::prelude::Pip> {
if hand.is_empty() {
return None;
}
let mut seen = std::collections::HashSet::new();
let mut ranks: Vec<cardpack::prelude::Pip> = Vec::new();
for card in hand {
if seen.insert(card.rank) {
ranks.push(card.rank);
}
}
ranks.sort_by(|a, b| b.weight.cmp(&a.weight));
if player_idx >= self.counters.len() {
self.counters.resize(player_idx + 1, 0);
}
let idx = self.counters[player_idx] % ranks.len();
self.counters[player_idx] += 1;
Some(ranks[idx])
}
}
fn step(game: &mut Game, bots: &mut BotCounters) -> bool {
if game.is_over() {
return false;
}
let state = game.state().unwrap();
match state.phase {
GamePhase::WaitingForAsk | GamePhase::BookCompleted => {
let cp = state.current_player;
let hand = state
.players
.iter()
.find(|v| v.index == cp)
.and_then(|v| v.hand.as_ref())
.expect("current player must see their own hand");
let rank = bots
.choose_rank(cp, hand)
.expect("hand must be non-empty when WaitingForAsk");
let target = state
.players
.iter()
.find(|v| v.index != cp)
.expect("at least one other player")
.index;
game.act(PlayerAction::Ask { target, rank })
.expect("round-robin ask must not error");
}
GamePhase::WaitingForDraw => {
game.act(PlayerAction::Draw)
.expect("draw must not error when WaitingForDraw");
}
GamePhase::GameOver => return false,
}
!game.is_over()
}
fn play_to_completion() -> GameEvent {
let players = vec![Player::new("Alice"), Player::new("Bob")];
let mut game = Game::new(GameVariant::Standard, players).expect("valid 2-player Standard game");
let mut bots = BotCounters::new(2);
for _ in 0..13_000 {
if !step(&mut game, &mut bots) {
break;
}
}
assert!(game.is_over(), "game must terminate within 13000 steps");
game.state()
.unwrap()
.last_event
.expect("finished game must have at least one event")
}
#[test]
fn test_two_player_game_reaches_game_over() {
let event = play_to_completion();
assert!(
matches!(event, GameEvent::GameOver { .. }),
"final event must be GameOver, got {event:?}"
);
}
#[test]
fn test_two_player_game_books_sum_at_most_thirteen() {
for _run in 0..5 {
let players = vec![Player::new("Alice"), Player::new("Bob")];
let mut game = Game::new(GameVariant::Standard, players).unwrap();
let mut bots = BotCounters::new(2);
for _ in 0..13_000 {
if !step(&mut game, &mut bots) {
break;
}
}
assert!(game.is_over(), "game must terminate");
let state = game.state().unwrap();
let total_books: usize = state.players.iter().map(|p| p.books).sum();
assert!(
total_books <= 13,
"cannot exceed 13 books in a 52-card game; got {total_books}"
);
assert!(total_books > 0, "at least one book must be completed");
}
}
#[test]
fn test_two_player_game_winner_valid() {
for _run in 0..5 {
let players = vec![Player::new("Alice"), Player::new("Bob")];
let mut game = Game::new(GameVariant::Standard, players).unwrap();
let mut bots = BotCounters::new(2);
for _ in 0..13_000 {
if !step(&mut game, &mut bots) {
break;
}
}
assert!(game.is_over());
let state = game.state().unwrap();
assert_eq!(state.phase, GamePhase::GameOver);
if let Some(winner) = state.winner {
assert!(
winner < state.players.len(),
"winner index {winner} out of range"
);
let max_books = state.players.iter().map(|p| p.books).max().unwrap_or(0);
assert_eq!(
state.players[winner].books, max_books,
"winner must have the most books"
);
}
}
}
#[test]
fn test_game_already_over_returns_error() {
use gfcore::prelude::GfError;
let players = vec![Player::new("Alice"), Player::new("Bob")];
let mut game = Game::new(GameVariant::Standard, players).unwrap();
let mut bots = BotCounters::new(2);
for _ in 0..13_000 {
if !step(&mut game, &mut bots) {
break;
}
}
assert!(game.is_over(), "game must terminate");
let result = game.act(PlayerAction::Draw);
assert!(
matches!(result, Err(GfError::GameAlreadyOver)),
"expected GameAlreadyOver after game ended, got {result:?}"
);
}
#[test]
fn test_state_snapshot_serializes_and_deserializes() {
let players = vec![Player::new("Alice"), Player::new("Bob")];
let game = Game::new(GameVariant::Standard, players).expect("valid game");
let state = game.state().unwrap();
let json = serde_json::to_string(&state).expect("state must serialize to JSON");
let back: gfcore::prelude::GameState =
serde_json::from_str(&json).expect("state must deserialize from JSON");
assert_eq!(back.current_player, state.current_player);
assert_eq!(back.draw_pile_size, state.draw_pile_size);
assert_eq!(back.players.len(), state.players.len());
}