feat(simulation): name dedup, behavior dedup, relationship pipeline, Want/State layer (#628 #629 #631 #632)

Four generator spike improvements in one pass:

- #628: Fix name pool first-pick bias. build_name_pool now derives a
  zone+culture-specific ChaCha20 RNG via FNV-1a mixing of (seed,
  zone_type, culture_id), isolating name ordering from main RNG
  consumption. Different zone types with the same seed now produce
  different first names.

- #629: Behavior dedup within a zone run. build_behavior_pools
  pre-shuffles each role's behavior list; gen_behaviors draws without
  replacement. Falls back to random repeat with warning when pool
  exhausts.

- #631: Relationship-to-behavior pipeline. Third generation pass
  (~50% chance) replaces primary behavior with relationship-revealing
  action — rivals talk past each other, friends drift together,
  subordinates defer.

- #632: Want/State layer. NpcWant enum (Neutral/Bored/Alert/Suspicious/
  AvoidingSomeone/LookingForInfo) biased by traits and role. Fourth
  generation pass produces observable tells that leak internal state
  through behavior. AvoidingSomeone resolves against negative-valence
  relationships for named targets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 11:26:48 +01:00
co-authored by Claude Opus 4.6
parent c12005843b
commit c3a8dcc481
2 changed files with 385 additions and 41 deletions
+29
View File
@@ -150,6 +150,29 @@ pub struct CulturalValues {
// NPC blueprint (output — generator produces these)
// ---------------------------------------------------------------------------
/// Internal motive/state driving an NPC's micro-behavior (#632).
///
/// The player never sees this directly — it leaks through the observable
/// behavior as a tell. The gap between what an NPC is doing and WHY is
/// the asymmetric information mechanic (D-010 principle 2).
///
/// `Neutral` means no notable motive — the NPC is simply doing their job.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum NpcWant {
/// Nothing stands out; routine work mode.
Neutral,
/// Disengaged from current task; attention elsewhere.
Bored,
/// Heightened attention to the environment; scanning, tracking.
Alert,
/// Something is wrong or off; watching without being obvious about it.
Suspicious,
/// Steering clear of a specific person in the zone.
AvoidingSomeone,
/// Actively trying to pick up information by proximity or conversation.
LookingForInfo,
}
/// Generator output for a single NPC.
///
/// Maps to the 10-axis model (D-024) but as a serializable data record,
@@ -163,7 +186,12 @@ pub struct NpcBlueprint {
pub role: String,
/// Personality traits (2-3, no contradictory pairs).
pub traits: Vec<PersonalityTrait>,
/// Internal motive that biases observable behavior (#632).
/// The player never sees this label — they infer it from the behavior.
pub want: NpcWant,
/// Observable behaviors the player can witness.
/// Index 0 is the primary role/relationship-driven action.
/// Index 1 (if present) is the Want tell — the leak of the internal state.
pub observable_behaviors: Vec<String>,
/// Cultural markers derived from the culture profile.
pub cultural_markers: CulturalMarkers,
@@ -301,6 +329,7 @@ mod tests {
name: "Kael".into(),
role: "dock_worker".into(),
traits: vec![PersonalityTrait::Bold, PersonalityTrait::Honest],
want: NpcWant::Alert,
observable_behaviors: vec!["works efficiently".into()],
cultural_markers: CulturalMarkers {
speech_register: "direct".into(),