//! Specialized Opcode Tests for Complex Instructions //! //! This module tests advanced bytecode opcodes and their interactions: //! - O_REVEAL_UNTIL: Reveal cards until condition met //! - O_DRAW_UNTIL: Draw cards until hand size reached //! - O_LOOK_AND_CHOOSE: Peek at cards and select subset //! - O_LOOK_DECK: Examine top N cards //! - Complex filter expressions and state transitions //! //! # Test Organization //! //! Tests are organized by opcode family: //! //! - **Reveal/Draw Operations**: O_REVEAL_UNTIL, O_DRAW_UNTIL, O_DRAW, O_LOOK_DECK //! - **Selection Mechanics**: O_LOOK_AND_CHOOSE, selection filters //! - **State Modifiers**: Tap, untap, state tracking //! - **Edge Cases**: Boundary conditions, special scenarios //! //! # Complexity Levels //! //! Each test is tagged with complexity: //! - **Basic**: Single opcode in isolation //! - **Medium**: Opcode with multiple filter conditions //! - **Advanced**: Multiple opcodes or complex filtering //! - **Edge Case**: Boundary conditions or rare scenarios //! //! # Running Opcode Tests //! //! ```bash //! # All opcode tests //! cargo test --lib opcode //! //! # Specific opcode family //! cargo test --lib test_opcode_reveal_until //! cargo test --lib test_opcode_draw_until //! cargo test --lib test_opcode_look_and_choose //! //! # With output //! cargo test --lib test_opcode_reveal_until -- --nocapture //! ``` //! //! # Key Test Areas //! //! | Opcode | Test Count | Priority | //! |--------|-----------|----------| //! | O_REVEAL_UNTIL | 4+ | Critical | //! | O_DRAW_UNTIL | 3+ | High | //! | O_LOOK_AND_CHOOSE | 3+ | High | //! | O_LOOK_DECK | 2+ | Medium | //! //! # Known Issues & Notes //! //! - REVEAL_UNTIL refresh semantics require careful state management //! - Filter expressions need comprehensive condition testing //! - Edge cases: empty deck, maximum hand size, rapid state changes use crate::core::logic::card_db::LOGIC_ID_MASK; use crate::core::logic::*; // use crate::core::enums::*; use crate::test_helpers::{add_card, create_test_db, create_test_state, BytecodeBuilder}; /// Verifies that O_DRAW_UNTIL draws the correct number of cards to reach a target hand size. #[test] fn test_opcode_draw_until() { let db = create_test_db(); let mut state = create_test_state(); state.players[0].deck = vec![1, 2, 3, 4, 5].into(); state.players[0].hand = vec![101, 102].into(); // Hand size 2 let ctx = AbilityContext { player_id: 0, ..Default::default() }; // O_DRAW_UNTIL 5 (Draw up to 5) let bc = BytecodeBuilder::new(O_DRAW_UNTIL).v(5).op(O_RETURN).build(); state.resolve_bytecode_cref(&db, &bc, &ctx); assert_eq!(state.players[0].hand.len(), 5); assert_eq!(state.players[0].deck.len(), 2); } /// Verifies that O_REVEAL_UNTIL with TYPE_CHECK correctly filters for Live cards. #[test] fn test_opcode_reveal_until_type_live() { let mut db = create_test_db(); let mut state = create_test_state(); // Deck: 10 (member), 15 (member), 10050 (live), 1 (fallback) add_card(&mut db, 10, "M10", vec![], vec![]); add_card(&mut db, 15, "M15", vec![], vec![]); add_card(&mut db, 1, "M1", vec![], vec![]); db.lives.insert( 10050, LiveCard { card_id: 10050, name: "L10050".to_string(), ..Default::default() }, ); db.lives_vec[50] = Some(db.lives[&10050].clone()); state.players[0].deck = vec![1, 10050, 15, 10].into(); let ctx = AbilityContext { player_id: 0, ..Default::default() }; // O_REVEAL_UNTIL C_TYPE_CHECK attr: 1 (Live), target: 6 (Hand) let bc = BytecodeBuilder::new(O_REVEAL_UNTIL) .v(C_TYPE_CHECK) .a(1) // Val=1 (Live) .target(6) .reveal_until_live(true) .op(O_RETURN) .build(); state.resolve_bytecode_cref(&db, &bc, &ctx); // Should have popped 10, 15, then 10050. // 10050 matches Live. It goes to hand. // 10 and 15 go to discard. assert!(state.players[0].hand.contains(&10050)); assert_eq!(state.players[0].discard.len(), 2); // 10 and 15 assert!(state.players[0].discard.contains(&10)); assert!(state.players[0].discard.contains(&15)); assert_eq!(state.players[0].deck.len(), 1); // 1 remains } /// Verifies that O_REVEAL_UNTIL with COST_GE correctly filters for members with a minimum cost. #[test] fn test_opcode_reveal_until_cost_ge() { let mut db = create_test_db(); let mut state = create_test_state(); let m10 = MemberCard { card_id: 60010, cost: 5, ..Default::default() }; let m15 = MemberCard { card_id: 60015, cost: 15, ..Default::default() }; db.members.insert(60010, m10.clone()); db.members.insert(60015, m15.clone()); if db.members_vec.len() <= 60015 { db.members_vec.resize(60020, None); } db.members_vec[60010] = Some(m10); db.members_vec[60015] = Some(m15); state.players[0].deck = vec![60015, 60010].into(); let ctx = AbilityContext { player_id: 0, ..Default::default() }; // O_REVEAL_UNTIL C_COST_CHECK val=10 (raw threshold) s=54 (Hand=6 | Mode=3/GE) let bc = BytecodeBuilder::new(O_REVEAL_UNTIL) .v(C_COST_CHECK) .a(10) .target(6) .comparison_mode(3) .op(O_RETURN) .build(); state.resolve_bytecode_cref(&db, &bc, &ctx); // Should pop 60010 (5 < 10), then 60015 (15 >= 10). assert!(state.players[0].hand.contains(&60015)); assert!(state.players[0].discard.contains(&60010)); } /// Verifies that O_IMMUNITY correctly toggles the FLAG_IMMUNITY on the player. #[test] fn test_opcode_immunity() { let db = create_test_db(); let mut state = create_test_state(); assert!(!state.players[0].get_flag(PlayerState::FLAG_IMMUNITY)); let ctx = AbilityContext { player_id: 0, ..Default::default() }; // O_IMMUNITY 1 let bc = BytecodeBuilder::new(O_IMMUNITY).v(1).op(O_RETURN).build(); state.resolve_bytecode_cref(&db, &bc, &ctx); assert!(state.players[0].get_flag(PlayerState::FLAG_IMMUNITY)); // O_IMMUNITY 0 let bc = BytecodeBuilder::new(O_IMMUNITY).v(0).op(O_RETURN).build(); state.resolve_bytecode_cref(&db, &bc, &ctx); assert!(!state.players[0].get_flag(PlayerState::FLAG_IMMUNITY)); } /// Verifies that O_PAY_ENERGY correctly taps the specified number of energy cards. #[test] fn test_opcode_pay_energy() { let db = create_test_db(); let mut state = create_test_state(); state.ui.silent = true; state.players[0].tapped_energy_mask = 0; let ctx = AbilityContext { player_id: 0, ..Default::default() }; // O_PAY_ENERGY 2 let bc = BytecodeBuilder::new(O_PAY_ENERGY).v(2).op(O_RETURN).build(); state.resolve_bytecode_cref(&db, &bc, &ctx); assert_eq!(state.players[0].tapped_energy_mask.count_ones(), 2); } /// Verifies that O_LOOK_DECK moves cards from deck to the looked_cards buffer. #[test] fn test_opcode_look_deck() { let db = create_test_db(); let mut state = create_test_state(); state.players[0].deck = vec![1, 2, 3, 4, 5].into(); let ctx = AbilityContext { player_id: 0, ..Default::default() }; // O_LOOK_DECK 3 let bc = BytecodeBuilder::new(O_LOOK_DECK).v(3).op(O_RETURN).build(); state.resolve_bytecode_cref(&db, &bc, &ctx); assert_eq!(state.players[0].looked_cards.len(), 3); assert_eq!(state.players[0].deck.len(), 2); assert_eq!(state.players[0].looked_cards.as_slice(), &[5, 4, 3]); } /// Verifies that O_LOOK_AND_CHOOSE correctly filters looked cards and transitions to Response phase. #[test] fn test_opcode_look_and_choose_filter_cost_ge() { let mut db = create_test_db(); let mut state = create_test_state(); // Deck: 10 (cost 5), 15 (cost 15) let m10 = MemberCard { card_id: 10, cost: 5, ..Default::default() }; let m15 = MemberCard { card_id: 15, cost: 15, ..Default::default() }; db.members.insert(10, m10.clone()); db.members_vec[10] = Some(m10); db.members.insert(15, m15.clone()); db.members_vec[15] = Some(m15); state.players[0].deck = vec![15, 10].into(); let ctx = AbilityContext { player_id: 0, area_idx: -1, choice_index: -1, ..Default::default() }; // O_LOOK_AND_CHOOSE 2 (Look 2) // attr: Bit 24 (Enable) | (10 << 25) (Min Cost 10) | Bit 31 (Cost Type) = 0x95000000 // Python _pack_filter_attr always sets bit 31 for cost filters let bc = vec![ O_LOOK_AND_CHOOSE, 2, 0x95000000u32 as i32, 0, 0, O_RETURN, 0, 0, 0, 0, ]; state.resolve_bytecode_cref(&db, &bc, &ctx); // Should be in Response phase, with looked_cards: [10, 15] assert_eq!(state.phase, Phase::Response); assert_eq!(state.players[0].looked_cards.len(), 2); assert_eq!(state.players[0].looked_cards[0], 10); assert_eq!(state.players[0].looked_cards[1], 15); // Check legal actions. base for slot -1 (default), ab -1 (default) // base = 550 + (-1 * 100) + (0 * 10) = 450 let actions = state.get_legal_action_ids(&db); println!("DEBUG: Actions: {:?}", actions); // card 15 is at index 1, aid = ACTION_BASE_CHOICE + 1 // card 10 is at index 0, aid = ACTION_BASE_CHOICE + 0 assert!( actions.contains(&(ACTION_BASE_CHOICE + 1)), "Card 15 (cost 15) should be legal for COST_GE=10" ); assert!( !actions.contains(&(ACTION_BASE_CHOICE + 0)), "Card 10 (cost 5) should NOT be legal for COST_GE=10" ); } /// Verifies the card matching logic used by various opcodes for Live cards based on heart requirements. /// NOTE: Live cards only match cost filters when card_type is explicitly set to Live (2). /// This prevents generic "Cost >= X" filters (meant for members) from matching high-heart live cards. #[test] fn test_card_matches_filter_live_hearts() { let mut db = create_test_db(); let state = create_test_state(); // Add a Live card with 8 hearts let cid_live = 10080; db.lives.insert( cid_live, LiveCard { card_id: cid_live, required_hearts: [1, 1, 1, 1, 1, 1, 2], // Sum = 8 ..Default::default() }, ); let logic_id = (cid_live & LOGIC_ID_MASK) as usize; db.lives_vec[logic_id] = Some(db.lives[&cid_live].clone()); // Filter: COST_GE = 8, Type = Live (2) // Bit 24: Enable (0x01000000) // Bits 25-30: Value 8 (8 << 25 = 0x10000000) // Bits 2-3: Type = Live (2 << 2 = 0x08) // Bit 30: is_le = 0 let filter_attr = 0x01000000 | (8 << 25) | (2 << 2); assert!( state.card_matches_filter(&db, cid_live, filter_attr), "Live with 8 hearts should match GE 8 with Live type filter" ); // Filter: COST_GE = 9, Type = Live (2) let filter_attr_fail = 0x01000000 | (9 << 25) | (2 << 2); assert!( !state.card_matches_filter(&db, cid_live, filter_attr_fail), "Live with 8 hearts should NOT match GE 9" ); // Filter: COST_LE = 8, Type = Live (2) // Bit 30: is_le = 1 (0x40000000) let filter_attr_le = 0x01000000 | (8 << 25) | 0x40000000 | (2 << 2); assert!( state.card_matches_filter(&db, cid_live, filter_attr_le), "Live with 8 hearts should match LE 8 with Live type filter" ); // Verify that generic cost filter (without type=Live) does NOT match Live cards // Note: bit 31 set = Cost mode. For members, this checks m.cost. Live cards have no cost field, // so they get actual_val=0 which fails GE 8. let filter_generic = 0x01000000 | (8 << 25) | (1u64 << 31); // Cost mode, no type filter assert!( !state.card_matches_filter(&db, cid_live, filter_generic), "Live should NOT match generic cost filter without Live type constraint" ); } /// Verifies that O_LOOK_AND_CHOOSE correctly uses Deck source even if Destination matches Hand (Arg 3 = 6). /// Also checks that Action 0 (Skip) is suppressed for mandatory choice. #[test] fn test_look_and_choose_source_zone_fix() { let db = create_test_db(); let mut state = create_test_state(); // Setup: Player has 5 cards in hand, 10 in deck state.players[0].hand = vec![1, 2, 3, 4, 5].into(); state.players[0].deck = (10..20).collect(); // Execute O_LOOK_AND_CHOOSE: Look 2, Filter 0, Destination Hand (6) // Bytecode: [Opcode, Value, Attr, Slot] // [41, 2, 0, 6] // Expected behavior: Source Zone defaults to Deck (8) despite Dest=6. let ctx = AbilityContext { player_id: 0, source_card_id: 99, ..Default::default() }; let bc = BytecodeBuilder::new(O_LOOK_AND_CHOOSE) .v(2) .optional(true) .target(6) .op(O_RETURN) .build(); state.resolve_bytecode_cref(&db, &bc, &ctx); // Verify 1: Source Zone Logic // If bug existed: source=6 -> reveal_count=hand.len()=5 -> looked_cards.len()=5 (from hand) // Fixed: source=8 -> reveal_count=v=2 -> looked_cards.len()=2 (from deck) assert_eq!( state.players[0].looked_cards.len(), 2, "Should look at 2 cards from deck, not all cards from hand" ); // Verify cards are from deck (10..20) not hand (1..5) // Deck pops from end, so should be 19, 18 let c1 = state.players[0].looked_cards[0]; let c2 = state.players[0].looked_cards[1]; assert!(c1 >= 10, "Looked card 1 should be from deck (ID >= 10)"); assert!(c2 >= 10, "Looked card 2 should be from deck (ID >= 10)"); // Verify 2: Action 0 is now ALLOWED (Skip Ability Feature) // Generate legal actions let actions = state.get_legal_action_ids(&db); // ACTION_BASE_CHOICE+0 and ACTION_BASE_CHOICE+1 should be present (for the 2 looked cards) assert!( actions.contains(&(ACTION_BASE_CHOICE + 0)), "Should allow choosing first looked card" ); assert!( actions.contains(&(ACTION_BASE_CHOICE + 1)), "Should allow choosing second looked card" ); // Action 0 is no longer suppressed, allowing users to skip abilities assert!( actions.contains(&0), "Action 0 (Skip) should be ALLOWED for O_LOOK_AND_CHOOSE" ); }