Skip to content

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.

  • Bun installed (v1.3.10+)
  • The RANDSUM repo cloned and dependencies installed:
git clone https://github.com/RANDSUM/randsum.git
cd randsum
bun install
  1. Write a .randsum.json spec

    Create 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.json

    Here’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 $schema field enables validation in editors that support JSON Schema.

    For the full field reference, see the Schema Reference.

  2. Validate the spec

    Run the validation check to ensure your spec conforms to the JSON Schema:

    bun run --filter @randsum/games gen:check

    This runs codegen.ts --check, which parses all .randsum.json files 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)
    • $ref pointing to a pool, table, or outcome that doesn’t exist
    • Table ranges with gaps or overlaps
    • Input constraints missing type or default
  3. Run codegen

    Generate the TypeScript module from your spec:

    bun run --filter @randsum/games gen

    This creates packages/games/src/<shortcode>.generated.ts. The generated file:

    • Imports roll as executeRoll from @randsum/roller
    • Exports a typed roll() function specific to your game
    • Exports GameRollResult and RollRecord types
    • Bakes outcome tables directly into the code (no runtime fetching)
  4. Add a subpath export

    Add an entry for your game in packages/games/package.json under the exports field. 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"
    }
  5. Write tests — Create two test files in packages/games/__tests__/. See test templates below the steps.

  6. Build and test

    # Build the games package (includes your new generated file)
    bun run --filter @randsum/games build
    # Run tests
    bun run --filter @randsum/games test
    # Run the full check suite
    bun run --filter @randsum/games check
  7. Submit a PR

    Once all checks pass, commit and open a pull request. Include:

    • The .randsum.json spec
    • The generated .ts file
    • Both test files
    • The package.json changes (export + size-limit)

    In the PR description, link to the game’s SRD or rules reference so reviewers can verify the outcome tables.

<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 loadSpec for rapid iteration. While developing your spec, use loadSpec() 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 SchemaError with code 'NO_TABLE_MATCH' at runtime.