Spaces:
Sleeping
Sleeping
| use std::fs; | |
| use std::time::Instant; | |
| use engine_rust::core::logic::{CardDatabase, GameState, ACTION_BASE_PASS}; | |
| use engine_rust::core::logic::turn_sequencer::TurnSequencer; | |
| use engine_rust::core::enums::Phase; | |
| struct EvalMetrics { | |
| total_nodes: usize, | |
| total_evals: usize, | |
| clones: usize, | |
| liveset_searches: usize, | |
| max_depth: usize, | |
| paths_by_length: Vec<usize>, // count of paths ending at each length (0-20) | |
| } | |
| 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<i32>, Vec<i32>) { | |
| 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 main() { | |
| let db = load_vanilla_db(); | |
| let (p0_members, p0_lives) = load_deck("ai/decks/liella_cup.txt", &db); | |
| let (p1_members, p1_lives) = load_deck("ai/decks/liella_cup.txt", &db); | |
| let mut state = GameState::default(); | |
| let energy_vec: Vec<i32> = db.energy_db.keys().take(12).cloned().collect(); | |
| state.initialize_game(p0_members, p1_members, energy_vec.clone(), energy_vec, p0_lives, p1_lives); | |
| state.ui.silent = true; | |
| // Skip to Main phase | |
| while state.phase != Phase::Main && !state.is_terminal() { | |
| state.auto_step(&db); | |
| } | |
| println!("\n=== CODE STRUCTURE ANALYSIS ===\n"); | |
| // Count actual number of legal actions in starting Main phase | |
| let mut actions: Vec<i32> = Vec::new(); | |
| state.generate_legal_actions(&db, state.current_player as usize, &mut actions); | |
| println!("[START] Player {}: {} legal actions available", state.current_player, actions.len()); | |
| println!(" Actions: {:?}", actions); | |
| // Measure evaluation cost | |
| println!("\n=== EVALUATION COST BREAKDOWN ===\n"); | |
| let eval_start = Instant::now(); | |
| for _ in 0..1000 { | |
| let _ = TurnSequencer::plan_full_turn(&state, &db); | |
| break; // Just ONE search to measure timing | |
| } | |
| let eval_time = eval_start.elapsed(); | |
| println!("[SEARCH 1] Full search time: {:.3}ms", eval_time.as_secs_f32() * 1000.0); | |
| // Breakdown what evaluate_stop_state involves: | |
| println!("\n=== WHAT HAPPENS IN SEARCH ===\n"); | |
| println!("exact_small_turn_search recursively:"); | |
| println!(" 1. Check if phase == Main AND depth > 0"); | |
| println!(" 2. If condition fails: call evaluate_stop_state"); | |
| println!(" - Clone state (1.7µs) <- EXPENSIVE"); | |
| println!(" - If in LiveSet phase: find_best_liveset_selection"); | |
| println!(" - Call evaluate_state_for_player_with_weights"); | |
| println!(" - For vanilla: count cards (fast)"); | |
| println!(" - Return score"); | |
| println!(" 3. If condition true:"); | |
| println!(" - Evaluate PASS action"); | |
| println!(" - For each other legal action:"); | |
| println!(" - Clone state"); | |
| println!(" - Call step()"); | |
| println!(" - Recurse with depth-1"); | |
| println!("\nKey insight: state.clone() happens for EVERY node"); | |
| println!("\n=== SEARCH TREE ANALYSIS ===\n"); | |
| let mut _typical_depth = 0; | |
| let mut _nodes_visited: Vec<usize> = Vec::new(); | |
| // Simulate search tree: show typical branching | |
| println!("Typical action count by depth:"); | |
| println!(" Depth 0 (start of turn): {} actions", actions.len()); | |
| // Try a few actions to see what's available | |
| for (i, &action) in actions.iter().take(2).enumerate() { | |
| let mut next_state = state.clone(); | |
| if next_state.step(&db, action).is_ok() { | |
| let mut next_actions: Vec<i32> = Vec::new(); | |
| if next_state.phase == Phase::Main { | |
| next_state.generate_legal_actions(&db, next_state.current_player as usize, &mut next_actions); | |
| println!(" Depth 1 (after action {}): {} actions available", i, next_actions.len()); | |
| } else { | |
| println!(" Depth 1 (after action {}): Phase changed to {:?}", i, next_state.phase); | |
| } | |
| } | |
| } | |
| println!("\n=== OPTIMIZATION OPPORTUNITIES ===\n"); | |
| println!("1. **State clone cost**: evaluate_stop_state clones state EVERY call"); | |
| println!(" - For vanilla, we just count cards - clone isn't needed!"); | |
| println!(" - Savings: 1.7µs × N nodes per turn"); | |
| println!("\n2. **Depth ceiling**: If average moves per depth = 2-3"); | |
| println!(" - Depth 10 probably hits phase change around depth 3-5"); | |
| println!(" - Exploring further is redundant"); | |
| println!("\n3. **LiveSet overhead**: If we're in MainPhase only"); | |
| println!(" - evaluate_stop_state checks for LiveSet each time"); | |
| println!(" - Main phase doesn't transition to LiveSet during search"); | |
| println!(" - This check is redundant"); | |
| println!("\n=== ACTUAL TIME BREAKDOWN ===\n"); | |
| // Profile what's actually slow | |
| let t = Instant::now(); | |
| for _ in 0..100 { | |
| let _state_clone = state.clone(); | |
| } | |
| println!("[CLONE x100] {:.3}ms ({:.3}µs each)", t.elapsed().as_secs_f32() * 1000.0, t.elapsed().as_secs_f32() * 1000.0 / 100.0); | |
| let t = Instant::now(); | |
| for _ in 0..1000 { | |
| let mut s = state.clone(); | |
| let _ = s.step(&db, ACTION_BASE_PASS); | |
| } | |
| println!("[STEP x1000] {:.3}ms ({:.3}µs each)", t.elapsed().as_secs_f32() * 1000.0, t.elapsed().as_secs_f32() * 1000.0 / 1000.0); | |
| println!("\n=== CONCLUSION ===\n"); | |
| println!("For vanilla (~700 nodes per turn with ~60ms total):"); | |
| println!("- If each node does: clone(1.7µs) + step(2.25µs) = ~4µs"); | |
| println!("- 700 × 4µs = 2.8ms just for tree traversal"); | |
| println!("- Remaining 57.2ms is in LiveSet and evaluation logic"); | |
| println!("\nFastest optimization:"); | |
| println!("1. Skip state clone in evaluate_stop_state for vanilla"); | |
| println!("2. Skip LiveSet check during Main phase search"); | |
| println!("3. Reduce depth to 6 (realistically max moves available)"); | |
| } |