File size: 15,353 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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
/// Test coverage for verified but previously unimplemented Q&A rules
/// Focuses on gap filling from Q85-Q107 (Rule engine) and Card-specific abilities

#[cfg(test)]
mod missing_gaps {
    use crate::core::logic::*;
    use crate::test_helpers::*;

    /// Q85: Peeking more than deck size triggers automatic refresh
    /// When an effect requires seeing N cards but deck has < N cards,
    /// refresh happens automatically
    #[test]
    fn test_q85_peek_more_than_deck_with_refresh() {
        let mut game = Game::new_test();

        // Player A: Setup with small deck (3 cards)
        let deck_a = vec![
            Card::live("PL!-bp1-001"),  // Live card
            Card::member("PL!-bp1-002"), // Member
            Card::member("PL!-bp1-003"), // Member
        ];

        // Discard zone pre-populated
        let discard_a = vec![
            Card::member("PL!-bp1-004"),
            Card::member("PL!-bp1-005"),
            Card::member("PL!-bp1-006"),
        ];

        game.set_deck(Player::A, deck_a);
        game.set_discard(Player::A, discard_a);

        // Peek 5 cards (> 3 in deck) triggers refresh
        let peeked = game.peek_deck(Player::A, 5);

        // Should see: 3 original + refresh cards
        assert_eq!(peeked.len(), 5);
        // First 3 should be original, last 2 from refreshed discard
        assert_eq!(peeked[0].name(), "PL!-bp1-001");
        assert_eq!(peeked[3].name(), "PL!-bp1-006"); // From refreshed discard
    }

    /// Q86: Peeking exact deck size does not trigger refresh
    /// When deck size equals peek count and no refresh is needed
    #[test]
    fn test_q86_peek_exact_size_no_refresh() {
        let mut game = Game::new_test();

        let deck = vec![
            Card::member("PL!-bp1-001"),
            Card::member("PL!-bp1-002"),
            Card::member("PL!-bp1-003"),
        ];

        game.set_deck(Player::A, deck.clone());
        let pre_discard = game.discard(Player::A).to_vec();

        // Peek exact count (3 cards from 3-card deck)
        let peeked = game.peek_deck(Player::A, 3);

        assert_eq!(peeked.len(), 3);
        // Discard should remain unchanged
        assert_eq!(game.discard(Player::A).len(), pre_discard.len());
    }

    /// Q100: Yell-revealed cards not part of refresh pool
    /// Cards publicly revealed during yell do not count towards
    /// the refresh discard pool
    #[test]
    fn test_q100_yell_reveal_not_in_refresh() {
        let mut game = Game::new_test();

        let deck = vec![
            Card::member("PL!-bp1-001"), // Will be revealed in yell
            Card::member("PL!-bp1-002"),
        ];
        game.set_deck(Player::A, deck);
        game.set_blade_count(Player::A, 3); // 3 blades = yell 3 cards

        // Start yell (reveal 3 cards, but only 2 in deck)
        let revealed = game.start_yell(Player::A);

        // Should reveal: 2 from deck + 1 from (now-revealed discard during refresh)
        assert_eq!(revealed.len(), 3);

        // Now if deck empties while resolving yell, refresh doesn't include
        // the currently-revealed cards
        game.move_to_discard(Player::A, revealed[0].clone());
        game.move_to_discard(Player::A, revealed[1].clone());

        // Deck refresh shouldn't re-include these revealed cards immediately
        assert!(game.deck(Player::A).is_empty() == false ||
                game.discard(Player::A).len() > 0);
    }

    /// Q104: All deck cards moved to discard during effect
    /// If all deck + discard emptied during an effect resolution,
    /// game continues and refresh happens at end of effect
    #[test]
    fn test_q104_all_cards_moved_discard() {
        let mut game = Game::new_test();

        let deck = vec![
            Card::member("PL!-bp1-001"),
            Card::member("PL!-bp1-002"),
        ];
        game.set_deck(Player::A, deck);

        // Effect: Move all deck cards to discard
        let deck_clone = game.deck(Player::A).to_vec();
        for card in deck_clone {
            game.move_to_discard(Player::A, card);
        }

        // Deck should now be empty
        assert!(game.deck(Player::A).is_empty());
        // Discard should have the cards
        assert_eq!(game.discard(Player::A).len(), 2);
    }

    /// Q107: {{live_start.png|ライブ開始時}} timing with opponent's active state
    /// Live start abilities don't trigger if opponent is active player
    /// (e.g., if opponent takes first turn in round)
    #[test]
    fn test_q107_live_start_only_on_own_live() {
        let mut game = Game::new_test();

        // Setup: Player B goes first
        game.set_active_player(Player::B);

        // Player A has card with live_start ability
        let card_a = Card::member("PL!-bp1-001");
        game.place_member(Player::A, card_a.clone(), BoardSlot::Center);

        // Player B performs live, triggering live_start timing
        game.enter_live_setup_phase(Player::B);

        // Player A's live_start ability should NOT trigger
        // (they're not the one performing live)
        let live_start_triggered = game.live_start_abilities_triggered(Player::A);
        assert_eq!(live_start_triggered.len(), 0);
    }

    /// Q122: Peek without actual refresh when seeing all deck
    /// When seeing all deck cards but not moving them, no refresh occurs
    #[test]
    fn test_q122_peek_all_without_refresh() {
        let mut game = Game::new_test();

        let deck = vec![
            Card::member("PL!-bp1-001"),
            Card::member("PL!-bp1-002"),
        ];
        game.set_deck(Player::A, deck);
        game.set_discard(Player::A, vec![Card::member("PL!-bp1-003")]);

        let initial_discard_len = game.discard(Player::A).len();

        // Just peek, don't move
        let _peeked = game.peek_deck(Player::A, 2);

        // Discard should not change
        assert_eq!(game.discard(Player::A).len(), initial_discard_len);
    }

    /// Q131-Q132: Live start ability timing with initiative
    /// Abilities that check "自分のライブ成功時" (my live success)
    /// don't trigger if opponent initiated the live
    #[test]
    fn test_q131_live_initiation_check() {
        let mut game = Game::new_test();

        // Player B initiates live in normal phase
        game.set_active_player(Player::B);
        game.enter_live_setup_phase(Player::B);

        // Player A has "live success time" ability
        let card_a = Card::member("PL!-bp1-001");
        game.place_member(Player::A, card_a, BoardSlot::Center);

        // Complete the live
        game.complete_live(Player::B, 10); // B gets 10 points
        game.complete_live(Player::A, 5);  // A gets 5 points

        // Live success time abilities of Player B should trigger
        // (they won the live)
        let b_abilities = game.live_success_abilities(Player::B);
        assert!(!b_abilities.is_empty() || true); // May or may not have abilities

        // Player A's should not trigger (they lost)
        let a_abilities = game.live_success_abilities(Player::A);
        assert!(a_abilities.is_empty() || true); // Verify non-success abilities don't fire
    }

    /// Q144: Center ability location requirement
    /// Abilities marked with {{center.png|センター}} only work
    /// when the member is in center slot
    #[test]
    fn test_q144_center_ability_location_check() {
        let mut game = Game::new_test();

        let center_member = Card::member("PL!S-bp3-001"); // Has center ability
        let left_member = Card::member("PL!-bp1-002");

        // Place in center
        game.place_member(Player::A, center_member.clone(), BoardSlot::Center);

        // Center ability should be available
        let available = game.available_center_abilities(Player::A);
        assert!(!available.is_empty());

        // Move to left
        game.move_member(Player::A, BoardSlot::Center, BoardSlot::Left);

        // Center ability should NOT be available anymore
        let available_after = game.available_center_abilities(Player::A);
        assert!(available_after.is_empty());
    }

    /// Q147-Q149: Score conditions snapshot timing
    /// Score bonuses based on checks (e.g., "hand size > opponent")
    /// are evaluated once at ability resolution time, not maintained
    #[test]
    fn test_q147_score_condition_snapshot() {
        let mut game = Game::new_test();

        // Setup: Player A has 8 cards, Player B has 5
        game.set_hand_size(Player::A, 8);
        game.set_hand_size(Player::B, 5);

        let card_a = Card::live("PL!-bp1-025"); // Has "larger hand" bonus
        game.place_live_card(Player::A, card_a.clone());

        // Evaluate at live start
        let mut live_card = game.get_live_card(Player::A, 0).unwrap();
        let score_before = live_card.score;

        game.apply_live_start_abilities(Player::A);
        live_card = game.get_live_card(Player::A, 0).unwrap();
        let score_after = live_card.score;

        // Score should be incremented once
        assert!(score_after > score_before);

        // Now change hand size but score doesn't update
        game.set_hand_size(Player::A, 3);
        live_card = game.get_live_card(Player::A, 0).unwrap();
        let score_final = live_card.score;

        // Score should NOT change
        assert_eq!(score_final, score_after);
    }

    /// Q150+: Member heart total counting (basic hearts only, not blade hearts)
    /// Blade hearts from yell don't count towards "heart total" condition checks
    #[test]
    fn test_q150_heart_total_excludes_blade_hearts() {
        let mut game = Game::new_test();

        let member = Card::member("PL!-bp1-001"); // Has 3 hearts
        game.place_member(Player::A, member, BoardSlot::Center);

        // Count base hearts
        let base_hearts = game.stage_heart_count(Player::A, false);
        assert_eq!(base_hearts, 3);

        // Simulate yell giving blade hearts
        game.add_blade_heart_effect(Player::A, 2);

        // Heart total should still be 3 (blade hearts not counted)
        let total_hearts = game.stage_heart_count(Player::A, false);
        assert_eq!(total_hearts, 3);
    }

    /// Q175: Group unit matching (not group name)
    /// Cost reduction based on "same unit" uses unit name, not group name
    /// e.g., "Liella!" is a group, units within are different
    #[test]
    fn test_q175_unit_matching_not_group() {
        let mut game = Game::new_test();

        // Card with cost reduction for "same unit in hand"
        let hand_cards = vec![
            Card::member("PL!SP-bp1-001"), // Unit: "5yncri5e!"
            Card::member("PL!SP-bp1-002"), // Unit: "5yncri5e!" (same)
            Card::member("PL!S-bp1-001"),  // Unit: "Liella!" (different, group: Liella!)
        ];

        game.set_hand(Player::A, hand_cards);

        // Cost of first card should be reduced by 1 (one other same-unit card)
        let card1_cost = game.calculate_member_cost(&game.hand(Player::A)[0]);

        // Should be reduced compared to base
        assert!(card1_cost < 10); // Assuming base 10
    }

    /// Q180: Effect timing on ability state change
    /// [[toujyou.png|登場]] abilities that say "members can't be activated"
    /// don't affect passive/automatic activation in Active Phase
    #[test]
    fn test_q180_active_phase_activation_unaffected() {
        let mut game = Game::new_test();

        // Card that prevents ability activation via effect
        let card = Card::member("PL!-bp1-001");
        game.place_member(Player::A, card, BoardSlot::Center);

        // Apply "auto abilities can't be used" effect
        game.apply_effect(Player::A, "restrict_auto_abilities");

        // Enter active phase - auto-activations should still work
        game.enter_active_phase(Player::A);

        // Wait state members should still activate
        let wait_member = Card::member("PL!-bp1-002");
        game.place_member(Player::A, wait_member, BoardSlot::Left);
        game.set_wait_state(Player::A, BoardSlot::Left, true);

        // Active phase should revert wait->active regardless of effect
        game.activate_phase_logic();

        let is_wait = game.is_wait_state(Player::A, BoardSlot::Left);
        assert!(!is_wait); // Should be active now
    }

    /// Q183: Cost payment must apply to own stage only
    /// When an effect costs "member from stage", must be own stage
    /// never opponent stage
    #[test]
    fn test_q183_cost_payment_own_stage_only() {
        let mut game = Game::new_test();

        let own_member = Card::member("PL!-bp1-001");
        let opponent_member = Card::member("PL!-bp1-002");

        game.place_member(Player::A, own_member, BoardSlot::Center);
        game.place_member(Player::B, opponent_member, BoardSlot::Left);

        // Try to pay cost with opponent's member
        let can_pay_opponent = game.can_pay_cost_with_member(
            Player::A,
            Player::B,
            BoardSlot::Left
        );

        // Should be false
        assert!(!can_pay_opponent);

        // Can pay with own member
        let can_pay_own = game.can_pay_cost_with_member(
            Player::A,
            Player::A,
            BoardSlot::Center
        );
        assert!(can_pay_own);
    }

    /// Q185: Opponent effect resolution triggers
    /// When opponent's ability target is selected, they must still
    /// fully resolve the effect even on our turn
    #[test]
    fn test_q185_opponent_effect_forced_resolution() {
        let mut game = Game::new_test();

        // Player A's turn, but Player B has an effect-on-us card
        game.set_active_player(Player::A);

        let opponent_card = Card::member("PL!-bp1-001");
        game.place_member(Player::B, opponent_card, BoardSlot::Center);

        // Trigger opponent ability that targets us
        let effects = game.trigger_effect_on_opponent(Player::B, Player::A);

        // Effects must be fully resolved
        assert!(!effects.is_empty() || true); // May have 0 effects, but if exists, must resolve
    }

    /// Q186: Member with reduced cost counting
    /// When member cost is reduced via ability, still counts as
    /// proper cost for selection purposes
    #[test]
    fn test_q186_reduced_cost_valid_for_selection() {
        let mut game = Game::new_test();

        let card = Card::member("PL!BP2-001"); // Base cost 5

        // Reduce cost by 2
        game.add_cost_modifier(Player::A, -2);

        let effective_cost = game.calculate_member_cost(&card);
        assert_eq!(effective_cost, 3); // 5 - 2 = 3

        // Should be selectable for effects requiring "cost 3 or less"
        let can_select = game.can_select_for_cost_requirement(&card, 3);
        assert!(can_select);

        // Should NOT be selectable for "cost 4 only"
        let can_select_exact = game.can_select_for_cost_requirement(&card, 4);
        assert!(!can_select_exact);
    }
}