Apps not written in JavaScript
The reference engine is TypeScript. If your app is in C#, Swift, Kotlin, Python, Rust, Go or GDScript, you have three options: run the reference engine from your language, call it as a local service, or use (or write) a port. All three give the same results, because everything crosses the boundary as JSON.
Everything is JSON
Section titled “Everything is JSON”Every engine call’s inputs and outputs are defined as JSON Schema in
schemas/engine/, published with the spec at
schemas/0.1/ on this site. Pack releases, character files and changes are JSON too. So in any
language you can:
- generate types for the calls from the schemas (quicktype, NJsonSchema, datamodel-code-generator and similar tools);
- validate character files and changes with any JSON Schema (draft 2020-12) validator;
- treat rulesets and paused actions as opaque JSON you store and pass back.
The engine is also pure: no file or network access, no clock, no memory between calls. Your app loads the packs and passes them in. That’s what makes the next two options work.
Option 1: embed the reference engine
Section titled “Option 1: embed the reference engine”Run the TypeScript engine, compiled to JavaScript, inside a JavaScript runtime embedded in your app:
| Platform | Embeddable runtime |
|---|---|
| iOS, macOS | JavaScriptCore (built into the OS) |
| Android, JVM | QuickJS or V8 bindings (J2V8, Javet) |
| .NET, Unity | Jint, ClearScript, or QuickJS bindings |
| Python | QuickJS bindings, or py_mini_racer (V8) |
| C, C++, Rust, Go | QuickJS (C), rquickjs (Rust), goja (Go) |
Bundle the engine into one JavaScript file (esbuild or similar), load it into the runtime, and expose thin wrappers that take and return JSON strings:
// bundled entry point, compiled to one fileimport * as engine from '@opencharacter/engine';
globalThis.ocf = { loadRuleset: (packs) => JSON.stringify(engine.loadRuleset(JSON.parse(packs))), computeSheet: (ruleset, character) => JSON.stringify(engine.computeSheet(JSON.parse(ruleset), JSON.parse(character))), // …the same for the other calls};Two things to plan for:
- Keep the ruleset on the JavaScript side. It’s large, and parsing it on every call is wasteful. Load it once into a variable in the runtime and refer to it by handle.
- Dice: the dice function is a callback, not data. Either let the engine use its own random
dice, or avoid callbacks entirely by running actions with
policy: { dice: 'pause' }and answering each pause with faces rolled in your language (Rolling dice). The pause-and-continue loop is all JSON.
This is the quickest way to get exactly the reference engine’s results. The project doesn’t test or package any particular embedding yet.
Option 2: a local service
Section titled “Option 2: a local service”If your app runs on a server, run the engine as a small Node, Deno or Bun process beside it and call it over HTTP or a pipe with the same JSON. Keep rulesets cached in that process. OCF has no hosted service, by design; this is a process you run yourself, next to your own app.
Option 3: a port
Section titled “Option 3: a port”A port reimplements the engine in another language. There is no certification: a port is
conformant for a spec version when it passes that version’s shared test suite. The cases are plain
YAML (tests/), so any language can run them:
| Area | Covers | Cases (spec 0.1) |
|---|---|---|
formulas/ |
Parsing, printing, evaluation, dice | 13 topic files |
loads/ |
loadRuleset |
10 |
sheets/ |
computeSheet |
66 |
options/ |
listOptions |
7 |
changes/ |
applyChanges |
38 |
actions/ |
startAction, continueAction |
122 |
upgrades/ |
upgrade, checkRelease |
37 |
A port doesn’t have to start with everything:
- Formula parsing is optional. Pack releases include each formula’s parsed tree (the
formulastable), so a port can walk trees and skip the parser. - A viewer needs less than a builder. Formulas,
loadRulesetandcomputeSheetare enough to show a sheet; addapplyChangesandlistOptionsto edit, actions to roll,upgradelast.checkReleaseis build tooling for pack authors, not something apps call. - Claim conformance only for the parts whose cases you pass.
The spec chapters for implementers are engine core, actions at run time and formulas. When the reference engine and the spec disagree, the spec wins.
Rulesets and paused actions aren’t portable between engines: their inner shape is PROVISIONAL and engine-specific, so a port builds its own ruleset from the same pack releases. Character files, changes and pack releases are standard, so a character moves freely between apps on different engines.
Not yet: WebAssembly
Section titled “Not yet: WebAssembly”A single engine compiled to WebAssembly would run in most languages with identical results. It’s an open idea, not a plan.