use cardpack::prelude::BasicPile;
use crate::game::PlayerAction;
use crate::player::{AskEntry, PlayerView};
use super::strategy::{BasicStrategy, BotStrategy, RandomStrategy};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StrategyKind {
Random,
Basic,
Custom,
}
pub struct BotProfile {
pub name: String,
strategy: Box<dyn BotStrategy>,
kind: StrategyKind,
}
impl std::fmt::Debug for BotProfile {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("BotProfile")
.field("name", &self.name)
.field("strategy", &"<dyn BotStrategy>")
.field("kind", &self.kind)
.finish()
}
}
impl BotProfile {
#[must_use]
pub fn random(name: impl Into<String>) -> Self {
Self {
name: name.into(),
strategy: Box::new(RandomStrategy),
kind: StrategyKind::Random,
}
}
#[must_use]
pub fn basic(name: impl Into<String>) -> Self {
Self {
name: name.into(),
strategy: Box::new(BasicStrategy),
kind: StrategyKind::Basic,
}
}
#[must_use]
pub fn custom(name: impl Into<String>, strategy: Box<dyn BotStrategy>) -> Self {
Self {
name: name.into(),
strategy,
kind: StrategyKind::Custom,
}
}
#[must_use]
pub fn decide(
&self,
hand: &BasicPile,
players: &[PlayerView],
ask_log: &[AskEntry],
) -> PlayerAction {
self.strategy.decide(hand, players, ask_log)
}
#[must_use]
pub fn default_profiles() -> Vec<Self> {
vec![
Self::basic("Harriet"),
Self::basic("Bertram"),
Self::random("Lucky"),
Self::random("Chance"),
]
}
}
impl Clone for BotProfile {
fn clone(&self) -> Self {
let strategy: Box<dyn BotStrategy> = match self.kind {
StrategyKind::Random => Box::new(RandomStrategy),
StrategyKind::Basic | StrategyKind::Custom => Box::new(BasicStrategy),
};
Self {
name: self.name.clone(),
strategy,
kind: self.kind,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bot_profile_random_name() {
let profile = BotProfile::random("Lucky");
assert_eq!(profile.name, "Lucky");
}
#[test]
fn test_bot_profile_basic_name() {
let profile = BotProfile::basic("Harriet");
assert_eq!(profile.name, "Harriet");
}
#[test]
fn test_bot_profile_default_profiles_count() {
let profiles = BotProfile::default_profiles();
assert_eq!(profiles.len(), 4);
}
#[test]
fn test_bot_profile_default_profiles_names() {
let profiles = BotProfile::default_profiles();
assert_eq!(profiles[0].name, "Harriet");
assert_eq!(profiles[1].name, "Bertram");
assert_eq!(profiles[2].name, "Lucky");
assert_eq!(profiles[3].name, "Chance");
}
#[test]
fn test_bot_profile_clone_preserves_name() {
let profile = BotProfile::basic("Harriet");
let cloned = profile.clone();
assert_eq!(cloned.name, "Harriet");
}
#[test]
fn test_bot_profile_decide_returns_ask() {
use crate::player::Player;
use cardpack::prelude::FrenchBasicCard;
let profile = BotProfile::basic("Harriet");
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 players = PlayerView::from_perspective(&[alice, bob], 0).unwrap();
let hand = players[0].hand.clone().unwrap_or_default();
let action = profile.decide(&hand, &players, &[]);
assert!(matches!(action, PlayerAction::Ask { .. }));
}
#[test]
fn test_bot_profile_custom() {
use crate::player::Player;
use cardpack::prelude::FrenchBasicCard;
struct FixedTarget;
impl BotStrategy for FixedTarget {
fn decide(
&self,
hand: &BasicPile,
_players: &[PlayerView],
_ask_log: &[AskEntry],
) -> PlayerAction {
let rank = hand
.v()
.first()
.map(|card| card.rank)
.unwrap_or(FrenchBasicCard::ACE_SPADES.rank);
PlayerAction::Ask { target: 1, rank }
}
}
let profile = BotProfile::custom("Custom", Box::new(FixedTarget));
assert_eq!(profile.name, "Custom");
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 players = PlayerView::from_perspective(&[alice, bob], 0).unwrap();
let hand = players[0].hand.clone().unwrap_or_default();
if let PlayerAction::Ask { target, .. } = profile.decide(&hand, &players, &[]) {
assert_eq!(target, 1);
} else {
panic!("expected Ask");
}
}
#[test]
fn test_bot_profile_debug_contains_name_and_struct_name() {
let profile = BotProfile::random("Lucky");
let debug = format!("{profile:?}");
assert!(
debug.contains("BotProfile"),
"debug must contain struct name"
);
assert!(debug.contains("Lucky"), "debug must contain the bot's name");
}
#[test]
fn test_bot_profile_debug_basic_strategy_contains_kind() {
let profile = BotProfile::basic("Harriet");
let debug = format!("{profile:?}");
assert!(debug.contains("BotProfile"));
assert!(debug.contains("Harriet"));
}
}