Docs / API Reference

API Reference

Public methods on the HexRenderer, HexMath, XorShift128, and HexApp globals, plus the iframe embed and postMessage APIs.

HexRenderer

Canvas-based hexagonal grid renderer. All methods are on the HexRenderer global object.

HexRenderer.create(canvasId, options)

Creates a renderer instance bound to a canvas element. Only one renderer can be active at a time. Creating a new one cleans up the previous.

var renderer = HexRenderer.create('hex-canvas', {
  hexSize: 40,
  flat: false,
  colors: { water: '#2196F3', trees: '#4CAF50' },
  labels: true,
  onHexClick: function(hex) { },
  onHexHover: function(hex) { }
});
OptionTypeDescription
hexSizenumberBase hex radius in pixels (auto-scaled to fit)
flatbooleanFlat-top (true) or pointy-top (false) orientation
colorsobjectMap of terrain type → CSS colour string
labelsbooleanShow single-character labels inside hexes
onHexClickfunctionCallback when a hex is clicked. Receives hex object
onHexHoverfunctionCallback when mouse enters a hex. Receives hex object

HexRenderer.setHexes(renderer, hexes)

Sets the hex data array and triggers a re-render. Automatically fits the grid to the canvas.

HexRenderer.setHexes(renderer, [
  { id: 'R0', q: 0, r: 0, type: 'grass', label: 'G' },
  { id: 'R1D1', q: 1, r: 0, type: 'water', label: 'W' }
]);

HexRenderer.render(renderer)

Redraws the current hex data without recalculating layout. Use after modifying hex properties (type, label).

HexRenderer.resize(renderer)

Recalculates canvas dimensions from parent element and re-fits the grid. Called automatically on window resize.

HexRenderer.fitToCanvas(renderer)

Recalculates hex size and offset to fit all hexes within the current canvas dimensions. Called automatically by setHexes and resize.

HexRenderer.preloadImages(imagePaths, callback)

Preloads tile images for artistic rendering. imagePaths is an object mapping keys to URL strings. Callback fires when all images are loaded (or failed). Images are cached globally; calling twice with the same paths is a no-op.

HexRenderer.preloadImages({
  water: 'img/tiles/nukes/water.png',
  trees: 'img/tiles/nukes/trees.png'
}, function() {
  HexRenderer.render(renderer);
});

Image rendering

When renderer.images is set, the renderer clips each hex path and draws the corresponding tile image instead of a flat colour fill. Two image modes are supported:

  • Type-level images: Set renderer.images = { terrain: 'path.png' } — each hex of that terrain type uses the same image.
  • Per-hex images: Set hex.imagePath = 'path.png' on individual hexes — each hex gets a unique image (used by Twilight for per-system artwork).

Per-hex imagePath takes priority over the type-level map. If no image is found (or not yet loaded), the renderer falls back to flat colour fill.

Hex object shape

PropertyTypeDescription
idstringUnique identifier (e.g. "R0", "R2D5")
qnumberAxial column coordinate
rnumberAxial row coordinate
typestringTerrain type key (maps to colours)
labelstringSingle character displayed in hex centre
imagePathstring (optional)Per-hex tile image URL — overrides type-level image when in artistic mode
tileNamestring (optional)Human-readable tile name (shown on hover)

HexMath

Coordinate math and grid generation utilities on the HexMath global object.

HexMath.axialToPixelPointy(q, r, size)

Converts axial coordinates to pixel position for pointy-top orientation. Returns { x, y }.

HexMath.axialToPixelFlat(q, r, size)

Converts axial coordinates to pixel position for flat-top orientation. Returns { x, y }.

HexMath.getHexCorners(cx, cy, size, flat)

Returns an array of 6 corner points [{ x, y }, ...] for a hex centred at (cx, cy).

HexMath.generateRing(ring)

Generates axial coordinates for all hexes in a given ring. Ring 0 returns the centre hex.

HexMath.generateRing(0);  // [{ q: 0, r: 0 }]
HexMath.generateRing(1);  // 6 hexes around centre
HexMath.generateRing(2);  // 12 hexes

HexMath.generateHexGrid(rings)

Generates a complete hex grid from ring 0 to the given ring count. Each hex includes a ring property.

var grid = HexMath.generateHexGrid(3);
// Returns: [{ q, r, ring }, ...] — 37 hexes total

HexMath.axialDistance(a, b)

Returns the hex-grid distance between two axial coordinates.

HexMath.axialDistance({ q: 0, r: 0 }, { q: 2, r: -1 }); // 2

HexMath.getNeighbors(q, r)

Returns an array of 6 neighbouring axial coordinates.

XorShift128 (Seeded RNG)

Deterministic pseudo-random number generator for reproducible map generation.

new XorShift128(seed)

Creates an RNG instance. Seed is an unsigned 32-bit integer.

rng.next()

Returns the next raw 32-bit integer from the sequence.

rng.unit()

Returns a float in [0, 1).

rng.integer(min, max)

Returns a random integer in [min, max] inclusive.

rng.reseed(seed)

Resets the internal state to a new seed.

createSeededRng(seed)

Helper that accepts a string or number seed and returns an XorShift128 instance. Strings are hashed to a numeric seed.

var rng = createSeededRng('hello');
rng.integer(0, 5);  // deterministic result

seededRandom(id, seed, min, max)

One-shot deterministic random for a given hex ID + map seed combination. Used internally for terrain assignment.

seededRandom('R2D3', '42', 0, 4); // always the same result

Embedding API

Embed hex maps in any page via iframe. Parameters control game, appearance, and interactivity.

Core parameters

ParameterValuesEffect
gamenukes, talisman, twilight, colony, mongo, endlessWhich game config to use
seedany integer or stringRNG seed for reproducible generation
size2–6 (nukes), 4–5 (talisman), 5 (endless), 6 (mongo)Ring count / map size
players2–8 (varies by game/size)Player count for base placement

Display parameters

ParameterValuesEffect
boardonly1Hide all UI chrome (nav, sidebar, footer, tabs). Map canvas fills the iframe
bghex colour (e.g. 1a1a2e or #1a1a2e)Background colour for canvas area. Omit # for cleaner URLs
themesee table belowVisual style (availability varies by game). Alias: style
modeview, edit, fullscreenview = read-only (default for boardonly); edit = click-to-cycle terrain; fullscreen = full-viewport map with hidden chrome (Exit button + Escape to return)

Styles per game

GameAvailable styles
nukesartistic, classic, kenney, realistic
talismanartistic, classic, kenney, realistic
twilightclassic only (no style parameter)
colonyclassic, kenney, realistic
mongoartistic, classic
endlessartistic, classic

Examples

Board-only Nukes map on a dark background:

<iframe
  src="https://hex.moddable.games/generate/?game=nukes&boardonly=1&seed=42&size=4&bg=0d1117"
  width="600" height="400" style="border:none"
></iframe>

Twilight Imperium galaxy, 6 players, read-only:

<iframe
  src="https://hex.moddable.games/generate/?game=twilight&boardonly=1&players=6&bg=000000"
  width="600" height="600" style="border:none"
></iframe>

Interactive embed with editing enabled:

<iframe
  src="https://hex.moddable.games/generate/?game=nukes&boardonly=1&mode=edit&size=3"
  width="500" height="400" style="border:none"
></iframe>

Full UI link (opens the generator directly, no embed):

https://hex.moddable.games/generate/?game=talisman&size=5&seed=999

postMessage Bridge

When embedded with boardonly=1, parent apps can communicate with the hex map via postMessage. This allows requesting map data, pushing maps, and triggering regeneration.

hexmap:getMap

Request the current map data from the embedded generator.

// Parent sends:
iframe.contentWindow.postMessage({ type: 'hexmap:getMap' }, '*');

// Hexmaps responds:
{
  type: 'hexmap:mapData',
  format: 'nukes',         // game key, or 'hex' for generic format
  game: 'nukes',
  data: [...]              // game-specific format (see below)
}

The format field tells the parent what shape to expect. Games with an exportForParent hook return a custom format; others return the generic hex-format.

Nukes format (ring arrays)

// data = array of rings, each containing hex entries
[
  [{ id: 'R0', t: 'grass' }],              // ring 0 (centre)
  [{ id: 'R1D1', t: 'water' }, ...],       // ring 1 (6 hexes)
  [{ id: 'R2D1', t: 'mount' }, ...],       // ring 2 (12 hexes)
  ...
]

Generic hex-format

// data = standard hex export object
{
  game: 'talisman',
  seed: '12345',
  size: 4,
  players: 0,
  hexes: [
    { id: 'R0D0', q: 0, r: 0, type: 'ending' },
    { id: 'R1D1', q: 1, r: 0, type: 'plains' },
    ...
  ]
}

hexmap:setMap

Push map data into the embedded generator. The active game's importFromParent hook processes the data.

iframe.contentWindow.postMessage({
  type: 'hexmap:setMap',
  data: [[{ id: 'R0', t: 'base' }], [{ id: 'R1D1', t: 'water' }, ...]]
}, '*');

hexmap:regenerate

Trigger map regeneration with new parameters. Any combination of seed, size, players, layout, and style can be provided.

iframe.contentWindow.postMessage({
  type: 'hexmap:regenerate',
  seed: '42',
  size: 4,
  players: 3,
  layout: '6p',       // optional — layout variant key
  style: 'kenney'     // optional — also switches visual style
}, '*');

hexmap:setStyle

Change the visual style without regenerating the map. The style must be valid for the current game.

iframe.contentWindow.postMessage({
  type: 'hexmap:setStyle',
  style: 'kenney'    // artistic, classic, kenney, or realistic (varies by game)
}, '*');

hexmap:setGame

Switch to a different game. Optionally set the style at the same time. This regenerates the map with the new game's default settings.

iframe.contentWindow.postMessage({
  type: 'hexmap:setGame',
  game: 'colony',
  style: 'realistic'  // optional — defaults to the game's first style
}, '*');

hexmap:exportSvg

Request an SVG export of the current map. Responds with a hexmap:svgData message containing the full SVG markup string.

// Parent sends:
iframe.contentWindow.postMessage({ type: 'hexmap:exportSvg' }, '*');

// Hexmaps responds:
{
  type: 'hexmap:svgData',
  svg: '<svg xmlns="...">...</svg>'   // complete SVG document string
}

The SVG includes hex polygons with fill colours and (when using a tile style) clipped tile images as linked <image> elements. Labels are included when in classic mode.

Listening for responses

window.addEventListener('message', function(event) {
  if (event.data.type === 'hexmap:mapData') {
    console.log('Game:', event.data.game);
    console.log('Format:', event.data.format);
    console.log('Map data:', event.data.data);
  }
  if (event.data.type === 'hexmap:svgData') {
    console.log('SVG:', event.data.svg);
  }
});

HexSvg

SVG serialiser for hex maps. Produces standalone SVG documents from hex data arrays. Used by the Export SVG button and the hexmap:exportSvg postMessage command.

HexSvg.toSVG(hexes, options)

Render a hex map to an SVG string.

var svg = HexSvg.toSVG(hexData, {
  hexSize: 40,
  flat: false,
  colors: { plains: '#C8E6C9', forest: '#388E3C' },
  images: { plains: 'img/tiles/talisman/plains.png' },
  imageMode: 'link',    // 'link' (href) or 'embed' (data URI) or 'none'
  labels: true,
  strokeColor: 'rgba(0,0,0,0.3)',
  strokeWidth: 1,
  padding: 15,
  bgColor: '#1a1a2e',
  scaleFactor: 0.95
});
OptionTypeDefaultDescription
hexSizenumber30Hex radius in SVG units
flatbooleanfalseFlat-top (true) or pointy-top (false) orientation
colorsobject{}Terrain type → fill colour map
imagesobject|nullnullTerrain type → image path map (or null for flat colours only)
imageModestring'none''link' for external hrefs, 'embed' for inline data URIs, 'none' to skip images
labelsbooleanfalseShow text labels on each hex
strokeColorstring'rgba(0,0,0,0.3)'Hex border stroke colour
strokeWidthnumber1Hex border width
paddingnumber15Padding around the map in SVG units
bgColorstring|nullnullBackground fill colour (null = transparent)
scaleFactornumber0.95Hex polygon inset (1.0 = edge-to-edge, 0.95 = small gap)

HexSvg.toAnnotatedSVG(hexes, annotations, options)

Same as toSVG but adds annotation overlays (highlights, tokens, arrows, legend). Used for tactical/analysis exports.

var svg = HexSvg.toAnnotatedSVG(hexData, {
  highlights: [{ q: 0, r: 1, style: 'valid' }],
  tokens: [{ q: 1, r: 0, color: '#1c4a4a', label: 'P1' }],
  arrows: [{ from: {q:0,r:0}, to: {q:1,r:0}, color: '#2aaa10' }],
  legend: [{ style: 'valid', text: 'Move' }, { style: 'blocked', text: 'Blocked' }]
}, options);

Annotation styles: valid (green border), blocked (red X), selected (gold border), friendly (gold border).

HexApp.registerGame()

Register a new hex game plugin. See the Plugin System docs for the full config shape and available hooks.

HexApp.registerGame(key, config)
ParamTypeDescription
keystringUnique game identifier (used in URL params and registry)
configobjectGame configuration with metadata, sizes, and hooks

HexApp.getGameConfig(key)

Returns the config object for a registered game, or null if not found.

HexApp.getRegisteredGames()

Returns an array of all registered game keys (e.g. ['nukes', 'talisman', 'twilight', 'colony']).

config.getDescriptions()

Optional hook returning a map of terrain type keys to metadata objects. When present, the hover info displays the full name and description instead of the raw type key.

getDescriptions: function() {
    return {
        plains: { name: 'Plains', desc: 'draw one card' },
        cave:   { name: "Warlock's Cave", desc: 'draw quest card' }
    };
}

Currently used by Talisman (55 terrain descriptions). Other games can add descriptions using the same interface.

Consumer SDK

Embed and control hex maps programmatically without iframes. Mount a fully interactive map controller into any DOM container.

Quick start

<div id="my-map" style="width:600px;height:400px"></div>
<script type="module">
import './js/hex-controller-entry.js';

const map = HexApp.createMapController(
  document.getElementById('my-map'),
  { game: 'nukes', seed: '42', size: 4, players: 4 }
);
</script>

Or import selectively (only the games you need):

import { HexApp } from './js/game-registry.js';
import './js/hex-controller.js';
import './js/games/nukes.js';

const map = HexApp.createMapController(container, { game: 'nukes' });

HexApp.createMapController(container, options)

Factory function. Creates a canvas inside the container, generates the initial map, and returns a controller object.

OptionTypeDefaultDescription
gamestring(required)Registered game key
seedstring|numberDate.now()RNG seed for reproducible generation
sizenumbergame defaultRing count / map size
playersnumbergame defaultPlayer count
layoutstringgame defaultLayout variant (for games with layouts)
stylestringfirst availableVisual style (classic, kenney, etc.)
interactivebooleantrueEnable hover/click interaction
bgColorstringnullCanvas background colour
hooksobject{}Render hooks (see below)

Controller methods

MethodReturnsDescription
regenerate(opts)voidRegenerate with new seed/size/players/layout
setStyle(style)voidSwitch visual style and re-render
getHexData()arrayReturns a copy of the hex array
getHexAt(q, r)object|nullLook up a single hex by coordinates
setHexType(q, r, type)voidChange a hex's terrain type and re-render
highlightHexes(coords, opts)voidAdd translucent overlays. opts: { color, opacity }
clearHighlights()voidRemove all highlights
exportSVG(opts)stringExport current map as SVG markup
exportPNG(opts)Promise<Blob>Export as PNG blob. opts: { scale, bgColor }
on(event, fn)voidSubscribe to an event
off(event, fn)voidUnsubscribe from an event
resize()voidForce resize (auto via ResizeObserver)
render()voidForce a re-render
destroy()voidRemove canvas, detach listeners, free memory

Events

EventPayload
hexClick{ hex, event }
hexHover{ hex, event }
hexLeave{}
regenerate{ hexes, seed, size, players, layout }
styleChange{ style }
destroy{}

Render hooks

Hooks receive the canvas 2D context and are called during the render loop.

const map = HexApp.createMapController(container, {
  game: 'nukes',
  hooks: {
    tilePainter: function(ctx, hex, bounds) {
      // Return true to replace default rendering
      if (hex.type === 'base') {
        ctx.fillStyle = '#ff0';
        ctx.fill();
        return true;
      }
      return false;
    },
    overlayProvider: function(hex) {
      // Return { color, opacity } to tint a hex
      if (hex.type === 'water') return { color: '#00bcd4', opacity: 0.2 };
      return null;
    },
    afterRender: function(ctx, hexes, controller) {
      // Draw tokens, labels, or any custom overlay
    }
  }
});

Multi-instance

Each controller is fully independent: own canvas, own state, own image cache. Multiple maps on one page work without interference.

const map1 = HexApp.createMapController(el1, { game: 'nukes', seed: 'a' });
const map2 = HexApp.createMapController(el2, { game: 'twilight', seed: 'b' });
// Both render independently; destroying one does not affect the other

Live SDK demo — two maps with hooks, events, and programmatic control.

MCP Server

The engine is available as an MCP (Model Context Protocol) server for use with Claude Code, Claude Desktop, or any MCP-compatible client. Six tools expose the full generation and analysis pipeline.

Setup

# Run standalone
node mcp/server.js

# Add to Claude Code
claude mcp add --transport stdio moddable-hexmaps node mcp/server.js

Available tools

ToolDescription
hex_list_gamesAll available games with map types, layouts, styles, and player counts
hex_generate_mapGenerate a complete hex grid for any game, seed, size, and player count
hex_get_infoLook up a hex by coordinates: terrain, adjacency list, distances to other hexes
hex_compute_fovCompute visible hexes from a position within a given range (with terrain blocking)
hex_export_svgExport any generated map as a standalone SVG string
hex_pathfindFind the shortest path between two hexes through terrain

Example: generate and export

// hex_generate_map
{ "game": "nukes", "seed": "42", "size": 4, "players": 4 }
// Returns: { hexes: [...], meta: { game, seed, size, players } }

// hex_export_svg
{ "game": "nukes", "seed": "42", "size": 4, "style": "classic" }
// Returns: { svg: "<svg ...>...</svg>" }

Example: analysis

// hex_get_info
{ "game": "nukes", "seed": "42", "size": 4, "q": 1, "r": -1 }
// Returns: { hex: {...}, neighbors: [...], distances: {...} }

// hex_compute_fov
{ "game": "nukes", "seed": "42", "size": 4, "q": 0, "r": 0, "range": 2 }
// Returns: { visible: [...], blocked: [...] }

// hex_pathfind
{ "game": "nukes", "seed": "42", "size": 4, "fromQ": 0, "fromR": 0, "toQ": 2, "toR": -1 }
// Returns: { path: [...], cost: 2 }