Files
settled-reach/server/tests/gen_gauntlet_fixtures.rs
T
jpmschweitzerandClaude Opus 4.6 cae3d3ab85 refactor(simulation): strip archetype trace + HeritageRoot per cascade (#877, #878)
Sprint 37 dead-code sweep closing out two stale supersession chains:

#877 (D-167, 2026-03-24): Removes HeritageRoot type alias and
ZonePaletteModifier::Heritage variant from server/src/simulation/
generator.rs. The 7 abstract heritage roots were retired in favour of
the corridor cultural system; these two stubs were the only remaining
references.

#878 (D-032 + cascade rule): Strips the entire CharacterArchetype
(Smuggler/Detective) trace from the server. Per lead direction
2026-04-21 and the development cascade (CLAUDE.md), character/NPC/
verb-differentiation/monologue code is Phase 6 detail that should
not exist in code yet. The running archetype trace was pre-cascade
filler, not production — production is only the client's character-
creation UI and insert screens (client follow-up in #882).

Deleted:
- CharacterArchetype enum + StartupMessage.character_archetype field
- archetype_verb_label() + archetype branch of apply_phase2_verb_filter
  (D-057 character-verb differentiation — marked superseded)
- MonologueState.character partitioning
- Gauntlet archetype plumbing (setup_gauntlet no longer takes an archetype)
- server/content/schemas/drama_module.schema.yaml (zero Rust consumers)
- server/content/modules/tier1/smuggling_ring_v0_1.yaml
- server/tests/archetype_monologue.rs (regression guard for the removed system)
- server/tests/v01_integration_playthrough.rs (archetype-dependent)

Decision updates:
- decisions/content.md D-032 supersession rewritten to cite the cascade
  (v0.2 drop invalidated the prior D-117 framing).
- decisions/content.md D-035 tag taxonomy: `character` enum footnote
  updated; field noted as unused, do not reintroduce without a
  confirmed Phase 6 design.
- decisions/perception.md D-057: archetype-verb differentiation marked
  superseded.

Also bundles the types.rs version-field removal from #874 since the
file was already touched here.

Full trace audit in docs/architecture/sprint-37-878-audit.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-22 08:55:48 +02:00

184 lines
6.9 KiB
Rust

//! Generate snapshot fixtures from the Gauntlet test world (D-030 full pipeline).
//!
//! Runs the full Gauntlet simulation pipeline — same code path as --test-mode —
//! then serializes ObserverSnapshots to both MessagePack (wire format) and JSON
//! (human-readable debug) for client-side visual testing.
//!
//! The .msgpack files are the actual wire-format bytes the server sends over IPC.
//! Client visual tests load them via Protocol.decode_snapshot() → GameState.apply_snapshot(),
//! exercising the exact same pipeline as the live game.
//!
//! The .json files are for human inspection only.
//!
//! Run with: cargo test --test gen_gauntlet_fixtures -- --ignored
//! Or: make fixtures-gauntlet
//!
//! Output: tests/fixtures/gauntlet/*.{msgpack,json} (relative to repo root)
use settled_reach_server::bridge::types::*;
use settled_reach_server::bridge::{BridgePlugin, SnapshotBuffer};
use settled_reach_server::knowledge::KnowledgePlugin;
use settled_reach_server::npc::NpcPlugin;
use settled_reach_server::perception::vision_cone::Facing;
use settled_reach_server::simulation::movement::{PlayerCharacter, TilePosition};
use settled_reach_server::simulation::rng::SimRng;
use settled_reach_server::simulation::SimulationPlugin;
use settled_reach_server::test_world;
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use serde_json::Value;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
const SEED: u64 = 42;
const FIXTURE_DIR: &str = "../tests/fixtures/gauntlet";
/// Build a deterministic Gauntlet simulation app.
/// Identical to what the server runs in --test-mode.
fn build_gauntlet(seed: u64) -> App {
let mut app = App::new();
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.add_plugins(NpcPlugin);
app.insert_resource(SimRng::new(seed));
test_world::setup_gauntlet(&mut app);
app
}
/// Teleport the player to a specific position and facing.
/// Directly modifies ECS components — same effect as PlayerAction::Teleport
/// but without needing a target room action.
fn teleport_player(app: &mut App, pos: TilePosition, facing: Facing) {
let player = {
let mut query = app
.world_mut()
.query_filtered::<Entity, With<PlayerCharacter>>();
query.single(app.world()).expect("player entity must exist")
};
app.world_mut().entity_mut(player).insert((pos, facing));
}
/// Run N ticks, feeding inputs each tick, return the last snapshot.
fn run_ticks(app: &mut App, inputs: &[Vec<PlayerInput>]) -> ObserverSnapshot {
let mut last_snapshot: Option<ObserverSnapshot> = None;
for tick_inputs in inputs {
{
let mut queue = app
.world_mut()
.resource_mut::<settled_reach_server::simulation::input::InputQueue>();
for input in tick_inputs {
queue.push(input.clone());
}
}
app.update();
let buffer = app.world().resource::<SnapshotBuffer>();
if let Some(snapshot) = &buffer.snapshot {
last_snapshot = Some(snapshot.clone());
}
}
last_snapshot.expect("no snapshot produced")
}
/// Recursively sort all object keys for deterministic JSON output.
fn sort_json_keys(value: &Value) -> Value {
match value {
Value::Object(map) => {
let sorted: BTreeMap<String, Value> = map
.iter()
.map(|(k, v)| (k.clone(), sort_json_keys(v)))
.collect();
Value::Object(sorted.into_iter().collect())
}
Value::Array(arr) => Value::Array(arr.iter().map(sort_json_keys).collect()),
other => other.clone(),
}
}
fn write_fixture(name: &str, snapshot: &ObserverSnapshot) {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_DIR);
fs::create_dir_all(&dir).expect("create fixture dir");
// MessagePack — exact wire-format bytes the server sends over IPC.
// Client visual tests load these via Protocol.decode_snapshot().
let msgpack = rmp_serde::to_vec_named(snapshot).expect("serialize to MessagePack");
let msgpack_path = dir.join(format!("{}.msgpack", name));
fs::write(&msgpack_path, &msgpack).expect("write msgpack fixture");
// JSON — human-readable debug companion (not loaded by tests).
let value: Value = serde_json::to_value(snapshot).expect("serialize to JSON");
let sorted = sort_json_keys(&value);
let json = serde_json::to_string_pretty(&sorted).expect("format JSON") + "\n";
let json_path = dir.join(format!("{}.json", name));
fs::write(&json_path, &json).expect("write json fixture");
eprintln!(
"Wrote {} ({} bytes msgpack, {} bytes json, {} visible_tiles, {} entities)",
name,
msgpack.len(),
json.len(),
snapshot.visible_tiles.len(),
snapshot.entities.len(),
);
}
#[test]
#[ignore] // Run manually: cargo test --test gen_gauntlet_fixtures -- --ignored
fn generate_gauntlet_snapshot_fixtures() {
// --- Hub (default spawn position) ---
// Player at (50, 58) facing North — tests fog rendering at the starting location.
// This is the exact state a new player sees on connect.
{
let mut app = build_gauntlet(SEED);
// Run 3 idle ticks to stabilize (cognitive delay, vision cone init)
let inputs: Vec<Vec<PlayerInput>> = vec![vec![], vec![], vec![]];
let snapshot = run_ticks(&mut app, &inputs);
write_fixture("hub_spawn", &snapshot);
}
// --- Fog Theater (observer position) ---
// Player at (56, 18) facing South — large open room with NPCs at varying distances.
// Tests visibility cone, fog layers, and distance-based fog rendering.
{
let mut app = build_gauntlet(SEED);
let fog_theater = test_world::constants::FOG_THEATER;
teleport_player(&mut app, fog_theater.observer, fog_theater.observer_facing);
let inputs: Vec<Vec<PlayerInput>> = vec![vec![], vec![], vec![]];
let snapshot = run_ticks(&mut app, &inputs);
write_fixture("fog_theater", &snapshot);
}
// --- Hub after movement (explored tiles + visible tiles differ) ---
// Player moves south from hub, creating a mix of explored-but-not-visible
// and currently-visible tiles — the exact fog boundary condition.
{
let mut app = build_gauntlet(SEED);
let inputs: Vec<Vec<PlayerInput>> = vec![
vec![],
vec![PlayerInput {
tick: 1,
action: PlayerAction::MoveSouth,
}],
vec![PlayerInput {
tick: 2,
action: PlayerAction::MoveSouth,
}],
vec![PlayerInput {
tick: 3,
action: PlayerAction::MoveSouth,
}],
vec![], // idle — snapshot has explored + visible tiles that differ
];
let snapshot = run_ticks(&mut app, &inputs);
write_fixture("hub_after_movement", &snapshot);
}
}