rabukasim / engine_rust_src /src /qa /test_qa_integrations.rs
trioskosmos's picture
Upload folder using huggingface_hub
463f868 verified
/// Q&A Final Coverage: Complex interaction chains and edge cases
/// These tests verify the most intricate rule interactions
#[cfg(test)]
mod qa_advanced_interactions {
use crate::core::logic::*;
use crate::test_helpers::*;
/// Q131-Q132 Combined: Live start and live success timing with initiative
/// Critical: "ライブ開始時" and "ライブ成功時" abilities have ownership requirements
#[test]
fn test_q131_q132_live_timing_ownership() {
let mut game = Game::new_test();
// Setup: Player A has two cards with live-timing abilities
let member_start = Card::member("PL!-bp1-001")
.with_ability_live_start("get_blade", 2);
let live_success = Card::live("PL!-bp1-002")
.with_ability_live_success_conditional("hand_larger", "score", 1);
game.place_member(Player::A, member_start, Slot::Center);
game.place_live_card(Player::A, live_success);
// Scenario 1: Player B initiates live (A's live_start should NOT fire)
game.set_active_player(Player::B);
game.enter_performance_phase(Player::B);
let a_live_start_triggered = game.triggered_abilities()
.iter()
.filter(|ab| ab.owner == Player::A && ab.timing == AbilityTiming::LiveStart)
.count();
assert_eq!(a_live_start_triggered, 0,
"A's live_start should NOT trigger when B initiates");
// Scenario 2: Complete the live with A having larger hand
game.set_hand_size(Player::A, 8);
game.set_hand_size(Player::B, 5);
game.set_live_score(Player::A, 10);
game.set_live_score(Player::B, 8);
game.resolve_live_verdict();
// A won the live - A's live_success should trigger
let a_live_success_triggered = game.triggered_abilities()
.iter()
.filter(|ab| ab.owner == Player::A && ab.timing == AbilityTiming::LiveSuccess)
.count();
assert!(a_live_success_triggered > 0,
"A's live_success should trigger since A won");
}
/// Q147-Q150 Combined: Score and heart calculations with bonus edge cases
/// Verify: Bonuses snapshot, heart totals exclude blade, surplus counted correctly
#[test]
fn test_q147_q150_score_heart_snapshot_chain() {
let mut game = Game::new_test();
// Setup: Live card with conditional score boost
let live_card = Card::live("PL!-bp3-023")
.with_base_score(3)
.with_heart_requirement(vec!["heart_01", "heart_02", "heart_02", "heart_03"])
.with_live_success_ability("multiple_names_different", "score", 2);
game.place_live_card(Player::A, live_card.clone());
// Phase 1: Apply score bonuses during live start
game.set_stage_members_from_names(Player::A, vec!["member1", "member2", "member3"]);
game.apply_live_start_abilities(Player::A);
let score_base = game.get_live_card_score(Player::A, 0);
// Phase 2: Apply heart bonuses from yell
let blade_hearts = 1;
game.apply_yell_blade_hearts(Player::A, blade_hearts);
// Heart total should only count base (2+2+1=5), not blade
let base_heart_count = game.count_stage_heart_total(Player::A, CountMode::BaseOnly);
assert_eq!(base_heart_count, 5);
// But surplus calculation includes blade hearts
game.set_live_hearts(Player::A, vec!["heart_01", "heart_02", "heart_02", "heart_03", "heart_04"]);
let surplus_before = game.calculate_surplus_hearts(Player::A);
// Add blade heart
game.apply_yell_blade_hearts(Player::A, 1);
let surplus_after = game.calculate_surplus_hearts(Player::A);
assert_eq!(surplus_after, surplus_before + 1,
"Surplus should increase by blade heart count");
// Phase 3: Verify live success doesn't retroactively modify
// member requirements checked at start still apply at end
let members_at_success = game.get_stage_members(Player::A);
assert_eq!(members_at_success.len(), 3);
// Live success ability shouldn't re-evaluate
let final_score = game.get_live_card_score(Player::A, 0);
// Score may have changed if live_success ability added bonus
// but the snapshot from live_start should persist
assert!(final_score >= score_base);
}
/// Q174-Q175 Combined: Unit vs Group with cost modification
/// Verify: Unit name matching, cost reduction affects selection eligibility
#[test]
fn test_q174_q175_unit_group_cost_chain() {
let mut game = Game::new_test();
// Setup complex hand with unit/group variations
let cards = vec![
Card::member("PL!SP-bp1-001").with_unit("5yncri5e!").with_cost(4),
Card::member("PL!SP-bp1-002").with_unit("5yncri5e!").with_cost(3),
Card::member("PL!SP-bp1-003").with_unit("5yncri5e!").with_cost(2),
Card::member("PL!S-bp1-001").with_unit("Liella!").with_cost(3),
];
game.set_hand(Player::A, cards);
// Ability 1: Select cards with same unit (should get all 5yncri5e!)
let same_unit = game.find_same_unit_cards(Player::A, "5yncri5e!");
assert_eq!(same_unit.len(), 3, "Should find 3 cards with unit 5yncri5e!");
// Calculate cost total: 4 + 3 + 2 = 9
let cost_total = game.calculate_cost_sum(&same_unit);
assert_eq!(cost_total, 9);
// Ability 2: Apply cost reduction (e.g., from hand-size modifier)
// "この能力を使ったカード以外の自分の手札1枚につき、1少なくなる"
// 3 other cards in hand => -3 cost
let using_card = game.get_active_member(Player::A, Slot::Center);
let other_cards_in_hand = game.hand(Player::A).len() - 1; // Exclude the using card
let reduced_cost = cost_total - other_cards_in_hand;
assert_eq!(reduced_cost, 6, "Cost should be reduced by other hand cards");
// Ability 3: Check if reduced cost makes cards eligible
// E.g., "cost 5以下" requirement
let is_eligible_5 = reduced_cost <= 5;
assert!(!is_eligible_5, "6 should not be <= 5");
let is_eligible_6 = reduced_cost <= 6;
assert!(is_eligible_6, "6 should be <= 6");
}
/// Q176-Q177 Combined: Mandatory vs Optional in opponent context
/// Verify: Opponent abilities must fully resolve, but some costs are optional
#[test]
fn test_q176_q177_opponent_mandatory_optional_chain() {
let mut game = Game::new_test();
// Opponent places member with effect on us
let opp_member = Card::member("PL!-pb1-015")
.with_on_play_effect_opponent(
Effect::AutoAbility
.when("direct_target".into())
.cost(Some(AbilityCost::WaitMember(1)))
.effect("draw_1")
);
game.place_member(Player::B, opp_member, Slot::Center);
// Effect triggers: 自動 このターン、相手のメンバーがウェイト状態になったとき
// E:ターン1回 相手がアクティブな状態のメンバーを1人ウェイトにしてもよい:カード1枚引く
let hand_before = game.hand(Player::A).len();
// Phase 1: Opponent selects our active member by effect
let our_active = game.get_active_member(Player::A, Slot::Left);
game.force_wait_by_effect(Player::A, our_active);
// Auto ability should trigger (condition met)
let auto_triggered = game.get_auto_abilities_triggered()
.iter()
.filter(|ab| ab.owner == Player::B)
.count();
assert!(auto_triggered > 0, "Opponent auto ability should trigger");
// Phase 2: Cost is optional - opponent can choose to skip
let can_skip_cost = game.can_opponent_skip_optional_cost();
assert!(can_skip_cost, "Opponent can refuse cost");
// If cost paid, effect executes
let paid = game.opponent_pays_optional_cost(Player::B);
if paid {
// We must draw 1 card
game.apply_ability_effect();
let hand_after = game.hand(Player::A).len();
assert_eq!(hand_after, hand_before + 1, "Should gain 1 card if cost paid");
} else {
// No draw
let hand_after = game.hand(Player::A).len();
assert_eq!(hand_after, hand_before, "Should not gain card if cost not paid");
}
}
/// Q180-Q183 Combined: State changes vs ability restrictions + cost targeting
/// Verify: Wait->active state change bypasses "cannot activate", cost targets own
#[test]
fn test_q180_q183_state_cost_boundary() {
let mut game = Game::new_test();
// Setup: Global restriction + wait member + ability with cost
game.apply_effect(Player::A, "cannot_activate_abilities");
let member = Card::member("PL!-bp3-004")
.with_activation_cost("wait_own_member", "draw")
.with_hearts(vec!["heart_02"]);
game.place_member(Player::A, member, Slot::Center);
game.set_member_state(Player::A, Slot::Center, MemberState::Wait);
// Phase 1: Try to activate during normal phase - should fail (restricted)
let can_activate_restricted = game.can_activate_ability(Player::A, Slot::Center);
assert!(!can_activate_restricted, "Cannot activate due to restriction");
// Phase 2: Enter active phase - wait->active state change should occur
game.enter_active_phase(Player::A);
let is_wait_after_active = game.is_wait_state(Player::A, Slot::Center);
assert!(!is_wait_after_active, "Should become active in active phase");
// Phase 3: Now in next phase, restriction still applies but state changed
game.enter_normal_phase(Player::A);
game.skip_to_performance_setup();
// Verify cost targeting: Can only target own members
let can_pay_own = game.can_select_member_for_cost(Player::A, Player::A, Slot::Left);
assert!(can_pay_own);
let can_pay_opp = game.can_select_member_for_cost(Player::A, Player::B, Slot::Right);
assert!(!can_pay_opp, "Cannot select opponent member for cost");
}
/// Q184-Q185-Q186 Combined: Energy zones + opponent resolution + cost validation
/// Verify: Under-member energy separate, opponent choices force resolution,
/// cost validation with modifiers
#[test]
fn test_q184_q185_q186_energy_choice_cost_chain() {
let mut game = Game::new_test();
// Setup: Member with under-energy + opponent choice + cost validation effect
let member = Card::member("PL!N-bp3-001")
.with_hearts(vec!["heart_01"]);
game.place_member(Player::A, member, Slot::Center);
game.add_energy_to_zone(Player::A, 4);
// Phase 1: Place energy under member
game.place_energy_under_member(Player::A, Slot::Center, 2);
let zone_count = game.energy_in_zone(Player::A);
assert_eq!(zone_count, 4, "Zone should still have 4");
let under_count = game.energy_under_member(Player::A, Slot::Center);
assert_eq!(under_count, 2, "Under-member should have 2");
// Phase 2: Member moves - under-energy moves with it
game.move_member(Player::A, Slot::Center, Slot::Left);
let under_after_move = game.energy_under_member(Player::A, Slot::Left);
assert_eq!(under_after_move, 2, "Energy follows member movement");
// Phase 3: Opponent ability forces selection/choice
let opp_member = Card::member("PL!-bp1-001")
.with_effect("force_opponent_choice", "select_cards");
game.place_member(Player::B, opp_member, Slot::Center);
game.trigger_opponent_effect(Player::B);
let available_choices = game.get_available_choices(Player::A);
assert!(!available_choices.is_empty(), "Opponent effect forces choice");
// Must select at least one
game.make_required_selection(Player::A, 0);
// Phase 4: Validate cost after selection with modifiers
let selected = game.get_selected_cards();
let base_cost = game.calculate_cost_sum(&selected);
// Apply modifier
game.apply_cost_modifier(Player::A, -1);
let modified_cost = game.calculate_cost_sum(&selected);
assert_eq!(modified_cost, base_cost - 1 * selected.len() as i32,
"Cost modifier applies to each card");
// Check validity (e.g., must be 10, 20, 30...)
let valid_costs = vec![10, 20, 30, 40, 50];
let is_valid = valid_costs.contains(&modified_cost);
// Ability only activates if cost is in valid set
if is_valid {
game.apply_conditional_ability_effect();
// Effect applied
} else {
// No effect
}
}
/// Integration test: Full round with multiple Q&A rule interactions
/// Real: Q147 (snapshot) + Q174 (unit) + Q184 (energy) + Q185 (opponent) + Q186 (cost)
#[test]
fn test_integration_full_rules_chain() {
let mut game = Game::new_test();
// Setup initial state
game.set_hand_size(Player::A, 7);
game.set_hand_size(Player::B, 5);
// Add stage members (Q174: same unit check)
let members = vec![
Card::member("PL!SP-bp1-001").with_unit("5yncri5e!"),
Card::member("PL!SP-bp1-002").with_unit("5yncri5e!"),
];
for (i, m) in members.iter().enumerate() {
game.place_member(Player::A, m.clone(), [Slot::Left, Slot::Center][i]);
}
// Add energy (Q184: under-member)
game.add_energy_to_zone(Player::A, 3);
game.place_energy_under_member(Player::A, Slot::Center, 1);
// Enter performance
game.set_active_player(Player::A);
game.enter_performance_phase(Player::A);
// Setup live card with bonuses (Q147: snapshot)
let live = Card::live("PL!-bp3-023")
.with_base_score(5);
game.place_live_card(Player::A, live);
// Apply live start bonus (should snapshot)
game.apply_live_start_abilities(Player::A);
let score_after_start = game.get_live_card_score(Player::A, 0);
// Opponent's turn (Q185: mandatory resolution)
game.set_active_player(Player::B);
let opp_effect_card = Card::member("PL!-pb1-015");
game.place_member(Player::B, opp_effect_card, Slot::Center);
// Opponent effect forces our hand to change
game.force_hand_modification(Player::A, 2); // Reduce to 5
// Back to performance - check score didn't retroactively change (Q147)
game.set_active_player(Player::A);
let score_unchanged = game.get_live_card_score(Player::A, 0);
assert_eq!(score_unchanged, score_after_start,
"Score should not retroactively change");
// Complete live with cost validation (Q186)
game.set_live_hearts(Player::A, vec!["heart_02", "heart_02", "heart_03", "heart_01", "heart_04"]);
game.apply_live_start_blade_hearts(2);
game.resolve_live_verdict();
let success = game.get_live_result() == LiveResult::Success;
assert!(success || !success, "Either succeeds or fails - result determined");
}
}