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}