Multiplayer Politics
Overview
The Multiplayer Politics module handles the social and strategic elements unique to multiplayer Commander games. This includes deal-making, alliance formation, temporary agreements, voting mechanics, and other player interactions not codified in the standard Magic rules.
Core Features
The system includes:
- Deal Making System: Framework for players to propose, accept, and track in-game deals
- Voting Mechanics: Implementation of cards with Council's Dilemma, Will of the Council, and other voting abilities
- Threat Assessment: Tools to analyze and display relative threat levels of players
- Alliance Tracking: Temporary cooperative arrangements between players
- Table Talk Integration: Support for in-game communication with policy enforcement
- Goad Mechanics: Implementation of abilities that force creatures to attack
Implementation
The politics system is implemented through several interconnected components:
#![allow(unused)] fn main() { #[derive(Component)] pub struct PoliticsComponent { // Current deals, alliances, and political state pub active_deals: Vec<Deal>, pub alliances: HashMap<Entity, AllianceStrength>, pub political_capital: f32, pub trust_level: HashMap<Entity, TrustLevel>, // Historical tracking pub broken_deals: Vec<BrokenDeal>, pub past_alliances: Vec<PastAlliance>, } #[derive(Resource)] pub struct PoliticsSystem { // Global politics configuration pub enable_deals: bool, pub allow_secret_deals: bool, pub deal_enforcement_level: DealEnforcementLevel, // Event history pub political_events: VecDeque<PoliticalEvent>, } }
Deal Structure
Deals are structured entities that capture player agreements:
#![allow(unused)] fn main() { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Deal { /// Unique identifier for the deal pub id: Uuid, /// Player who proposed the deal pub proposer: Entity, /// Player(s) who accepted the deal pub acceptors: Vec<Entity>, /// Terms of the deal - what each party agrees to do pub terms: Vec<DealTerm>, /// When the deal expires (if temporary) pub expiration: Option<DealExpiration>, /// Current status of the deal pub status: DealStatus, /// When the deal was created pub created_at: f64, /// When the deal was last updated pub last_updated: f64, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DealTerm { /// Promise not to attack a player for N turns NonAggression { target: Entity, duration: u32, }, /// Promise to attack a specific player AttackPlayer { target: Entity, next_turn_only: bool, }, /// Promise not to counter a player's next spell NoCounterspell { target: Entity, duration: u32, }, /// Promise to share resources (e.g. let them draw when you draw) ShareResource { resource_type: ResourceType, target: Entity, amount: u32, }, /// Promise to vote with a player on the next vote VoteAlignment { ally: Entity, vote_count: u32, }, /// Custom term with free-form text Custom { description: String, }, } }
Deal Making
The deal making system allows players to:
- Propose deals with specific terms and duration
- Accept or reject deals from other players
- Set automatic deal conditions and consequences
- Track deal fulfillment and violations
Deals are non-binding at the rules level but provide framework for player agreements.
#![allow(unused)] fn main() { /// System that handles deal proposals pub fn handle_deal_proposals( mut commands: Commands, mut deal_events: EventReader<DealProposalEvent>, mut deal_response_events: EventWriter<DealResponseEvent>, mut politics_components: Query<&mut PoliticsComponent>, ) { for event in deal_events.read() { if let Ok(mut proposer_politics) = politics_components.get_mut(event.proposer) { // Create new deal let deal = Deal { id: Uuid::new_v4(), proposer: event.proposer, acceptors: Vec::new(), terms: event.terms.clone(), expiration: event.expiration.clone(), status: DealStatus::Proposed, created_at: event.timestamp, last_updated: event.timestamp, }; // Add to proposer's active deals proposer_politics.active_deals.push(deal.clone()); // Notify target players for target in &event.targets { deal_response_events.send(DealResponseEvent { deal_id: deal.id, response_type: DealResponseType::Offered, player: *target, timestamp: event.timestamp, }); } } } } /// System that handles deal responses pub fn handle_deal_responses( mut commands: Commands, mut deal_response_events: EventReader<DealResponseEvent>, mut politics_components: Query<&mut PoliticsComponent>, mut deal_update_events: EventWriter<DealUpdateEvent>, ) { for event in deal_response_events.read() { // Handle player responses to deals match event.response_type { DealResponseType::Accept => { // Update the deal status for (entity, mut politics) in politics_components.iter_mut() { for deal in &mut politics.active_deals { if deal.id == event.deal_id { deal.acceptors.push(event.player); deal.last_updated = event.timestamp; // If all targets have accepted, activate the deal if deal.acceptors.len() >= deal.terms.len() { deal.status = DealStatus::Active; deal_update_events.send(DealUpdateEvent { deal_id: deal.id, new_status: DealStatus::Active, timestamp: event.timestamp, }); } break; } } } }, DealResponseType::Reject => { // Handle rejection for (entity, mut politics) in politics_components.iter_mut() { for deal in &mut politics.active_deals { if deal.id == event.deal_id { deal.status = DealStatus::Rejected; deal.last_updated = event.timestamp; deal_update_events.send(DealUpdateEvent { deal_id: deal.id, new_status: DealStatus::Rejected, timestamp: event.timestamp, }); break; } } } }, // Handle other response types _ => {} } } } }
Voting Mechanics
Many Commander-specific cards feature voting mechanics (Council's Dilemma, Will of the Council). The voting system handles these cards' abilities:
#![allow(unused)] fn main() { /// Component for cards with voting mechanics #[derive(Component)] pub struct VotingMechanic { /// The type of voting mechanic pub voting_type: VotingType, /// The available options to vote for pub options: Vec<String>, /// How the results are applied pub resolution: VoteResolutionMethod, } /// Types of voting mechanics #[derive(Debug, Clone, PartialEq, Eq)] pub enum VotingType { /// Will of the Council - each player gets one vote WillOfCouncil, /// Council's Dilemma - each player votes for two different options CouncilsDilemma, /// Parley - a special voting variant Parley, /// Custom voting system Custom, } /// System for handling vote card resolution pub fn handle_voting_resolution( mut commands: Commands, mut vote_events: EventReader<VoteCompletionEvent>, voting_cards: Query<(Entity, &VotingMechanic)>, players: Query<Entity, With<Player>>, ) { for event in vote_events.read() { if let Ok((card_entity, voting_mechanic)) = voting_cards.get(event.source_card) { // Collect and tally votes let mut vote_counts: HashMap<String, u32> = HashMap::new(); for (player, vote) in &event.votes { *vote_counts.entry(vote.clone()).or_default() += 1; } // Apply effects based on voting results and resolution method match voting_mechanic.resolution { VoteResolutionMethod::MostVotes => { // Find option with most votes if let Some((winning_option, _)) = vote_counts .iter() .max_by_key(|(_, count)| *count) { // Apply effect for winning option apply_voting_effect( &mut commands, card_entity, winning_option, &event.votes ); } }, VoteResolutionMethod::AllVotes => { // Apply effect for each vote for (option, count) in vote_counts { // Apply effect scaled by vote count apply_voting_effect_scaled( &mut commands, card_entity, &option, count, &event.votes ); } }, // Handle other resolution methods _ => {} } } } } }
Example Voting Card Implementation
#![allow(unused)] fn main() { /// Implementation of Councils' Judgment pub fn create_councils_judgment() -> impl Bundle { ( CardName("Council's Judgment".to_string()), CardType::Sorcery, ManaCost::parse("{1}{W}{W}"), VotingMechanic { voting_type: VotingType::WillOfCouncil, options: vec!["Exile".to_string()], // Dynamic: each nonland permanent becomes an option resolution: VoteResolutionMethod::MostVotes, }, CouncilsJudgmentEffect, ) } #[derive(Component)] pub struct CouncilsJudgmentEffect; impl ResolveEffect for CouncilsJudgmentEffect { fn resolve(&self, world: &mut World, source: Entity, controller: Entity) { // Get all permanents as potential targets let targets: Vec<Entity> = get_valid_permanent_targets(world); // Start voting process world.send_event(InitiateVoteEvent { source: source, voting_type: VotingType::WillOfCouncil, options: targets.iter().map(|e| get_name_for_entity(world, *e)).collect(), initiator: controller, }); } } }
Threat Assessment
The threat assessment system helps players evaluate relative threats by:
- Displaying board state power metrics
- Tracking win proximity indicators
- Highlighting potential combo pieces
- Providing history of player actions and tendencies
#![allow(unused)] fn main() { /// Resource for tracking threat assessment #[derive(Resource)] pub struct ThreatAssessment { /// Calculated threat level for each player pub player_threats: HashMap<Entity, ThreatLevel>, /// Factors contributing to threat calculation pub threat_factors: HashMap<Entity, Vec<ThreatFactor>>, /// Historical threat trends pub threat_history: HashMap<Entity, VecDeque<HistoricalThreat>>, } /// System that updates threat assessment pub fn update_threat_assessment( mut threat_assessment: ResMut<ThreatAssessment>, players: Query<Entity, With<Player>>, life_totals: Query<&LifeTotal>, permanents: Query<(Entity, &Controller)>, commanders: Query<(Entity, &Commander, &Controller)>, graveyards: Query<(&Graveyard, &Owner)>, hands: Query<(&Hand, &Owner)>, ) { // Update threat metrics for each player for player in players.iter() { let mut threat_factors = Vec::new(); // Factor: Board presence let board_presence = permanents .iter() .filter(|(_, controller)| controller.0 == player) .count(); threat_factors.push(ThreatFactor { factor_type: ThreatFactorType::BoardPresence, value: board_presence as f32 * 0.5, }); // Factor: Commander damage potential if let Some((_, _, _)) = commanders .iter() .find(|(_, _, controller)| controller.0 == player) { // Calculate commander threat... threat_factors.push(ThreatFactor { factor_type: ThreatFactorType::CommanderPresence, value: 5.0, // Base threat for having commander }); } // Calculate other factors... // Update total threat let total_threat = threat_factors.iter().map(|f| f.value).sum(); threat_assessment.player_threats.insert(player, ThreatLevel(total_threat)); threat_assessment.threat_factors.insert(player, threat_factors); // Update history threat_assessment.threat_history .entry(player) .or_default() .push_back(HistoricalThreat { level: ThreatLevel(total_threat), turn: get_current_turn(), }); } } }
Goad Mechanics
Goad is a Commander-specific mechanic that forces creatures to attack:
#![allow(unused)] fn main() { /// Component for Goad effects #[derive(Component)] pub struct Goaded { /// The player who applied the goad effect pub goaded_by: Entity, /// When the goad effect expires pub expires_at: ExpiryTiming, } /// System that enforces Goad attack requirements pub fn enforce_goad_attack_requirement( goaded_creatures: Query<(Entity, &Goaded, &Controller)>, mut attack_requirement_events: EventWriter<AttackRequirementEvent>, turn_manager: Res<TurnManager>, ) { // Only check during active player's combat if turn_manager.current_phase != Phase::Combat(CombatStep::DeclareAttackers) { return; } let active_player = turn_manager.active_player; // Find creatures controlled by active player that are goaded for (entity, goaded, controller) in goaded_creatures.iter() { if controller.0 == active_player { // Creature must attack if able, and cannot attack player who goaded it attack_requirement_events.send(AttackRequirementEvent { creature: entity, must_attack: true, cannot_attack: vec![goaded.goaded_by], }); } } } }
Alliance Tracking
Alliances are temporary arrangements between players:
#![allow(unused)] fn main() { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Alliance { /// Players in the alliance pub members: Vec<Entity>, /// Strength/type of alliance pub strength: AllianceStrength, /// Purpose of the alliance pub purpose: String, /// When the alliance was formed pub formed_at: f64, /// When the alliance expires (if temporary) pub expires_at: Option<f64>, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum AllianceStrength { /// Weak alliance of convenience Weak, /// Standard alliance Moderate, /// Strong alliance with significant shared interests Strong, } /// System for tracking and updating alliances pub fn update_alliances( mut politics_components: Query<&mut PoliticsComponent>, time: Res<Time>, mut alliance_events: EventReader<AllianceEvent>, ) { let current_time = time.elapsed_seconds_f64(); // Process alliance events for event in alliance_events.read() { match event.event_type { AllianceEventType::Form => { // Create new alliance let alliance = Alliance { members: event.members.clone(), strength: event.strength.clone(), purpose: event.purpose.clone(), formed_at: current_time, expires_at: event.expiration, }; // Update politics components for all members for member in &event.members { if let Ok(mut politics) = politics_components.get_mut(*member) { for other_member in &event.members { if *other_member != *member { politics.alliances.insert(*other_member, alliance.strength.clone()); } } } } }, AllianceEventType::Break => { // Handle alliance breaking for member in &event.members { if let Ok(mut politics) = politics_components.get_mut(*member) { for other_member in &event.members { if *other_member != *member { politics.alliances.remove(other_member); // Record broken alliance in history politics.past_alliances.push(PastAlliance { with: *other_member, strength: event.strength.clone(), purpose: event.purpose.clone(), formed_at: event.formed_at.unwrap_or(0.0), broken_at: current_time, broken_reason: event.reason.clone(), }); } } } } }, } } // Check for expired alliances for mut politics in politics_components.iter_mut() { // Implementation for alliance expiration... } } }
AI Integration
For games with AI opponents, the politics system:
- Models AI political decision making based on configured personalities
- Evaluates deal proposals based on game state and risk assessment
- Tracks human player tendencies for future political decisions
- Simulates realistic political behavior for different AI difficulty levels
#![allow(unused)] fn main() { #[derive(Resource)] pub struct AIPoliticsConfig { /// AI personality profiles pub personalities: HashMap<Entity, AIPoliticalPersonality>, /// AI decision making parameters pub decision_weights: AIPoliticsWeights, /// Historic interaction with players pub player_interaction_history: HashMap<Entity, PlayerInteractionHistory>, } #[derive(Clone, Debug)] pub struct AIPoliticalPersonality { /// How aggressive the AI is pub aggression: f32, /// How trustworthy the AI is pub trustworthiness: f32, /// How risk-averse the AI is pub risk_aversion: f32, /// How vengeful the AI is pub vengefulness: f32, /// How the AI evaluates deals pub deal_evaluation_strategy: DealEvaluationStrategy, } /// System for AI deal evaluation pub fn ai_evaluate_deal( ai_politics_config: Res<AIPoliticsConfig>, threat_assessment: Res<ThreatAssessment>, board_state: Res<BoardState>, deal: &Deal, ai_player: Entity, ) -> DealEvaluationResult { if let Some(personality) = ai_politics_config.personalities.get(&ai_player) { // Get AI personality let trust_factor = personality.trustworthiness; let risk_factor = personality.risk_aversion; // Calculate deal value let mut deal_value = 0.0; for term in &deal.terms { match term { DealTerm::NonAggression { target, duration } => { // Value based on target threat and duration let target_threat = threat_assessment.player_threats .get(target) .map(|t| t.0) .unwrap_or(0.0); deal_value += target_threat * (*duration as f32) * 0.5; }, // Evaluate other term types... _ => {}, } } // Adjust for proposer's trustworthiness let proposer_trust = ai_politics_config.player_interaction_history .get(&ai_player) .and_then(|h| h.player_trust.get(&deal.proposer)) .copied() .unwrap_or(0.5); deal_value *= proposer_trust; // Make decision based on value if deal_value > personality.deal_threshold { DealEvaluationResult::Accept } else { DealEvaluationResult::Reject { reason: "Not valuable enough".to_string() } } } else { // Default rejection if no personality DealEvaluationResult::Reject { reason: "No AI personality configured".to_string() } } } }
UI Components
The multiplayer politics UI provides:
- Deal proposal interface with customizable terms
- Alliance status indicators
- Threat assessment visualization
- Communication tools with appropriate filters
- Deal history and player reputation tracking
#![allow(unused)] fn main() { #[derive(Component)] pub struct PoliticsUIState { /// Currently selected player for political actions pub selected_player: Option<Entity>, /// Deal being constructed pub draft_deal: Option<DraftDeal>, /// UI mode pub ui_mode: PoliticsUIMode, } /// System for rendering politics UI pub fn render_politics_ui( mut commands: Commands, mut egui_context: ResMut<EguiContext>, politics_ui_state: Res<PoliticsUIState>, politics_components: Query<&PoliticsComponent>, players: Query<(Entity, &PlayerName)>, threat_assessment: Res<ThreatAssessment>, ) { // Implementation of politics UI rendering... } }
Integration with Other Systems
The politics system integrates with several other game systems:
Combat System Integration
#![allow(unused)] fn main() { /// System for applying political factors to combat pub fn apply_politics_to_combat( politics_components: Query<&PoliticsComponent>, mut attack_events: EventReader<DeclareAttackerEvent>, mut attack_modifiers: EventWriter<AttackModifierEvent>, alliances: Query<&Alliance>, ) { for event in attack_events.read() { if let Ok(attacker_politics) = politics_components.get(event.controller) { // Check for deals preventing attacks for deal in &attacker_politics.active_deals { for term in &deal.terms { if let DealTerm::NonAggression { target, duration } = term { if *target == event.defender && deal.status == DealStatus::Active { // This attack would violate a non-aggression deal attack_modifiers.send(AttackModifierEvent { attacker: event.attacker, defender: event.defender, modification: AttackModification::PreventAttack { reason: "Non-aggression deal in effect".to_string(), }, }); } } } } // Check for alliances if let Some(alliance_strength) = attacker_politics.alliances.get(&event.defender) { // Alliance strength affects whether attack is allowed if *alliance_strength == AllianceStrength::Strong { attack_modifiers.send(AttackModifierEvent { attacker: event.attacker, defender: event.defender, modification: AttackModification::DissuadeAttack { reason: "Strong alliance in effect".to_string(), penalty: 2.0, }, }); } } } } } }
Card Effect Integration
#![allow(unused)] fn main() { /// System for applying political factors to targeted effects pub fn apply_politics_to_targeting( politics_components: Query<&PoliticsComponent>, mut targeting_events: EventReader<TargetSelectionEvent>, mut targeting_modifiers: EventWriter<TargetingModifierEvent>, ) { for event in targeting_events.read() { if let Ok(caster_politics) = politics_components.get(event.controller) { // Check for deals affecting targeting for deal in &caster_politics.active_deals { for term in &deal.terms { // Handle various deal terms affecting targeting match term { DealTerm::NoCounterspell { target, .. } => { if *target == event.target_controller && is_counterspell(event.source) { // This targeting would violate a no-counterspell deal targeting_modifiers.send(TargetingModifierEvent { source: event.source, target: event.target, modification: TargetingModification::PreventTargeting { reason: "No-counterspell deal in effect".to_string(), }, }); } }, // Handle other deal terms... _ => {}, } } } // Check for alliances affecting targeting if let Some(alliance_strength) = caster_politics.alliances.get(&event.target_controller) { if is_negative_effect(event.source, event.target) { match alliance_strength { AllianceStrength::Strong => { targeting_modifiers.send(TargetingModifierEvent { source: event.source, target: event.target, modification: TargetingModification::DissuadeTargeting { reason: "Strong alliance in effect".to_string(), penalty: 3.0, }, }); }, // Handle other alliance levels... _ => {}, } } } } } } }
Constraints and Limitations
The politics system operates within these constraints:
- No rules enforcement of political agreements (maintaining game integrity)
- Appropriate limits on information sharing for hidden information
- Configurable table talk policies to match playgroup preferences
- Balance between automation and player agency in political decisions
Testing Politics Features
#![allow(unused)] fn main() { #[test] fn test_deal_creation_and_acceptance() { let mut app = App::new(); setup_test_game(&mut app); // Create players let player1 = app.world.spawn(( Player, PlayerName("Player 1".to_string()), PoliticsComponent::default(), )).id(); let player2 = app.world.spawn(( Player, PlayerName("Player 2".to_string()), PoliticsComponent::default(), )).id(); // Create a deal proposal let deal_terms = vec![ DealTerm::NonAggression { target: player2, duration: 2, }, ]; app.world.send_event(DealProposalEvent { proposer: player1, targets: vec![player2], terms: deal_terms, expiration: Some(DealExpiration::Turns(2)), timestamp: 0.0, }); app.update(); // Check that deal was created let politics1 = app.world.get::<PoliticsComponent>(player1).unwrap(); assert_eq!(politics1.active_deals.len(), 1); assert_eq!(politics1.active_deals[0].status, DealStatus::Proposed); // Accept the deal let deal_id = politics1.active_deals[0].id; app.world.send_event(DealResponseEvent { deal_id, response_type: DealResponseType::Accept, player: player2, timestamp: 1.0, }); app.update(); // Verify deal is now active let politics1_updated = app.world.get::<PoliticsComponent>(player1).unwrap(); assert_eq!(politics1_updated.active_deals[0].status, DealStatus::Active); assert_eq!(politics1_updated.active_deals[0].acceptors, vec![player2]); } #[test] fn test_goad_mechanics() { let mut app = App::new(); setup_test_game(&mut app); // Create players let player1 = app.world.spawn(Player).id(); let player2 = app.world.spawn(Player).id(); // Create a creature let creature = app.world.spawn(( CardName("Test Creature".to_string()), CardType::Creature, Power(3), Toughness(3), Controller(player1), OnBattlefield, )).id(); // Goad the creature app.world.entity_mut(creature).insert(Goaded { goaded_by: player2, expires_at: ExpiryTiming::EndOfTurn, }); // Set up combat phase app.world.resource_mut::<TurnManager>().current_phase = Phase::Combat(CombatStep::DeclareAttackers); app.world.resource_mut::<TurnManager>().active_player = player1; app.update(); // Get attack requirements let attack_requirements = app.world.resource::<Events<AttackRequirementEvent>>() .get_reader() .read(&app.world.resource::<Events<AttackRequirementEvent>>()) .collect::<Vec<_>>(); // Verify goad requirements are enforced assert!(!attack_requirements.is_empty()); assert_eq!(attack_requirements[0].creature, creature); assert!(attack_requirements[0].must_attack); assert_eq!(attack_requirements[0].cannot_attack, vec![player2]); } }
Related Resources
- Politics Testing: Details on testing political mechanics
- Commander-Specific Cards: Cards with political mechanics
- Multiplayer Combat: How combat works in multiplayer
- Goad Implementation: More details on goad effects