Skip to content

Gossip Propagation System

The Gossip System enables NPCs to spread knowledge to each other organically. When an NPC witnesses an event, that knowledge can propagate to nearby NPCs through gossip, creating a living information network where news travels realistically through the game world.

Overview

graph TD
    A[World Event] --> B["KnowledgeObservationSystem<br/>(available, not default-registered)"]
    B --> C[KnowledgeManager.add]
    A2[Admin / Content Pack] --> C
    C --> D[KnowledgeLearnedEvent]
    C --> E[NPC Knowledge Store]
    E --> F[GossipSystem]
    F --> G{"Same room? NEUTRAL+ tier?"}
    G -->|Yes| H[KnowledgeManager.propagate]
    H --> I[GossipSpreadEvent]
    H --> J[Receiver's Knowledge Store]
    G -->|No| K[Skip]

Two components are involved:

  1. KnowledgeObservationSystem — Converts world events into NPC knowledge entries. This system exists in maid_stdlib.knowledge.observation but is not registered by default; ClassicRPGContentPack only registers GossipSystem. Content packs can register it manually if automatic knowledge creation from world events is desired.
  2. GossipSystem — Spreads existing knowledge between NPCs who share a room (registered by ClassicRPGContentPack)

KnowledgeObservationSystem

This system subscribes to world events and creates knowledge entries for NPCs who witness them.

Observed Events

Event Knowledge Created TTL
CombatStartEvent "X attacked Y" 1 hour
EntityDeathEvent "X was defeated by Y" / "X died" 2 hours
ItemPickedUpEvent "X picked up item" 30 minutes
ItemDroppedEvent "X dropped item" 30 minutes
RoomEnterEvent "X arrived" 15 minutes
RoomLeaveEvent "X departed" 15 minutes

How It Works

When an event fires, the system:

  1. Identifies NPCs in the same room as the event
  2. Creates a KnowledgeEntry with appropriate subject, predicate, and object
  3. Stores it via KnowledgeManager.add()
  4. The manager emits a KnowledgeLearnedEvent
# Example: A combat event creates knowledge for nearby NPCs
KnowledgeEntry(
    subject="warrior_player",
    predicate="attacked",
    object="goblin_chief",
    confidence=1.0,           # Witnessed firsthand
    source_npc_id="bartender",
    source_type="observed",
    ttl_seconds=3600,         # Expires in 1 hour
)

GossipSystem: Propagation Mechanics

The GossipSystem is an ECS System that runs each tick and spreads knowledge between NPCs.

Pair Selection

Every rebuild_ticks (default 30), the system rebuilds its gossip pair cache:

  1. Scans all NPCs with a position component
  2. Groups NPCs by room
  3. Creates pairs from NPCs sharing the same room
  4. Filters out pairs where the relationship is below NEUTRAL tier
  5. Missing relationships (no relationship record) are treated as neutral and allowed to gossip

Exchange Process

Each tick, up to exchanges_per_tick (default 3) gossip exchanges occur:

  1. Select a pair from the cached list
  2. Validate — both NPCs still exist and share a room
  3. Check cooldown — pairs can't gossip again for cooldown_ticks (default 10)
  4. Select knowledge — pick sender's knowledge that the receiver doesn't already have
  5. Compute distortion — based on sender-receiver friendliness
  6. Build GossipPacket and call KnowledgeManager.propagate()
  7. Set cooldown and emit GossipSpreadEvent

Confidence and Distortion

Each time knowledge is gossiped, its confidence degrades:

new_confidence = original_confidence × (1.0 - distortion_factor)

The distortion factor is derived from the relationship's friendliness dimension (on the -100 to 100 scale):

distortion = clamp((100 - friendliness) / 200, min=0.05, max=0.50)
  • High friendliness (100) → low distortion (0.05)
  • Neutral friendliness (0) → high distortion (0.50)
  • Low friendliness (-100) → max distortion (0.50, clamped)

Distortion is clamped between 0.05 and 0.50. Knowledge with confidence below the propagation threshold is not spread further.

Note

The GossipSystem (in maid_stdlib.knowledge.gossip) uses friendliness for distortion. The separate KnowledgeGossipProcessor (in maid_classic_rpg.systems.npc.gossip) uses trust with the formula 0.5 - 0.4 * trust (trust normalized to 0..1). Missing relationships are treated as neutral (friendliness=0 → distortion=0.5, trust=0.5 → distortion=0.3).

Stale Knowledge Expiry

Every maintenance_ticks (default 100), the system calls KnowledgeManager.expire_stale() to remove knowledge entries past their TTL. This prevents NPCs from gossiping about ancient events indefinitely.

Configuration

Engine Settings (MAID_MEMORY_ prefix)

# Maximum gossip exchanges per game tick
MAID_MEMORY_GOSSIP_BUDGET_PER_TICK=3

# Ticks before the same NPC pair can gossip again
MAID_MEMORY_GOSSIP_COOLDOWN_TICKS=10

Warning

These settings are defined in MemorySettings but are not currently passed to GossipSystem by ClassicRPGContentPack. The system uses its own constructor defaults. This is a known limitation.

GossipSystem Constructor Defaults

These are set when the system is instantiated (typically by the content pack):

Parameter Default Description
exchanges_per_tick 3 Max gossip exchanges per tick
cooldown_ticks 10 Cooldown between same-pair gossip
rebuild_ticks 30 How often to rebuild pair cache
maintenance_ticks 100 How often to expire stale knowledge

NPC Archetype Tuning

Individual NPC archetypes can define a gossip_tendency value:

# In archetypes.yaml
archetypes:
  - archetype_id: bartender
    display_name: Bartender
    gossip_tendency: 0.8    # Very chatty

  - archetype_id: hermit
    display_name: Hermit
    gossip_tendency: 0.1    # Rarely shares information

Note

The gossip_tendency field is defined on archetypes and loaded from YAML, but is not currently used by GossipSystem to influence pair selection or exchange likelihood. It is available for custom systems or future use.

Quest System Integration

The quest generation system uses a gossip confidence cap:

# In maid_classic_rpg.systems.quests.generation.constants
GOSSIP_CONFIDENCE_CAP = 0.9  # Quest-injected gossip starts at max 0.9 confidence

This prevents quest-generated knowledge from appearing as "absolute truth."

Events

KnowledgeLearnedEvent

Emitted when an NPC gains new knowledge (from observation or gossip):

@dataclass
class KnowledgeLearnedEvent(Event):
    npc_id: str
    subject: str
    predicate: str
    object: str
    source_type: str      # "observed" or "gossip"
    confidence: float

GossipSpreadEvent

Emitted when knowledge successfully propagates between NPCs:

@dataclass
class GossipSpreadEvent(Event):
    sender_npc_id: str
    receiver_npc_id: str
    subject: str
    predicate: str
    confidence: float
    distortion_factor: float

Subscribe to these events to build reactive systems (e.g., quest triggers, NPC reactions).

Admin Commands

@gossip status

Shows the current state of the gossip system. The command retrieves the system via ctx.world.get_data("gossip_system"):

> @gossip status
Gossip pairs: 12, cooldowns: 3

Warning

ClassicRPGContentPack registers the GossipSystem as an ECS system but does not call world.set_data("gossip_system", ...). Unless a content pack explicitly sets this world data key, @gossip status will report "Gossip system is not active."

View and manage the knowledge that gossip spreads:

> @knowledge npc_bartender
Knowledge (8):
- warrior_player attacked goblin_chief (confidence=0.95)
- stranger arrived from east road (confidence=0.72)
  ...

> @knowledge add npc_bartender dragon spotted_near "mountain pass"
Added knowledge entry for Bartender.

Examples

Scenario: News Travels Through a Village

  1. A player fights a bandit near the village gate.
  2. The gate guard NPC witnesses this → KnowledgeObservationSystem creates knowledge: "player defeated bandit" (confidence: 1.0).
  3. Next tick, the gate guard and merchant share the gate area. GossipSystem propagates: "player defeated bandit" (confidence: 0.95, distortion 0.05 from high friendliness).
  4. Later, the merchant moves to the tavern. The merchant and bartender gossip: "player defeated bandit" (confidence: 0.475, distortion 0.50 from neutral friendliness).
  5. When the player visits the tavern and talks to the bartender, the EnrichedPromptBuilder injects: "You've heard rumors that this player defeated a bandit near the gate."

Scenario: Distortion in Action

  1. An NPC observes: "wizard cast fireball at dragon" (confidence: 1.0)
  2. First gossip (high friendliness, distortion=0.05): confidence → 0.95
  3. Second gossip (neutral friendliness, distortion=0.50): confidence → 0.475
  4. Third gossip (neutral friendliness, distortion=0.50): confidence → 0.2375
  5. Fourth gossip (neutral friendliness, distortion=0.50): confidence → 0.119
  6. Fifth gossip (neutral friendliness, distortion=0.50): confidence → 0.059 — below propagation threshold (0.1), chain ends

With neutral friendliness, gossip can survive several hops before confidence drops below the 0.1 threshold.

Scenario: Stale Knowledge Cleanup

  1. NPC learns "stranger arrived" with TTL of 900 seconds (15 minutes)
  2. After 15 minutes of game time, expire_stale() removes the entry
  3. NPCs stop gossiping about old arrivals, keeping conversations relevant

Architecture Notes

The Classic RPG content pack also includes a KnowledgeGossipProcessor (in maid_classic_rpg.systems.npc.gossip) that provides directed gossip capabilities for specific NPC interactions (e.g., an NPC intentionally telling another about something). This processor is wired into SocialFabricSystem by ClassicRPGContentPack.on_load(). It uses the same KnowledgeManager.propagate() path but is triggered by NPC AI decisions rather than the automatic GossipSystem, and uses trust-based distortion instead of friendliness-based.

See Also