use rand::{Rng, SeedableRng, rngs::StdRng};
use crate::env::{
Environment, SpaceInfo, SpaceType, StepResult,
games::cartpole::{CartPole, CartPoleState},
};
pub const DEFAULT_FLICKER_PROBABILITY: f64 = 0.5;
pub const DEFAULT_BURST_LEN: f64 = 4.0;
#[derive(Debug, Clone)]
pub struct FlickeringCartPoleState {
inner: CartPoleState,
flickered: bool,
rng: StdRng,
}
#[derive(Debug)]
pub struct FlickeringCartPole {
inner: CartPole,
flicker_prob: f64,
rng: StdRng,
flickered: bool,
burst_len: Option<f64>,
}
impl FlickeringCartPole {
pub fn new() -> Self {
Self::with_probability(DEFAULT_FLICKER_PROBABILITY)
}
pub fn with_probability(flicker_prob: f64) -> Self {
assert!(
(0.0..=1.0).contains(&flicker_prob),
"flicker probability must be in [0, 1], got {flicker_prob}"
);
Self {
inner: CartPole::new(),
flicker_prob,
rng: StdRng::from_os_rng(),
flickered: false,
burst_len: None,
}
}
pub fn with_seed(seed: u64) -> Self {
Self::with_seed_and_probability(seed, DEFAULT_FLICKER_PROBABILITY)
}
pub fn with_seed_and_probability(seed: u64, flicker_prob: f64) -> Self {
assert!(
(0.0..=1.0).contains(&flicker_prob),
"flicker probability must be in [0, 1], got {flicker_prob}"
);
Self {
inner: CartPole::new(),
flicker_prob,
rng: StdRng::seed_from_u64(seed),
flickered: false,
burst_len: None,
}
}
pub fn with_seed_probability_and_burst(seed: u64, flicker_prob: f64, burst_len: f64) -> Self {
assert!(
flicker_prob > 0.0 && flicker_prob < 1.0,
"burst mode requires flicker probability in (0, 1), got {flicker_prob}"
);
assert!(burst_len >= 1.0, "burst length must be >= 1.0, got {burst_len}");
let max_p = burst_len / (burst_len + 1.0);
assert!(
flicker_prob <= max_p,
"burst mode: flicker_prob {flicker_prob} exceeds the achievable maximum \
burst_len/(burst_len+1) = {max_p:.4} for burst_len {burst_len}; \
the mean visible-run length would be < 1 frame. \
Either reduce flicker_prob or increase burst_len."
);
Self {
inner: CartPole::new(),
flicker_prob,
rng: StdRng::seed_from_u64(seed),
flickered: false,
burst_len: Some(burst_len),
}
}
pub fn flicker_probability(&self) -> f64 {
self.flicker_prob
}
pub fn is_flickered(&self) -> bool {
self.flickered
}
pub fn burst_length(&self) -> Option<f64> {
self.burst_len
}
fn draw_flicker(&mut self) -> bool {
self.rng.random::<f64>() < self.flicker_prob
}
fn advance_flicker(&mut self) -> bool {
match self.burst_len {
None => self.draw_flicker(),
Some(mean_blank_run) => {
let u = self.rng.random::<f64>();
if self.flickered {
let p_switch = (1.0 / mean_blank_run).clamp(0.0, 1.0);
u >= p_switch
} else {
let mean_visible_run =
mean_blank_run * (1.0 - self.flicker_prob) / self.flicker_prob;
let p_switch = (1.0 / mean_visible_run).clamp(0.0, 1.0);
u < p_switch
}
}
}
}
const OBS_DIM: usize = 4;
}
impl Default for FlickeringCartPole {
fn default() -> Self {
Self::new()
}
}
impl Environment for FlickeringCartPole {
type Action = i64;
type State = FlickeringCartPoleState;
fn reset(&mut self) {
self.inner.reset();
self.flickered = self.draw_flicker();
}
fn get_observation(&self) -> Vec<f32> {
if self.flickered {
vec![0.0; Self::OBS_DIM]
} else {
Environment::get_observation(&self.inner)
}
}
fn step(&mut self, action: i64) -> StepResult {
let mut result = self.inner.step(action);
self.flickered = self.advance_flicker();
if self.flickered {
for v in result.observation.iter_mut() {
*v = 0.0;
}
}
result
}
fn observation_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![Self::OBS_DIM], space_type: SpaceType::Box }
}
fn action_space(&self) -> SpaceInfo {
self.inner.action_space()
}
fn render(&self) -> Vec<u8> {
self.inner.render()
}
fn close(&mut self) {
self.inner.close();
}
fn clone_state(&self) -> FlickeringCartPoleState {
FlickeringCartPoleState {
inner: self.inner.clone_state(),
flickered: self.flickered,
rng: self.rng.clone(),
}
}
fn restore_state(&mut self, state: &FlickeringCartPoleState) {
self.inner.restore_state(&state.inner);
self.flickered = state.flickered;
self.rng = state.rng.clone();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_observation_space_is_four_dimensional() {
let env = FlickeringCartPole::new();
let obs_space = env.observation_space();
assert_eq!(obs_space.shape, vec![4], "flickering obs keeps CartPole's 4-D shape");
assert!(matches!(obs_space.space_type, SpaceType::Box));
}
#[test]
fn test_action_space_delegates() {
let env = FlickeringCartPole::new();
let action_space = env.action_space();
assert!(matches!(action_space.space_type, SpaceType::Discrete(2)));
}
#[test]
fn test_default_probability() {
let env = FlickeringCartPole::new();
assert_eq!(env.flicker_probability(), DEFAULT_FLICKER_PROBABILITY);
assert_eq!(env.flicker_probability(), 0.5);
}
#[test]
fn test_observation_length_is_always_four() {
let mut env = FlickeringCartPole::with_seed_and_probability(7, 0.5);
env.reset();
assert_eq!(env.get_observation().len(), 4);
for i in 0..200 {
let result = env.step((i % 2) as i64);
assert_eq!(result.observation.len(), 4, "obs length invariant under flicker");
if result.terminated || result.truncated {
env.reset();
}
}
}
#[test]
fn test_flickered_observation_is_all_zeros() {
let mut env = FlickeringCartPole::with_seed_and_probability(1, 1.0);
env.reset();
assert!(env.is_flickered(), "p=1.0 must blank every frame");
assert_eq!(env.get_observation(), vec![0.0; 4]);
let result = env.step(1);
assert!(env.is_flickered());
assert_eq!(result.observation, vec![0.0; 4], "stepped obs blanked under p=1.0");
}
#[test]
fn test_never_flickers_at_zero_probability() {
let mut env = FlickeringCartPole::with_seed_and_probability(2, 0.0);
env.reset();
assert!(!env.is_flickered(), "p=0.0 must never blank");
for i in 0..300 {
let result = env.step((i % 2) as i64);
assert!(!env.is_flickered(), "p=0.0 must never blank");
assert_eq!(result.observation, Environment::get_observation(&env.inner));
if result.terminated || result.truncated {
env.reset();
}
}
}
#[test]
fn test_flicker_schedule_is_deterministic_under_seed() {
let mut a = FlickeringCartPole::with_seed_and_probability(42, 0.5);
let mut b = FlickeringCartPole::with_seed_and_probability(42, 0.5);
a.reset();
b.reset();
assert_eq!(a.is_flickered(), b.is_flickered(), "reset flicker decision must match");
let mut any_flicker = false;
let mut any_visible = false;
for i in 0..500 {
let action = (i % 2) as i64;
a.step(action);
b.step(action);
assert_eq!(a.is_flickered(), b.is_flickered(), "flicker schedule diverged at step {i}");
any_flicker |= a.is_flickered();
any_visible |= !a.is_flickered();
}
assert!(any_flicker, "expected at least one flickered frame at p=0.5");
assert!(any_visible, "expected at least one visible frame at p=0.5");
}
#[test]
fn test_flicker_rate_is_approximately_p() {
let mut env = FlickeringCartPole::with_seed_and_probability(123, 0.5);
env.reset();
let mut blanked = 0usize;
let n = 5000;
for i in 0..n {
env.step((i % 2) as i64);
if env.is_flickered() {
blanked += 1;
}
if env.get_observation().is_empty() {
unreachable!();
}
}
let rate = blanked as f64 / n as f64;
assert!((rate - 0.5).abs() < 0.05, "blank rate {rate} should be ≈ 0.5");
}
#[test]
fn test_reward_and_done_unaffected_by_flicker() {
let mut env = FlickeringCartPole::with_seed_and_probability(9, 0.5);
env.reset();
for i in 0..100 {
let result = env.step((i % 2) as i64);
assert!(result.reward == 0.0 || result.reward == 1.0, "reward inherited from CartPole");
if result.terminated || result.truncated {
env.reset();
}
}
}
#[test]
fn test_clone_restore_reproduces_flicker_stream() {
let mut env = FlickeringCartPole::with_seed_and_probability(555, 0.5);
env.reset();
for i in 0..10 {
env.step((i % 2) as i64);
}
let snap = env.clone_state();
let mut first = Vec::new();
for i in 0..20 {
let r = env.step((i % 2) as i64);
first.push((env.is_flickered(), r.observation.clone(), r.reward));
}
env.restore_state(&snap);
let mut second = Vec::new();
for i in 0..20 {
let r = env.step((i % 2) as i64);
second.push((env.is_flickered(), r.observation.clone(), r.reward));
}
assert_eq!(first, second, "restore must reproduce flicker + physics stream");
}
#[test]
fn test_hundred_random_steps_no_panic() {
let mut env = FlickeringCartPole::with_seed(0);
env.reset();
for i in 0..100 {
let result = env.step((i % 2) as i64);
assert_eq!(result.observation.len(), 4);
if result.terminated || result.truncated {
env.reset();
}
}
}
#[test]
#[should_panic(expected = "flicker probability must be in [0, 1]")]
fn test_invalid_probability_panics() {
let _ = FlickeringCartPole::with_probability(1.5);
}
fn collect_flicker_stream(env: &mut FlickeringCartPole, n: usize) -> Vec<bool> {
env.reset();
let mut stream = Vec::with_capacity(n);
for i in 0..n {
env.step((i % 2) as i64);
stream.push(env.is_flickered());
}
stream
}
fn mean_blank_run_length(stream: &[bool]) -> f64 {
let mut runs = Vec::new();
let mut cur = 0usize;
for &b in stream {
if b {
cur += 1;
} else if cur > 0 {
runs.push(cur);
cur = 0;
}
}
if cur > 0 {
runs.push(cur);
}
if runs.is_empty() {
0.0
} else {
runs.iter().sum::<usize>() as f64 / runs.len() as f64
}
}
#[test]
fn test_burst_mode_reports_burst_length() {
let env = FlickeringCartPole::with_seed_probability_and_burst(1, 0.5, 4.0);
assert_eq!(env.burst_length(), Some(4.0));
assert_eq!(FlickeringCartPole::new().burst_length(), None);
}
#[test]
fn test_burst_observation_shape_invariant() {
let mut env = FlickeringCartPole::with_seed_probability_and_burst(7, 0.5, 4.0);
assert_eq!(env.observation_space().shape, vec![4]);
env.reset();
assert_eq!(env.get_observation().len(), 4);
for i in 0..200 {
let r = env.step((i % 2) as i64);
assert_eq!(r.observation.len(), 4);
if r.terminated || r.truncated {
env.reset();
}
}
}
#[test]
fn test_burst_mode_blank_rate_matches_p() {
for &p in &[0.3_f64, 0.5, 0.7, 0.8] {
let mut env = FlickeringCartPole::with_seed_probability_and_burst(123, p, 4.0);
let stream = collect_flicker_stream(&mut env, 20000);
let rate = stream.iter().filter(|&&b| b).count() as f64 / stream.len() as f64;
assert!(
(rate - p).abs() < 0.05,
"burst blank rate {rate} should track p={p} (same as i.i.d.)"
);
}
}
#[test]
fn test_burst_mode_produces_longer_runs_than_iid() {
let mut burst = FlickeringCartPole::with_seed_probability_and_burst(99, 0.5, 4.0);
let mut iid = FlickeringCartPole::with_seed_and_probability(99, 0.5);
let burst_run = mean_blank_run_length(&collect_flicker_stream(&mut burst, 20000));
let iid_run = mean_blank_run_length(&collect_flicker_stream(&mut iid, 20000));
assert!(iid_run < 2.5, "i.i.d. mean blank run {iid_run} should be ~2");
assert!(
(burst_run - 4.0).abs() < 1.0,
"burst mean blank run {burst_run} should be ≈ 4.0"
);
assert!(burst_run > iid_run + 1.0, "burst runs must be longer than i.i.d. runs");
}
#[test]
fn test_burst_schedule_is_deterministic_under_seed() {
let mut a = FlickeringCartPole::with_seed_probability_and_burst(42, 0.5, 4.0);
let mut b = FlickeringCartPole::with_seed_probability_and_burst(42, 0.5, 4.0);
let sa = collect_flicker_stream(&mut a, 2000);
let sb = collect_flicker_stream(&mut b, 2000);
assert_eq!(sa, sb, "burst schedule must be identical under the same seed");
}
#[test]
fn test_burst_clone_restore_reproduces_stream() {
let mut env = FlickeringCartPole::with_seed_probability_and_burst(555, 0.5, 4.0);
env.reset();
for i in 0..10 {
env.step((i % 2) as i64);
}
let snap = env.clone_state();
let mut first = Vec::new();
for i in 0..40 {
env.step((i % 2) as i64);
first.push(env.is_flickered());
}
env.restore_state(&snap);
let mut second = Vec::new();
for i in 0..40 {
env.step((i % 2) as i64);
second.push(env.is_flickered());
}
assert_eq!(first, second, "restore must reproduce the burst stream");
}
#[test]
#[should_panic(expected = "burst mode requires flicker probability in (0, 1)")]
fn test_burst_invalid_probability_panics() {
let _ = FlickeringCartPole::with_seed_probability_and_burst(0, 0.0, 4.0);
}
#[test]
#[should_panic(expected = "burst length must be >= 1.0")]
fn test_burst_invalid_length_panics() {
let _ = FlickeringCartPole::with_seed_probability_and_burst(0, 0.5, 0.5);
}
#[test]
#[should_panic(expected = "burst mode: flicker_prob")]
fn test_burst_prob_above_achievable_max_panics() {
let _ = FlickeringCartPole::with_seed_probability_and_burst(0, 0.9, 4.0);
}
#[test]
fn test_burst_prob_at_achievable_max_does_not_panic() {
let env = FlickeringCartPole::with_seed_probability_and_burst(0, 0.8, 4.0);
assert_eq!(env.flicker_probability(), 0.8);
assert_eq!(env.burst_length(), Some(4.0));
}
}