use rand::seq::SliceRandom;
use rand::Rng;
use rand_chacha::ChaCha8Rng;
use rand::SeedableRng;
use tcg_core::{Action, Attack, AttackCost, CardInstanceId, GameView, Prompt, Type};
use crate::traits::AiController;
pub struct RandomAi {
rng: ChaCha8Rng,
}
impl RandomAi {
pub fn new(seed: u64) -> Self {
Self {
rng: ChaCha8Rng::seed_from_u64(seed),
}
}
#[allow(dead_code)]
fn shuffle_actions(&mut self, actions: &mut Vec<Action>) {
actions.shuffle(&mut self.rng);
}
fn dummy_attack() -> Attack {
Attack {
name: String::new(),
damage: 0,
attack_type: Type::Colorless,
cost: AttackCost {
total_energy: 0,
types: Vec::new(),
},
effect_ast: None,
}
}
fn choose_k(&mut self, options: &[CardInstanceId], k: usize) -> Vec<CardInstanceId> {
let mut ids: Vec<_> = options.to_vec();
ids.shuffle(&mut self.rng);
ids.truncate(k);
ids
}
fn choose_one(&mut self, options: &[CardInstanceId]) -> Option<CardInstanceId> {
options.choose(&mut self.rng).copied()
}
fn choose_one_pair(
&mut self,
left: &[CardInstanceId],
right: &[CardInstanceId],
) -> Option<(CardInstanceId, CardInstanceId)> {
Some((self.choose_one(left)?, self.choose_one(right)?))
}
fn best_attack(attacks: &[Attack]) -> Option<Attack> {
attacks.iter().cloned().max_by(|a, b| {
let dmg = a.damage.cmp(&b.damage);
if dmg != std::cmp::Ordering::Equal {
return dmg;
}
a.cost.total_energy.cmp(&b.cost.total_energy).reverse()
})
}
}
impl AiController for RandomAi {
fn propose_prompt_response(&mut self, view: &GameView, prompt: &Prompt) -> Vec<Action> {
let mut actions: Vec<Action> = Vec::new();
match prompt {
Prompt::ChooseStartingActive { options } => {
let valid_options: Vec<_> = options
.iter()
.filter(|id| view.action_hints.playable_basic_ids.contains(id))
.copied()
.collect();
if let Some(card_id) = valid_options.first().copied() {
actions.push(Action::ChooseActive { card_id });
}
}
Prompt::ChooseBenchBasics { options, min, max } => {
let valid_options: Vec<_> = options
.iter()
.filter(|id| view.action_hints.playable_basic_ids.contains(id))
.copied()
.collect();
let required_min = (*min).min(valid_options.len());
let allowed_max = (*max).min(valid_options.len()).max(required_min);
let desired = (required_min + 1).min(allowed_max);
let picked = self.choose_k(&valid_options, desired);
actions.push(Action::ChooseBench { card_ids: picked });
}
Prompt::ChooseAttack { attacks } => {
if let Some(best) = Self::best_attack(attacks) {
actions.push(Action::DeclareAttack { attack: best });
}
let mut shuffled = attacks.clone();
shuffled.shuffle(&mut self.rng);
for attack in shuffled {
actions.push(Action::DeclareAttack { attack });
}
}
Prompt::ChooseCardsFromDeck {
player,
count,
options,
min,
max,
..
} => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let required_min = min.unwrap_or(*count);
let required_max = max.unwrap_or(*count);
if options.is_empty() && required_min > 0 {
return vec![Action::EndTurn];
}
let take = required_min.min(required_max).min(options.len());
let picked = self.choose_k(options, take);
actions.push(Action::TakeCardsFromDeck { card_ids: picked });
}
Prompt::ChooseCardsFromDiscard {
player,
count,
options,
min,
max,
..
} => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let required_min = min.unwrap_or(*count);
let required_max = max.unwrap_or(*count);
if options.is_empty() && required_min > 0 {
return vec![Action::EndTurn];
}
let take = required_min.min(required_max).min(options.len());
let picked = self.choose_k(options, take);
actions.push(Action::TakeCardsFromDiscard { card_ids: picked });
}
Prompt::ChoosePokemonInPlay {
player,
options,
min,
max,
} => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let take = (*min).min(*max).min(options.len());
let picked = self.choose_k(options, take);
actions.push(Action::ChoosePokemonTargets { target_ids: picked });
}
Prompt::ReorderDeckTop { player, options } => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let mut ids = options.clone();
ids.shuffle(&mut self.rng);
actions.push(Action::ReorderDeckTop { card_ids: ids });
}
Prompt::ChooseAttachedEnergy {
player,
pokemon_id,
count,
min,
} => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let required_min = min.unwrap_or(*count);
let mut energies: Vec<CardInstanceId> = Vec::new();
if let Some(active) = view.my_active.as_ref().filter(|p| p.card.id == *pokemon_id) {
energies = active.attached_energy.iter().map(|c| c.id).collect();
} else if let Some(slot) = view.my_bench.iter().find(|p| p.card.id == *pokemon_id) {
energies = slot.attached_energy.iter().map(|c| c.id).collect();
}
energies.shuffle(&mut self.rng);
if energies.is_empty() || (required_min == 0 && self.rng.gen_bool(0.5)) {
actions.push(Action::ChooseAttachedEnergy { energy_ids: vec![] });
} else {
energies.truncate(*count);
actions.push(Action::ChooseAttachedEnergy { energy_ids: energies });
}
}
Prompt::ChooseCardsFromHand {
player,
count,
options,
min,
max,
return_to_deck,
} => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let required_min = min.unwrap_or(*count);
let required_max = max.unwrap_or(*count);
let pool: Vec<CardInstanceId> = if options.is_empty() {
view.my_hand.iter().map(|c| c.id).collect()
} else {
options.clone()
};
if pool.is_empty() && required_min > 0 {
return vec![Action::EndTurn];
}
let take = required_min.min(required_max).min(pool.len());
let picked = self.choose_k(&pool, take);
if *return_to_deck {
actions.push(Action::ReturnCardsFromHandToDeck { card_ids: picked });
} else {
actions.push(Action::DiscardCardsFromHand { card_ids: picked });
}
}
Prompt::ChooseCardsInPlay {
player,
options,
min,
max,
} => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let take = (*min).min(*max).min(options.len());
let picked = self.choose_k(options, take);
actions.push(Action::ChooseCardsInPlay { card_ids: picked });
}
Prompt::ChoosePrizeCards {
player,
options,
min,
max,
} => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let take = (*min).min(*max).min(options.len());
let picked = self.choose_k(options, take);
actions.push(Action::ChoosePrizeCards { card_ids: picked });
}
Prompt::ChooseDefenderAttack { player, attacks, .. } => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
if let Some(name) = attacks.iter().max().cloned() {
actions.push(Action::DeclareAttack {
attack: Attack {
name,
..Self::dummy_attack()
},
});
}
let mut attacks = attacks.clone();
attacks.shuffle(&mut self.rng);
for name in attacks {
actions.push(Action::DeclareAttack {
attack: Attack {
name,
..Self::dummy_attack()
},
});
}
}
Prompt::ChoosePokemonAttack { player, attacks, .. } => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
if let Some(name) = attacks.iter().max().cloned() {
actions.push(Action::ChoosePokemonAttack { attack_name: name });
}
let mut names = attacks.clone();
names.shuffle(&mut self.rng);
for name in names {
actions.push(Action::ChoosePokemonAttack { attack_name: name });
}
}
Prompt::ChooseSpecialCondition { player, options } => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
if let Some(condition) = options.first().copied() {
actions.push(Action::ChooseSpecialCondition { condition });
}
let mut choices = options.clone();
choices.shuffle(&mut self.rng);
for condition in choices {
actions.push(Action::ChooseSpecialCondition { condition });
}
}
Prompt::ChooseNewActive { player, options } => {
if *player != view.player_id {
return vec![Action::EndTurn];
}
let mut candidates: Vec<CardInstanceId> = if options.is_empty() {
view.my_bench.iter().map(|p| p.card.id).collect()
} else {
options.clone()
};
if let Some(card_id) = candidates.first().copied() {
actions.push(Action::ChooseNewActive { card_id });
}
candidates.shuffle(&mut self.rng);
for card_id in candidates {
actions.push(Action::ChooseNewActive { card_id });
}
}
}
actions
}
fn propose_free_actions(&mut self, view: &GameView) -> Vec<Action> {
if view.current_player != view.player_id {
return Vec::new();
}
if view.pending_prompt.is_some() {
return Vec::new();
}
let mut actions: Vec<Action> = Vec::new();
let hints = &view.action_hints;
if let Some(card_id) = self.choose_one(&hints.playable_basic_ids) {
actions.push(Action::PlayBasic { card_id });
}
if let Some((energy_id, target_id)) =
self.choose_one_pair(&hints.playable_energy_ids, &hints.attach_targets)
{
actions.push(Action::AttachEnergy { energy_id, target_id });
}
if let Some(best) = Self::best_attack(&hints.usable_attacks) {
actions.push(Action::DeclareAttack { attack: best });
} else if hints.can_declare_attack {
actions.push(Action::DeclareAttack {
attack: Self::dummy_attack(),
});
}
if hints.can_end_turn {
actions.push(Action::EndTurn);
} else {
actions.push(Action::EndTurn);
}
actions
}
}