Skip to main content

amble_engine/
scheduler.rs

1//! Event Scheduler
2//!
3//! Simple future one-off or recurring events can be accomplished using flags and their associated
4//! "turnstamps" (turn on which they were set.) This system will allow for more complicated series
5//! of events scheduled at arbitrary times in the future. Because it is owned by `AmbleWorld`, all
6//! scheduled events should persist correctly across saves.
7//!
8//! ### Designer Note:
9//! This implementation as a priority queue using a binary heap is almost certainly overkill for most
10//! likely use cases of this engine. (To be honest, I'm largely using it just to gain experience using
11//! the `std::collections::BinaryHeap`). If problematic, a simpler Vec with a filter or partition on
12//! turn due would be sufficient.
13
14use std::cmp::Reverse;
15use std::collections::BinaryHeap;
16
17use log::info;
18use serde::{Deserialize, Serialize};
19
20use crate::trigger::{ScriptedAction, TriggerCondition};
21
22#[cfg(test)]
23const PLACEHOLDER_THRESHOLD: usize = 4;
24#[cfg(not(test))]
25const PLACEHOLDER_THRESHOLD: usize = 64;
26
27/// The event scheduler.
28///
29/// Uses a reversed binary heap to maintain a priority queue for upcoming events.
30#[derive(Debug, Clone, Serialize, Deserialize, Default)]
31pub struct Scheduler {
32    pub heap: BinaryHeap<Reverse<(usize, usize)>>, /* (turn_due, event_idx) */
33    pub events: Vec<ScheduledEvent>,
34}
35impl Scheduler {
36    /// Schedule some `TriggerActions` to fire a specified number of turns in the future.
37    pub fn schedule_in(&mut self, now: usize, turns_ahead: usize, actions: Vec<ScriptedAction>, note: Option<String>) {
38        let idx = self.events.len();
39        let on_turn = now + turns_ahead;
40        let log_msg = match &note {
41            Some(msg) => msg.as_str(),
42            None => "<no note provided>",
43        };
44        info!("scheduling event (turn now/due = {now}/{on_turn}): \"{log_msg}\"");
45        self.heap.push(Reverse((on_turn, idx)));
46        self.events.push(ScheduledEvent {
47            on_turn,
48            actions,
49            note,
50            condition: None,
51            on_false: OnFalsePolicy::Cancel,
52        });
53    }
54
55    /// Schedule some `TriggerActions` to fire on a specific turn.
56    pub fn schedule_on(&mut self, on_turn: usize, actions: Vec<ScriptedAction>, note: Option<String>) {
57        let idx = self.events.len();
58        let log_msg = match &note {
59            Some(note) => note.as_str(),
60            None => "<no note provided>",
61        };
62        info!("scheduling event (turn due = {on_turn}): \"{log_msg}\"");
63        self.heap.push(Reverse((on_turn, idx)));
64        self.events.push(ScheduledEvent {
65            on_turn,
66            actions,
67            note,
68            condition: None,
69            on_false: OnFalsePolicy::Cancel,
70        });
71    }
72
73    /// Schedule actions in the future with an optional condition and on-false policy.
74    ///
75    /// Primarily used by conditional scheduling trigger actions; events that fail
76    /// the condition at execution time can be retried according to `on_false`.
77    pub fn schedule_in_if(
78        &mut self,
79        now: usize,
80        turns_ahead: usize,
81        condition: Option<EventCondition>,
82        on_false: OnFalsePolicy,
83        actions: Vec<ScriptedAction>,
84        note: Option<String>,
85    ) {
86        let idx = self.events.len();
87        let on_turn = now + turns_ahead;
88        let log_msg = match &note {
89            Some(msg) => msg.as_str(),
90            None => "<no note provided>",
91        };
92        info!("scheduling conditional event (turn now/due = {now}/{on_turn}): \"{log_msg}\"");
93        self.heap.push(Reverse((on_turn, idx)));
94        self.events.push(ScheduledEvent {
95            on_turn,
96            actions,
97            note,
98            condition,
99            on_false,
100        });
101    }
102
103    /// Schedule actions on a specific turn with an optional condition and on-false policy.
104    pub fn schedule_on_if(
105        &mut self,
106        on_turn: usize,
107        condition: Option<EventCondition>,
108        on_false: OnFalsePolicy,
109        actions: Vec<ScriptedAction>,
110        note: Option<String>,
111    ) {
112        let idx = self.events.len();
113        let log_msg = match &note {
114            Some(note) => note.as_str(),
115            None => "<no note provided>",
116        };
117        info!("scheduling conditional event (turn due = {on_turn}): \"{log_msg}\"");
118        self.heap.push(Reverse((on_turn, idx)));
119        self.events.push(ScheduledEvent {
120            on_turn,
121            actions,
122            note,
123            condition,
124            on_false,
125        });
126    }
127
128    /// Pop the next due event, if any.
129    ///
130    /// Returns `None` when the earliest scheduled event is still in the future.
131    pub fn pop_due(&mut self, now: usize) -> Option<ScheduledEvent> {
132        if let Some(Reverse((turn_due, idx))) = self.heap.peek().copied()
133            && now >= turn_due
134        {
135            self.heap.pop();
136            // "take" instead of "remove" keeps indices stable for the heap entries
137            // leaves default placeholders
138            let event = std::mem::take(&mut self.events[idx]);
139            self.compact_if_needed();
140            return Some(event);
141        }
142        None
143    }
144
145    /// Rebuild the underlying storage when too many placeholder tombstones accumulate.
146    fn compact_if_needed(&mut self) {
147        let placeholder_count = self.events.iter().filter(|e| e.is_placeholder()).count();
148        if placeholder_count > PLACEHOLDER_THRESHOLD {
149            let old_events = std::mem::take(&mut self.events);
150            let mut index_map = vec![0; old_events.len()];
151            for (old_idx, event) in old_events.into_iter().enumerate() {
152                if event.is_placeholder() {
153                    continue;
154                }
155                let new_idx = self.events.len();
156                index_map[old_idx] = new_idx;
157                self.events.push(event);
158            }
159            let mut new_heap = BinaryHeap::with_capacity(self.heap.len());
160            while let Some(Reverse((turn_due, old_idx))) = self.heap.pop() {
161                let new_idx = index_map[old_idx];
162                new_heap.push(Reverse((turn_due, new_idx)));
163            }
164            self.heap = new_heap;
165        }
166    }
167}
168
169/// An event (sequence of `TriggerActions`) scheduled for a particular turn.
170///
171/// ### Fields:
172/// `on_turn` = turn on which to fire
173/// actions = list of `TriggerActions` to take when the turn arrives
174/// note = description of event (for logging)
175#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
176pub struct ScheduledEvent {
177    pub on_turn: usize,
178    pub actions: Vec<ScriptedAction>,
179    pub note: Option<String>,
180    /// Optional condition that must be true for the event to fire.
181    pub condition: Option<EventCondition>,
182    /// Policy to apply when the condition evaluates to false.
183    pub on_false: OnFalsePolicy,
184}
185
186impl ScheduledEvent {
187    /// Placeholder events mark consumed slots within the scheduler. Determine whether this is one of them.
188    fn is_placeholder(&self) -> bool {
189        *self == ScheduledEvent::default()
190    }
191}
192
193/// Policy controlling behavior when a scheduled event's condition is false.
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
195pub enum OnFalsePolicy {
196    /// Cancel the event and do not retry.
197    #[default]
198    Cancel,
199    /// Retry after the specified number of turns.
200    RetryAfter(usize),
201    /// Retry on the next turn.
202    RetryNextTurn,
203}
204
205/// Condition for scheduled events. Can wrap a `TriggerCondition` or combine multiple.
206#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
207pub enum EventCondition {
208    /// Single trigger condition; evaluated via existing machinery.
209    Trigger(TriggerCondition),
210    /// All subconditions must be true.
211    All(Vec<EventCondition>),
212    /// Any subcondition must be true.
213    Any(Vec<EventCondition>),
214}
215
216impl EventCondition {
217    /// Evaluate the condition against the current world state and any recent events.
218    pub fn eval_with_events(&self, world: &crate::world::AmbleWorld, events: &[TriggerCondition]) -> bool {
219        match self {
220            EventCondition::Trigger(tc) => tc.matches_event_in(events) || tc.is_ongoing(world),
221            EventCondition::All(conds) => conds.iter().all(|c| c.eval_with_events(world, events)),
222            EventCondition::Any(conds) => conds.iter().any(|c| c.eval_with_events(world, events)),
223        }
224    }
225
226    /// Evaluate the condition against the current world state.
227    pub fn eval(&self, world: &crate::world::AmbleWorld) -> bool {
228        self.eval_with_events(world, &[])
229    }
230
231    /// Determine whether the condition contains an Ambient that applies to the current
232    /// player location.
233    pub fn eval_ambient(&self, world: &crate::world::AmbleWorld) -> bool {
234        match self {
235            EventCondition::Trigger(tc) => match tc {
236                TriggerCondition::Ambient { room_ids, .. } => world
237                    .player
238                    .location
239                    .room_id()
240                    .is_ok_and(|room_id| room_ids.is_empty() || room_ids.contains(&room_id)),
241                _ => tc.is_ongoing(world),
242            },
243            EventCondition::All(conds) => conds.iter().all(|c| c.eval_ambient(world)),
244            EventCondition::Any(conds) => conds.iter().any(|c| c.eval_ambient(world)),
245        }
246    }
247
248    /// Returns true if any nested trigger condition satisfies the matcher.
249    pub fn any_trigger<F>(&self, matcher: F) -> bool
250    where
251        F: FnMut(&TriggerCondition) -> bool,
252    {
253        let mut matcher = matcher;
254        self.any_trigger_inner(&mut matcher)
255    }
256
257    /// Apply a visitor closure to every nested trigger condition.
258    pub fn for_each_condition<F>(&self, visitor: F)
259    where
260        F: FnMut(&TriggerCondition),
261    {
262        let mut visitor = visitor;
263        self.for_each_trigger_inner(&mut visitor);
264    }
265
266    fn any_trigger_inner<F>(&self, matcher: &mut F) -> bool
267    where
268        F: FnMut(&TriggerCondition) -> bool,
269    {
270        match self {
271            EventCondition::Trigger(tc) => matcher(tc),
272            EventCondition::All(conds) | EventCondition::Any(conds) => {
273                conds.iter().any(|c| c.any_trigger_inner(matcher))
274            },
275        }
276    }
277
278    fn for_each_trigger_inner<F>(&self, visitor: &mut F)
279    where
280        F: FnMut(&TriggerCondition),
281    {
282        match self {
283            EventCondition::Trigger(tc) => visitor(tc),
284            EventCondition::All(conds) | EventCondition::Any(conds) => {
285                for cond in conds {
286                    cond.for_each_trigger_inner(visitor);
287                }
288            },
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::trigger::TriggerAction;
297
298    fn create_test_action() -> ScriptedAction {
299        ScriptedAction::new(TriggerAction::ShowMessage("Test message".to_string()))
300    }
301
302    fn create_test_actions(count: usize) -> Vec<ScriptedAction> {
303        (0..count)
304            .map(|i| ScriptedAction::new(TriggerAction::ShowMessage(format!("Message {i}"))))
305            .collect()
306    }
307
308    #[test]
309    fn scheduler_new_is_empty() {
310        let scheduler = Scheduler::default();
311        assert!(scheduler.heap.is_empty());
312        assert!(scheduler.events.is_empty());
313    }
314
315    #[test]
316    fn schedule_in_adds_event_correctly() {
317        let mut scheduler = Scheduler::default();
318        let actions = vec![create_test_action()];
319        let note = Some("Test event".to_string());
320
321        scheduler.schedule_in(5, 3, actions.clone(), note.clone());
322
323        assert_eq!(scheduler.events.len(), 1);
324        assert_eq!(scheduler.heap.len(), 1);
325
326        let event = &scheduler.events[0];
327        assert_eq!(event.on_turn, 8); // 5 + 3
328        assert_eq!(event.actions.len(), 1);
329        assert_eq!(event.note, note);
330    }
331
332    #[test]
333    fn schedule_on_adds_event_correctly() {
334        let mut scheduler = Scheduler::default();
335        let actions = vec![create_test_action()];
336        let note = Some("Direct schedule test".to_string());
337
338        scheduler.schedule_on(10, actions.clone(), note.clone());
339
340        assert_eq!(scheduler.events.len(), 1);
341        assert_eq!(scheduler.heap.len(), 1);
342
343        let event = &scheduler.events[0];
344        assert_eq!(event.on_turn, 10);
345        assert_eq!(event.actions.len(), 1);
346        assert_eq!(event.note, note);
347    }
348
349    #[test]
350    fn schedule_multiple_events() {
351        let mut scheduler = Scheduler::default();
352
353        scheduler.schedule_in(0, 5, vec![create_test_action()], Some("Event 1".to_string()));
354        scheduler.schedule_in(0, 3, vec![create_test_action()], Some("Event 2".to_string()));
355        scheduler.schedule_on(10, vec![create_test_action()], Some("Event 3".to_string()));
356
357        assert_eq!(scheduler.events.len(), 3);
358        assert_eq!(scheduler.heap.len(), 3);
359    }
360
361    #[test]
362    fn pop_due_returns_none_when_nothing_due() {
363        let mut scheduler = Scheduler::default();
364        scheduler.schedule_in(5, 5, vec![create_test_action()], None);
365
366        let result = scheduler.pop_due(8); // Event due on turn 10
367        assert!(result.is_none());
368        assert_eq!(scheduler.heap.len(), 1); // Event should still be in heap
369    }
370
371    #[test]
372    fn pop_due_returns_event_when_due() {
373        let mut scheduler = Scheduler::default();
374        let actions = vec![create_test_action()];
375        let note = Some("Due event".to_string());
376
377        scheduler.schedule_in(5, 3, actions.clone(), note.clone());
378
379        let result = scheduler.pop_due(8); // Event due exactly on turn 8
380        assert!(result.is_some());
381
382        let event = result.unwrap();
383        assert_eq!(event.on_turn, 8);
384        assert_eq!(event.note, note);
385        assert_eq!(event.actions.len(), 1);
386
387        // Heap should now be empty
388        assert!(scheduler.heap.is_empty());
389    }
390
391    #[test]
392    fn pop_due_returns_event_when_overdue() {
393        let mut scheduler = Scheduler::default();
394        scheduler.schedule_in(5, 3, vec![create_test_action()], Some("Overdue event".to_string()));
395
396        let result = scheduler.pop_due(10); // Event was due on turn 8, now turn 10
397        assert!(result.is_some());
398
399        let event = result.unwrap();
400        assert_eq!(event.on_turn, 8);
401    }
402
403    #[test]
404    fn events_fire_in_correct_order() {
405        let mut scheduler = Scheduler::default();
406
407        // Schedule events in reverse chronological order
408        scheduler.schedule_on(15, create_test_actions(1), Some("Third".to_string()));
409        scheduler.schedule_on(5, create_test_actions(1), Some("First".to_string()));
410        scheduler.schedule_on(10, create_test_actions(1), Some("Second".to_string()));
411
412        // Pop events in chronological order
413        let first = scheduler.pop_due(5).unwrap();
414        assert_eq!(first.note, Some("First".to_string()));
415        assert_eq!(first.on_turn, 5);
416
417        let second = scheduler.pop_due(10).unwrap();
418        assert_eq!(second.note, Some("Second".to_string()));
419        assert_eq!(second.on_turn, 10);
420
421        let third = scheduler.pop_due(15).unwrap();
422        assert_eq!(third.note, Some("Third".to_string()));
423        assert_eq!(third.on_turn, 15);
424
425        // Nothing left
426        assert!(scheduler.pop_due(20).is_none());
427    }
428
429    #[test]
430    fn events_with_same_turn_fire_in_fifo_order() {
431        let mut scheduler = Scheduler::default();
432
433        // Schedule multiple events for the same turn
434        scheduler.schedule_on(10, create_test_actions(1), Some("First scheduled".to_string()));
435        scheduler.schedule_on(10, create_test_actions(1), Some("Second scheduled".to_string()));
436        scheduler.schedule_on(10, create_test_actions(1), Some("Third scheduled".to_string()));
437
438        // They should come out in FIFO order (first scheduled, first fired)
439        let first = scheduler.pop_due(10).unwrap();
440        assert_eq!(first.note, Some("First scheduled".to_string()));
441
442        let second = scheduler.pop_due(10).unwrap();
443        assert_eq!(second.note, Some("Second scheduled".to_string()));
444
445        let third = scheduler.pop_due(10).unwrap();
446        assert_eq!(third.note, Some("Third scheduled".to_string()));
447    }
448
449    #[test]
450    fn pop_due_multiple_events_same_turn() {
451        let mut scheduler = Scheduler::default();
452
453        scheduler.schedule_on(5, create_test_actions(1), Some("Event A".to_string()));
454        scheduler.schedule_on(5, create_test_actions(1), Some("Event B".to_string()));
455        scheduler.schedule_on(10, create_test_actions(1), Some("Event C".to_string()));
456
457        // Pop all events due on turn 5
458        let mut events_turn_5 = Vec::new();
459        while let Some(event) = scheduler.pop_due(5) {
460            if event.on_turn == 5 {
461                events_turn_5.push(event);
462            } else {
463                break;
464            }
465        }
466
467        assert_eq!(events_turn_5.len(), 2);
468        assert!(events_turn_5.iter().any(|e| e.note == Some("Event A".to_string())));
469        assert!(events_turn_5.iter().any(|e| e.note == Some("Event B".to_string())));
470
471        // Event C should still be in scheduler
472        let event_c = scheduler.pop_due(10).unwrap();
473        assert_eq!(event_c.note, Some("Event C".to_string()));
474    }
475
476    #[test]
477    fn schedule_with_no_note() {
478        let mut scheduler = Scheduler::default();
479        scheduler.schedule_in(0, 5, vec![create_test_action()], None);
480
481        let event = scheduler.pop_due(5).unwrap();
482        assert_eq!(event.note, None);
483    }
484
485    #[test]
486    fn schedule_with_empty_actions() {
487        let mut scheduler = Scheduler::default();
488        scheduler.schedule_in(0, 5, vec![], Some("Empty actions".to_string()));
489
490        let event = scheduler.pop_due(5).unwrap();
491        assert!(event.actions.is_empty());
492        assert_eq!(event.note, Some("Empty actions".to_string()));
493    }
494
495    #[test]
496    fn schedule_with_multiple_actions() {
497        let mut scheduler = Scheduler::default();
498        let actions = create_test_actions(5);
499
500        scheduler.schedule_in(0, 3, actions.clone(), Some("Multi-action event".to_string()));
501
502        let event = scheduler.pop_due(3).unwrap();
503        assert_eq!(event.actions.len(), 5);
504    }
505
506    #[test]
507    fn scheduled_event_default() {
508        let event = ScheduledEvent::default();
509        assert_eq!(event.on_turn, 0);
510        assert!(event.actions.is_empty());
511        assert_eq!(event.note, None);
512    }
513
514    #[test]
515    fn mem_take_leaves_default_placeholder() {
516        let mut scheduler = Scheduler::default();
517        scheduler.schedule_in(0, 5, vec![create_test_action()], Some("Test".to_string()));
518
519        let _event = scheduler.pop_due(5).unwrap();
520
521        // The event vector should still have the placeholder
522        assert_eq!(scheduler.events.len(), 1);
523        let placeholder = &scheduler.events[0];
524        assert_eq!(placeholder.on_turn, 0);
525        assert!(placeholder.actions.is_empty());
526        assert_eq!(placeholder.note, None);
527    }
528
529    #[test]
530    fn compact_events_when_placeholder_threshold_exceeded() {
531        let mut scheduler = Scheduler::default();
532
533        for i in 1..=6 {
534            scheduler.schedule_on(i, create_test_actions(1), Some(format!("Event {i}")));
535        }
536
537        for turn in 1..=5 {
538            let ev = scheduler.pop_due(turn).unwrap();
539            assert_eq!(ev.note, Some(format!("Event {turn}")));
540        }
541
542        assert_eq!(scheduler.events.len(), 1);
543        assert_eq!(scheduler.heap.len(), 1);
544        assert_eq!(scheduler.events[0].note, Some("Event 6".to_string()));
545
546        let final_event = scheduler.pop_due(6).unwrap();
547        assert_eq!(final_event.note, Some("Event 6".to_string()));
548    }
549
550    #[test]
551    fn edge_case_turn_zero() {
552        let mut scheduler = Scheduler::default();
553        scheduler.schedule_on(0, vec![create_test_action()], Some("Turn zero".to_string()));
554
555        let event = scheduler.pop_due(0).unwrap();
556        assert_eq!(event.on_turn, 0);
557    }
558
559    #[test]
560    fn edge_case_large_turn_numbers() {
561        let mut scheduler = Scheduler::default();
562        let large_turn = usize::MAX - 1000;
563
564        scheduler.schedule_on(large_turn, vec![create_test_action()], Some("Large turn".to_string()));
565
566        let event = scheduler.pop_due(large_turn).unwrap();
567        assert_eq!(event.on_turn, large_turn);
568    }
569
570    #[test]
571    fn serialization_roundtrip() {
572        let mut scheduler = Scheduler::default();
573        scheduler.schedule_in(5, 10, create_test_actions(3), Some("Serialization test".to_string()));
574        scheduler.schedule_on(20, create_test_actions(2), None);
575
576        // Serialize
577        let serialized = serde_json::to_string(&scheduler).expect("Failed to serialize");
578
579        // Deserialize
580        let deserialized: Scheduler = serde_json::from_str(&serialized).expect("Failed to deserialize");
581
582        // Verify structure is preserved
583        assert_eq!(deserialized.events.len(), scheduler.events.len());
584        assert_eq!(deserialized.heap.len(), scheduler.heap.len());
585
586        // Verify functionality is preserved
587        let mut des_scheduler = deserialized;
588        let event1 = des_scheduler.pop_due(15).unwrap();
589        assert_eq!(event1.on_turn, 15);
590        assert_eq!(event1.actions.len(), 3);
591
592        let event2 = des_scheduler.pop_due(20).unwrap();
593        assert_eq!(event2.on_turn, 20);
594        assert_eq!(event2.actions.len(), 2);
595        assert_eq!(event2.note, None);
596    }
597}