fortress_rollback/input_queue/prediction.rs
1//! Input prediction strategies for rollback networking.
2//!
3//! This module contains the [`PredictionStrategy`] trait and built-in implementations
4//! for predicting player inputs when actual inputs haven't arrived yet.
5//!
6//! # Determinism Requirement
7//!
8//! **CRITICAL**: All prediction strategies MUST be deterministic across all peers.
9//! Both peers must produce the exact same predicted input given the same arguments.
10//! If predictions differ between peers, they will desync during rollback.
11//!
12//! # Built-in Strategies
13//!
14//! - [`RepeatLastConfirmed`]: Repeats the last confirmed input (default)
15//! - [`BlankPrediction`]: Always returns the default (blank) input
16//!
17//! # Custom Strategies
18//!
19//! You can implement custom prediction strategies for game-specific behavior:
20//!
21//! ```ignore
22//! use fortress_rollback::{Frame, PredictionStrategy};
23//!
24//! struct MyPrediction;
25//!
26//! impl<I: Copy + Default> PredictionStrategy<I> for MyPrediction {
27//! fn predict(&self, frame: Frame, last_confirmed_input: Option<I>, _player_index: usize) -> I {
28//! // For a fighting game, you might predict "hold block" as a safe default
29//! // This MUST be deterministic - don't use random values or timing-dependent data!
30//! last_confirmed_input.unwrap_or_default()
31//! }
32//! }
33//! ```
34
35use crate::Frame;
36
37/// Defines the strategy used to predict inputs when we haven't received the actual input yet.
38///
39/// Input prediction is crucial for rollback networking - when we need to advance the game
40/// but haven't received a remote player's input, we must predict what they will do.
41///
42/// # Determinism Requirement
43///
44/// **CRITICAL**: The prediction strategy MUST be deterministic across all peers.
45/// Both peers must produce the exact same predicted input given the same arguments.
46/// If predictions differ between peers, they will desync during rollback.
47///
48/// The default implementation (`RepeatLastConfirmed`) is deterministic because:
49/// - `last_confirmed_input` is synchronized across all peers via the network protocol
50/// - Both peers will have received and confirmed the same input before using it for prediction
51///
52/// # Custom Strategies
53///
54/// You can implement custom prediction strategies for game-specific behavior:
55///
56/// ```ignore
57/// struct MyPrediction;
58///
59/// impl<I: Copy + Default> PredictionStrategy<I> for MyPrediction {
60/// fn predict(&self, frame: Frame, last_confirmed_input: Option<I>, _player_index: usize) -> I {
61/// // For a fighting game, you might predict "hold block" as a safe default
62/// // This MUST be deterministic - don't use random values or timing-dependent data!
63/// last_confirmed_input.unwrap_or_default()
64/// }
65/// }
66/// ```
67pub trait PredictionStrategy<I: Copy + Default>: Send + Sync {
68 /// Predicts the input for a player when their actual input hasn't arrived yet.
69 ///
70 /// # Arguments
71 ///
72 /// * `frame` - The frame number we're predicting for
73 /// * `last_confirmed_input` - The most recent confirmed input from this player, if any.
74 /// This is deterministic across all peers since confirmed inputs are synchronized.
75 /// * `player_index` - The index of the player we're predicting for
76 ///
77 /// # Returns
78 ///
79 /// The predicted input to use. Must be deterministic across all peers.
80 fn predict(&self, frame: Frame, last_confirmed_input: Option<I>, player_index: usize) -> I;
81}
82
83/// The default prediction strategy: repeat the last confirmed input.
84///
85/// This strategy is deterministic because `last_confirmed_input` is guaranteed
86/// to be the same across all peers after synchronization.
87///
88/// If there is no confirmed input yet (e.g., at the start of the game),
89/// this returns the default input value.
90#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
91pub struct RepeatLastConfirmed;
92
93impl std::fmt::Display for RepeatLastConfirmed {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(f, "RepeatLastConfirmed")
96 }
97}
98
99impl<I: Copy + Default> PredictionStrategy<I> for RepeatLastConfirmed {
100 fn predict(&self, _frame: Frame, last_confirmed_input: Option<I>, _player_index: usize) -> I {
101 last_confirmed_input.unwrap_or_default()
102 }
103}
104
105/// A prediction strategy that always returns the default (blank) input.
106///
107/// This is useful when you want a "do nothing" prediction, which can be
108/// safer for some game types where repeating the last input could be dangerous.
109#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
110pub struct BlankPrediction;
111
112impl std::fmt::Display for BlankPrediction {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 write!(f, "BlankPrediction")
115 }
116}
117
118impl<I: Copy + Default> PredictionStrategy<I> for BlankPrediction {
119 fn predict(&self, _frame: Frame, _last_confirmed: Option<I>, _player_index: usize) -> I {
120 I::default()
121 }
122}
123
124#[cfg(test)]
125#[allow(
126 clippy::panic,
127 clippy::unwrap_used,
128 clippy::expect_used,
129 clippy::indexing_slicing
130)]
131mod tests {
132 use super::*;
133 use serde::{Deserialize, Serialize};
134
135 #[repr(C)]
136 #[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, Debug)]
137 struct TestInput {
138 inp: u8,
139 }
140
141 #[test]
142 fn test_blank_prediction_strategy() {
143 let strategy = BlankPrediction;
144
145 // Should always return default regardless of last confirmed
146 let result: TestInput = strategy.predict(Frame::new(0), Some(TestInput { inp: 42 }), 0);
147 assert_eq!(result, TestInput::default());
148
149 let result: TestInput = strategy.predict(Frame::new(10), None, 1);
150 assert_eq!(result, TestInput::default());
151 }
152
153 #[test]
154 fn test_repeat_last_confirmed_strategy() {
155 let strategy = RepeatLastConfirmed;
156
157 // Should return last confirmed input when available
158 let result: TestInput = strategy.predict(Frame::new(5), Some(TestInput { inp: 99 }), 0);
159 assert_eq!(result.inp, 99);
160
161 // Should return default when no last confirmed
162 let result: TestInput = strategy.predict(Frame::new(0), None, 0);
163 assert_eq!(result, TestInput::default());
164 }
165
166 #[test]
167 fn test_prediction_strategy_player_index_ignored_by_default() {
168 // Both default strategies ignore player_index
169 let repeat = RepeatLastConfirmed;
170 let blank = BlankPrediction;
171
172 let last_input = Some(TestInput { inp: 42 });
173
174 // Same result regardless of player_index
175 for player_idx in 0..10 {
176 let repeat_result: TestInput = repeat.predict(Frame::new(5), last_input, player_idx);
177 assert_eq!(repeat_result.inp, 42);
178
179 let blank_result: TestInput = blank.predict(Frame::new(5), last_input, player_idx);
180 assert_eq!(blank_result, TestInput::default());
181 }
182 }
183
184 #[test]
185 fn test_repeat_last_confirmed_debug() {
186 let strategy = RepeatLastConfirmed;
187 let debug_str = format!("{:?}", strategy);
188 assert!(debug_str.contains("RepeatLastConfirmed"));
189 }
190
191 #[test]
192 fn test_blank_prediction_debug() {
193 let strategy = BlankPrediction;
194 let debug_str = format!("{:?}", strategy);
195 assert!(debug_str.contains("BlankPrediction"));
196 }
197
198 #[test]
199 fn test_repeat_last_confirmed_display() {
200 assert_eq!(RepeatLastConfirmed.to_string(), "RepeatLastConfirmed");
201 }
202
203 #[test]
204 fn test_blank_prediction_display() {
205 assert_eq!(BlankPrediction.to_string(), "BlankPrediction");
206 }
207
208 #[test]
209 fn test_repeat_last_confirmed_copy() {
210 let a = RepeatLastConfirmed;
211 let b = a; // Copy
212 let debug_a = format!("{:?}", a);
213 let debug_b = format!("{:?}", b);
214 assert_eq!(debug_a, debug_b);
215 }
216
217 #[test]
218 fn test_blank_prediction_copy() {
219 let a = BlankPrediction;
220 let b = a; // Copy
221 let debug_a = format!("{:?}", a);
222 let debug_b = format!("{:?}", b);
223 assert_eq!(debug_a, debug_b);
224 }
225}