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");
}