Spaces:
Sleeping
Sleeping
| //! 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. | |
| 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. | |
| 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. | |
| 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. | |
| 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. | |
| 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. | |
| 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. | |
| 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. | |
| 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. | |
| 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" | |
| ); | |
| } | |