Skip to content

fix(openworlds): persist house + biography in engine snapshot (Closes #383) - #392

Closed
100yenadmin wants to merge 2 commits into
mainfrom
fix/383-house-biography-persistence
Closed

fix(openworlds): persist house + biography in engine snapshot (Closes #383)#392
100yenadmin wants to merge 2 commits into
mainfrom
fix/383-house-biography-persistence

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 30, 2026

Copy link
Copy Markdown
Member

TL;DR

Loop-10 follow-up to #277. PR #369 wired house + biography from the Creation wizard's inputs into the bindHero spec, but the engine seating path silently dropped both fields at FIVE sites: the Character model, create_character's signature, the Character() constructor call, play.sh's spec-to-create_character bridge, and the /character-surface projection. A player who authored House = "Three Bells" + Biography = "..." in the wizard saw both vanish the moment they hit Bind.

This PR threads both fields end-to-end + adds the UI rendering + the regression test the parent issue's AC5 asked for.

Closes

The five sites threaded

# File What
1 servers/engine/models.py Character model gains house: str = "" and biography: str = "" in the identity block. _StrictModel rejects extra fields — adding them at the model is what makes accepting these kwargs possible.
2 servers/engine/server.py create_character signature accepts both kwargs (positional callers stay working) + the Character(...) constructor call threads them inside campaign_lock.
3 scripts/play.sh The embedded Python that translates the bindHero spec into create_character kwargs now reads both fields from the spec dict with "" coalesce. This is the single place where the wizard's prose crosses the process boundary into the engine.
4 viewer/server.py _character_sheet projects "house" and "biography" to the /character-surface read-model via _text() for string safety.
5 viewer/openworlds/screen-character.jsx (a) Hero header card renders subtle italic "of House {hero.house}" line under the name when set. (b) LineagePanel renders a "Biography" section with eyebrow label + whiteSpace: pre-wrap paragraph when set. Both honest-empty otherwise.

Tests

New: servers/engine/tests/test_house_biography_persistence.py — 4 tests

Test Asserts
test_create_character_accepts_house_and_biography_kwargs Sanity: no TypeError on call
test_house_and_biography_persist_on_the_character_model Both fields survive the save_campaign round-trip inside create_character (asserted via get_state)
test_house_and_biography_default_to_empty_when_omitted Existing call sites (NPC spawn without new kwargs) keep working; defaults are "" not None
test_house_and_biography_round_trip_through_get_state End-to-end: explicit values → snapshot dict → fields intact + pre-existing identity (race, background) still works alongside

Tests follow the canonical pattern from test_action_economy.py + test_adversarial_release.py (server.create_campaign(name)["id"]).

Verification

Check Result
ast.parse on all 5 modified .py + new test
Brace + paren balance screen-character.jsx ✅ 652/652, 540/540
Snapshot round-trip via existing _StrictModel ser/de ✅ (no custom dump/load needed; plain str = "")
Local pytest NOT RUN (per CLAUDE.md: ClawDnD-val is not on the test-execution allowlist; GitHub CI is the verification path)

Acceptance criteria (per #383)

  • ☑ Pydantic field added (house, biography)
  • create_character signature accepts both kwargs
  • play.sh threads both from spec to create_character
  • /character-surface projects both
  • screen-character.jsx renders both
  • ☑ Pytest asserts the contract end-to-end (4 tests: kwargs accept, persistence, defaults, round-trip)

Out of scope (intentionally)

  • BACKSTORY MERGE: the issue body called out that fix(openworlds): persist house + biography in engine snapshot (Loop 10 follow-up to #277) #383 should NOT rename personality/backstory or merge biography into them. These remain DISTINCT fields with distinct sources. Lineage prose from engine-side flavor stays in lineageNote; player-authored biography stays in biography.
  • Player-rename mid-play: this PR seeds house+bio at character creation time. No update_character path here — out of scope until a "rename / edit your hero" affordance lands.
  • Companion / NPC house + biography: the model now CARRIES the fields for all characters, but only the player wizard authors them today. Companions/NPCs default "".

Collision audit

File Last touched on main Overlap with this PR
servers/engine/models.py #355 (max_hp) Zero — different field section
servers/engine/server.py #355 (max_hp) Zero — different signature region
scripts/play.sh #360 (DM resolver fallback) Zero — different Python block
viewer/server.py #370 (#286 feats fix) Zero — different sheet section
viewer/openworlds/screen-character.jsx #364 (#308 #288) Zero — conditionally-rendered nodes only appear when fields are set
Main agent's open #388 DM/cold-open territory; zero overlap

DO NOT MERGE yet

Per owner direction. CI will verify pytest correctness. Ready for review.

Refs

Summary by CodeRabbit

Release Notes

  • New Features
    • Players can now add a house affiliation and personal biography to their characters.
    • The character's house displays as a subtitle beneath their name on the character sheet.
    • A dedicated Biography section in the Lineage panel displays the player-authored biography text.
    • Both fields are optional and remain empty by default.

Review Change Stack

…383)

Loop-10 follow-up to #277. PR #369 wired house + biography from the
Creation wizard's inputs into the bindHero spec, but the engine seating
path silently dropped both fields at FIVE sites: the Character model,
create_character's signature, the Character() constructor call,
play.sh's spec-to-create_character bridge, and the
/character-surface projection. A player who authored
House = "Three Bells" + Biography = "..." in the wizard saw both vanish
the moment they hit Bind.

This PR threads both fields end-to-end + adds the UI rendering + the
regression test the parent issue's AC5 asked for.

The five sites
--------------

1. servers/engine/models.py — Character model
   Added `house: str = ""` and `biography: str = ""` in the identity
   block (next to race/classes/background/alignment). Both are simple
   Optional[str]-style fields with "" default. _StrictModel REJECTS
   extra fields, so this PR is what makes accepting these kwargs
   possible at all. Defaults preserve backward-compat: existing
   snapshots without the keys deserialize to "" with no migration.

2. servers/engine/server.py — create_character
   Added `house: str = ""` + `biography: str = ""` kwargs at the end
   of the signature (after `met=False`) so positional callers stay
   working. Threaded both into the Character(...) constructor inside
   the campaign_lock so the persist path picks them up. The save then
   carries them into the snapshot per the normal model save flow.

3. scripts/play.sh — wizard-spec bridge
   The embedded Python that translates the bindHero spec JSON into
   create_character kwargs now reads both fields from the spec dict
   (with "" coalesce for null safety) and passes them. This is the
   single place where the wizard's prose actually crosses the
   process boundary into the engine.

4. viewer/server.py — /character-surface projection
   Added "house" and "biography" to the _character_sheet return
   dict, projected via _text() so they're string-safe for the read-
   model contract (never None or non-string). The Character screen
   reads this projection.

5. viewer/openworlds/screen-character.jsx — render
   - Hero header card: subtle italic "of House {hero.house}" line
     between the name and the race/class line, only rendered when
     hero.house is non-empty. Reads as a subtitle to the name.
   - LineagePanel (right column): "Biography" section with eyebrow
     label and a whiteSpace:pre-wrap paragraph for the player's
     authored narrative. Only renders when hero.biography is non-
     empty. Distinct from `lineageNote` (which projects from the
     engine's backstory/personality fields) so an authored hero's
     biography never overrides the lineage prose.

Tests
-----

New: servers/engine/tests/test_house_biography_persistence.py

  - test_create_character_accepts_house_and_biography_kwargs
    Sanity: the signature actually carries the new kwargs (no
    TypeError on call).

  - test_house_and_biography_persist_on_the_character_model
    The model carries both fields after creation and the save_campaign
    round-trip inside create_character preserves them. Asserts via
    get_state.

  - test_house_and_biography_default_to_empty_when_omitted
    Existing call sites (NPC/monster spawn without the new kwargs)
    keep working — defaults are "" + the snapshot carries "" not
    None.

  - test_house_and_biography_round_trip_through_get_state
    End-to-end: explicit values written, snapshot serialized to dict
    via get_state, fields present + intact. This is the contract the
    viewer's /character-surface depends on. Pre-existing identity
    fields (race, background) still work alongside the new ones.

Verification
------------

- All 5 modified .py files parse via `ast.parse()`.
- Brace + paren balance clean on screen-character.jsx (652/652,
  540/540).
- Test follows the canonical pattern from test_action_economy.py +
  test_adversarial_release.py (server.create_campaign(name)["id"]).
- Snapshot round-trip relies on the existing Pydantic _StrictModel
  ser/de path — no model_validate / dump customization needed because
  the new fields are plain `str = ""`.

Acceptance criteria (per #383)
------------------------------

- [x] Pydantic field added (house, biography)
- [x] create_character signature accepts both kwargs
- [x] play.sh threads both from spec to create_character
- [x] /character-surface projects both
- [x] screen-character.jsx renders both
- [x] Pytest asserts the contract end-to-end (4 tests covering kwargs
      accept, persistence, defaults, round-trip)

Out of scope
------------

- BACKSTORY MERGE: the issue body called out that #383 should NOT
  rename `personality`/`backstory` or merge `biography` into them.
  These remain DISTINCT fields with distinct sources. Lineage prose
  from engine-side flavor stays in `lineageNote`; player-authored
  biography stays in `biography`.
- Player-rename mid-play: this PR seeds house+bio at character
  creation time. There's no update_character path here for player-
  authored prose — out of scope until a "rename / edit your hero"
  affordance lands.
- Companion / NPC house + biography: the engine model now CARRIES
  the fields for all characters, but only the player wizard
  authors them. Companions/NPCs default "" and remain so unless
  some future seed or update path sets them.

Collision audit
---------------

- servers/engine/models.py — last touched on main by PR #355.
  This PR adds 2 fields to an existing class. No overlap.
- servers/engine/server.py — last touched on main by PR #355.
  This PR extends create_character. No overlap with #355's max_hp work.
- scripts/play.sh — last touched on main by PR #360 (DM resolver
  fallback). This PR extends the create_character call inside a
  different python block. No overlap.
- viewer/server.py — last touched on main by PR #370 (#286 feats
  fix). This PR adds 2 entries to the _character_sheet return dict.
  No overlap.
- viewer/openworlds/screen-character.jsx — last touched on main by
  PR #364. This PR adds conditionally-rendered nodes that only
  appear when fields are set. No overlap with #364's surface.
- Main agent's open #388 — cold-open DM territory, zero overlap.

Refs
----

- Closes #383 (Loop-10 follow-up: persist house + biography in engine snapshot)
- Parent #277 (Creation wizard wires the fields; PR #369 closed the viewer half)
- Builds on PR #369 (the wizard-side fix); completes the chain to the engine
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds two new optional player-authored identity fields to the character system: house and biography. The fields are added to the Character model, accepted as parameters in the create_character API, tested for proper persistence and round-trip serialization, projected through the viewer endpoint, and rendered conditionally in the character screen UI with appropriate formatting.

Changes

House and Biography Character Fields

Layer / File(s) Summary
Character model schema
servers/engine/models.py
Character model adds two new optional text fields (house and biography) with empty-string defaults for backward compatibility.
Engine create_character API
servers/engine/server.py
create_character function signature and implementation accept two new optional parameters (house and biography) and pass them to the Character instance during construction.
Persistence verification tests
servers/engine/tests/test_house_biography_persistence.py
New test module with a campaign helper and four end-to-end tests verify that fields are accepted by the API, persist through snapshots, default to empty strings when omitted, and round-trip correctly through get_character while preserving existing fields.
Viewer character sheet projection
viewer/server.py
Viewer server's character sheet output now includes house and biography fields sourced from character snapshots, returning empty strings when unset.
Character screen rendering
viewer/openworlds/screen-character.jsx
Character screen conditionally renders house as an italic subtitle (only when set) beneath the hero name, and biography as a separate paragraph in the lineage panel with pre-wrap formatting (only when non-empty).
Play script integration
scripts/play.sh
Play script extracts house and biography from the authored hero spec and passes them as arguments to the create_character engine call.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

  • electricsheephq/WorldOS#383: Implements the exact requirements—adding house and biography fields to Character model, threading them through the create_character API, play script, viewer projection, and character screen UI rendering.

Possibly related PRs

  • electricsheephq/WorldOS#369: Adds hero.house and hero.biography to the New Hero wizard spec sent to startProviderSession, which this PR then persists and renders.
  • electricsheephq/WorldOS#364: Modifies the same viewer/openworlds/screen-character.jsx LineagePanel rendering logic for hero/lineage visibility changes.

Poem

A rabbit hops through fields so grand,
Where heroes claim their house and land,
Their tales now writ in biography's page,
Each character finds their rightful stage. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: persisting house and biography fields in the engine snapshot, with issue reference.
Description check ✅ Passed The description follows the template with comprehensive Summary, Licensing/CLA checklist, and Validation sections covering all five modified sites, tests, and acceptance criteria verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

…383)

CI revealed get_state returns a summary shape (party as list of dicts,
no top-level 'characters' key). get_character is the right tool for
asserting Character model fields — it returns ch.model_dump(mode="json")
which carries every field on the model, including the new house +
biography.

Changes
-------
- test_house_and_biography_persist_on_the_character_model
- test_house_and_biography_default_to_empty_when_omitted
- test_house_and_biography_round_trip_through_get_state

All three now call:
  pc = server.get_character(camp, rec["id"])

instead of:
  snapshot = server.get_state(camp)
  pc = snapshot["characters"][rec["id"]]  # KeyError

The 4th test (kwargs accept) was already passing; unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
viewer/openworlds/screen-character.jsx (1)

771-773: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Biography is skipped when race/note are empty due to early return.

Line 771 short-circuits before the new biography block, so a hero with only biography set still shows “No lineage recorded” and never renders biography.

Suggested fix
-  if (!race && !note) {
+  const biography = (hero.biography || "").trim();
+  if (!race && !note && !biography) {
     return <p className="body muted" style={{ marginTop: 0, fontSize: 14 }}>No lineage recorded for this hero.</p>;
   }
@@
-      {(hero.biography || "").trim() && (
+      {biography && (
         <div style={{ marginTop: (note || race) ? 12 : 0 }}>
           <div className="eyebrow" style={{ fontSize: 11, letterSpacing: "0.14em", color: "var(--ink-600)", marginBottom: 4 }}>Biography</div>
           <p className="body" style={{ margin: 0, fontSize: 14, color: "var(--ink-700)", whiteSpace: "pre-wrap" }}>
-            {hero.biography}
+            {hero.biography}
           </p>
         </div>
       )}

Also applies to: 801-808

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@viewer/openworlds/screen-character.jsx` around lines 771 - 773, The early
return checks only race and note causing biography to be skipped; update the
condition that returns the "No lineage recorded" paragraph to also check
biography (i.e., change the if (!race && !note) to if (!race && !note &&
!biography>) or alternatively move the biography rendering above that return so
a non-empty biography is rendered; apply the same fix to the similar block
around the code handling lines 801-808 to ensure biography is considered there
too.
🧹 Nitpick comments (1)
servers/engine/tests/test_house_biography_persistence.py (1)

76-77: ⚡ Quick win

Strengthen round-trip assertions to exact equality.

For persistence regressions, startswith/substring checks can pass even with truncation or unexpected mutation. Prefer asserting full string equality for house and biography in these checks.

Suggested assertion tightening
-    assert pc["biography"].startswith("Three winters in the Iron Shield")
+    assert pc["biography"] == "Three winters in the Iron Shield; one summer at the Spear Gate."
...
-    assert "Ember" in pc["house"]
-    assert "Hellfire" in pc["biography"]
+    assert pc["house"] == "Ember (foundling — no kin recorded)"
+    assert pc["biography"] == (
+        "Born into the Avernus engine-shops with the Hellfire still in her chest. "
+        "The infernal contract was cut; the heart is hers again. The forge owes her a name."
+    )

Also applies to: 121-122

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@servers/engine/tests/test_house_biography_persistence.py` around lines 76 -
77, The assertions use startswith which allows truncation; change both checks to
strict equality: replace the assertion on pc["house"] and pc["biography"] to
assert pc["house"] == "Anvilforge" and assert pc["biography"] == "Three winters
in the Iron Shield..." (use the full expected biography string present in the
test), and make the same exact-equality replacements for the corresponding
assertions around lines 121-122 so the round-trip persistence test validates
exact matches for pc["house"] and pc["biography"].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@viewer/openworlds/screen-character.jsx`:
- Around line 771-773: The early return checks only race and note causing
biography to be skipped; update the condition that returns the "No lineage
recorded" paragraph to also check biography (i.e., change the if (!race &&
!note) to if (!race && !note && !biography>) or alternatively move the biography
rendering above that return so a non-empty biography is rendered; apply the same
fix to the similar block around the code handling lines 801-808 to ensure
biography is considered there too.

---

Nitpick comments:
In `@servers/engine/tests/test_house_biography_persistence.py`:
- Around line 76-77: The assertions use startswith which allows truncation;
change both checks to strict equality: replace the assertion on pc["house"] and
pc["biography"] to assert pc["house"] == "Anvilforge" and assert pc["biography"]
== "Three winters in the Iron Shield..." (use the full expected biography string
present in the test), and make the same exact-equality replacements for the
corresponding assertions around lines 121-122 so the round-trip persistence test
validates exact matches for pc["house"] and pc["biography"].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54f2b378-58db-4fa4-aa55-ed24539ee02f

📥 Commits

Reviewing files that changed from the base of the PR and between bb1870d and 7c48f63.

📒 Files selected for processing (6)
  • scripts/play.sh
  • servers/engine/models.py
  • servers/engine/server.py
  • servers/engine/tests/test_house_biography_persistence.py
  • viewer/openworlds/screen-character.jsx
  • viewer/server.py

100yenadmin added a commit that referenced this pull request Jun 5, 2026
…383) (#656)

Loop-10 follow-up to #277/#369. PR #369 wired `house` + `biography` from the
Creation wizard's inputs into the bindHero spec, but the engine seating path
silently dropped both fields. A player who authored House + Biography in the
wizard saw both vanish the moment they hit Bind.

Threads both fields end-to-end (additive, schema additive-only):
  1. servers/engine/models.py    Character gains house/biography (str = "")
  2. servers/engine/server.py     create_character kwargs + constructor
  3. scripts/play.sh              bindHero spec -> create_character bridge
  4. viewer/server.py             /character-surface read-model projection
  5. screen-character.jsx         "of House X" subtitle + Biography paragraph

Empty strings are honest no-ops — additive defaults deserialize existing
snapshots unchanged; NPC/monster spawn call sites keep working.

Regression guard: servers/engine/tests/test_house_biography_persistence.py
(4 tests: kwargs accept, model persistence, empty defaults, round-trip).

Revives PR #392 (rebased onto current main; LineagePanel empty-guard also
includes biography so a bio-only hero isn't dropped).

Co-authored-by: Eva <arncalso@gmail.com>
@100yenadmin

Copy link
Copy Markdown
Member Author

Superseded by #656 (merged) — a fresh re-implementation off current main with a 4-test regression guard. Closing the stale branch.

@100yenadmin 100yenadmin closed this Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant