Contributing a Game
This guide walks through adding a new game system to @randsum/games. Every game in the package follows the same path: write a .randsum.json spec, generate TypeScript from it, test it, and submit a PR.
Prerequisites
Section titled “Prerequisites”- Bun installed (v1.3.10+)
- The RANDSUM repo cloned and dependencies installed:
git clone https://github.com/RANDSUM/randsum.gitcd randsumbun installThe process
Section titled “The process”-
Write a
.randsum.jsonspecCreate a spec file in the games package root. The filename must be
<shortcode>.randsum.json, where<shortcode>is a short, lowercase identifier for the game (e.g.,fate,ironsworn).packages/games/<shortcode>.randsum.jsonHere’s a minimal skeleton to start from:
{"$schema": "https://randsum.dev/schemas/v1/randsum.json","name": "My Game","shortcode": "mygame","version": "1.0.0","game_url": "https://example.com/mygame","pools": {"main": { "sides": 6 }},"tables": {"outcome": {"ranges": [{ "min": 5, "max": 6, "result": "success" },{ "min": 3, "max": 4, "result": "partial" },{ "min": 1, "max": 2, "result": "failure" }]}},"outcomes": {"mainOutcome": {"tableLookup": { "$ref": "#/tables/outcome" }}},"roll": {"dice": {"pool": { "$ref": "#/pools/main" },"quantity": 1},"resolve": "sum","outcome": { "$ref": "#/outcomes/mainOutcome" }}}The
$schemafield enables validation in editors that support JSON Schema.For the full field reference, see the Schema Reference.
-
Validate the spec
Run the validation check to ensure your spec conforms to the JSON Schema:
bun run --filter @randsum/games gen:checkThis runs
codegen.ts --check, which parses all.randsum.jsonfiles in the package root and reports validation errors without writing any files.Fix any errors before proceeding. Common issues:
- Missing required fields (
name,shortcode,pools,roll) $refpointing to a pool, table, or outcome that doesn’t exist- Table ranges with gaps or overlaps
- Input constraints missing
typeordefault
- Missing required fields (
-
Run codegen
Generate the TypeScript module from your spec:
bun run --filter @randsum/games genThis creates
packages/games/src/<shortcode>.generated.ts. The generated file:- Imports
roll as executeRollfrom@randsum/roller - Exports a typed
roll()function specific to your game - Exports
GameRollResultandRollRecordtypes - Bakes outcome tables directly into the code (no runtime fetching)
- Imports
-
Add a subpath export
Add an entry for your game in
packages/games/package.jsonunder theexportsfield. Follow the pattern of existing games:"./<shortcode>": {"import": {"types": "./dist/<shortcode>.generated.d.ts","default": "./dist/<shortcode>.generated.js"},"require": {"types": "./dist/<shortcode>.generated.d.cts","default": "./dist/<shortcode>.generated.cjs"}}Also add a size-limit entry in the same file:
{"path": "dist/<shortcode>.generated.js","limit": "8 KB"} -
Write tests — Create two test files in
packages/games/__tests__/. See test templates below the steps. -
Build and test
# Build the games package (includes your new generated file)bun run --filter @randsum/games build# Run testsbun run --filter @randsum/games test# Run the full check suitebun run --filter @randsum/games check -
Submit a PR
Once all checks pass, commit and open a pull request. Include:
- The
.randsum.jsonspec - The generated
.tsfile - Both test files
- The
package.jsonchanges (export + size-limit)
In the PR description, link to the game’s SRD or rules reference so reviewers can verify the outcome tables.
- The
Test templates
Section titled “Test templates”<shortcode>.test.ts — behavioral tests:
import { describe, expect, test } from 'bun:test'import { roll } from '@randsum/games/<shortcode>'
describe('roll', () => { test('returns a valid outcome', () => { const { result } = roll() expect(['success', 'partial', 'failure']).toContain(result) })
test('total is within die bounds', () => { const { rolls } = roll() const total = rolls[0]?.total ?? 0 expect(total).toBeGreaterThanOrEqual(1) expect(total).toBeLessThanOrEqual(6) })})<shortcode>.property.test.ts — property-based tests with fast-check:
import { describe, expect, test } from 'bun:test'import fc from 'fast-check'import { roll } from '@randsum/games/<shortcode>'
describe('roll property-based tests', () => { test('result is always a valid outcome', () => { fc.assert( fc.property(fc.constant(undefined), () => { const { result } = roll() return ['success', 'partial', 'failure'].includes(result) }) ) })})Adapt these templates to your game’s inputs and outcomes. See existing test files (e.g., salvageunion.test.ts, fifth.test.ts) for patterns handling game-specific inputs like tableName or modifier.
- Start simple. Get a basic spec working before adding inputs, conditional pools, or complex outcome trees. You can iterate in follow-up PRs.
- Use
loadSpecfor rapid iteration. While developing your spec, useloadSpec()to test it without running codegen on every change. - Check existing specs. The six built-in game specs in
packages/games/cover a range of mechanics — table-based with dynamic lookup (salvageunion), advantage/disadvantage (fifth), pool-based (blades), dual-pool (daggerheart), and 2d6+stat (pbta). The Salvage Union spec is the richest example, with multiple roll tables and range-based outcomes. Find the one closest to your game and use it as a starting point. - Table ranges must cover all possible values. If your die produces 1-6, your outcome table must account for every value from 1 to 6. Gaps cause
SchemaErrorwith code'NO_TABLE_MATCH'at runtime.
Learn more
Section titled “Learn more”- Schema Overview — how
.randsum.jsonspecs work - Schema Reference — field-by-field spec documentation
- Using loadSpec() — runtime spec loading for development and custom games