Spaces:
Sleeping
Sleeping
File size: 7,791 Bytes
463f868 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | 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");
}
|