Deterministic Replay

Probar enables frame-perfect replay of game sessions.

Why Deterministic Replay?

  • Bug Reproduction: Replay exact sequence that caused a bug
  • Regression Testing: Verify behavior matches after changes
  • Test Generation: Record gameplay, convert to tests
  • Demo Playback: Record and replay gameplay sequences

Recording a Session

#![allow(unused)]
fn main() {
use jugar_probar::{Recorder, Recording};

let mut recorder = Recorder::new(seed);
let mut platform = WebPlatform::new_for_test(config);

// Play game and record
for frame in 0..1000 {
    let inputs = get_user_inputs();
    recorder.record_frame(frame, &inputs);

    platform.process_inputs(&inputs);
    platform.advance_frame(1.0 / 60.0);
}

// Get recording
let recording = recorder.finish();

// Save to file
recording.save("gameplay.replay")?;
}

Replaying a Session

#![allow(unused)]
fn main() {
use jugar_probar::{Recording, Replayer};

// Load recording
let recording = Recording::load("gameplay.replay")?;

let mut replayer = Replayer::new(&recording);
let mut platform = WebPlatform::new_for_test(config);

// Replay exactly
while let Some(inputs) = replayer.next_frame() {
    platform.process_inputs(&inputs);
    platform.advance_frame(1.0 / 60.0);
}

// Verify final state matches
assert_eq!(
    replayer.expected_final_hash(),
    platform.state_hash()
);
}

Recording Format

#![allow(unused)]
fn main() {
pub struct Recording {
    pub version: u32,
    pub seed: u64,
    pub config: GameConfig,
    pub frames: Vec<FrameData>,
    pub final_state_hash: u64,
}

pub struct FrameData {
    pub frame_number: u32,
    pub inputs: Vec<InputEvent>,
    pub state_hash: Option<u64>,  // Optional checkpoints
}
}

State Hashing

#![allow(unused)]
fn main() {
// Hash game state for comparison
let hash = platform.state_hash();

// Or hash specific components
let ball_hash = hash_state(&platform.get_ball_state());
let score_hash = hash_state(&platform.get_score());
}

Verification

#![allow(unused)]
fn main() {
use jugar_probar::{verify_replay, ReplayVerification};

let result = verify_replay(&recording);

match result {
    ReplayVerification::Perfect => {
        println!("Replay is deterministic!");
    }
    ReplayVerification::Diverged { frame, expected, actual } => {
        println!("Diverged at frame {}", frame);
        println!("Expected hash: {}", expected);
        println!("Actual hash: {}", actual);
    }
    ReplayVerification::Failed(error) => {
        println!("Replay failed: {}", error);
    }
}
}

Checkpoints

Add checkpoints for faster debugging:

#![allow(unused)]
fn main() {
let mut recorder = Recorder::new(seed)
    .with_checkpoint_interval(60);  // Every 60 frames

// Or manual checkpoints
recorder.add_checkpoint(platform.snapshot());
}

Binary Replay Format

#![allow(unused)]
fn main() {
// Compact binary format for storage
let bytes = recording.to_bytes();
let recording = Recording::from_bytes(&bytes)?;

// Compressed
let compressed = recording.to_compressed_bytes();
let recording = Recording::from_compressed_bytes(&compressed)?;
}

Replay Speed Control

#![allow(unused)]
fn main() {
let mut replayer = Replayer::new(&recording);

// Normal speed
replayer.set_speed(1.0);

// Fast forward
replayer.set_speed(4.0);

// Slow motion
replayer.set_speed(0.25);

// Step by step
replayer.step();  // Advance one frame
}

Example: Test from Replay

#![allow(unused)]
fn main() {
#[test]
fn test_from_recorded_gameplay() {
    let recording = Recording::load("tests/fixtures/win_game.replay").unwrap();

    let mut replayer = Replayer::new(&recording);
    let mut platform = WebPlatform::new_for_test(recording.config.clone());

    // Replay all frames
    while let Some(inputs) = replayer.next_frame() {
        platform.process_inputs(&inputs);
        platform.advance_frame(1.0 / 60.0);
    }

    // Verify end state
    let state = platform.get_game_state();
    assert_eq!(state.winner, Some(Player::Left));
    assert_eq!(state.score_left, 10);
}
}

CI Integration

# Verify all replay files are still deterministic
cargo test replay_verification -- --include-ignored

# Or via make
make verify-replays

Debugging with Replays

#![allow(unused)]
fn main() {
// Find frame where bug occurs
let bug_frame = binary_search_replay(&recording, |state| {
    state.ball.y < 0.0  // Bug condition
});

println!("Bug first occurs at frame {}", bug_frame);

// Get inputs leading up to bug
let inputs = recording.frames[..bug_frame].to_vec();
println!("Inputs: {:?}", inputs);
}