moonpool_explorer/simulations/
adaptive.rs1use std::cell::Cell;
12use std::fmt;
13
14use crate::{AdaptiveConfig, ExplorationConfig};
15
16#[derive(Debug)]
18pub enum AdaptiveTestError {
19 Init(std::io::Error),
21 StatsUnavailable,
23 NoForks {
25 total: u64,
27 },
28 NoForkPoints {
30 points: u64,
32 },
33 EnergyExceeded {
35 total: u64,
37 limit: u64,
39 },
40 EnergyNegative {
42 energy: i64,
44 },
45 PoolNegative {
47 pool: i64,
49 },
50}
51
52impl fmt::Display for AdaptiveTestError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::Init(e) => write!(f, "init failed: {e}"),
56 Self::StatsUnavailable => write!(f, "stats unavailable"),
57 Self::NoForks { total } => {
58 write!(f, "expected forked children, got total_timelines={total}")
59 }
60 Self::NoForkPoints { points } => {
61 write!(f, "expected fork points, got fork_points={points}")
62 }
63 Self::EnergyExceeded { total, limit } => {
64 write!(
65 f,
66 "energy limit exceeded: total_timelines={total} (expected <= {limit})"
67 )
68 }
69 Self::EnergyNegative { energy } => {
70 write!(f, "energy went negative: global_energy={energy}")
71 }
72 Self::PoolNegative { pool } => {
73 write!(f, "realloc pool went negative: realloc_pool={pool}")
74 }
75 }
76 }
77}
78
79impl std::error::Error for AdaptiveTestError {
80 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81 match self {
82 Self::Init(e) => Some(e),
83 _ => None,
84 }
85 }
86}
87
88impl From<std::io::Error> for AdaptiveTestError {
89 fn from(e: std::io::Error) -> Self {
90 Self::Init(e)
91 }
92}
93
94thread_local! {
99 pub static RNG_STATE: Cell<u64> = const { Cell::new(1) };
101 pub static CALL_COUNT: Cell<u64> = const { Cell::new(0) };
103}
104
105pub fn count() -> u64 {
107 CALL_COUNT.with(|c| c.get())
108}
109
110pub fn reseed(seed: u64) {
112 RNG_STATE.with(|c| c.set(if seed == 0 { 1 } else { seed }));
114 CALL_COUNT.with(|c| c.set(0));
115}
116
117pub fn next_random() -> u64 {
119 CALL_COUNT.with(|c| c.set(c.get() + 1));
120 RNG_STATE.with(|c| {
121 let mut s = c.get();
122 s ^= s << 13;
123 s ^= s >> 7;
124 s ^= s << 17;
125 c.set(s);
126 s
127 })
128}
129
130pub fn random_below(divisor: u32) -> u32 {
132 (next_random() % divisor as u64) as u32
133}
134
135pub fn run_adaptive_maze_cascade() -> Result<(), AdaptiveTestError> {
149 crate::set_rng_hooks(count, reseed);
150 reseed(42);
151
152 crate::init(ExplorationConfig {
153 max_depth: 8,
154 timelines_per_split: 4,
155 global_energy: 150,
156 adaptive: Some(AdaptiveConfig {
157 batch_size: 4,
158 min_timelines: 4,
159 max_timelines: 20,
160 per_mark_energy: 15,
161 warm_min_timelines: None,
162 }),
163 parallelism: None,
164 })?;
165
166 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_entry");
168
169 let g0a = random_below(10) < 3;
171 if g0a {
172 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_g0a");
173 let g0b = random_below(10) < 3;
174 if g0b {
175 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_lock0");
176
177 let g1a = random_below(10) < 3;
179 if g1a {
180 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_g1a");
181 let g1b = random_below(10) < 3;
182 if g1b {
183 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_lock1");
184
185 let g2a = random_below(10) < 3;
187 if g2a {
188 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_g2a");
189 let g2b = random_below(10) < 3;
190 if g2b {
191 crate::assertion_bool(
192 crate::AssertKind::Sometimes,
193 true,
194 true,
195 "maze_lock2",
196 );
197
198 if crate::explorer_is_child() {
200 crate::exit_child(42);
201 }
202 }
203 }
204 }
205 }
206 }
207 }
208
209 if crate::explorer_is_child() {
211 crate::exit_child(0);
212 }
213
214 let stats = crate::exploration_stats().ok_or(AdaptiveTestError::StatsUnavailable)?;
216 crate::cleanup();
217
218 if stats.total_timelines == 0 {
219 return Err(AdaptiveTestError::NoForks {
220 total: stats.total_timelines,
221 });
222 }
223 if stats.fork_points == 0 {
224 return Err(AdaptiveTestError::NoForkPoints {
225 points: stats.fork_points,
226 });
227 }
228
229 Ok(())
230}
231
232pub fn run_adaptive_dungeon_floors() -> Result<(), AdaptiveTestError> {
245 crate::set_rng_hooks(count, reseed);
246 reseed(7777);
247
248 crate::init(ExplorationConfig {
249 max_depth: 7,
250 timelines_per_split: 4,
251 global_energy: 200,
252 adaptive: Some(AdaptiveConfig {
253 batch_size: 4,
254 min_timelines: 4,
255 max_timelines: 25,
256 per_mark_energy: 20,
257 warm_min_timelines: None,
258 }),
259 parallelism: None,
260 })?;
261
262 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "dungeon_entry");
264
265 let mut floors_cleared = 0u32;
266
267 for floor in 0..5 {
268 let has_key = random_below(5) == 0;
270 if !has_key {
271 break;
272 }
273
274 let name = match floor {
276 0 => "dungeon_f0",
277 1 => "dungeon_f1",
278 2 => "dungeon_f2",
279 3 => "dungeon_f3",
280 4 => "dungeon_f4",
281 _ => break,
282 };
283 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, name);
284 floors_cleared = floor + 1;
285 }
286
287 if floors_cleared == 5 && crate::explorer_is_child() {
289 crate::exit_child(42);
290 }
291
292 if crate::explorer_is_child() {
293 crate::exit_child(0);
294 }
295
296 let stats = crate::exploration_stats().ok_or(AdaptiveTestError::StatsUnavailable)?;
297 crate::cleanup();
298
299 if stats.total_timelines == 0 {
300 return Err(AdaptiveTestError::NoForks {
301 total: stats.total_timelines,
302 });
303 }
304 if stats.fork_points == 0 {
305 return Err(AdaptiveTestError::NoForkPoints {
306 points: stats.fork_points,
307 });
308 }
309
310 Ok(())
311}
312
313pub fn run_adaptive_energy_budget() -> Result<(), AdaptiveTestError> {
323 crate::set_rng_hooks(count, reseed);
324 reseed(99);
325
326 crate::init(ExplorationConfig {
327 max_depth: 3,
328 timelines_per_split: 4,
329 global_energy: 8,
330 adaptive: Some(AdaptiveConfig {
331 batch_size: 2,
332 min_timelines: 2,
333 max_timelines: 6,
334 per_mark_energy: 3,
335 warm_min_timelines: None,
336 }),
337 parallelism: None,
338 })?;
339
340 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "energy_a");
342 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "energy_b");
343 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "energy_c");
344
345 if crate::explorer_is_child() {
346 crate::exit_child(0);
347 }
348
349 let stats = crate::exploration_stats().ok_or(AdaptiveTestError::StatsUnavailable)?;
350 crate::cleanup();
351
352 if stats.total_timelines > 8 {
353 return Err(AdaptiveTestError::EnergyExceeded {
354 total: stats.total_timelines,
355 limit: 8,
356 });
357 }
358 if stats.global_energy < 0 {
359 return Err(AdaptiveTestError::EnergyNegative {
360 energy: stats.global_energy,
361 });
362 }
363 if stats.realloc_pool_remaining < 0 {
364 return Err(AdaptiveTestError::PoolNegative {
365 pool: stats.realloc_pool_remaining,
366 });
367 }
368
369 Ok(())
370}