Commit Graph

4 Commits

Author SHA1 Message Date
Patrick Buckley 9f54a97cc6 fix(understone): rebalance early game and add inn turn refresh
The opening hours were a gold/HP death spiral: a fresh hero spent more gold
healing a fight than the kill paid, mid-tier foes out-damaged a starting HP
bar, the map gave no read on where harder foes spawned, and a spent day left
the player idle until the UTC rollover. This eases the on-ramp across both
shipped worlds.

Economy
- Healer drops from 2 to 1 gold per HP, so topping up no longer outruns income.
- Starting purse 20 -> 37: enough to buy the cheapest armor and one potion up
  front, a one-point DEF bump plus a heal cushion the player chooses to spend.

Combat
- Tier 2-4 common foes lose 1 ATK each, trimming the burst that could halve or
  end a fresh hero in a single bout. Rares and the boss are untouched.

Wayfinding
- The road glyph changes from "=" to a shaded path that reads as one continuous
  road in every orientation; "=" only looked right horizontally and broke into
  stacked dashes on vertical runs. Cinder's basalt path gets the same treatment
  with a crosshatch glyph free in its palette.

Rest
- Sleeping at the inn now rolls a spent adventurer into a fresh day's turns (it
  already fully heals). The top-up fires only at zero turns, so it never banks
  past the daily cap.

Verified: full suite green (+2 rest tests, ruff/mypy clean); the greedy balance
bot still clears the world 11/12 seeds at an unchanged pace on both packs.
2026-06-27 09:40:03 -07:00
Patrick Buckley 917e391b1f feat(examples): Understone v0.9 — colour roles for every object type
Graphics polish: distinct terrain and structures now read by COLOUR on the
Watch, not only by glyph. One unified palette, shared by every world — the
fix is to grow the set of distinct object-type roles, not to fork per-world.

- Roads were the tell: road shared the "floor" green with grass, so a path
  vanished into the meadow on the lobby TV. Likewise forest shared "tree",
  the three town buildings all shared "town", and the Cinder Wastes' molten
  slag borrowed "water" and rendered BLUE. Each is now its own role: road
  (stone), forest (lush green) with scrub (its barren ember-brown
  counterpart for volcanic/desert dense terrain that must NOT read as
  woods), lava (molten orange), barren (wasteland taupe), and inn/shop/
  healer split out of the generic town.
- Both worlds remap onto the shared vocabulary; in each, no two distinct
  terrain/building types share a colour. A live render caught the Cinder
  cinder-fields rendering green under the generic "forest" role — hence the
  scrub role, so the volcanic waste reads warm. The text frame renderer
  stays monochrome (it never read colour), so frames and goldens are
  untouched — this is Watch-only.
- The bug class is now closed by construction: a test asserts the Watch
  PALETTE carries a hex for EVERY Color role, so a role can never ship
  unpaintable and silently fall back (which is exactly how road hid).
- Color.assignable() is the single source for the overlay-vs-assignable
  split (runtime actor/item colours and the DEFAULT fallback are not
  author-pickable); the authoring manual's colour vocabulary generates
  from it, so it can't drift.

Tests 373 -> 382. floor/tree/forest are three greens kept deliberately
distinct (forest is olive-hued); verified on a real render along with the
scrub fix.
2026-06-13 04:40:40 -07:00
Patrick Buckley b76b2a98d0 feat(examples): Understone v0.6 — UTF-8 graphics and the width discipline
The look of the next age — the modern equivalent of the ASCII->CP437 leap.
Full Unicode is available now, but the whole stack (text frames, golden
tests, the Watch's 1ch grid) assumes one glyph = one column, so the
enabling piece is a WIDTH RULE, not the glyphs themselves.

- textwidth.is_grid_safe: one code point, printable, East-Asian width not
  Wide/Fullwidth, no combining/format/control category. This is the
  one-glyph-one-column contract. Ambiguous-width glyphs are ACCEPTED on
  purpose — they ARE CP437 (the wall, the club-tree, the up-arrow forest)
  and render single-column on the Western-monospace metrics every surface
  uses; only genuinely double-width runes are barred. The loader enforces
  it on every map glyph; the player-name/free-text sanitizer enforces the
  same rule (the narrow ledger), so a wide name can't shear a frame.
- Re-skin: water ~ -> ≋, inn -> ⌂, healer -> ✚, dungeon mouth -> ∩, and
  the other adventurer -> ☻ (CP437's own player glyph). The colour field
  the renderer has carried unused since v0.1 now has a second consumer.
- Texture variants: grass and water vary by a deterministic per-coordinate
  hash, rendered identically in the Python frame builder and the Watch's
  JS. The two are kept in lockstep by shared hash constants + an agreement
  test that replays the JS arithmetic and asserts it equals the Python
  output for every variant over a grid — not a comment-coupled copy.
- Watch glow-up: a Noto Sans Mono font stack and a UTC-hour day/night tint
  (the Vale darkens at dusk on the lobby TV).
- The curated SAFE_PALETTE is enforced author-usable: a test asserts no
  palette glyph collides with the reserved player markers, so AUTHORING's
  generated appendix can't advertise a glyph the loader would reject.
- Resume is identity-preserving: an existing character resumes by exact
  stored name without re-validating the width rule (which governs creation
  only) — resume must never lock anyone out.

Tests 231 -> 283; width edges (CJK/emoji/combining/fullwidth), the
Python<->JS lockstep, the palette/reserved guard, and resume-vs-create all
pinned and revert-verified.
2026-06-13 04:40:40 -07:00
Patrick Buckley 99e7dc17ec feat(examples): Understone — a BBS door game as a standalone MCP server
A shared-world, classic-door-game-style door game in examples/door-game/: a pure-stdlib
game engine (tile overworld + location menus, seeded combat, daily turn
budget, leveling, shop, event log, leaderboard) behind nine sync door_*
FastMCP tools returning monochrome box-drawing frames. The connecting
session's LLM plays dungeon master — tool descriptions plus a door_help
manual teach a cold model to run the game with zero setup, while the server
owns all dice and state, so the DM narrates around facts it cannot bend.

Non-obvious decisions:
- engine/screen/world/persistence import stdlib only; server.py is the only
  mcp import. All nine handlers are sync def: on mcp 1.27 they execute
  inline on the event loop (verified against func_metadata), so tool bodies
  serialize and one SQLite connection (WAL, per-action commit) is safe.
  check_same_thread=False exists only because the Store may be constructed
  on a different thread than the serving loop.
- Streamable HTTP serves ONE process = one shared world (players appear on
  each other's maps; async "while you were away" event feed); stdio is the
  solo-world fallback.
- The economy is content, not code: daily_turns, costs, xp curve, bestow
  budget, and dungeon tiers live in world.json settings, band-validated by
  the loader. door_bestow gives the DM capped, event-audited largesse
  (gold/heal only, never turns) so story generosity cannot melt the shared
  leaderboard.
- Player names and bestow reasons are sanitized (printable-only, length
  caps) because they flow into the shared event log and from there into
  other players' DM context — embedded newlines would forge log lines.
- Daily turn/bestow pools lazy-reset per UTC day on every consuming path
  (injectable clock); the dungeon gauntlet is a fixed boss ladder by design.

Tests: 109 — engine units with seeded RNG + frozen clock, hand-authored
golden frames paired with structural asserts, loader band rejections, and
one real-wire integration test (uvicorn + streamablehttp_client) with a
two-session shared-world assertion. Negative-tested by reverting the guard
and watching the suite fail: the daily turn-budget guard, the bestow cap,
and the sanitizer's isprintable clause.
2026-06-13 04:40:40 -07:00