use cardpack::prelude::BasicPile;
use rand::Rng as _;
use crate::game::PlayerAction;
use crate::player::{AskEntry, PlayerView};
use super::BotStrategy;
#[derive(Debug, Clone)]
pub struct RandomStrategy;
impl BotStrategy for RandomStrategy {
fn decide(
&self,
hand: &BasicPile,
players: &[PlayerView],
_ask_log: &[AskEntry],
) -> PlayerAction {
if hand.is_empty() {
return PlayerAction::Draw;
}
let mut rng = rand::rng();
let observer = players
.iter()
.position(|player_view| player_view.hand.is_some())
.unwrap_or(0);
let cards = hand.v();
let card_index = rng.random_range(0..cards.len());
#[allow(clippy::indexing_slicing)]
let rank = cards[card_index].rank;
let targets: Vec<usize> = (0..players.len()).filter(|&idx| idx != observer).collect();
let target = if targets.is_empty() {
(observer + 1) % players.len().max(2)
} else {
let pick = rng.random_range(0..targets.len());
targets[pick]
};
PlayerAction::Ask { target, rank }
}
}
#[cfg(test)]
mod tests {
use super::*;
use cardpack::prelude::FrenchBasicCard;
fn two_player_views() -> (BasicPile, Vec<PlayerView>) {
use crate::player::Player;
let mut alice = Player::new("Alice");
alice.receive_card(FrenchBasicCard::ACE_SPADES);
alice.receive_card(FrenchBasicCard::ACE_HEARTS);
let mut bob = Player::new("Bob");
bob.receive_card(FrenchBasicCard::KING_HEARTS);
let views = PlayerView::from_perspective(&[alice, bob], 0).unwrap();
let hand = views[0].hand.clone().unwrap_or_default();
(hand, views)
}
#[test]
fn test_random_strategy_returns_ask() {
let strategy = RandomStrategy;
let (hand, players) = two_player_views();
let action = strategy.decide(&hand, &players, &[]);
assert!(matches!(action, PlayerAction::Ask { .. }));
}
#[test]
fn test_random_strategy_target_is_not_self() {
let strategy = RandomStrategy;
let (hand, players) = two_player_views();
for _ in 0..20 {
if let PlayerAction::Ask { target, .. } = strategy.decide(&hand, &players, &[]) {
assert_ne!(target, 0, "target must not be the bot's own index");
}
}
}
#[test]
fn test_random_strategy_rank_in_hand() {
let strategy = RandomStrategy;
let (hand, players) = two_player_views();
let held: std::collections::HashSet<_> = hand.v().iter().map(|card| card.rank).collect();
for _ in 0..20 {
if let PlayerAction::Ask { rank, .. } = strategy.decide(&hand, &players, &[]) {
assert!(held.contains(&rank), "rank must come from hand");
}
}
}
fn single_player_view() -> (BasicPile, Vec<PlayerView>) {
use crate::player::Player;
let mut alice = Player::new("Alice");
alice.receive_card(FrenchBasicCard::ACE_SPADES);
alice.receive_card(FrenchBasicCard::ACE_HEARTS);
let views = PlayerView::from_perspective(&[alice], 0).unwrap();
let hand = views[0].hand.clone().unwrap_or_default();
(hand, views)
}
#[test]
fn test_random_strategy_degenerate_single_player_target() {
let strategy = RandomStrategy;
let (hand, players) = single_player_view();
if let PlayerAction::Ask { target, .. } = strategy.decide(&hand, &players, &[]) {
assert_eq!(target, 1, "degenerate target must be 1, got {target}");
} else {
panic!("expected Ask action");
}
}
}