Commit Graph

67 Commits

Author SHA1 Message Date
ejlewis 2a9b6155ef feat(mystery): add opening and main-floor content 2026-05-09 21:51:12 -05:00
ejlewis e46b2359c0 docs(mystery): Phase 2 content-rewrite roadmap
Workflow guide for authoring the bible into markdown now that engine
prereqs have shipped. Suggests slicing by region rather than by kind so
each PR produces a playable surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:11:06 -05:00
ejlewis 5f8e3b1a34 fix(ui): keep input typable post-end so player can type restart/undo
Disabling the input via the disabled attribute blocks keydown events
entirely, so players couldn't type 'restart' or 'undo' after reaching an
ending. Switch to a CSS class for the faded visual state; the keydown
handler already restricts post-end input to those two commands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 15:06:37 -05:00
ejlewis e167979fa7 feat(ui): render ending lines distinctly and lock input on end-state
Ending-kind lines get a separator and italic styling. Once endedWith is
set, the terminal disables the input and rejects all commands except
restart and undo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:57:52 -05:00
ejlewis 19d1efc586 feat(engine): detect endings on every successful turn
After each state-mutating dispatch, evaluate world.endings in priority
order (true > wrong > bad). The first whose whenFlags are all satisfied
sets state.endedWith and appends a kind:'ending' transcript line. Once
ended, further dispatches return a "story has ended" narration.

Also update test-world fixtures and placeholder ending markdown files
to use whenFlags: { _never: true } instead of {} so that vacuously-true
empty flags don't accidentally fire on every successful turn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:53:07 -05:00
ejlewis 0d9db9bb55 test(engine): self-contained locked-exit fixture replaces the stub
Verifies blocked movement, key-permitted passage, and that the key is
not consumed by passing through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:42:22 -05:00
ejlewis b870d884ef feat(engine): wire verb-target-prep — explicit \light X with Y\ and \use\ routing
light X with Y validates the named instrument and reuses handleLight.
use X / use X on Y route through the encounter dispatcher; if no encounter
consumes it, the dispatcher narrates the fallback. The encounter matcher
also rejects transitions whose required item doesn't match the typed
instrument, so a mistyped instrument fails cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:22:03 -05:00
ejlewis 8401e7d281 feat(engine): light/extinguish verbs with implicit lighter selection
\`light X\` finds a lighter (item with lighter:true and remaining state.uses)
in inventory, decrements its charges, and toggles target.state.lit. The
target's litText / extinguishedText / the lighter's lighterEmptyText
provide narration. Refuses politely on each error path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:18:54 -05:00
ejlewis dac8487dbe feat(engine): read verb narrates item.readableText
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:15:35 -05:00
ejlewis 2fecc7878d feat(world): annotate lamp/matches/letter for read/light/extinguish
Adds the new schema flags and per-state body sections so the dispatcher's
new verb handlers have content to narrate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:12:10 -05:00
ejlewis ee3cfcc00d feat(world): parseItem extracts optional ## read / lit / extinguished / lighter-empty sections
Existing items with no body sections continue to load unchanged. New items
can author per-state prose in dedicated sections; the dispatcher will read
these in subsequent commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:08:33 -05:00
ejlewis df50afa479 feat(world): item schema — readable, lightable, lighter, lighterUses
Optional fields used by the new read/light/extinguish dispatcher branches.
Loader updates and dispatcher logic follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 14:01:31 -05:00
ejlewis ab8c17fdd5 feat(engine): dispatcher handles ambiguous parses with a disambiguation prompt
Sets pendingDisambiguation on state and emits "Which X — A, or B?" using
each candidate item's short text. The existing disambiguation reply path
then re-issues the original verb against the chosen target.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:58:32 -05:00
ejlewis b318747840 feat(parser): emit verb-target-prep on 'with'/'on'/'in'/'to' separators
Enables `light lamp with matches`, `use shears on vines`, and similar
multi-noun forms. Both the target and indirect noun must resolve;
otherwise the command falls back to unknown-noun.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:56:19 -05:00
ejlewis 46f851bc3a feat(parser): return ambiguous variant when noun matches multiple aliases
Replaces the previous behavior of returning unknown-noun. The dispatcher
will use this in the next commit to prompt the player to disambiguate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:53:38 -05:00
ejlewis b325f04b02 feat(parser): strip leading stop-words (at, the, a, an) from noun phrase
Allows `look at lamp`, `examine the letter`, `take a key`, `take an oil lamp`.
Stop-words are only removed from the head of the noun phrase, not from
anywhere in the middle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:36:34 -05:00
ejlewis 14a58481b1 refactor(engine): theme is a UI preference; remove it from GameState
Previously, clicking the theme button updated localStorage and the DOM but
not state.theme, so the engine's `theme` meta-verb toggled from stale state.
Theme is now exclusively UI/storage concern. Old saves with the field still
load; the field is silently ignored.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:33:57 -05:00
ejlewis 657ed22b48 refactor(engine): drop redundant string[] casts now that RoomState includes arrays
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:30:35 -05:00
ejlewis 6cffb87a63 feat(engine): widen state value unions and add 'ending' transcript kind
Drops redundant string[] casts in dispatcher paths and prepares the
TranscriptLine kind for the ending-screen render path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:28:14 -05:00
ejlewis e21a308e9d docs(mystery): implementation plan for engine prereqs
17 tasks covering type widening, theme removal, parser stop-words +
ambiguous + verb-target-prep variants, dispatcher handlers for
read/light/extinguish/use, ending detection, and UI ending screen.
Includes self-contained locked-exit fixture and manual playthrough.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:21:13 -05:00
ejlewis bcff8a42f9 docs(mystery): spec for engine prereqs (verbs, disambiguation, ending UI)
Hard prereqs from halfstreet-followon-notes plus should-fix items.
Polish items deferred. Phase 2 (full bible content draft) follows after
this lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:07:24 -05:00
ejlewis 2b3a18f208 ci: add Cloudflare Pages deploy + split success/failure notify
ci/woodpecker/push/woodpecker Pipeline was successful
Mirrors the ethanjlewis.com pipeline structure:

- single combined build step (npm ci + test + build)
- deploy via npx wrangler to project halfstreet-io
- notify-success at priority 0, notify-failure at priority 1
- triggers on push to main + manual

Secret names are lowercase (cloudflare_api_token, cloudflare_account_id,
pushover_token, pushover_user) to match what's configured on the
halfstreet repo in Woodpecker.
2026-05-09 11:56:38 -05:00
ejlewis 72f99295ca build: re-add wrangler as devDependency
Needed by the deploy step (npx wrangler pages deploy). Keeping it as a
local devDep means CI doesn't re-download wrangler on every run, matching
how ethanjlewis.com is set up.
2026-05-09 11:56:38 -05:00
ejlewis e044141043 ci: pushover notification on success too
ci/woodpecker/push/woodpecker Pipeline was successful
Single notify step now runs for both success and failure
(when.status: [success, failure]), branching on
CI_PIPELINE_STATUS for the title. Success uses Pushover
priority -1 (quiet) so passing-build pings don't alert at
night; failures stay at priority 0 (default).
2026-05-09 11:50:28 -05:00
ejlewis c0061491ab ci: pushover notification on pipeline failure
ci/woodpecker/push/woodpecker Pipeline was successful
Uses pushover_token and pushover_user secrets configured in Woodpecker.
Step runs only on failure (when.status: failure) so successful runs stay
quiet. Message includes branch, commit message, and a link back to the
pipeline.
2026-05-09 11:47:50 -05:00
ejlewis b80e4c32a5 fix(ci): commit package-lock.json so npm ci works
The Woodpecker pipeline runs `npm ci` which requires a lockfile;
without it the install step errors with EUSAGE.
2026-05-09 11:47:50 -05:00
ejlewis e31bf0fbff chore: standalone Halfstreet repo scaffolding
ci/woodpecker/manual/woodpecker Pipeline failed
- package.json: drop @astrojs/sitemap, fontsource fonts, wrangler,
  @cloudflare/workers-types, tsx, spotify scripts. Keep astro/yaml/zod
  + vitest/typescript/@astrojs/check/@types/node. Add GPL-3.0-or-later.
- astro.config.mjs: drop sitemap integration, sharp image config; set
  site to halfstreet.io.
- tsconfig.json: drop cloudflare-workers-types, drop functions/scripts
  from include.
- .gitignore: cloudflare-free; Obsidian workspace cache rules updated
  to post-rename src/world/.obsidian/ paths.
- README.md: stack, layout, design-doc index, license note.
- .woodpecker.yml: install + npm test + npm run build pipeline.
2026-05-09 11:36:19 -05:00
ejlewis 86e1aeb973 feat: rename mystery.astro -> index.astro, fix imports for src/ root
Halfstreet now lives at halfstreet.io as the entire site, so the game
serves at / instead of /mystery. After git-filter-repo lifted
src/mystery/ to src/, the page's css/ts imports need to drop the
mystery/ segment.
2026-05-09 11:33:06 -05:00
ejlewis 78b749dac4 chore(halfstreet): bible Obsidian-formatted edits + vault config
Substantial bible expansion (frontmatter, Obsidian list/heading formatting,
expanded core themes section, target length 25–28 rooms).

Adds .obsidian/ vault config (app/appearance/core-plugins) and gitignore
rules for the per-machine workspace cache, mirroring the world vault setup.

Done before extracting Halfstreet to its own repo so this work travels with
the filter-repo extraction.
2026-05-09 11:29:18 -05:00
ejlewis bc21a88786 Merge feature: mystery markdown content migration
Move Halfstreet game content (rooms, items, encounter narration, endings)
from TypeScript object literals into markdown files editable in Obsidian.
Engine, UI, and the public World type are unchanged. Three-room prototype
verified end-to-end via tests and manual playthrough.

Includes code-review followups: locked-exit requires validation,
endings-completeness check, and clear errors for malformed section headers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 11:13:38 -05:00
ejlewis 1f472402fd fix(mystery): code-review followups (locked-exit, endings, headers)
- Validate lockedExits[*].requires resolves to a known item or flag
- Throw if any of true/wrong/bad ending markdown files are missing
- Detect malformed ## headers (spaces, dots, etc.) and throw a clear
  error rather than silently dropping the section

Tests: 86 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 11:12:26 -05:00
ejlewis 4b8ebafe6f fix(mystery): swap gray-matter for yaml package (browser-safe)
gray-matter eagerly loads Node's Buffer API path even when only
matter(rawString) is called, crashing browser bundles. Replace it with
an inline frontmatter parser backed by the browser-safe yaml package.
All 84 mystery tests pass; build is clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:52:21 -05:00
ejlewis 20619cec09 chore(mystery): commit minimal Obsidian vault config; ignore workspace cache 2026-05-09 09:43:53 -05:00
ejlewis 506e36b801 refactor(mystery): remove story.ts; endings live in markdown 2026-05-09 09:36:28 -05:00
ejlewis 1b992642ec feat(mystery): encounters.ts uses narration() helper for prose 2026-05-09 09:35:41 -05:00
ejlewis c0c1a7e930 feat(mystery): assemble World from markdown via import.meta.glob
Rooms, items, and endings now come from .md files under world/{rooms,items,endings}/.
Encounters still come from encounters.ts (Tasks 11–12 will complete that leg).
Cross-reference validation at module init ensures exits, item refs, and encounter
refs are all consistent. Deletes rooms.ts, items.ts, roundtrip.test.ts, and the
one-shot migration script (whose output is already committed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:34:27 -05:00
ejlewis 0523158e61 test(mystery): round-trip verification of migrated markdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:27:39 -05:00
ejlewis bbea3f4473 feat(mystery): migration script and produced markdown content
Adds scripts/migrate-mystery-content.ts which reads rooms, items,
encounters, and endings from TypeScript source and emits byte-identical
markdown files under src/mystery/world/{rooms,items,encounters,endings}/.
Installs tsx as a devDep to support .ts imports across src/ during the
one-shot run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:26:02 -05:00
ejlewis d3a2f4e1d7 feat(mystery): narration() helper and encounter narration registry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:23:37 -05:00
ejlewis bf8a693949 feat(mystery): parseEncounterNarration — phase and transition prose
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:21:29 -05:00
ejlewis e60844a937 feat(mystery): parseEnding — markdown to typed Ending
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:17:11 -05:00
ejlewis e108ca16e0 feat(mystery): parseItem — markdown to typed Item
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:13:51 -05:00
ejlewis cf257c040a fix(mystery): handle aliased wikilinks; symmetric locked-exit validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:09:38 -05:00
ejlewis 5f3356ffb5 feat(mystery): parseRoom — markdown to typed Room
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 09:00:35 -05:00
ejlewis da7b6fac83 feat(mystery): add Zod schemas for markdown frontmatter
Defines runtime validation schemas (roomFrontmatterSchema,
itemFrontmatterSchema, endingFrontmatterSchema, encounterFrontmatterSchema)
and their inferred TypeScript types. All 8 TDD tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 08:56:44 -05:00
ejlewis 2ad81f356a docs(mystery): implementation plan for markdown content migration
14-task plan covering: gray-matter+zod setup, schemas, four pure
parsers (room/item/ending/encounter), narration() helper with
auto-registration, one-shot migration script, round-trip
verification, world assembly cutover, encounters.ts refactor,
manual playthrough, and Obsidian vault config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 08:50:58 -05:00
ejlewis baed9dd2f7 docs(mystery): camelCase frontmatter, prototype goal, content collections rationale
Address spec review feedback: switch frontmatter convention from snake_case
to camelCase to match existing TS field names; surface the working
three-room prototype as the explicit deliverable; rephrase the Astro
content collections out-of-scope item to clarify the import.meta.glob
choice is a tooling decision, not a feature exclusion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 08:42:12 -05:00
ejlewis 1a283076ac docs(mystery): spec for markdown content migration
Move rooms, items, encounter narration, and endings from TypeScript
object literals to markdown files editable in Obsidian. Engine code,
tests, and the World shape are unchanged. Tonal refinement is a
separate spec that follows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 08:36:38 -05:00
ejlewis 157dd07c9c fix(mystery): letter doesn't open in foyer 2026-05-09 01:03:47 -05:00
ejlewis f75030e7ee fix(mystery): persist player input lines so they survive reload
appendLines() only updated the DOM, never state.transcript, so any
UI-originated line (player input, restart/undo/quit messages) vanished
on page reload. Engine narration was unaffected because dispatch()
already adds its lines to state.transcript.

Fix: appendLines() now pushes into state.transcript (capped at
TRANSCRIPT_CAP) and renders. Engine output uses renderAll() directly
since dispatch already added its lines to state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 00:54:54 -05:00