Schema Reference
Every .randsum.json spec conforms to the JSON Schema at https://randsum.dev/schemas/v1/randsum.json (referenced via $schema). This page documents every field.
Top-level fields
Section titled “Top-level fields”{"$schema": "https://randsum.dev/schemas/v1/randsum.json","name": "Blades in the Dark","shortcode": "blades","version": "1.0.0","game_url": "https://bladesinthedark.com","srd_url": "https://bladesinthedark.com/basics"}| Field | Type | Required | Description |
|---|---|---|---|
$schema | string | Yes | JSON Schema URL for validation |
name | string | Yes | Human-readable game name |
shortcode | string | Yes | Package identifier — used for subpath exports (@randsum/games/{shortcode}) and generated filenames |
version | string | Yes | Spec version (semver) |
game_url | string | No | Official game website |
srd_url | string | No | System reference document URL |
Named dice pool definitions. Each pool describes a type of die used by the game.
"pools": {"actionDice": { "sides": 6 },"hope": { "sides": 12 },"fear": { "sides": 12 }}| Field | Type | Required | Description |
|---|---|---|---|
sides | number | Yes | Number of sides on the die |
Pools are referenced elsewhere in the spec via { "$ref": "#/pools/{poolName}" }.
tables
Section titled “tables”Lookup tables that map numeric results to string outcomes. Each table has an array of ranges.
"tables": {"coreMechanic": { "ranges": [ { "poolCondition": { "countWhere": { "operator": "=", "value": 6 }, "atLeast": 2 }, "result": "critical" }, { "exact": 6, "result": "success" }, { "min": 4, "max": 5, "result": "partial" }, { "min": 1, "max": 3, "result": "failure" } ]}}Range entries
Section titled “Range entries”Each entry in the ranges array matches a roll total (or pool condition) to a result string. Ranges are evaluated in order — the first match wins.
| Field | Type | Description |
|---|---|---|
result | string | The outcome string returned when this range matches |
exact | number | Matches when the total equals this value |
min | number | Matches when the total is at least this value (used with max) |
max | number | Matches when the total is at most this value (used with min) |
poolCondition | object | Matches based on individual die values instead of the total |
poolCondition
Section titled “poolCondition”Inspects the raw die values before modifiers, not the total. Used for mechanics like Blades criticals (two or more 6s).
"poolCondition": {"countWhere": { "operator": "=", "value": 6 },"atLeast": 2}| Field | Type | Description |
|---|---|---|
countWhere | object | Condition to test each die: { "operator": "=" | ">" | "<" | ">=" | "<=", "value": number } |
atLeast | number | Minimum number of dice that must satisfy countWhere |
outcomes
Section titled “outcomes”Named outcome definitions that can be referenced by $ref. An outcome either delegates to a table or defines its own inline ranges.
"outcomes": {"coreMechanicOutcome": { "tableLookup": { "$ref": "#/tables/coreMechanic" } },"desperateActionOutcome": { "ranges": [ { "exact": 6, "result": "success" }, { "min": 4, "max": 5, "result": "partial" }, { "min": 1, "max": 3, "result": "failure" } ]}}| Field | Type | Description |
|---|---|---|
tableLookup | { "$ref": string } | Reference to a named table |
ranges | array | Inline range entries (same format as tables ranges) |
The core of the spec. Defines inputs, dice configuration, modifiers, resolution, outcome determination, and conditional overrides.
roll.inputs
Section titled “roll.inputs”Defines what the caller passes to the game’s roll() function. Each key becomes a property on the input object (or a positional argument for single-input games).
"inputs": {"rating": { "type": "integer", "minimum": 0, "maximum": 4, "default": 1, "description": "Action rating (0-4)"},"rollingWith": { "type": "string", "enum": ["Advantage", "Disadvantage"], "optional": true},"crit": { "type": "boolean", "optional": true, "description": "When true, check for natural 1 and natural 20"}}| Field | Type | Description |
|---|---|---|
type | "integer" | "string" | "boolean" | Input value type |
minimum | number | Minimum value (integer inputs only) |
maximum | number | Maximum value (integer inputs only) |
default | any | Default value when the input is omitted |
optional | boolean | Whether the input can be omitted (no default needed) |
enum | string[] | Allowed values (string inputs only) |
description | string | Human-readable description |
Codegen produces validateRange() and validateFinite() calls for integer inputs with minimum/maximum, and enum validation for string inputs with enum.
roll.dice
Section titled “roll.dice”Configures the dice to roll. For single-pool games.
"dice": {"pool": { "$ref": "#/pools/actionDice" },"quantity": { "$input": "rating" }}| Field | Type | Description |
|---|---|---|
pool | object | Either { "$ref": "#/pools/{name}" } or inline { "sides": number } |
quantity | number | { "$input": string } | Number of dice, or dynamic from an input value |
roll.dicePools
Section titled “roll.dicePools”For multi-pool games (e.g., Daggerheart’s hope/fear dice). Each key names a pool that can be referenced in resolve and details.
"dicePools": {"hope": { "pool": { "sides": { "$input": "amplifyHope", "ifTrue": 20, "ifFalse": 12 } } },"fear": { "pool": { "sides": { "$input": "amplifyFear", "ifTrue": 20, "ifFalse": 12 } } }}Dynamic sides use { "$input": string, "ifTrue": number, "ifFalse": number } for boolean-conditional values.
roll.conditionalPools
Section titled “roll.conditionalPools”Pools that are only rolled when a condition is met. Used for Daggerheart’s advantage/disadvantage extra die.
"conditionalPools": {"advantage": { "condition": { "input": "rollingWith", "operator": "=", "value": "Advantage" }, "pool": { "sides": 6 }, "arithmetic": "add"}}| Field | Type | Description |
|---|---|---|
condition | object | When to include this pool (same format as when conditions) |
pool | object | Die definition ({ "sides": number }) |
arithmetic | "add" | "subtract" | How to combine this pool’s result with the total |
roll.modify
Section titled “roll.modify”Array of modifier operations applied to the dice. Modifiers run in the order specified.
"modify": [{ "keepHighest": 1 },{ "add": { "$input": "modifier" } }]Available modifiers:
| Modifier | Value | Description |
|---|---|---|
keepHighest | number | Keep the N highest dice |
keepLowest | number | Keep the N lowest dice |
add | number | { "$input": string } | Add to the total |
The { "$input": string } form reads the value from a named input at runtime.
roll.resolve
Section titled “roll.resolve”How the roll total is determined.
| Value | Description |
|---|---|
"sum" | Sum all dice values after modifiers (most games) |
{ "comparePoolHighest": ... } | Compare highest values across named pools (Daggerheart) |
{ "remoteTableLookup": ... } | Look up results from an external table (Salvage Union) |
comparePoolHighest
Section titled “comparePoolHighest”"resolve": {"comparePoolHighest": { "pools": ["hope", "fear"], "ties": "critical hope", "outcomes": { "hope": "hope", "fear": "fear" }}}| Field | Type | Description |
|---|---|---|
pools | string[] | Named pools to compare |
ties | string | Result string when pools tie |
outcomes | object | Maps winning pool name to result string |
remoteTableLookup
Section titled “remoteTableLookup”Tables are fetched at build time and baked into the generated code. No network calls at runtime.
"resolve": {"remoteTableLookup": { "url": "https://salvageunion.io/schema/roll-tables.json", "find": { "field": "name", "input": "tableName", "errorMessage": "Invalid table name: \"${value}\"" }, "tableField": "table", "resultMapping": { "key": { "$lookupResult": "key" }, "label": { "$lookupResult": "result.label", "fallback": { "$lookupResult": "key" } }, "description": { "$lookupResult": "result.value" }, "tableName": { "$input": "tableName" }, "table": { "$foundTable": "table" }, "roll": { "expr": "total" } }}}roll.postResolveModifiers
Section titled “roll.postResolveModifiers”Modifiers applied after resolution. Used when the resolve step produces a non-sum result (e.g., Daggerheart applies the stat modifier after comparing hope/fear pools).
"postResolveModifiers": [{ "add": { "$input": "modifier" } }]roll.outcome
Section titled “roll.outcome”Determines the string outcome from the numeric total. Either a $ref to a named outcome or inline ranges.
// Reference a named outcome"outcome": { "$ref": "#/outcomes/coreMechanicOutcome" }
// Inline ranges"outcome": {"ranges": [ { "min": 10, "max": 27, "result": "strong_hit" }, { "min": 7, "max": 9, "result": "weak_hit" }, { "min": -11, "max": 6, "result": "miss" }]}Games without an outcome field return a numeric result (like D&D 5e).
roll.details
Section titled “roll.details”Additional structured data attached to the GameRollResult.details field. Each key becomes a property on the details object.
"details": {"criticals": { "when": { "input": "crit" }, "value": { "isNatural1": { "$dieCheck": { "pool": 0, "field": "final", "die": 0, "operator": "=", "value": 1 } }, "isNatural20": { "$dieCheck": { "pool": 0, "field": "final", "die": 0, "operator": "=", "value": 20 } } }}}Detail values can be:
| Form | Description |
|---|---|
{ "$input": string } | Value from a named input |
{ "$input": string, "default": any } | Input value with a default |
{ "$pool": string, "field": "total" } | Total from a named pool |
{ "$conditionalPool": string, "field": "total" } | Total from a conditional pool |
{ "$dieCheck": object } | Check a specific die value |
{ "expr": string } | Expression reference (e.g., "diceTotal", "total") |
{ "when": object, "value": object } | Conditional detail — only present when input is truthy |
$dieCheck
Section titled “$dieCheck”Inspects a specific die in the roll results.
| Field | Type | Description |
|---|---|---|
pool | number | Pool index (0-based) |
field | "final" | "initial" | Check the post-modifier or pre-modifier value |
die | number | Die index within the pool (0-based) |
operator | "=" | ">" | "<" | ">=" | "<=" | Comparison operator |
value | number | Value to compare against |
roll.when
Section titled “roll.when”Conditional overrides that change the roll behavior based on input values. Each entry has a condition and an override that replaces specific fields.
"when": [{ "condition": { "input": "rating", "operator": "=", "value": 0 }, "override": { "dice": { "pool": { "$ref": "#/pools/actionDice" }, "quantity": 2 }, "modify": [{ "keepLowest": 1 }], "outcome": { "$ref": "#/outcomes/desperateActionOutcome" } }}]Condition
Section titled “Condition”| Field | Type | Description |
|---|---|---|
input | string | Input field to check |
operator | "=" | "!=" | ">" | "<" | ">=" | "<=" | Comparison operator |
value | any | Value to compare against |
Override
Section titled “Override”An override can replace any of dice, dicePools, modify, resolve, or outcome. Only the fields specified are overridden — unmentioned fields keep their default values.
When multiple when entries match, they are evaluated in order and the first match is used.
$ref resolution
Section titled “$ref resolution”Specs use JSON Pointer-style references ($ref) to avoid duplication. References always point within the same spec file using the #/ prefix.
{ "$ref": "#/pools/actionDice" } // resolves to pools.actionDice{ "$ref": "#/tables/coreMechanic" } // resolves to tables.coreMechanic{ "$ref": "#/outcomes/coreMechanicOutcome" } // resolves to outcomes.coreMechanicOutcomeComplete example
Section titled “Complete example”For a complete, working spec, see the Salvage Union source — the richest spec in the ecosystem with multiple roll tables, range-based outcomes, and dynamic table selection — or any other game spec in the packages/games/ directory.
Related
Section titled “Related”- Schema Overview — philosophy and codegen pipeline
- Using loadSpec() — load specs at runtime
- Contributing a Game — write and submit a new spec