Skip to main content

manabrew_engine/replacement/
replace_add_counter.rs

1//! Replacement logic for `Event$ AddCounter`.
2//!
3//! Mirrors Java `ReplaceAddCounter.java` in `forge/game/replacement/`.
4
5use crate::agent::GameEntity;
6use crate::card::valid_filter;
7use crate::card::Card;
8use crate::game::GameState;
9use crate::ids::CardId;
10use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
11
12use super::replacement_effect::ReplacementEffect;
13use super::replacement_handler::ReplacementEvent;
14use super::replacement_result::ReplacementResult;
15use super::replacement_type::ReplacementType;
16use crate::card_trait_base::CardTrait;
17
18/// Check if the effect has a `ValidCounterType$` that matches the given counter type.
19///
20/// If no `ValidCounterType$` param is present, the effect applies to any counter
21/// type, so return `true`. Otherwise, check if the given counter type name
22/// matches the param value.
23///
24/// Mirrors Java `ReplaceAddCounter.hasAnyInCounterMap()`.
25pub fn has_any_in_counter_map(effect: &ReplacementEffect, counter_type: Option<&str>) -> bool {
26    match effect.ir.valid_counter_type_text.as_deref() {
27        None => true, // No restriction — matches any counter type
28        Some(valid) => match counter_type {
29            None => false, // Effect requires a specific type but none given
30            Some(ct) => valid.split(',').any(|v| v.trim().eq_ignore_ascii_case(ct)),
31        },
32    }
33}
34
35/// Check if this effect's event type matches the given event for AddCounter purposes.
36///
37/// Returns `true` if event is `AddCounter`, or if event is `Moved` and the
38/// effect handles counter-on-move (has a `CounterMap` interaction).
39///
40/// Mirrors Java `ReplaceAddCounter.modeCheck()`.
41pub fn mode_check(effect: &ReplacementEffect, event: &ReplacementType) -> bool {
42    match event {
43        ReplacementType::AddCounter => true,
44        ReplacementType::Moved => effect.ir.counter_map,
45        _ => false,
46    }
47}
48
49/// Mirrors Java `ReplaceAddCounter.canReplace()`.
50pub fn can_replace(
51    effect: &ReplacementEffect,
52    event: &ReplacementEvent,
53    game: &GameState,
54    source_card: &Card,
55) -> bool {
56    if effect.event != ReplacementType::AddCounter {
57        return false;
58    }
59    let (target, counter_map, cause, is_effect, etb) = match event {
60        ReplacementEvent::AddCounter {
61            target,
62            counter_map,
63            cause,
64            is_effect,
65            ..
66        } => (*target, counter_map, cause.as_deref(), *is_effect, false),
67        ReplacementEvent::Moved {
68            card,
69            destination: ZoneType::Battlefield,
70            counter_map: Some(counter_map),
71            counter_cause,
72            counter_is_effect,
73            ..
74        } => (
75            GameEntity::Card(*card),
76            counter_map,
77            counter_cause.as_deref(),
78            *counter_is_effect,
79            true,
80        ),
81        _ => return false,
82    };
83    if effect.ir.effect_only && !is_effect {
84        return false;
85    }
86    if let Some(valid) = effect.ir.valid_card_selector.as_ref() {
87        let GameEntity::Card(target) = target else {
88            return false;
89        };
90        let mut target_card = game.card(target).clone();
91        if etb {
92            target_card.zone = ZoneType::Battlefield;
93        }
94        if !effect.matches_compiled_valid_card(valid, &target_card, source_card) {
95            return false;
96        }
97    }
98    if let Some(valid) = effect.ir.valid_player_selector.as_ref() {
99        let GameEntity::Player(target) = target else {
100            return false;
101        };
102        if !effect.matches_compiled_valid_player(valid, target, source_card) {
103            return false;
104        }
105    }
106    if let Some(valid) = effect.ir.valid_object_selector.as_ref() {
107        let matches = match target {
108            GameEntity::Card(target) => {
109                let mut target_card = game.card(target).clone();
110                if etb {
111                    target_card.zone = ZoneType::Battlefield;
112                }
113                effect.matches_compiled_valid_card(valid, &target_card, source_card)
114            }
115            GameEntity::Player(target) => {
116                effect.matches_compiled_valid_player(valid, target, source_card)
117            }
118        };
119        if !matches {
120            return false;
121        }
122    }
123    if etb
124        && effect.base().host_card_id()
125            == match target {
126                GameEntity::Card(card) => card,
127                GameEntity::Player(_) => return false,
128            }
129        && !effect
130            .ir
131            .valid_card_text
132            .as_deref()
133            .is_some_and(|valid| valid.starts_with("Card.Self"))
134    {
135        return false;
136    }
137    if let Some(valid) = effect.ir.valid_cause_text.as_deref() {
138        let Some(cause) = cause else {
139            return false;
140        };
141        let ability_host = cause.source.map(|card| game.card(card));
142        if !crate::spellability::matches_valid_sa(valid, cause, source_card, ability_host) {
143            return false;
144        }
145    }
146    if !counter_map.iter().any(|entry| {
147        let source_matches = effect
148            .ir
149            .valid_source_selector
150            .as_ref()
151            .is_none_or(|valid| {
152                entry.source.is_some_and(|source| {
153                    valid_filter::matches_valid_player_selector(
154                        valid,
155                        source,
156                        source_card.controller,
157                    )
158                })
159            });
160        let type_matches = effect
161            .ir
162            .valid_counter_type_text
163            .as_deref()
164            .is_none_or(|valid| {
165                let expected = crate::ability::effects::parse_counter_type(valid);
166                entry.counters.contains_key(&expected)
167            });
168        source_matches && type_matches
169    }) {
170        return false;
171    }
172    true
173}
174
175/// Mirrors Java `ReplacementHandler.executeReplacement()` for AddCounter.
176pub fn execute(
177    effect: &ReplacementEffect,
178    event: &mut ReplacementEvent,
179    _game: &mut GameState,
180    _source_card_id: CardId,
181    agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
182) -> ReplacementResult {
183    let etb = matches!(event, ReplacementEvent::Moved { .. });
184    let (target, cause, is_effect, counter_map, after_replacement_static_abilities) = match event {
185        ReplacementEvent::AddCounter {
186            target,
187            cause,
188            is_effect,
189            counter_map,
190            after_replacement_static_abilities,
191            ..
192        } => (
193            *target,
194            cause.clone(),
195            *is_effect,
196            counter_map,
197            after_replacement_static_abilities,
198        ),
199        ReplacementEvent::Moved {
200            card,
201            counter_map: Some(counter_map),
202            counter_cause,
203            counter_is_effect,
204            after_replacement_static_abilities,
205            ..
206        } => (
207            GameEntity::Card(*card),
208            counter_cause.clone(),
209            *counter_is_effect,
210            counter_map,
211            after_replacement_static_abilities,
212        ),
213        _ => return ReplacementResult::NotReplaced,
214    };
215    if let Some(replace) = effect.replace_with() {
216        match replace {
217            "AddOneMoreCounter" | "AddOneMoreCounters" => {
218                update_matching_counters(
219                    effect,
220                    counter_map,
221                    _game.card(_source_card_id).controller,
222                    |amount| amount + 1,
223                );
224                return ReplacementResult::Updated;
225            }
226            "AddTwiceCounters" | "DoubleCounters" => {
227                update_matching_counters(
228                    effect,
229                    counter_map,
230                    _game.card(_source_card_id).controller,
231                    |amount| amount * 2,
232                );
233                return ReplacementResult::Updated;
234            }
235            _ => {
236                let original = counter_map.clone();
237                let selected_sources =
238                    selected_counter_sources(effect, &original, target, _game, agents);
239                for entry in original {
240                    for (counter_type, amount) in entry.counters {
241                        if !entry_matches(
242                            effect,
243                            entry.source,
244                            &counter_type,
245                            _game.card(_source_card_id).controller,
246                        ) {
247                            continue;
248                        }
249                        if selected_sources.iter().any(|(selected_type, source)| {
250                            *selected_type == counter_type && *source != entry.source
251                        }) {
252                            continue;
253                        }
254                        let mut single = ReplacementEvent::AddCounter {
255                            target,
256                            source: entry.source,
257                            counter_type: counter_type.clone(),
258                            count: amount,
259                            counter_map: Vec::new(),
260                            after_replacement_static_abilities: Vec::new(),
261                            cause: cause.clone(),
262                            is_effect,
263                        };
264                        if super::replacement_handler::execute_replace_with_numeric_update(
265                            effect,
266                            &mut single,
267                            _game,
268                            _source_card_id,
269                            "CounterNum",
270                        )
271                        .is_some()
272                        {
273                            let ReplacementEvent::AddCounter { count, .. } = single else {
274                                unreachable!();
275                            };
276                            if let Some(target_entry) = counter_map
277                                .iter_mut()
278                                .find(|target_entry| target_entry.source == entry.source)
279                            {
280                                target_entry.counters.insert(counter_type, count);
281                            }
282                        }
283                    }
284                }
285                if let Some(
286                    crate::replacement::replacement_effect::ReplacementChainIr::ReplaceCounter {
287                        after_replacement_static_abilities: static_abilities,
288                        ..
289                    },
290                ) = crate::replacement::replacement_effect::resolve_replace_with_chain(
291                    effect,
292                    _game.card(_source_card_id),
293                ) {
294                    if !static_abilities.is_empty() {
295                        after_replacement_static_abilities
296                            .push((_source_card_id, static_abilities));
297                    }
298                }
299                return ReplacementResult::Updated;
300            }
301        }
302    }
303    if etb {
304        counter_map.clear();
305        return ReplacementResult::Updated;
306    }
307    ReplacementResult::Replaced
308}
309
310fn selected_counter_sources(
311    effect: &ReplacementEffect,
312    counter_map: &[super::replacement_handler::CounterMapValue],
313    target: GameEntity,
314    game: &GameState,
315    mut agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
316) -> Vec<(crate::card::CounterType, Option<crate::ids::PlayerId>)> {
317    let choose_counter = matches!(
318        crate::replacement::replacement_effect::resolve_replace_with_chain(
319            effect,
320            game.card(effect.base().host_card_id()),
321        ),
322        Some(
323            crate::replacement::replacement_effect::ReplacementChainIr::ReplaceCounter {
324                choose_counter: true,
325                ..
326            }
327        )
328    );
329    if !choose_counter || counter_map.len() <= 1 {
330        return Vec::new();
331    }
332    let chooser = match target {
333        GameEntity::Card(card) => game.card(card).controller,
334        GameEntity::Player(player) => player,
335    };
336    let mut counter_types = Vec::new();
337    for entry in counter_map {
338        for counter_type in entry.counters.keys() {
339            if !counter_types.contains(counter_type) {
340                counter_types.push(counter_type.clone());
341            }
342        }
343    }
344    counter_types
345        .into_iter()
346        .filter_map(|counter_type| {
347            let sources: Vec<_> = counter_map
348                .iter()
349                .filter(|entry| entry.counters.contains_key(&counter_type))
350                .filter_map(|entry| entry.source)
351                .collect();
352            let source = if let Some(agents) = agents.as_deref_mut() {
353                agents[chooser.index()].choose_target_player(chooser, &sources, None)
354            } else {
355                sources.first().copied()
356            };
357            source.map(|source| (counter_type, Some(source)))
358        })
359        .collect()
360}
361
362pub(crate) fn apply_after_replacement_static_abilities(
363    game: &mut GameState,
364    source_card_id: CardId,
365    after_replacement_static_abilities: Vec<String>,
366) {
367    let source = game.card(source_card_id).clone();
368    let mut effect_card = Card::new(
369        CardId(0),
370        format!("{}'s Effect", source.card_name),
371        source.controller,
372        CardTypeLine::parse("Effect"),
373        ManaCost::parse("0"),
374        ColorSet::COLORLESS,
375        None,
376        None,
377        vec![],
378        vec![],
379    );
380    effect_card.set_controller(source.controller);
381    effect_card.set_effect_source(Some(source_card_id));
382    effect_card.set_temp_effect_until_eot(true);
383    for raw in after_replacement_static_abilities {
384        if let Some(mut static_ability) =
385            crate::staticability::parse_static_ability(&format!("S$ {raw}"))
386        {
387            static_ability.ir.active_zones = vec![ZoneType::Command];
388            static_ability.ir.has_zone_keys = true;
389            effect_card.add_static_ability(static_ability);
390        }
391    }
392    for (name, value) in source.svars {
393        effect_card.set_s_var_if_absent(name, value);
394    }
395    let effect_id = game.create_card(effect_card);
396    game.move_card(effect_id, ZoneType::Command, source.controller);
397}
398
399fn entry_matches(
400    effect: &ReplacementEffect,
401    source: Option<crate::ids::PlayerId>,
402    counter_type: &crate::card::CounterType,
403    source_controller: crate::ids::PlayerId,
404) -> bool {
405    let source_matches = effect
406        .ir
407        .valid_source_selector
408        .as_ref()
409        .is_none_or(|valid| {
410            source.is_some_and(|source| {
411                valid_filter::matches_valid_player_selector(valid, source, source_controller)
412            })
413        });
414    let type_matches = effect
415        .ir
416        .valid_counter_type_text
417        .as_deref()
418        .is_none_or(|valid| crate::ability::effects::parse_counter_type(valid) == *counter_type);
419    source_matches && type_matches
420}
421
422fn update_matching_counters(
423    effect: &ReplacementEffect,
424    counter_map: &mut [super::replacement_handler::CounterMapValue],
425    source_controller: crate::ids::PlayerId,
426    update: impl Fn(i32) -> i32,
427) {
428    for entry in counter_map {
429        for (counter_type, amount) in &mut entry.counters {
430            if entry_matches(effect, entry.source, counter_type, source_controller) {
431                *amount = update(*amount).max(0);
432            }
433        }
434    }
435}