git @ Cat's Eye Technologies Backtracking-Wang-Tiler / master src / yoob / playfield.js
master

Tree @master (Download .tar.gz)

playfield.js @masterraw · history · blame

/*
 * This file is part of yoob.js version 0.6
 * Available from https://github.com/catseye/yoob.js/
 * This file is in the public domain.  See http://unlicense.org/ for details.
 */
if (window.yoob === undefined) yoob = {};

/*
 * A two-dimensional Cartesian grid of values.
 */
yoob.Playfield = function() {
    this._store = {};
    this.minX = undefined;
    this.minY = undefined;
    this.maxX = undefined;
    this.maxY = undefined;
    this._default = undefined;

    /*
     * Set the default value for this Playfield.  This
     * value is returned by get() for any cell that was
     * never written to, or had `undefined` put() into it.
     */
    this.setDefault = function(v) {
        this._default = v;
        return this;
    };

    /*
     * Obtain the value at (x, y).  The default value will
     * be returned if the cell was never written to.
     */
    this.get = function(x, y) {
        var v = this._store[x+','+y];
        if (v === undefined) return this._default;
        return v;
    };

    /*
     * Write a new value into (x, y).  Note that writing
     * `undefined` into a cell has the semantics of deleting
     * the value at that cell; a subsequent get() for that
     * location will return this Playfield's default value.
     */
    this.put = function(x, y, value) {
        var key = x+','+y;
        if (value === undefined || value === this._default) {
            delete this._store[key];
            return;
        }
        if (this.minX === undefined || x < this.minX) this.minX = x;
        if (this.maxX === undefined || x > this.maxX) this.maxX = x;
        if (this.minY === undefined || y < this.minY) this.minY = y;
        if (this.maxY === undefined || y > this.maxY) this.maxY = y;
        this._store[key] = value;
    };

    /*
     * Like put(), but does not update the playfield bounds.  Do
     * this if you must do a batch of put()s in a more efficient
     * manner; after doing so, call recalculateBounds().
     */
    this.putDirty = function(x, y, value) {
        var key = x+','+y;
        if (value === undefined || value === this._default) {
            delete this._store[key];
            return;
        }
        this._store[key] = value;
    };

    /*
     * Recalculate the bounds (min/max X/Y) which are tracked
     * internally to support methods like foreach().  This is
     * not needed *unless* you've used putDirty() at some point.
     * (In which case, call this immediately after your batch
     * of putDirty()s.)
     */
    this.recalculateBounds = function() {
        this.minX = undefined;
        this.minY = undefined;
        this.maxX = undefined;
        this.maxY = undefined;

        for (var cell in this._store) {
            var pos = cell.split(',');
            var x = parseInt(pos[0], 10);
            var y = parseInt(pos[1], 10);
            if (this.minX === undefined || x < this.minX) this.minX = x;
            if (this.maxX === undefined || x > this.maxX) this.maxX = x;
            if (this.minY === undefined || y < this.minY) this.minY = y;
            if (this.maxY === undefined || y > this.maxY) this.maxY = y;
        }
    };

    /*
     * Clear the contents of this Playfield.
     */
    this.clear = function() {
        this._store = {};
        this.minX = undefined;
        this.minY = undefined;
        this.maxX = undefined;
        this.maxY = undefined;
    };

    /*
     * Scroll a rectangular subrectangle of this Playfield, up.
     * TODO: support other directions.
     */
    this.scrollRectangleY = function(dy, minX, minY, maxX, maxY) {
        if (dy < 1) {
            for (var y = minY; y <= (maxY + dy); y++) {
                for (var x = minX; x <= maxX; x++) {
                    this.put(x, y, this.get(x, y - dy));
                }
            }
        } else { alert("scrollRectangleY(" + dy + ") notImplemented"); }
    };

    this.clearRectangle = function(minX, minY, maxX, maxY) {
        // Could also do this with a foreach that checks
        // each position.  Would be faster on sparser playfields.
        for (var y = minY; y <= maxY; y++) {
            for (var x = minX; x <= maxX; x++) {
                this.put(x, y, undefined);
            }
        }
    };

    /*
     * Load a string into this Playfield.
     * The string may be multiline, with newline (ASCII 10)
     * characters delimiting lines.  ASCII 13 is ignored.
     *
     * If transformer is given, it should be a one-argument
     * function which accepts a character and returns the
     * object you wish to write into the playfield upon reading
     * that character.
     */
    this.load = function(x, y, string, transformer) {
        var lx = x;
        var ly = y;
        if (transformer === undefined) {
            transformer = function(c) {
                if (c === ' ') {
                    return undefined;
                } else {
                    return c;
                }
            }
        }
        for (var i = 0; i < string.length; i++) {
            var c = string.charAt(i);
            if (c === '\n') {
                lx = x;
                ly++;
            } else if (c === '\r') {
            } else {
                this.putDirty(lx, ly, transformer(c));
                lx++;
            }
        }
        this.recalculateBounds();
    };

    /*
     * Convert this Playfield to a multi-line string.  Each row
     * is a line, delimited with a newline (ASCII 10).
     *
     * If transformer is given, it should be a one-argument
     * function which accepts a playfield element and returns a
     * character (or string) you wish to place in the resulting
     * string for that element.
     */
    this.dump = function(transformer) {
        var text = "";
        if (transformer === undefined) {
            transformer = function(c) { return c; }
        }
        for (var y = this.minY; y <= this.maxY; y++) {
            var row = "";
            for (var x = this.minX; x <= this.maxX; x++) {
                row += transformer(this.get(x, y));
            }
            text += row + "\n";
        }
        return text;
    };

    /*
     * Iterate over every defined cell in the Playfield.
     * fun is a callback which takes three parameters:
     * x, y, and value.  If this callback returns a value,
     * it is written into the Playfield at that position.
     * This function ensures a particular order.
     */
    this.foreach = function(fun) {
        for (var y = this.minY; y <= this.maxY; y++) {
            for (var x = this.minX; x <= this.maxX; x++) {
                var key = x+','+y;
                var value = this._store[key];
                if (value === undefined)
                    continue;
                var result = fun(x, y, value);
                if (result !== undefined) {
                    if (result === ' ') {
                        result = undefined;
                    }
                    this.put(x, y, result);
                }
            }
        }
    };

    /*
     * Analogous to (monoid) map in functional languages,
     * iterate over this Playfield, transform each value using
     * a supplied function, and write the transformed value into
     * a destination Playfield.
     *
     * Supplied function should take a Playfield (this Playfield),
     * x, and y, and return a value.
     *
     * The map source may extend beyond the internal bounds of
     * the Playfield, by giving the min/max Dx/Dy arguments
     * (which work like margin offsets.)
     *
     * Useful for evolving a cellular automaton playfield.  In this
     * case, min/max Dx/Dy should be computed from the neighbourhood.
     */
    this.map = function(destPf, fun, minDx, minDy, maxDx, maxDy) {
        if (minDx === undefined) minDx = 0;
        if (minDy === undefined) minDy = 0;
        if (maxDx === undefined) maxDx = 0;
        if (maxDy === undefined) maxDy = 0;
        for (var y = this.minY + minDy; y <= this.maxY + maxDy; y++) {
            for (var x = this.minX + minDx; x <= this.maxX + maxDx; x++) {
                destPf.putDirty(x, y, fun(pf, x, y));
            }
        }
        destPf.recalculateBounds();
    };

    /*
     * Accessors for the minimum (resp. maximum) x (resp. y) values of
     * occupied (non-default-valued) cells in this Playfield.  If there are
     * no cells in this Playfield, these will refturn undefined.  Note that
     * these are not guaranteed to be tight bounds; if values in cells
     * are deleted, these bounds may still be considered to be outside them.
     */
    this.getMinX = function() {
        return this.minX;
    };
    this.getMaxX = function() {
        return this.maxX;
    };
    this.getMinY = function() {
        return this.minY;
    };
    this.getMaxY = function() {
        return this.maxY;
    };

    /*
     * Returns the number of occupied cells in the x direction.
     */
    this.getExtentX = function() {
        if (this.maxX === undefined || this.minX === undefined) {
            return 0;
        } else {
            return this.maxX - this.minX + 1;
        }
    };

    /*
     * Returns the number of occupied cells in the y direction.
     */
    this.getExtentY = function() {
        if (this.maxY === undefined || this.minY === undefined) {
            return 0;
        } else {
            return this.maxY - this.minY + 1;
        }
    };
};