Files
settled-reach/docs/sprints/sprint-20/server.md
T
jpmschweitzerandClaude Opus 4.6 bccdcfcdcb docs(docs): add frontmatter to all sprint briefings
Standardized YAML frontmatter on all 115 sprint briefing files across
sprints 1-26 with title, description, type, status, sprint number, and
team fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:15:45 +01:00

13 KiB

title, description, type, status, sprint, team
title description type status sprint team
Sprint 20 — Server Briefing Role definition schema, spatial requirements, triangle definition and generation sprint archived 20 server

Sprint 20: Shape — Server Tasks

Goal: The social site template system gains its foundational schema; triangles become generatable and observable as escalating tensions; the client gains save/load UI and code quality improvements.

Branch: server Agents: Dudley (simulation), Tyre (architecture), Hoshe (QA)

Carry-over from Sprint 19

None. Sprint 19 treated as complete.

New Tickets

# Title Blocked by
#163 Role definition schema
#164 Spatial requirement specification
#165 Single-ownership model
#106 Triangle definition schema
#107 Intra-template triangle generation
#250 Triangle escalation system — (#103, #105 done)

Use db/connectors/ticket show <id> for full details.

Key Decisions

  • decisions/content.md — D-023 (three-tier content model: Tier 1 drama modules, Tier 2 templates, Tier 3 procedural), D-024 (NPC generation model: 10 axes, triangles as atomic social unit — 2 per template minimum), D-025 (social site / functional cluster as atomic template unit: 4-8 NPCs, 15-40 tiles, single-ownership with reference links), D-029 (population entanglement ratio: 30/50/20 — triangles are the 50% mundane layer)
  • decisions/scope.md — D-087 (v0.1 triangle configuration: T1 Kael-Smuggler-Ring, T2 Sera-Detective-Commission, T4 Drin-System-Ring as active forks; T3 and T5 as passive tensions), D-089 (self-contained triangle forks for v0.1, no cross-triangle cascade)
  • decisions/architecture.md — D-010 (deterministic simulation: BTreeMap for all collections, no HashMap), D-026 (simulation tiers: Active-tier NPCs are fully simulated; template instantiation populates Active tier), D-041 (KnowledgeGraph: per-entity component — template instantiation must assign KnowledgeGraph to each spawned NPC)

Notes

#163 — Role definition schema

The RoleDefinition struct already exists in server/src/npc/generate.rs as a procedural generation input — it defines name, location pool entries, and per-axis ranges. This ticket extends that to become the canonical Tier 2 role schema.

What this ticket must deliver:

  • A RoleSchema type (new, distinct from RoleDefinition) in a new server/src/content/template/ module (or server/src/content/types.rs extended). Fields: role_id: RoleId (newtype over String), required_traits: Vec<PersonalityTrait>, skill_focus: Vec<Skill>, relationship_constraints: Vec<RelationshipConstraint>, routine_template: Vec<RoutineEntry> — these are constraints fed into the NPC generator, not hardcoded values.
  • A RelationshipConstraint type: { with_role: RoleId, kind: RelationshipKind, required_trust: TrustRange }. Constrains who this role must be in relationship with within the same template.
  • YAML deserialization via serde. Schema files will live at server/data/templates/ (create the directory).
  • Unit tests: round-trip YAML serialize/deserialize a sample role schema. Validate constraint logic (no self-referential constraints, no duplicate role_id within a template).

Integration points: server/src/npc/generate.rs (RoleDefinition → becomes a builder derived from RoleSchema), server/src/content/types.rs (existing content type infrastructure), server/src/knowledge/types.rs (StableId, RelationshipKind).

Gotcha: RoleId must be stable across save/load — it's a string slug, not a bevy Entity. Keep it a newtype over String so it serializes cleanly with StableId.

#164 — Spatial requirement specification

No existing spatial specification type exists. This is greenfield within the template system.

What this ticket must deliver:

  • A SpaceSpec type: { tile_count_min: u32, tile_count_max: u32, sightline_zones: Vec<SightlineZone>, privacy_level: PrivacyLevel, traffic_pattern: TrafficPattern }.
  • SightlineZone: a named sub-area with a coverage radius in sim tiles (0.5m each, per D-066). Example: { name: "bar_counter", radius: 4 } — 4 sim tiles = 2m clear sightline.
  • PrivacyLevel enum: Public, SemiPrivate, Private. Governs NPC behavior (NPCs are less likely to disclose secrets in Public spaces).
  • TrafficPattern enum: Thoroughfare, Destination, Restricted. Governs procedural NPC routine routing through this space.
  • YAML deserialization. Schema files co-locate with role schemas at server/data/templates/.
  • Unit tests: sample spec round-trip, validation that min <= max tile count.

Integration points: server/src/content/template/ (new module or extended server/src/content/types.rs), future chunk generation (server/src/simulation/ — spatial specs will inform where templates are placed in the map). No simulation code changes needed this sprint — spec types only.

Gotcha: Tile counts are in sim tiles (0.5m). A 15-40 visual tile space (per D-025) = 30-80 sim tiles. Document this conversion explicitly in code comments to prevent future confusion.

#165 — Single-ownership model

NPCs are owned by exactly one template, with reference links to others (D-025). No existing ownership component exists.

What this ticket must deliver:

  • A TemplateOwnership ECS component: { template_id: TemplateId, role_id: RoleId }. Assigned at template instantiation, never reassigned.
  • A TemplateId newtype over u64 — deterministic from world seed + template slug hash.
  • A TemplateReference struct: { from_template: TemplateId, to_template: TemplateId, via_role: RoleId, relationship_metadata: RelationshipKind }. Stored in a TemplateReferenceMap resource (a BTreeMap<TemplateId, Vec<TemplateReference>>).
  • Logic for lifecycle coordination: when a template is unloaded (NPC tier drops to State-saved or Ungenerated per D-026), TemplateReference links are preserved in the serialized state, not destroyed.
  • Unit tests: spawn two templates with cross-references, verify TemplateReferenceMap entries, verify TemplateOwnership components.

Integration points: server/src/simulation/tier.rs (tier transitions must preserve TemplateOwnership), server/src/simulation/save_state.rs (serialize TemplateOwnership and TemplateReferenceMap as part of SaveStateV1 — add fields), server/src/npc/generate.rs (generator receives TemplateId + RoleId at spawn time).

Gotcha: TemplateId from seed + slug hash must be deterministic across save/load — use StdHasher is prohibited (non-deterministic), use a seeded hash (e.g., std::hash::Hasher from a fixed algorithm) or simply hash the slug string bytes with a fixed polynomial. Log the TemplateId computed value in tests for debugging.

#106 — Triangle definition schema

The D-024 spec says triangles are the atomic unit of social intrigue — 2 per template minimum, 1 cross-template. No TriangleDef type exists anywhere in the codebase.

What this ticket must deliver:

  • A TriangleDef type: { triangle_id: TriangleId, roles: [RoleId; 3], conflict_type: ConflictType, interest_axes: [NpcAxis; 3], relationship_constraints: Vec<RelationshipConstraint> }. Three roles, each with a conflicting axis (Want, Secret, Tolerance, etc.).
  • ConflictType enum based on D-087 active fork patterns: ResourceCompetition, LoyaltyConflict, SecretExposure, AuthorityChallenge. Passive tensions use LatentTension variant.
  • TriangleId newtype over u64 — deterministic from template seed + role triple.
  • Validation: all three roles must be distinct within the template; the conflict type must map to at least one axis divergence (no conflict on identical axis values).
  • YAML deserialization. Triangle definitions are authored as part of a template file or as a standalone triangles.yaml per template — Dudley to decide the co-location approach.
  • Unit tests: sample triangle round-trip, validation for duplicate roles, validation for self-consistent conflict.

Integration points: server/src/content/template/ (lives alongside RoleSchema and SpaceSpec), server/src/npc/generate.rs (the generator will consume TriangleDef in #107 to assign axis values that produce the desired conflict), decisions/content.md D-087 (v0.1 triangles T1-T5 should be expressible in this schema).

Gotcha: D-089 — self-contained triangles for v0.1, no cross-triangle cascade. Do not add cross-triangle state fields to TriangleDef. Cross-template triangles are expressed by a TriangleDef that references a RoleId from a different TemplateId — the cross-template link is in the role, not a special triangle type.

#107 — Intra-template triangle generation

The template system can now describe triangles (#106). This ticket generates them from the description.

What this ticket must deliver:

  • A generate_intra_template_triangles(world: &mut World, template_id: TemplateId, defs: &[TriangleDef], rng: &mut SimRng) -> Vec<TriangleState> function.
  • TriangleState ECS component: { triangle_id: TriangleId, role_assignments: BTreeMap<RoleId, StableId>, tension: u8, phase: TrianglePhase }. tension starts at a seeded value within a configured range. TrianglePhase enum: Dormant, Simmering, Active, Resolved.
  • Constraint satisfaction: for each TriangleDef, assign generated NPCs (by StableId) to the three roles. Validate that the NPC's axis values satisfy the conflict (e.g., for a LoyaltyConflict, the NPC filling the loyalty_torn role must have a Relationships axis with entries for both of the other two roles).
  • Minimum 2 triangles per template — emit an error (not a panic) if the template definition provides fewer than 2 TriangleDef entries.
  • Unit tests: spawn a 4-NPC template, generate 2 triangles, assert TriangleState components exist and role assignments are valid, assert constraint satisfaction.

Integration points: server/src/npc/generate.rs (NPC generation runs first; triangle generation consumes the generated NPCs' axis values), server/src/content/template/ (#106 types), server/src/simulation/rng.rs (SimRng for determinism).

Gotcha: Constraint satisfaction can fail if the NPC pool doesn't provide a suitable candidate for a role. Implement a fallback: if no NPC satisfies the strict constraint, pick the closest match and log a warning. Do not panic — world generation must be robust to imperfect seeds.

#250 — Triangle escalation system

Blockers #103 (relationship dynamics) and #105 (tolerance threshold triggers) are done. TriangleState from #107 is available this sprint.

What this ticket must deliver:

  • An ECS system tick_triangle_escalation that runs once per game-minute (every 10 ticks per D-031). For each TriangleState in Simmering or Active phase: increment tension by a seeded per-triangle rate (drawn from SimRng at world-gen time, stored on TriangleState). When tension exceeds the lowest ToleranceThreshold among the triangle's three NPCs, transition phase from Simmering to Active.
  • Observable events: when a triangle enters Active, emit a TriangleCrisisEvent (new event type) containing triangle_id, role_assignments, and trigger_npc: StableId. The monologue system and knowledge system can subscribe to this event — but do not wire those subscribers this sprint. Emit the event; downstream consumption is future work.
  • Resolved transition: when the player resolves an active fork (mechanism TBD — stub a ResolveTriangle(TriangleId) command for now), set phase = Resolved. D-089: resolution does not cascade.
  • Unit tests: simulate 60 ticks on a triangle with a known tension rate, assert Active transition at the expected tick. Test Resolved command sets phase correctly.

Integration points: server/src/simulation/tier.rs (tick_triangle_escalation only runs on Active-tier NPCs per D-026), server/src/simulation/time.rs (game-minute scheduler — 10-tick interval), server/src/npc/tolerance.rs (ToleranceThreshold component), server/src/npc/relationships.rs (RelationshipGraph — tension rate influenced by relationship stress), server/src/bridge/types.rs (add TriangleCrisisEvent to ObserverSnapshot for future client rendering).

Gotcha: Different seeds produce different tolerance thresholds — the same triangle template can escalate in 5 minutes or 30 minutes depending on the seed. This is intentional (D-087). Do not hardcode a tension rate — it must come from SimRng at world-gen time and be stored on the component.

Dependency Chain

#163 (Role definition schema)    ─┐
#164 (Spatial requirement spec)   ├─ parallel, no inter-dependency
#165 (Single-ownership model)    ─┘
         │
         └─ feeds #166 (Template-to-instance mapping, Sprint 21)

#106 (Triangle definition schema) ──► #107 (Intra-template generation) ──► #250 (Escalation system)
         │
         └─ feeds #108 (Cross-template generation, Sprint 21)

#163/#164/#165 and #106/#107/#250 are two parallel tracks. All six tickets can begin in week 1; #107 and #250 gate on #106 completing first.

PR Workflow

When ready to submit, create a PR with the tea CLI. All flags are required to avoid TTY prompts:

tea pr create --repo jpmschweitzer/settled-reach --login schweitz \
  --title "feat(simulation): social site template schema and triangle system" \
  --description "body" --base main --head server