Creating games

Guide for adding a new game to gameslib. For API details see Game object and Helpers.

Workflow

  1. Fork gameslib and work on the develop branch.
  2. Before first npm install, run npm run npm-login for GitHub Packages access.
  3. Choose a base class (see below) and create src/games/<uid>.ts.
  4. Run npm run generate-registry (or any build/test) — the game registry is auto-generated from static gameinfo.
  5. Add i18n strings to locales/en/apgames.json (and apresults.json if needed).
  6. Flag new games with experimental in gameinfo.
  7. Test locally — Testing.
  8. PR against develop; test on play.dev.abstractplay.com after merge.

Choosing a base class

This is one of the first decisions when authoring a game. The base class controls how getPlies() / getRounds() group stack entries for published gamerecord export and how the finished-game move table lays out columns (via header["turn-model"] and engine.turnModel()).

It does not change how you implement move() day-to-day unless your rules genuinely need different turn structure. Pick the class that matches who acts on each saveState() and how rounds close.

Quick decision guide

flowchart TD
  start[New game] --> q1{All seats submit one move\nper stack entry at once?}
  q1 -->|Yes| sim[GameBaseSimultaneous\n+ simultaneous flag]
  q1 -->|No| q2{Can seats be inactive\nmid-game without moving?}
  q2 -->|Yes| skip[GameBaseSkipTurn\n+ isSeatActive]
  q2 -->|No| q3{Can one seat act twice\nbefore every seat has acted?}
  q3 -->|Yes| seq[GameBaseSequenced\nor GameBase + mixin hooks]
  q3 -->|No| base[GameBase\ndefault sequential]

Base classes at a glance

Base class turnModel() One stack entry = Round closes when Example games
GameBase "sequential" One ply by one seat Every numplayers plies (or every ply at 1p) Complica, Hex, Frogger without refills
GameBaseSimultaneous "simultaneous" One round (all seats) Each stack entry is one round Robo Battle Pigs, Entropy
GameBaseSkipTurn "skip-turn" One ply by one active seat Seat cycle wraps to round opener Armadas, Homeworlds
GameBaseSequenced "sequenced" One ply (same as sequential) Seat cycle wraps (not fixed ply count) Future: Gnostica; use when every game uses sequenced export

Solo games (numplayers === 1): always use GameBase. Skip-turn, simultaneous, and sequenced base classes do not apply; each ply closes its own round automatically.

What each turn model means for export

turnModel() Move table / record shape When you need it
sequential Fixed alternation: row i seat j ≈ ply i×N+j (legacy stride). Default for strict round-robin. Turns advance seat 1 → 2 → … → 1 with exactly one ply per seat per cycle.
simultaneous One row per stack entry; columns are comma-split moves per seat. null or \u0091 for eliminated seats. Everyone submits together each round (set simultaneous in gameinfo.flags).
skip-turn Full-width rows; null where a seat did not act (eliminated / inactive). Turn order skips seats that cannot act (elimination, no ships, no homeworld). Implement isSeatActive(seat, stackIndex).
sequenced Sparse rows: often one ply per row, move in the actor’s column; { move, sequence } when play order ≠ seating. A seat may act more than once before the cycle completes (refill follow-ups, branch depth).

Setting turnModel() alone is not enough for sequenced export — you also need shouldCloseRound, and usually sparse getRounds() (see Game object — mixin hooks). GameBaseSequenced bundles those defaults. For a worked example (including Frogger-style two actions in one round), see Sequenced turn model.

Worked examples

GameBase — strict round-robin (most games)

Complica: each move() advances currplayer; one stack entry per submit; P1 then P2 then P1…

export class ComplicaGame extends GameBase {
  // No turn-model overrides needed.
}

GameBaseSimultaneous — one stack entry per round

Pigs: lastmove is "moveP1,moveP2,moveP3,moveP4" (comma-separated). One saveState() per round.

GameBaseSkipTurn — inactive seats, null columns

Armadas 3p+: when a player loses all ships, they stop acting but other seats continue. Export must show null in that column, not a mis-assigned move.

export class ArmadasGame extends GameBaseSkipTurn {
  protected isSeatActive(seat: number, stackIndex: number): boolean {
    // true iff seat had at least one ship before stack[stackIndex]
  }
}

2p Armadas degenerates to no nulls (all seats always active) but still uses GameBaseSkipTurn.

Sequenced export — consecutive plies by one seat

Frogger with the refills variant is the reference case: one seat announces a refill (!), then submits again on the next ply before the seat cycle continues. Sequenced export puts both moves in that seat’s column — without fake pass plies for other seats.

Full walkthrough (recommended refillPending shape, hooks, tests): Sequenced turn model.

Summary:

GameBase + hooks — variant-gated or special-case

Use GameBase with protected overrides when turn structure depends on a variant or a one-off rule (Frogger refills pattern above). Do not create a new base class for every game.

Mixin hooks (all bases)

These protected methods on GameBase are the extension points when the stock base class is close but not exact:

Hook Default Override when
plyActor(stackIndex) stack[stackIndex - 1].currplayer Actor is not the pre-move currplayer (unusual; prefer fixing currplayer in move()).
shouldCloseRound(roundPlies, stackIndex) Close every numplayers plies Round ends on seat-cycle wrap, not fixed ply count; or while supplemental obligation still open (refillPending).
getRounds() Pack plies into numplayers-wide rows Consecutive plies by one seat would overwrite in buildRoundRow — emit one row per ply instead.
compactExportRounds() Trim trailing null seats Skip-turn / simultaneous / sequenced sparse rows must keep full width.
turnModel() "sequential" (or base class default) Consumer hint for replay and UI; must match export shape.

Full detail: Game object — mixin hooks.

Common mistakes

Implementation checklist

Start from /gameslib/templates/new-game-template.ts and Complica.

Choosing helpers

Most board games use either:

Use the examples by feature index to find games similar to yours.

Renderer

Implement render(opts?) returning APRenderRep for @abstractplay/renderer. Prototype JSON in the renderer playground.

Example games

Pattern Game
Default sequential Complica, Hex
Simultaneous rounds Robo Battle Pigs, Entropy
Skip-turn / elimination Armadas, Homeworlds
Sequenced / duplicate actor per round Sequenced turn model · Frogger (refills)
Hex graph Yavalath
Custom recordExportExclude Volcano, Tablero