test(simulation): sprint 10 — replay loading, content scaling, serialization v9, observer tests
#483: Replay loading in test-client — JSONL file loading, tick-scheduled PlayerInput sending, 13 unit tests, 3 sample replay files. #500: Content scaling test — baseline + extra NPC comparative, tick budget assertion (D-026), determinism check across content packs. #514: Serialization tests for protocol v9 — blocked_entities roundtrip, backward compat (v5→v9, v8→v9), regenerated msgpack fixtures. Observer perception tests for confrontation + walk-away mechanics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,10 @@ use settled_reach_server::bridge::types::PlayerInput;
|
||||
use std::path::Path;
|
||||
|
||||
/// Load a JSONL replay file. Returns one Vec<PlayerInput> per tick.
|
||||
///
|
||||
/// Format: one JSON array per line. Each array contains PlayerInput objects
|
||||
/// for that tick. Blank lines are skipped. Returns Err with line number on
|
||||
/// parse failure.
|
||||
pub fn load_replay(path: &Path) -> Result<Vec<Vec<PlayerInput>>, String> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("failed to read replay file {}: {}", path.display(), e))?;
|
||||
@@ -22,3 +26,152 @@ pub fn load_replay(path: &Path) -> Result<Vec<Vec<PlayerInput>>, String> {
|
||||
}
|
||||
Ok(ticks)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use settled_reach_server::bridge::types::PlayerAction;
|
||||
use std::io::Write;
|
||||
|
||||
fn write_temp_file(content: &str) -> tempfile::NamedTempFile {
|
||||
let mut f = tempfile::NamedTempFile::new().unwrap();
|
||||
f.write_all(content.as_bytes()).unwrap();
|
||||
f.flush().unwrap();
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_single_tick_single_action() {
|
||||
let f = write_temp_file(r#"[{"tick":0,"action":"MoveNorth"}]"#);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 1);
|
||||
assert_eq!(ticks[0].len(), 1);
|
||||
assert!(ticks[0][0].action.is_movement());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_multiple_ticks() {
|
||||
let content = r#"[{"tick":0,"action":"MoveNorth"}]
|
||||
[{"tick":1,"action":"MoveEast"}]
|
||||
[{"tick":2,"action":"MoveSouth"}]"#;
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_multiple_actions_per_tick() {
|
||||
let content = r#"[{"tick":0,"action":"MoveNorth"},{"tick":0,"action":"Pause"}]"#;
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 1);
|
||||
assert_eq!(ticks[0].len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_empty_array_idle_tick() {
|
||||
let content = "[]";
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 1);
|
||||
assert!(ticks[0].is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_lines_skipped() {
|
||||
let content = r#"[{"tick":0,"action":"MoveNorth"}]
|
||||
|
||||
[{"tick":2,"action":"MoveSouth"}]
|
||||
|
||||
"#;
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 2, "blank lines should be skipped, not counted as ticks");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_file_returns_empty_vec() {
|
||||
let f = write_temp_file("");
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert!(ticks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only_file_returns_empty_vec() {
|
||||
let f = write_temp_file(" \n \n\n ");
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert!(ticks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_json_reports_line_number() {
|
||||
let content = r#"[{"tick":0,"action":"MoveNorth"}]
|
||||
not valid json
|
||||
[{"tick":2,"action":"MoveSouth"}]"#;
|
||||
let f = write_temp_file(content);
|
||||
let err = load_replay(f.path()).unwrap_err();
|
||||
assert!(err.contains("replay line 2"), "error should reference line 2, got: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_returns_error() {
|
||||
let err = load_replay(Path::new("/nonexistent/replay.jsonl")).unwrap_err();
|
||||
assert!(err.contains("failed to read replay file"), "got: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interact_action_parses() {
|
||||
let content =
|
||||
r#"[{"tick":0,"action":{"Interact":{"target_entity_id":42,"verb":"Talk"}}}]"#;
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 1);
|
||||
match &ticks[0][0].action {
|
||||
PlayerAction::Interact {
|
||||
target_entity_id,
|
||||
verb,
|
||||
} => {
|
||||
assert_eq!(*target_entity_id, Some(42));
|
||||
assert_eq!(verb.as_deref(), Some("Talk"));
|
||||
}
|
||||
other => panic!("expected Interact, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn teleport_to_hub_parses() {
|
||||
let content = r#"[{"tick":0,"action":"TeleportToHub"}]"#;
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 1);
|
||||
assert!(matches!(ticks[0][0].action, PlayerAction::TeleportToHub));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walk_away_parses() {
|
||||
let content = r#"[{"tick":0,"action":"WalkAway"}]"#;
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert!(matches!(ticks[0][0].action, PlayerAction::WalkAway));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_replay_scenario() {
|
||||
// Simulates a realistic Gauntlet replay: move, idle, interact, move, teleport
|
||||
let content = r#"[{"tick":0,"action":"MoveNorth"}]
|
||||
[{"tick":1,"action":"MoveNorth"}]
|
||||
[]
|
||||
[{"tick":3,"action":{"Interact":{"target_entity_id":100,"verb":"Talk"}}}]
|
||||
[{"tick":4,"action":"MoveEast"}]
|
||||
[{"tick":5,"action":"TeleportToHub"}]"#;
|
||||
let f = write_temp_file(content);
|
||||
let ticks = load_replay(f.path()).unwrap();
|
||||
assert_eq!(ticks.len(), 6);
|
||||
assert_eq!(ticks[0].len(), 1); // MoveNorth
|
||||
assert_eq!(ticks[1].len(), 1); // MoveNorth
|
||||
assert_eq!(ticks[2].len(), 0); // Idle
|
||||
assert_eq!(ticks[3].len(), 1); // Interact
|
||||
assert_eq!(ticks[4].len(), 1); // MoveEast
|
||||
assert_eq!(ticks[5].len(), 1); // TeleportToHub
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user