Spaces:
Sleeping
Sleeping
| /// Card-specific ability tests for Q&A validation | |
| /// Coverage: Q122-Q186 Card-specific mechanics and edge cases | |
| mod card_specific_qa_gaps { | |
| use crate::core::logic::*; | |
| use crate::test_helpers::*; | |
| /// Q122: Peek ability without actual refresh (just viewing) | |
| /// 『登場 手札を1枚控え室に置いてもよい:自分のデッキの上からカードを3枚見る。 | |
| /// その中から好きな枚数を好きな順番でデッキの上に置き、残りを控え室に置く。』 | |
| /// If viewing all cards, no refresh until cards are actually moved | |
| fn test_q122_view_all_deck_no_refresh_until_move() { | |
| let mut game = Game::new_test(); | |
| let deck = vec![ | |
| Card::member("PL!N-bp3-002"), | |
| Card::member("PL!N-bp3-003"), | |
| ]; | |
| game.set_deck(Player::A, deck); | |
| game.set_discard(Player::A, vec![Card::member("PL!N-bp3-004")]); | |
| // Viewing phase - deck still has cards | |
| let viewed = game.peek_deck(Player::A, 2); | |
| assert_eq!(viewed.len(), 2); | |
| // Discard not activated yet | |
| let discard_before = game.discard(Player::A).len(); | |
| // Now player arranges and places back | |
| game.move_viewed_to_deck_top(Player::A, &viewed[0..1]); | |
| game.move_viewed_to_discard(Player::A, &viewed[1..2]); | |
| // After moving, discard might have changed | |
| let discard_after = game.discard(Player::A).len(); | |
| assert!(discard_after >= discard_before); | |
| } | |
| /// Q131-Q132 variant: Live start ability with opponent as attacker | |
| /// 『ライブ開始時 self is opponent attacker...』 | |
| /// Abilities that check "my live start" don't trigger if opponent initiated | |
| fn test_q131_opponent_initiated_live() { | |
| let mut test = GameTest::new(); | |
| // Opponent (Player B) starts a live | |
| test.set_active(Player::B); | |
| // Player A has live start card on stage | |
| let card_a = Card::member("PL!-bp1-001"); | |
| test.place(Player::A, card_a, Slot::Center); | |
| // B enters performance (their live) | |
| test.enter_performance(Player::B); | |
| // A's live_start trigger condition (if any) should NOT fire | |
| // since it's B's live, not A's | |
| let triggered = test.get_triggered_abilities(); | |
| for ability in &triggered { | |
| assert_ne!(ability.owner, Player::A); // No A abilities should trigger | |
| } | |
| } | |
| /// Q132 variant: Live success time with opponent as winner | |
| /// Similar to Q131 but for {{live_success.png|ライブ成功時}} | |
| fn test_q132_opponent_won_live() { | |
| let mut test = GameTest::new(); | |
| test.set_active(Player::B); | |
| let card_a = Card::live("PL!-bp3-001"); // Has live_success ability | |
| test.place_live(Player::A, card_a); | |
| let card_b = Card::live("PL!-bp1-001"); | |
| test.place_live(Player::B, card_b); | |
| // Complete performance - B wins | |
| test.set_live_score(Player::A, 5); | |
| test.set_live_score(Player::B, 15); | |
| test.resolve_live(); | |
| // A's live_success abilities should NOT trigger (A didn't win) | |
| let triggered = test.get_triggered_abilities_for(Player::A); | |
| for ab in &triggered { | |
| assert_ne!(ab.timing, AbilityTiming::LiveSuccess); | |
| } | |
| } | |
| /// Q144: Center ability location check | |
| /// [[kidou.png|起動]] [[center.png|センター]] ターン1回 | |
| /// メンバー1人をウェイトにする:ライブ終了時まで、... | |
| /// Only works when member is in center slot | |
| fn test_q144_center_activation_only_in_center() { | |
| let mut test = GameTest::new(); | |
| let center_card = Card::member("PL!S-bp3-001"); | |
| test.place(Player::A, center_card.clone(), Slot::Center); | |
| // Activation should work in center | |
| let can_activate_center = test.can_activate_center_ability(Player::A, Slot::Center); | |
| assert!(can_activate_center); | |
| // Move to left | |
| test.move_member(Player::A, Slot::Center, Slot::Left); | |
| // Should NOT be activatable anymore | |
| let can_activate_left = test.can_activate_center_ability(Player::A, Slot::Left); | |
| assert!(!can_activate_left); | |
| // Verify it's in left now | |
| assert_eq!(test.member_at(Player::A, Slot::Left).unwrap().id, center_card.id); | |
| } | |
| /// Q147-Q149: "Until live end" effect persistence | |
| /// {{jyouji.png|常時}} score bonuses from {{live_start.png|ライブ開始時}} | |
| /// persist even if live doesn't happen | |
| fn test_q147_until_live_end_persists_no_live() { | |
| let mut test = GameTest::new(); | |
| let member = Card::member("PL!-bp1-001"); | |
| test.place(Player::A, member, Slot::Center); | |
| // Trigger "until live end" bonus | |
| test.apply_bonus_until_live_end(Player::A, "score", 2); | |
| // Even if no live happens this turn... | |
| test.skip_live(); | |
| test.enter_end_phase(); | |
| // Bonus should persist if it's until "live end" and we skipped live | |
| // (Live end phase resets it even without actual live) | |
| let has_bonus = test.get_bonus(Player::A, "score") > 0; | |
| assert!(!has_bonus); // Should be gone after phase ends | |
| } | |
| /// Q150: Surplus heart definition for conditions | |
| /// {{heart_00.png|heart0}} in surplus means hearts > required | |
| /// Used in damage calculations but not "heart total" counts | |
| fn test_q150_surplus_heart_calculation() { | |
| let mut test = GameTest::new(); | |
| let card = Card::live("PL!-bp1-001"); | |
| test.place_live(Player::A, card); | |
| // Set required hearts | |
| test.set_live_requirement(Player::A, vec![ | |
| Heart::Red, Heart::Red, Heart::Blue | |
| ]); | |
| // Set actual hearts | |
| test.set_live_hearts(Player::A, vec![ | |
| Heart::Red, Heart::Red, Heart::Blue, | |
| Heart::Green // +1 surplus | |
| ]); | |
| let surplus = test.calculate_surplus_hearts(Player::A); | |
| assert_eq!(surplus, 1); | |
| // Add blade hearts | |
| test.add_blade_hearts(Player::A, 2); | |
| // Surplus should increase | |
| let surplus_with_blade = test.calculate_surplus_hearts(Player::A); | |
| assert_eq!(surplus_with_blade, 3); | |
| } | |
| /// Q151-Q160: Advanced member state transitions | |
| /// Members that move areas within same turn have reset turn-once | |
| /// Abilities that check "on appearance" vs "on area move" | |
| fn test_q151_member_area_move_state_reset() { | |
| let mut test = GameTest::new(); | |
| let member = Card::member("PL!-bp1-001"); | |
| test.place(Player::A, member.clone(), Slot::Left); | |
| // Mark as "appeared this turn" | |
| test.set_appeared_this_turn(Player::A, Slot::Left, true); | |
| // Move within stage (Left -> Center) | |
| test.move_member(Player::A, Slot::Left, Slot::Center); | |
| // Turn-once abilities should NOT have fired | |
| // (member moved, not placed new) | |
| let turn_once_ready = test.is_turn_once_ready(Player::A, Slot::Center); | |
| assert!(!turn_once_ready); | |
| } | |
| /// Q168-Q170: Multi-user on-play effects | |
| /// 『登場 自分と相手はそれぞれ、自身の控え室から | |
| /// コスト2以下のメンバーカードを1枚、メンバーのいないエリアに | |
| /// ウェイト状態で登場させる。』 | |
| /// Both players place, area stays locked from further placement | |
| fn test_q168_mutual_placement_area_lock() { | |
| let mut test = GameTest::new(); | |
| // Card that triggers mutual placement | |
| let card = Card::member("LL-bp3-001"); // Nico card | |
| test.place(Player::A, card, Slot::Center); | |
| // Triggering appearance effect | |
| test.trigger_on_play_effect(Player::A, Slot::Center); | |
| // Both players place cost 2 or less members | |
| let cheap_card_a = Card::member("PL!-bp1-002"); | |
| let cheap_card_b = Card::member("PL!-bp1-003"); | |
| test.place_from_discard(Player::A, cheap_card_a, Slot::Left); | |
| test.place_from_discard(Player::B, cheap_card_b, Slot::Right); | |
| // Both placed in wait state | |
| assert!(test.is_wait(Player::A, Slot::Left)); | |
| assert!(test.is_wait(Player::B, Slot::Right)); | |
| // Left/Right areas should be locked from further placement | |
| let can_place = test.can_place_at(Player::A, Slot::Left); | |
| assert!(!can_place); | |
| } | |
| /// Q174: Group name vs unit name resolution | |
| /// 『ライブ成功時 自分のライブ中の『Aqours』のカードが2枚以上ある場合...』 | |
| /// Uses group name (Aqours) not unit name | |
| /// But for "unique group members" checks, it's per-individual member name | |
| fn test_q174_group_name_vs_unit_name() { | |
| let mut test = GameTest::new(); | |
| let card1 = Card::member("PL!S-bp2-001"); // Aqours, name: Character1 | |
| let card2 = Card::member("PL!S-bp2-002"); // Aqours, name: Character2 | |
| test.place_live(Player::A, card1); | |
| test.place_live(Player::A, card2); | |
| // Check group: should be 2 Aqours | |
| let aqours_count = test.count_group_in_live(Player::A, "Aqours"); | |
| assert_eq!(aqours_count, 2); | |
| // For abilities requiring "unique names in group", different | |
| let unique_names = test.unique_member_names_in_live(Player::A, "Aqours"); | |
| assert_eq!(unique_names.len(), 2); | |
| } | |
| /// Q175: Unit name cost reduction (distinct from group) | |
| /// 『ライブ開始時 手札の同じユニット名を持つカード2枚を控え室に置いてもよい...』 | |
| /// ユニット名 = specific unit, not group name | |
| fn test_q175_unit_name_cost_matching() { | |
| let mut test = GameTest::new(); | |
| // Cards from same unit but possibly different groups | |
| // e.g., 5yncri5e! Setsuna cards | |
| let card1 = Card::member("PL!SP-bp1-001"); // Unit: 5yncri5e! | |
| let card2 = Card::member("PL!SP-bp1-002"); // Unit: 5yncri5e! (same) | |
| let card3 = Card::member("PL!SP-bp2-001"); // Unit: 5yncri5e! (same) | |
| let card4 = Card::member("PL!S-bp1-001"); // Unit: Different | |
| test.set_hand(Player::A, vec![ | |
| card1.clone(), card2.clone(), card3, card4 | |
| ]); | |
| // Ability checks "same unit name" | |
| let cards_of_unit = test.find_cards_by_unit(Player::A, "5yncri5e!"); | |
| assert_eq!(cards_of_unit.len(), 3); | |
| // Can satisfy ability with 2 from same unit | |
| let can_satisfy = test.can_satisfy_unit_cost(Player::A, 2, "5yncri5e!"); | |
| assert!(can_satisfy); | |
| } | |
| /// Q176-Q177: Opponent targeted effects | |
| ///『起動 このメンバーをウェイトにしてもよい...』 | |
| /// Cannot wait opponent's members, only own | |
| fn test_q176_opponent_effect_boundary() { | |
| let mut test = GameTest::new(); | |
| let my_member = Card::member("PL!-bp1-001"); | |
| let opp_member = Card::member("PL!-bp1-002"); | |
| test.place(Player::A, my_member, Slot::Center); | |
| test.place(Player::B, opp_member, Slot::Left); | |
| // Try to activate effect that targets opponent's member | |
| let can_target = test.can_target_for_effect( | |
| Player::A, | |
| EffectType::MakeWait, | |
| Player::B, | |
| Slot::Left | |
| ); | |
| // Should fail - effects target own board | |
| assert!(!can_target); | |
| } | |
| /// Q177: Mandatory vs optional ability execution | |
| /// 『自動 ターン1回 自分のカードの効果によって...』 | |
| /// Mandatory auto abilities MUST execute if conditions met | |
| /// Cannot choose to skip | |
| fn test_q177_mandatory_auto_ability() { | |
| let mut test = GameTest::new(); | |
| let member = Card::member("PL!-pb1-015"); | |
| test.place(Player::A, member, Slot::Center); | |
| // Trigger condition: opponent's member becomes wait | |
| test.force_opponent_wait(Player::B, Slot::Left); | |
| // Mandatory ability should trigger automatically | |
| let triggered = test.get_auto_triggered_this_phase(Player::A); | |
| assert!(!triggered.is_empty()); | |
| // Player cannot skip execution | |
| let can_skip = test.can_skip_auto_ability(Player::A, triggered[0].id); | |
| assert!(!can_skip); | |
| } | |
| /// Q180: Area movement vs "cannot activate" | |
| /// [[toujyou.png|登場]] Effect saying members "cannot be activated" | |
| /// doesn't prevent area movement state changes (wait->active in active phase) | |
| fn test_q180_area_activation_override() { | |
| let mut test = GameTest::new(); | |
| let member = Card::member("PL!-bp3-004"); | |
| test.place(Player::A, member, Slot::Center); | |
| // Place restriction: "members cannot be activated" | |
| test.apply_restriction("cannot_activate_members"); | |
| // Force member to wait | |
| test.set_wait(Player::A, Slot::Center, true); | |
| // Enter active phase | |
| test.enter_active_phase(Player::A); | |
| // Despite "cannot activate" restriction, active phase logic | |
| // should still trigger (state restoration, not activation) | |
| let is_wait = test.is_wait(Player::A, Slot::Center); | |
| assert!(!is_wait); // Should be active now | |
| } | |
| /// Q178-Q179: Printemps center member effect | |
| /// 『ライブ開始時 自分のステージにいるプリンテンプス...』 | |
| /// Checks for group, activates center slot, field counts correctly | |
| fn test_q178_group_member_activation() { | |
| let mut test = GameTest::new(); | |
| // Place Printemps members | |
| let card1 = Card::member("PL!-pb1-028"); // Printemps, Center | |
| let card2 = Card::member("PL!-bp1-050"); // Printemps, Left | |
| test.place(Player::A, card1, Slot::Center); | |
| test.place(Player::A, card2, Slot::Left); | |
| // Trigger effect that activates all "Printemps members" | |
| test.trigger_group_effect(Player::A, "Printemps", "activate_wait"); | |
| // Both should be activatable | |
| let acts_center = test.is_wait(Player::A, Slot::Center); | |
| let acts_left = test.is_wait(Player::A, Slot::Left); | |
| // If they were wait, now should be active | |
| assert!(!acts_center && !acts_left); | |
| } | |
| /// Q182: Energy placement vs yell conditions | |
| /// 『ライブ成功時 ...公開されたカードの中にブレードハートを持たないカード | |
| /// が0枚の場合か、または...』 | |
| /// With restricted yell (0 cards public due to wait effects), | |
| /// condition checks pass | |
| fn test_q182_zero_revealed_yell_condition() { | |
| let mut test = GameTest::new(); | |
| let member = Card::member("PL!S-bp3-019"); // Has this ability | |
| test.place_live(Player::A, member); | |
| // Apply effect: yell shows 0 cards (due to wait members) | |
| test.set_yell_reveal_count(Player::A, 0); | |
| // Live success check: "no cards without blade hearts" = TRUE (0 cards) | |
| test.enter_live_verdict(Player::A, LiveVerdict::Success); | |
| // Ability condition should be satisfied | |
| let abilities_triggered = test.get_live_success_abilities(Player::A); | |
| assert!(!abilities_triggered.is_empty()); | |
| } | |
| /// Q183: Cost payment side restriction | |
| /// Cost effects like "player1をウェイトにする" cannot target opponent | |
| /// Even if the ability doesn't restrict it explicitly | |
| fn test_q183_cost_effect_own_side() { | |
| let mut test = GameTest::new(); | |
| let cost_ability = Card::member("PL!-bp3-004"); | |
| test.place(Player::A, cost_ability, Slot::Center); | |
| // Effect has cost: "player 1 を wait状態にする (選択)" | |
| // Player A tries to target their own member | |
| let can_target_own = test.can_pay_cost_by_waiting( | |
| Player::A, | |
| Player::A, | |
| Slot::Left | |
| ); | |
| assert!(can_target_own); | |
| // But cannot target opponent | |
| let can_target_opp = test.can_pay_cost_by_waiting( | |
| Player::A, | |
| Player::B, | |
| Slot::Right | |
| ); | |
| assert!(!can_target_opp); | |
| } | |
| /// Q184: Under-member energy not counted in energy total | |
| /// メンバーの下に置かれたエネルギーカードはエネルギーの数として数えない | |
| /// Separate zone from energy field | |
| fn test_q184_under_member_energy_separate() { | |
| let mut test = GameTest::new(); | |
| let member = Card::member("PL!N-bp3-001"); | |
| test.place(Player::A, member, Slot::Center); | |
| // Place energy in zone (counts) | |
| test.add_energy(Player::A, 3); | |
| assert_eq!(test.energy_count(Player::A), 3); | |
| // Place energy under member | |
| test.place_energy_under_member(Player::A, Slot::Center, 2); | |
| // Total energy still 3 (under-member not counted) | |
| assert_eq!(test.energy_count(Player::A), 3); | |
| // But member "has" 2 energy underneath | |
| assert_eq!(test.energy_under_member(Player::A, Slot::Center), 2); | |
| } | |
| } | |