Project Structure
Sharpee Project Structure Guide
A guide for developers working with the Sharpee platform.
Overview
Sharpee is organized as a monorepo with platform packages and story implementations. Stories are self-contained projects that use the platform to create interactive fiction games.
Repository Layout
sharpee/├── packages/ # Platform packages│ ├── core/ # Core types and events│ ├── engine/ # Game engine runtime│ ├── world-model/ # Entity system, traits, behaviors│ ├── stdlib/ # Standard actions and patterns│ ├── parser-en-us/ # English parser and grammar│ ├── lang-en-us/ # English language messages│ ├── event-processor/ # Event handling pipeline│ ├── text-service/ # Output rendering│ ├── text-blocks/ # Text block processing│ ├── if-domain/ # IF domain types│ ├── if-services/ # IF service interfaces│ ├── plugin-npc/ # NPC turn plugin│ ├── plugin-scheduler/ # Daemon/fuse scheduler plugin│ ├── plugin-state-machine/ # State machine plugin│ ├── plugins/ # Plugin registry and interfaces│ ├── platform-browser/ # Browser platform adapter│ ├── extensions/│ │ └── testing/ # Test utilities│ ├── transcript-tester/ # Transcript test runner│ └── sharpee/ # Umbrella package (CLI + re-exports)├── stories/ # Story implementations│ └── dungeo/ # Mainframe Zork port (~191 rooms)├── website/ # Documentation site (Astro/Starlight)├── docs/ # Internal documentation│ ├── architecture/adrs/ # Architecture Decision Records│ ├── reference/ # API reference│ └── work/ # Work-in-progress tracking├── build.sh # Main build script└── scripts/ # Utility scriptsStory Structure
Each story lives in stories/{story-name}/ and follows a canonical structure:
stories/{story}/├── package.json├── tsconfig.json├── src/│ ├── index.ts # Story class and entry point│ ├── regions/ # Room definitions (one file per region)│ ├── npcs/ # NPC entities and behaviors (one folder per NPC)│ ├── actions/ # Story-specific actions (one folder per action)│ ├── handlers/ # Event handlers and puzzles│ ├── grammar/ # Parser extensions│ ├── messages/ # Language extensions│ ├── orchestration/ # Engine registrations│ ├── scheduler/ # Daemons and fuses│ ├── traits/ # Custom traits│ └── scoring/ # Scoring system (if needed)└── tests/ └── transcripts/ # Integration test transcriptsKey principle: Flat file organization within each folder. Regions are single files containing all rooms and objects for that area, not nested directories.
Core Files
src/index.ts — Story Entry Point
The main story file implements the Story interface:
import { Story, StoryConfig, GameEngine } from '@sharpee/engine';import { WorldModel, IFEntity, EntityType, IdentityTrait, ActorTrait, ContainerTrait } from '@sharpee/world-model';import type { Parser } from '@sharpee/parser-en-us';import type { LanguageProvider } from '@sharpee/lang-en-us';
export const config: StoryConfig = { id: 'my-story', title: 'My Story Title', author: 'Your Name', version: '1.0.0', description: 'A brief description of your story',};
export class MyStory implements Story { config = config; private world!: WorldModel;
// Required: set up all rooms, objects, connections initializeWorld(world: WorldModel): void { this.world = world; // Create regions, objects, event handlers... }
// Required: create the player entity createPlayer(world: WorldModel): IFEntity { const player = world.createEntity('yourself', EntityType.ACTOR); player.add(new IdentityTrait({ name: 'yourself', aliases: ['self', 'me'], description: 'As good-looking as ever.', properName: false, })); player.add(new ActorTrait({ isPlayer: true })); player.add(new ContainerTrait({ capacity: 100 })); return player; }
// Optional: extend parser with story-specific grammar extendParser?(parser: Parser): void { // Add custom commands... }
// Optional: add story-specific messages extendLanguage?(language: LanguageProvider): void { // Add custom messages... }
// Optional: provide story-specific actions getCustomActions?(): any[] { return []; }
// Optional: post-engine initialization (register NPCs, daemons, etc.) onEngineReady?(engine: GameEngine): void { // Access plugin registry, register NPC behaviors, etc. }}
export const story = new MyStory();export default story;src/regions/ — Room Definitions
Each region is a single file containing all rooms and objects for that area:
src/regions/├── white-house.ts # Starting area├── house-interior.ts # Inside the house├── forest.ts # Forest paths and clearings├── underground.ts # Cellar, passages, troll room└── temple.ts # Temple areaPattern:
- Type export —
XxxRoomIdsinterface for type-safe room references createXxxRegion()— Creates all rooms, returns ID mapcreateXxxObjects()— Creates and places objects in roomsconnectXxxTo...()— Cross-region connections
src/npcs/ — Non-Player Characters
Each NPC gets its own folder:
src/npcs/├── guard/│ ├── index.ts # Registration function│ ├── guard-entity.ts # Entity creation│ ├── guard-behavior.ts # Turn logic│ └── guard-messages.ts # Message IDs└── merchant/ └── ...src/actions/ — Story-Specific Actions
Each action gets its own folder:
src/actions/├── index.ts # Exports customActions array├── ring/│ ├── ring-action.ts│ └── ring-messages.ts└── pray/ └── ...src/grammar/ — Parser Extensions
Organize grammar patterns by feature:
src/grammar/├── index.ts # Exports registerAllGrammar()├── puzzle-grammar.ts # Puzzle-specific commands└── speech-grammar.ts # SAY, TALK TO patternssrc/messages/ — Language Extensions
Organize messages by feature:
src/messages/├── index.ts # Exports registerAllMessages()├── npc-messages.ts # NPC dialogue and actions└── puzzle-messages.ts # Puzzle feedbackBest Practices
Message ID Naming
Use a consistent convention:
export const MyMessages = { // Pattern: {story}.{feature}.{event} DOOR_OPENS: 'mystory.door.opens', DOOR_LOCKED: 'mystory.door.locked', GUARD_GREETS: 'mystory.guard.greets',} as const;Separation of Concerns
| Layer | Responsibility |
|---|---|
grammar/ | How commands are parsed |
messages/ | What text is shown |
actions/ | What mutations occur |
handlers/ | How events are processed |
orchestration/ | How systems are wired |
Testing
Write transcript tests for integration testing:
> examine door* locked
> unlock door with key* unlocked
> open door* opensRun tests:
node dist/cli/sharpee.js --test stories/mystory/tests/transcripts/puzzle.transcript