Docs / Guides / Colony Plugin

Building a Game Plugin: Colony

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.

Prerequisites

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

Step 1: Create the Plugin File

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.

Step 2: Generate the Board

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 });
    }
}

Step 3: Terrain Distribution

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'
    };
}

Step 4: Number Token Overlays

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++;
    }
}

Step 5: Placement Constraints

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.

Step 6: Layout Variants

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...
}

Step 7: Tile Images

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'],

Summary

A complete game plugin uses these hooks:

HookPurpose
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
layoutsDefine board variants (hides size selector automatically)
rendererOptions()Set hex size and orientation

See the full API Reference for all available hooks and config properties.