A Modern TypeScript Interactive Fiction Platform
Sharpee is a modern, TypeScript-based Interactive Fiction platform designed for extensibility, clean separation of concerns, and developer experience. This report covers one week of intensive development since February 11, 2026, highlighted by the completion of the Dungeon validation story at a perfect 650/650 score, endgame integration (750 total), a comprehensive documentation overhaul, and the Zifmia 0.9.90 desktop release with Linux support.
The Dungeon validation story — a critical platform test porting 1981 Mainframe Zork — achieved a perfect 650/650 main game score with the completion of the Canvas puzzle. Endgame integration adds 100 additional points for a 750-point total. All 17 walkthroughs pass (771+ tests). The complete game is playable from start to victory across CLI, browser, and Tauri desktop.
Replaced ScoringService with WorldModel primitives (awardScore, revokeScore, hasScore, getScore, getScoreEntries, setMaxScore). Score state stored in world model, survives serialization.
esbuild bundles platform + story + tester into single JS file. Full walkthrough chain build time reduced from 95s to 2.5s. Template: scripts/test-bundle-template.js.
Envelope format { worldState, pluginStates } preserves state machine and scheduler state across save/restore. Critical for walkthrough chaining.
Capability registry and interceptor registry use globalThis to share state across require() boundaries in bundled JavaScript. Fixes ISSUE-052 cross-module registry loss.
Linux installers (AppImage, .deb). Engine restart fix. Version sync across all 4 sources (package.json, version.ts, tauri.conf.json, Cargo.toml). Mac release script updated for Tauri v2.
Full audit of Zifmia React client. 3-tier ARIA implementation plan. Critical gap: no aria-live region on transcript output. Status moved from IDENTIFIED to ACCEPTED.
Root README expanded to 20 packages with roadmap. CONTRIBUTING.md rewritten. Architecture README with key ADR links. API Reference README listing 16 HTML pages. Full ADR index: 90 Implemented, 12 Proposed, 3 Accepted.
output contains condition type for randomized content. [WHILE:] loop auto-skip bug fixed. Commands without assertions now a validation error. Conditional [IF:] blocks for non-deterministic NPC behavior.
All 24 stdlib actions declare defaultScope + use context.requireScope()
// Scope levels (highest = most restrictive)
enum ScopeLevel {
UNAWARE = 0, // Entity not known to player
AWARE = 1, // Player knows it exists (can hear/smell)
VISIBLE = 2, // Player can see it
REACHABLE = 3, // Player can touch it
CARRIED = 4, // In player's inventory
}
// Grammar: declares ONLY semantic constraints
grammar.forAction('if.action.opening')
.verbs(['open'])
.pattern(':target')
.hasTrait('target', TraitType.OPENABLE) // What kind of thing
.build();
// Action: enforces scope dynamically
validate(context) {
const check = context.requireScope(target, ScopeLevel.REACHABLE);
if (!check.ok) return check.error!;
}
6 actions auto-take items when reachable but not carried
// Player types: "wear hat" (hat is on the floor, not in inventory) // Engine: "(first taking the hat)" -> then wears it const result = context.requireCarriedOrImplicitTake(item); if (!result.ok) return result.error!; // Applied to: dropping, inserting, giving, showing, throwing, wearing
Entities now declare adjectives on IdentityTrait. Parser scores adjective matches (+4 points) for multi-word disambiguation. "press yellow button" correctly resolves among 4 identically-named buttons. Wire disambiguation ("braided wire" vs "shiny wire") fixed via adjective declarations.
42 action templates converted to perspective-aware output
// Article selection by noun type
{a:item} // common -> "a sword" / "an apple"
// proper -> "Excalibur" (no article)
// mass -> "some water"
// List formatting with Oxford comma
{a:items:list} // "a sword, a key, and a lantern"
{the:items:or-list} // "the north, the south, or the west"
// Perspective placeholders (42 template files updated)
'taken': "{You} {take} {the:item}."
// 2nd person: "You take the sword."
// 1st person: "I take the sword."
// 3rd person: "She takes the sword."
Entities use literal text or message IDs for localization
// Either pattern works through the same API
description: "You are in a dark room" // Literal text
description: "dungeo.msg.room.west_house" // Message ID (localized)
// Single unified call handles both
const text = textService.getEntityText(entity, 'description', {}, world);
// Key lesson: trait properties like cantTakeMessage must use
// message IDs, not literal strings, to avoid dot-heuristic misrouting
4 packages + envelope save/restore
interface TurnPlugin {
id: string;
priority: number; // NPC=100, StateMachine=75, Scheduler=50
onAfterAction(context: TurnPluginContext): ISemanticEvent[];
getState?(): unknown; // For save/restore
setState?(state: unknown): void;
}
// Save format preserves plugin state alongside world state
{ worldState: {...}, pluginStates: { scheduler: {...}, stateMachine: {...} } }
| Plugin Package | Priority | Responsibility |
|---|---|---|
@sharpee/plugin-npc |
100 | NPC service (extracted from engine, -57 lines) |
@sharpee/plugin-state-machine |
75 | Declarative FSM runtime (ADR-119) |
@sharpee/plugin-scheduler |
50 | Daemon/fuse scheduler service |
@sharpee/plugins |
— | Core TurnPlugin contract + PluginRegistry |
Five imperative event handlers migrated to declarative state machines:
| Machine | States | Pattern |
|---|---|---|
| Trapdoor | open / closed | Linear progression |
| Death Penalty | alive / one_death / game_over | Linear with CustomEffect scoring |
| Rainbow | inactive / active | Bidirectional toggle |
| Reality Altered | inactive / shown | Terminal |
| Victory | playing / victory | Terminal (replaced daemon polling) |
Zip archive: story.js (ESM) + meta.json + optional theme.css and assets/. Bundle size: ~147KB for Dungeon.
8 Rust IPC commands. Saves to AppData. Windows MSI + macOS DMG + Linux AppImage/DEB installers. Version sync across all 4 sources. Engine restart fix.
Multi-slot localStorage with lz-string compression (~70% reduction). Auto-save every turn. Envelope format preserves plugin state. Delta-based world state serialization.
Entity annotations with illustrations. Scoped CSS themes. Asset map plumbing via React context. Float layout for images.
DOS-era Infocom aesthetic. Command history, status line, PC speaker beep. eventemitter3 for browser compatibility. Updated to 0.9.90-beta.
Product identity boundary established: Zifmia = generic runner, story installer = author's branded product. Future npx sharpee build --tauri packaging.
Story-level engine with platform interceptor hooks
// PC attacks NPC:
attacking.ts -> ActionInterceptor -> MeleeInterceptor (Dungeon)
-> BasicCombatInterceptor (generic)
// NPC attacks PC:
npc-service.ts -> NpcCombatResolver -> meleeNpcResolver (Dungeon)
-> basicNpcResolver (generic)
// No combat extension loaded:
"Violence is not the answer." // validation block
| Component | Details |
|---|---|
melee.ts |
390 lines. Canonical FIGHT-STRENGTH, BLOW resolution, 9 outcomes |
melee-tables.ts |
189 lines. DEF1/DEF2/DEF3 result tables from MDL source |
melee-messages.ts |
273 lines. 5 weapon tables, 3-7 variants per outcome |
@sharpee/ext-basic-combat |
Generic combat for stories without custom melee |
| DIAGNOSE command | Wound status messages matching MDL melee.137:302-324 |
| Cure daemon | CURE-CLOCK: heals 1 wound per 30 turns |
Cascade, override, and keyed registration modes
// Chain: opening a container automatically reveals contents
world.chainEvent('if.event.opened', (event, world) => {
const items = getVisibleContents(event.data.entityId);
if (items.length === 0) return null;
return { type: 'if.event.revealed', data: { items } };
}, { mode: 'keyed', key: 'stdlib.chain.opened-revealed' });
// Transaction grouping
// if.event.opened { transactionId: 'txn-123', chainDepth: 0 }
// if.event.revealed { transactionId: 'txn-123', chainDepth: 1 }
25+ actions migrated from action.success to domain events
// Before: Generic action.success with messageId
world.createEvent('action.success', { messageId: 'if.action.taking.taken' });
// After: Domain event with typed data
world.createEvent('if.event.took', {
messageId: 'if.action.taking.taken',
params: { item: itemName, container: containerName },
});
// Key fix: NPC template substitution (ISSUE-054)
// Must include params: action.data in npc.spoke/npc.emoted events
| Hook Point | Purpose |
|---|---|
preValidate |
Block action before validation (e.g., hot axe prevents taking) |
postValidate |
Add requirements after standard validation |
preExecute |
Modify state before execution |
postExecute |
React to execution (e.g., trap triggers) |
postReport |
Add custom messages to output |
9 stdlib actions with interceptor support: going, putting, throwing, pushing, entering, taking, attacking, switching_on, dropping.
Going action checks BOTH source room interceptors (for if.action.going) and destination room interceptors (for if.action.entering_room). Used for gas room explosion on entry with lit flame source.
Zifmia is a standalone desktop application (not published on npm). See ADR-130 for the product identity boundary between the Zifmia runner and author story installers.
Canonical fidelity verified against 1981 MDL source (mdlzork_810722). Score: 650/650 main game + 100 endgame = 750 total. All NPCs complete. Endgame implemented with randomized trivia. Full game playable start to victory.
Ghost ritual: burn incense, pray, drop frame piece. 34-point scoring arc. Three-phase basin puzzle. Frame breaking sequence after thief death.
Position-based card detection (pos 36 per MDL CPOBJS[37]). 48-move canonical solution with 19 pushes. Template rendering and entity resolution fixes.
4 critical bugs fixed: crypt door, stone button, scoring, inventory strip. Mirror Room soft-lock resolved. 100 milestone points across 3 endgame rooms.
MDL-accurate timed delivery: order, 3-turn fuse, mailbox placement. OTVAL=1 ("One Lousy Point"). Donald Woods ASCII art.
10 room connections fixed per MDL source. Glacier melting, brick/fuse/safe explosion. Balloon flight refactor with VehicleTrait.
MDL 616-point version confirmed canonical. 4 non-canonical bonuses removed (55 pts). Score fully reconciled: 115 rooms + 260 OFVAL + 231 OTVAL + 10 light-shaft + 34 canvas = 650.
All 131 ADRs catalogued in flat table with verified statuses. Full roadmap section listing all non-implemented, non-abandoned ADRs. Plus 10 Superseded, 7 Abandoned, 4 Other.
WorldModel primitives replace ScoringService
Product identity boundary for packaging
BFS-driven game explorer for breadth coverage testing
| ADR | Change |
|---|---|
| ADR-078 | Canvas puzzle — now fully Implemented |
| ADR-100 | Screen Reader Client — IDENTIFIED to ACCEPTED, 3-tier plan written |
| ADR-004 | Marked Superseded (by four-phase action pattern) |
| ADR-013 | Marked Abandoned (lighting as extension) |
| ADR-034 | Marked Abandoned (event sourcing) |
| ADR-048 | Marked Abandoned (static language arch) |
| ADR-059 | Marked Abandoned (action customization boundaries) |
| ADR-112 | Marked Abandoned (client security model) |
| ADR-113 | Marked Abandoned (map position hints) |
Previous ADRs (093-127) documented in the Feb 11 report.
esbuild bundles platform + story + tester into single JS. Build: 95s to 2.5s (38x). --version VER flag on build.sh for explicit version control.
Eliminated dist-npm/ directory across all 19+ packages. Fixed dual module resolution bug where esbuild resolved stale dist-npm/ over current dist/.
ts-forge was rewriting imports to broken relative paths and stripping workspace dependencies. Fixed in tsf repo. All packages publishable.
Updated for Tauri v2 (DMG path changed). Restructured for extract, codesign, rebuild. Fixed root-owned file permissions and trap cleanup race.
Mobile responsive nav (flex-wrap + media queries). Browser client updated to 0.9.90-beta. Stale games/dungeo/ directory pruned. Downloads page with Linux installers.
README: 20 packages + roadmap. CONTRIBUTING: rewritten (149 in, 450 out). Architecture README with key ADRs. API Reference: 16 HTML pages. ADR index: complete.
ADR-129: Transactional Score Ledger + NPC Serialization Fix
Replaced ScoringService with WorldModel primitives. Fixed NPC canAct getter lost after JSON serialization. Thief re-stealing fix via IdentityTrait.concealed.
dist-npm/ Elimination + Capability Registry Fix + Volcano Region
Eliminated dual module resolution across all packages. globalThis pattern for cross-module registry (ISSUE-052). Volcano room connections fixed per MDL. 5 new MDX developer guide pages.
Volcano Puzzles + Balloon VehicleTrait Refactor
Glacier melting, brick/fuse/safe explosion. Balloon flight rewritten with VehicleTrait integration. GDT commands removed from walkthroughs, replaced with real navigation.
Serialization Audit + NPM Publishing Fix + Exorcism Fix
~50 (entity as any) instances converted to entity.attributes. ts-forge import rewriting fix. Exorcism handler event ID corrected. Commands without assertions now validation errors.
Fast Test Bundle (38x) + Plugin State Save + Walkthrough Renumber
esbuild single-file test bundle. Save/restore envelope format with plugin states. All walkthroughs renumbered wt-01 through wt-15. Score: 600/650. canContain(), entity.name, scheduler fuse, disambiguation fixes.
Royal Puzzle + Canvas Puzzle + Don Woods Stamp = 650/650
Royal puzzle rewritten with position-based card detection. Canvas/ghost ritual completed (34 pts). Timed brochure delivery. ADR-130 Zifmia packaging. Zifmia 0.9.90 released. 771 tests across 16 walkthroughs.
Endgame Complete + ADR-100 Accessibility + Documentation Overhaul
4 endgame bugs fixed, 100 milestone points, wt-17 endgame walkthrough. ADR-100 accepted with 3-tier ARIA plan. Mobile nav fix. All docs rewritten: README, CONTRIBUTING, architecture, API, ADR index (131 ADRs). Version 0.9.91.
For the full timeline from April 2025 through February 11, 2026, see the previous report.
| Bug | Fix |
|---|---|
NPC serialization (canAct getter lost) |
Replace with direct isAlive && isConscious property access |
Dual module resolution (dist/ vs dist-npm/) |
Eliminated dist-npm/ entirely across all packages |
| Capability registry cross-module (ISSUE-052) | globalThis pattern for Map persistence |
canContain() missing VEHICLE/ENTERABLE |
One-line fix adding both trait checks |
entity.name priority |
Fixed: IdentityTrait.name > displayName > id |
| Scheduler fuse same-turn tick (off-by-one) | skipNextTick boolean flag |
Disambiguation {options} rendering (ISSUE-054) |
Skip tryProcessDomainEventMessage for client.query |
MessageEffect event loss (ISSUE-056) |
One-line push to pendingEmittedEvents |
| Engine restart double entities | New restartGame() method with world.clear() |
| NPM publishing (ts-forge import rewriting) | Fixed in tsf repo; stopped stripping workspace deps |
The platform has reached a new milestone: the Dungeon validation story is complete with a perfect score, the endgame is implemented, all 17 walkthroughs pass, and all documentation has been rewritten. 131 ADRs catalogued and verified, 20 npm packages published, accessibility roadmap accepted.
| Aspect | Rating | Key Improvement (Since Feb 11) |
|---|---|---|
| Architecture | Excellent | Score ledger in world model, globalThis registry, plugin state save |
| Extensibility | Excellent | ADR-130 packaging boundary, condition evaluator extensions |
| Text Pipeline | Excellent | NPC template substitution fix (ISSUE-054), message ID enforcement |
| Parser | Excellent | Wire/alias disambiguation, entity.name priority fix |
| Desktop Client | Excellent | 0.9.90 release, Linux installers, engine restart, version sync |
| Combat System | Excellent | NPC serialization fix, full game verified through all walkthroughs |
| Build System | Excellent | 38x fast test bundle, dist-npm elimination, NPM publishing fixes |
| Test Coverage | Excellent | 3,500+ unit tests, 17 walkthroughs (771+ tests), all passing |
| Documentation | Excellent | Complete rewrite: README, CONTRIBUTING, architecture, API, 131 ADRs indexed |
| Story Fidelity | Excellent | 650/650 perfect score, endgame complete, FORTRAN audit reconciled |
| Accessibility | Planned | ADR-100 accepted, 3-tier ARIA plan, audit complete |
| Category | Remaining Work |
|---|---|
| Accessibility (ADR-100) | Tier 1: transcript aria-live, input label, status line ARIA. Tier 2: semantic HTML5, dialog focus traps. Tier 3: announcements, skip links, high contrast |
| ADR-131 | Automated world explorer (BFS-driven breadth coverage testing) |
| ADR-130 | Story installer packaging (npx sharpee build --tauri) |
| ADR-089 Phase E | Advanced verb conjugation, past tense support |
| Dungeon Polish | Thief movement to fixed room-list cycling (matches MDL). Royal puzzle second exit (slot + steel door). Full 750/750 walkthrough chain clean pass |
| GenAI Layer | Story spec templates, LLM code generation pipeline |
| NPC System | Conversation trees, behavior scheduling, dialogue extensions |
| File Renames | ADR-013b merge into ADR-014, ADR-013c to intfiction-post.md, ADR-082 duplicate to ADR-082z |