Skip to main content

manabrew_engine/
game_entity_counter_table.rs

1use std::collections::BTreeMap;
2
3use crate::agent::{GameEntity, PlayerAgent};
4use crate::card::CounterType;
5use crate::event::{CounterTableEntry, RunParams};
6use crate::game::GameState;
7use crate::ids::PlayerId;
8use crate::replacement::replacement_handler::{
9    apply_replacements, apply_replacements_with_agents, CounterMapValue, ReplacementEvent,
10};
11use crate::replacement::ReplacementResult;
12use crate::spellability::SpellAbility;
13use crate::trigger::{TriggerHandler, TriggerType};
14
15#[derive(Debug, Clone)]
16struct CounterCell {
17    source: Option<PlayerId>,
18    object: GameEntity,
19    counters: BTreeMap<CounterType, i32>,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct GameEntityCounterTable {
24    cells: Vec<CounterCell>,
25}
26
27impl GameEntityCounterTable {
28    pub fn from_counter_map(
29        object: GameEntity,
30        counter_map: Vec<CounterMapValue>,
31    ) -> GameEntityCounterTable {
32        let mut table = GameEntityCounterTable::default();
33        for entry in counter_map {
34            for (counter_type, amount) in entry.counters {
35                table.put(entry.source, object, counter_type, amount);
36            }
37        }
38        table
39    }
40
41    pub fn put(
42        &mut self,
43        source: Option<PlayerId>,
44        object: GameEntity,
45        counter_type: CounterType,
46        amount: i32,
47    ) {
48        if amount <= 0 {
49            return;
50        }
51        if let Some(cell) = self
52            .cells
53            .iter_mut()
54            .find(|cell| cell.source == source && cell.object == object)
55        {
56            *cell.counters.entry(counter_type).or_default() += amount;
57        } else {
58            self.cells.push(CounterCell {
59                source,
60                object,
61                counters: BTreeMap::from([(counter_type, amount)]),
62            });
63        }
64    }
65
66    pub fn is_empty(&self) -> bool {
67        self.cells.is_empty()
68    }
69
70    fn total_values(&self) -> i32 {
71        self.cells
72            .iter()
73            .flat_map(|cell| cell.counters.values())
74            .sum()
75    }
76
77    pub fn get(
78        &self,
79        source: Option<PlayerId>,
80        object: GameEntity,
81        counter_type: &CounterType,
82    ) -> i32 {
83        self.cells
84            .iter()
85            .find(|cell| cell.source == source && cell.object == object)
86            .and_then(|cell| cell.counters.get(counter_type))
87            .copied()
88            .unwrap_or(0)
89    }
90
91    pub fn counters(game: &GameState, object: GameEntity) -> Vec<(CounterType, i32)> {
92        match object {
93            GameEntity::Card(card) => game
94                .card(card)
95                .counters
96                .iter()
97                .filter(|(_, amount)| **amount > 0)
98                .map(|(counter_type, amount)| (counter_type.clone(), *amount))
99                .collect(),
100            GameEntity::Player(player) => {
101                let player = game.player(player);
102                [
103                    (CounterType::Poison, player.poison_counters),
104                    (
105                        CounterType::Named("ENERGY".to_string()),
106                        player.energy_counters,
107                    ),
108                    (
109                        CounterType::Named("RAD".to_string()),
110                        player.radiation_counters,
111                    ),
112                ]
113                .into_iter()
114                .filter(|(_, amount)| *amount > 0)
115                .collect()
116            }
117        }
118    }
119
120    pub fn replace_counter_effect(
121        &self,
122        game: &mut GameState,
123        trigger_handler: Option<&mut TriggerHandler>,
124        agents: Option<&mut [Box<dyn PlayerAgent>]>,
125        cause: Option<&SpellAbility>,
126        is_effect: bool,
127        params: RunParams,
128    ) -> GameEntityCounterTable {
129        self.apply_counter_effect(
130            game,
131            trigger_handler,
132            agents,
133            cause,
134            is_effect,
135            params,
136            false,
137        )
138    }
139
140    pub fn apply_replaced_counter_effect(
141        &self,
142        game: &mut GameState,
143        trigger_handler: Option<&mut TriggerHandler>,
144        cause: Option<&SpellAbility>,
145        params: RunParams,
146    ) -> GameEntityCounterTable {
147        self.apply_counter_effect(game, trigger_handler, None, cause, true, params, true)
148    }
149
150    fn apply_counter_effect(
151        &self,
152        game: &mut GameState,
153        mut trigger_handler: Option<&mut TriggerHandler>,
154        mut agents: Option<&mut [Box<dyn PlayerAgent>]>,
155        cause: Option<&SpellAbility>,
156        is_effect: bool,
157        params: RunParams,
158        already_replaced: bool,
159    ) -> GameEntityCounterTable {
160        let remember_amount = self.total_values();
161        let mut result = GameEntityCounterTable::default();
162        let mut remembered_objects = Vec::new();
163        let mut objects = Vec::new();
164        for cell in &self.cells {
165            if !objects.contains(&cell.object) {
166                objects.push(cell.object);
167            }
168        }
169
170        for object in objects {
171            let first_time = matches!(object, GameEntity::Card(_))
172                && game.counter_added_this_turn(object, None) == 0;
173            let counter_map: Vec<_> = self
174                .cells
175                .iter()
176                .filter(|cell| cell.object == object)
177                .map(|cell| CounterMapValue {
178                    source: cell.source,
179                    counters: cell.counters.clone(),
180                })
181                .collect();
182            let Some((source, counter_type, count)) = counter_map.iter().find_map(|entry| {
183                entry
184                    .counters
185                    .iter()
186                    .next()
187                    .map(|(counter_type, amount)| (entry.source, counter_type.clone(), *amount))
188            }) else {
189                continue;
190            };
191            let mut event = ReplacementEvent::AddCounter {
192                target: object,
193                source,
194                counter_type,
195                count,
196                counter_map,
197                after_replacement_static_abilities: Vec::new(),
198                cause: cause.cloned().map(Box::new),
199                is_effect,
200            };
201            let replacement_result = if already_replaced {
202                ReplacementResult::NotReplaced
203            } else {
204                match agents.as_deref_mut() {
205                    Some(agents) => apply_replacements_with_agents(game, agents, &mut event),
206                    None => apply_replacements(game, &mut event),
207                }
208            };
209            if matches!(
210                replacement_result,
211                ReplacementResult::NotReplaced | ReplacementResult::Updated
212            ) {
213                let ReplacementEvent::AddCounter {
214                    counter_map,
215                    after_replacement_static_abilities,
216                    ..
217                } = event
218                else {
219                    continue;
220                };
221                for entry in counter_map {
222                    for (counter_type, count) in entry.counters {
223                        let count = match (object, cause.and_then(|cause| cause.ir.max_from_effect))
224                        {
225                            (GameEntity::Card(card), Some(max)) => {
226                                (max - game.card(card).counter_count(&counter_type)).clamp(0, count)
227                            }
228                            _ => count,
229                        };
230                        let added = add_counter_internal(
231                            game,
232                            trigger_handler.as_deref_mut(),
233                            object,
234                            entry.source,
235                            &counter_type,
236                            count,
237                            cause,
238                            params.clone(),
239                        );
240                        if added > 0 {
241                            result.put(entry.source, object, counter_type, added);
242                            if cause.is_some_and(|cause| cause.ir.remember_put)
243                                && !remembered_objects.contains(&object)
244                            {
245                                remembered_objects.push(object);
246                            }
247                        }
248                    }
249                }
250                for (source, static_abilities) in after_replacement_static_abilities {
251                    crate::replacement::replace_add_counter::apply_after_replacement_static_abilities(
252                        game,
253                        source,
254                        static_abilities,
255                    );
256                }
257            }
258            if result.cells.iter().any(|cell| cell.object == object) {
259                if let Some(handler) = trigger_handler.as_deref_mut() {
260                    handler.run_trigger(
261                        TriggerType::CounterTypeAddedAll,
262                        object_params(
263                            object,
264                            RunParams {
265                                first_time: Some(first_time),
266                                ..Default::default()
267                            },
268                        ),
269                        false,
270                    );
271                }
272            }
273        }
274
275        if let Some(source) = cause.and_then(|cause| cause.source) {
276            for object in remembered_objects {
277                match object {
278                    GameEntity::Card(card) => game.card_mut(source).add_remembered_card(card),
279                    GameEntity::Player(player) => {
280                        game.card_mut(source).add_remembered_player(player)
281                    }
282                }
283            }
284            if cause.is_some_and(|cause| cause.ir.remember_amount) && remember_amount > 0 {
285                game.card_mut(source).add_remembered_cmc(remember_amount);
286            }
287        }
288
289        if let Some(handler) = trigger_handler {
290            result.trigger_counters_put_all(handler);
291        }
292        result
293    }
294
295    fn trigger_counters_put_all(&self, trigger_handler: &mut TriggerHandler) {
296        if self.is_empty() {
297            return;
298        }
299        for cell in &self.cells {
300            let counter_map = counter_map(&cell.counters);
301            trigger_handler.run_trigger(
302                TriggerType::CounterPlayerAddedAll,
303                object_params(
304                    cell.object,
305                    RunParams {
306                        source_player: cell.source,
307                        counter_amount: Some(cell.counters.values().sum()),
308                        counter_map: Some(counter_map),
309                        ..Default::default()
310                    },
311                ),
312                false,
313            );
314        }
315        trigger_handler.run_trigger(
316            TriggerType::CounterAddedAll,
317            RunParams {
318                counter_table: Some(self.entries()),
319                ..Default::default()
320            },
321            false,
322        );
323    }
324
325    fn entries(&self) -> Vec<CounterTableEntry> {
326        self.cells
327            .iter()
328            .map(|cell| CounterTableEntry {
329                source: cell.source,
330                object_card: match cell.object {
331                    GameEntity::Card(card) => Some(card),
332                    GameEntity::Player(_) => None,
333                },
334                object_player: match cell.object {
335                    GameEntity::Player(player) => Some(player),
336                    GameEntity::Card(_) => None,
337                },
338                counters: counter_map(&cell.counters),
339            })
340            .collect()
341    }
342}
343
344fn add_counter_internal(
345    game: &mut GameState,
346    trigger_handler: Option<&mut TriggerHandler>,
347    object: GameEntity,
348    source: Option<PlayerId>,
349    counter_type: &CounterType,
350    amount: i32,
351    cause: Option<&SpellAbility>,
352    mut params: RunParams,
353) -> i32 {
354    if amount <= 0 || !can_receive_counter(game, object, counter_type) {
355        return 0;
356    }
357    let old_value = counter_count(game, object, counter_type);
358    let amount = match object {
359        GameEntity::Card(card) => {
360            if let Some(max) = crate::staticability::static_ability_max_counter::max_counter(
361                &game.cards,
362                game.card(card),
363                counter_type,
364            ) {
365                (max - old_value).clamp(0, amount)
366            } else {
367                amount
368            }
369        }
370        GameEntity::Player(_) => amount,
371    };
372    if amount <= 0 {
373        return 0;
374    }
375
376    let first_time = game.counter_added_this_turn(object, Some(counter_type)) == 0;
377    match object {
378        GameEntity::Card(card) => game
379            .card_mut(card)
380            .add_counter_internal(counter_type, amount),
381        GameEntity::Player(player) => add_player_counter(game, player, counter_type, amount),
382    }
383    let new_value = counter_count(game, object, counter_type);
384    let added = new_value - old_value;
385    if added <= 0 {
386        return 0;
387    }
388    game.record_counter_added(object, counter_type, added);
389
390    if let Some(trigger_handler) = trigger_handler {
391        params = object_params(object, params);
392        params.source_player = source;
393        params.cause = cause.cloned();
394        params.counter_type = Some(counter_type.to_string());
395        for counter_amount in (old_value + 1)..=new_value {
396            params.counter_amount = Some(counter_amount);
397            trigger_handler.run_trigger(TriggerType::CounterAdded, params.clone(), false);
398        }
399        params.counter_amount = Some(added);
400        params.first_time = match object {
401            GameEntity::Card(_) => Some(first_time),
402            GameEntity::Player(_) => None,
403        };
404        trigger_handler.run_trigger(TriggerType::CounterAddedOnce, params, false);
405    }
406    added
407}
408
409fn can_receive_counter(game: &GameState, object: GameEntity, counter_type: &CounterType) -> bool {
410    match object {
411        GameEntity::Card(card) => {
412            !game.card(card).phased_out
413                && !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
414                    &game.cards,
415                    game.card(card),
416                    counter_type,
417                )
418        }
419        GameEntity::Player(player) => {
420            !game.player(player).has_lost
421                && !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
422                    &game.cards,
423                    player,
424                    counter_type,
425                )
426        }
427    }
428}
429
430fn counter_count(game: &GameState, object: GameEntity, counter_type: &CounterType) -> i32 {
431    match object {
432        GameEntity::Card(card) => game.card(card).counter_count(counter_type),
433        GameEntity::Player(player) => match counter_type {
434            CounterType::Poison => game.player(player).poison_counters,
435            CounterType::Named(name) if name == "ENERGY" => game.player(player).energy_counters,
436            CounterType::Named(name) if name == "RAD" => game.player(player).radiation_counters,
437            _ => 0,
438        },
439    }
440}
441
442fn add_player_counter(
443    game: &mut GameState,
444    player: PlayerId,
445    counter_type: &CounterType,
446    amount: i32,
447) {
448    match counter_type {
449        CounterType::Poison => game.player_add_poison(player, amount),
450        CounterType::Named(name) if name == "ENERGY" => game.player_add_energy(player, amount),
451        CounterType::Named(name) if name == "RAD" => {
452            game.player_mut(player).radiation_counters += amount;
453        }
454        _ => {}
455    }
456}
457
458fn object_params(object: GameEntity, mut params: RunParams) -> RunParams {
459    match object {
460        GameEntity::Card(card) => {
461            params.card = Some(card);
462            params.object_card = Some(card);
463        }
464        GameEntity::Player(player) => {
465            params.player = Some(player);
466            params.object_player = Some(player);
467        }
468    }
469    params
470}
471
472fn counter_map(counters: &BTreeMap<CounterType, i32>) -> BTreeMap<String, i32> {
473    counters
474        .iter()
475        .map(|(counter_type, amount)| (counter_type.to_string(), *amount))
476        .collect()
477}