Appearance
Sections & layout
This is the densest part of the file: three different structures work together under the sections key. This chapter takes them one at a time.
Why arrays?
The confusion almost always comes from the same place: seeing [ ] where { } was expected. The rule:
Rule of thumb
Whenever the game can have more than one of something (several sections shown side by side, several tokens, several formats...), it's an array [ ] of objects, one per instance. Whenever there's only one of something at that level (the mulligan settings, the general info), it's an object { }.
Three arrays of objects intersect under sections, and it's their nesting — not any one of them alone — that makes the whole thing hard to read at a glance:
- The
layouttree — a recursive array of "containers" (rows/columns) that can themselves contain other containers. - The
sectionsDictdictionary — not an array, an object mapping each section name used in the layout to its settings (size, alignment, visibility...). - Custom sections — a special section type whose visual content is itself described by a tree of objects (a small UI language).
The layout tree
sections.layout describes the visual arrangement of the board: which sections sit side by side, which are stacked. direction controls this literally — "column" stacks its children top to bottom, "row" places them side by side, left to right. A node can nest, so a column can contain a row as one of its children:
That diagram is the visual result of this exact JSON — a column with two children: a nested row, then a section:
json
{
"direction": "column",
"content": [
{
"direction": "row",
"content": [
{ "section": "Mana" },
{ "section": "Items" }
]
},
{ "section": "Board" }
]
}The outer column stacks its two children top to bottom — first the row block, then Board. Inside that row block, direction: "row" places Mana and Items side by side. That's the whole mechanism: nest rows and columns to build any arrangement, exactly like nested <div>s on a web page.
Every node in the tree has the same shape:
One node of the recursive layout tree — either a container (row/column) or a leaf pointing at a real section.
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
direction | S | optional | How this container's children are placed. Absent on a leaf. possible values: rowcolumn | "row" |
content | [] | optional | Children of this container. Each item is either another container or a leaf { "section": "..." }. | [{ "section": "Hand" }] |
section | S | optional | Leaf only: the real section name, must exist as a key in sectionsDict. | "Hand" |
style | {} | optional | Raw CSS styles applied to this node. | { "width": "20%" } |
optional | {} | optional | Makes this node an opt-in toggle: players can turn it on or off before the match starts. key is the label shown for that toggle. | { "key": "Sideboard zone" } |
isSymetricalForOpponents | B | optional | Mirrors this node's position for the opponent's side of the board. | true |
reverseForOppositeSide | B | optional | Used only inside sections.sharedZone.layout (2v2), in place of isSymetricalForOpponents: reverses this node vertically for players 3 & 4. | true |
json
{
"direction": "row",
"content": [
{ "section": "Commander", "style": { "width": "15vh" } },
{ "section": "Creatures" }
]
}Building your own
Start with a single leaf ({ "content": [{ "section": "Board" }] }), confirm it renders, then add one section at a time. Building the tree incrementally beats trying to write it all in one pass.
Shared zone
Some games have a zone that only exists once, shared by every player, instead of being duplicated on each side of the board — e.g. a battlefield row in a two-headed-giant format. That's sections.sharedZone, a sibling of layout:
An optional zone shown once in the middle of the board, shared by all players (e.g. a battlefield row) instead of being duplicated per player like the rest of layout. Any section placed here lets every player put cards in it — players can still only interact with their own cards. Sections used here still need an entry in the same sectionsDict as regular sections.
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
height | N | required | Height of the shared zone, in vh. | 14 |
layout | {} | required | A layout tree, same node shape as sections.layout. For 2v2 boards, use reverseForOppositeSide on a node instead of isSymetricalForOpponents. | {
"direction": "column",
"content": [ ... ]
} |
json
"sharedZone": {
"height": 14,
"layout": {
"direction": "column",
"content": [{ "section": "Attackers" }]
}
}sharedZone.layout uses the exact same node shape as layout above — it's a separate tree, not a special node type.
sectionsDict
Once a section is placed in the layout tree, you still need to define how it behaves — is it hidden from the opponent, how big, how aligned? That's sectionsDict — not an array, an object keyed by the exact name used in the layout.
The behaviour of one named section referenced by the layout tree.
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
isHidden | S | required | Who can see the cards in this section. possible values: noyesopponent-only | "no" |
height | S | required | Size of the section — one of the presets below, or a plain number to set an exact height in vh. possible values: SMALLMEDIUMDEFAULT | "MEDIUM" |
alignment | S | required | How cards align within the section. possible values: STARTCENTERENDNONEDECK | "CENTER" |
opponentAlignment | S | optional | Overrides alignment specifically for how this section appears on the opponent's side. | "END" |
heightReference | S | optional | Set to "horizontal" so this section's height is measured against a horizontal (rotated) card instead of an upright one — useful for a row meant only for tapped/rotated cards. possible values: horizontal | "horizontal" |
isHorizontalAllowed | B | optional | Can a card be rotated horizontally in this section (e.g. to show it's tapped)? | true |
displayedTitle | S | optional | Label shown above the section, if different from the technical name. | "Extra Deck" |
noQuickActions | B | optional | Disables the right-click quick action menu on this section. | true |
noAutoPayTo | B | optional | Excludes this section from being auto-selected when the engine auto-pays a card's cost. | true |
enterTapped | B | optional | Cards entering this section start tapped/rotated. | true |
isGroupForbidden | B | optional | Disallows grouping multiple cards together into a single stack in this section. | true |
keepTappedNewTurn | B | optional | Cards stay tapped across turn changes instead of auto-untapping. | true |
showHiddenCardInHistory | B | optional | Once a hidden card is revealed, shows its identity in the game history/log. | true |
logWhenPlayed | B | optional | Posts a line in the chat log whenever a card enters this section. | true |
isDefaultSection | B | optional | Used only when overriding one of the engine's built-in sections (its default display, e.g. the stack) rather than defining a new custom one. | true |
cardActionShortcut | {} | optional | A one-click shortcut icon shown on cards in this section, e.g. to move them straight to another section. | {
"action": "MOVE",
"actionData": {
"destination": "Discard"
}
} |
drawDestinations | {} | optional | For a deck-like section (e.g. an extra deck): where a card goes when drawn from it, either by click or at new turn, keyed by card type. Use "_default" for any type not explicitly listed. | {
"_default": "Stack",
"Creature": "Board"
} |
cardBackColor | S | optional | Overrides the game-wide cards.cardBackColor for this section specifically — useful for an extra deck whose cards use a different-colored back. | "#c8bdb3" |
json
"sectionsDict": {
"Board": {
"isHidden": "no",
"height": "MEDIUM",
"alignment": "CENTER"
}
}Common mistake
A section name used in layout (e.g. "section": "Creatures") with no matching entry in sectionsDict won't render correctly. The two structures must stay in sync: layout says where, the dictionary says how.
No spaces in section names
Every key in sectionsDict (and every matching "section": "..." in layout) must be a single word with no spaces — use Extra_Deck or ExtraDeck, never Extra Deck. Use displayedTitle if you want a label with spaces shown to the player.
Extra decks
An "extra deck" (a second, separate deck players draw or play from — e.g. MTG's sideboard-like zones, Yu-Gi-Oh's Extra Deck) is just a sectionsDict entry with alignment: "DECK". Most of the optional fields aren't needed for a basic one:
json
"sectionsDict": {
"Extra-deck": {
"isHidden": "yes",
"height": "MEDIUM",
"alignment": "DECK",
"displayedTitle": "Extra deck"
}
}Clicking a card in this section plays it using the section's autoPlayFromHand value (below). Use drawDestinations on the same entry if drawn cards should scatter to different sections depending on card type instead of a single default.
Auto-play destinations
autoPlayFromHand and autoPlayFromStack are dictionaries, not arrays — they map a card type to the section it lands in, so each type only needs one destination:
Dictionaries mapping a card type to the section it should land in when the player plays it — from hand, or from the resolving stack. Not arrays: one entry per card type, since each type has exactly one default destination.
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
autoPlayFromHand | {} | optional | Where a card goes when played directly from hand, keyed by card type. Use "GROUP" as a destination to auto-attach it to another card instead of a section. | {
"Creature": "Board",
"Item": "GROUP"
} |
autoPlayFromStack | {} | optional | Where a card goes once it finishes resolving from the stack, keyed by card type — typically "Discard" for spells, or a permanent's board section. | { "Spell": "Discard" } |
json
"autoPlayFromHand": {
"Creature": "Board",
"Land": "Mana",
"Sorcery": "Stack"
},
"autoPlayFromStack": {
"Sorcery": "Discard"
}Custom sections
A custom section isn't a separate structure — it's a normal sectionsDict entry, just with "type": "custom" instead of the usual isHidden/height/alignment fields. Use it for a section that renders a small UI instead of cards — e.g. a life/mana counter panel — described as a tree of elements (much like HTML) via blueprint.
Not a separate structure — this is a normal sectionsDict entry, just with type: "custom" instead of the usual isHidden/height/alignment fields, which renders a small UI (e.g. a counter panel) instead of cards.
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
type | S | required | Must be "custom" to enable this mode. possible values: custom | "custom" |
defaultValue | {} | optional | Initial state stored for this section, later accessible via game.data.<SectionName>. | {
"white": 0,
"blue": 0
} |
blueprint | {} | required | The UI element tree (type, props, children) that renders this section. onChange expressions have access to value (the new value), delta (the change amount), and a chatLog(message) helper to post to the game log. | { "type": "div", "children": [...] } |
isShared | B | optional | If true, this custom section's state is shared/visible across all players rather than being per-player. | true |
events | {} | optional | Script expressions run on specific triggers. possible values: onStartonNewTurnonOpponentUpdate | { "onNewTurn": "game.data.Boost.value = 0" } |
json
"sectionsDict": {
"Counters": {
"type": "custom",
"defaultValue": { "white": 0, "blue": 0 },
"blueprint": {
"type": "div",
"children": [
{
"type": "input-number",
"props": { "value": "{{ game.data.Counters.white }}" },
"onChange": "game.data.Counters.white = value"
}
]
}
}
}A custom section can also react to game events instead of only user input — see the events row in the table above (onStart, onNewTurn, onOpponentUpdate).
Placement matters
Custom sections are often positioned with absolute CSS (position: absolute) so they can float over the board. Place them last in your layout tree — otherwise a section defined after them can end up rendered on top and block their inputs from being clicked.
This mode is more technical than the rest of the file. For a first game, it's fine to define only plain sections and add a custom one later if you need a specific widget.
Naming conventions
A handful of section names carry built-in meaning, or are reserved outright — no extra config needed beyond naming the section this way (or avoiding the name) in sectionsDict:
Special name: "Mana"
Naming a section Mana makes the engine display a simple card-count indicator for that zone instead of rendering full cards — useful for a resource pool where only the count matters, not which cards are in it.
Reserved names
Stack, Hand, Deck, Discard, Sideboard, Remove, and RemoveHidden are used internally by the engine. Don't use them as the name of a new custom section — only reference them in sectionsDict if you're deliberately overriding one of the engine's own built-in sections.
Cheat sheet
| Structure | Role |
|---|---|
layout | Visual arrangement — who sits next to who. A tree of rows/columns, duplicated per player. |
sharedZone | Same tree shape as layout, but rendered once and shared by all players. |
sectionsDict | Behaviour of each named section — size, visibility, alignment. |
blueprint | Custom sections only — a small UI described as a tree of elements. |
Not sure if something should be an array?
Ask: "can this spot in the game legitimately hold more than one thing side by side?" If yes → array [ ]. If it's a fixed set of settings for one single thing → object { }.