Cloak of Darkness Tutorial
Cloak of Darkness is a simple game used to compare interactive fiction systems. By building it, you'll learn the core concepts of Sharpee.
The Game
Three rooms: Foyer, Bar, Cloakroom
One object: A velvet cloak (worn by the player)
The puzzle: The bar is dark. If you try to do anything in the dark, you "disturb" a message written in the dust. Hang your cloak on the hook in the cloakroom first — the cloak blocks light. Then you can read the message in the now-lit bar.
Win condition: Read the message in the bar without disturbing it.
Prerequisites
- Node.js 18+
- Basic TypeScript knowledge
Step 1: Create the Project
Use the Sharpee CLI to scaffold your project:
npx @sharpee/sharpee init cloak-of-darkness
Answer the prompts:
- Story title: Cloak of Darkness
- Story ID: cloak-of-darkness
- Author name: Your Name
- Description: A basic IF demonstration
Then install dependencies:
cd cloak-of-darkness
npm install
Your project now has:
cloak-of-darkness/
├── src/
│ └── index.ts # Your story code
├── package.json
├── tsconfig.json
└── .gitignore
Step 2: Understand the Story Structure
Open src/index.ts. Your story is a class that implements the Story interface with two required methods: initializeWorld (creates rooms and objects) and createPlayer (creates the player entity).
import {
Story,
StoryConfig,
GameEngine,
} from '@sharpee/engine';
import {
WorldModel,
IFEntity,
EntityType,
IdentityTrait,
ActorTrait,
ContainerTrait,
RoomTrait,
WearableTrait,
SceneryTrait,
SupporterTrait,
LightSourceTrait,
Direction,
} from '@sharpee/world-model';
export const config: StoryConfig = {
id: 'cloak-of-darkness',
title: 'Cloak of Darkness',
author: 'Your Name',
version: '1.0.0',
description: 'A basic IF demonstration',
};
export class CloakOfDarkness implements Story {
config = config;
initializeWorld(world: WorldModel): void {
// Your rooms and objects go here
}
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;
}
}
export const story = new CloakOfDarkness();
export default story;
Step 3: Create the Rooms
Rooms are entities with RoomTrait and IdentityTrait. Fill in the initializeWorld method:
initializeWorld(world: WorldModel): void {
// === FOYER ===
const foyer = world.createEntity('foyer', EntityType.ROOM);
foyer.add(new IdentityTrait({
name: 'Foyer of the Opera House',
description: 'You are standing in a spacious hall, splendidly decorated in red and gold, with glittering chandeliers overhead. The entrance from the street is to the north, and there are doorways south and west.',
}));
foyer.add(new RoomTrait({
exits: {
[Direction.SOUTH]: { destination: 'bar' },
[Direction.WEST]: { destination: 'cloakroom' },
},
}));
// === CLOAKROOM ===
const cloakroom = world.createEntity('cloakroom', EntityType.ROOM);
cloakroom.add(new IdentityTrait({
name: 'Cloakroom',
description: 'The walls of this small room were clearly once lined with hooks, though now only one remains. The exit is a door to the east.',
}));
cloakroom.add(new RoomTrait({
exits: {
[Direction.EAST]: { destination: foyer.id },
},
}));
// === BAR (dark) ===
const bar = world.createEntity('bar', EntityType.ROOM);
bar.add(new IdentityTrait({
name: 'Foyer Bar',
description: 'The bar, much rougher than you would have guessed after the opulence of the foyer to the north, is completely empty. There seems to be some sort of message scrawled in the sawdust on the floor.',
}));
bar.add(new RoomTrait({
isDark: true,
exits: {
[Direction.NORTH]: { destination: foyer.id },
},
}));
// Fix foyer exits to use actual IDs
const foyerTrait = foyer.get(RoomTrait);
if (foyerTrait) {
foyerTrait.exits[Direction.SOUTH] = { destination: bar.id };
foyerTrait.exits[Direction.WEST] = { destination: cloakroom.id };
}
// Place player in foyer
const player = world.getPlayer();
if (player) {
world.moveEntity(player.id, foyer.id);
}
}
Note: We update the foyer exits after creating all rooms so we can reference the actual entity IDs.
Step 4: Create the Cloak
The cloak is key — it blocks light while worn. Add this before the player placement:
// === THE CLOAK ===
const cloak = world.createEntity('cloak', EntityType.OBJECT);
cloak.add(new IdentityTrait({
name: 'velvet cloak',
aliases: ['cloak', 'velvet', 'dark cloak', 'black cloak'],
description: 'A handsome cloak, of velvet trimmed with satin, and slightly spattered with raindrops. Its blackness is so deep that it almost seems to suck light from the room.',
}));
cloak.add(new WearableTrait({ isWorn: true }));
// Place on player (worn)
if (player) {
world.moveEntity(cloak.id, player.id);
}
Step 5: Create the Hook
The hook in the cloakroom holds the cloak:
// === THE HOOK ===
const hook = world.createEntity('hook', EntityType.OBJECT);
hook.add(new IdentityTrait({
name: 'small brass hook',
aliases: ['hook', 'brass hook', 'peg'],
description: "It's just a small brass hook, screwed to the wall.",
}));
hook.add(new SceneryTrait());
hook.add(new SupporterTrait({ capacity: 1 }));
world.moveEntity(hook.id, cloakroom.id);
Step 6: Create the Message
The message in the sawdust is the win condition:
// === THE MESSAGE ===
const message = world.createEntity('message', EntityType.OBJECT);
message.add(new IdentityTrait({
name: 'message',
aliases: ['message', 'sawdust', 'scrawl', 'writing', 'floor'],
description: '', // Set dynamically based on disturbance
}));
message.add(new SceneryTrait());
world.moveEntity(message.id, bar.id);
Step 7: Build and Test
Build your story:
npx @sharpee/sharpee build
open dist/web/index.html
Sample Playthrough
Winning path
> look
Foyer of the Opera House
You are standing in a spacious hall...
> west
Cloakroom
The walls of this small room were clearly once lined with hooks...
> hang cloak on hook
You put the velvet cloak on the small brass hook.
> east
Foyer of the Opera House
> south
Foyer Bar
The bar, much rougher than you would have guessed...
> read message
The message, neatly marked in the sawdust, reads: "You have won!"
Losing path
> south
It's pitch dark, and you can't see a thing.
> look
In the dark? You could easily disturb something!
> north
Foyer of the Opera House
> west
Cloakroom
> hang cloak on hook
You put the velvet cloak on the small brass hook.
> east. south
Foyer Bar
> read message
The message has been carelessly trampled...
What You Learned
- Project setup: Using
npx @sharpee/sharpee initto scaffold a project - Story class: Implementing the
Storyinterface withinitializeWorldandcreatePlayer - Entity creation: Using
world.createEntity(name, EntityType.X)for rooms, objects, and actors - Traits: Adding behaviors with
entity.add(new SomeTrait({...}))— like snapping Lego pieces together - Room connections: Setting exits via
RoomTrait - Dark rooms: Using
isDark: trueinRoomTrait
Next Steps
Ready for more? The Family Zoo tutorial covers multi-file stories, custom actions, NPCs, timed events, and scoring.