Skip to main content

turnbase/
effects.rs

1//! Tier-2 triggered effects: an ordered effect queue.
2//!
3//! A game whose moves resolve as a queue of effects, where applying one effect
4//! can trigger more, implements [`EffectSystem`] and drives resolution with
5//! [`resolve_effects`]. Effects are always appended and resolved in FIFO order,
6//! never invoked inline. That is the Tier-2 boundary from `ARCHITECTURE.md`
7//! (queued enum effects, deterministic order, no player responses); a future
8//! Tier-3 (priority stack, state-based-action rechecking, response windows)
9//! slots around this loop rather than replacing it.
10
11use std::collections::VecDeque;
12
13/// A game that resolves effects through a queue.
14///
15/// The move logic ([`Game::apply`](crate::Game::apply)) builds the initial
16/// effects of a move and calls [`resolve_effects`]; the queue does the rest.
17pub trait EffectSystem {
18    /// The state effects mutate.
19    type State;
20    /// One atomic effect (a game-defined enum).
21    type Effect;
22
23    /// Applies one effect's direct consequence to state.
24    fn apply(&self, state: &mut Self::State, effect: &Self::Effect);
25
26    /// After `effect` is applied, performs state-based actions (deaths,
27    /// removals) and returns the follow-up effects they trigger, in
28    /// deterministic order. Returned effects are appended to the queue and
29    /// resolved later, never inline.
30    fn react(&self, state: &mut Self::State, effect: &Self::Effect) -> Vec<Self::Effect>;
31}
32
33/// Upper bound on effects resolved by one [`resolve_effects`] call, a guard
34/// against a game whose triggers never terminate. A well-formed game resolves
35/// in far fewer steps.
36pub const MAX_EFFECTS: usize = 100_000;
37
38/// Resolves `initial` and everything it triggers, applying each effect then
39/// enqueuing its follow-ups, in FIFO order.
40///
41/// Returns the number of effects resolved. Stops at [`MAX_EFFECTS`] to bound a
42/// non-terminating trigger loop (a game bug).
43pub fn resolve_effects<S: EffectSystem>(
44    system: &S,
45    state: &mut S::State,
46    initial: impl IntoIterator<Item = S::Effect>,
47) -> usize {
48    let mut queue: VecDeque<S::Effect> = initial.into_iter().collect();
49    let mut resolved = 0;
50    while let Some(effect) = queue.pop_front() {
51        system.apply(state, &effect);
52        for follow_up in system.react(state, &effect) {
53            queue.push_back(follow_up);
54        }
55        resolved += 1;
56        if resolved >= MAX_EFFECTS {
57            break;
58        }
59    }
60    resolved
61}
62
63#[cfg(test)]
64mod tests {
65    use super::{EffectSystem, MAX_EFFECTS, resolve_effects};
66
67    /// Knocking domino `i` knocks `i + 1`: a one-at-a-time cascade.
68    struct Dominoes;
69
70    impl EffectSystem for Dominoes {
71        type State = Vec<bool>;
72        type Effect = usize;
73
74        fn apply(&self, state: &mut Self::State, effect: &Self::Effect) {
75            state[*effect] = true;
76        }
77
78        fn react(&self, state: &mut Self::State, effect: &Self::Effect) -> Vec<Self::Effect> {
79            let next = *effect + 1;
80            if next < state.len() && !state[next] {
81                vec![next]
82            } else {
83                Vec::new()
84            }
85        }
86    }
87
88    #[test]
89    fn cascade_resolves_in_order() {
90        let mut state = vec![false; 5];
91        let steps = resolve_effects(&Dominoes, &mut state, [0]);
92        assert!(state.iter().all(|&knocked| knocked));
93        assert_eq!(steps, 5);
94    }
95
96    #[test]
97    fn nonterminating_triggers_are_capped() {
98        struct Forever;
99        impl EffectSystem for Forever {
100            type State = u32;
101            type Effect = ();
102            fn apply(&self, state: &mut u32, _effect: &()) {
103                *state += 1;
104            }
105            fn react(&self, _state: &mut u32, _effect: &()) -> Vec<()> {
106                vec![()]
107            }
108        }
109        let mut count = 0;
110        let steps = resolve_effects(&Forever, &mut count, [()]);
111        assert_eq!(steps, MAX_EFFECTS);
112    }
113}