use std::fs; use std::time::Instant; use engine_rust::core::enums::Phase; use engine_rust::core::logic::turn_sequencer::{SearchConfig, SequencerConfig, TurnSequencer, WeightsConfig}; use engine_rust::core::logic::{CardDatabase, GameState, ACTION_BASE_PASS}; use rand::prelude::StdRng; use rand::seq::IndexedRandom; use rand::SeedableRng; use serde::Serialize; #[derive(Clone)] struct Candidate { name: &'static str, config: SequencerConfig, } #[derive(Serialize)] struct FaceoffResult { candidate: String, games: usize, p0_wins: usize, p1_wins: usize, draws: usize, avg_turns: f32, avg_duration_secs: f32, avg_evaluations: f32, } 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 json = fs::read_to_string(path).expect("Failed to read DB"); let mut db = CardDatabase::from_json(&json).expect("Failed to parse DB"); db.is_vanilla = true; return db; } panic!("cards_vanilla.json not found"); } fn load_deck(path: &str, db: &CardDatabase) -> (Vec, Vec) { let content = fs::read_to_string(path).expect("Failed to read deck"); 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(); if parts.is_empty() { continue; } 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 choose_best_live_result_action(state: &GameState, db: &CardDatabase) -> i32 { let p_idx = state.current_player as usize; let legal = state.get_legal_action_ids(db); let mut best_action = ACTION_BASE_PASS; let mut best_score = i32::MIN; for action in legal { if (600..=602).contains(&action) { let slot_idx = (action - 600) as usize; let cid = state.players[p_idx].live_zone[slot_idx]; let live_score = db.get_live(cid).map(|live| live.score as i32).unwrap_or(-1); if live_score > best_score { best_score = live_score; best_action = action; } } } best_action } fn execute_main_sequence(state: &mut GameState, db: &CardDatabase, planned_seq: &[i32]) -> usize { let mut executed = 0usize; for &action in planned_seq { if state.phase != Phase::Main { break; } let legal = state.get_legal_action_ids(db); if !legal.contains(&action) { break; } if state.step(db, action).is_err() { break; } executed += 1; } if state.phase == Phase::Main { let _ = state.step(db, ACTION_BASE_PASS); } executed } fn set_config(cfg: &SequencerConfig) { *engine_rust::core::logic::turn_sequencer::get_config().write().unwrap() = cfg.clone(); } fn run_single_game( db: &CardDatabase, p0_deck: &(Vec, Vec), p1_deck: &(Vec, Vec), p0_config: &SequencerConfig, p1_config: &SequencerConfig, seed: u64, ) -> (i32, u32, f32, usize) { let mut state = GameState::default(); let energy: Vec = db.energy_db.keys().take(12).cloned().collect(); state.initialize_game( p0_deck.0.clone(), p1_deck.0.clone(), energy.clone(), energy, p0_deck.1.clone(), p1_deck.1.clone(), ); state.ui.silent = true; let start = Instant::now(); let mut rng = StdRng::seed_from_u64(seed); let mut total_evals = 0usize; let mut main_turns_played = 0usize; const TIMEOUT_SECONDS: u64 = 45; while state.phase != Phase::Main && !state.is_terminal() { match state.phase { Phase::Rps | Phase::MulliganP1 | Phase::MulliganP2 | Phase::TurnChoice | Phase::Response => { let legal = state.get_legal_action_ids(db); if let Some(&action) = legal.choose(&mut rng) { let _ = state.step(db, action); } else { let _ = state.step(db, ACTION_BASE_PASS); } } _ => state.auto_step(db), } if start.elapsed().as_secs() > TIMEOUT_SECONDS { break; } } while !state.is_terminal() && main_turns_played < 20 { if start.elapsed().as_secs() > TIMEOUT_SECONDS { break; } let active_cfg = if state.current_player == 0 { p0_config } else { p1_config }; match state.phase { Phase::Main => { main_turns_played += 1; set_config(active_cfg); let (best_seq, _, _, evals) = TurnSequencer::plan_full_turn(&state, db); total_evals += evals; let _executed_actions = execute_main_sequence(&mut state, db, &best_seq); } Phase::LiveSet => { set_config(active_cfg); let (seq, _, _) = TurnSequencer::find_best_liveset_selection(&state, db); for &action in &seq { let _ = state.step(db, action); } let _ = state.step(db, ACTION_BASE_PASS); } Phase::Active | Phase::Draw | Phase::Energy | Phase::PerformanceP1 | Phase::PerformanceP2 => { state.auto_step(db); } Phase::LiveResult => { let action = choose_best_live_result_action(&state, db); let _ = state.step(db, action); } Phase::Terminal => break, _ => { let legal = state.get_legal_action_ids(db); if let Some(&action) = legal.choose(&mut rng) { let _ = state.step(db, action); } else { state.auto_step(db); } } } } (state.get_winner(), state.turn as u32, start.elapsed().as_secs_f32(), total_evals) } fn make_candidates(base: &SequencerConfig) -> Vec { let mut out = Vec::new(); out.push(Candidate { name: "baseline", config: base.clone(), }); let mut depth12 = base.clone(); depth12.search.max_dfs_depth = base.search.max_dfs_depth.max(12); out.push(Candidate { name: "depth12", config: depth12, }); let mut depth12_live = base.clone(); depth12_live.search.max_dfs_depth = base.search.max_dfs_depth.max(12); depth12_live.weights.live_ev_multiplier += 3.0; depth12_live.weights.liveset_placement_bonus = depth12_live.weights.liveset_placement_bonus.max(2.0); out.push(Candidate { name: "depth12_live_focus", config: depth12_live, }); let mut depth12_board = base.clone(); depth12_board.search.max_dfs_depth = base.search.max_dfs_depth.max(12); depth12_board.weights.board_presence += 0.75; depth12_board.weights.saturation_bonus += 1.0; depth12_board.weights.energy_penalty = (depth12_board.weights.energy_penalty - 0.15).max(0.0); out.push(Candidate { name: "depth12_tempo", config: depth12_board, }); let defaults = SequencerConfig { weights: WeightsConfig::default(), search: SearchConfig::default(), }; out.push(Candidate { name: "library_defaults", config: defaults, }); out } fn main() { let args: Vec = std::env::args().collect(); let mut games_per_candidate = 4usize; let mut seed_base = 100u64; let mut deck0_path = "ai/decks/liella_cup.txt".to_string(); let mut deck1_path = "ai/decks/liella_cup.txt".to_string(); let mut i = 1usize; while i < args.len() { match args[i].as_str() { "--count" if i + 1 < args.len() => { games_per_candidate = args[i + 1].parse().unwrap_or(games_per_candidate); i += 2; } "--seed" if i + 1 < args.len() => { seed_base = args[i + 1].parse().unwrap_or(seed_base); i += 2; } "--deck-p0" if i + 1 < args.len() => { deck0_path = args[i + 1].clone(); i += 2; } "--deck-p1" if i + 1 < args.len() => { deck1_path = args[i + 1].clone(); i += 2; } _ => i += 1, } } if !std::path::Path::new(&deck0_path).exists() { deck0_path = format!("../{}", deck0_path); } if !std::path::Path::new(&deck1_path).exists() { deck1_path = format!("../{}", deck1_path); } let db = load_vanilla_db(); let p0_deck = load_deck(&deck0_path, &db); let p1_deck = load_deck(&deck1_path, &db); let baseline = engine_rust::core::logic::turn_sequencer::get_config().read().unwrap().clone(); let candidates = make_candidates(&baseline); let mut results = Vec::new(); let mut best_name = "baseline".to_string(); let mut best_margin = isize::MIN; let mut best_config = baseline.clone(); for candidate in candidates { let mut p0_wins = 0usize; let mut p1_wins = 0usize; let mut draws = 0usize; let mut total_turns = 0u32; let mut total_duration = 0.0f32; let mut total_evals = 0usize; for game_idx in 0..games_per_candidate { let (winner, turns, duration, evals) = run_single_game( &db, &p0_deck, &p1_deck, &candidate.config, &baseline, seed_base + game_idx as u64, ); match winner { 0 => p0_wins += 1, 1 => p1_wins += 1, _ => draws += 1, } total_turns += turns; total_duration += duration; total_evals += evals; } let margin = p0_wins as isize - p1_wins as isize; if candidate.name != "baseline" && margin > best_margin { best_margin = margin; best_name = candidate.name.to_string(); best_config = candidate.config.clone(); } results.push(FaceoffResult { candidate: candidate.name.to_string(), games: games_per_candidate, p0_wins, p1_wins, draws, avg_turns: total_turns as f32 / games_per_candidate as f32, avg_duration_secs: total_duration / games_per_candidate as f32, avg_evaluations: total_evals as f32 / games_per_candidate as f32, }); } println!("{}", serde_json::to_string_pretty(&results).unwrap()); if best_margin > 0 { fs::write( "sequencer_config.json", serde_json::to_string_pretty(&best_config).unwrap(), ) .expect("failed to write sequencer_config.json"); eprintln!("best_candidate={} margin={} -> sequencer_config.json updated", best_name, best_margin); } else { eprintln!("no candidate beat baseline; sequencer_config.json unchanged"); } }