Stack Resolution Tests

Overview

This document outlines test cases for the stack system in Commander format. These tests ensure that the stack operates correctly, spells and abilities resolve in the right order, and that priority passing works as expected in this multiplayer format.

Test Case: Basic Stack Resolution

Test: Last In, First Out Resolution Order

#![allow(unused)]
fn main() {
#[test]
fn test_lifo_resolution() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_stack_resolution, update_game_state));
       
    // Create player
    let player = app.world.spawn(Player {}).id();
    
    // Create spells for the stack
    let spell1 = app.world.spawn((
        Card { name: "First Spell".to_string() },
        Spell,
        StackPosition(0),
    )).id();
    
    let spell2 = app.world.spawn((
        Card { name: "Second Spell".to_string() },
        Spell,
        StackPosition(1),
    )).id();
    
    let spell3 = app.world.spawn((
        Card { name: "Third Spell".to_string() },
        Spell,
        StackPosition(2),
    )).id();
    
    // Set up stack with spells in order
    app.insert_resource(Stack {
        items: vec![spell1, spell2, spell3],
    });
    
    // Track resolution order
    app.insert_resource(ResolutionOrder { items: Vec::new() });
    
    // All players pass priority
    app.insert_resource(PrioritySystem {
        has_priority: player,
        stack_is_empty: false,
        all_players_passed: true,
    });
    
    // Resolve stack
    app.update();
    
    // Verify resolution happened in LIFO order
    let resolution_order = app.world.resource::<ResolutionOrder>();
    assert_eq!(resolution_order.items, vec![spell3, spell2, spell1]);
}
}

Test Case: Interrupting Stack Resolution

Test: Adding to the Stack During Resolution

#![allow(unused)]
fn main() {
#[test]
fn test_stack_interruption() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_stack_resolution, handle_triggered_abilities));
       
    // Create player
    let player = app.world.spawn(Player {}).id();
    
    // Create an ability that will trigger when a spell resolves
    let permanent = app.world.spawn((
        Card { name: "Ability Source".to_string() },
        Permanent,
        TriggeredAbility {
            trigger: Trigger::SpellResolution,
            ability: AbilityType::CreateEffect,
        },
        Controller { player },
    )).id();
    
    // Create spells for the stack
    let spell1 = app.world.spawn((
        Card { name: "First Spell".to_string() },
        Spell,
        StackPosition(0),
    )).id();
    
    let spell2 = app.world.spawn((
        Card { name: "Second Spell".to_string() },
        Spell,
        StackPosition(1),
    )).id();
    
    // Set up stack with spells
    app.insert_resource(Stack {
        items: vec![spell1, spell2],
    });
    
    // All players pass priority
    app.insert_resource(PrioritySystem {
        has_priority: player,
        stack_is_empty: false,
        all_players_passed: true,
    });
    
    // Partially resolve stack (just the top spell)
    app.update();
    
    // Verify the triggered ability was put on the stack
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 2); // Original spell1 + new triggered ability
    
    // Check that the new ability is at the top of the stack
    let top_item = stack.items.last().unwrap();
    assert!(app.world.get::<TriggeredAbility>(*top_item).is_some());
}
}

Test Case: Priority Passing in Multiplayer

Test: Full Round of Priority After Spell Cast

#![allow(unused)]
fn main() {
#[test]
fn test_multiplayer_priority() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_priority, handle_spell_cast));
       
    // Create multiple players
    let player1 = app.world.spawn(Player { id: 1 }).id();
    let player2 = app.world.spawn(Player { id: 2 }).id();
    let player3 = app.world.spawn(Player { id: 3 }).id();
    let player4 = app.world.spawn(Player { id: 4 }).id();
    
    // Set up turn order
    app.insert_resource(TurnOrder {
        players: vec![player1, player2, player3, player4],
        current_index: 0,
    });
    
    app.insert_resource(TurnManager {
        current_phase: Phase::PreCombatMain,
        active_player: player1,
    });
    
    // Create a spell
    let spell = app.world.spawn((
        Card { name: "Test Spell".to_string() },
        Spell,
        Owner { player: player1 },
    )).id();
    
    // Player 1 has priority initially
    app.insert_resource(PrioritySystem {
        has_priority: player1,
        stack_is_empty: true,
        all_players_passed: false,
    });
    
    // Player 1 casts a spell
    app.world.send_event(SpellCastEvent {
        caster: player1,
        spell: spell,
    });
    app.update();
    
    // Verify spell is on stack
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 1);
    assert_eq!(stack.items[0], spell);
    
    // Verify priority returned to active player
    let priority = app.world.resource::<PrioritySystem>();
    assert_eq!(priority.has_priority, player1);
    
    // Player 1 passes priority
    app.world.send_event(PriorityPassedEvent { player: player1 });
    app.update();
    
    // Priority passes to player 2
    let priority = app.world.resource::<PrioritySystem>();
    assert_eq!(priority.has_priority, player2);
    
    // Player 2 has opportunity to respond
    let response_spell = app.world.spawn((
        Card { name: "Response Spell".to_string() },
        Spell,
        Owner { player: player2 },
    )).id();
    
    // Player 2 casts a response
    app.world.send_event(SpellCastEvent {
        caster: player2,
        spell: response_spell,
    });
    app.update();
    
    // Verify both spells are on stack with response on top
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 2);
    assert_eq!(stack.items[1], response_spell);
    
    // Verify priority returned to player 2
    let priority = app.world.resource::<PrioritySystem>();
    assert_eq!(priority.has_priority, player2);
    
    // All players need to pass again for anything to resolve
    app.world.send_event(PriorityPassedEvent { player: player2 });
    app.update();
    app.world.send_event(PriorityPassedEvent { player: player3 });
    app.update();
    app.world.send_event(PriorityPassedEvent { player: player4 });
    app.update();
    app.world.send_event(PriorityPassedEvent { player: player1 });
    app.update();
    
    // Top spell should resolve
    app.update();
    
    // Verify response spell resolved
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 1);
    assert_eq!(stack.items[0], spell);
}
}

Test Case: Split Second and Interrupts

Test: Split Second Prevents Further Responses

#![allow(unused)]
fn main() {
#[test]
fn test_split_second() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_priority, handle_spell_cast, validate_spell_cast));
       
    // Create players
    let player1 = app.world.spawn(Player { id: 1 }).id();
    let player2 = app.world.spawn(Player { id: 2 }).id();
    
    // Set up turn order
    app.insert_resource(TurnOrder {
        players: vec![player1, player2],
        current_index: 0,
    });
    
    app.insert_resource(TurnManager {
        current_phase: Phase::PreCombatMain,
        active_player: player1,
    });
    
    // Create a split second spell
    let split_second_spell = app.world.spawn((
        Card { name: "Split Second Spell".to_string() },
        Spell,
        SplitSecond,
        Owner { player: player1 },
    )).id();
    
    // Player 1 has priority initially
    app.insert_resource(PrioritySystem {
        has_priority: player1,
        stack_is_empty: true,
        all_players_passed: false,
    });
    
    // Player 1 casts split second spell
    app.world.send_event(SpellCastEvent {
        caster: player1,
        spell: split_second_spell,
    });
    app.update();
    
    // Verify split second spell is on stack
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 1);
    assert_eq!(stack.items[0], split_second_spell);
    
    // Create a response spell
    let response_spell = app.world.spawn((
        Card { name: "Response Spell".to_string() },
        Spell,
        Owner { player: player2 },
    )).id();
    
    // Priority passes to player 2
    app.world.resource_mut::<PrioritySystem>().has_priority = player2;
    
    // Player 2 attempts to cast a response
    app.world.send_event(SpellCastEvent {
        caster: player2,
        spell: response_spell,
    });
    app.update();
    
    // Verify response spell was prevented from being cast
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 1); // Still only the split second spell
    
    // Only mana abilities and special actions can be taken
    let mana_ability = app.world.spawn((
        Card { name: "Mana Source".to_string() },
        ManaAbility,
        Owner { player: player2 },
    )).id();
    
    // Player 2 activates a mana ability
    app.world.send_event(ActivateManaAbilityEvent {
        player: player2,
        ability: mana_ability,
    });
    app.update();
    
    // Verify mana ability was allowed
    assert!(app.world.resource::<Events<ManaProducedEvent>>().is_empty() == false);
}
}

Test Case: Counterspells and Responses

Test: Counterspell on the Stack

#![allow(unused)]
fn main() {
#[test]
fn test_counterspell() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_stack_resolution, handle_spell_cast, handle_counterspell));
       
    // Create player
    let player1 = app.world.spawn(Player { id: 1 }).id();
    let player2 = app.world.spawn(Player { id: 2 }).id();
    
    // Create a target spell
    let target_spell = app.world.spawn((
        Card { name: "Target Spell".to_string() },
        Spell,
        Owner { player: player1 },
    )).id();
    
    // Create a counterspell
    let counterspell = app.world.spawn((
        Card { name: "Counterspell".to_string() },
        Spell,
        CounterspellEffect,
        Owner { player: player2 },
    )).id();
    
    // Put target spell on stack
    app.insert_resource(Stack {
        items: vec![target_spell],
    });
    
    // Player 2 casts counterspell targeting the spell
    app.world.send_event(SpellCastEvent {
        caster: player2,
        spell: counterspell,
    });
    
    // Set up targets for counterspell
    app.world.spawn((
        Target {
            source: counterspell,
            targets: vec![TargetInfo {
                entity: target_spell,
                target_type: TargetType::Spell,
            }],
        },
    ));
    app.update();
    
    // Verify both spells on stack with counterspell on top
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 2);
    assert_eq!(stack.items[1], counterspell);
    
    // All players pass priority
    app.insert_resource(PrioritySystem {
        has_priority: player1,
        stack_is_empty: false,
        all_players_passed: true,
    });
    
    // Resolve counterspell
    app.update();
    
    // Verify target spell was countered and removed from stack
    let stack = app.world.resource::<Stack>();
    assert!(stack.items.is_empty());
    
    // Verify target spell moved to graveyard
    assert_eq!(app.world.get::<Zone>(target_spell).unwrap(), &Zone::Graveyard);
}
}

Test: Uncounterable Spell

#![allow(unused)]
fn main() {
#[test]
fn test_uncounterable_spell() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_stack_resolution, handle_spell_cast, handle_counterspell));
       
    // Create player
    let player1 = app.world.spawn(Player { id: 1 }).id();
    let player2 = app.world.spawn(Player { id: 2 }).id();
    
    // Create an uncounterable spell
    let uncounterable_spell = app.world.spawn((
        Card { name: "Uncounterable Spell".to_string() },
        Spell,
        CannotBeCountered,
        Owner { player: player1 },
    )).id();
    
    // Create a counterspell
    let counterspell = app.world.spawn((
        Card { name: "Counterspell".to_string() },
        Spell,
        CounterspellEffect,
        Owner { player: player2 },
    )).id();
    
    // Put uncounterable spell on stack
    app.insert_resource(Stack {
        items: vec![uncounterable_spell],
    });
    
    // Player 2 casts counterspell targeting the spell
    app.world.send_event(SpellCastEvent {
        caster: player2,
        spell: counterspell,
    });
    
    // Set up targets for counterspell
    app.world.spawn((
        Target {
            source: counterspell,
            targets: vec![TargetInfo {
                entity: uncounterable_spell,
                target_type: TargetType::Spell,
            }],
        },
    ));
    app.update();
    
    // Verify both spells on stack
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 2);
    
    // All players pass priority
    app.insert_resource(PrioritySystem {
        has_priority: player1,
        stack_is_empty: false,
        all_players_passed: true,
    });
    
    // Resolve counterspell
    app.update();
    
    // Verify uncounterable spell remains on stack
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 1);
    assert_eq!(stack.items[0], uncounterable_spell);
    
    // Verify counterspell went to graveyard
    assert_eq!(app.world.get::<Zone>(counterspell).unwrap(), &Zone::Graveyard);
}
}

Test Case: Triggered Abilities and The Stack

Test: Multiple Triggered Abilities Order

#![allow(unused)]
fn main() {
#[test]
fn test_multiple_triggers() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_triggered_abilities, order_triggered_abilities));
       
    // Create players
    let player1 = app.world.spawn(Player { id: 1 }).id();
    let player2 = app.world.spawn(Player { id: 2 }).id();
    
    // Turn order
    app.insert_resource(TurnOrder {
        players: vec![player1, player2],
        current_index: 0,
    });
    
    app.insert_resource(TurnManager {
        current_phase: Phase::Beginning(BeginningPhaseStep::Upkeep),
        active_player: player1,
    });
    
    // Create permanents with upkeep triggers for both players
    let permanent1 = app.world.spawn((
        Card { name: "Permanent 1".to_string() },
        Permanent,
        TriggeredAbility {
            trigger: Trigger::Upkeep(TriggerController::ActivePlayer),
            ability: AbilityType::DrawCard,
        },
        Controller { player: player1 },
    )).id();
    
    let permanent2 = app.world.spawn((
        Card { name: "Permanent 2".to_string() },
        Permanent,
        TriggeredAbility {
            trigger: Trigger::Upkeep(TriggerController::ActivePlayer),
            ability: AbilityType::GainLife(1),
        },
        Controller { player: player1 },
    )).id();
    
    let permanent3 = app.world.spawn((
        Card { name: "Permanent 3".to_string() },
        Permanent,
        TriggeredAbility {
            trigger: Trigger::Upkeep(TriggerController::ActivePlayer),
            ability: AbilityType::LoseLife(1),
        },
        Controller { player: player2 },
    )).id();
    
    // Trigger abilities
    app.world.send_event(PhaseChangedEvent {
        new_phase: Phase::Beginning(BeginningPhaseStep::Upkeep),
    });
    app.update();
    
    // Verify triggers were put on stack
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 3);
    
    // Verify triggers from active player go on stack in APNAP order
    // (Active Player, Non-Active Player)
    let stack_items = &stack.items;
    
    // Active player's abilities should be first (in controller's choice order)
    let first_two_controllers: Vec<Entity> = stack_items.iter()
        .take(2)
        .map(|e| app.world.get::<Controller>(*e).unwrap().player)
        .collect();
    
    // The first two should belong to player1 (active player)
    assert_eq!(first_two_controllers, vec![player1, player1]);
    
    // The last ability should belong to player2
    let last_controller = app.world.get::<Controller>(stack_items[2]).unwrap().player;
    assert_eq!(last_controller, player2);
}
}

Test Case: Stack and State-Based Actions

Test: State-Based Actions Between Stack Resolutions

#![allow(unused)]
fn main() {
#[test]
fn test_state_based_actions_and_stack() {
    // Test setup
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_systems(Update, (handle_stack_resolution, handle_state_based_actions));
       
    // Create player and creature
    let player = app.world.spawn(Player {}).id();
    
    let creature = app.world.spawn((
        Card { name: "Creature".to_string() },
        Creature { power: 3, toughness: 3 },
        Health { current: 3, maximum: 3 },
        Zone::Battlefield,
        Owner { player },
    )).id();
    
    // Create damage spell
    let damage_spell = app.world.spawn((
        Card { name: "Lightning Bolt".to_string() },
        Spell,
        DamageEffect { amount: 3 },
        Owner { player },
    )).id();
    
    // Create spell target
    app.world.spawn((
        Target {
            source: damage_spell,
            targets: vec![TargetInfo {
                entity: creature,
                target_type: TargetType::Creature,
            }],
        },
    ));
    
    // Create heal spell that will be cast in response
    let heal_spell = app.world.spawn((
        Card { name: "Healing Touch".to_string() },
        Spell,
        HealEffect { amount: 3 },
        Owner { player },
    )).id();
    
    // Create heal target
    app.world.spawn((
        Target {
            source: heal_spell,
            targets: vec![TargetInfo {
                entity: creature,
                target_type: TargetType::Creature,
            }],
        },
    ));
    
    // Put spells on stack in order (heal on top, will resolve first)
    app.insert_resource(Stack {
        items: vec![damage_spell, heal_spell],
    });
    
    // Setup priority for resolution
    app.insert_resource(PrioritySystem {
        has_priority: player,
        stack_is_empty: false,
        all_players_passed: true,
    });
    
    // Resolve heal spell
    app.update();
    
    // Verify creature healed to full
    let health = app.world.get::<Health>(creature).unwrap();
    assert_eq!(health.current, 3);
    
    // Still one spell on stack
    let stack = app.world.resource::<Stack>();
    assert_eq!(stack.items.len(), 1);
    
    // Resolve damage spell
    app.update();
    
    // Verify creature took damage
    let health = app.world.get::<Health>(creature).unwrap();
    assert_eq!(health.current, 0);
    
    // Verify stack is empty
    let stack = app.world.resource::<Stack>();
    assert!(stack.items.is_empty());
    
    // Apply state-based actions check
    app.update();
    
    // Verify creature died due to state-based actions
    assert_eq!(app.world.get::<Zone>(creature).unwrap(), &Zone::Graveyard);
}
}

These test cases ensure the stack resolution system functions correctly, handling priority passing, triggered abilities, counterspells, and state-based actions in accordance with Commander's rules.