feat(simulation): sprint 9 gauntlet — test infrastructure and first 3 rooms

Add Gauntlet test world with 3 rooms (Inventory Warehouse, Occlusion
Corridor, Pause Chamber) + Central Hub, room constants module, room
reset trigger mechanism, Layer 3 subprocess integration test, golden
file comparison engine and test suite, and content runtime validation.

Tickets: #482, #484, #485, #487, #488, #489, #490

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 02:25:57 +01:00
co-authored by Claude Opus 4.6
parent 58072bc119
commit e66352e0ea
19 changed files with 7906 additions and 23 deletions
+40
View File
@@ -0,0 +1,40 @@
//! Central Hub — Room 0 (24x24)
//!
//! Connector room with 4 directional sign markers. No test entities.
//! Hub spawn point: (12, 12) relative = (50, 58) absolute.
//!
//! Entities (StableId 1-4):
//! sign_north (50, 48) — "Fog Theater"
//! sign_east (60, 58) — "Occlusion Corridor"
//! sign_south (50, 68) — "Pause Chamber"
//! sign_west (40, 58) — "Inventory Warehouse"
use bevy_app::prelude::*;
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
use crate::simulation::interaction::Interactable;
use crate::simulation::movement::TilePosition;
/// Room origin (top-left corner including walls).
const ORIGIN_X: i32 = 38;
const ORIGIN_Y: i32 = 46;
/// Sign positions as relative offsets from room origin.
const SIGNS: &[(&str, i32, i32)] = &[
("sign_north", 12, 2), // North marker
("sign_east", 22, 12), // East marker
("sign_south", 12, 22), // South marker
("sign_west", 2, 12), // West marker
];
/// Spawn hub sign entities in canonical order (StableId 1-4).
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
for &(_name, rx, ry) in SIGNS {
let pos = TilePosition::new(ORIGIN_X + rx, ORIGIN_Y + ry, 0);
let entity = app.world_mut().spawn((Interactable, pos)).id();
let sid = registry.register(entity);
app.world_mut()
.entity_mut(entity)
.insert(StableEntityId(sid));
}
}
@@ -0,0 +1,88 @@
//! Inventory Warehouse — Room 3 (30x28)
//!
//! Tests D-065 (9-slot inventory), pickup/drop verbs, CarriedBy component.
//!
//! Layout: Open warehouse floor with 10 item crates in a grid pattern
//! and 1 NPC near the back wall for interaction testing while carrying items.
//!
//! Observer position: (15, 14) relative = (17, 54) absolute, facing East.
//!
//! Entities (StableId 13-23):
//! crate_01..crate_10 — Floor items (Container ObjectType)
//! npc_warehouse — Interaction target
use bevy_app::prelude::*;
use crate::bridge::types::ObjectType;
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
use crate::simulation::interaction::Interactable;
use crate::simulation::inventory::ItemName;
use crate::simulation::movement::TilePosition;
use crate::simulation::path_follow::MovementSpeed;
/// Room origin (top-left corner including walls).
const ORIGIN_X: i32 = 2;
const ORIGIN_Y: i32 = 40;
/// Item crate definitions: (name, relative_x, relative_y, item_name).
/// 10 crates for testing the 9-slot inventory limit.
const CRATES: &[(&str, i32, i32, &str)] = &[
("crate_01", 4, 4, "Keycard"),
("crate_02", 8, 4, "Manifest"),
("crate_03", 12, 4, "Datapad"),
("crate_04", 16, 4, "Toolkit"),
("crate_05", 4, 10, "Badge"),
("crate_06", 8, 10, "Medkit"),
("crate_07", 12, 10, "Ration"),
("crate_08", 16, 10, "Cable"),
("crate_09", 4, 16, "Seal"),
("crate_10", 8, 16, "Chip"),
];
/// Spawn Inventory Warehouse entities in canonical order (StableId 13-23).
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
// Crates (StableId 13-22)
for &(_name, rx, ry, item_name) in CRATES {
let pos = TilePosition::new(ORIGIN_X + rx, ORIGIN_Y + ry, 0);
let entity = app
.world_mut()
.spawn((
Interactable,
ObjectType::Container,
ItemName(item_name.to_string()),
pos,
))
.id();
let sid = registry.register(entity);
app.world_mut()
.entity_mut(entity)
.insert(StableEntityId(sid));
}
// Warehouse NPC (StableId 23)
let npc_pos = TilePosition::new(ORIGIN_X + 22, ORIGIN_Y + 14, 0);
let entity = app
.world_mut()
.spawn((
Npc,
Interactable,
npc_pos,
Want {
primary: WantKind::Wealth,
intensity: 5,
description: "Warehouse supervisor wants efficiency".to_string(),
},
Contentment { level: 10 },
ToleranceThreshold {
current_stress: 15,
threshold: 65,
},
MovementSpeed::default(),
))
.id();
let sid = registry.register(entity);
app.world_mut()
.entity_mut(entity)
.insert(StableEntityId(sid));
}
+9
View File
@@ -0,0 +1,9 @@
//! Gauntlet room builders.
//!
//! Each room module exports a `spawn_entities()` function that creates
//! entities in canonical order for deterministic StableId assignment.
pub mod hub;
pub mod inventory_warehouse;
pub mod occlusion_corridor;
pub mod pause_chamber;
@@ -0,0 +1,66 @@
//! Occlusion Corridor — Room 2 (42x22)
//!
//! Tests D-035 (symmetric shadowcasting), D-017 (perception modes),
//! D-015 (vision cone sectors).
//!
//! Layout: Long east-west corridor with perpendicular wall segments
//! creating visibility pockets. North alcove with hidden NPC,
//! south alcove with partially visible NPC.
//!
//! Observer position: (10, 10) relative = (84, 58) absolute, facing East.
//!
//! Entities (StableId 9-12):
//! npc_guard_visible (92, 58) — Clear LOS baseline
//! npc_hidden_wall (92, 52) — Behind north wall segment
//! npc_peripheral (80, 64) — South alcove, peripheral sector
//! npc_far_end (110, 58) — Far end of corridor, tests range
use bevy_app::prelude::*;
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
use crate::simulation::interaction::Interactable;
use crate::simulation::movement::TilePosition;
use crate::simulation::path_follow::MovementSpeed;
/// Room origin (top-left corner including walls).
const ORIGIN_X: i32 = 74;
const ORIGIN_Y: i32 = 48;
/// NPC definitions: (name, relative_x, relative_y, want_kind, want_intensity).
const NPCS: &[(&str, i32, i32, WantKind, u8)] = &[
("npc_guard_visible", 18, 10, WantKind::Safety, 5),
("npc_hidden_wall", 18, 4, WantKind::Freedom, 3),
("npc_peripheral", 6, 16, WantKind::Knowledge, 6),
("npc_far_end", 36, 10, WantKind::Wealth, 4),
];
/// Spawn Occlusion Corridor entities in canonical order (StableId 9-12).
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
for &(name, rx, ry, want_kind, intensity) in NPCS {
let pos = TilePosition::new(ORIGIN_X + rx, ORIGIN_Y + ry, 0);
let entity = app
.world_mut()
.spawn((
Npc,
Interactable,
pos,
Want {
primary: want_kind,
intensity,
description: format!("Occlusion Corridor test NPC: {}", name),
},
Contentment { level: 0 },
ToleranceThreshold {
current_stress: 0,
threshold: 50,
},
MovementSpeed::default(),
))
.id();
let sid = registry.register(entity);
app.world_mut()
.entity_mut(entity)
.insert(StableEntityId(sid));
}
}
@@ -0,0 +1,51 @@
//! Pause Chamber — Room 5 (16x16)
//!
//! Tests D-031 (pause/unpause), Bug #3 regression (movement while paused).
//!
//! Layout: Minimal open room with a single NPC. No visual complexity —
//! this room is pure state machine testing.
//!
//! Observer position: (8, 8) relative = (50, 86) absolute, facing North.
//!
//! Entities (StableId 29):
//! npc_pause_target — Interaction target during pause (Talk should work)
use bevy_app::prelude::*;
use crate::knowledge::registry::{EntityRegistry, StableEntityId};
use crate::npc::{Contentment, Npc, ToleranceThreshold, Want, WantKind};
use crate::simulation::interaction::Interactable;
use crate::simulation::movement::TilePosition;
use crate::simulation::path_follow::MovementSpeed;
/// Room origin (top-left corner including walls).
const ORIGIN_X: i32 = 42;
const ORIGIN_Y: i32 = 78;
/// Spawn Pause Chamber entities in canonical order (StableId 29).
pub fn spawn_entities(app: &mut App, registry: &mut EntityRegistry) {
let pos = TilePosition::new(ORIGIN_X + 6, ORIGIN_Y + 6, 0);
let entity = app
.world_mut()
.spawn((
Npc,
Interactable,
pos,
Want {
primary: WantKind::Safety,
intensity: 3,
description: "Wants a quiet posting".to_string(),
},
Contentment { level: 5 },
ToleranceThreshold {
current_stress: 10,
threshold: 40,
},
MovementSpeed::default(),
))
.id();
let sid = registry.register(entity);
app.world_mut()
.entity_mut(entity)
.insert(StableEntityId(sid));
}