Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions simulation/character_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"""
Character memory system for a simulation.

Each of the 5 characters maintains a personal memory bank. The `memory_call`
function retrieves contextually relevant memories for a given character based
on keyword matching and recency, simulating how a character "remembers" things
during a scene.
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class Memory:
content: str
tags: list[str]
importance: int # 1 (trivial) – 10 (critical)
timestamp: float = field(default_factory=time.time)
recalled_count: int = 0

def relevance_score(self, context_tags: list[str]) -> float:
tag_overlap = len(set(self.tags) & set(context_tags))
recency_bonus = 1.0 / (1.0 + (time.time() - self.timestamp) / 3600)
return tag_overlap * self.importance + recency_bonus


@dataclass
class Character:
name: str
role: str
backstory: str
memories: list[Memory] = field(default_factory=list)

def remember(self, content: str, tags: list[str], importance: int = 5) -> None:
"""Store a new memory."""
self.memories.append(Memory(content=content, tags=tags, importance=importance))

def __repr__(self) -> str:
return f"<Character name={self.name!r} role={self.role!r} memories={len(self.memories)}>"


def memory_call(
character: Character,
context: str,
top_k: int = 3,
min_importance: int = 1,
) -> list[Memory]:
"""
Retrieve the most relevant memories for *character* given a natural-language
*context* string.

Parameters
----------
character : The character whose memory bank is searched.
context : A phrase or sentence describing the current scene/situation.
top_k : Maximum number of memories to return.
min_importance: Only consider memories at or above this importance level.

Returns
-------
A list of Memory objects sorted by relevance (highest first).
"""
context_tags = [word.lower().strip(".,!?") for word in context.split()]

candidates = [m for m in character.memories if m.importance >= min_importance]
ranked = sorted(candidates, key=lambda m: m.relevance_score(context_tags), reverse=True)

results = ranked[:top_k]
for m in results:
m.recalled_count += 1
return results


# ---------------------------------------------------------------------------
# 5 pre-built characters with seed memories
# ---------------------------------------------------------------------------

def _build_characters() -> dict[str, Character]:
characters: dict[str, Character] = {}

# 1 — Aria (Scout / Ranger)
aria = Character(
name="Aria",
role="Scout",
backstory="A sharp-eyed ranger who grew up on the forest edge and has survived three wars.",
)
aria.remember("Spotted enemy scouts near the northern ridge at dusk.", ["enemy", "scout", "north", "ridge", "dusk"], importance=8)
aria.remember("Found a hidden passage behind the waterfall leading to the old keep.", ["passage", "waterfall", "keep", "hidden"], importance=9)
aria.remember("Lost her partner Kaen to a trap near the iron gate.", ["loss", "trap", "gate", "kaen"], importance=10)
aria.remember("The forest at night becomes impassable without a fire-stone.", ["forest", "night", "fire", "stone"], importance=6)
aria.remember("There is a safe camp two hours east of the broken bridge.", ["camp", "east", "bridge", "safe"], importance=7)
characters["aria"] = aria

# 2 — Dex (Engineer / Inventor)
dex = Character(
name="Dex",
role="Engineer",
backstory="A tinkerer from the capital who specialises in mechanical traps and explosive devices.",
)
dex.remember("The city's power grid runs through sublevel 3 — a single overload would black it out.", ["power", "grid", "sublevel", "city", "overload"], importance=9)
dex.remember("Prototype K-7 destabilises when exposed to high humidity.", ["prototype", "humidity", "unstable", "k7"], importance=8)
dex.remember("Traded the schematics for the vault door in exchange for safe passage.", ["schematics", "vault", "door", "trade"], importance=7)
dex.remember("The foreman owes him a favour after the Morrow bridge incident.", ["foreman", "favour", "bridge", "morrow"], importance=5)
dex.remember("Found an uncharted tunnel beneath the old foundry.", ["tunnel", "foundry", "underground", "hidden"], importance=8)
characters["dex"] = dex

# 3 — Lyra (Healer / Alchemist)
lyra = Character(
name="Lyra",
role="Healer",
backstory="A travelling alchemist who has studied ancient texts on medicinal herbs and poisons.",
)
lyra.remember("Moon-petal extract neutralises iron-rot poison within one hour.", ["poison", "iron", "rot", "cure", "moon", "petal"], importance=9)
lyra.remember("The plague that hit Silverfen in 412 was engineered, not natural.", ["plague", "silverfen", "engineered", "disease"], importance=10)
lyra.remember("Stored emergency antidotes beneath the healer's tent — third crate from the left.", ["antidote", "tent", "crate", "emergency"], importance=8)
lyra.remember("Treated the general's wound two seasons ago; he trusts her implicitly.", ["general", "trust", "wound", "ally"], importance=6)
lyra.remember("Nightshade tea induces vivid visions when brewed under starlight.", ["nightshade", "tea", "vision", "starlight"], importance=5)
characters["lyra"] = lyra

# 4 — Marcus (Commander / Veteran)
marcus = Character(
name="Marcus",
role="Commander",
backstory="A seasoned military commander who has led armies for twenty years and never retreated.",
)
marcus.remember("The enemy commander, General Voss, always overextends his left flank.", ["enemy", "general", "voss", "flank", "tactics"], importance=9)
marcus.remember("Lost the siege at Ironhold because of a supply route ambush.", ["siege", "ironhold", "supply", "ambush", "defeat"], importance=10)
marcus.remember("Soldiers morale breaks when they go three days without hot food.", ["morale", "soldiers", "food", "supply"], importance=7)
marcus.remember("Signed a quiet alliance with the river lords — they will send archers if asked.", ["alliance", "river", "archers", "reinforcement"], importance=8)
marcus.remember("The old barracks east of the city has a hidden armory stocked for 300 men.", ["barracks", "east", "armory", "weapons", "hidden"], importance=9)
characters["marcus"] = marcus

# 5 — Sable (Spy / Infiltrator)
sable = Character(
name="Sable",
role="Spy",
backstory="A ghost operative who has infiltrated courts, guilds, and criminal syndicates for a decade.",
)
sable.remember("The guild master uses a rotating cipher key — changes every new moon.", ["guild", "cipher", "key", "code", "moon"], importance=9)
sable.remember("Lord Cayne is secretly funding the rebel faction in the east.", ["cayne", "rebel", "east", "funding", "secret"], importance=10)
sable.remember("The palace kitchen staff change shifts at midnight — that's the blind spot.", ["palace", "kitchen", "midnight", "shift", "blind"], importance=8)
sable.remember("Used the alias 'Mira Vane' in the northern ports; cover is still intact.", ["alias", "mira", "vane", "north", "cover"], importance=7)
sable.remember("The informant in the docks will only meet at the second bell after low tide.", ["informant", "docks", "tide", "meeting"], importance=8)
characters["sable"] = sable

return characters


CHARACTERS = _build_characters()


# ---------------------------------------------------------------------------
# Demo
# ---------------------------------------------------------------------------

if __name__ == "__main__":
scenarios = [
("aria", "We need to find a hidden passage to reach the keep quickly."),
("dex", "The city power might be our way in — what do you know about the grid?"),
("lyra", "Someone has been poisoned — do we have any cure or antidote?"),
("marcus", "We are running low on supplies and morale is dropping fast."),
("sable", "We need to infiltrate the palace tonight — what's the best window?"),
]

for char_key, context in scenarios:
character = CHARACTERS[char_key]
print(f"\n{'='*60}")
print(f"[{character.name} | {character.role}] context: \"{context}\"")
print(f"{'='*60}")
recalled = memory_call(character, context, top_k=2)
if recalled:
for i, mem in enumerate(recalled, 1):
print(f" [{i}] (importance={mem.importance}) {mem.content}")
else:
print(" (no relevant memories found)")