Commit Graph
13 Commits
Author SHA1 Message Date
parththakkar106andClaude Opus 5 2c57b1ceab Remove three kinds of duplication in the backend
Stage 2, items 1, 4, and 5 of `plan/17-refactor.md`.

**One path resolver in `worldstate`.** `apply_delta` and `apply_override` routed
`flags.<name>`, `milestones.<id>`, `world.<stat>`, `player.<stat>`, and
`npc.<id>.<stat>` with parallel code, about 100 lines each. `_resolve` now says
what a path points at and returns either a target or the rejection to report.
Each function keeps its own write rule, because the rules genuinely differ: an
override sets a number rather than adding to it, ignores `cooldown`,
`max_delta_per_turn`, and the rule that a counter only counts up, and can un-set
a milestone.

A differential check ran both implementations over 3960 payloads: twenty paths,
fourteen values, three starting states, plus every three-path combination. The
results are identical except that 674 rejections from `apply_override` now carry
a `fix` string. `apply_delta` already worded those, and the world-state editor
renders them, so an override that names an unknown flag now explains itself the
way a delta does.

**`sse`, `SSE_HEADERS`, and `turn_error` move to `app/sse.py`.** Two routers
stream, and `chat.py` had to import from `routers.adventures` to reach them.

**`get_adventure_or_404` becomes the `current_adventure` dependency.** All 32
handlers repeated the call as their first statement. The ownership check now
reads in the signature and runs before the body. FastAPI caches a dependency for
one request, so the handler's `db` is the session the adventure came from.

The generated OpenAPI document is byte-identical except on `rename_branch`,
where `branch_id` is now listed before `adventure_id`, because that handler no
longer names `adventure_id` itself. Parameter order in the document is
cosmetic.

Six tests in `test_state_revert.py` call `undo_turn` and `retry_action`
directly rather than over HTTP. They pass the adventure they already hold
instead of an id.

549 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Dix4oGV3njgWRdu7P9t6r
2026-08-29 02:04:02 +05:30
parththakkar106andClaude Opus 5 8422ff24f6 Split the world-state engine into four modules
`backend/app/worldstate/engine.py` held 918 lines covering four separate jobs:
reading a scenario's schema, parsing the block the model writes, applying a
change within the schema's limits, and rendering state as prompt text. Each is
now its own module, the largest 418 lines.

An AST comparison against the old file confirms all 29 definitions are
identical. No call site changes, because `worldstate/__init__.py` exports the
same names it did before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Dix4oGV3njgWRdu7P9t6r
2026-08-29 01:09:41 +05:30
parththakkar106andClaude Opus 5 47c7800903 Report the world-state changes the engine refuses
`apply_delta` records three outcomes for every change the model sends:
`applied`, `clamped`, and `rejected`. Everything downstream read only
`applied`. A refused change reached the player as an ordinary chip, and
reached the model on the next turn as a change that had succeeded.

Five parts:

- `Action.world_changes` reads `clamped` and `rejected` beside `applied`.
  Accepted stats carry a `clamped` flag; refusals become `kind: "rejected"`
  entries. The `fix` key is present only when the engine wrote one, because
  this property runs for every action of every list response.
- The UI separates the three outcomes. A clamp to a standstill reads
  `no change - at its limit` on a dashed chip, a partial clamp is marked
  `(limited)`, and a rejection carries its reason. Dashed and dimmed rather
  than red: a refused change means the rules are working.
- The goals line names the milestone id, as `milestones.<id>`. The ids
  appeared nowhere in the prompt before, so the model could not send one.
- Each rejection, and each clamp that moved nothing, builds a `fix` string
  from the stat definition at the point of refusal. `render_refusals()`
  renders them into the next prompt above `EMIT_REMINDER`.
- `_history_text` replays `applied_delta()` instead of the sent delta, so a
  past turn's state block shows only what the engine accepted.

A clamp that reduced a change but still moved the value reports nothing. If
you tell a model its 80 damage became 30, it can treat the shortfall as a
debt and send the remaining 50 next turn, which is the swing
`max_delta_per_turn` prevents.

In the demo scenario, `pokemon_left` becomes `pokemon_fainted`
(`type: counter`, `initial: 0`). Starting at the ceiling turned a wrong-signed
delta into a silent no-op; counting up puts the wrong sign on the counter
rule, which refuses it out loud. The instructions also now ask for
`world.turn`, which sat at 0 for a whole playtest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PacdRuPXSkQQy4ZYdH32hF
2026-08-28 16:26:34 +05:30
Parth e7d75c3b05 Rewrite Python comments in Google developer documentation style (#12)
* Rewrite comments in Google developer documentation style

Rewrite the comments and docstrings across the backend core modules so they
read plainly. The previous prose was accurate but dense and figurative, which
made it slow to skim.

Applies the Google developer documentation style guide: short sentences, active
voice, present tense, American spelling, and no metaphors, idioms, or
rhetorical asides. Replaces em-dash chains with separate sentences.
2026-08-26 15:37:25 +05:30
parththakkar106andClaude Opus 5 aed28d9295 Add "Update from scenario" to pull scenario edits into a running adventure
An adventure copies its scenario's plot text and story cards at creation so
later authoring never disturbs a story in progress. This is the explicit
opt-out, alongside the existing per-script "Sync from library".

GET /adventures/{id}/refresh returns a plan (per-field old/new diff, card
add/update/remove, world-state added/removed paths, and any ${...} answers
still needed); POST applies it under the turn lock so it can't race a
generating turn. The Plot panel shows the plan in a confirm modal first.

Overwrites the plot fields and scenario-derived cards. Deliberately left
alone: the opening `start` action (the story is built on it, and it is baked
into memories and the summary), the adventure's own title and summary,
player-authored story cards, and the live value of every stat the schema
still defines.

Two enablers were needed:

- adventures.placeholders (migration 32). ${...} answers were used once at
  creation and discarded, so re-copying scenario text would have re-injected
  a literal ${Hero}. Adventures predating the column re-prompt once via the
  existing modal, then the answers are saved.

- story_cards.source_ref (migration 33), "card:<id>" / "npc:<key>", NULL for
  player-authored. Adventure cards had no link back to their source, so a
  rename read as delete-plus-add and player cards would have been clobbered.
  Legacy cards name-match once, then adopt the ref.

World state goes through a new worldstate.reconcile(): keep values the schema
still defines, add missing ones at their initial, drop removed ones and their
cooldown bookkeeping. Not instantiate(), which would heal the player to full
and wipe their milestones.

The confirm modal is portalled to <body>: .side-panel's panel-in animation
has fill mode `both`, which makes it the containing block for position:fixed
descendants, so an overlay rendered in place was trapped in the 420px panel
and clipped by its overflow.

14 new tests in backend/tests/test_scenario_refresh.py; 85 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XzBCyXH4hBEVHqEaertcq4
2026-08-01 09:51:49 +05:30
parththakkar106andClaude Opus 4.8 970d71a5b6 Keep world-state emit reliable across turns
The AI would sometimes stop emitting the `state` delta block once it missed
a turn. Two compounding causes: the emit rule sat only in the system block
(far from where the model generates), and the block was stripped before
storage — so every replayed history turn looked blockless, biasing the model
by imitation to stop emitting too.

- EMIT_REMINDER: a one-line reminder appended last in the prompt (strongest
  recency slot), gated on has_ws and counted against the token budget.
- render_delta_block + _history_text: re-attach each past AI turn's own delta
  block in replayed history (reconstructed from the stored snapshot delta), so
  the model always sees its emit format. Action.text stays clean, so UI,
  embeddings, and card/NPC trigger-matching are unaffected. History budgeting
  counts the augmented text so it can't overflow.

Undo/retry untouched (read the separate world_state_before column). 46 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWVyFKvqJGjfbXdibLgkMe
2026-07-25 12:03:41 +05:30
parththakkar106andClaude Sonnet 5 141eec7c64 Add a world-state edit mode for overriding live RPG values
Players/authors can now directly correct the live values of stats the
schema already defines (health, trust, flags, milestones, etc.) without
waiting for the AI to emit a delta. New apply_override() sets values
absolutely rather than adding deltas, and — unlike the AI-facing
apply_delta() — bypasses cooldown/max_delta_per_turn and lets
milestones be un-set, since this is a deliberate correction rather
than a turn to police. Exposed via PUT /adventures/{id}/world-state
and an edit toggle in the World State drawer. Also fixes the
schema-editor stat-kind dropdown and free-text initial-value input
to size consistently with the numeric fields next to them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 18:19:31 +05:30
parththakkar106andClaude Sonnet 5 42073d5d89 Add free-text stat type to the world-state RPG layer
Numeric stats couldn't represent things like worn armor or held items.
A stat can now be marked type: "text" — the AI reports the new full
value instead of a delta, with no clamping/bands (cooldown still
applies). Updated the scenario editor's stat kind selector, the
World State drawer's stat display, and the emit-rule prompt; added
tests and an outfit stat to the RPG demo scenario.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 17:35:57 +05:30
parththakkar106andClaude Opus 4.8 db1170d0db Add proportionality guidance to the emit rule (story-neutral)
Tell the model to read each stat's range and band labels and keep changes
proportionate — minor events nudge a value, while large jumps or hitting a
min/max are reserved for pivotal moments. Examples are relationship/story
based (not combat) so it fits any scenario.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:59:49 +05:30
parththakkar106andClaude Opus 4.8 b153bc6efe Generalize the emit rule beyond combat
Reword the state-emit rule to be scenario-agnostic: record any tracked
change — health/resources, time, relationships/mood, status, progress,
items/info — not just fight outcomes. Soften the demo's AI instructions to
cover relationships and objectives alongside combat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:55:11 +05:30
parththakkar106andClaude Opus 4.8 8c18541d4e Harden the state-emit rule to reduce under-reporting
Rewrite the global emit rule to be directive: treat narration as
authoritative and record numeric changes (damage, healing, time, emotion
shifts, fights intensifying), not just easy on/off flags — updating every
stat the scene affected. Also firm up the demo scenario's AI instructions
to reflect combat outcomes in the numbers each turn. Helps weaker free
models emit numeric deltas; the emit rule applies live so all RPG
adventures benefit immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:52:20 +05:30
parththakkar106andClaude Opus 4.8 cf464a48b0 Make NPCs a dedicated section with per-NPC stats
Replace the single shared `npc` stat template (+ npc_card_types) with an
`npcs` section: each NPC keyed by a stable id, carrying its own name,
description, trigger keys, and its OWN stats block. The AI addresses NPCs
as npc.<id>.<stat> (id shown in context), which also fixes the old
card-id-guessing problem. On adventure creation each NPC auto-creates a
story card (name/keys/desc) for lore + in-scene detection, unless a
same-name card already exists. All NPCs instantiate up front.

- engine: npcs instantiate/apply/render/reference, npc_name/npc_triggers
- builder: _visible_npcs matches each NPC's own keys
- create_adventure: auto-create story cards from npcs
- WorldStateDrawer: render defined NPCs with their own stats + desc tooltip
- demo seed: Gwen (health/trust) + Bandit Leader (health/aggression)
- tests updated (34 pass); no new migration (npcs lives in stat_schema JSON)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:17:47 +05:30
parththakkar106andClaude Opus 4.8 4dac445f90 Add Phase 12: native RPG world state
Structured world/player/NPC stats, two-way flags, and sticky milestones
per scenario (stat_schema). The AI proposes a per-turn delta; a Python
engine referees it (clamp to min/max, per-turn cap, cooldown, counters).
Band word-labels plus a fixed stat guide (descriptions + full ranges)
keep the model grounded. World State drawer + Insights delta report;
undo/retry roll it back via the Phase 11 snapshot pattern.

- migrations 26-28 (scenarios.stat_schema, adventures.world_state,
  actions.world_state_before); all nullable, additive, safe on existing rows
- migration 29 raises the default context budget 4096 -> 16384
  (custom values preserved)
- seeded demo scenario 04-rpg-world-state.json (Bandit Camp)
- 19 new tests (33 total pass)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:47:29 +05:30