Every playable family is reached through one SDK, @moddable/engine/play. The tools server and other sites use it to list games, play them headless, ask the AI for a move and draw a position. The table below is measured by running the SDK against each family whenever the site is built.
The package is @moddable/engine, pure ES modules with no build step. Link a checkout of the repository, as the tools server does:
{
"dependencies": { "@moddable/engine": "file:../moddable-engine" }
}
import { getFamilies, listVariants } from '@moddable/engine/play'
getFamilies() // every family the engine plays
listVariants('tafl') // its variants, with their labels and groups
import { createGameForFamily } from '@moddable/engine/play'
const game = createGameForFamily('senet', { variant: 'standard', rngSeed: 7 })
game.getLegalMoves() // moves are plain objects; pass one back to apply it
game.applyMove(game.getLegalMoves()[0])
game.checkWin() // a seat index, 'draw', or null
game.getState() // serialisable; game.loadState(state) restores it
A game with dice, sticks or shells throws from its own seeded generator, so the same rngSeed replays the same game. A game with hidden hands answers game.viewForSeat(seat) with only what that seat may see.
import { createAI } from '@moddable/engine/play'
const ai = createAI('pachisi', 'standard', { difficulty: 'medium' })
const state = game.getState()
const move = ai.pickMove(state.slice, state.players.currentIndex)
createAI chooses the right search for the family: minimax, Monte Carlo tree search, or the family plugin's own policy for games of chance and hidden hands. Call it rather than a search directly: a minimax search cannot play a game whose next position depends on a throw.
import { renderStateAsSvg } from '@moddable/engine/play'
const gallery = await fetch('https://engine.moddable.games/pieces/gallery-index.json').then(r => r.json())
const svg = renderStateAsSvg('tafl', game.getState(), {
variant: 'brandubh',
gallery, // piece and card artwork
assetBase: 'https://engine.moddable.games/', // where artwork paths resolve
highlights: ['d4', { key: 'd5', color: '#f5c542' }],
})
A position is drawn the way the play page draws it: the variant's own board, surface and piece set. A board is written back to the variant's setup notation, and a table of cards, tiles or dice is drawn as it looks to one seat (seat, default 0), with that seat's hand face up and the others face down. Highlights name cells by the ids the board draws them with (e4, point-6, n1); a square grid also takes an index. A variant that names a piece set needs gallery, and the call throws without it rather than drawing a board with its pieces missing.
A variant whose board is a data file names it under content.source, and the SDK leaves reading it to you so it runs in a browser as well as Node. The Landlord's Game is one: fetch https://engine.moddable.games/data/landlords-game-boards.json and pass it as content. Without it the call throws and names the file, where it used to draw an empty board.
import { cardArtwork } from '@moddable/engine/play'
import { createDeck } from '@moddable/engine/component-deck'
const art = cardArtwork('standard-52', { variant: 'hearts', gallery, assetBase: 'https://engine.moddable.games/' })
art.cardUrl(createDeck('standard-52')[0]) // .../pieces/sets/letele-cards/S-A.svg
art.backUrl() // .../pieces/sets/letele-cards/B-1.svg
Artwork belongs to the family, not the deck: each family's frontmatter names the set its cards, tiles or dominoes are drawn from, and cardArtwork resolves through it as the play page does.