API Docs for:
Show:

File: js/conway.js

/**
 * A simulator for Conway's Game of Life.
 * @module gallery-conway
 */
(function (Y, moduleName) {
    'use strict';

    var _null = null,
        _string_cellChange = 'cellChange',
        _string_grid = 'grid',
        _string_height = 'height',
        _string_initOnly = 'initOnly',
        _string_width = 'width',
        _true = true,

        _Array = Y.Array,
        _Async = Y.Async,
        _Base = Y.Base,
        _Lang = Y.Lang,
        _Math = Math,

        _each = Y.each,
        _filter = _Array.filter,
        _floor = _Math.floor,
        _indexOf = _Array.indexOf,
        _isArray = _Lang.isArray,
        _isFunction = _Lang.isFunction,
        _isValue = _Lang.isValue,
        _makeItAnArray = function (array) {
            return _isArray(array) ? array : [
                array
            ];
        },
        _max = _Math.max,
        _min = _Math.min,
        _soon = Y.soon;

    /**
     * @class Conway
     * @constructor
     * @extends Base
     * @param {Object} config Configuration Object.
     */
    Y.Conway = _Base.create(moduleName, _Base, [], {
        destructor: function () {
            delete this._history;
        },
        /**
         * Returns the value of a specific cell.  The cell may be specified with
         * x and y coordinates or with a single array index.
         * (index = x + y * width)
         * @method getCell
         * @param {Number} x The x coordinate or the array index.
         * @param {Number} [y] Optional.  The y coordinate.
         * @return {Boolean} A truthy value means the cell is currently alive.
         */
        getCell: function (x, y) {
            return this.get(_string_grid)[_isValue(y) ? x + y * this.get(_string_width) : x];
        },
        initializer: function () {
            var me = this,

                grid = me.get(_string_grid),
                i = 0,
                length = me.get(_string_height) * me.get(_string_width);

            /**
             * If this event is prevented, the cell's value will not be changed.
             * @event cellChange
             * @param {Boolean} cell The current value of the cell.  Note that
             * the cell's value is changed by the default function so the value
             * of this property will be different between on and after
             * subscriptions.
             * @param {Number} height The current height of the grid.
             * @param {Number} index The cell's array index.
             * @param {Boolean} toroidal True if the grid is currently toroidal.
             * @param {Number} width The current width of the grid.
             * @param {Number} x The cell's x coordinate.
             * @param {Number} y The cell's y coordinate.
             */
            me.publish(_string_cellChange, {
                defaultFn: me._cellChange
            });

            while (i < length) {
                grid.push(false);
                i += 1;
            }

            /**
             * This object's keys are the indices of cells that have changed
             * value since the previous step of the simulation.  This is used to
             * improve performance while resolving the next step of the
             * simulation.
             * @property _history
             * @protected
             * @type Object
             */
            me._history = {};
        },
        /**
         * Sets the value of a specific cell.  The cell may be specified with x
         * and y coordinates or with a single array index.
         * (index = x + y * width)
         * @method setCell
         * @chainable
         * @param {Number} x The x coordinate orthe array index.
         * @param {Number} [y] Optional.  The y coordinate.
         * @param {Boolean} value The new value for the cell.  A truthy value means
         * the cell is currently alive.
         */
        setCell: function (x, y, value) {
            var cell,
                index,
                me = this,
                width = me.get(_string_width);

            if (_isValue(value)) {
                index = x + y * width;
            } else {
                index = x;
                value = y;
                x = index % width;
                y = _floor(index / width);
            }

            cell = me.get(_string_grid)[index];

            if (cell !== value) {
                me.fire(_string_cellChange, {
                    cell: cell,
                    height: me.get(_string_height),
                    index: index,
                    toroidal: me.get('toroidal'),
                    width: width,
                    x: x,
                    y: y
                });
            }

            return me;
        },
        /**
         * Runs one step of the simulation.  The simulation is resolved
         * asynchronously and will call the callback function when it completes.
         * Once this method is called, it should not be called again until the
         * first call is complete.  Calling getCell, setCell, or step while the
         * simulation is still being resolved may cause unexpected side effects.
         * If you wish to ensure that unexpected side effects never occur and
         * don't mind incurring a small overhead cost, use gallery-mutex to
         * obtain an exclusive lock when calling these methods.
         * @method step
         * @chainable
         * @param {Function} [callbackFunction] Optional.  This function will be
         * called once the simulation is resolved.  This function will be passed
         * a single argument, an array.  For each cell that changed value during
         * the simulation, there will be an object in the array with the
         * following properties:
         * <dl>
         *     <dt>
         *          cell
         *     </dt>
         *     <dd>
         *         Boolean.  The current value of the cell.
         *     </dd>
         *     <dt>
         *         index
         *     </dt>
         *     <dd>
         *         Number.  The cell's array index.
         *     </dd>
         *     <dt>
         *         x
         *     </dt>
         *     <dd>
         *         Number.  The cell's x coordinate.
         *     </dd>
         *     <dt>
         *         y
         *     </dt>
         *     <dd>
         *         Number.  The cell's y coordinate.
         *     </dd>
         * </dl>
         */
        step: function (callbackFunction) {
            var me = this,

                attributes = me.getAttrs(),
                birth = attributes.birth,
                grid = attributes.grid.slice(),
                height = attributes.height,
                history = me._history,
                life = attributes.life,
                run = [],
                toroidal = attributes.toroidal,
                width = attributes.width;

            _each(history, function (value, index) {
                run.push(function (success) {
                    _soon(function () {
                        success(me._cellStep(grid, height, width, index, birth, life, toroidal));
                    });
                });
            });

            _Async.runQueue(run).on('complete', function (eventFacade) {
                var value = _filter(eventFacade.value, function (value) {
                    if (value) {
                        me.fire(_string_cellChange, {
                            cell: value.cell,
                            height: height,
                            index: value.index,
                            toroidal: toroidal,
                            width: width,
                            x: value.x,
                            y: value.y
                        });
                    }

                    return value;
                });

                if (_isFunction(callbackFunction)) {
                    callbackFunction(value);
                }
            });

            me._history = {};

            return me;
        },
        /**
         * Default function for the cellChange event.
         * @method _cellChange
         * @chainable
         * @param {Object} eventFacade
         * @protected
         */
        _cellChange: function (eventFacade) {
            var me = this,

                cell = !eventFacade.cell,
                height = eventFacade.height,
                history = me._history,
                i,
                iEdge,
                j,
                jEdge,
                width = eventFacade.width,
                x = eventFacade.x,
                y = eventFacade.y,
                yEdge;

            me.get(_string_grid)[eventFacade.index] = cell;
            eventFacade.cell = cell;

            if (eventFacade.toroidal) {
                for (j = y - 1; j <= y + 1; j += 1) {
                    yEdge = (j < 0 ? height - 1 : j >= height ? 0 : j) * width;

                    for (i = x - 1; i <= x + 1; i += 1) {
                        history[(i < 0 ? width - 1 : i >= width ? 0 : i) + yEdge] = _true;
                    }
                }
            } else {
                iEdge = _min(x + 1, width - 1);
                jEdge = _min(y + 1, height - 1);

                for (j = _max(y - 1, 0); j <= jEdge; j += 1) {
                    yEdge = j * width;

                    for (i = _max(x - 1, 0); i <= iEdge; i += 1) {
                        history[i + yEdge] = _true;
                    }
                }
            }

            return me;
        },
        /**
         * Determines the outcome of one step of the simulation for one cell.
         * @method _cellStep
         * @param {[Boolean]} grid
         * @param {Number} height
         * @param {Number} width
         * @param {Number} index
         * @param {[Number]} birth
         * @param {[Number]} life
         * @param {Boolean} toroidal
         * @protected
         * @return {Object} If the cell changed, returns an object with the
         * following properties:
         * <dl>
         *     <dt>
         *          cell
         *     </dt>
         *     <dd>
         *         Boolean.  The current value of the cell.
         *     </dd>
         *     <dt>
         *         index
         *     </dt>
         *     <dd>
         *         Number.  The cell's array index.
         *     </dd>
         *     <dt>
         *         x
         *     </dt>
         *     <dd>
         *         Number.  The cell's x coordinate.
         *     </dd>
         *     <dt>
         *         y
         *     </dt>
         *     <dd>
         *         Number.  The cell's y coordinate.
         *     </dd>
         * </dl>
         * otherwise returns null.
         */
        _cellStep: function (grid, height, width, index, birth, life, toroidal) {
            var cell = grid[index],
                i,
                iEdge,
                j,
                jEdge,
                neighbors = 0,
                x = index % width,
                y = _floor(index / width),
                yEdge;

            if (toroidal) {
                for (j = y - 1; j <= y + 1; j += 1) {
                    yEdge = (j < 0 ? height - 1 : j >= height ? 0 : j) * width;

                    for (i = x - 1; i <= x + 1; i += 1) {
                        if ((i !== x || j !== y) && grid[(i < 0 ? width - 1 : i >= width ? 0 : i) + yEdge]) {
                            neighbors += 1;
                        }
                    }
                }
            } else {
                iEdge = _min(x + 1, width - 1);
                jEdge = _min(y + 1, height - 1);

                for (j = _max(y - 1, 0); j <= jEdge; j += 1) {
                    yEdge = j * width;

                    for (i = _max(x - 1, 0); i <= iEdge; i += 1) {
                        if ((i !== x || j !== y) && grid[i + yEdge]) {
                            neighbors += 1;
                        }
                    }
                }
            }

            if (cell && _indexOf(life, neighbors) === -1 || !cell && _indexOf(birth, neighbors) !== -1) {
                return {
                    cell: cell,
                    index: index,
                    x: x,
                    y: y
                };
            }

            return _null;
        }
    }, {
        ATTRS: {
            /**
             * In Conway's Game of Life, a cell is born (becomes alive) if it is
             * not currently alive and if it has exactly three living neighbors.
             * This is the default functionality but it can be modified.  For
             * example, if this attribute is set to [1,4,6] a cell will be born
             * if it is not currently alive and if it has exactly one, four, or
             * six living neighbors.
             * @attribute birth
             * @default [3]
             * @type [Number]
             */
            birth: {
                setter: _makeItAnArray,
                value: [
                    3
                ]
            },
            /**
             * The grid of cell values.  The array values should never be
             * manipulated directly; doing so may cause unexpected side effects.
             * @attribute grid
             * @readonly
             * @type [Boolean]
             */
            grid: {
                readOnly: _true,
                value: []
            },
            /**
             * The height of the grid.
             * @attribute height
             * @initonly
             * @type Number
             */
            height: {
                value: _null,
                writeOnce: _string_initOnly
            },
            /**
             * In Conway's Game of Life, a living cell remains alive if it
             * currently has exactly two or three living neighbors.  Otherwise
             * the cell will die either from loneliness or overcrowding.  This
             * is the default functionality but it can be modified.  For
             * example, if this attribute is set to [1,3,5] a living cell
             * will remain alive if it has exactly one, three, or five living
             * neighbors.
             * @attribute life
             * @default [2,3]
             * @type [Number]
             */
            life: {
                setter: _makeItAnArray,
                value: [
                    2,
                    3
                ]
            },
            /**
             * Since the grid is a fixed size, there is a good chance that the
             * simulation will have to interact with the boundaries of the grid.
             * By default, the grid exists on an infinite two-dimensional plane
             * but all cells beyond the limits of the grid are considered to be
             * dead.  When this attribute is set to a truthy value, the universe
             * of the simulation will become a three-dimensional torus instead
             * of a two-dimensional plane.  The grid will represent the entire
             * surface area of the torus.  Basically all this means is, when the
             * grid is toroidal, the left and right edges of the grid will be
             * connected and the top and bottom edges of the grid will be
             * connected, so while there is still a finite area there are no
             * real boundaries.
             * @attribute toroidal
             * @default false
             * @type Boolean
             */
            toroidal: {
                value: false
            },
            /**
             * The width of the grid.
             * @attribute width
             * @initonly
             * @type Number
             */
            width: {
                value: _null,
                writeOnce: _string_initOnly
            }
        }
    });
}(Y, arguments[1]));