Skip to content

Repository files navigation

opencase

A deterministic detective game engine and a browser desktop.

Build and deploy GPL-2.0-or-later license

Play the game · Source code · Write a case

opencase runs investigation stories from portable YAML packages. A case owns its story, translations, media, and tests. The engine owns generic commands, events, rules, clocks, and saves. The browser app connects them through a player-safe projection, without adding case-specific logic to React or TypeScript.

The production game is fully static. It includes a desktop, case notebook, case board, inbox, phone, files, research, local player profiles, and per-case progress.

Features

  • Portable case folders with YAML, images, audio, translations, and tests
  • Deterministic command → event → reducer → rule execution
  • Case tests written from the detective's point of view
  • Multilingual case catalogs
  • Browser-local profiles, saves, imports, and desktop layouts
  • Static deployment with no gameplay server or database
  • A decoupling gate that rejects case IDs and story tokens in engine code

Quick start

You need a current Node.js release and npm.

git clone https://github.com/f/opencase.git
cd opencase
npm install
npm run dev

Open http://127.0.0.1:4173.

Run the complete repository gate before opening a pull request:

npm run check

Case packages

Every case is a self-contained, lowercase kebab-case folder:

cases/
└── my-first-case/
    ├── case.yml
    ├── assets/
    │   └── room-photo.webp
    ├── i18n/
    │   ├── en.yml
    │   └── tr.yml
    └── tests/
        └── shortest-solution.yml

case.yml is the only source of story logic. assets/ contains media, i18n/ contains player-facing text, and tests/ contains executable case scenarios. A new case does not require an engine or app change.

A source file begins with engine-independent data like this (excerpt):

schema: case-source/v0.1
case:
  id: my_first_case
  version: 1.0.0
  locale: en
  title: The First Clue
  duration: 5m
  mode: elastic
  final_conclusion: first-write-wins
  time: {date: "2026-01-01", timezone: UTC, starts_at: "09:00"}
  synopsis: A mug shows where the courier stopped.

use: [investigation@1, artifacts@1]

# Cast, places, things, truth, and perspectives are authored here.
opening:
  call: {from: dispatcher, text: Start with the mug photo.}
  grants: [mug_photo]
  starts: []

evidence:
  mug_photo:
    tool: image
    at: start
    reports: {place: hall}

Use a tiny example for a complete working package. The YAML reference explains every field, and the case-test reference explains tests/*.yml.

Compile and test one package:

npx tsx scripts/compile-cases.ts cases/my-first-case
npx tsx src/simulator/cli.ts cases/my-first-case

Architecture

flowchart LR
    subgraph Authoring["Portable case package"]
        Source["case.yml<br/>assets/<br/>i18n/"]
        Tests["tests/*.yml"]
    end

    subgraph Build["Build and verification"]
        Compiler["Package compiler"]
        Kernel["Private kernel IR"]
        Manifest["Player-safe public manifest"]
        Bundle["Static runtime bundle"]
        Runner["Case test runner"]
    end

    subgraph Play["Browser runtime"]
        Host["Browser host"]
        Engine["Deterministic engine"]
        Save[("Opaque save")]
        Shell["Desktop shell"]
    end

    Source --> Compiler
    Tests --> Runner
    Compiler --> Kernel
    Kernel --> Runner
    Compiler --> Manifest
    Compiler --> Bundle
    Manifest --> Host
    Bundle --> Host
    Host -->|generic command| Engine
    Engine -->|player-safe projection| Shell
    Engine -->|event log and snapshot| Save
    Save -->|restore| Host
Loading

The boundaries are strict:

Layer Owns Must not own
Case Story, evidence, routes, translations, assets, tests Engine code, UI code, player data
Engine Generic commands, events, rules, clocks, projections, saves Case IDs, character names, authored routes, windows
Browser host Runtime loading, imports, save slots, asset resolution Case-specific gameplay rules
Desktop shell Windows, visual state, input, animations Hidden truth or duplicate gameplay state

Read Architecture for the full execution, persistence, and asset model. Normative rules are in the engine contract.

Static build

npm ci
npm run build

The output is in dist/. It works at a domain root or a repository subpath. GitHub Actions checks the project, builds it, and deploys it to Pages.

Static runtime bundles contain complete case mechanics. Someone who inspects downloaded JSON or JavaScript can find the answers. The projection boundary keeps normal gameplay honest, but it is not a secrecy boundary against source inspection.

Documentation

Document Use it for
Case YAML reference Fields, expressions, validation, and examples
Case localization $text catalogs, fallback, and digest rules
Detective case tests Scenario steps, public assertions, and diagnostics
Tiny case examples Small packages you can copy and change
AI case-authoring skill Writing a case with an AI workflow
Architecture Data flow, state ownership, replay, and assets
Engine contract Normative kernel and public-boundary rules
Profiles and case library Saves, GitHub imports, and verification labels
Desktop shell Window behavior and shell-state boundaries
Development and deployment Commands, generated files, repository map, and Pages
Example research Sources used for realistic fictional cases
Third-party assets Interface asset origins and licenses

Project status

opencase is an early working demo (0.1.0). The engine, browser host, desktop, built-in cases, case compiler, and conformance runner work together. The case schema and save compatibility may still change before a stable release.

License

opencase is free software licensed under the GNU General Public License version 2 or later (GPL-2.0-or-later). Third-party assets keep their respective licenses; see Third-party assets for details.

Contributing

Issues and pull requests are welcome. Keep each change inside the layer that owns the behavior. In particular, do not solve a case-authoring problem with a case-specific condition in the engine or app.

Before submitting a change, add the smallest relevant tests, run npm run check, and review generated public manifests for unintended private story data. More details are in Development and deployment.

About

Data-driven detective game engine and playable case demo

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages