use tcg_core::{
GameState, PlayerId, StepResult,
};
use crate::traits::AiController;
use crate::RandomAiV4;
use super::react_ai::ReactAi;
use super::render::render_game_view_compact;
#[derive(Debug, Clone)]
pub struct GameResult {
pub winner: Option<PlayerId>,
pub turns: u32,
pub steps: u32,
pub history: Vec<TurnSummary>,
pub p1_prizes_remaining: usize,
pub p2_prizes_remaining: usize,
pub end_reason: GameEndReason,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GameEndReason {
PrizesTaken,
NoPokemon,
DeckOut,
Concede,
MaxSteps,
InProgress,
}
#[derive(Debug, Clone)]
pub struct TurnSummary {
pub turn: u32,
pub player: PlayerId,
pub actions: Vec<ActionSummary>,
pub state_after: String,
}
#[derive(Debug, Clone)]
pub struct ActionSummary {
pub action: String,
pub prompt_response: bool,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RunConfig {
pub max_steps: u32,
pub record_history: bool,
pub verbose: bool,
}
impl Default for RunConfig {
fn default() -> Self {
Self {
max_steps: 1000,
record_history: true,
verbose: false,
}
}
}
pub fn run_game(
game: &mut GameState,
p1_ai: &mut dyn AiController,
p2_ai: &mut dyn AiController,
config: &RunConfig,
) -> GameResult {
let mut steps = 0u32;
let mut turns = 0u32;
let mut history = Vec::new();
let mut current_turn_actions = Vec::new();
let mut last_turn_player = PlayerId::P1;
while steps < config.max_steps {
let result = game.step();
steps += 1;
match result {
StepResult::GameOver { winner } => {
if config.record_history && !current_turn_actions.is_empty() {
let view = game.view_for_player(last_turn_player);
history.push(TurnSummary {
turn: turns,
player: last_turn_player,
actions: current_turn_actions.clone(),
state_after: render_game_view_compact(&view),
});
}
return GameResult {
winner: Some(winner),
turns,
steps,
history,
p1_prizes_remaining: game.view_for_player(PlayerId::P1).my_prizes_count,
p2_prizes_remaining: game.view_for_player(PlayerId::P2).my_prizes_count,
end_reason: GameEndReason::PrizesTaken, };
}
StepResult::Prompt { prompt, for_player } => {
let view = game.view_for_player(for_player);
let actions = match for_player {
PlayerId::P1 => p1_ai.propose_prompt_response(&view, &prompt),
PlayerId::P2 => p2_ai.propose_prompt_response(&view, &prompt),
};
if config.verbose {
eprintln!("[Step {}] {:?} prompt: {:?}", steps, for_player, prompt);
eprintln!(" Actions: {:?}", actions);
}
for action in actions {
let action_str = format!("{:?}", action);
match game.apply_action(for_player, action) {
Ok(_) => {
current_turn_actions.push(ActionSummary {
action: action_str,
prompt_response: true,
error: None,
});
}
Err(e) => {
if config.verbose {
eprintln!(" Error: {:?}", e);
}
current_turn_actions.push(ActionSummary {
action: action_str,
prompt_response: true,
error: Some(format!("{:?}", e)),
});
}
}
}
}
StepResult::Continue => {
let current_player = game.turn.player;
if current_player != last_turn_player {
if config.record_history && !current_turn_actions.is_empty() {
let view = game.view_for_player(last_turn_player);
history.push(TurnSummary {
turn: turns,
player: last_turn_player,
actions: current_turn_actions.clone(),
state_after: render_game_view_compact(&view),
});
}
current_turn_actions.clear();
last_turn_player = current_player;
turns += 1;
}
let view = game.view_for_player(current_player);
if matches!(view.phase, tcg_rules_ex::Phase::Main | tcg_rules_ex::Phase::Attack) {
let actions = match current_player {
PlayerId::P1 => p1_ai.propose_free_actions(&view),
PlayerId::P2 => p2_ai.propose_free_actions(&view),
};
if config.verbose && !actions.is_empty() {
eprintln!("[Step {}] {:?} free actions: {:?}", steps, current_player, actions);
}
if let Some(action) = actions.into_iter().next() {
let action_str = format!("{:?}", action);
match game.apply_action(current_player, action) {
Ok(_) => {
current_turn_actions.push(ActionSummary {
action: action_str,
prompt_response: false,
error: None,
});
}
Err(e) => {
if config.verbose {
eprintln!(" Error: {:?}", e);
}
current_turn_actions.push(ActionSummary {
action: action_str,
prompt_response: false,
error: Some(format!("{:?}", e)),
});
}
}
}
}
}
StepResult::Event { event } => {
if config.verbose {
eprintln!("[Step {}] Event: {:?}", steps, event);
}
}
}
}
GameResult {
winner: None,
turns,
steps,
history,
p1_prizes_remaining: game.view_for_player(PlayerId::P1).my_prizes_count,
p2_prizes_remaining: game.view_for_player(PlayerId::P2).my_prizes_count,
end_reason: GameEndReason::MaxSteps,
}
}
pub fn run_react_vs_v4(
game: &mut GameState,
react_ai: &mut ReactAi,
v4_seed: u64,
config: &RunConfig,
) -> GameResult {
let mut v4_ai = RandomAiV4::new(v4_seed);
run_game(game, react_ai, &mut v4_ai, config)
}
#[derive(Debug, Clone)]
pub struct StepObservation {
pub game_state: String,
pub compact: String,
pub phase: String,
pub current_player: String,
pub has_prompt: bool,
pub prompt_description: Option<String>,
pub available_actions: Vec<String>,
pub game_over: bool,
pub winner: Option<String>,
pub prizes: (usize, usize),
}
pub fn create_observation(game: &GameState, for_player: PlayerId) -> StepObservation {
use super::render::render_game_view;
let view = game.view_for_player(for_player);
let hints = &view.action_hints;
let mut available_actions = Vec::new();
if !hints.playable_basic_ids.is_empty() {
available_actions.push(format!("PlayBasic ({})", hints.playable_basic_ids.len()));
}
if !hints.playable_energy_ids.is_empty() {
available_actions.push(format!("AttachEnergy ({})", hints.playable_energy_ids.len()));
}
if !hints.playable_evolution_ids.is_empty() {
available_actions.push(format!("Evolve ({})", hints.playable_evolution_ids.len()));
}
if !hints.playable_trainer_ids.is_empty() {
available_actions.push(format!("PlayTrainer ({})", hints.playable_trainer_ids.len()));
}
if hints.can_declare_attack {
available_actions.push(format!("Attack ({})", hints.usable_attacks.len()));
}
if hints.can_end_turn {
available_actions.push("EndTurn".to_string());
}
let prompt_desc = view.pending_prompt.as_ref().map(|p| format!("{:?}", p));
StepObservation {
game_state: render_game_view(&view),
compact: render_game_view_compact(&view),
phase: format!("{:?}", view.phase),
current_player: format!("{:?}", view.current_player),
has_prompt: view.pending_prompt.is_some(),
prompt_description: prompt_desc,
available_actions,
game_over: false, winner: None,
prizes: (view.my_prizes_count, view.opponent_prizes_count),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_config_default() {
let config = RunConfig::default();
assert_eq!(config.max_steps, 1000);
assert!(config.record_history);
}
}