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 BasicStrategy;
impl BotStrategy for BasicStrategy {
fn decide(
&self,
hand: &BasicPile,
players: &[PlayerView],
ask_log: &[AskEntry],
) -> PlayerAction {
if hand.is_empty() {
return PlayerAction::Draw;
}
let observer = players
.iter()
.position(|player_view| player_view.hand.is_some())
.unwrap_or(0);
let rank = {
let mut rank_counts: Vec<(cardpack::prelude::Pip, usize)> = {
let mut counts: std::collections::HashMap<cardpack::prelude::Pip, usize> =
std::collections::HashMap::new();
for card in hand.v() {
*counts.entry(card.rank).or_insert(0) += 1;
}
counts.into_iter().collect()
};
rank_counts.sort_by(|(rank_a, count_a), (rank_b, count_b)| {
count_b
.cmp(count_a)
.then_with(|| rank_b.index.cmp(&rank_a.index))
});
match rank_counts.first() {
Some((pip, _)) => *pip,
None => return PlayerAction::Draw,
}
};
let target = ask_log
.iter()
.rev()
.find(|entry| entry.asker != observer)
.map_or_else(
|| {
let mut rng = rand::rng();
let targets: Vec<usize> =
(0..players.len()).filter(|&idx| idx != observer).collect();
if targets.is_empty() {
(observer + 1) % players.len().max(2)
} else {
let pick = rng.random_range(0..targets.len());
targets[pick]
}
},
|entry| entry.asker,
);
PlayerAction::Ask { target, rank }
}
}
#[cfg(test)]
mod tests {
use super::*;
use cardpack::prelude::FrenchBasicCard;
fn two_player_views_alice_two_aces() -> (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);
alice.receive_card(FrenchBasicCard::KING_SPADES);
let mut bob = Player::new("Bob");
bob.receive_card(FrenchBasicCard::QUEEN_HEARTS);
let views = PlayerView::from_perspective(&[alice, bob], 0).unwrap();
let hand = views[0].hand.clone().unwrap_or_default();
(hand, views)
}
#[test]
fn test_basic_strategy_picks_most_common_rank() {
let strategy = BasicStrategy;
let (hand, players) = two_player_views_alice_two_aces();
let action = strategy.decide(&hand, &players, &[]);
if let PlayerAction::Ask { rank, .. } = action {
assert_eq!(rank.index, 'A');
} else {
panic!("expected Ask action");
}
}
#[test]
fn test_basic_strategy_targets_last_other_asker() {
let strategy = BasicStrategy;
let (hand, players) = two_player_views_alice_two_aces();
let log = vec![
AskEntry {
asker: 0,
rank: "K".to_string(),
},
AskEntry {
asker: 1,
rank: "Q".to_string(),
},
];
let action = strategy.decide(&hand, &players, &log);
if let PlayerAction::Ask { target, .. } = action {
assert_eq!(target, 1, "should target the most recent other asker");
} else {
panic!("expected Ask action");
}
}
#[test]
fn test_basic_strategy_fallback_when_log_empty() {
let strategy = BasicStrategy;
let (hand, players) = two_player_views_alice_two_aces();
let action = strategy.decide(&hand, &players, &[]);
if let PlayerAction::Ask { target, .. } = action {
assert_ne!(target, 0, "target must not be the bot itself");
} else {
panic!("expected Ask action");
}
}
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_basic_strategy_degenerate_single_player_target() {
let strategy = BasicStrategy;
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");
}
}
#[test]
fn test_basic_strategy_skips_self_in_log() {
use crate::player::Player;
let mut alice = Player::new("Alice");
alice.receive_card(FrenchBasicCard::ACE_SPADES);
let mut bob = Player::new("Bob");
bob.receive_card(FrenchBasicCard::KING_HEARTS);
let mut carol = Player::new("Carol");
carol.receive_card(FrenchBasicCard::QUEEN_HEARTS);
let views = PlayerView::from_perspective(&[alice, bob, carol], 0).unwrap();
let hand = views[0].hand.clone().unwrap_or_default();
let log = vec![
AskEntry {
asker: 2,
rank: "Q".to_string(),
}, AskEntry {
asker: 0,
rank: "A".to_string(),
}, ];
let action = BasicStrategy.decide(&hand, &views, &log);
if let PlayerAction::Ask { target, .. } = action {
assert_eq!(target, 2);
} else {
panic!("expected Ask action");
}
}
}