|
// ---- Cellular automaton |
|
// |
|
// An agent that contains basic information required |
|
// to implement cellular automaton on a grid. |
|
// The agent.state can be used by apps using the |
|
// library but normally Agent is extended to hold |
|
// values specific to an apps needs. |
|
// Agent.msg is standard place to place data that |
|
// is to be cleared at beginning of each timestep. |
|
// |
|
// An agent contains: |
|
// Cell number, row, and column |
|
// Persistent state associated with this cell |
|
// A message object that resets at start of each generation |
|
class Agent { |
|
constructor(n, r, c) { |
|
this.nbr = n; this.row = r; this.col = c; |
|
this.state = {}; |
|
this.msg = {}; |
|
} |
|
|
|
// MUST define when agent is alive. |
|
// Can be here or in extended class |
|
// (in this case is defined in class Cell - see below) |
|
get alive() { throw 'agent alive getter not defined!'; } |
|
set alive(v) { throw 'agent alive setter not defined!'; } |
|
// Example: |
|
//get alive() { return this.isAlive ? true : false; } |
|
//set alive(v) { this.isAlive = v ? true : false; } |
|
} |
|
|
|
// The world is a grid populated by agents with a edge |
|
// around all sides. |
|
// The 'arena' is the region of play within the edges. |
|
// The neighbor(hood) access is defined by a rectangle |
|
// or a 'region' which is an object of top/left position |
|
// by number of rows/columns. |
|
class World { |
|
constructor(rows, cols, edge = 1, agentType = Agent) { |
|
// Grid containing population of agents |
|
this.agent = []; |
|
|
|
// Access to grid is 'top' and 'left' position |
|
// then number of 'rows' and 'cols'(columns) |
|
// inclusive - absolute from grid 0,0. |
|
|
|
// Origin of region of play within grid |
|
this.origin = {top: edge, left: edge}; |
|
// The complete grid including the boundries |
|
this.grid = {top: 0, left: 0, rows: rows+(edge*2), cols: cols+(edge*2)}; |
|
// The edge regions |
|
this.edge = { |
|
top: { top: 0, left: 0, rows: edge, cols: cols+(edge*2) }, |
|
right: { top: edge, left: cols+edge, rows: rows, cols: edge }, |
|
bottom: { top: rows+edge, left: 0, rows: edge, cols: cols+(edge*2) }, |
|
left: { top: edge, left: 0, rows: rows, cols: edge }, |
|
}; |
|
|
|
// The region of play is inside the edge |
|
this.arena = {top: edge, left: edge, rows: rows, cols: cols }; |
|
|
|
// Access to neighbors is 'top' and 'left' position |
|
// and number of 'rows' and 'cols'(columns) |
|
// inclusive - relative to requested cell(agent). |
|
|
|
// Define region of two most common neighborhood types: |
|
// The default region of neighbors - relative to agent |
|
// 3x3 region - eight neighbors - Conway Game of Life |
|
this.conwayNeighbors = {top:-1, left:-1, rows:3, cols:3}; |
|
// Elementary region of neighbors - relative to agent |
|
// 1x3 region - two neighbors - (Stephen) Wolfram Code |
|
this.wolframNeighbors = {top:0, left:-1, rows:1, cols:3}; |
|
|
|
// Default neighbors region - conways - app can set |
|
this.neighborsRegion = this.conwayNeighbors; |
|
|
|
|
|
// A continuous sequence of timesteps are running |
|
this.isRunning = false; |
|
this.stepWait = 0; // millisec wait between timesteps |
|
this.stepTimeout = null; // setTimeout timer |
|
|
|
// Phases of cellular automaton |
|
// Functions pushed onto the appropriate phase are |
|
// executed in sequence |
|
this.on = { |
|
initialize: [], // Run world construction and/or reset |
|
// Steps run each timestep: |
|
prepare: [], // Gather data for computing new state |
|
compute: [], // Compute new state |
|
resolve: [], // Apply new state to world |
|
finalize: [] // Display/store generation results |
|
}; |
|
|
|
// Create grid of agents - flag edge agents |
|
this.createAgents(rows, cols, edge, agentType); |
|
|
|
// Resets all agents for restart |
|
this.on.initialize.push(() => { |
|
this.forEachAgent((agent) => { agent.msg = {}; }, this.grid); |
|
}) |
|
|
|
// Resets also at start of timestep |
|
this.on.prepare.push(this.on.initialize[0]); |
|
|
|
} // constructor |
|
|
|
// Execute functions pushed to the phases |
|
automate(phase) { phase.forEach((fn) => fn()); } |
|
|
|
// Convert region relative to agent to an absolute region |
|
regionRelToAbs(agent, region) { return { |
|
top: agent.row+region.top, left: agent.col+region.left, |
|
rows: region.rows, cols: region.cols}; } |
|
|
|
// Instantiate population of agents |
|
createAgents(rows, cols, edge, agentType) { |
|
let n = 0; |
|
for (let r=0; r<rows+(edge*2); r++) { |
|
this.agent[r] = []; |
|
for (let c=0; c<cols+(edge*2); c++) { |
|
this.agent[r][c] = new agentType(n++, r, c); |
|
} |
|
} |
|
this.flagEdges(rows, cols, edge); |
|
} |
|
|
|
// Flag agents out of play - ie: on the edge |
|
flagEdges(rows, cols, edge) { |
|
this.forEachAgent((agent) => { |
|
if (this.isEdge(agent, rows, cols, edge)) { agent.state.isEdge = true; } |
|
}, this.grid); |
|
} |
|
|
|
// Check if an agent is on the edge |
|
isEdge(agent, rows, cols, edge) { |
|
return (agent.row < edge || agent.col < edge || |
|
agent.row >= rows+edge || agent.col >= cols+edge); |
|
} |
|
|
|
// ----------- |
|
|
|
// Rest of the functions are called by the class |
|
// that extends this class (World) |
|
|
|
// Clear the arena |
|
clear() { this.forEachAgent((agent) => agent.alive = false); } |
|
|
|
// Initialize/reset grid |
|
initialize() { this.automate(this.on.initialize); } |
|
|
|
// Run phases of the cellular animation |
|
timestep() { |
|
this.automate(this.on.prepare); |
|
this.automate(this.on.compute); |
|
this.automate(this.on.resolve); |
|
this.automate(this.on.finalize); |
|
if (this.running) { |
|
this.stepTimeout = setTimeout(() => this.timestep(), this.stepWait); |
|
} |
|
} |
|
|
|
// Start timestep sequence |
|
startStepSequence(ms) { |
|
if (this.running) { return; } |
|
this.running = true; |
|
this.timestep(); |
|
} |
|
|
|
// Stop timestep sequence |
|
stopStepSequence(ms) { |
|
if (!this.running) { return; } |
|
this.running = false; |
|
clearInterval(this.stepTimeout); |
|
} |
|
|
|
// Run function fn(agent) for each agent |
|
// in the arena or agents within given absolute region |
|
forEachAgent(fn, absregion) { |
|
let f = absregion || this.arena; |
|
for (let r=f.top, y=f.top+f.rows; r<y; r++) |
|
for (let c=f.left, x=f.left+f.cols; c<x; c++) |
|
fn(this.agent[r][c], this); |
|
} |
|
|
|
// Execute function on agent neighbors |
|
// Region values are relative to the agent |
|
// (note: if agentoo is true the agent is treated |
|
// as a neighbor and function will |
|
// execute against the agent as well) |
|
neighbors(fn, agent, relregion, agentoo) { |
|
this.forEachAgent((nbor) => { |
|
if (!agentoo && nbor.row === agent.row && nbor.col === agent.col) { return; } |
|
fn(nbor); |
|
}, |
|
relregion |
|
? this.regionRelToAbs(agent, relregion) |
|
: this.regionRelToAbs(agent, this.neighborsRegion)); |
|
} |
|
|
|
// Executes two functions on agent neighbors |
|
// first function executes once on the agent |
|
// then second executes for each neighbor |
|
// (note: if agentoo is true the agent is treated |
|
// as a neighbor and second function will |
|
// execute against the agent as well) |
|
neighborhood(agentfn, nborfn, relregion, agentoo) { |
|
this.forEachAgent((agent) => { |
|
agentfn(agent); |
|
this.neighbors((nbor) => nborfn(agent, nbor), agent, relregion, agentoo); |
|
}); |
|
} |
|
} // class World |