Skip to main content

manabrew_engine/ability/effects/
vote_effect.rs

1//! Vote effect — Council's Dilemma and Will of the Council voting.
2//!
3//! Ported from Java's `VoteEffect.java`.
4//! Each player votes from a set of choices. The option(s) with the most votes
5//! determine which sub-ability resolves. Handles ties, secret votes, and
6//! additional vote amounts.
7
8use std::collections::HashMap;
9
10use super::EffectContext;
11use crate::event::RunParams;
12use crate::ids::PlayerId;
13use crate::parsing::keys;
14use crate::trigger::TriggerType;
15
16/// Struct form of this effect so it can participate in the
17/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
18/// `VoteEffect` class extending `SpellAbilityEffect`.
19#[manabrew_engine_macros::spell_effect(VoteEffect)]
20fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
21    let controller = sa.activating_player;
22
23    // Get voting players (usually all players, starting with activator)
24    let mut voters: Vec<PlayerId> = if let Some(def) = sa.defined_player() {
25        super::resolve_defined_players(def, controller, ctx.game)
26    } else {
27        ctx.game.player_order.clone()
28    };
29
30    // Rotate so activator votes first
31    if let Some(pos) = voters.iter().position(|&p| p == controller) {
32        voters.rotate_left(pos);
33    }
34
35    // Get vote choices (from Choices$ param or VoteMessage$)
36    let choices: Vec<String> = if let Some(choices_str) = sa.ir.choices.as_deref() {
37        choices_str
38            .split(',')
39            .map(|s| s.trim().to_string())
40            .collect()
41    } else if let Some(msg) = sa.ir.vote_message_text.as_deref() {
42        // Parse choice names from message — usually "A or B"
43        msg.split(" or ").map(|s| s.trim().to_string()).collect()
44    } else {
45        return;
46    };
47
48    if choices.is_empty() {
49        return;
50    }
51
52    // Collect votes
53    let mut vote_counts: HashMap<String, Vec<PlayerId>> = HashMap::new();
54    for choice in &choices {
55        vote_counts.insert(choice.clone(), Vec::new());
56    }
57
58    for &voter in &voters {
59        if ctx.game.player(voter).has_lost {
60            continue;
61        }
62
63        // Ask each player to vote
64        ctx.agents[voter.index()].snapshot_state(ctx.game, ctx.mana_pools);
65        let chosen = ctx.agents[voter.index()].confirm_action(
66            voter,
67            Some("Vote"),
68            &format!("Vote: {}", choices.join(" or ")),
69            &choices,
70            sa.source,
71            sa.api,
72        );
73
74        // confirm_action returns bool — map to first or second choice
75        let choice_idx = if chosen { 0 } else { 1.min(choices.len() - 1) };
76        let chosen_str = &choices[choice_idx];
77
78        if let Some(voters_list) = vote_counts.get_mut(chosen_str) {
79            voters_list.push(voter);
80        }
81    }
82
83    // Determine winner(s) — most votes
84    let max_votes = vote_counts.values().map(|v| v.len()).max().unwrap_or(0);
85    let winners: Vec<String> = vote_counts
86        .iter()
87        .filter(|(_, v)| v.len() == max_votes)
88        .map(|(k, _)| k.clone())
89        .collect();
90
91    let all_votes = vote_counts
92        .iter()
93        .map(|(choice, voters)| (choice.clone(), voters.clone()))
94        .collect();
95    ctx.trigger_handler.run_trigger(
96        TriggerType::Vote,
97        RunParams {
98            all_votes: Some(all_votes),
99            ..Default::default()
100        },
101        false,
102    );
103
104    // Store vote results for sub-abilities
105    if sa.param_is_true(keys::STORE_VOTE_NUM) {
106        if let Some(source_id) = sa.source {
107            for (choice, voters_list) in &vote_counts {
108                let svar_name = format!("VoteNum{}", choice);
109                let svar_val = format!("Number${}", voters_list.len());
110                ctx.game.card_mut(source_id).set_s_var(svar_name, svar_val);
111            }
112        }
113    }
114
115    // RememberVotedObjects$
116    if sa.param_is_true(keys::REMEMBER_VOTED_OBJECTS) {
117        // Remember the winning choice indices (simplified)
118        if let Some(source_id) = sa.source {
119            for winner in &winners {
120                if let Some(idx) = choices.iter().position(|c| c == winner) {
121                    ctx.game.card_mut(source_id).add_remembered_cmc(idx as i32);
122                }
123            }
124        }
125    }
126
127    // The winning sub-ability is resolved by the parent SA's sub-ability chain.
128    // In Java, VoteSubAbility or the Choice abilities are resolved here.
129    // In Rust, the sub-ability system handles this via the spell resolution pipeline.
130}