Public methods on the HexRenderer, HexMath, XorShift128, and HexApp globals, plus the iframe embed and postMessage APIs.
Canvas-based hexagonal grid renderer. All methods are on the HexRenderer global object.
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) { }
});
| Option | Type | Description |
|---|---|---|
hexSize | number | Base hex radius in pixels (auto-scaled to fit) |
flat | boolean | Flat-top (true) or pointy-top (false) orientation |
colors | object | Map of terrain type → CSS colour string |
labels | boolean | Show single-character labels inside hexes |
onHexClick | function | Callback when a hex is clicked. Receives hex object |
onHexHover | function | Callback when mouse enters a hex. Receives hex object |
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' }
]);
Redraws the current hex data without recalculating layout. Use after modifying hex properties (type, label).
Recalculates canvas dimensions from parent element and re-fits the grid. Called automatically on window resize.
Recalculates hex size and offset to fit all hexes within the current canvas dimensions. Called automatically by setHexes and resize.
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);
});
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:
renderer.images = { terrain: 'path.png' } — each hex of that terrain type uses the same image.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.
| Property | Type | Description |
|---|---|---|
id | string | Unique identifier (e.g. "R0", "R2D5") |
q | number | Axial column coordinate |
r | number | Axial row coordinate |
type | string | Terrain type key (maps to colours) |
label | string | Single character displayed in hex centre |
imagePath | string (optional) | Per-hex tile image URL — overrides type-level image when in artistic mode |
tileName | string (optional) | Human-readable tile name (shown on hover) |
Coordinate math and grid generation utilities on the HexMath global object.
Converts axial coordinates to pixel position for pointy-top orientation. Returns { x, y }.
Converts axial coordinates to pixel position for flat-top orientation. Returns { x, y }.
Returns an array of 6 corner points [{ x, y }, ...] for a hex centred at (cx, cy).
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
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
Returns the hex-grid distance between two axial coordinates.
HexMath.axialDistance({ q: 0, r: 0 }, { q: 2, r: -1 }); // 2
Returns an array of 6 neighbouring axial coordinates.
Deterministic pseudo-random number generator for reproducible map generation.
Creates an RNG instance. Seed is an unsigned 32-bit integer.
Returns the next raw 32-bit integer from the sequence.
Returns a float in [0, 1).
Returns a random integer in [min, max] inclusive.
Resets the internal state to a new 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
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
Embed hex maps in any page via iframe. Parameters control game, appearance, and interactivity.
| Parameter | Values | Effect |
|---|---|---|
game | nukes, talisman, twilight, colony, mongo, endless | Which game config to use |
seed | any integer or string | RNG seed for reproducible generation |
size | 2–6 (nukes), 4–5 (talisman), 5 (endless), 6 (mongo) | Ring count / map size |
players | 2–8 (varies by game/size) | Player count for base placement |
| Parameter | Values | Effect |
|---|---|---|
boardonly | 1 | Hide all UI chrome (nav, sidebar, footer, tabs). Map canvas fills the iframe |
bg | hex colour (e.g. 1a1a2e or #1a1a2e) | Background colour for canvas area. Omit # for cleaner URLs |
theme | see table below | Visual style (availability varies by game). Alias: style |
mode | view, edit, fullscreen | view = read-only (default for boardonly); edit = click-to-cycle terrain; fullscreen = full-viewport map with hidden chrome (Exit button + Escape to return) |
| Game | Available styles |
|---|---|
| nukes | artistic, classic, kenney, realistic |
| talisman | artistic, classic, kenney, realistic |
| twilight | classic only (no style parameter) |
| colony | classic, kenney, realistic |
| mongo | artistic, classic |
| endless | artistic, classic |
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
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.
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.
// 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)
...
]
// 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' },
...
]
}
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' }, ...]]
}, '*');
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
}, '*');
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)
}, '*');
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
}, '*');
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.
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);
}
});
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.
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
});
| Option | Type | Default | Description |
|---|---|---|---|
hexSize | number | 30 | Hex radius in SVG units |
flat | boolean | false | Flat-top (true) or pointy-top (false) orientation |
colors | object | {} | Terrain type → fill colour map |
images | object|null | null | Terrain type → image path map (or null for flat colours only) |
imageMode | string | 'none' | 'link' for external hrefs, 'embed' for inline data URIs, 'none' to skip images |
labels | boolean | false | Show text labels on each hex |
strokeColor | string | 'rgba(0,0,0,0.3)' | Hex border stroke colour |
strokeWidth | number | 1 | Hex border width |
padding | number | 15 | Padding around the map in SVG units |
bgColor | string|null | null | Background fill colour (null = transparent) |
scaleFactor | number | 0.95 | Hex polygon inset (1.0 = edge-to-edge, 0.95 = small gap) |
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).
Register a new hex game plugin. See the Plugin System docs for the full config shape and available hooks.
HexApp.registerGame(key, config)
| Param | Type | Description |
|---|---|---|
key | string | Unique game identifier (used in URL params and registry) |
config | object | Game configuration with metadata, sizes, and hooks |
Returns the config object for a registered game, or null if not found.
Returns an array of all registered game keys (e.g. ['nukes', 'talisman', 'twilight', 'colony']).
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.
Embed and control hex maps programmatically without iframes. Mount a fully interactive map controller into any DOM container.
<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' });
Factory function. Creates a canvas inside the container, generates the initial map, and returns a controller object.
| Option | Type | Default | Description |
|---|---|---|---|
game | string | (required) | Registered game key |
seed | string|number | Date.now() | RNG seed for reproducible generation |
size | number | game default | Ring count / map size |
players | number | game default | Player count |
layout | string | game default | Layout variant (for games with layouts) |
style | string | first available | Visual style (classic, kenney, etc.) |
interactive | boolean | true | Enable hover/click interaction |
bgColor | string | null | Canvas background colour |
hooks | object | {} | Render hooks (see below) |
| Method | Returns | Description |
|---|---|---|
regenerate(opts) | void | Regenerate with new seed/size/players/layout |
setStyle(style) | void | Switch visual style and re-render |
getHexData() | array | Returns a copy of the hex array |
getHexAt(q, r) | object|null | Look up a single hex by coordinates |
setHexType(q, r, type) | void | Change a hex's terrain type and re-render |
highlightHexes(coords, opts) | void | Add translucent overlays. opts: { color, opacity } |
clearHighlights() | void | Remove all highlights |
exportSVG(opts) | string | Export current map as SVG markup |
exportPNG(opts) | Promise<Blob> | Export as PNG blob. opts: { scale, bgColor } |
on(event, fn) | void | Subscribe to an event |
off(event, fn) | void | Unsubscribe from an event |
resize() | void | Force resize (auto via ResizeObserver) |
render() | void | Force a re-render |
destroy() | void | Remove canvas, detach listeners, free memory |
| Event | Payload |
|---|---|
hexClick | { hex, event } |
hexHover | { hex, event } |
hexLeave | {} |
regenerate | { hexes, seed, size, players, layout } |
styleChange | { style } |
destroy | {} |
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
}
}
});
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.
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.
# Run standalone
node mcp/server.js
# Add to Claude Code
claude mcp add --transport stdio moddable-hexmaps node mcp/server.js
| Tool | Description |
|---|---|
hex_list_games | All available games with map types, layouts, styles, and player counts |
hex_generate_map | Generate a complete hex grid for any game, seed, size, and player count |
hex_get_info | Look up a hex by coordinates: terrain, adjacency list, distances to other hexes |
hex_compute_fov | Compute visible hexes from a position within a given range (with terrain blocking) |
hex_export_svg | Export any generated map as a standalone SVG string |
hex_pathfind | Find the shortest path between two hexes through terrain |
// 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>" }
// 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 }