Voting System Testing

Overview

This document outlines the comprehensive testing approach for the voting system in Rummage's Commander format implementation. The voting mechanic appears on various cards like Council's Judgment, Expropriate, and Capital Punishment, and represents a key political interaction point in multiplayer games.

Vote Initialization Tests

Tests for the initialization of voting events:

#![allow(unused)]
fn main() {
#[test]
fn test_vote_initialization() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Create vote options
    let option_a = "Exile target permanent";
    let option_b = "Each player sacrifices a creature";
    
    // Initialize vote
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec![option_a.to_string(), option_b.to_string()],
        timing_restriction: VoteTiming::OnResolve,
    });
    app.update();
    
    // Verify vote was created
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.options.len(), 2);
    assert_eq!(vote_state.initiator, players[0]);
    assert_eq!(vote_state.status, VoteStatus::Active);
    assert_eq!(vote_state.eligible_voters.len(), 4);
}

#[test]
fn test_vote_with_timing_restrictions() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin)
       .add_plugin(TurnStructurePlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Set current player and phase
    app.world.resource_mut::<TurnState>().current_player = players[0];
    app.world.resource_mut::<TurnState>().current_phase = Phase::Main1;
    
    // Initialize vote with sorcery timing
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::SorcerySpeed,
    });
    app.update();
    
    // Verify vote is allowed during main phase of initiator's turn
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.status, VoteStatus::Active);
    
    // Change to different player's turn
    app.world.resource_mut::<TurnState>().current_player = players[1];
    
    // Try to initialize another vote with sorcery timing
    let vote_id2 = app.world.send_event(InitializeVoteEvent {
        initiator: players[1],
        options: vec!["Option X".to_string(), "Option Y".to_string()],
        timing_restriction: VoteTiming::SorcerySpeed,
    });
    app.update();
    
    // Verify second vote is active (as it's now player1's turn)
    let vote_state2 = app.world.resource::<VoteRegistry>().get_vote(vote_id2).unwrap();
    assert_eq!(vote_state2.status, VoteStatus::Active);
    
    // Change to non-main phase
    app.world.resource_mut::<TurnState>().current_phase = Phase::Combat;
    
    // Try to initialize another vote with sorcery timing
    let vote_id3 = app.world.send_event(InitializeVoteEvent {
        initiator: players[1],
        options: vec!["Option C".to_string(), "Option D".to_string()],
        timing_restriction: VoteTiming::SorcerySpeed,
    });
    app.update();
    
    // Verify third vote is rejected due to timing
    let vote_state3 = app.world.resource::<VoteRegistry>().get_vote(vote_id3).unwrap();
    assert_eq!(vote_state3.status, VoteStatus::Rejected);
    assert_eq!(vote_state3.rejection_reason, Some(VoteRejectionReason::InvalidTiming));
}
}

Vote Casting Tests

Tests for the vote casting mechanics:

#![allow(unused)]
fn main() {
#[test]
fn test_basic_vote_casting() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Initialize vote
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
    });
    app.update();
    
    // Players cast votes
    app.world.send_event(CastVoteEvent {
        voter: players[0],
        vote_id,
        option_index: 0, // Option A
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[1],
        vote_id,
        option_index: 0, // Option A
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[2],
        vote_id,
        option_index: 1, // Option B
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[3],
        vote_id,
        option_index: 1, // Option B
    });
    
    app.update();
    
    // Verify votes were recorded
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.votes.len(), 4);
    assert_eq!(vote_state.vote_counts[0], 2); // Option A has 2 votes
    assert_eq!(vote_state.vote_counts[1], 2); // Option B has 2 votes
}

#[test]
fn test_vote_weight_modifiers() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Add a vote weight modifier to player0
    app.world.entity_mut(players[0]).insert(VoteWeightModifier {
        multiplier: 2.0,
        expiration: VoteWeightExpiration::Permanent,
    });
    
    // Initialize vote
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
    });
    app.update();
    
    // Players cast votes
    app.world.send_event(CastVoteEvent {
        voter: players[0],
        vote_id,
        option_index: 0, // Option A - should count as 2 votes
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[1],
        vote_id,
        option_index: 1, // Option B
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[2],
        vote_id,
        option_index: 1, // Option B
    });
    
    app.update();
    
    // Verify vote weights were applied correctly
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.vote_counts[0], 2); // Option A has 2 votes (from weighted player)
    assert_eq!(vote_state.vote_counts[1], 2); // Option B has 2 votes (1 each from two players)
}

#[test]
fn test_vote_time_limits() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin)
       .add_plugin(TimePlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Initialize vote with time limit
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
        time_limit: Some(std::time::Duration::from_secs(30)),
    });
    app.update();
    
    // Players cast some votes
    app.world.send_event(CastVoteEvent {
        voter: players[0],
        vote_id,
        option_index: 0,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[1],
        vote_id,
        option_index: 1,
    });
    
    app.update();
    
    // Fast forward time past the limit
    advance_time(&mut app, std::time::Duration::from_secs(31));
    app.update();
    
    // Verify vote was automatically closed
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.status, VoteStatus::Closed);
    
    // Try to cast another vote
    app.world.send_event(CastVoteEvent {
        voter: players[2],
        vote_id,
        option_index: 0,
    });
    app.update();
    
    // Verify late vote was not counted
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.votes.len(), 2); // Still only 2 votes
}
}

Vote Resolution Tests

Tests for resolving votes and applying their effects:

#![allow(unused)]
fn main() {
#[test]
fn test_vote_resolution_with_clear_winner() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Initialize vote
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
    });
    app.update();
    
    // Players cast votes with clear winner
    app.world.send_event(CastVoteEvent {
        voter: players[0],
        vote_id,
        option_index: 0,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[1],
        vote_id,
        option_index: 0,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[2],
        vote_id,
        option_index: 0,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[3],
        vote_id,
        option_index: 1,
    });
    
    app.update();
    
    // Resolve vote
    app.world.send_event(ResolveVoteEvent { vote_id });
    app.update();
    
    // Verify correct option won
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.status, VoteStatus::Resolved);
    assert_eq!(vote_state.winning_option, Some(0)); // Option A won
}

#[test]
fn test_vote_tie_breaking() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Initialize vote with tie-breaker
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
        tie_breaker: VoteTieBreaker::Initiator,
    });
    app.update();
    
    // Players cast votes resulting in tie
    app.world.send_event(CastVoteEvent {
        voter: players[0],
        vote_id,
        option_index: 0, // Initiator votes for Option A
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[1],
        vote_id,
        option_index: 0,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[2],
        vote_id,
        option_index: 1,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[3],
        vote_id,
        option_index: 1,
    });
    
    app.update();
    
    // Resolve vote
    app.world.send_event(ResolveVoteEvent { vote_id });
    app.update();
    
    // Verify initiator's choice won the tie
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.status, VoteStatus::Resolved);
    assert_eq!(vote_state.winning_option, Some(0)); // Option A won due to initiator tie-break
    
    // Test different tie-breaker: Random
    let vote_id2 = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option X".to_string(), "Option Y".to_string()],
        timing_restriction: VoteTiming::OnResolve,
        tie_breaker: VoteTieBreaker::Random,
    });
    app.update();
    
    // Players cast votes resulting in tie
    for player in &players {
        app.world.send_event(CastVoteEvent {
            voter: *player,
            vote_id: vote_id2,
            option_index: player.index() % 2, // Evenly split votes
        });
    }
    app.update();
    
    // Set up deterministic RNG for testing
    app.insert_resource(TestRng::with_seed(12345));
    
    // Resolve vote
    app.world.send_event(ResolveVoteEvent { vote_id: vote_id2 });
    app.update();
    
    // Verify a random winner was selected
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id2).unwrap();
    assert_eq!(vote_state.status, VoteStatus::Resolved);
    assert!(vote_state.winning_option.is_some()); // Some option was chosen randomly
}

#[test]
fn test_vote_effect_application() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Create test permanent to potentially exile
    let target_permanent = app.world.spawn((
        Permanent { controller: players[1], ..Default::default() },
    )).id();
    
    // Create vote with exile effect
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Exile permanent".to_string(), "Draw cards".to_string()],
        timing_restriction: VoteTiming::OnResolve,
        effects: vec![
            VoteEffect::ExilePermanent { target: target_permanent },
            VoteEffect::DrawCards { player: players[0], count: 2 },
        ],
    });
    app.update();
    
    // Players vote to exile the permanent
    for player in &players {
        app.world.send_event(CastVoteEvent {
            voter: *player,
            vote_id,
            option_index: 0, // Everyone votes to exile
        });
    }
    app.update();
    
    // Resolve vote
    app.world.send_event(ResolveVoteEvent { vote_id });
    app.update();
    
    // Verify permanent was exiled
    let zone = app.world.get::<Zone>(target_permanent).unwrap();
    assert_eq!(*zone, Zone::Exile);
}
}

Edge Case Tests

Testing unusual voting scenarios:

#![allow(unused)]
fn main() {
#[test]
fn test_vote_abstention() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Initialize vote
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
        allow_abstention: true,
    });
    app.update();
    
    // Two players vote, two abstain
    app.world.send_event(CastVoteEvent {
        voter: players[0],
        vote_id,
        option_index: 0,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[1],
        vote_id,
        option_index: 1,
    });
    
    // Players 2 and 3 abstain by not voting
    
    // Close vote manually
    app.world.send_event(ResolveVoteEvent { vote_id });
    app.update();
    
    // Verify vote was counted correctly with abstentions
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.votes.len(), 2); // Only 2 votes cast
    assert_eq!(vote_state.eligible_voters.len(), 4); // But 4 eligible voters
    assert_eq!(vote_state.abstention_count, 2); // 2 abstentions
}

#[test]
fn test_vote_with_modifiers() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Create card that affects votes
    let vote_modifier_card = app.world.spawn((
        Card::default(),
        VoteModifier {
            effect_type: VoteModifierType::DoubleVotesForOption { option_index: 0 },
            controller: players[0],
        },
    )).id();
    
    // Initialize vote
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
    });
    app.update();
    
    // Players cast votes
    app.world.send_event(CastVoteEvent {
        voter: players[0],
        vote_id,
        option_index: 0, // This vote should be doubled
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[1],
        vote_id,
        option_index: 0,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[2],
        vote_id,
        option_index: 1,
    });
    
    app.world.send_event(CastVoteEvent {
        voter: players[3],
        vote_id,
        option_index: 1,
    });
    
    app.update();
    
    // Resolve vote
    app.world.send_event(ResolveVoteEvent { vote_id });
    app.update();
    
    // Verify vote modifier was applied
    let vote_state = app.world.resource::<VoteRegistry>().get_vote(vote_id).unwrap();
    assert_eq!(vote_state.modified_vote_counts[0], 3); // 2 votes, but player0's vote doubled
    assert_eq!(vote_state.modified_vote_counts[1], 2); // 2 normal votes
    assert_eq!(vote_state.winning_option, Some(0));
}
}

UI and Network Tests

Testing the voting UI and network synchronization:

#![allow(unused)]
fn main() {
#[test]
fn test_vote_ui_representation() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin)
       .add_plugin(UiPlugin);
       
    // Setup players
    let players = setup_four_player_game(&mut app);
    
    // Initialize vote
    let vote_id = app.world.send_event(InitializeVoteEvent {
        initiator: players[0],
        options: vec!["Option A".to_string(), "Option B".to_string()],
        timing_restriction: VoteTiming::OnResolve,
    });
    app.update();
    
    // Verify vote UI elements were created
    let vote_ui_elements = app.world.query_filtered::<Entity, With<VoteUiElement>>()
        .iter(&app.world)
        .collect::<Vec<_>>();
    
    assert!(!vote_ui_elements.is_empty());
    
    // Verify options are displayed
    let vote_options = app.world.query_filtered::<&Text, With<VoteOptionText>>()
        .iter(&app.world)
        .collect::<Vec<_>>();
    
    assert_eq!(vote_options.len(), 2);
    assert!(vote_options[0].sections[0].value.contains("Option A"));
    assert!(vote_options[1].sections[0].value.contains("Option B"));
}

#[test]
fn test_vote_network_synchronization() {
    let mut app = TestNetworkApp::new();
    app.add_plugins(MinimalPlugins)
       .add_plugin(PoliticsPlugin)
       .add_plugin(NetworkPlugin);
       
    // Setup networked game with host and client
    let (host_id, client_id) = app.setup_network_game(2);
    
    // Host initializes vote
    app.send_host_message(InitializeVoteMessage {
        initiator: host_id,
        options: vec!["Option A".to_string(), "Option B".to_string()],
        vote_id: VoteId::new(),
    });
    app.update();
    
    // Verify vote was synchronized to client
    let client_votes = app.get_client_resource::<VoteRegistry>().active_votes();
    assert_eq!(client_votes.len(), 1);
    
    // Client casts vote
    app.send_client_message(CastVoteMessage {
        voter: client_id,
        vote_id: client_votes[0].id,
        option_index: 1,
    });
    app.update();
    
    // Verify vote was recorded on host
    let host_vote = app.get_host_resource::<VoteRegistry>().get_vote(client_votes[0].id).unwrap();
    assert_eq!(host_vote.votes.len(), 1);
    assert_eq!(host_vote.votes[0].voter, client_id);
    assert_eq!(host_vote.votes[0].option_index, 1);
}
}

Conclusion

The voting system is a complex but essential political mechanic in Commander format. These tests ensure that votes are properly initialized, cast, tallied, and resolved under all circumstances, ensuring a fair and predictable voting experience for players.