refactor(simulation): PR #137 review — audit override + regression tests + docs
Addresses Tyre, Hoshe, and lead review comments on PR #137: - **Audit doc amendment** (Tyre E1 / Hoshe H1 / Lead): add "Lead override (2026-04-21)" section at top of docs/architecture/sprint-37-878-audit.md. Rewrites the conclusion to "DECISION: STRIP" with the cascade-based rationale. Preserves the original audit body as the pre-override record. - **Regression tests** (Lead 2a-2b / Hoshe H2 / H3): add POSITIVE assertions of the new uniform behavior so silent reintroduction fails. - `phase2_container_verb_labels_uniform_regardless_of_player_state` — two trials (empty KG, POI-bearing KG) assert container verb labels equal Phase-1 defaults. - `monologue_pool_selection_uniform_no_archetype_key` — two observers with divergent MonologueState both draw from OBSERVE_NPC_LINES. - **Decision record amendments** (Lead 3 / Tyre S2): D-032, D-035, and D-057 amended with Phase 6 deferral wording. "Retired pending Phase 6, not deferred with scaffolding." Reintroduction gate: a confirmed Phase 6 character-model design. - **types.rs doc fixes** (Tyre S1 / Hoshe H5): StartupMessage protocol- flow comment updated to reflect no-version handshake (D-192). ObserverSnapshot version-history block grows a "Sprint 37 wire-format shifts" section documenting D-192 + #878 schema drops. - **observer/tests.rs:944 comment** (Hoshe H6): rewritten to cite cascade rationale instead of the stale D-032-SUPERSEDED premise. - **tests/run-atlas-determinism exit** (Hoshe H7): exit 0 when EXIT_CODE=2 (venv/DB missing = skip, not fail). Preserves skip semantics for tests/run-all on machines without the Python venv. Follow-up tickets filed: - #895 (server, low): expand check-systems-db-stamp GENERATOR_SOURCES to cover gemma_naming.py + naming_core.py (Tyre S3). - #896 (planning, low): add CLAUDE.md carveout for server wiki writes closing coverage gates (Tyre S4 / Hoshe H8). H4 investigation: v01_integration_playthrough.rs was not the only E2E handshake→tick→snapshot test; coverage preserved by bridge_ipc.rs, bridge_tcp.rs, and game_loop.rs (the latter is pre-existing-broken per #885). No replacement test needed. 1142/1142 lib tests pass. cargo clippy -- -D warnings clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,9 +44,10 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
|
||||
- **Dissent:** None
|
||||
- **Amendment (2026-03-05, Where's the Fun? Workshop):** The 30/50/20 population split rationale survives but implementation context changes. [D-122](#d-122-all-npcs-generated--no-named-hand-authored-characters) (all NPCs generated) means no NPC is hand-authored. The "entangled 20%" are generated NPCs whose triangles happen to be flagged for intrigue content. For the tycoon v0.2 bookmark ([D-117](scope.md#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content)), the split applies to economic, social, and mundane triangles rather than investigation-intrigue triangles. The specific ratios will be revisited after the generator spike ([D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path)) proves what population density the generator can sustain.
|
||||
|
||||
### D-032: Separate monologue pools per character [SUPERSEDED]
|
||||
### D-032: Separate monologue pools per character [SUPERSEDED — deferred to Phase 6]
|
||||
- **Date:** 2026-02-11
|
||||
- **Superseded by:** Development cascade (CLAUDE.md) — character/NPC monologue content is Phase 6 detail-coloring, below the current Phase 1 (wiki content). The smuggler/detective archetype enum and its hard-partitioned monologue pools were pre-cascade scaffolding and have been fully stripped from the server codebase (Sprint 37, #878). The original D-117 supersession framing (single tycoon character in v0.2) is itself obsolete now that v0.2 is dropped (CLAUDE.md: "v0.2 target is dropped"). The partition *design pattern* is preserved in this record for when culture-driven / generator-produced monologue is reintroduced in Phase 6, but no corresponding code or content exists today. Amendment (2026-04-21, Sprint 37): enum + content scaffolding deleted from `server/src/bridge/types.rs`, `server/src/simulation/monologue.rs`, observer relabeling logic, and `server/content/modules/tier1/smuggling_ring_v0_1.yaml`.
|
||||
- **Superseded by:** Development cascade (CLAUDE.md) — character/NPC monologue content is Phase 6 detail-coloring, below the current Phase 1 (wiki content). The smuggler/detective archetype enum and its hard-partitioned monologue pools were pre-cascade scaffolding and have been fully stripped from the server codebase (Sprint 37, #878, PR #137). The original D-117 supersession framing (single tycoon character in v0.2) is itself obsolete now that v0.2 is dropped (CLAUDE.md: "v0.2 target is dropped"). The partition *design pattern* is preserved in this record for when culture-driven / generator-produced monologue is reintroduced in Phase 6, but no corresponding code or content exists today.
|
||||
- **Amendment (2026-04-22, Sprint 37, PR #137):** Monologue pool selection is now unkeyed by archetype until a Phase 6 character system exists. `MonologueState.character` field and `CharacterArchetype` enum deleted from `server/src/bridge/types.rs`, `server/src/simulation/monologue.rs`, observer relabeling logic, and `server/content/modules/tier1/smuggling_ring_v0_1.yaml`. Uniform single-pool behavior is the intended end state pre-Phase-6 — not scaffolding deferred in place with a stub, but retired pending the real character system. Lead override recorded in `docs/architecture/sprint-37-878-audit.md` (2026-04-21 override section). Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite before this decision is revived.
|
||||
- **Decision:** Internal monologue content is hard-partitioned by playable character. The smuggler and detective have completely separate monologue pools — no shared lines. The `character` tag on monologue lines is a hard partition, not a filter. File structure uses separate files per character per location (e.g., `monologue-smuggler.yaml`, `monologue-detective.yaml`).
|
||||
- **Rationale:** Shared monologue would dilute character voice and undermine the dual-lens experience. Each character's internal voice must be independently coherent. Same trigger, different pool — this is how mirror moments work without either pool knowing about the other.
|
||||
- **Cross-reference:** Dialogue lines remain character-agnostic — the access tier system (D-028 Layer 1) handles per-character filtering without separate pools.
|
||||
@@ -92,6 +93,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
|
||||
- **Amendment (Sprint 14):** Mood vocabulary renamed to match voice guide (monologue-voice-guide.md). Old → new: `fond`→`warm`, `comfortable`→`content`, `worried`→`anxious`, `concerned`→`frustrated`. Dropped: `analytical` (merged into `focused`), `conflicted` (modeled as `suspicious`+`warm` collision). Added: `hostile`. Final 8 moods: `anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`. Neutral = untagged.
|
||||
- **Amendment (Sprint 24):** `triangle_activated` added as 15th situation (fires post-TriangleActivated when player observes anchor NPCs). Two freeform tags registered as conventions: `triangle-signal` (line is part of the triangle activation sequence) and `tell-observation` (line observes a behavioral tell without naming its cause). `npc_in_los` prerequisite added for LOS-gated monologue lines. Schema updated to match.
|
||||
- **Amendment (Sprint 15):** Line ID namespace changed from location-scoped to NPC-scoped. Old scheme: `{location_slug}_{d|m}_{###}` (e.g., `the-terminal_d_039`) — all NPCs at a location share one ID sequence, requiring cross-file coordination and causing collisions at scale. New scheme: `{npc-slug}_{d|m}_{###}` for dialogue, `{npc-slug}_m_{s|d}_{###}` for monologue (e.g., `kael-davan_d_001`, `dock-worker_d_001`). Each NPC's IDs are independent — no cross-file coordination needed. Auto-generated NPCs use their generated slug. Schema regex patterns unchanged (prefix is still `^[a-z][a-z0-9-]*`), only the `description` field and convention documentation update. Migration: mechanical rename of all existing line IDs across ~20 dialogue files and monologue pools.
|
||||
- **Amendment (2026-04-22, Sprint 37, PR #137):** The `character` monologue-specific tag (historically `smuggler | detective`) is retired pending a Phase 6 character-model design — not deferred in place with a stub enum. Per the development cascade (CLAUDE.md), character-partitioned monologue is Phase 6 detail-coloring; the codebase has been stripped of the `CharacterArchetype` enum and the `MonologueState.character` field that keyed pool selection (#878). Pool selection is now archetype-independent. Authoring files that carry historical `character:` tags are content artifacts and will be re-evaluated when the Phase 6 character system is designed; engine consumption of the field is gone. Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite. See `docs/architecture/sprint-37-878-audit.md` (lead override section) for the cascade rationale.
|
||||
|
||||
### D-036: Sova Transit District / Van Maanen's Star as v0.1 setting
|
||||
- **Date:** 2026-02-11
|
||||
|
||||
@@ -225,9 +225,9 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
|
||||
- `GameState.insert_active` is the source of truth (defaults true in v0.1; wired from snapshot field `insert_active`).
|
||||
- Rationale: diegetically consistent — the body reacts to proximity; the insert reacts to commands.
|
||||
|
||||
### D-057: Entity interaction — vertical list, insert-styled [PARTIALLY SUPERSEDED]
|
||||
### D-057: Entity interaction — vertical list, insert-styled [PARTIALLY SUPERSEDED — archetype portion deferred to Phase 6]
|
||||
- **Date:** 2026-02-13
|
||||
- **Supersession note (2026-04-21, Sprint 37, #878):** The character-archetype verb variation portion of this decision is superseded by the development cascade (CLAUDE.md) — archetype-driven verb relabeling is Phase 6 detail-coloring and has been stripped from the server. Vertical-list structure, Phase 1/Phase 2 split, POI priority flips, and contradiction markers remain live. Relabeling (Open→"Move"/"Stash" vs "Scan"/"Flag") is deleted; verbs now render with their base labels regardless of character.
|
||||
- **Supersession note (2026-04-21 / amended 2026-04-22, Sprint 37, #878, PR #137):** The character-archetype verb variation portion of this decision is retired pending a Phase 6 character-model design — not deferred in place with a stub. Per the development cascade (CLAUDE.md), archetype-driven verb relabeling is Phase 6 detail-coloring and has been stripped from the server. Vertical-list structure, Phase 1/Phase 2 split, POI priority flips, and contradiction markers remain live. Relabeling (Open→"Move"/"Stash" vs "Scan"/"Flag") is deleted; container verb labels are now identical across all player states, and uniform labeling is the intended pre-Phase-6 end state, not a regression. Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite. See `docs/architecture/sprint-37-878-audit.md` (lead override section) for the cascade rationale.
|
||||
- **Decision:** Entity interactions use a compact vertical list (not radial). 2-4 options max, anchored to entity position. Insert-styled with Araminta's geometric aesthetic. New options unlocked by knowledge changes are highlighted with a gradient glow background. Radial menu reserved for world menu only ([D-058](#d-058-world-menu--radial-4-spokes)). Max 3 visible response options in dialogue context.
|
||||
- **Server architecture:** Two-phase verb computation. Phase 1 (simulation, no KG): compute maximum possible verb set from ObjectType component (Readable, Container, Terminal, Door, Pickup, Furniture — each with specific verb sets). Phase 2 (observer, reads KG): filter by character's knowledge (Confront requires KnowsDetails+ per [D-041](architecture.md#d-041-knowledge-graph-data-model)), apply POI priority flips, add contradiction markers. ~~Character-archetype verb variation implemented as Phase 2 observer filter rules (same crate: smuggler sees "Move/Stash", detective sees "Scan/Flag").~~ *[Removed Sprint 37 — archetype verb relabeling deleted; see supersession note above.]*
|
||||
- **Diegetic test:** Labels render on z-layer 6. If insert is off, labels disappear.
|
||||
|
||||
@@ -1,11 +1,108 @@
|
||||
# Sprint 37 #878 — CharacterArchetype audit (D-032 obsolete claim)
|
||||
|
||||
**Author:** Tyre (architecture)
|
||||
**Date:** 2026-04-21
|
||||
**Date:** 2026-04-21 (audit); 2026-04-22 (lead override amendment)
|
||||
**Ticket:** #878 — Audit and purge dead smuggler/detective character enum (D-032 obsolete)
|
||||
**Status:** BLOCKED — load-bearing code found, awaiting lead decision before destructive work.
|
||||
**Status:** CLOSED — lead override 2026-04-21: strip stays. See override section below.
|
||||
|
||||
## TL;DR
|
||||
---
|
||||
|
||||
## Lead override (2026-04-21)
|
||||
|
||||
**Decision:** STRIP the full `CharacterArchetype` trace from the server. The
|
||||
original audit (below) recommended Option A (no-op, docs-only) on the basis
|
||||
that grep identified five load-bearing consumers. The lead reframed the
|
||||
live-vs-filler determination:
|
||||
|
||||
> "It is live because we have not scrapped the system loading it in the
|
||||
> client. This is not live gameplay. Only the character creation elements
|
||||
> and the insert screens are actual production code. The rest is uncleaned
|
||||
> filler — per the cascade, we don't deal with character and NPC
|
||||
> instructions." — Jeroen, 2026-04-21
|
||||
|
||||
**Rationale — the cascade framing.** Per `CLAUDE.md`, development follows a
|
||||
strict six-phase cascade. Current focus is Phase 1 (wiki content). Character
|
||||
model, NPC differentiation, verb-label relabeling, and character-keyed
|
||||
monologue pools all belong to Phase 6 (detail coloring). Code that executes
|
||||
at runtime is **not** automatically production — if the topic belongs to a
|
||||
later cascade phase, it is pre-cascade filler regardless of how deeply it is
|
||||
wired in.
|
||||
|
||||
The audit's identification of `archetype_verb_label` as "live feature code"
|
||||
was mechanically correct: the code runs, produces output, and is observed on
|
||||
the wire. But the **feature itself is premature**. Container verb
|
||||
differentiation (Smuggler Move/Stash vs Detective Scan/Flag) and
|
||||
archetype-keyed monologue pools are Phase 6 detail, not Phase 1–3
|
||||
scaffolding. Uniform verb labels and a single monologue pool are the
|
||||
intended end state until a Phase 6 character system is designed — not a
|
||||
regression.
|
||||
|
||||
**Audit correction.** The audit's TL;DR conclusion ("v0.2 dropped reverses
|
||||
D-117 which reverses D-032-obsolete, therefore keep the enum") was the
|
||||
wrong frame. The supersession chain collapsed — yes — but the correct
|
||||
reading is that **all three decisions sit below the current cascade
|
||||
floor**, so the enum is cruft on cascade grounds independent of the D-117
|
||||
revocation. The audit should have consulted the cascade phase before
|
||||
grep-counting consumers; that is the non-obvious precedent captured in
|
||||
memory (`feedback_running_code_not_production.md`).
|
||||
|
||||
**Scope of the strip (Sprint 37, #878, merged via PR #137). Commit `cae3d3ab`
|
||||
— "refactor(simulation): strip archetype trace + HeritageRoot per cascade
|
||||
(#877, #878)" — is the authoritative file list; the summary below is the
|
||||
high-signal view:**
|
||||
|
||||
- `server/src/bridge/types.rs` — `CharacterArchetype` enum +
|
||||
`StartupMessage.character_archetype` field removed. Protocol break rides
|
||||
the `PROTOCOL_VERSION` drop in #874 (D-192), co-shipped in the same PR.
|
||||
- `server/src/perception/observer/mod.rs` — `apply_phase2_verb_filter`
|
||||
loses its archetype parameter; `archetype_verb_label` helper + the
|
||||
container-relabeling block deleted.
|
||||
- `server/src/simulation/monologue.rs` — `MonologueState.character`
|
||||
field + `Default` value removed. Pool selection is now uniform.
|
||||
- `server/src/simulation/examine.rs` — `generate_examine_text` collapses
|
||||
two archetype-specific branches into a single detective-style frame.
|
||||
- `server/src/test_world/mod.rs` + `server/src/main.rs` — `setup_gauntlet`
|
||||
and `setup_proof_room` drop the archetype parameter; all internal
|
||||
callsites + the production main loop updated.
|
||||
- `server/tests/archetype_monologue.rs` — deleted (regression guard for
|
||||
#587's archetype→monologue wiring; wiring itself deleted).
|
||||
- `server/tests/v01_integration_playthrough.rs` — deleted (5
|
||||
archetype-using integration tests; superseded by Gauntlet coverage).
|
||||
- `server/content/schemas/drama_module.schema.yaml` — deleted.
|
||||
- `server/content/modules/tier1/smuggling_ring_v0_1.yaml` — deleted.
|
||||
|
||||
**Client follow-up: ticket #882** — *"Strip archetype-driven client code
|
||||
(follow-up to #878 server)"*. The server strip leaves client-side code
|
||||
referencing the removed `character_archetype` wire field and archetype-
|
||||
keyed palette/monologue branches. Client cleanup preserves the character-
|
||||
creation UI and insert screens (production per the lead call) and strips
|
||||
`lattice_profile` branching, `character.txt` session I/O, and protocol
|
||||
decoding of the removed field. Blocked by this PR; cross-referenced in
|
||||
the server task description.
|
||||
|
||||
**Decision record amendments (2026-04-22):**
|
||||
|
||||
- `decisions/content.md` D-032 `[SUPERSEDED]` header rewritten to cite the
|
||||
cascade instead of the dropped D-117.
|
||||
- `decisions/content.md` D-035 monologue `character` enum note updated
|
||||
(was `[Obsolete post-D-117]`, now cites the cascade strip).
|
||||
- `decisions/perception.md` D-057 marked `[PARTIALLY SUPERSEDED]` with
|
||||
the archetype verb relabeling portion crossed out; vertical-list +
|
||||
Phase 1/2 split preserved.
|
||||
|
||||
**Regression guards for the new uniform behavior** are being added under
|
||||
separate tasks (Hoshe, Sprint 37) — positive assertions that container
|
||||
verb labels and monologue pool selection are archetype-independent, to
|
||||
prevent silent reintroduction.
|
||||
|
||||
**DECISION: STRIP.**
|
||||
|
||||
The original audit body below is retained as a historical record of the
|
||||
pre-override analysis. Do not take its recommendation as current.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR (original audit, SUPERSEDED by lead override above)
|
||||
|
||||
The ticket's premise — that `CharacterArchetype` (Smuggler/Detective) is dead
|
||||
code per D-032's "[Obsolete post-D-117]" footnote — is **stale**. The
|
||||
@@ -102,9 +199,13 @@ Three options, cheapest first:
|
||||
doesn't actually change runtime. **Not recommended** unless the lead
|
||||
specifically wants the naming to match post-cascade vocabulary.
|
||||
|
||||
## Recommendation
|
||||
## Recommendation (SUPERSEDED — see "Lead override" at top)
|
||||
|
||||
Go with **Option A**. The enum is architecturally sound, the ticket is
|
||||
~~Go with **Option A**. The enum is architecturally sound, the ticket is
|
||||
a casualty of the v0.2→cascade pivot, and the cleanup is documentation-
|
||||
only. I have completed #877 cleanly; I'll park #878 in_progress pending
|
||||
the lead's call on A/B/C.
|
||||
only.~~
|
||||
|
||||
**Actual decision:** strip (Option D, not enumerated above — full trace
|
||||
purge driven by cascade framing, not by the D-117/v0.2 supersession
|
||||
chain). The audit's grep-count-first methodology was the wrong starting
|
||||
heuristic; cascade phase comes first. See top of document.
|
||||
|
||||
@@ -23,12 +23,11 @@ pub struct HandshakeMessage {}
|
||||
/// Startup message sent by the client after receiving HandshakeMessage (#175).
|
||||
/// Contains the world seed for deterministic simulation (D-010, D-029).
|
||||
///
|
||||
/// Protocol flow:
|
||||
/// Protocol flow (D-192: no version field, no validation step):
|
||||
/// 1. Server sends HandshakeMessage (server → client)
|
||||
/// 2. Client validates protocol_version
|
||||
/// 3. Client sends StartupMessage (client → server)
|
||||
/// 4. Server reads world_seed, initializes SimRng
|
||||
/// 5. Normal tick loop begins
|
||||
/// 2. Client sends StartupMessage (client → server)
|
||||
/// 3. Server reads world_seed, initializes SimRng
|
||||
/// 4. Normal tick loop begins
|
||||
///
|
||||
/// Wire format: MessagePack, same 4-byte length-prefixed framing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -65,13 +64,21 @@ pub struct StartupMessage {
|
||||
/// sim_errors (#85, structured error reporting to client).
|
||||
/// v18 adds: debug_response (#580, debug console server — command/response wire).
|
||||
/// v19 adds: current_ticker (#591). (character_archetype on StartupMessage was
|
||||
/// added in #587 and removed in Sprint 37 per D-032 purge / cascade cleanup.)
|
||||
/// added in #587 and removed in Sprint 37 — see "Sprint 37 wire-format shifts" below.)
|
||||
/// v20 adds: settings_response (#627, SQLite settings IPC).
|
||||
/// v21 adds: economy_snapshot (#822, D-181 7-signal snapshot per queried system),
|
||||
/// EconStateQuery PlayerAction variant (#822).
|
||||
/// v22 adds: bookmark_catalog (#614, D-115/D-117 CK3-style bookmark system),
|
||||
/// RequestBookmarkCatalog + ConfirmBookmark PlayerAction variants (#614).
|
||||
/// v23 removes: conversation_events, conversation_ended (D-078 scrapped per R-012).
|
||||
/// Sprint 37 wire-format shifts (no snapshot-version bump needed — PROTOCOL_VERSION lockstep gone):
|
||||
/// - D-192 (#874): `PROTOCOL_VERSION` field removed from `HandshakeMessage`. The
|
||||
/// handshake is now an empty marker ("server ready"); there is no negotiated
|
||||
/// version field on the wire. Genuine schema drift surfaces as MessagePack
|
||||
/// missing-field errors downstream — that is the intended signal per D-192.
|
||||
/// - #878 (cascade cleanup): `character_archetype` field removed from
|
||||
/// `StartupMessage`. This protocol break rides the D-192 drop.
|
||||
///
|
||||
/// Future fields: ambient sound events, HUD state (D-020 expansion).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObserverSnapshot {
|
||||
|
||||
@@ -941,7 +941,11 @@ fn phase2_no_contradiction_for_active_knowledge() {
|
||||
|
||||
#[test]
|
||||
fn phase2_non_container_keeps_default_labels() {
|
||||
// Readable objects keep their default labels (post-archetype cleanup, D-032 SUPERSEDED).
|
||||
// Readable objects keep their Phase 1 verb labels unchanged through Phase 2.
|
||||
// Regression guard: archetype-verb differentiation is Phase 6 detail — not present
|
||||
// in the current server per the development cascade (CLAUDE.md). D-057 superseded.
|
||||
// If this test fails, a character-class relabelling branch was reintroduced before
|
||||
// Phase 6 scope is confirmed by the team lead.
|
||||
let mut world = setup_world(32, 32);
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
@@ -2596,3 +2600,124 @@ fn tell_state_none_when_npc_has_no_derived_tell_component() {
|
||||
"NPC without DerivedTellState component should have tell_state = None"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Regression: Phase 2 container verb labels are uniform (D-057 / #878)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn phase2_container_verb_labels_uniform_regardless_of_player_state() {
|
||||
// Regression guard (D-057 superseded, #878 cascade purge):
|
||||
// apply_phase2_verb_filter no longer has an archetype branch that relabels
|
||||
// container verbs. Labels must be the Phase-1 defaults — "Open", "Search",
|
||||
// "Observe" — regardless of the observer's KnowledgeGraph contents or
|
||||
// relationship state with other entities.
|
||||
//
|
||||
// This test FAILS if a character-class verb-label branch is reintroduced
|
||||
// without a confirmed Phase 6 scope decision from the team lead.
|
||||
|
||||
// --- Trial A: empty KnowledgeGraph ---
|
||||
{
|
||||
let mut world = setup_world(32, 32);
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
let container = world
|
||||
.spawn((
|
||||
TilePosition::new(16, 15, 0),
|
||||
crate::simulation::interaction::Interactable,
|
||||
ObjectType::Container,
|
||||
))
|
||||
.id();
|
||||
registry.register(container);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
KnowledgeGraph::new(),
|
||||
NearbyInteractionBuffer::default(),
|
||||
MonologueBuffer::default(),
|
||||
));
|
||||
world.insert_resource(registry);
|
||||
|
||||
run_full_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
assert_eq!(snapshot.nearby_interactions.len(), 1);
|
||||
let labels: Vec<&str> = snapshot.nearby_interactions[0]
|
||||
.verbs
|
||||
.iter()
|
||||
.map(|v| v.label.as_str())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
labels,
|
||||
["Open", "Search", "Observe"],
|
||||
"Trial A (empty KG): container verb labels must equal Phase-1 defaults"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Trial B: KG with PersonOfInterest NPC nearby ---
|
||||
// Player has a non-trivial knowledge state; container labels must still be
|
||||
// the Phase-1 defaults — Phase 2 NPC-specific logic must not bleed into
|
||||
// ObjectType::Container interactions.
|
||||
{
|
||||
let mut world = setup_world(32, 32);
|
||||
let mut registry = EntityRegistry::new(0);
|
||||
|
||||
let container = world
|
||||
.spawn((
|
||||
TilePosition::new(16, 15, 0),
|
||||
crate::simulation::interaction::Interactable,
|
||||
ObjectType::Container,
|
||||
))
|
||||
.id();
|
||||
registry.register(container);
|
||||
|
||||
// NPC out of close-range so Confront is not injected; still in KG as POI.
|
||||
let npc = world
|
||||
.spawn((
|
||||
crate::npc::Npc,
|
||||
TilePosition::new(16, 13, 0),
|
||||
crate::simulation::interaction::Interactable,
|
||||
))
|
||||
.id();
|
||||
let npc_sid = registry.register(npc);
|
||||
|
||||
let mut kg = KnowledgeGraph::new();
|
||||
kg.observe_entity(npc_sid, TilePosition::new(16, 13, 0), 5);
|
||||
kg.set_relationship(&npc_sid, RelationshipState::PersonOfInterest);
|
||||
|
||||
world.spawn((
|
||||
PlayerCharacter,
|
||||
TilePosition::new(16, 16, 0),
|
||||
Facing(FacingDirection::North),
|
||||
kg,
|
||||
NearbyInteractionBuffer::default(),
|
||||
MonologueBuffer::default(),
|
||||
));
|
||||
world.insert_resource(registry);
|
||||
|
||||
run_full_pipeline(&mut world);
|
||||
|
||||
let buffer = world.resource::<SnapshotBuffer>();
|
||||
let snapshot = buffer.snapshot.as_ref().unwrap();
|
||||
|
||||
let container_interaction = snapshot
|
||||
.nearby_interactions
|
||||
.iter()
|
||||
.find(|i| i.object_type == Some(ObjectType::Container))
|
||||
.expect("container interaction must be present");
|
||||
|
||||
let labels: Vec<&str> = container_interaction
|
||||
.verbs
|
||||
.iter()
|
||||
.map(|v| v.label.as_str())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
labels,
|
||||
["Open", "Search", "Observe"],
|
||||
"Trial B (POI NPC in KG): container verb labels must equal Phase-1 defaults"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2125,4 +2125,103 @@ mod tests {
|
||||
"queue should be empty after monologue consumed the event"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Regression: monologue pool selection is uniform (D-032 / #878)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn monologue_pool_selection_uniform_no_archetype_key() {
|
||||
// Regression guard (D-032 cascade purge, #878):
|
||||
// trigger_event_monologue previously partitioned pool selection by
|
||||
// CharacterArchetype key (MonologueState.character). That field is gone.
|
||||
// Pool selection is now by trigger string only — "observe_npc", "hear_sound",
|
||||
// "post_conversation" — and the line IDs are drawn exclusively from the
|
||||
// corresponding hardcoded constant (OBSERVE_NPC_LINES et al.).
|
||||
//
|
||||
// This test asserts the POSITIVE behaviour: an observe_npc trigger always
|
||||
// produces a line whose ID begins with "observe_npc_", regardless of any
|
||||
// additional observer state. It FAILS if an archetype-keyed dispatch path
|
||||
// is reintroduced (which would produce IDs outside that prefix or panic on
|
||||
// a missing archetype field).
|
||||
|
||||
// Known line IDs from OBSERVE_NPC_LINES (compile-checked below).
|
||||
const VALID_OBSERVE_NPC_IDS: &[&str] =
|
||||
&["observe_npc_01", "observe_npc_02", "observe_npc_03"];
|
||||
|
||||
// --- Observer A: minimal state (no extra components) ---
|
||||
let line_a = {
|
||||
let mut world = setup_event_world();
|
||||
let player = spawn_event_player(&mut world);
|
||||
|
||||
world
|
||||
.resource_mut::<ObservationEventQueue>()
|
||||
.push(ObservationEvent {
|
||||
tick: 1,
|
||||
trigger: ObservationTrigger::NewEntity {
|
||||
entity: StableId(10),
|
||||
location: TilePosition::new(12, 12, 0),
|
||||
},
|
||||
observer: player,
|
||||
});
|
||||
|
||||
run_event_system(&mut world);
|
||||
|
||||
let buf = world.get::<MonologueBuffer>(player).unwrap();
|
||||
buf.event
|
||||
.as_ref()
|
||||
.expect("observe_npc trigger must fire a monologue")
|
||||
.id
|
||||
.clone()
|
||||
};
|
||||
|
||||
// --- Observer B: player has heard a previous sound (last_fired_tick set) ---
|
||||
// Simulates a player with non-default MonologueState — the pool key must
|
||||
// still resolve to OBSERVE_NPC_LINES, not an archetype-partitioned variant.
|
||||
let line_b = {
|
||||
let mut world = setup_event_world();
|
||||
let player = spawn_event_player(&mut world);
|
||||
|
||||
// Pre-populate state to exercise a non-fresh observer
|
||||
world.resource_mut::<SimulationTime>().tick = 10;
|
||||
{
|
||||
let mut state = world.get_mut::<MonologueState>(player).unwrap();
|
||||
state.last_fired_tick = 3;
|
||||
}
|
||||
|
||||
world
|
||||
.resource_mut::<ObservationEventQueue>()
|
||||
.push(ObservationEvent {
|
||||
tick: 5,
|
||||
trigger: ObservationTrigger::NewEntity {
|
||||
entity: StableId(20),
|
||||
location: TilePosition::new(14, 14, 0),
|
||||
},
|
||||
observer: player,
|
||||
});
|
||||
|
||||
run_event_system(&mut world);
|
||||
|
||||
let buf = world.get::<MonologueBuffer>(player).unwrap();
|
||||
buf.event
|
||||
.as_ref()
|
||||
.expect("observe_npc trigger must fire for observer B")
|
||||
.id
|
||||
.clone()
|
||||
};
|
||||
|
||||
// Both observers must produce IDs from the unified observe_npc pool.
|
||||
assert!(
|
||||
VALID_OBSERVE_NPC_IDS.contains(&line_a.as_str()),
|
||||
"Observer A line_id '{}' is not from OBSERVE_NPC_LINES — \
|
||||
archetype-keyed pool dispatch may have been reintroduced",
|
||||
line_a
|
||||
);
|
||||
assert!(
|
||||
VALID_OBSERVE_NPC_IDS.contains(&line_b.as_str()),
|
||||
"Observer B line_id '{}' is not from OBSERVE_NPC_LINES — \
|
||||
archetype-keyed pool dispatch may have been reintroduced",
|
||||
line_b
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,4 +133,7 @@ fi
|
||||
|
||||
printf '{"suite":"atlas-determinism","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \
|
||||
"$TOTAL" "$PASSED" "$FAILED" "$DURATION_MS"
|
||||
exit $EXIT_CODE
|
||||
|
||||
# Exit code 2 = venv/DB missing → treated as skip, not failure (exit 0 for run-all).
|
||||
# Exit code 1 = determinism failure → exit 1 to fail CI.
|
||||
[[ $EXIT_CODE -eq 2 ]] && exit 0 || exit $EXIT_CODE
|
||||
|
||||
Reference in New Issue
Block a user