UNPKG

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
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