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
105#[must_use]
107pub fn count() -> u64 {
108 CALL_COUNT.with(std::cell::Cell::get)
109}
110
111pub fn reseed(seed: u64) {
113 RNG_STATE.with(|c| c.set(if seed == 0 { 1 } else { seed }));
115 CALL_COUNT.with(|c| c.set(0));
116}
117
118#[must_use]
120pub fn next_random() -> u64 {
121 CALL_COUNT.with(|c| c.set(c.get() + 1));
122 RNG_STATE.with(|c| {
123 let mut s = c.get();
124 s ^= s << 13;
125 s ^= s >> 7;
126 s ^= s << 17;
127 c.set(s);
128 s
129 })
130}
131
132#[must_use]
134pub fn random_below(divisor: u32) -> u32 {
135 u32::try_from(next_random() % u64::from(divisor)).unwrap_or(0)
138}
139
140pub fn run_adaptive_maze_cascade() -> Result<(), AdaptiveTestError> {
159 crate::set_rng_hooks(count, reseed);
160 reseed(42);
161
162 crate::init(&ExplorationConfig {
163 max_depth: 8,
164 timelines_per_split: 4,
165 global_energy: 150,
166 adaptive: Some(AdaptiveConfig {
167 batch_size: 4,
168 min_timelines: 4,
169 max_timelines: 20,
170 per_mark_energy: 15,
171 warm_min_timelines: None,
172 }),
173 parallelism: None,
174 })?;
175
176 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_entry");
178
179 let g0a = random_below(10) < 3;
181 if g0a {
182 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_g0a");
183 let g0b = random_below(10) < 3;
184 if g0b {
185 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_lock0");
186
187 let g1a = random_below(10) < 3;
189 if g1a {
190 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_g1a");
191 let g1b = random_below(10) < 3;
192 if g1b {
193 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_lock1");
194
195 let g2a = random_below(10) < 3;
197 if g2a {
198 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "maze_g2a");
199 let g2b = random_below(10) < 3;
200 if g2b {
201 crate::assertion_bool(
202 crate::AssertKind::Sometimes,
203 true,
204 true,
205 "maze_lock2",
206 );
207
208 if crate::explorer_is_child() {
210 crate::exit_child(42);
211 }
212 }
213 }
214 }
215 }
216 }
217 }
218
219 if crate::explorer_is_child() {
221 crate::exit_child(0);
222 }
223
224 let stats = crate::exploration_stats().ok_or(AdaptiveTestError::StatsUnavailable)?;
226 crate::cleanup();
227
228 if stats.total_timelines == 0 {
229 return Err(AdaptiveTestError::NoForks {
230 total: stats.total_timelines,
231 });
232 }
233 if stats.fork_points == 0 {
234 return Err(AdaptiveTestError::NoForkPoints {
235 points: stats.fork_points,
236 });
237 }
238
239 Ok(())
240}
241
242pub fn run_adaptive_dungeon_floors() -> Result<(), AdaptiveTestError> {
260 crate::set_rng_hooks(count, reseed);
261 reseed(7777);
262
263 crate::init(&ExplorationConfig {
264 max_depth: 7,
265 timelines_per_split: 4,
266 global_energy: 200,
267 adaptive: Some(AdaptiveConfig {
268 batch_size: 4,
269 min_timelines: 4,
270 max_timelines: 25,
271 per_mark_energy: 20,
272 warm_min_timelines: None,
273 }),
274 parallelism: None,
275 })?;
276
277 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "dungeon_entry");
279
280 let mut floors_cleared = 0u32;
281
282 for floor in 0..5 {
283 let has_key = random_below(5) == 0;
285 if !has_key {
286 break;
287 }
288
289 let name = match floor {
291 0 => "dungeon_f0",
292 1 => "dungeon_f1",
293 2 => "dungeon_f2",
294 3 => "dungeon_f3",
295 4 => "dungeon_f4",
296 _ => break,
297 };
298 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, name);
299 floors_cleared = floor + 1;
300 }
301
302 if floors_cleared == 5 && crate::explorer_is_child() {
304 crate::exit_child(42);
305 }
306
307 if crate::explorer_is_child() {
308 crate::exit_child(0);
309 }
310
311 let stats = crate::exploration_stats().ok_or(AdaptiveTestError::StatsUnavailable)?;
312 crate::cleanup();
313
314 if stats.total_timelines == 0 {
315 return Err(AdaptiveTestError::NoForks {
316 total: stats.total_timelines,
317 });
318 }
319 if stats.fork_points == 0 {
320 return Err(AdaptiveTestError::NoForkPoints {
321 points: stats.fork_points,
322 });
323 }
324
325 Ok(())
326}
327
328pub fn run_adaptive_energy_budget() -> Result<(), AdaptiveTestError> {
344 crate::set_rng_hooks(count, reseed);
345 reseed(99);
346
347 crate::init(&ExplorationConfig {
348 max_depth: 3,
349 timelines_per_split: 4,
350 global_energy: 8,
351 adaptive: Some(AdaptiveConfig {
352 batch_size: 2,
353 min_timelines: 2,
354 max_timelines: 6,
355 per_mark_energy: 3,
356 warm_min_timelines: None,
357 }),
358 parallelism: None,
359 })?;
360
361 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "energy_a");
363 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "energy_b");
364 crate::assertion_bool(crate::AssertKind::Sometimes, true, true, "energy_c");
365
366 if crate::explorer_is_child() {
367 crate::exit_child(0);
368 }
369
370 let stats = crate::exploration_stats().ok_or(AdaptiveTestError::StatsUnavailable)?;
371 crate::cleanup();
372
373 if stats.total_timelines > 8 {
374 return Err(AdaptiveTestError::EnergyExceeded {
375 total: stats.total_timelines,
376 limit: 8,
377 });
378 }
379 if stats.global_energy < 0 {
380 return Err(AdaptiveTestError::EnergyNegative {
381 energy: stats.global_energy,
382 });
383 }
384 if stats.realloc_pool_remaining < 0 {
385 return Err(AdaptiveTestError::PoolNegative {
386 pool: stats.realloc_pool_remaining,
387 });
388 }
389
390 Ok(())
391}