git @ Cat's Eye Technologies Backtracking-Wang-Tiler / master src / backtracking-wang-tiler.js
master

Tree @master (Download .tar.gz)

backtracking-wang-tiler.js @masterraw · history · blame

"use strict";

function launch(prefix, containerId, config) {
    var config = config || {};
    var deps = [
        "element-factory.js",
        "playfield.js",
        "animation.js"
    ];
    var loaded = 0;
    for (var i = 0; i < deps.length; i++) {
        var elem = document.createElement('script');
        elem.src = prefix + deps[i];
        elem.onload = function() {
            if (++loaded < deps.length) return;
            var container = document.getElementById(containerId);

            var tileSize = 40;
            var gridWidth = 11;  // odd is nicer b/c first tile is in centre
            var gridHeight = 11;
            var canvas = yoob.makeCanvas(
                container, tileSize * gridWidth, tileSize * gridHeight
            );    

            (new WangTiler()).init({
                'canvas': canvas,
                'tileSize': tileSize,
                'gridWidth': gridWidth,
                'gridHeight': gridHeight
            });
        };
        document.body.appendChild(elem);
    }
}

// Clockwise starting from top.

var NORTH = 0;
var EAST = 1;
var SOUTH = 2;
var WEST = 3;

var DELTA = [
    [0, -1],
    [1, 0],
    [0, 1],
    [-1, 0]
];

// R = Red, G = Green, B = Blue, Y = Yellow, K = Grey

var TILES = [
    "GGBR", // 0
    "GBGR",
    "GBBG",
    "RRGG",
    "RRBB",
    "RGGB",
    "YYRY",
    "BYGY",
    "GKRY",
    "GKYY",
    "YKRK",
    "BKGK",
    "GYGK", // 12
];

var COLOURS = {
    'R': '#ff0000',
    'G': '#00ff00',
    'Y': '#ffff00',
    'B': '#0000ff',
    'K': '#808080'
};

var YES = function(t) {
    return true;
};

var Tile = function() {
    this.init = function(cfg) {
        this.x = cfg.x;                         // grid position in playfield
        this.y = cfg.y;                         //   (mostly for backtracking)
        this.possibilities = cfg.possibilities; // list of indices into TILES
        this.prev = cfg.prev;                   // Tile | null
        this.choice = 0;                        // index into possibilities
        this.type = this.possibilities[this.choice];
        return this;
    };

    /*
     * Try the next possibility for this Tile (used during
     * backtracking.)  Returns false if the possibilities
     * for this Tile have been exhausted.
     */
    this.retry = function() {
        this.choice++;
        if (this.choice >= this.possibilities.length) {
            return false;
        }
        this.type = this.possibilities[this.choice];
        return true;        
    };

    /*
     * Draw this Tile in a drawing context.
     * (x, y) is the top-left corner, given in context-units.
     */
    this.draw = function(ctx, x, y, w, h) {
        var td = TILES[this.type];

        /*
         * If there're going to be hairline cracks between the
         * triangles anyway, they look better in black.
         */
        ctx.fillStyle = 'black';
        ctx.fillRect(x, y, w, h);

        ctx.beginPath();
        ctx.fillStyle = COLOURS[td.charAt(NORTH)];
        ctx.moveTo(x, y);
        ctx.lineTo(x + w/2, y + h/2);
        ctx.lineTo(x + w, y);
        ctx.closePath();
        ctx.fill();

        ctx.beginPath();
        ctx.fillStyle = COLOURS[td.charAt(EAST)];
        ctx.moveTo(x + w, y);
        ctx.lineTo(x + w/2, y + h/2);
        ctx.lineTo(x + w, y + h);
        ctx.closePath();
        ctx.fill();

        ctx.beginPath();
        ctx.fillStyle = COLOURS[td.charAt(SOUTH)];
        ctx.moveTo(x + w, y + h);
        ctx.lineTo(x + w/2, y + h/2);
        ctx.lineTo(x, y + h);
        ctx.closePath();
        ctx.fill();

        ctx.beginPath();
        ctx.fillStyle = COLOURS[td.charAt(WEST)];
        ctx.moveTo(x, y + h);
        ctx.lineTo(x + w/2, y + h/2);
        ctx.lineTo(x, y);
        ctx.closePath();
        ctx.fill();

        ctx.strokeStyle = 'black';
        ctx.lineWidth = 0.5;
        ctx.strokeRect(x, y, w, h);
    };
};

var WangTiler = function() {
    var canvas;
    var ctx;

    var tileSize;
    var pf;
    var t;
    var gridW, gridH;
    var cx, cy;
    var wait;

    /*
     * Find the set of possibilities that fit into cell (x, y)
     * in the playfield, and returns it.
     */
    this.possibilitiesForTile = function(x, y) {
        var n = pf.get(x, y - 1);
        var e = pf.get(x + 1, y);
        var s = pf.get(x, y + 1);
        var w = pf.get(x - 1, y);

        var predN = n ? function(t) {
            return t.charAt(NORTH) === TILES[n.type].charAt(SOUTH);
        } : YES;
        var predE = e ? function(t) {
            return t.charAt(EAST) === TILES[e.type].charAt(WEST);
        } : YES;
        var predS = s ? function(t) {
            return t.charAt(SOUTH) === TILES[s.type].charAt(NORTH);
        } : YES;
        var predW = w ? function(t) {
            return t.charAt(WEST) === TILES[w.type].charAt(EAST);
        } : YES;

        var possibilities = [];
        for (var i = 0; i < TILES.length; i++) {
            var t = TILES[i];
            if (predN(t) && predE(t) && predS(t) && predW(t)) {
                possibilities.push(i);
            }
        }

        return possibilities;
    };

    /*
     * Assuming the last-placed tile is at (x, y), determine where the
     * next-placed tile shall be.
     */
    this.moveAlong = function(x, y) {
        // find tile's neighbours
        var n = pf.get(x, y - 1);
        var ne = pf.get(x + 1, y - 1);
        var e = pf.get(x + 1, y);
        var se = pf.get(x + 1, y + 1);
        var s = pf.get(x, y + 1);
        var sw = pf.get(x - 1, y + 1);
        var w = pf.get(x - 1, y);
        var nw = pf.get(x - 1, y - 1);

        // pick a direction
        var dx, dy;

        // if there is a tile to the E, or if there is a tile to the NE but not the N, go N.
        // if there is a tile to the S, or if there is a tile to the SE but not the E, go E.
        // if there is a tile to the W, or if there is a tile to the SW but not the S, go S.
        // if there is a tile to the N, or if there is a tile to the NW but not the W, go W.
        // else just go east and we'll figure out the rest later.
        if ((e && !n) || (ne && !n)) {
            dx = 0;
            dy = -1;
        } else if ((s && !e) || (se && !e)) {
            dx = 1;
            dy = 0;
        } else if ((w && !s) || (se && !s)) {
            dx = 0;
            dy = 1;
        } else if ((n && !w) || (nw && !w)) {
            dx = -1;
            dy = 0;
        } else {
            dx = 1;
            dy = 0;
        }

        if (pf.get(x + dx, y + dy)) {
            alert("We messed up navigation somehow!")
        }

        return [dx, dy];
    };

    /*
     * Update the state of the tiling -- move to the next position and
     * place a new Tile.  If that turns out to not be possible, backtrack.
     */
    this.update = function() {
        var tile = pf.get(cx, cy);

        var delta = this.moveAlong(cx, cy);
        var nx = cx + delta[0];
        var ny = cy + delta[1];

        var possibilities = this.possibilitiesForTile(nx, ny);
        if (possibilities.length > 0) {

            if (true) {
                var newPossibilities = [];
                while (possibilities.length > 0) {
                    newPossibilities.push(
                        possibilities.splice(
                            Math.random() * possibilities.length, 1
                        )[0]
                    );
                }        
                possibilities = newPossibilities;
            }

            var newTile = (new Tile()).init({
                'x': nx,
                'y': ny,
                'possibilities': possibilities,
                'prev': tile
            });
            cx = nx;
            cy = ny;
            pf.put(cx, cy, newTile);
        } else {
            // backtrack
            var done = false;
            while (!done) {
                if (tile.retry()) {
                    done = true;
                } else {
                    // remove tile from playfield, and backtrack further
                    pf.put(tile.x, tile.y, null);
                    tile = tile.prev;
                    cx = tile.x;
                    cy = tile.y;
                }
            }
        }
    };

    /*
     * Draw the current state of the tiling on the canvas.
     */
    this.draw = function() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        pf.foreach(function(x, y, tile) {
            tile.draw(ctx, x * tileSize, y * tileSize, tileSize, tileSize);
        });
    };

    this.init = function(cfg) {
        canvas = cfg.canvas;
        ctx = canvas.getContext('2d');

        tileSize = cfg.tileSize || 20;
        pf = (new yoob.Playfield()).setDefault(null);
        wait = 0;

        gridW = cfg.gridWidth || 20;
        gridH = cfg.gridHeight || 20;
        cx = Math.trunc(gridW / 2);
        cy = Math.trunc(gridH / 2);

        var possibilities = this.possibilitiesForTile(cx, cy);
        if (possibilities.length === 0) {
            alert('wat');
        }
        var initTile = (new Tile()).init({
            'x': cx,
            'y': cy,
            'possibilities': possibilities
        });
        pf.put(cx, cy, initTile);

        this.animation = (new yoob.Animation()).init({
            'object': this
        });
        this.animation.start();
    };
};