A TypeScript/Node.js port of Open Adventure, which is itself a forward-port of Colossal Cave Adventure 2.5 (Crowther & Woods, 1995). The goal is byte-identical gameplay output compared to the C original.
This repository is a pnpm workspace with two packages:
@open-adventure/core— platform-agnostic game engine. Zeronode:*imports (lint-enforced). Exports a host-drivenrunGame()entry point, factories, and pure save helpers (serializeGame,deserializeGame,summarizeSave). Browser projects can depend on this package directly.@open-adventure/cli— Node.js CLI wrapper. ProvidesConsoleIO(terminal I/O vianode:readline) andNodeFileStorage(file-based saves), wires them intorunGame, and exposes theadventbinary.
- Node.js 24 or later
- pnpm (the repo is a pnpm workspace)
pnpm install # install workspace dependencies
pnpm play # play the game in the terminal-o Old-style mode (no prompt, no stripping of articles)
-r FILE Resume from a saved game file
-l FILE Log input to a file
-d Enable debug output (random-trace to stderr)
You can also pipe input from a file:
pnpm play < script.txtA browser host implements two small interfaces and calls runGame:
import { runGame, type GameIO, type SaveStorage } from "@open-adventure/core";
class BrowserIO implements GameIO {
readonly echoInput = false;
print(msg: string): void {
/* append msg to a DOM log */
}
readline(prompt: string): Promise<string | null> {
return new Promise((resolve) => {
/* call resolve(line) when the user submits a line, resolve(null) on EOF */
});
}
}
class LocalStorageStorage implements SaveStorage {
async read(name: string): Promise<string | null> {
return localStorage.getItem(`adventure:${name}`);
}
async write(name: string, data: string): Promise<void> {
localStorage.setItem(`adventure:${name}`, data);
}
}
const exitCode = await runGame({
io: new BrowserIO(),
storage: new LocalStorageStorage(),
});runGame returns when the game ends (player quits, dwarf kill, normal endgame). Hosts can also use the pure helpers directly to build save-picker UIs:
import { serializeGame, deserializeGame, summarizeSave } from "@open-adventure/core";
const json = serializeGame(state); // → JSON string
const result = deserializeGame(json); // → { ok, state } | { ok: false, reason, ... }
const summary = summarizeSave(json); // → location/score/inventory/phase metadatapnpm build # compile both packages to packages/*/dist
pnpm typecheck # type-check both packages and root scripts
pnpm test # run unit tests (vitest)
pnpm lint # ESLint (enforces no node:* imports in core)
pnpm test:regress # run all 107 regression tests (TAP output)
npx tsx scripts/regress.ts --test pitfall # run a single regression test
pnpm generate # regenerate dungeon.generated.ts from adventure.yaml
pnpm generate:check # verify generated dungeon data is up to date
pnpm generate:graph # Graphviz DOT of the dungeon map (stdout)The development scripts that consume @open-adventure/core (typecheck, test, test:regress, play) automatically build core first via pnpm build:core so a clean checkout works without a manual setup step.
Core package release artifacts are automated through GitHub Actions. Pushing a tag in the format vX.Y.Z triggers the Release Core Package workflow, which:
- validates the tag version against
packages/core/package.json - builds only
@open-adventure/core - creates an npm package tarball (
.tgz) - uploads the tarball to the matching GitHub Release
Release steps:
- Bump
packages/core/package.jsonversion to the target release version. - Commit and push to your branch.
- Create and push the tag:
git tag vX.Y.Z
git push origin vX.Y.Z- Confirm the workflow run succeeds and the
.tgzasset is attached to the GitHub Release for that tag.
For dry runs, trigger Release Core Package manually via workflow_dispatch from your current branch commit, provide a vX.Y.Z tag string, and leave upload_release_asset disabled to validate build and packaging without uploading a release asset.
Detailed release steps (including the safest npm version auto-tag flow via pnpm exec) are documented in docs/release.md.
These scripts compare TypeScript output directly against the original C advent binary built in ../open-adventure. Run them after any gameplay change.
# Diff every regression .log against both implementations
npx tsx scripts/cross-compare.ts
# Random command sequences, deterministic seeds for reproducibility
npx tsx scripts/fuzz-compare.tspackages/
core/ @open-adventure/core (browser-portable)
adventure.yaml Game data source
scripts/
make-dungeon.ts Code generator (reads adventure.yaml)
make-graph.ts Graphviz DOT generator
src/
index.ts Public API barrel
types.ts Interfaces, enums, constants
dungeon.ts Re-exports from generated data
dungeon.generated.ts Generated game data
run-game.ts runGame() entry point
deps.ts GameLoopDeps wiring
game-loop.ts Main game loop and phase dispatch
actions.ts All verb action handlers
movement.ts Travel table lookup and player movement
dwarves.ts Dwarf AI and pirate logic
input.ts Input handling, yes/no prompts
format.ts Message formatting (speak, rspeak, pspeak)
vocabulary.ts Word lookup
object-manipulation.ts Carry, drop, move, juggle
init.ts Game state initialization
save.ts In-game suspend/resume verbs
save-pure.ts Pure save helpers + savefile + isValid
score.ts Scoring (computeScore + terminate)
rng.ts Deterministic LCG PRNG
test-io.ts ScriptIO (in-memory GameIO for tests)
cli/ @open-adventure/cli (Node.js)
src/
main.ts CLI entry (parses argv, calls runGame)
console-io.ts ConsoleIO (node:readline)
node-storage.ts NodeFileStorage (SaveStorage on node:fs)
cheat.ts Test fixture generator
scripts/
regress.ts Regression test runner
cross-compare.ts Diff TS output against the C reference binary
fuzz-compare.ts Random-input comparison against the C reference binary
tests/
*.log / *.chk Regression test inputs and expected outputs
maps/ Pre-rendered SVG dungeon maps
BSD-2-Clause, following the original.