xxxxxxxxxx
320
/**
* A 2D region for performimg cellular
*/
class Grid {
/**
* Create a life-grid where the valid position coordinates are
* X: 1 to width inclusive
* Y: 1 to height inclusive
* No error cheking is performed on coordinate parametrs
*
* @param {*} width the number of horizontal cells in the grid
* @param {*} height the number of vertical cells in the grid
* @param {*} horz_wrap whether the cells wrap horizontally
* @param {*} vert_wrap whether the cells wrap vertically
*/
constructor(width, height, horz_wrap = true, vert_wrap = true) {
// The working area
this.w = width;
this.h = height;
// The grid array
this.aw = this.w + 2;
this.ah = this.h + 2;
// Can wrap with cells on opposite side
this.wrapX = horz_wrap;
this.wrapY = vert_wrap;
// The iteration area depends on wrap values
this.left = this.wrapX ? 0 : 1;
this.right = this.wrapX ? this.aw - 1 : this.w;
this.top = this.wrapY ? 0 : 1;
this.bottom = this.wrapY ? this.ah - 1 : this.h;
// The generation rule
this.rule = new DefaultRule();
this.changes = [];
this.nbrAlive = 0;
// Run attributes
this.nbrAlive;
this.gps = 20; // number of generations per second
this.siIdx0 = -1;
// Grid arrays
// array buffer element byte width
let bufferBW = 32;
let buffer = new ArrayBuffer(bufferBW * this.aw * this.ah);
console.log(`Buffer element byte width = ${bufferBW} Buffer size ${buffer.byteLength}`);
// cell view array
this.cell = new Uint32Array(buffer);
this.cellBW = bufferBW / 4;
// type view array
this.type = new Uint8Array(buffer);
this.typeBW = bufferBW / 1;
// neighbour view array
this.nbor = new Uint8Array(buffer);
this.nborBW = bufferBW / 1;
// next type view array
this.next = new Uint16Array(buffer);
this.nextBW = bufferBW / 2;
}
getCell(x, y) { return this.cell[this.cellBW * (x + y * this.aw)]; }
getType(x, y) { return this.type[this.typeBW * (x + y * this.aw)]; }
getNbor(x, y) { return this.nbor[this.nborBW * (x + y * this.aw) + 1]; }
getNext(x, y) { return this.next[this.nextBW * (x + y * this.aw) + 1]; }
getCellIndex(x, y) { return this.cellBW * (x + y * this.aw); }
getTypeIndex(x, y) { return this.typeBW * (x + y * this.aw); }
getNborIndex(x, y) { return this.nborBW * (x + y * this.aw) + 1; }
getNextIndex(x, y) { return this.nextBW * (x + y * this.aw) + 1; }
set speed(s) { this.gps = s; }
get speed() { return this.gps; }
get width() { return this.w; }
get height() { return this.h; }
/** Clears the grd of all life */
clear() {
this.cell.fill(0);
return this;
}
/**
* Adds a life to a given location in the grid.
*
* @param {*} x valid range is 1 - w inclusive
* @param {*} y valid range is 1 - h inclusive
* @param {*} type valid range is 1 - 255 inclusive
* @returns this grid;
*/
addLife(x, y, type = 1) {
if (this.getType(x, y) > 0) return; // life already exists
type = type > 0 ? type : 1; // safety
this._birth(x, y, type);
if (this.wrapX && (x == 1 || x == this.w)) {
let xx = x == 1 ? this.w + 1 : 0;
this._birth(xx, y, type);
}
if (this.wrapY && (y == 1 || y == this.h)) {
let yy = y == 1 ? this.h + 1 : 0;
this._birth(x, yy, type);
}
this._wrap();
return this;
}
/**
* Deletes a life to a given location in the grid.
*
* @param {*} x valid range is 1 - w inclusive
* @param {*} y valid range is 1 - h inclusive
* @returns this grid;
*/
delLife(x, y) {
if (this.getType(x, y) == 0) return; // dead already
this._death(x, y);
if (this.wrapX && (x == 1 || x == this.w)) {
let xx = x == 1 ? this.w + 1 : 0;
this._death(xx, y);
}
if (this.wrapY && (y == 1 || y == this.h)) {
let yy = y == 1 ? this.h + 1 : 0;
this._death(x, yy);
}
this._wrap();
return this;
}
/**
* Adds a like pattern to the grid. It is the users responsibility to
* ensure that all the life-cell coordinates fit inside the valid
* coordiates ranges.
* @param {*} x valid range is 1 - w inclusive
* @param {*} y valid range is 1 - h inclusive
* @param {*} pattern the pattern to be added
* @param {*} type the life type valid range 1 - 255 incl to be added
* @param {*} copyRule if true then use the pattern rule for life evaluation
* @returns this grid;
*/
addPattern(x, y, pattern, type = 1, copyRule = false) {
pattern.points.forEach(point => {
this.addLife(x + point.x, y + point.y, type);
});
if (copyRule) this.rule = pattern.rule;
return this;
}
animate(speed = this.gps) {
this.gps = speed;
clearInterval(this.siIdx0);
this.siIdx0 = setInterval(this.step.bind(this), Math.round(1000 / this.gps));
return this;
}
stop() {
clearInterval(this.siIdx0);
this.siIdx0 = -1;
return this;
}
step(repeats = 1, left = this.left, right = this.right, top = this.top, bottom = this.bottom) {
for (let i = 0; i < repeats; i++)
this._doIteration();
}
/** Calculates the type generated when a cell is 'born' */
calcNextType(next, nbr) {
return 1;
}
_doIteration(left = this.left, right = this.right, top = this.top, bottom = this.bottom) {
this.changes = [];
for (let y = top; y <= bottom; y++) {
for (let x = left; x <= right; x++) {
let type = this.getType(x, y);
let nbor = this.getNbor(x, y);
let next = this.getNext(x, y);
if (this.rule.apply(type, nbor)) {
// nextType = 0 for death else calculate it based on 'next count' and
// the number of neighbours
let nextType = type != 0 ? 0 : this.calcNextType(nbor, next);
this.changes.push(new GridCellChange(x, y, type, nextType, nbor));
}
}
}
this.changes.forEach(p => {
if (p.nextType > 0)
this._birth(p.x, p.y, p.nextType);
else
this._death(p.x, p.y);
});
this._wrap();
}
/** Used internally when calculating next generation. */
_birth(x, y, type = 1) {
this.nbrAlive++;
this.type[this.getTypeIndex(x, y)] = type;
this._changeNeighbors(x, y, 1);
this._changeNext(x, y, type);
}
/** Used internally when calculating next generation. */
_death(x, y) {
this.nbrAlive--;
let type = this.getType(x, y);
this._changeNeighbors(x, y, -1);
this._changeNext(x, y, -type);
this.type[this.getTypeIndex(x, y)] = 0;
}
_changeNeighbors(x, y, n) {
let idx = this.getNborIndex(x, y)
let dx = this.nborBW, dy = dx * this.aw;
this.nbor[idx - dy - dx] += n; // NW
this.nbor[idx - dy] += n; // N
this.nbor[idx - dy + dx] += n; // NE
this.nbor[idx - dx] += n; // W
this.nbor[idx + dx] += n; // E
this.nbor[idx + dy - dx] += n; // SW
this.nbor[idx + dy] += n; // S
this.nbor[idx + dy + dx] += n; // SE
}
_changeNext(x, y, tn) {
let idx = this.getNextIndex(x, y);
let dx = this.nextBW, dy = dx * this.aw;
this.next[idx - dy - dx] += tn; // NW
this.next[idx - dy] += tn; // N
this.next[idx - dy + dx] += tn; // NE
this.next[idx - dx] += tn; // W
this.next[idx + dx] += tn; // E
this.next[idx + dy - dx] += tn; // SW
this.next[idx + dy] += tn; // S
this.next[idx + dy + dx] += tn; // SE
}
_wrap() {
if (this.wrapX || this.wrapY) {
let a = this.cell, w = this.w, h = this.h;
let dx = this.cellBW, dy = this.cellBW * this.aw;
let fromIdx, toIdx;
if (this.wrapX) {
// W >> E
fromIdx = this.getCellIndex(1, 1);
toIdx = this.getCellIndex(w + 1, 1);
for (let i = 0; i < w; i++)
a[toIdx + i * dy] = a[fromIdx + i * dy];
// E >> W
fromIdx = this.getCellIndex(w, 1);
toIdx = this.getCellIndex(0, 1);
for (let i = 0; i < w; i++)
a[toIdx + i * dy] = a[fromIdx + i * dy];
}
if (this.wrapY) {
// N >> S
fromIdx = this.getCellIndex(1, 1);
toIdx = this.getCellIndex(1, h + 1);
for (let i = 0; i < w; i++)
a[toIdx + dx * i] = a[fromIdx + dx * i];
// S >> N
fromIdx = this.getCellIndex(1, h);
toIdx = this.getCellIndex(1, 0);
for (let i = 0; i < w; i++)
a[toIdx + dx * i] = a[fromIdx + dx * i];
}
if (this.wrapX && this.wrapY) {
a[0] = a[this.getCellIndex(w, h)]; // NW
a[this.getCellIndex(w + 1, 0)] = a[this.getCellIndex(1, h)]; // NE
a[this.getCellIndex(0, h + 1)] = a[this.getCellIndex(w, 1)]; // SW
a[this.getCellIndex(w + 1, h + 1)] = a[this.getCellIndex(1, 1)]; // SE
}
}
}
}
/**
* Represents a postion in a 2D grid.
* @author Peter Lager 2024
*/
class GridCell {
constructor(x, y) {
this._x = x; this._y = y;
}
set x(v) { this._x = v };
get x() { return this._x };
set y(v) { this._y = v };
get y() { return this._y };
copy() {
return new GridCell(this._x, this._y);
}
toString() {
return `[${this._x}, ${this._y}]`;
}
}
/**
* Represents a cell life-status change found when performing a
* generation iteration.
*/
class GridCellChange extends GridCell {
constructor(x, y, type, ntype, nbors) {
super(x, y);
this._type = type;
this._ntype = ntype;
this._nbors = nbors;
}
get type() { return this._type; }
get nextType() { return this._ntype; }
get nbors() { return this._nbors; }
toString() {
return `[${this._x}, ${this._y}] Type: ${this._type} Next type: ${this._ntype} Nbors: ${this._nbors}`;
}
}