/// test_turn_runner.rs Diagnostic Solitaire Game Runner for Training /// /// Run with: cargo run --bin test_turn_runner [--release] /// /// Purpose: Fast, diagnostics-focused game runner for the no-abilities variant. /// Output: Per-turn and per-action diagnostics including: /// - Number of nodes explored (exhaustive search) /// - Time to run (microseconds) /// - Score breakdown (board_score + live_ev) /// - Performance metrics /// // /// TUNABLE PARAMETERS const NUM_GAMES: usize = 5; const VERBOSE: bool = true; const TURN_LIMIT: u16 = 6; // Quick games for diagnostics const _ACTIONS_PER_TURN_LIMIT: usize = 50; // Max play actions per turn // /// use std::fs; use std::time::Instant; use engine_rust::core::enums::Phase; use engine_rust::core::logic::turn_sequencer::{TurnSequencer}; use engine_rust::core::logic::{CardDatabase, GameState, ACTION_BASE_PASS}; use rand::SeedableRng; use rand::seq::IndexedRandom; // // // Diagnostics Tracking // // #[derive(Debug, Clone, Default)] struct ActionDiagnostic { _action_id: i32, action_label: String, board_score: f32, live_ev: f32, total_score: f32, _nodes_explored: usize, _time_us: u128, } #[derive(Debug, Clone, Default)] struct TurnDiagnostic { _turn_number: u16, _phase_name: String, actions: Vec, final_board_score: f32, final_live_ev: f32, final_total_score: f32, total_nodes: usize, total_time_us: u128, } #[derive(Debug, Clone, Default)] struct GameDiagnostic { game_number: usize, turns: Vec, winner: i32, final_scores: (u32, u32), total_time_ms: f32, } // // // Database Loading // // fn load_vanilla_db() -> CardDatabase { let candidates = [ "data/cards_vanilla.json", "../data/cards_vanilla.json", "../../data/cards_vanilla.json", ]; for path in &candidates { if !std::path::Path::new(path).exists() { continue; } let abs = std::fs::canonicalize(path) .unwrap_or_else(|_| std::path::PathBuf::from(path)); println!("[DB_LOAD] Loading vanilla DB from: {:?}", abs); let json = fs::read_to_string(path).expect("Failed to read vanilla DB"); let mut db = CardDatabase::from_json(&json).expect("Failed to parse vanilla DB"); db.is_vanilla = true; return db; } panic!("Could not find cards_vanilla.json"); } fn load_deck_combined(path: &str, db: &CardDatabase) -> (Vec, Vec) { let content = fs::read_to_string(path).expect("Failed to read deck file"); let mut members = Vec::new(); let mut lives = Vec::new(); for line in content.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } let parts: Vec<&str> = line.split_whitespace().collect(); let card_no = parts[0]; let count: usize = if parts.len() >= 3 && parts[1] == "x" { parts[2].parse().unwrap_or(1) } else { 1 }; if let Some(id) = db.id_by_no(card_no) { for _ in 0..count { if db.lives.contains_key(&id) { lives.push(id); } else { members.push(id); } } } } while members.len() < 48 { if let Some(&id) = db.members.keys().next() { members.push(id); } else { break; } } while lives.len() < 12 { if let Some(&id) = db.lives.keys().next() { lives.push(id); } else { break; } } members.truncate(48); lives.truncate(12); (members, lives) } fn fallback_deck(db: &CardDatabase) -> (Vec, Vec) { let members: Vec = db.members.keys().take(48).cloned().collect(); let lives: Vec = db.lives.keys().take(12).cloned().collect(); (members, lives) } // // // Main Phase Handler (Exhaustive DFS) // // struct MainPhaseResult { best_sequence: Vec, board_score: f32, live_ev: f32, total_score: f32, nodes_explored: usize, time_us: u128, evaluated_actions: Vec, } fn handle_main_phase(state: &GameState, db: &CardDatabase) -> MainPhaseResult { let start = Instant::now(); let (evals, best_seq, total_nodes, _search_secs, (board_score, live_ev)) = TurnSequencer::plan_full_turn_with_stats(state, db); let total_score = board_score + live_ev; let duration = start.elapsed().as_micros(); // Convert evaluations to action diagnostics let mut evaluated_actions = Vec::new(); for (action_id, total_val, b_score, l_ev) in evals { let label = state.get_verbose_action_label(action_id, db); evaluated_actions.push(ActionDiagnostic { _action_id: action_id, action_label: label, board_score: b_score, live_ev: l_ev, total_score: total_val, _nodes_explored: 0, _time_us: 0, }); } MainPhaseResult { best_sequence: best_seq, board_score, live_ev, total_score, nodes_explored: total_nodes, time_us: duration, evaluated_actions, } } // // // LiveSet Phase Handler // // struct LiveSetPhaseResult { best_sequence: Vec, live_ev: f32, nodes_explored: usize, time_us: u128, } fn handle_liveset_phase(state: &GameState, db: &CardDatabase) -> LiveSetPhaseResult { let start = Instant::now(); let (seq, nodes, val_encoded) = TurnSequencer::find_best_liveset_selection(state, db); let duration = start.elapsed().as_micros(); let live_ev = val_encoded as f32 / 1000.0; LiveSetPhaseResult { best_sequence: seq, live_ev, nodes_explored: nodes, time_us: duration, } } // // // Single Turn Runner (Main 竊・LiveSet) // // fn run_single_turn(state: &mut GameState, db: &CardDatabase, turn_num: u16) -> TurnDiagnostic { let mut turn_diag = TurnDiagnostic { _turn_number: turn_num, _phase_name: "Main竊鱈iveSet".to_string(), ..Default::default() }; if VERBOSE { println!( "\n[Turn {}] Starting Main Phase: P0_Score={}, P1_Score={}", turn_num, state.players[0].score, state.players[1].score ); println!( " P0 Hand: {}, Deck: {}, Lives: {}", state.players[0].hand.len(), state.players[0].deck.len(), state.players[0].success_lives.len() ); } // // MAIN PHASE let main_phase_start = Instant::now(); let main_result = handle_main_phase(state, db); if VERBOSE && !main_result.evaluated_actions.is_empty() { println!("-------------------------------------------------------"); for action_diag in main_result.evaluated_actions.iter().take(5) { println!( " {:30} Board={:6.2} Live={:6.2} Total={:7.2}", action_diag.action_label, action_diag.board_score, action_diag.live_ev, action_diag.total_score ); } if main_result.evaluated_actions.len() > 5 { println!("-------------------------------------------------------"); } println!( " Best Sequence: {} actions, Nodes: {}, Time: {}s", main_result.best_sequence.len(), main_result.nodes_explored, main_result.time_us ); println!( " Best Score: Board={:.2} + Live={:.2} = Total={:.2}", main_result.board_score, main_result.live_ev, main_result.total_score ); } turn_diag.actions = main_result.evaluated_actions; turn_diag.total_nodes += main_result.nodes_explored; turn_diag.total_time_us += main_result.time_us as u128; // Execute best sequence for &action in &main_result.best_sequence { if state.step(db, action).is_err() { break; } if state.phase != Phase::Main { break; } } // Ensure we transition to EndMain (or LiveSet if applicable) let _ = state.step(db, ACTION_BASE_PASS); // // LIVESET PHASE if state.phase == Phase::LiveSet { let liveset_result = handle_liveset_phase(state, db); if VERBOSE { println!( " LiveSet Phase: Nodes={}, Live_EV={:.2}, Time={}s", liveset_result.nodes_explored, liveset_result.live_ev, liveset_result.time_us ); } // Execute best liveset sequence for &action in &liveset_result.best_sequence { let _ = state.step(db, action); } turn_diag.final_live_ev = liveset_result.live_ev; turn_diag.total_nodes += liveset_result.nodes_explored; turn_diag.total_time_us += liveset_result.time_us as u128; } // Pass to end turn let _ = state.step(db, ACTION_BASE_PASS); // Auto-advance phases until next Main or Terminal (with safety limit) let mut phase_advance_count = 0; const MAX_PHASE_STEPS: usize = 100; while !state.is_terminal() && state.phase != Phase::Main && state.turn == turn_num && phase_advance_count < MAX_PHASE_STEPS { let phase_before = state.phase.clone(); state.auto_step(db); if state.phase == Phase::Response { // Randomize response (e.g., Mulligan, TurnChoice, RPS) let legal = state.get_legal_action_ids(db); if !legal.is_empty() { let action = legal[0]; let _ = state.step(db, action); } } // Detect if we're stuck in a phase if state.phase == phase_before { phase_advance_count += 1; if phase_advance_count > 10 { eprintln!("笞・・ WARNING: Phase stuck at {:?} for {} steps. Breaking.", state.phase, phase_advance_count); break; } } else { phase_advance_count = 0; } } turn_diag.final_board_score = main_result.board_score; turn_diag.final_total_score = main_result.total_score; turn_diag.total_time_us += main_phase_start.elapsed().as_micros(); println!( " Turn {} Summary: Nodes={}, Time={:.3}ms, Score={:.2}", turn_num, turn_diag.total_nodes, turn_diag.total_time_us as f64 / 1000.0, turn_diag.final_total_score ); turn_diag } // // // Full Game Runner // // fn run_game( game_idx: usize, member_cards: &[i32], live_cards: &[i32], energy_ids: &[i32], db: &CardDatabase, ) -> GameDiagnostic { let game_start = Instant::now(); let mut game_diag = GameDiagnostic { game_number: game_idx + 1, ..Default::default() }; let mut state = GameState::default(); state.initialize_game( member_cards.to_vec(), member_cards.to_vec(), energy_ids.to_vec(), energy_ids.to_vec(), live_cards.to_vec(), live_cards.to_vec(), ); state.ui.silent = true; println!("======================================================="); println!("-------------------------------------------------------"); println!("======================================================="); // Auto-advance to first Main phase (with proper handling of RPS/Mulligan/etc) let mut init_step_count = 0; const MAX_INIT_STEPS: usize = 50; let mut rng = rand::rngs::SmallRng::from_os_rng(); while !state.is_terminal() && state.phase != Phase::Main && init_step_count < MAX_INIT_STEPS { match state.phase { Phase::Rps | Phase::MulliganP1 | Phase::MulliganP2 | Phase::TurnChoice | Phase::Response => { let legal = state.get_legal_action_ids(db); if !legal.is_empty() { if let Some(&action) = legal.choose(&mut rng) { let _ = state.step(db, action as i32); } else { break; } } else { break; } } _ => { state.auto_step(db); } } init_step_count += 1; } if init_step_count >= MAX_INIT_STEPS { eprintln!("笞・・ WARNING: Could not reach first Main phase after {} steps", MAX_INIT_STEPS); } if state.is_terminal() { println!("[TERMINAL] Game ended before first Main phase"); game_diag.winner = state.get_winner(); game_diag.final_scores = (state.players[0].score, state.players[1].score); game_diag.total_time_ms = game_start.elapsed().as_secs_f32() * 1000.0; return game_diag; } // Run turns (with safety limit) let mut step_count = 0; const MAX_STEPS_TOTAL: usize = 1000; while !state.is_terminal() && state.turn <= TURN_LIMIT && step_count < MAX_STEPS_TOTAL { // Ensure current player Main phase if state.phase != Phase::Main { match state.phase { Phase::Rps | Phase::MulliganP1 | Phase::MulliganP2 | Phase::TurnChoice | Phase::Response => { let legal = state.get_legal_action_ids(db); if !legal.is_empty() { if let Some(&action) = legal.choose(&mut rng) { let _ = state.step(db, action as i32); } else { break; } } else { break; } } _ => { state.auto_step(db); } } step_count += 1; continue; } let current_turn = state.turn; let turn_diag = run_single_turn(&mut state, db, current_turn); game_diag.turns.push(turn_diag); step_count += 1; } if step_count >= MAX_STEPS_TOTAL { eprintln!("笞・・ WARNING: Reached max step limit ({}) - game may have infinite loop", MAX_STEPS_TOTAL); } game_diag.winner = state.get_winner(); game_diag.final_scores = (state.players[0].score, state.players[1].score); game_diag.total_time_ms = game_start.elapsed().as_secs_f32() * 1000.0; println!( "\n \n Final Result: Winner=P{}, Turns={}, Time={:.2}ms\n Final Score: P0={} P1={}\n ", game_diag.winner, state.turn, game_diag.total_time_ms, game_diag.final_scores.0, game_diag.final_scores.1 ); game_diag } // // // Summary & Output // // fn print_summary(games: &[GameDiagnostic]) { println!("======================================================="); println!("-------------------------------------------------------"); println!("======================================================="); let total_games = games.len(); let total_nodes: usize = games .iter() .flat_map(|g| &g.turns) .map(|t| t.total_nodes) .sum(); let total_time_ms: f32 = games.iter().map(|g| g.total_time_ms).sum(); let avg_time_ms = total_time_ms / total_games as f32; let avg_nodes_per_turn = if games.iter().map(|g| g.turns.len()).sum::() > 0 { total_nodes as f32 / games.iter().map(|g| g.turns.len()).sum::() as f32 } else { 0.0 }; println!("Games Run: {}", total_games); println!("Total Nodes: {}", total_nodes); println!("Total Time: {:.2}ms", total_time_ms); println!("Avg Time Per Game: {:.2}ms", avg_time_ms); println!("Avg Nodes Per Turn: {:.0}", avg_nodes_per_turn); println!("Throughput: {:.1} games/sec", 1000.0 / avg_time_ms); println!("-------------------------------------------------------"); for game in games { println!( "Game {}: {} turns, {} nodes, {:.2}ms, P0_Score={} P1_Score={}", game.game_number, game.turns.len(), game.turns.iter().map(|t| t.total_nodes).sum::(), game.total_time_ms, game.final_scores.0, game.final_scores.1 ); } println!("-------------------------------------------------------"); } fn main() { println!("======================================================="); println!("-------------------------------------------------------"); println!("-------------------------------------------------------"); println!("======================================================="); let cfg = engine_rust::core::logic::turn_sequencer::get_config().read().unwrap().clone(); println!( "Config: DFS_Depth={}, MC_Trials={}\n", cfg.search.max_dfs_depth, cfg.search.mc_trials ); let db = load_vanilla_db(); let deck_path = "ai/decks/liella_cup.txt"; let (member_cards, live_cards) = if std::path::Path::new(deck_path).exists() { load_deck_combined(deck_path, &db) } else { fallback_deck(&db) }; let energy_ids: Vec = db.energy_db.keys().take(12).cloned().collect(); let mut all_diagnostics = Vec::new(); for i in 0..NUM_GAMES { let game_diag = run_game(i, &member_cards, &live_cards, &energy_ids, &db); all_diagnostics.push(game_diag); } print_summary(&all_diagnostics); }