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 month of intensive development since January 9, 2026 — the most transformative period in the project's history.
Complete architectural separation: grammar declares semantics (what kind of thing), actions enforce scope (can the player reach it). Six-phase refactor completed in one day.
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.
Complete rewrite of text output. FyreVM-inspired channel I/O. Pure function: events in, TextBlocks out.
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);
Engine defines hook points in the turn cycle. NPCs, scheduler, and state machines are now plugins implementing TurnPlugin. Engine shrinks, becomes pure orchestrator.
4 new packages extracted from engine core
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;
}
// Future subsystems don't modify engine - just create new plugins
engine.registerPlugin(new MyCustomPlugin());
| 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) |
Tauri v2 desktop app (~10MB) with React UI, .sharpee bundle format, rich media support, and multi-slot save/restore.
Zip archive: story.js (ESM) + meta.json + optional theme.css and assets/. Bundle size: ~147KB for Dungeo.
8 Rust IPC commands. Saves to AppData. Windows MSI + macOS DMG + Linux DEB installers. Professional gold "Z" branding.
Multi-slot localStorage with lz-string compression (~70% reduction). Auto-save every turn. 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.
Illustration toggle and size settings. localStorage persistence. React context + usePreferences() hook.
Full combat system: FIGHT-STRENGTH formula, BLOW resolution, 9 outcome types, 3 result tables, wound healing, disengagement rules.
Story-level engine with platform interceptor hooks
// PC attacks NPC:
attacking.ts -> ActionInterceptor -> MeleeInterceptor (Dungeo)
-> BasicCombatInterceptor (generic)
// NPC attacks PC:
npc-service.ts -> NpcCombatResolver -> meleeNpcResolver (Dungeo)
-> 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 |
New package: 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 },
});
Pre/post hooks on stdlib actions for story-specific puzzle logic. Destination interceptors for room entry conditions.
| 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.
Canonical fidelity verified against 1981 MDL source (mdlzork_810722). Score: 281/616 implemented. Troll, Thief, and Cyclops NPCs complete with full combat.
Three combat states, NPC inventory, 3-hit kill, 2-turn recovery, axe white-hot blocking, death with sinister smoke.
Eat-me cake shrink/teleport, red cake dissolves pool, cage/sphere trap with 10-turn poison gas, Low Room carousel.
Troll: canonical combat, axe mechanics. Thief: WINNING? AI, egg opening vulnerability. Combat disengagement + wound healing.
Complete rewrite vs MDL source. 15 twisty rooms + 7 coal mine rooms corrected. Self-loops restored.
118 pts treasure taking, 98 pts trophy case, 45 pts room visits, 20 pts achievements. OTVAL/OFVAL fixed across 14 files.
TR-001 (5 issues), TR-002 (43 discrepancies), TR-003 (6 gaps) — all resolved against canonical 1994 DOS version.
| Feature | Syntax | Purpose |
|---|---|---|
| Loop construct | [DO] ... [UNTIL "text"] |
Variable-length combat sequences |
| Retry blocks | [RETRY: max=5] ... [ENSURES: ...] [END RETRY] |
Handle combat randomness with state save/restore |
| Multi-match | [OK: contains_any "a" "b" "c"] |
Validate against multiple acceptable outputs |
| OR patterns | [UNTIL "win" OR "die"] |
Loop exit on any matching condition |
Adjective disambiguation on IdentityTrait
Cascade/override/keyed chain modes
Article, list, text formatters with chaining
Stateless FyreVM-inspired channel I/O
Pluggable dialogue extensions, narrative-as-code
Pronoun-triggered inference, auto-take for verbs
Event sourcing, literal + localized text
Pre/post hooks for story customization
States, transitions, triggers, guards, effects
TurnPlugin contract, 4 extracted plugins
.sharpee bundles, Tauri, illustrations, CSS themes
Multiple annotations per entity, trigger conditions
CSS Grid windowing (transcript, grid, image, status)
Room entry conditions, location-scoped actions
Plus ADR-097 (React Client), ADR-099/100 (GLK/Accessibility), ADR-123 (Daemon Hierarchy, partially accepted), and approximately 16 additional ADRs in the 108-117 range covering engine remediation, service extraction, and build system changes.
Dual TSConfig (CJS + ESM). 19 tsconfig.esm.json files. Explicit --alias flags for bundle resolution. 10 scripts consolidated to 3.
4 services extracted: VocabularyManager, SaveRestoreService, TurnEventProcessor, PlatformOperationHandler. Engine reduced 21%.
Plain Astro + Expressive Code. 18 pages. API reference for traits, actions, grammar. DSLM pattern catalog with D3.js visualization (124 patterns).
792 old session files archived. 5 obsolete doc folders deleted. README updated for 0.9.85. Comprehensive API reference (traits, actions, grammar).
Parser Refactor Complete (6 Phases)
Scope enforcement moved from grammar to actions. ScopeLevel enum, defaultScope on 24 actions, implicit takes for 6 actions. PR #49 merged.
ADR-093: Adjective Disambiguation + ADR-094: Event Chaining
IdentityTrait adjectives. Event chains with cascade/override/keyed modes. opened->revealed chain. 38 new tests.
ADR-095/096: Text Pipeline + Perspective Placeholders
Stateless TextService, formatter system, 42 template files updated. New text-blocks and text-service packages.
ADR-104: Implicit Inference + Engine Remediation
Implicit object inference, implicit take. 4 services extracted from GameEngine (-21%). Event-adapter reduced 44%.
Universal Capability Dispatch + Browser Client
5 core dispatch functions, resolution modes. Browser save/restore with multi-slot localStorage. DOS-era Infocom aesthetic.
ADR-106/107: Domain Events Migration
25+ actions migrated to domain events. Dual-mode authored content. Build scripts consolidated.
ADR-118: Action Interceptors + Handler Audit
Interceptor pattern for 9 actions. Full handler mutation audit (25 files). Dam state consolidation. FORTRAN source fidelity.
ADR-120: Engine Plugin Architecture (4 Phases)
4 new packages: plugins, plugin-npc, plugin-scheduler, plugin-state-machine. Engine becomes pure orchestrator.
Zifmia Runner (7 Phases) + Tauri Desktop
Bundle loader, storage, illustrations, CSS scoping, preferences, 8 Rust IPC commands. Inline SAVE/RESTORE dialogs.
Website Rebuild + Build System Overhaul
Astro site with 18 pages. Dual TSConfig architecture. Critical bundle resolution fix (--alias flags). StoryInfoTrait.
Tea Room Puzzle Area (5 Phases)
Eat-me cakes, cage/sphere trap, Low Room carousel, robot commands. 9 room corrections. 35+ transcript tests.
Combat System (Canonical Melee Engine)
390-line melee engine from MDL source. @sharpee/ext-basic-combat package. Troll/Thief/Cyclops integration. DO/UNTIL transcript loops.
The platform has undergone its most transformative month: 8 new packages, a plugin architecture, a desktop application, a canonical combat system, and comprehensive text pipeline. 379 development sessions produced 34 new ADRs and 700+ new tests.
| Aspect | Rating | Key Improvement (Since Jan 9) |
|---|---|---|
| Architecture | Excellent | Plugin architecture, scope separation, interceptors |
| Extensibility | Excellent | TurnPlugin, ActionInterceptors, CapabilityBehaviors |
| Text Pipeline | Excellent | Stateless service, formatters, perspective support |
| Parser | Excellent | Trait-based grammar, adjective disambiguation, implicit takes |
| Desktop Client | New | Zifmia: Tauri app, .sharpee bundles, multi-slot saves |
| Combat System | New | Canonical melee engine, generic combat extension |
| State Machines | New | Declarative FSM plugin, 5 handlers migrated |
| Test Coverage | Excellent | 3,500+ tests, 12 walkthroughs, DO/UNTIL loops |
| Documentation | Excellent | 127+ ADRs, API reference, pattern catalog (124 patterns) |
| Story Fidelity | Excellent | MDL source verification, 43 discrepancies resolved |
| Category | Remaining Work |
|---|---|
| Dungeo Completion | Remaining treasures (335/616 pts), endgame sequence, Thief roaming AI |
| ADR-089 Phase E | Advanced verb conjugation, past tense support |
| ADR-125 Implementation | Zifmia panel/windowing system (CSS Grid layout) |
| ADR-127 Implementation | Location-scoped interceptors (room-level action hooks) |
| Grammar Migration | Remaining verbs to .forAction() API |
| GenAI Layer | Story spec templates, LLM code generation pipeline |
| NPC System | Conversation trees, behavior scheduling, dialogue extensions |
| Accessibility | Screen reader support (ADR-100), GLK compatibility (ADR-099) |