Dicey Table

The Manifest

Game Variants

Offer one game pack in several set-ups, from board sizes to a fully scripted ranked version. The manifest list, the setup.json block and what is refused.

On this page12 sections

A variant is the same game, set up differently. Go on a 9×9, a 13×13 or a 19×19 board. A six-seat and a ten-seat table with the same cards. A scripted game, and the same entities with no scripts attached so people can play it by hand. A fully scripted version whose result can be ranked. One game pack carries all of them, and the room's owner picks one in the lobby before the table exists.

You do not need a script to offer variants, and you do not write any of the JSON on this page by hand: the editor's variant controls produce both halves. This page is the reference for what they produce — read it when you are reading a diff, debugging a refused publish, or deciding whether a variant is the right tool.

If you want the walkthrough instead, it is Making a variant of your game.

Variants or setup options?#

Both are chosen in the lobby by the room's owner, and they answer different questions.

Variant Setup option
What it is A different scene — entities, table, seats, zones, snap points, scene scripts A typed answer — a number, a toggle, a choice, a short text
Who acts on it The platform, before any script runs Your script, in setup(api, manifest, options)
Needs entry.script No Yes — a scriptless game is refused for declaring one
Can change the table, the board or the seats Yes Only as far as your script can spawn and move things
Can change the player range Yes, per variant No
Can run different scripts Yes — script attachments are part of each variant's scene No
How many Up to 12, one chosen Up to 12 options, each answered
Visible to your script Yes — see What a script can see Yes

Rule of thumb: if the table itself is different, it is a variant; if the same table is played with a different number, it is a setup option. "Hand size: 5 or 7" is an option. "The four-player board or the six-player board" is a variant. A game may declare both, and the lobby shows the variant first, because the variant decides what the game is and the options are answers handed to whatever script that variant runs.

The two halves#

A game's variants are recorded in two documents, because two different readers need them.

  • The manifest (diceytable.mod.json) holds the list: each variant's id, name, description, player range, whether it is fully scripted and how it is ranked. The registry stores the manifest, so this is the half a lobby, the server and a listing page can read without fetching your scene.
  • The scene (setup.json) holds what each variant changes. The lobby never reads it; the host does, once, when the table starts.

The two are joined by the variant's id, and they must agree — see What publishing refuses.

The manifest list#

JSON
{
  "schemaVersion": "1.0",
  "id": "example.go",
  "name": "Go",
  "version": "1.0.0",
  "type": "game-pack",
  "entry": { "setup": "setup.json" },
  "compatibility": { "engine": ">=0.1 <1.0" },
  "license": "MIT",
  "players": { "min": 2, "max": 2 },
  "variants": [
    { "id": "19x19", "name": "19×19", "description": "The full board." },
    { "id": "9x9", "name": "9×9", "description": "A quick game on the small board." }
  ]
}

This game has no entry.script and declares variants anyway. That is legal — it is the main difference from setup options.

Field Required What it is
id yes The stable identifier: what a room stores as the owner's choice, and the key the scene files this variant's differences under. Lowercase letters, digits and single dashes, 1–40 characters; constructor and prototype are refused. Unique within the list.
name yes What the lobby's dropdown shows. Plain text, 1–60 characters.
description no One line shown under the dropdown and in the lobby header. Plain text, up to 240 characters.
players no { "min", "max" } — this variant's player range when it differs from the game's. See A different player count.
rulesEnforced no true makes the variant Fully scripted: its script enforces the rules and reports the result. See Fully scripted and ranked.
ranking no Ranked-play settings, every one with a default. Only on a fully scripted variant. See Fully scripted and ranked.

An entry is strict: an unknown key is a parse error, not a silently ignored one. The per-field detail is in the manifest reference.

Three rules about the list as a whole:

  • The first entry is the lobby's default. A room whose owner never touches the dropdown plays it. So does a room that stored an id your game has since removed — an old room record opens on the first variant rather than failing.
  • At most 12. This is one dropdown in a lobby; a dropdown with thirty entries is a catalogue.
  • Game packs only. A room, table or asset pack is never the mod a lobby resolves as the room's game, so a list on one is offered to nobody and publishing it is refused.

A list with fewer than two entries is not a choice. The editor removes the key when you delete your way down to one variant, and an absent variants means what it always has: one way to play.

The setup.json block#

The scene stores variants as a base plus differences:

  • The top level of setup.json is the base variant. It is a complete, ordinary scene. variants.base says which declared id that scene is.
  • Every other variant is stored as what it changes about the base, under variants.deltas[<id>]. The base never has an entry there.

Two non-base variants never share with each other. What they have in common is the base. That is the whole model: "the base, and how this one differs from it".

Here is the Go game above, with 19×19 as the base and a 9×9 variant that removes the big board, adds a small one, and has its own snap grid:

JSON
{
  "schemaVersion": 2,
  "sceneId": "go",
  "name": "Go",
  "environment": {
    "tableSurfaceColor": "#2e5d4b", "tableSurfaceTextureAssetId": null, "ambientColor": "#ffffff",
    "fogEnabled": false, "fogColor": "#000000", "fogDensity": 0, "skyboxAssetId": null
  },
  "objects": [
    {
      "id": "board-19", "kind": "board", "label": "board-19", "displayName": "Board 19×19",
      "position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "x": 0, "y": 0, "z": 0 },
      "scale": { "x": 1.5, "y": 1, "z": 1.5 }, "color": "#d9b36c", "ownerSeat": null,
      "faceDown": false, "locked": true, "stackCount": 1, "metadata": {},
      "velocity": { "x": 0, "y": 0, "z": 0 }, "angularVelocity": { "x": 0, "y": 0, "z": 0 }
    }
  ],
  "snapPointGrids": [
    {
      "id": "grid-19", "label": "Board 19×19", "center": { "x": 0, "y": 0.05, "z": 0 }, "rows": 19, "cols": 19,
      "spacingX": 0.075, "spacingZ": 0.075, "rotationY": 0, "snapRadius": 0.03,
      "ownerSeat": null, "metadata": {}
    }
  ],
  "variants": {
    "base": "19x19",
    "deltas": {
      "9x9": {
        "removed": ["board-19"],
        "objects": [
          {
            "id": "board-9", "kind": "board", "label": "board-9", "displayName": "Board 9×9",
            "position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "x": 0, "y": 0, "z": 0 },
            "scale": { "x": 0.75, "y": 1, "z": 0.75 }, "color": "#d9b36c", "ownerSeat": null,
            "faceDown": false, "locked": true, "stackCount": 1, "metadata": {},
            "velocity": { "x": 0, "y": 0, "z": 0 }, "angularVelocity": { "x": 0, "y": 0, "z": 0 }
          }
        ],
        "scene": {
          "snapPointGrids": [
            {
              "id": "grid-9", "label": "Board 9×9", "center": { "x": 0, "y": 0.05, "z": 0 }, "rows": 9, "cols": 9,
              "spacingX": 0.075, "spacingZ": 0.075, "rotationY": 0, "snapRadius": 0.03,
              "ownerSeat": null, "metadata": {}
            }
          ]
        }
      }
    }
  }
}

Delete the variants key and what remains is a valid scene: the 19×19 game. Playing 9x9 yields one entity, board-9, and the 9×9 grid.

What a variant's differences can hold#

Every part is optional. An empty object — or no entry at all — means "this variant is the base, unchanged".

Key Holds Meaning
removed Entity ids Base entities this variant does not have.
objects Whole entities Entities this variant has its own version of. An id the base also holds is that entity customised here and replaces the base's in place; an id the base does not hold is an entity added here. Up to 2,000.
order Entity ids This variant's own entity order. Written only when the natural order (the base's, with additions at the end) would not reproduce what you arranged.
scene Scene keys This variant's own copies of the keys in the next table.

Entities are handled one at a time, which is what keeps a variant that changes one board from forking the whole entity list: the 200 stones the two variants share are stored once, in the base, and a fix to one of them reaches both.

What a variant can have its own copy of#

Everything else in the scene is grouped the way you think about it while authoring. The editor uses these four group names; the keys are what you will see in the file.

Group Keys in scene Covers
Table & room packRefs, environment, room The assigned table and room packs, the table surface, lighting and sky
Seats & zones seatZones, seatTemplate, tableZones, seatVariables, zones, roomSettings Seats, the seat template, hand and table zones, and the player-count rules
Snap points snapPoints, snapPointGrids Snap points and snap-point grids
Scene scripts sceneScriptIds, sceneScriptVars Which scripts are attached to the scene, and their variable values

Two things about these that are easy to get wrong:

  • A key is copied whole. Unlike entities, these are not merged item by item. Once a variant has its own snapPoints, it has its own list of snap points, and a snap point you later add to the base does not appear in it. The editor's Variants dialog shows which groups a variant holds its own copy of, and can drop the copy so the variant follows the base again.
  • null is a value. A key that is absent from scene is inherited from the base. A key set to null means "this variant does not have one" — it is how a variant says the base assigns a table pack and it assigns none. (environment is the exception: a scene always has one, so it can be replaced but not removed.)

What is always shared#

scripts, materials, textures and prefabs are libraries, and every variant reads the same ones. A variant chooses which scene scripts are attached (sceneScriptIds); it never has its own copy of a script's source. If it did, a fix made in one variant would silently not reach the others.

The Hierarchy's folders are shared too, and so is the document's identity (schemaVersion, sceneId, name).

The base is not the default#

These are two separate facts, and they live in two separate documents.

  • The default is the first entry in the manifest's list — what a lobby starts on.
  • The base is variants.base in the scene — the variant the top level of setup.json is, and the one every other variant is stored as a difference from.

They are usually the same variant, because the editor makes your existing scene both when you create your first variant. They do not have to be. Reordering the lobby's list changes the default and touches nothing in the scene. Make this the base in the Variants dialog changes the base and touches nothing a player sees: the file is rewritten so a different variant sits at the top level, and every variant still plays exactly what it played before.

Pick the base for the reason in Backwards compatibility: it is the scene a reader that has never heard of variants will load.

Playing by hand#

A by-hand variant is one whose scene attaches no scripts. There is no flag for it: which scene scripts a variant attaches (sceneScriptIds) is one of its scene keys, and an entity's script is part of the entity, so a variant that detaches them runs none of them. The editor does this per variant with Changes apply to: Only variant selected; see the same game, by hand.

Two scripts a variant cannot detach, so check them:

  • The entry script (entry.script) runs in every variant. If it should do nothing by hand, it reads api.getVariant() and returns.
  • A prefab's script. Prefabs are a shared library, so a piece spawned from a scripted prefab runs its script in every variant.

A variant whose scripts do not register a bot's handler cannot seat that bot: the lobby, the table's Bots panel and the API refuse it (bot-variant-no-handler), and switching a lobby to the variant removes the bots already seated. Anything your scripts would have dealt or spawned is not there either, so author it into that variant's scene.

scripts: false has been removed. The old flag switched every script off at once. A manifest that still carries scripts on a variant is refused with variant-scripts-removed. Opening such a draft in the editor converts it once: the variant's scene and entity scripts are detached in its own scene, the key is dropped, and the editor reminds you about the entry script.

Fully scripted and ranked#

JSON
{
  "id": "standard",
  "name": "Standard",
  "description": "Rules enforced. Can be played ranked.",
  "rulesEnforced": true,
  "ranking": { "placementGames": 5 }
}

rulesEnforced: true says the variant's script is the referee: it enforces the rules and reports the result with match.end. The lobby shows a Fully scripted badge beside it.

ranking offers ranked play. Every field has a default, so "ranking": {} is complete; the fields are in the manifest reference. The lobby shows Ranked available, and the room's owner may switch Ranked on. Only a fully scripted variant may carry it, and a script attached to the variant must call match.end(, or publishing refuses it. What ranked play guarantees, and how Scores and Player Trust work, is on Ranked games, Rankings and Player Trust.

The same script serves ranked and casual play: in a room that is not ranked, its matches simply play unranked.

A different player count#

JSON
{ "id": "ten-seat", "name": "Big table", "players": { "min": 2, "max": 10 } }

players on a variant replaces the game's own players range while that variant is chosen. A variant without it uses the game's.

The range is enforced where the room's owner sets the seat cap: the lobby accepts only a cap inside the chosen variant's range, and the server checks the same write against the same range. Changing to a variant that cannot seat the current cap clears the cap and says so, rather than clamping it. The platform seats at most ten players whatever a manifest declares.

Declaring the range does not build the seats. A ten-seat variant also needs its own Seats & zones — and usually its own table — in the scene, or ten people will be offered a table laid out for six.

What a script can see#

Both script surfaces can read which variant is being played, read-only and synchronously:

Both answer { id, rulesEnforced, ranked }: the declared id (or null for a game with no variants), whether the variant is fully scripted, and whether this table plays ranked. The variant is public, so reading it reveals nothing hidden. The manifest argument of setup(api, manifest, options) still carries no variant, and options carries setup-option answers only.

Branch on id when one script is attached in several variants. You can still give a script settings per variant without code: sceneScriptVars is a per-variant key, so one script attached in two variants can have different variable values.

In the lobby#

For a game with two or more variants, the room's owner gets a Variant dropdown under Game options, above the setup options. It lists the variants in manifest order and saves the moment it changes — there is no Save button. Under it: the variant's description, the range it seats, and the Fully scripted and Ranked available badges where they apply. When the chosen variant offers ranked play, the owner also gets the Ranked switch.

Everyone in the lobby, owner or not, sees the choice in the lobby header: Variant: 9×9, with the variant's own player range and badges, and its description below.

The choice belongs to the room, and three things about it are fixed:

  • Only the owner can change it. Anyone else's write is refused.
  • It locks at Start. The host resolves the scene for the variant the room started with, so the dropdown is disabled afterwards and the server refuses a later change.
  • It does not survive a change of game. A variant chosen for one game is dropped when the owner switches the lobby to another, even if the new game happens to declare the same id.

What publishing refuses#

Each variant's own content is validated when setup.json is parsed, exactly as the base scene is: a malformed entity inside a variant fails the scan the same way a malformed entity at the top level does. On top of that, four rules exist because the list and the differences live in two documents and can disagree. All four are errors, so the mod registers as incompatible. Ranked variants have four rules of their own.

Code What you did Why it is refused
variants-unread Declared variants on a room, table or asset pack Only a game pack is ever a room's game, so nobody would be offered the choice
variant-undeclared setup.json records differences for an id the manifest does not list Nobody could ever choose it
variant-base-undeclared variants.base names an id the manifest does not list The scene itself would belong to a variant nobody can choose
variant-scene-without-manifest setup.json records differences, and the manifest declares no variants at all Same as above, for the whole block

The reverse is not an error. A variant the manifest lists and the scene says nothing about is the base scene unchanged: a variant that differs only in its name, its player range or rulesEnforced is written that way.

The editor changes both halves together, so you meet these refusals only after editing one of the files by hand (or after a merge brought in one file without the other). The fix is always to make the ids match. The editor never drops a variant's differences on its own — a manifest that fails to read looks exactly like one that declares no variants, and that must not cost you a variant — so for differences the manifest no longer lists, open Mod ▸ Variants…: the dialog names them and offers Remove them. If you meant to keep the variant, add it back to the manifest's list with the same id instead.

The scene's own shape has two limits of its own, reported as a schema failure rather than under a code above: at most 12 variants may have differences recorded, and the base may not also appear under deltas.

Backwards compatibility#

The top level of setup.json is the base variant, and it is a whole game. A reader that has never heard of variants — an older copy of DiceyTable, a tool that counts a pack's entities — skips the variants key and loads a complete, playable scene. It does not see three boards stacked on one table, and it does not see an empty one. That is the reason the format is "a base plus differences" rather than "a list of scenes".

The same goes for the manifest: variants is optional, an older reader ignores it, and a manifest without it is unchanged.

What an older reader cannot do is offer the other variants. So make the base the variant you would want someone to get if they could only get one.

A game that declares no variants is untouched by any of this. Its setup.json has no variants key, and its manifest has no list.

See also#

Did this page answer your question?