Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
571f581
sprint 022: plan — intentions and travel
vladmesh Jul 10, 2026
5a60a22
sprint 022 phase 1: task breakdown
vladmesh Jul 10, 2026
080c24f
sprint 022 phase 1 task 1: own dice RNG per session
vladmesh Jul 10, 2026
687233b
sprint 022 phase 1 task 2: gate world mutations
vladmesh Jul 10, 2026
1fd8c2d
sprint 022 phase 1 task 3: unify save snapshots
vladmesh Jul 10, 2026
e534475
sprint 022 phase 1 task 4: make load lifecycle atomic
vladmesh Jul 10, 2026
e7aab3f
sprint 022 phase 1: close after reconnect fix
vladmesh Jul 10, 2026
ff22cbb
sprint 022 phase 2: task breakdown
vladmesh Jul 10, 2026
387f9aa
sprint 022 phase 2 task 1: persist anchors and intents
vladmesh Jul 10, 2026
84d4578
sprint 022 phase 2 task 2: drive activation by anchors
vladmesh Jul 10, 2026
30becfc
sprint 022 phase 2 task 3: complete timed rests on wake
vladmesh Jul 11, 2026
01857a4
sprint 022 phase 2: close anchors and timed intents
vladmesh Jul 11, 2026
f216c97
sprint 022 phase 3: task breakdown
vladmesh Jul 11, 2026
ab5e128
sprint 022 phase 3 task 1: persist travel routes
vladmesh Jul 11, 2026
69e2c79
sprint 022 phase 3 task 2: advance travel intents
vladmesh Jul 11, 2026
1b647d4
sprint 022 phase 3 task 3: show journey status
vladmesh Jul 11, 2026
ccf10fe
sprint 022 phase 3: close travel intent
vladmesh Jul 11, 2026
adf219b
sprint 022 phase 4: task breakdown
vladmesh Jul 11, 2026
06b8b71
sprint 022 phase 4 task 1: interrupt active intents
vladmesh Jul 11, 2026
a0484d6
sprint 022 phase 4 task 2: pin interruption lifecycle through session…
vladmesh Jul 11, 2026
44b688a
sprint 022 phase 4 task 3: target-aware attack accessible names + jou…
vladmesh Jul 11, 2026
eef9303
sprint 022 phase 4: close — interruptible journeys
vladmesh Jul 11, 2026
ae7c60d
audit: full codebase - 15 issues found
vladmesh Jul 11, 2026
3a8e9d9
sprint 022 phase 5: task breakdown
vladmesh Jul 11, 2026
a287d30
sprint 022 phase 5 task 1: bound round shutdown
vladmesh Jul 11, 2026
1ebdf9e
sprint 022 phase 5 task 2: handle lifecycle stop failures
vladmesh Jul 11, 2026
9f84fd4
sprint 022 phase 5: close — bounded round shutdown
vladmesh Jul 11, 2026
70e44c2
audit: full codebase - 13 issues found
vladmesh Jul 11, 2026
8cb4aae
sprint 022: audit triaged
vladmesh Jul 11, 2026
ed32eff
sprint 022: post-audit E2E
vladmesh Jul 11, 2026
7575bd3
backlog: contain action errors in round loop
vladmesh Jul 11, 2026
236442b
docs: update living docs (full sync, 9 files)
vladmesh Jul 11, 2026
019ddda
sprint 022: close — intents and travel
vladmesh Jul 11, 2026
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
8 changes: 4 additions & 4 deletions .claude/skills/update-docs/state.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"last_run": "2026-07-10",
"last_run": "2026-07-12",
"last_run_mode": "full",
"last_commit": "cb81afb",
"last_full_run": "2026-07-10"
}
"last_commit": "7575bd3",
"last_full_run": "2026-07-12"
}
10 changes: 5 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ Round orchestrator (round.py):
run_loop:
while not stopped:
update_activation → get active creatures
if none active → fast_forward to nearest wake_at or exit
if none active → fast_forward to nearest intent boundary or exit
run_round → on_round_end callback

Player input flow (service/, command-based):
Expand Down Expand Up @@ -176,15 +176,15 @@ Calendar: 30 days/month, 12 months/year.

```
Entity (id, name, location_id, active, on_tick)
└── Creature (ability_scores, HP, AC, in_combat, is_dodging, is_disengaging, wake_at_seconds, brain, turn_budget, combat_position, equipped_weapon, equipped_armor, equipped_shield, equipped_head, equipped_feet, equipped_ring, resource_pools, faction_id, reputation, squad_id, xp_value, execute_action)
└── Creature (ability_scores, HP, AC, in_combat, is_dodging, is_disengaging, is_anchor, current_intent, brain, turn_budget, combat_position, equipment, resource_pools, faction_id, reputation, squad_id, xp_value, execute_action)
└── Character (race, class, alignment, gold, appearance, class_features, level, experience, level_up_available, perceive_by_id, get_npc_data)
├── PlayerCharacter (interactive I/O, overrides take_turn directly)
└── Npc (role, personality, schedule, memory: NpcMemory, ai_type — brain assigned by content_loader/adapter)
```

All tracked entities live on the `EntitiesLayer`. The layer's `tick()` is a no-op — the Round orchestrator calls `run_creature_turn` directly for both combat and peaceful turns. `Entity.on_tick(hour)` is called by the Round to update NPC activity based on daily schedule.

**Activation system:** `EntitiesLayer.update_activation(time)` runs at the start of each round. Players without `wake_at_seconds` are anchors; creatures at an anchor's location become active, all others go dormant. Creatures in combat stay active. NPCs are moved to their scheduled location when activated. The `wait` action sets `wake_at_seconds` on the creature and marks it dormant. When no active creatures exist, `Round.run_loop()` fast-forwards `World.advance_time()` to the nearest `wake_at`, re-runs activation, and continues. If nobody has a `wake_at`, the loop exits.
**Activation system:** `EntitiesLayer.update_activation(time)` runs at the start of each round. Any living creature with `is_anchor=True` and no `current_intent` holds its location active; creatures at an anchor's location become active, all others go dormant. Creatures in combat stay active. NPCs are moved to their scheduled location when activated. Wait and sleep use persisted `TimedIntent`; travel uses persisted `TravelIntent` with a destination, remaining route, and next edge-arrival boundary. `Round.run_loop()` fast-forwards to the nearest intent boundary when no creature needs a turn. Travel advances one graph edge per boundary and can be interrupted by damage, combat, or arrival in an occupied scene.

`World.location_graph` (`LocationGraph`) provides a flat graph of all locations. Each `Location` node has a `region_id` tag (for weather/terrain lookups) and an optional `settlement_id` tag (for economy/NPC binding). Entities hold a `location_id` and the graph resolves which region/settlement they are in. Edges between locations carry distances in meters; `travel_seconds()` computes travel time.

Expand Down Expand Up @@ -277,9 +277,9 @@ Centralized derived stat computation replacing ad-hoc logic scattered across com

**Save format** (Sprint 021): one versioned Pydantic envelope — `SaveGame(schema_version=1, meta, world)` in `storage/save_schema.py`. `WorldSave` carries the world seed, dice RNG state, time, last tick times, and typed layer states: each layer owns a state model (`layers/*/state.py`, `layers/entities/save_models.py`) that is the authoritative format (`extra="forbid"`), while the `Layer` ABC keeps its dict-facing `get_state()/load_state()` signatures (core stays pydantic-free). Entity payloads are a discriminated union on `entity_type` (`PlayerSave`/`NpcSave`/`CreatureSave`/`ContainerSave`) built directly from live objects in `entity_serialization.py`; combat state persists turn order, round, battle map, and sides. `save_game()` and `autosave_session()` build the same envelope; `load_game()` validates it and rejects legacy saves without `schema_version`.

**Reproducibility**: `DND_WORLD_SEED` (env; random + logged when absent) seeds the world in `game_service`per-layer seeds are derived deterministically and passed to layer constructors, which own their `random.Random` streams (weather, politics, ecology roam/retreat/lair depletion, entity encounters). The dice RNG (`rules/dice.py`, `DND_DICE_SEED`) is a separate stream. All RNG states are serialized into the save, so a loaded game continues the same random sequences — same seed, same content identical world evolution (pinned by `tests/unit/test_world_seed.py`).
**Reproducibility**: `DND_WORLD_SEED` (env; random + logged when absent) seeds the world in `game_service`; per-layer seeds are derived deterministically and passed to layer-owned `random.Random` streams. Each `GameSession` owns its dice RNG (`DND_DICE_SEED` supplies the initial seed), so concurrent sessions cannot shift one another's rolls. All RNG states are serialized into the save, so a loaded game continues the same random sequences. Same seed plus the same content produces identical world evolution.

**Autosave**: per-action (create_player), on session evict, on shutdown, plus a periodic lifespan task every `DND_AUTOSAVE_SECONDS` (default 120, cancelled before the final shutdown autosave). Autosave failures are logged, never suppressed.
**Autosave and round lifecycle**: per-action (create_player), session evict, shutdown, and periodic autosave all take a session snapshot through the same world-mutation gate used by round actions. File I/O happens after the snapshot is built. Load stops the old round before replacing world/RNG state and resumes only after a player reconnects. Round shutdown has a bounded timeout (`DND_ROUND_STOP_TIMEOUT_SECONDS`, default 5); on timeout the session retains the live round/thread references and load or eviction aborts safely. Autosave failures are logged, never suppressed.

## Logging

Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ frontend/ — React + TypeScript SPA (Vite, shadcn/ui, Zustand)

### Entity Hierarchy

`Entity` (id, name, location_id, active, on_tick) → `Creature` (ability scores, HP, AC, speed, attacks, conditions, inventory, gold, in_combat, is_dodging, is_disengaging, wake_at_seconds, brain, turn_budget, combat_position, equipped_weapon, equipped_armor, equipped_shield, equipped_head, equipped_feet, equipped_ring, resource_pools, faction_id, reputation, xp_value) → `Character` (race, class, alignment, class_features, level, experience, level_up_available) → `PlayerCharacter` / `Npc`. Creature delegates decisions to `brain.choose_action()`. `Container` (`core/container.py`) is a separate `Entity` sibling of `Creature` — inventory + gold, no HP/turn/brain — used for lair treasuries and other lootable world objects (`EntityKind.CONTAINER`). The `perceive()` method controls what information an observer sees about a target — LLM prompts never receive raw character data. All tracked entities live on the `EntitiesLayer`. `World.location_graph` (`LocationGraph`) maps locations to regions/settlements; entities reference `location_id`, and the graph resolves which region/settlement a location belongs to. NPCs have structured memory (`NpcMemory`: tags, recent, inner_state, current_conversation) readable by both LLM and RuleBrain; a `MemorySummarizer` compresses events into memory via LLM after combat/conversation ends. Combat is managed via `CombatState` (initiative order, round tracking, combat sides, auto-exit after 2 idle rounds) and `BattleMap` (2D grid with positions, walls, and movement). Movement rules live in `rules/movement.py` (D&D 5e diagonal distance, wall collision, occupied-cell blocking). `move_to(x, y)` action uses BFS pathfinding (`find_path`) and budget-aware stepping (`step_cost`); player-only (frontend click-to-move), excluded from LLM action schemas via `provider_managed=True`.
`Entity` (id, name, location_id, active, on_tick) → `Creature` (physical stats, combat state, `is_anchor`, persisted `current_intent`, brain, turn budget, equipment and resources) → `Character` (race, class, alignment, class features, level and XP) → `PlayerCharacter` / `Npc`. Creature delegates decisions to `brain.choose_action()`. `Container` (`core/container.py`) is a separate `Entity` sibling of `Creature`: inventory + gold, no HP/turn/brain, used for lair treasuries and other lootable world objects (`EntityKind.CONTAINER`). The `perceive()` method controls what an observer sees; LLM prompts never receive raw character data. All tracked entities live on `EntitiesLayer`. `World.location_graph` maps locations to regions/settlements and supplies weighted routes for travel intents. NPCs have structured memory (`NpcMemory`) readable by both LLM and RuleBrain. Combat is managed via `CombatState` and `BattleMap`; grid movement rules live in `rules/movement.py`.

### Multi-Action Turns

Expand Down Expand Up @@ -133,7 +133,7 @@ Centralized derived stat computation (`core/modifiers.py` data types, `rules/mod

### Activation & Fast-Forward

Proximity-based activation: `EntitiesLayer.update_activation(time)` runs at the start of each round. Players without `wake_at_seconds` are anchors — creatures at an anchor's location become active, all others go dormant. Creatures in combat stay active regardless. `wait` action sets `creature.wake_at_seconds` and marks it dormant. When no active creatures exist, `Round.run_loop()` fast-forwards time to the nearest `wake_at`, then re-checks activation. Content requires explicit locations — no auto-generation from regions. On entering a location with an encounter table, `ActivationManager` rolls encounters (see Lairs, Encounters & Loot).
Anchor-based activation: `EntitiesLayer.update_activation(time)` runs at the start of each round. Any living creature with `is_anchor=True` and no current intent holds its scene active; creatures at an anchor's location become active, all others go dormant. Creatures in combat stay active regardless. Wait, sleep, and travel are typed, persisted `current_intent` values. When no creature needs a turn, `Round.run_loop()` fast-forwards to the nearest timer or travel-leg boundary. Travel follows deterministic shortest paths one graph edge at a time and can be interrupted by damage, combat, or an occupied scene. Content requires explicit locations. On entering a location with an encounter table, `ActivationManager` rolls encounters (see Lairs, Encounters & Loot).

### Lairs, Encounters & Loot

Expand Down Expand Up @@ -166,6 +166,7 @@ Kill reputation drop (`rules/reputation.py`): omniscient, delta scaled by victim
- `DND_LANGUAGE` env var selects game language (default: `ru`); locale files in `src/dnd_simulator/locale/`
- `DND_WORLD_SEED` env var seeds world simulation layers; when absent, `GameService` logs the generated seed.
- `DND_AUTOSAVE_SECONDS` env var controls periodic autosave interval (default: `120`; must be greater than `0`).
- `DND_ROUND_STOP_TIMEOUT_SECONDS` bounds round-thread shutdown before load/eviction aborts safely (default: `5`).
- Save files: `saves/` directory (JSON)
- Backend API: `make serve` → http://localhost:8001/docs (Swagger UI)
- Frontend: `make frontend` → http://localhost:5173 (entry point, proxies /api to :8001)
Loading
Loading