Go

Nine Go variants with territory scoring, an MCTS opponent, embed support, and the same headless SDK every family uses.

Embed via iframe

Every game family shares one embed surface. Select the family and variant through the URL.

<iframe
  src="https://engine.moddable.games/play/?embed=1&family=go&variant=9x9"
  style="width:100%;max-width:560px;aspect-ratio:1/1;border:none"
  title="Play Go"
></iframe>

URL parameters

ParamValuesDefault
embed1Required for embed mode
familygochess
variantAny variant key belowstandard
opponentai | humanai in embed
difficultybeginner | easy | medium | hard | expertmedium
themeBoard theme idclassic

postMessage control

Commands accept either the generic game: prefix or the family prefix, so game:undo and go:undo both work.

iframe.contentWindow.postMessage({ type: 'game:setVariant', variant: 'capture-go' }, '*')
iframe.contentWindow.postMessage({ type: 'game:setDifficulty', difficulty: 'hard' }, '*')
iframe.contentWindow.postMessage({ type: 'game:newGame' }, '*')
iframe.contentWindow.postMessage({ type: 'game:undo' }, '*')
iframe.contentWindow.postMessage({ type: 'game:pass' }, '*')
iframe.contentWindow.postMessage({ type: 'game:resign' }, '*')
iframe.contentWindow.postMessage({ type: 'game:requestState' }, '*')

Events from embed

window.addEventListener('message', (e) => {
  switch (e.data.type) {
    case 'game:ready':  break  // { family, variant, state }
    case 'game:move':   break  // { move, state }
    case 'game:status': break  // { text, gameOver, family, variant }
    case 'game:state':  break  // { state } in reply to requestState
  }
})

Variants

Variant parameters follow the Go hub in moddable-rules. Nine of the fourteen hub variants are registered; the rest are listed under gaps below.

KeyBoardDistinguishing rule
standard19×19Territory scoring, komi 6.5, positional superko
13x1313×13Standard rules, komi 6.5
9x99×9Standard rules, komi 5.5
capture-go9×9First capture wins, passing removed from the move list
one-colour19×19Standard rules rendered with a single stone colour
stoical19×19Cannot capture on the turn after being captured
toroidal-go11×11Edges wrap both ways, komi 4.5
gomoku15×15No captures, exactly five in a row wins, overlines do not
ninuki-renju15×15Five in a row, or ten stones captured in pairs

Variants as plugins

A variant is a plain object of declarative configuration plus optional hooks. Nothing about a variant requires touching the plugin.

export const captureGo = {
  key: 'capture-go',
  label: 'Capture Go',
  size: 9,
  komi: 0,
  allowPass: false,

  hooks: {
    moveFilter(moves) {
      return moves.filter(m => m.action !== 'pass')
    },
  },

  winCondition(slice) {
    if (slice.captures[0] >= 1) return 'black'
    if (slice.captures[1] >= 1) return 'white'
    return null
  },
}

Available hooks: init, validateMove, applyMove, getLegalMoves, checkWin, moveFilter, captureEffect, continueTurn, beforeMove, afterMove. Top-level winCondition and evaluate are checked before the default rules. Variants compose through extends, which merges hooks rather than replacing them.

Scoring

Two passes end play and report scoring rather than a winner, so the interface can run a dead-stone marking phase before the count. Call the plugin's score() once stones are agreed.

import { scoreGame } from 'moddable-engine/plugins/go'

const result = scoreGame(slice, {
  getNeighbours,
  method: 'territory',   // or 'area'
  komi: 6.5,
  deadStones: [40, 41],
})
// { winner, margin, scores: { black, white }, territory, stones, neutral }

Territory scoring counts surrounded points plus prisoners. Area scoring counts surrounded points plus stones on the board. Empty regions bordered by both colours are neutral and count to neither side.

SDK

import { createGame, getLegalMoves, getGameStatus, createAI, listVariants } from 'moddable-engine/play'

listVariants('go')                       // [{ key, label, group, board, description }]
const game = createGame('go', '9x9')
game.applyMove({ coord: 40 })
getGameStatus('go', '9x9', game.getState())

const ai = createAI('go', '9x9', { difficulty: 'hard' })
ai.pickMove(game.getState().slice, 0)    // MCTS by default for Go

Known gaps

Five hub variants are deliberately not registered, because each needs a mechanic the engine does not model yet: