This guide walks through creating a complete game plugin from scratch. We will build Colony (a Catan-style hex board generator) step by step, using every major engine feature: layouts, tile images, number token overlays, and placement constraints.
You need a local copy of the moddable-hexmaps repository and a browser. No build tools required.
git clone https://github.com/Moddable-Games/moddable-hexmaps.git
cd moddable-hexmaps
Create js/games/colony.js. Every plugin imports registerGame from the game registry and any utilities it needs:
import { registerGame } from '../game-registry.js';
registerGame('colony', {
label: 'Colony',
orientation: 'pointy',
sizes: [{ value: 2, label: 'Standard' }],
defaultSize: 2,
defaultPlayers: 0,
styles: ['classic'],
hasEditor: false,
playerCounts: function() { return []; },
generate: function(size, players, seed, layout) {
return [{ id: 'R0', q: 0, r: 0, type: 'desert', label: 'D' }];
},
getColors: function() {
return { desert: '#FFF8E1' };
},
rendererOptions: function() {
return { hexSize: 50, flat: false };
}
});
Add an import in js/app.js:
import './games/colony.js';
Reload the page. A "Colony" tab now appears, showing a single desert hex.
Colony uses a standard hex grid built from concentric rings. Ring 0 is the centre hex, ring 1 has 6 hexes, ring 2 has 12. A standard Catan board is 2 rings (19 hexes total).
function generateRing(radius) {
if (radius === 0) return [{ q: 0, r: 0 }];
var results = [];
var directions = [
{ q: 1, r: 0 }, { q: 0, r: 1 }, { q: -1, r: 1 },
{ q: -1, r: 0 }, { q: 0, r: -1 }, { q: 1, r: -1 }
];
var q = 0, r = -radius;
for (var d = 0; d < 6; d++) {
for (var step = 0; step < radius; step++) {
results.push({ q: q, r: r });
q += directions[d].q;
r += directions[d].r;
}
}
return results;
}
The key insight: start at position (0, -radius) and walk each of the 6 hex directions for radius steps. This traces the ring perimeter.
Build the full board by iterating rings 0 through N:
var positions = [];
for (var ring = 0; ring <= size; ring++) {
var hexes = generateRing(ring);
for (var i = 0; i < hexes.length; i++) {
positions.push({ q: hexes[i].q, r: hexes[i].r, ring: ring });
}
}
Catan has a fixed pool of terrain tiles that are shuffled and placed randomly:
var terrainPool = [
{ type: 'forest', count: 4 },
{ type: 'pasture', count: 4 },
{ type: 'fields', count: 4 },
{ type: 'hills', count: 3 },
{ type: 'mountains', count: 3 },
{ type: 'desert', count: 1 }
];
Build a flat array from the pool, then shuffle using the seeded RNG:
var tiles = [];
for (var i = 0; i < terrainPool.length; i++) {
for (var j = 0; j < terrainPool[i].count; j++) {
tiles.push(terrainPool[i].type);
}
}
var rng = createSeededRng(seed + '_colony');
for (var i = tiles.length - 1; i > 0; i--) {
var j = rng.integer(0, i);
var tmp = tiles[i]; tiles[i] = tiles[j]; tiles[j] = tmp;
}
Assign tiles to positions in order. Define colours so each terrain is visually distinct:
getColors: function() {
return {
forest: '#2E7D32',
pasture: '#8BC34A',
fields: '#FDD835',
hills: '#D84315',
mountains: '#616161',
desert: '#FFF8E1'
};
}
The engine supports an overlay property on each hex object. When present, the renderer draws a circular token with text on top of the hex:
hex.overlay = {
text: '8',
color: '#C62828',
size: 0.35
};
For Colony, we distribute number tokens (2–12, no 7) across all non-desert hexes. The values 6 and 8 are highlighted in red as high-probability rolls:
var numberTokens = [2,3,3,4,4,5,5,6,6,8,8,9,9,10,10,11,11,12];
var tokenColors = { 6: '#C62828', 8: '#C62828' };
// Shuffle tokens same way as tiles...
var tokenIdx = 0;
for (var i = 0; i < hexData.length; i++) {
if (hexData[i].type !== 'desert' && tokenIdx < tokens.length) {
var num = tokens[tokenIdx];
hexData[i].overlay = {
text: String(num),
color: tokenColors[num] || '#FFF8E1',
size: 0.35
};
tokenIdx++;
}
}
In Catan, the 6 and 8 tokens (highest probability) must not be placed on adjacent hexes. The engine supports a constraints hook. If it returns false, the framework re-generates with an incremented seed (up to 100 retries).
constraints: function(hexData) {
var hexMap = {};
for (var i = 0; i < hexData.length; i++) {
hexMap[hexData[i].q + ',' + hexData[i].r] = hexData[i];
}
var dirs = [
{ q: 1, r: 0 }, { q: 0, r: 1 }, { q: -1, r: 1 },
{ q: -1, r: 0 }, { q: 0, r: -1 }, { q: 1, r: -1 }
];
for (var i = 0; i < hexData.length; i++) {
var hex = hexData[i];
if (!hex.overlay) continue;
var num = parseInt(hex.overlay.text);
if (num !== 6 && num !== 8) continue;
for (var d = 0; d < dirs.length; d++) {
var nKey = (hex.q + dirs[d].q) + ',' + (hex.r + dirs[d].r);
var neighbor = hexMap[nKey];
if (neighbor && neighbor.overlay) {
var nNum = parseInt(neighbor.overlay.text);
if (nNum === 6 || nNum === 8) return false;
}
}
}
return true;
}
The framework automatically handles retries. Your generate function just needs to produce candidates, and constraints validates them.
Colony supports 7 layouts: the base game (19 hexes, 2 rings), expanded 5-6 player (30 hexes, custom coordinates), and 5 Seafarers expansion scenarios. The engine's layout system lets you define variants without changing the generate logic:
layouts: [
{ value: 'standard', label: 'Standard (3-4 Players)' },
{ value: 'expanded', label: 'Expanded (5-6 Players)' },
{ value: 'newWorld', label: 'Seafarers: New World' },
{ value: 'fourIslands', label: 'Seafarers: Four Islands' },
{ value: 'newShores', label: 'Seafarers: New Shores' },
{ value: 'desert', label: 'Seafarers: Through the Desert' },
{ value: 'fogIslands', label: 'Seafarers: Fog Islands' }
],
defaultLayout: 'standard',
When layouts is defined, the framework shows a Layout dropdown and passes the selected value as the fourth argument to generate(). The size selector is automatically hidden.
The Seafarers layouts use a ring-3 grid (37 hexes) with land placed by different algorithms (random island, four separated clusters, etc.) and remaining positions filled with sea. A decorative sea frame ring surrounds the playing area. Additional terrain types (sea, gold, fog) extend the base colour/image maps.
In your generate function, use the layout to determine ring count and tile pool:
generate: function(size, players, seed, selectedLayout) {
var layouts = {
standard: { rings: 2, pool: basePool, tokens: baseTokens },
expanded: { rings: 3, pool: expandedPool, tokens: expandedTokens }
};
var def = layouts[selectedLayout] || layouts.standard;
// Build board with def.rings, shuffle def.pool...
}
Add visual tile artwork by implementing getImages(). Place PNG files in img/tiles/colony/ named by terrain type:
getImages: function(style) {
if (style === 'classic') return null;
var base = (window.location.pathname.indexOf('/generate') !== -1) ? '../' : '';
var folder = style === 'realistic' ? 'colony-realistic' : 'colony';
return {
forest: base + 'img/tiles/' + folder + '/forest.png',
pasture: base + 'img/tiles/' + folder + '/pasture.png',
fields: base + 'img/tiles/' + folder + '/fields.png',
hills: base + 'img/tiles/' + folder + '/hills.png',
mountains: base + 'img/tiles/' + folder + '/mountains.png',
desert: base + 'img/tiles/' + folder + '/desert.png'
};
}
The renderer clips images to the hex shape automatically. Any square-ish PNG will work. The engine handles pointy-top and flat-top clipping.
Style names must be alphabetically sorted in the styles array:
styles: ['classic', 'kenney', 'realistic'],
A complete game plugin uses these hooks:
| Hook | Purpose |
|---|---|
generate() | Build the hex array with terrain, overlays, and positions |
getColors() | Map terrain types to flat colours for classic mode |
getImages() | Map terrain types to image paths for artistic modes |
getDescriptions() | Map terrain types to name + description for hover info |
constraints() | Validate placement rules, trigger re-generation if invalid |
layouts | Define board variants (hides size selector automatically) |
rendererOptions() | Set hex size and orientation |
See the full API Reference for all available hooks and config properties.