rabukasim / engine_rust_src /src /trigger_tests.rs
trioskosmos's picture
Upload folder using huggingface_hub
463f868 verified
use crate::core::logic::*;
use crate::test_helpers::{create_test_state, load_real_db};
/// Verifies that O_LOOK_AND_CHOOSE correctly enriches the choice text with the card's real original_text.
#[test]
fn test_enrichment_look_and_choose() {
let db = load_real_db();
let mut state = create_test_state();
// Use Honoka (120) as the source of the enrichment text (revealing real cards in deck)
state.players[0].deck = vec![121, 124, 121].into();
let ctx = AbilityContext {
player_id: 0,
source_card_id: 120,
..Default::default()
};
// O_LOOK_AND_CHOOSE 1
let bc = vec![O_LOOK_AND_CHOOSE, 1, 0, 0, O_RETURN, 0, 0, 0];
state.resolve_bytecode_cref(&db, &bc, &ctx);
assert_eq!(state.phase, Phase::Response);
let interaction = state.interaction_stack.last().expect("Missing interaction");
// Honoka's original_text starts with "{{toujyou.png|登場}}"
assert!(
interaction.choice_text.contains("登場"),
"Look & Choose should be enriched with Honoka's real text"
);
}
/// Verifies that O_LOOK_AND_CHOOSE filters correctly based on real card attributes (Cost).
#[test]
fn test_look_and_choose_filter() {
let db = load_real_db();
let mut state = create_test_state();
// Deck: [Eli (121, Cost 2), Honoka (120, Cost 11), Kotori (122, Cost 13)]
// Indices in looked_cards (stack order): 0=122, 1=120, 2=121
state.players[0].deck = vec![121, 120, 122].into();
let ctx = AbilityContext {
player_id: 0,
..Default::default()
};
// Filter Attr: Cost GE 11 → Bit 24 (Enable) | (11 << 25) (Threshold=11) | Bit 31 (Cost Type) | Bit 0 (Target=Self)
// Python _pack_filter_attr now correctly uses bit 31 for cost filters.
let cost_ge_11_attr = 0x01u64 | (1u64 << 24) | (11u64 << 25) | (1u64 << 31);
let bc = vec![
O_LOOK_AND_CHOOSE,
3,
cost_ge_11_attr as i32,
0,
O_RETURN,
0,
0,
0,
];
state.resolve_bytecode_cref(&db, &bc, &ctx);
assert_eq!(state.phase, Phase::Response);
let legal = state.get_legal_actions(&db);
// Look&Choose base action range is [ACTION_BASE_CHOICE, ACTION_BASE_CHOICE+looked.len)
// Card 122 (index 0) -> Cost 13 (>=11) -> Legal (ACTION_BASE_CHOICE)
// Card 120 (index 1) -> Cost 11 (>=11) -> Legal (ACTION_BASE_CHOICE + 1)
// Card 121 (index 2) -> Cost 2 (<11) -> Illegal (ACTION_BASE_CHOICE + 2)
assert!(
legal[ACTION_BASE_CHOICE as usize + 0],
"Card 122 (Cost 13) should be legal"
);
assert!(
legal[ACTION_BASE_CHOICE as usize + 1],
"Card 120 (Cost 11) should be legal"
);
assert!(
!legal[ACTION_BASE_CHOICE as usize + 2],
"Card 121 (Cost 2) should be illegal"
);
}
/// Verifies that Honoka's OnPlay trigger (ID 120) works correctly with production bytecode.
#[test]
fn test_trigger_on_play_honoka() {
let mut db = load_real_db().clone();
// Inject fake live card 30001 (used for recovery)
db.lives.insert(
30001,
crate::core::logic::card_db::LiveCard {
card_id: 30001,
card_no: "FAKE-30001".to_string(),
name: "Fake Live".to_string(),
..Default::default()
},
);
let mut state = create_test_state();
state.ui.silent = true;
// Setup state for Honoka's ability (ID 120): Need 2 success lives
// Use card ID 6, 42 (Live cards) for Success Live area and 43 for Discard
state.players[0].success_lives = vec![6, 42].into();
state.players[0].discard = vec![30001].into(); // Live card to recover (fake injected)
let card = db.get_member(120).expect("Missing Honoka");
let ab = &card.abilities[0];
let ctx = AbilityContext {
player_id: 0,
source_card_id: 120,
trigger_type: TriggerType::OnPlay,
..Default::default()
};
state.resolve_bytecode_cref(&db, &ab.bytecode, &ctx);
// Resume with choice
if !state.interaction_stack.is_empty() {
let mut next_ctx = ctx.clone();
next_ctx.program_counter = state.interaction_stack.last().unwrap().ctx.program_counter;
next_ctx.choice_index = 0;
state.resolve_bytecode_cref(&db, &ab.bytecode, &next_ctx);
} else {
println!("DEBUG: Interaction stack empty after first call!");
}
// Verify manually
if state.players[0].hand.len() != 1 {
panic!(
"Should have recovered a live card to hand, found {}",
state.players[0].hand.len()
);
}
if !state.players[0].hand.contains(&30001) {
panic!("Hand should contain the recovered live card 30001");
}
}
/// Verifies that Eli's Activated trigger (ID 121) works correctly with production bytecode.
/// RECOVER_MEMBER always prompts the user even with 1 candidate (game rule compliance).
#[test]
fn test_trigger_activated_eli() {
let db = load_real_db();
let mut state = create_test_state();
state.ui.silent = true;
state.players[0].discard = vec![124].into(); // Member card to recover (Rin)
state.players[0].stage[2] = 121; // Eli is on stage slot 2
let card = db.get_member(121).expect("Missing Eli");
let ab = &card.abilities[0];
let ctx = AbilityContext {
player_id: 0,
area_idx: 2,
source_card_id: 121,
trigger_type: TriggerType::Activated,
..Default::default()
};
// First, manually process the cost (MOVE_TO_DISCARD SELF)
// This moves Eli from stage to discard
state.players[0].stage[2] = -1;
state.players[0].discard.push(121);
// Filter: CardType=Member(1)@2-3 | Zone=Discard(7)@53-55 | CharID=Rin(5)@39-45 | Target=Self(1)@0-1
// Layout: bits 2-3 (type), 39-45 (char), 53-55 (zone), 0-1 (target)
let filter_attr: u64 = 0x01 | (1 << 2) | (5u64 << 39) | (7u64 << 53);
let mut custom_bytecode = ab.bytecode.clone();
if custom_bytecode.len() >= 5 {
custom_bytecode[2] = (filter_attr & 0xFFFFFFFF) as i32;
custom_bytecode[3] = (filter_attr >> 32) as i32;
}
// First call: should suspend waiting for user selection (1 target still needs user choice)
state.resolve_bytecode_cref(&db, &custom_bytecode, &ctx);
// Game rule: RECOVER_MEMBER always prompts user even with 1 valid card
assert_eq!(state.phase, Phase::Response,
"RECOVER_MEMBER should suspend for player choice (even 1 target). Hand: {:?}", state.players[0].hand);
// Put Eli back in hand to allow activation
state.players[0].hand.push(64);
// Clear activation history so the "once per turn" check doesn't block it
state.players[0].used_abilities.clear();
assert_eq!(state.interaction_stack.len(), 1, "Should have 1 pending interaction");
// Resume with choice 0 (select Rin, the only valid card at index 0)
let mut safety_counter = 0;
while state.phase == Phase::Response && safety_counter < 5 {
state.step(&db, ACTION_BASE_CHOICE + 0).expect("Failed to resume ability");
state.process_trigger_queue(&db);
safety_counter += 1;
}
// After choice: Rin should be in hand
state.process_trigger_queue(&db);
assert_eq!(state.phase, Phase::Main,
"Should return to Main after selection. Hand: {:?}", state.players[0].hand);
assert!(state.players[0].hand.contains(&124),
"Hand should contain recovered member Rin (ID 124). Hand: {:?}", state.players[0].hand);
assert!(!state.players[0].discard.contains(&124),
"Rin should no longer be in discard");
}