#[cfg(test)] mod tests { use crate::core::enums::*; use crate::core::logic::card_db::CardDatabase; use crate::core::logic::game::{ActionReceiver, GameState}; use crate::test_helpers::Action; struct TestReceiver { actions: Vec, ids: Vec, } impl ActionReceiver for TestReceiver { fn add_action(&mut self, action_id: usize) { self.ids.push(action_id); } fn reset(&mut self) { self.ids.clear(); self.actions.clear(); } fn is_empty(&self) -> bool { self.ids.is_empty() } } fn load_db() -> CardDatabase { let json_str = std::fs::read_to_string("../data/cards_compiled.json") .expect("Failed to read cards_compiled.json"); CardDatabase::from_json(&json_str).expect("Failed to parse CardDatabase") } fn new_receiver() -> TestReceiver { TestReceiver { actions: Vec::new(), ids: Vec::new(), } } fn stage_actions_in_range(receiver: &TestReceiver, lo: usize, hi: usize) -> Vec { receiver .ids .iter() .filter(|&&id| id >= lo && id < hi) .cloned() .collect() } // ===================== Original Repro Test ===================== #[test] fn test_ability_activation_zone_repro() { let db = load_db(); // Card ID 4264 (PL!HS-bp1-003-R+) — has Activated ability at index 1 let target_cid = 4264; let mut state = GameState::default(); state.phase = Phase::Main; state.current_player = 0; state.players[0] .energy_zone .extend(std::iter::repeat(100).take(10)); // CASE 1: Card in Discard → NO activation state.players[0].discard.push(target_cid); let mut receiver = new_receiver(); state.generate_legal_actions(&db, 0, &mut receiver); let discard_actions = stage_actions_in_range( &receiver, ACTION_BASE_DISCARD_ACTIVATE as usize, ACTION_BASE_ENERGY as usize, ); println!("Discard Actions (IDs): {:?}", discard_actions); assert!( discard_actions.is_empty(), "Ability should NOT be activatable from Discard" ); // CASE 2: Card on Stage → activation at ab_idx=1 state.players[0].discard.clear(); state.players[0].stage[0] = target_cid; state.players[0].set_tapped(0, false); receiver.reset(); state.generate_legal_actions(&db, 0, &mut receiver); let action_id = (ACTION_BASE_STAGE + 0 * 100 + 1 * 10) as usize; let stage_actions = stage_actions_in_range(&receiver, action_id, action_id + 1); println!("Stage Actions (IDs): {:?}", stage_actions); assert!( !stage_actions.is_empty(), "Ability 1 (second ability) should be activatable from Stage" ); } // ===================== Test 3: Multi-Ability Stage Actions ===================== // Verify that a card with TWO activated abilities generates TWO separate action IDs. // Card 4264 has: Ability 0 (Constant, trigger=6) + Ability 1 (Activated, trigger=7) // So only 1 activated action expected. We test that the encoding is correct. // For a true multi-activated-ability test, we place the card in slot 1 instead. #[test] fn test_multi_ability_encoding_slots() { let db = load_db(); let target_cid = 4264; // Has 1 activated ability at index 1 let mut state = GameState::default(); state.phase = Phase::Main; state.current_player = 0; state.players[0] .energy_zone .extend(std::iter::repeat(100).take(10)); // Place same card in slot 0 AND slot 2 to verify encoding differs state.players[0].stage[0] = target_cid; state.players[0].stage[2] = target_cid; state.players[0].set_tapped(0, false); state.players[0].set_tapped(2, false); let mut receiver = new_receiver(); state.generate_legal_actions(&db, 0, &mut receiver); // Slot 0, ab_idx 1 → ACTION_BASE_STAGE + 0*100 + 1*10 // Slot 2, ab_idx 1 → ACTION_BASE_STAGE + 2*100 + 1*10 let slot0_id = (ACTION_BASE_STAGE + 0 * 100 + 1 * 10) as usize; let slot2_id = (ACTION_BASE_STAGE + 2 * 100 + 1 * 10) as usize; let slot0_actions = stage_actions_in_range(&receiver, slot0_id, slot0_id + 1); let slot2_actions = stage_actions_in_range(&receiver, slot2_id, slot2_id + 1); println!("Slot 0 actions: {:?}", slot0_actions); println!("Slot 2 actions: {:?}", slot2_actions); assert!( !slot0_actions.is_empty(), "Slot 0 should have activated ability" ); assert!( !slot2_actions.is_empty(), "Slot 2 should have activated ability" ); assert_ne!( slot0_actions[0], slot2_actions[0], "Action IDs must differ between slots" ); } // ===================== Test 4: Tapped Member Activation ===================== // A tapped member should still be able to activate abilities that don't have TapSelf cost. // Card 4264 ability 1 has only Energy cost, so tapping the member should NOT block it. #[test] fn test_tapped_member_can_activate_non_tapself() { let db = load_db(); let target_cid = 4264; let mut state = GameState::default(); state.phase = Phase::Main; state.current_player = 0; state.players[0] .energy_zone .extend(std::iter::repeat(100).take(10)); state.players[0].stage[0] = target_cid; // Case A: Untapped → should activate state.players[0].set_tapped(0, false); let mut receiver = new_receiver(); state.generate_legal_actions(&db, 0, &mut receiver); let untapped_actions = stage_actions_in_range( &receiver, (ACTION_BASE_STAGE + 10) as usize, (ACTION_BASE_STAGE + 11) as usize, ); println!("Untapped: {:?}", untapped_actions); assert!( !untapped_actions.is_empty(), "Untapped member should be able to activate Energy-cost ability" ); // Case B: Tapped → should STILL activate (ability doesn't require TapSelf) state.players[0].set_tapped(0, true); receiver.reset(); state.generate_legal_actions(&db, 0, &mut receiver); let tapped_actions = stage_actions_in_range( &receiver, (ACTION_BASE_STAGE + 10) as usize, (ACTION_BASE_STAGE + 11) as usize, ); println!("Tapped: {:?}", tapped_actions); // This ability uses Energy cost, NOT TapSelf. A tapped member CAN still use it. assert!( !tapped_actions.is_empty(), "Tapped member should still activate Energy-cost ability (no TapSelf required)" ); } // ===================== Test 5: prevent_activate Blocks All Zones ===================== #[test] fn test_prevent_activate_blocks_all() { let db = load_db(); let target_cid = 4264; let mut state = GameState::default(); state.phase = Phase::Main; state.current_player = 0; state.players[0] .energy_zone .extend(std::iter::repeat(100).take(10)); state.players[0].stage[0] = target_cid; state.players[0].set_tapped(0, false); // Baseline: Should generate stage activation let mut receiver = new_receiver(); state.generate_legal_actions(&db, 0, &mut receiver); let baseline_actions = stage_actions_in_range( &receiver, ACTION_BASE_STAGE as usize, (ACTION_BASE_STAGE + 300) as usize, ); println!("Baseline stage actions: {:?}", baseline_actions); assert!( !baseline_actions.is_empty(), "Baseline: should have stage activation" ); // Set prevent_activate to block state.players[0].prevent_activate = 1; receiver.reset(); state.generate_legal_actions(&db, 0, &mut receiver); let blocked_stage = stage_actions_in_range( &receiver, ACTION_BASE_STAGE as usize, (ACTION_BASE_STAGE + 300) as usize, ); let blocked_discard = stage_actions_in_range( &receiver, ACTION_BASE_DISCARD_ACTIVATE as usize, ACTION_BASE_ENERGY as usize, ); println!("Blocked stage: {:?}", blocked_stage); println!("Blocked discard: {:?}", blocked_discard); assert!( blocked_stage.is_empty(), "prevent_activate should block all stage activations" ); assert!( blocked_discard.is_empty(), "prevent_activate should block all discard activations" ); } // ===================== Test 2: Once-Per-Turn Slot Movement ===================== // If a card uses once-per-turn ability in slot 0, then is baton-touched to slot 1, // the once-per-turn tracking should still block re-activation. // The action generator keys by (source_type=0, card_id, ab_idx), so // moving to a different slot does NOT bypass the once-per-turn restriction. #[test] fn test_once_per_turn_slot_movement() { let db = load_db(); let target_cid = 4264; // Has Activated ability at index 1 let mut state = GameState::default(); state.phase = Phase::Main; state.current_player = 0; state.players[0] .energy_zone .extend(std::iter::repeat(100).take(10)); state.players[0].stage[0] = target_cid; state.players[0].set_tapped(0, false); // Step 1: Generate actions - ability should be available let mut receiver = new_receiver(); state.generate_legal_actions(&db, 0, &mut receiver); let action_id = (ACTION_BASE_STAGE + 0 * 100 + 1 * 10) as usize; let initial_actions = stage_actions_in_range(&receiver, action_id, action_id + 1); println!("Initial slot 0 actions: {:?}", initial_actions); assert!( !initial_actions.is_empty(), "Should have activation initially" ); // Step 2: Consume once_per_turn using the same keying the action generator uses: // source_type=0, id=card_id, ab_idx=1 state.consume_once_per_turn(0, 0, 0, target_cid as u32, 1); // Step 3: Verify blocked at slot 0 receiver.reset(); state.generate_legal_actions(&db, 0, &mut receiver); let after_consume = stage_actions_in_range(&receiver, action_id, action_id + 1); println!("After consume at slot 0: {:?}", after_consume); assert!( after_consume.is_empty(), "Should be blocked after once_per_turn consume" ); // Step 4: "Baton touch" - move card from slot 0 to slot 1 state.players[0].stage[1] = target_cid; state.players[0].stage[0] = -1; // Empty slot 0 state.players[0].set_tapped(1, false); // Step 5: Check if ability is STILL blocked at slot 1 // Because tracking is by card_id (not slot_idx), the restriction persists. receiver.reset(); state.generate_legal_actions(&db, 0, &mut receiver); // Slot 1, ab_idx 1 → ACTION_BASE_STAGE + 1*100 + 1*10 let action_id_slot1 = (ACTION_BASE_STAGE + 1 * 100 + 1 * 10) as usize; let slot1_actions = stage_actions_in_range(&receiver, action_id_slot1, action_id_slot1 + 1); println!("After baton touch to slot 1: {:?}", slot1_actions); assert!( slot1_actions.is_empty(), "Once-per-turn should persist across slot movement (card_id keyed)" ); } #[test] fn test_once_per_turn_duplicate_copies_remain_independent() { let db = load_db(); let target_cid = 4264; // Has Activated ability at index 1 let mut state = GameState::default(); state.phase = Phase::Main; state.current_player = 0; state.players[0] .energy_zone .extend(std::iter::repeat(100).take(10)); state.players[0].stage[0] = target_cid; state.players[0].stage[1] = target_cid; state.players[0].set_tapped(0, false); state.players[0].set_tapped(1, false); state.consume_once_per_turn(0, 0, 0, target_cid as u32, 1); let mut receiver = new_receiver(); state.generate_legal_actions(&db, 0, &mut receiver); let slot0_action_id = (ACTION_BASE_STAGE + 0 * 100 + 1 * 10) as usize; let slot1_action_id = (ACTION_BASE_STAGE + 1 * 100 + 1 * 10) as usize; let slot0_actions = stage_actions_in_range(&receiver, slot0_action_id, slot0_action_id + 1); let slot1_actions = stage_actions_in_range(&receiver, slot1_action_id, slot1_action_id + 1); assert!( slot0_actions.is_empty(), "The consumed copy in slot 0 should remain blocked" ); assert!( !slot1_actions.is_empty(), "A second copy with the same card ID should keep its own once-per-turn availability" ); } // ===================== Test 1: Stage Choice Ability Generation ===================== // Verifies whether activated abilities with choice_flags generate // ACTION_BASE_STAGE_CHOICE actions. #[test] fn test_stage_choice_ability_generation() { let db = load_db(); // Find a card with an Activated ability that has choice_flags > 0 // We'll scan the database programmatically. let mut found_choice_card: Option<(i32, usize)> = None; // (card_id, ab_idx) // Scan all member cards for an Activated ability with choice for cid in 0..8000i32 { if let Some(card) = db.get_member(cid) { for (ab_idx, ab) in card.abilities.iter().enumerate() { if ab.trigger == TriggerType::Activated && ab.choice_flags > 0 { found_choice_card = Some((cid, ab_idx)); println!( "Found choice card: ID={}, ab_idx={}, choice_flags={}, choice_count={}", cid, ab_idx, ab.choice_flags, ab.choice_count ); break; } } if found_choice_card.is_some() { break; } } } if let Some((cid, ab_idx)) = found_choice_card { let mut state = GameState::default(); state.phase = Phase::Main; state.current_player = 0; state.players[0] .energy_zone .extend(std::iter::repeat(100).take(10)); state.players[0].stage[0] = cid; state.players[0].set_tapped(0, false); state.debug.debug_ignore_conditions = true; // Bypass conditions to focus on choice generation let mut receiver = new_receiver(); state.generate_legal_actions(&db, 0, &mut receiver); // Check for ACTION_BASE_STAGE_CHOICE actions (4300-4599) let choice_actions = stage_actions_in_range(&receiver, 4300, 4600); // Check for ACTION_BASE_STAGE non-choice actions let non_choice_actions = stage_actions_in_range(&receiver, 4000, 4300); println!("Choice actions (4300-4599): {:?}", choice_actions); println!("Non-choice actions (4000-4299): {:?}", non_choice_actions); // KNOWN ISSUE: The generation loop does NOT check choice_flags. // It always emits ACTION_BASE_STAGE, never ACTION_BASE_STAGE_CHOICE. // This test documents the gap. if choice_actions.is_empty() && !non_choice_actions.is_empty() { println!("CONFIRMED GAP: Activated ability with choice_flags={} only generates non-choice action.", db.get_member(cid).unwrap().abilities[ab_idx].choice_flags); println!(" This may cause soft-lock if bytecode expects a choice index."); } } else { println!("No cards with choice-bearing Activated abilities found in database."); println!(" Stage choice generation test is VACUOUSLY TRUE (no data to test)."); } } #[test] fn test_cost_13_passive_repro() { let db = load_db(); // ID 410: PL!S-PR-029-PR (Passive: +2 blades if anyone has cost 13+) let target_cid = 410; // ID 2: PL!-sd1-001-SD (Cost 11) - Note: sd1-003 is cost 13 let cost_11_cid = 2; // Find a cost 13 card programmatically let mut cost_13_cid = -1; for cid in 0..1000 { if let Some(m) = db.get_member(cid) { if m.cost >= 13 { cost_13_cid = cid; break; } } } assert!(cost_13_cid != -1, "Could not find a cost 13 member in DB"); let mut state = GameState::default(); state.debug.debug_mode = true; state.phase = Phase::Main; state.current_player = 0; // Place the passive card state.players[0].stage[0] = target_cid; state.players[0].set_tapped(0, false); // Get the base blades from the card let base_blades = db .get_member(target_cid) .map(|m| m.blades as u32) .unwrap_or(3); // Verification 1: No other members on stage. let blades_solitary = state.get_effective_blades(0, 0, &db, 0); println!("Blades (solitary): {}", blades_solitary); // Verification 2: Add a cost 11 member. state.players[0].stage[1] = cost_11_cid; let blades_with_11 = state.get_effective_blades(0, 0, &db, 0); println!("Blades (with cost 11): {}", blades_with_11); // Verification 3: Add a cost 13 member. state.players[1].stage[0] = cost_13_cid; // On opponent stage let blades_with_13 = state.get_effective_blades(0, 0, &db, 0); println!("Blades (with cost 13): {}", blades_with_13); assert_eq!( blades_solitary, base_blades, "Card 410 should not gain bonus blades without a cost 13+ member on either stage" ); assert_eq!( blades_with_11, base_blades, "A cost 11 member should not satisfy card 410's passive" ); assert_eq!( blades_with_13, base_blades + 2, "A cost 13+ member on either stage should grant card 410 exactly +2 blades" ); } }