agentscape
Version:
Agentscape is a library for creating agent-based simulations. It provides a simple API for defining agents and their behavior, and for defining the environment in which the agents interact. Agentscape is designed to be flexible and extensible, allowing
281 lines • 10.4 kB
JavaScript
import { RandomGenerator } from '../numbers';
import { Animate2D, InfoPane, Inspector } from './ui';
import Controls from '../io/Controls';
/**
* Designates a Model's property as a variable set by a UI control.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function ControlVariable() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
return function (target, propertyKey) {
Object.defineProperty(target, propertyKey, {
get: function () {
return Runtime.controls.getSetting(propertyKey);
},
set: function (value) {
Runtime.controls[propertyKey] = value;
}
});
};
}
/**
* The base class for all model runtimes. A `Runtime` consists of a `CellGrid` and zero or more `AgentSet`.
* The runtime is responsible for updating the simulation's state and rendering the output to the screen.
*
* The runtime's main loop is split into four methods:
* 1. `preUpdate` - runs first, before the model is updated.
* 2. `step` - the model's update function. Calls the `act` method for each agent in the model.
* 3. `render` - renders the cell grid and agents to the screen.
* 4. `postUpdate` - runs last, after the model is rendered.
*
* While each of these methods can be overridden to provide custom behaviors, most needs should be
* met by overriding the `preUpdate` and `postUpdate` methods.
*/
export class Runtime {
constructor(opts) {
this.initialized = false;
this.runCondition = () => true;
this.cellClickHandler = () => { };
this.keydownCallbackMap = {};
this.renderLayers = { grid: true, agents: true };
const { frameRate = 60, autoPlay = true, title = 'Model', } = opts;
this.documentRoot = opts.documentRoot;
this.renderWidth = opts.renderWidth;
this.id = opts.id;
this.title = title;
this.frameRate = frameRate;
this.autoPlay = autoPlay;
this.renderHeight = opts.renderHeight || opts.renderWidth;
if (opts.parameters) {
Runtime.controls = new Controls({
root: opts.documentRoot,
title: 'Controls',
settings: opts.parameters,
id: opts.id + '_default_parameter_controls',
});
}
if (opts.about) {
new InfoPane({
root: opts.documentRoot,
info: opts.about,
id: opts.id
});
}
// insert the app footer
const footer = document.createElement('app-footer');
this.documentRoot.appendChild(footer);
this.rng = new RandomGenerator();
}
/**
* Enables a maximum population for all agents in the model.
* If the population reaches the maximum, agent reproduction is disabled.
*/
setMaxPopulation(population) {
Runtime.maxPopulation = population;
}
/**
* Enables selective rendering of the grid and agent sets.
*/
setRenderLayers(opts) {
var _a, _b;
this.renderLayers = {
grid: (_a = opts.grid) !== null && _a !== void 0 ? _a : this.renderLayers.grid,
agents: (_b = opts.agents) !== null && _b !== void 0 ? _b : this.renderLayers.agents
};
}
/**
* Sets the random seed for the model.
*/
setRandomSeed(seed) {
this.rng = new RandomGenerator(seed);
}
/**
* The model's run condition is a function that returns a boolean.
* The model will continue to run until the run condition returns false.
*/
setRunCondition(condition) {
this.runCondition = condition;
}
/**
* Sets a callback function to be called when a cell is clicked.
* The callback function is passed the `Cell` object that was clicked.
*/
onClick(callback) {
this.cellClickHandler = callback;
}
/**
* Sets a callback function to be called when a key is pressed.
* The key is specified by its code (i.e., `event.code`).
* The callback function is passed the `KeyboardEvent` object.
*
* Optionally, you can specify whether the default behavior should be prevented.
*
* This method can be called multiple times to set multiple keydown callbacks.
*
* Default key behaviors are:
* - Space: toggles play/pause
* - Enter: steps the simulation forward
*
* Defaults may be overridden by setting a new callback for the same key.
*/
onKeydown(code, callback, options) {
this.keydownCallbackMap[code] = {
callback,
options: options !== null && options !== void 0 ? options : { preventDefault: false }
};
}
/**
* Override this method to provide custom update behaviors
* after the model has been updated. Additionally gives
* access to the renderer.
*/
postUpdate() {
return () => { };
}
/**
* Override this method to provide custom behaviors
* before the model has been updated.
*/
preUpdate() {
return () => { };
}
/**
* Starts the simulation. Only needs to be called once.
*/
start() {
if (this.initialized)
return;
this.initialized = true;
this.grid = this.initGrid();
this.agents = this.initAgents();
// for each agent set, add the agents to the grid
Object.values(this.agents).forEach((agentSet) => {
agentSet.forEach((agent) => {
this.grid.insertAgent(agent);
});
});
new Inspector({
root: this.documentRoot,
fetchCell: (pos) => this.grid.getCell(pos),
id: this.id + '_inspector',
});
new Animate2D({
root: this.documentRoot,
renderWidth: this.renderWidth,
renderHeight: this.renderHeight,
worldWidth: this.grid.width,
onCellClick: (location, renderer) => {
const cell = this.grid.getCell(location);
this.cellClickHandler(cell);
this.render(renderer);
this.postUpdate()(0, renderer);
},
keydownCallbackMap: this.keydownCallbackMap,
frameRenderCallback: (tick, renderer) => {
if (!this.runCondition(this)) {
window.dispatchEvent(new CustomEvent('stop'));
console.info('Model has reached end condition.');
return;
}
this.preUpdate()(tick);
this.step(tick);
this.render(renderer);
this.postUpdate()(tick, renderer);
},
autoPlay: this.autoPlay,
frameRate: this.frameRate,
title: this.title,
});
}
/**
* Renders the model to the screen by drawing
* the grid and then all agent sets.
*
* Override this method to provide custom rendering behaviors.
*
* To provide _additional_ render behaviors, use the `postUpdate` method instead.
*/
render(renderer) {
renderer.clear();
// draw the grid
if (this.renderLayers.grid) {
renderer.drawCellGrid(this.grid, {
colorFunction: (cell) => {
return {
fill: cell.color,
stroke: cell.strokeColor
};
},
});
}
// if renderLayers.agents is a map, draw only the agent sets specified.
if (typeof this.renderLayers.agents === 'object') {
Object.entries(this.renderLayers.agents).forEach(([agentSet, shouldRender]) => {
if (shouldRender) {
renderer.drawAgentSet(this.agents[agentSet], {
colorFunction: (agent) => {
return {
fill: agent.color,
stroke: agent.strokeColor
};
},
styleFunction: (agent) => agent.style
});
}
});
// else if renderLayers.agents is a bool, draw everything conditionally
}
else if (this.renderLayers.agents) {
Object.values(this.agents).forEach((agentSet) => {
renderer.drawAgentSet(agentSet, {
colorFunction: (agent) => {
return {
fill: agent.color,
stroke: agent.strokeColor
};
},
styleFunction: (agent) => agent.style
});
});
}
}
/**
* The model's step function. This is where the the `act` method
* for the model's Agents are called and dead agents are removed form their sets.
*
* Override this method to provide custom logic for the model's step function.
*
* To provide _additional_ step behaviors, use the `preUpdate` or `postUpdate` methods instead.
*/
step(tick) {
// cull from sets and the grid agents that have died
this.grid.forEach((cell) => {
cell.tenantAgents.forEach((agent) => {
if (!agent.isAlive) {
// remove dead agent from grid
cell.tenantAgents.delete(agent);
}
});
});
// remove from sets agents that have died
Object.values(this.agents).forEach((agentSet) => {
agentSet.forEach((agent) => {
if (!agent.isAlive) {
agentSet.remove(agent);
}
});
});
// get the current population
Runtime.currentPopulation = Object.values(this.agents).reduce((acc, agentSet) => {
return acc + agentSet.size;
}, 0);
// call the step function for each agent set
Object.values(this.agents).forEach((agentSet) => {
agentSet.step(this.grid, this.agents, tick);
});
}
}
Runtime.controls = undefined;
Runtime.maxPopulation = Infinity;
Runtime.currentPopulation = 0;
//# sourceMappingURL=index.js.map