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

803 lines 38.3 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; import { v4 as uuidv4 } from 'uuid'; import { AgentSet } from '../structures'; import { RandomGenerator, Vector2, Angle, Color } from '../numbers'; import { Runtime } from '../runtime'; export var AgentStyle; (function (AgentStyle) { AgentStyle["CIRCLE"] = "circle"; AgentStyle["SQUARE"] = "square"; AgentStyle["TRIANGLE"] = "triangle"; })(AgentStyle || (AgentStyle = {})); /** * Traits represent some custom property of the agent. * Traits are inherited by children via the Agent's `reproduce` method * and are initialized with the parent's current value. Traits are constants * and do not change independently over time or by reproduction. * * For genetic traits that are inherited with mutation, see {@link GeneticTraitValue}. * For dynamic traits that are a function of the agent's current state, see {@link DynamicTrait}. */ export const Trait = (options) => { return (target, propertyKey) => { const clamp = (value) => { const { min = -Infinity, max = Infinity } = options !== null && options !== void 0 ? options : {}; return Math.min(Math.max(value, min), max); }; Object.defineProperty(target, propertyKey, { get: function () { const value = this.inheritedTraits.get(propertyKey); if (typeof value === 'number') { return clamp(value); } else { return value; } }, set: function (value) { // return (this as T).inheritedTraits.set(propertyKey, value) if (typeof value === 'number') { return this.inheritedTraits.set(propertyKey, clamp(value)); } else { return this.inheritedTraits.set(propertyKey, value); } } }); }; }; /** * Genetic traits are inherited by children and initialized * with the parent's current value plus some mutation. * This can be used with new properties or to override existing properties. * * Default mutation functions are provided for numbers, booleans, and colors. * * - Numbers: The value is mutated by a random amount between `-traitMutationRate` and `traitMutationRate`. * - Booleans: The value has a `traitMutationRate` chance of flipping. * - Colors: The color's RGB values are mutated by a normally distributed random amount with mean `traitMutationRate`. * * Default recombinant functions are provided for numbers, booleans, and colors. * * - Numbers: The value is the average of the two parent values. * - Booleans: The value is randomly chosen from the two parent values. * - Colors: The color is a blend of the two parent colors. * * These defaults can be overridden by providing custom mutation and recombinant functions. * See {@link GeneticTraitOptions} for more information. * * See also: {@link Trait} */ export const GeneticTrait = (options) => { return (target, propertyKey) => { Object.defineProperty(target, propertyKey, { get: function () { return this.geneticTraits.get(propertyKey).value; }, set: function (value) { var _a, _b, _c, _d; const defaultOptions = { mutationFunction: (value, rng, traitMutationRate) => { if (typeof value === 'boolean') { // there is a traitMutationRate chance that the trait will flip return rng.uniformFloat(0, 1) < traitMutationRate ? !value : value; } else if (typeof value === 'number') { // mutate the value slightly, plus or minus traitMutationRate of the value const modifier = rng.normalFloat(0, traitMutationRate) - rng.normalFloat(0, traitMutationRate); return value + modifier; } else if (typeof value === 'object' && value instanceof Color) { // mutate the color by a random amount const r = Math.min(Math.max(value.r + rng.normalFloat(0, traitMutationRate) * 1000, 0), 255); const g = Math.min(Math.max(value.g + rng.normalFloat(0, traitMutationRate) * 1000, 0), 255); const b = Math.min(Math.max(value.b + rng.normalFloat(0, traitMutationRate) * 1000, 0), 255); return Color.fromRGB(r, g, b); } else if (typeof value === 'object' && value instanceof Array) { // what type does the array contain? // type can be bool, string or number const type = typeof value[0]; if (type === 'number') { // each number in the array is mutated by a random amount return value.map((v) => { const modifier = rng.normalFloat(0, traitMutationRate) - rng.normalFloat(0, traitMutationRate); return v + modifier; }); } else if (type === 'boolean') { // there is a `traitMutationRate` chance for each array value to flip return value.map((v) => rng.uniformFloat(0, 1) < traitMutationRate ? !v : v); } else { throw new Error(`Array<${type}> is not supported by the default mutation function.`); } } }, recombinantFunction: (value, otherValue, rng) => { if (typeof value === 'number' && typeof otherValue === 'number') { return (value + otherValue) / 2; } else if (typeof value === 'boolean' && typeof otherValue === 'boolean') { return rng.uniformFloat(0, 1) < 0.5 ? value : otherValue; } else if (typeof value === 'object' && value instanceof Color && otherValue instanceof Color) { return value.blend(otherValue, 0.5); } else if (typeof value === 'object' && value instanceof Array && otherValue instanceof Array) { // what type does the array contain? // type can be bool, string or number const type = typeof value[0]; const otherType = typeof otherValue[0]; if (type === 'number' && otherType === 'number') { // each number in the array is the average of the two parent values return value.map((v, i) => (v + otherValue[i]) / 2); } else if (type === 'boolean' && otherType === 'boolean') { // each boolean in the array is randomly chosen from the two parent values return value.map((v, i) => rng.uniformFloat(0, 1) < 0.5 ? v : otherValue[i]); } else { throw new Error(`Array<${type}> and Array<${otherType}> is not supported by the default recombinant function.`); } } } }; const traitObject = { value, min: (_a = options === null || options === void 0 ? void 0 : options.min) !== null && _a !== void 0 ? _a : undefined, max: (_b = options === null || options === void 0 ? void 0 : options.max) !== null && _b !== void 0 ? _b : undefined, mutationFunction: (_c = options === null || options === void 0 ? void 0 : options.mutationFunction) !== null && _c !== void 0 ? _c : defaultOptions.mutationFunction, recombinantFunction: (_d = options === null || options === void 0 ? void 0 : options.recombinantFunction) !== null && _d !== void 0 ? _d : defaultOptions.recombinantFunction, }; return this.geneticTraits.set(propertyKey, traitObject); } }); }; }; /** * Dynamic traits are `Traits` which are a function of the agent's current state. * This can be used with new properties or to override existing properties. * * See also: {@link Trait} */ export function DynamicTrait(f) { return function (target, propertyKey) { Object.defineProperty(target, propertyKey, { get: function () { return f(this); }, set: function (value) { return value; } }); }; } /** * An Agent-class decorator that enables the agent's energy to decrease as it moves. * By default, the agent's energy decreases by `metabolism` units per cell moved. * If the agent's energy drops below 0, the agent dies. * * Can also be enabled by setting `this.enableHunger = true` in the agent's constructor. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type export function Hungry(constructor) { return class extends constructor { constructor() { super(...arguments); this.enableHunger = true; } }; } /** * Base class for all agents in the simulation. * The agent comes with the following defaults: * * - energy = 1 * - maxEnergy = 5 * - metabolism = 0.01 * - reproductionThreshold = maxEnergy / 2 * - sightRange = 10 * - fov = 90 degrees * - radius = 1 * - color = blue * - strokeColor = black * - style = circle * - traitMutationRate = 0.01 * - traitCrossoverRate = 0.5 * * By default, all traits are stable and do not change by reproduction. * This behavior can be overridden by using the `GeneticTrait` decorator. * Moreover, the agent's energy will not decrease as it moves. * This behavior can be enabled using the `Hungry` class decorator. * * No agent should be instantiated directly. Instead, create a subclass that implements the `act` method. * Any custom fields which are to be inherited by children using the `reproduce` method * should be decorated with the `Trait` decorator. */ export default class Agent { /** * Agent Constructor * @constructor * @param {AgentConstructor} opts - The options for the agent */ constructor(opts) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; /** * The unique identifier for the agent */ this.id = uuidv4(); /** * Used as a time step for the agent's behavior. */ this.dt = 0.1; /** * The generation of the agent. * Children created by this agent will have their `generation` * value incremented by 1. */ this.generation = 0; /** * The agent's genetic traits. These traits are inherited by children with some mutation. * Genetic traits should be decorated with the `GeneticTrait` decorator. * See also: {@link GeneticTraitValue} */ this.geneticTraits = new Map(); /** * The agent's inherited traits. These traits are inherited by children without mutation. * Inherited traits should be decorated with the `Trait` or `DynamicTrait` decorator. * See also: {@link Trait}, {@link DynamicTrait} */ this.inheritedTraits = new Map(); this.className = 'Agent'; this.className = this.constructor.name; // position is the only required input this.position = new Vector2(opts.initialPosition); this.previousPosition = this.position; this.rotation = (_a = opts.rotation) !== null && _a !== void 0 ? _a : new Angle(0, 'rad'); this.rng = new RandomGenerator(opts.randomSeed); this.isAlive = true; this.radius = (_b = opts.radius) !== null && _b !== void 0 ? _b : 1; this.energy = (_c = opts.initialEnergy) !== null && _c !== void 0 ? _c : 1; this.maxEnergy = (_d = opts.maxEnergy) !== null && _d !== void 0 ? _d : 5; this.metabolism = (_e = opts.metabolism) !== null && _e !== void 0 ? _e : 0.01; this.reproductionThreshold = (_f = opts.reproductionThreshold) !== null && _f !== void 0 ? _f : this.maxEnergy / 2; this.sightRange = (_g = opts.sightRange) !== null && _g !== void 0 ? _g : 10; this.fov = (_h = opts.fov) !== null && _h !== void 0 ? _h : new Angle(90, 'deg'); this.traitMutationRate = (_j = opts.traitMutationRate) !== null && _j !== void 0 ? _j : 0.01; this.traitCrossoverRate = (_k = opts.traitCrossoverRate) !== null && _k !== void 0 ? _k : 0.5; this.color = (_l = opts.color) !== null && _l !== void 0 ? _l : Color.fromName('blue'); this.strokeColor = (_m = opts.strokeColor) !== null && _m !== void 0 ? _m : Color.fromName('black'); this.style = (_o = opts.style) !== null && _o !== void 0 ? _o : AgentStyle.CIRCLE; this.enableHunger = (_p = opts.enableHunger) !== null && _p !== void 0 ? _p : false; } /** * Moves the agent to a specified location while updating related properties * such as the agent's energy level and rotation. * * If the next location is outside the world bounds: * - **FINITE** An error will be thrown. * - **PERIODIC** The next location will be recalculated to wrap the agent around to the other side of the world. */ moveTo(world, nextLocation) { // assert the next location is within the world bounds if (world.boundaryCondition === 'finite' && !world.isInBounds(nextLocation)) { throw new Error(`Agent cannot move to location ${nextLocation} because it is outside the world bounds: (X: ${world.width}, Y: ${world.height})`); } let adjustedLocation = nextLocation; if (world.boundaryCondition === 'periodic') { adjustedLocation = [ (nextLocation[0] + world.width) % world.width, (nextLocation[1] + world.height) % world.height ]; } // remove the agent from the current cell world.getCellNearest(this.position.components).tenantAgents.delete(this); // calculate the displacement based on the world's boundary condition let displacement; if (world.boundaryCondition === 'finite') { displacement = Math.sqrt((adjustedLocation[0] - this.position.x) ** 2 + (adjustedLocation[1] - this.position.y) ** 2); } else if (world.boundaryCondition === 'periodic') { const dx = Math.abs(adjustedLocation[0] - this.position.x); const dy = Math.abs(adjustedLocation[1] - this.position.y); const x = Math.min(dx, world.width - dx); const y = Math.min(dy, world.height - dy); displacement = Math.sqrt(x ** 2 + y ** 2); } // update the agent's position this.previousPosition = this.position; this.position = new Vector2(adjustedLocation); // update angle as the agent's current position and the last position // the rotation is normalized to the range [0, 2π) let newRotation = Math.atan2(this.position.y - this.previousPosition.y, this.position.x - this.previousPosition.x); if (newRotation < 0) { newRotation += 2 * Math.PI; } this.rotation.set(newRotation, 'rad'); // add the agent to the new cell world.getCellNearest(this.position.components).tenantAgents.add(this); // update the agent's energy level if hunger is enabled if (this.enableHunger) { this.energy -= (this.metabolism * displacement); } if (this.energy < 0) { this.die(); } } /** * Moves the agent in the direction it is facing. * The agent's default time step is used to determine the distance moved. Else, a specified distance may be used. * * Depending on the world's boundary condition, the agent will behave differently: * - **FINITE** If the next location is outside the world bounds, the agent will move to the edge of the world instead. * - **PERIODIC** If the next location is outside the world bounds, the agent will wrap around to the other side of the world. * * See {@link moveTo()} for Agent side effects. */ move(world, distance = this.dt) { const dx = distance * Math.cos(this.rotation.asRadians()); const dy = distance * Math.sin(this.rotation.asRadians()); const nextLocation = [this.position.x + dx, this.position.y + dy]; if (world.boundaryCondition === 'finite') { const x = Math.min(Math.max(nextLocation[0], 0), world.width - 1); const y = Math.min(Math.max(nextLocation[1], 0), world.height - 1); this.moveTo(world, [x, y]); return; } if (world.boundaryCondition === 'periodic') { this.moveTo(world, nextLocation); return; } // else, throw an error throw new Error(`Boundary condition '${world.boundaryCondition}' is not recognized.`); } /** * Swaps the agent's position with another agent without effecting their energy level or rotation. */ swap(world, other) { if (!other) { throw new Error('Agent cannot swap with undefined agent'); } // remove each agents from their current cells this.getCell(world).tenantAgents.delete(this); other.getCell(world).tenantAgents.delete(other); // swap the agents' positions const tempPosition = this.position; this.position = other.position; other.position = tempPosition; // add the agents to their new cells this.getCell(world).tenantAgents.add(this); other.getCell(world).tenantAgents.add(other); } /** * The agent moves in a random direction with a specified wiggle angle (default 90 deg). * The agent's `dt` property is used to determine the default distance moved. */ wander(world, options) { const { wiggle = new Angle(90, 'deg'), strideLength = this.dt } = options !== null && options !== void 0 ? options : {}; const theta = this.rng.uniformFloat(0, wiggle.asDegrees()) - this.rng.uniformFloat(0, wiggle.asDegrees()); this.rotation.increment(theta, 'deg'); this.move(world, strideLength); } /** * Rotates the agent to face a specified cell. */ faceCell(world, cell) { if (world.boundaryCondition === 'finite') { this.rotation.set(Math.atan2(cell.location[1] - this.position.y, cell.location[0] - this.position.x), 'rad'); return; } if (world.boundaryCondition === 'periodic') { const dx = Math.abs(this.position.x - cell.location[0]); const dy = Math.abs(this.position.y - cell.location[1]); const wrappedDistance_x = Math.min(dx, world.width - dx); const wrappedDistance_y = Math.min(dy, world.height - dy); const wrappedDistanceToCell = Math.sqrt(wrappedDistance_x ** 2 + wrappedDistance_y ** 2); const directDistanceToCell = Math.sqrt(dx ** 2 + dy ** 2); if (directDistanceToCell <= wrappedDistanceToCell) { this.rotation.set(Math.atan2(cell.location[1] - this.position.y, cell.location[0] - this.position.x), 'rad'); return; } else { return; } } throw new Error('Boundary condition not recognized'); } /** * Rotates the agent to face a specified agent. */ faceAgent(world, agent) { if (world.boundaryCondition === 'finite') { this.rotation.set(Math.atan2(agent.position.y - this.position.y, agent.position.x - this.position.x), 'rad'); return; } if (world.boundaryCondition === 'periodic') { const dx = Math.abs(this.position.x - agent.position.x); const dy = Math.abs(this.position.y - agent.position.y); const wrappedDistance_x = Math.min(dx, world.width - dx); const wrappedDistance_y = Math.min(dy, world.height - dy); const wrappedDistanceToCell = Math.sqrt(wrappedDistance_x ** 2 + wrappedDistance_y ** 2); const directDistanceToCell = Math.sqrt(dx ** 2 + dy ** 2); if (directDistanceToCell <= wrappedDistanceToCell) { this.rotation.set(Math.atan2(agent.position.y - this.position.y, agent.position.x - this.position.x), 'rad'); return; } else { return; } } throw new Error('Boundary condition not recognized'); } /** * The agent consumes an environmental resource and gains energy. The source of the energy can be another agent or a cell. * The agent's net energy gain is determined by the source's (Agent or Cell) energy level and the `efficiencyFunction` and `greed` options. * An agent eats by following these rules in order: * 1. Agents are greedy and will take 100% of the energy of the source. This can be modified by the `greed` option. * 2. Agents are perfectly efficient and will add 100% of the taken energy to their energy. This can be modified by the `efficiencyFunction` option. * * Both options are functions that take an energy level at a stage in the process and return a new energy level. By default, both functions are the identity function * of their input. */ eat(source, options) { var _a, _b; const greedFunction = (_a = options === null || options === void 0 ? void 0 : options.greedFunction) !== null && _a !== void 0 ? _a : ((sourceEnergy) => sourceEnergy); const efficiencyFunction = (_b = options === null || options === void 0 ? void 0 : options.efficiencyFunction) !== null && _b !== void 0 ? _b : ((energyTakenFromSource) => energyTakenFromSource); const energyTakenFromSource = greedFunction(source.energy); const energyConsumed = efficiencyFunction(energyTakenFromSource); source.energy -= energyTakenFromSource; this.energy = Math.min(this.energy + energyConsumed, this.maxEnergy); if (source instanceof Agent) { source.die(); } } /** * Sets `isAlive` to `false`. * An agent dies when it runs out of energy or is eaten by another agent. */ die() { this.isAlive = false; } /** * Gives the distance between the agent and another agent or cell. */ distanceTo(world, other, options) { const { metric = 'euclidean', boundaryCondition = world.boundaryCondition } = options !== null && options !== void 0 ? options : {}; const otherPosition = other instanceof Agent ? other.position : new Vector2(other.location); if (boundaryCondition === 'finite') { if (metric === 'euclidean') { return Math.sqrt((this.position.x - otherPosition.x) ** 2 + (this.position.y - otherPosition.y) ** 2); } else { return Math.abs(this.position.x - otherPosition.x) + Math.abs(this.position.y - otherPosition.y); } } if (boundaryCondition === 'periodic') { const dx = Math.abs(this.position.x - otherPosition.x); const dy = Math.abs(this.position.y - otherPosition.y); const distance_x = Math.min(dx, world.width - dx); const distance_y = Math.min(dy, world.height - dy); if (metric === 'euclidean') { return Math.sqrt(distance_x ** 2 + distance_y ** 2); } else { return distance_x + distance_y; } } throw new Error('Boundary condition not recognized'); } /** * Reproduces the agent by creating a new agent with half the energy of the parent. * The parent agent loses half of its energy in the process. * * If a maximum population is set and the current population is at the maximum, the agent will not reproduce * and this function will return `undefined`. * * The new agent inherits all of the parent's traits. Genetic traits are mutated according to their `mutationFunction`. * If another agent is provided, the new agent will inherit traits that both agents share and a * `traitCrossoverRate` chance of inheriting new traits that the other agent does not have. * Genetic traits are recombined according to their `recombinantFunction`. * * A new random seed is generated for the new agent using the parent's random number generator. */ reproduce(opts) { if (Runtime.currentPopulation >= Runtime.maxPopulation) { return undefined; } const { childPosition = this.position.components, other = undefined } = opts !== null && opts !== void 0 ? opts : {}; this.energy /= 2; // eslint-disable-next-line @typescript-eslint/no-explicit-any const child = new this.constructor({ initialPosition: childPosition, initialEnergy: this.energy / 2, randomSeed: this.rng.uniformInt(0, 1000000), }); this.inheritedTraits.forEach((value, key) => { child.inheritedTraits.set(key, value); }); // if another agent is provided, apply crossover to traits they share if (other) { for (const [key, value] of this.geneticTraits) { if (!other.geneticTraits.has(key)) { // if the other agent does not have the trait, // there is a small chance the child will inherit the trait if (this.rng.uniformFloat(0, 1) < this.traitCrossoverRate) { child.geneticTraits.set(key, value); } continue; } const otherValue = other.geneticTraits.get(key).value; const newValue = value.recombinantFunction(value.value, otherValue, this.rng); child.geneticTraits.set(key, { value: newValue, min: value.min, max: value.max, mutationFunction: value.mutationFunction, recombinantFunction: value.recombinantFunction }); } } // apply mutation to all genetic traits this.geneticTraits.forEach((value, key) => { var _a, _b; let newValue = value.mutationFunction(value.value, this.rng, this.traitMutationRate); // clamp mutated value to min and max if they exist if (typeof value.value === 'number' && typeof newValue === 'number') { newValue = Math.min(Math.max(newValue, (_a = value.min) !== null && _a !== void 0 ? _a : -Infinity), (_b = value.max) !== null && _b !== void 0 ? _b : Infinity); } child.geneticTraits.set(key, { value: newValue, min: value.min, max: value.max, mutationFunction: value.mutationFunction, recombinantFunction: value.recombinantFunction }); }); child.generation = this.generation + 1; return child; } /** * Gets the cell the agent is currently occupying. * If the agent's position is not an integer, it will be rounded to the nearest integer. * If the agent's position is not a number, an error will be thrown. * If the agent's position is outside the world bounds, undefined will be returned. */ getCell(world) { // the agent's location may not be an integer const x = Math.floor(this.position.x); const y = Math.floor(this.position.y); // check if x & y are numbers if (isNaN(x) || isNaN(y)) { throw new Error(`Agent position is not a number: ${this.position.x}, ${this.position.y}`); } return world.getCell([x, y]); } /** * Gets the cells adjacent to the agent. Optionally include diagonal cells. * Excludes the agent's location cell by default. */ getNeighborCells(world, options) { const { includeDiagonals = false, includeOwnCell = false } = options !== null && options !== void 0 ? options : {}; const thisCell = this.getCell(world); if (!thisCell) { throw new Error(`Agent is outside the world bounds: ${this.position.x}, ${this.position.y}`); } return thisCell.getNeighbors(world, { includeDiagonals, includeSelf: includeOwnCell }); } /** * Gets the neighboring cells within the agent's vision range. * Uses the agent's `sightRange` property by default. * Excludes the agent's own cell by default. * * If the agent is outside the world bounds, an empty array will be returned. */ getCellsWithinRange(world, options) { if (!world.isInBounds(this.position.components)) { return []; } const { range = this.sightRange, includeOwnCell = false } = options !== null && options !== void 0 ? options : {}; const thisCell = this.getCell(world); if (!thisCell) { throw new Error(`Agent is outside the world bounds: ${this.position.x}, ${this.position.y}`); } return thisCell.getNeighborsInRadius(world, { radius: range, includeSelf: includeOwnCell }); } /** * Gets agents within this agent's adjacent cells. */ getNeighbors(world, options) { return AgentSet.fromArray(this.getNeighborCells(world, options).flatMap(cell => [...cell.tenantAgents])); } /** * Gets the agents within the some range of the agent. * Uses the agent's `sightRange` property by default. */ getAgentsWithinRange(world, agents, options) { const { includeSelf = false, range = this.sightRange, } = options !== null && options !== void 0 ? options : {}; const neighbors = agents.getWithinRange(world, this.position.components, range, options); if (!includeSelf) { neighbors.remove(this); } return neighbors; } /** * Gets the agents within the agent's field of view and range. * Uses agent's `fov` and `sightRange` properties by default. */ getAgentsWithinCone(world, options) { const cells = this.getCellsWithinCone(world, options); const agents = cells.flatMap(cell => [...cell.tenantAgents]); return AgentSet.fromArray(agents); } /** * Gets any cells within the agent's field of view. * Uses agent's `fov` and `sightRange` properties by default. */ getCellsWithinCone(world, options) { const { fov = this.fov, range = this.sightRange } = options !== null && options !== void 0 ? options : {}; const prelimNeighbors = this.getCellsWithinRange(world, { range }); const isPointInArc = (x, y, xc, yc, r, theta1, theta2, L) => { // Normalize the point and center for periodic boundary conditions const dx = ((x - xc + L) % L + L) % L; // Wrap x-distance const dy = ((y - yc + L) % L + L) % L; // Wrap y-distance // Adjust dx and dy to account for the shortest periodic distance const adjustedDx = dx > L / 2 ? dx - L : dx; const adjustedDy = dy > L / 2 ? dy - L : dy; // Calculate the distance from the center const distance = Math.sqrt(adjustedDx ** 2 + adjustedDy ** 2); // Check if the point lies within the radius of the arc if (distance > r) return false; // Calculate the angle of the point relative to the arc center const pointAngle = Math.atan2(adjustedDy, adjustedDx); // Normalize angles to [0, 2π] const normalizeAngle = (angle) => (angle + 2 * Math.PI) % (2 * Math.PI); const normalizedTheta1 = normalizeAngle(theta1); const normalizedTheta2 = normalizeAngle(theta2); const normalizedPointAngle = normalizeAngle(pointAngle); // Check if the point angle lies within the arc's angle range if (normalizedTheta1 <= normalizedTheta2) { return (normalizedTheta1 <= normalizedPointAngle && normalizedPointAngle <= normalizedTheta2); } else { // Handle arcs that cross the 0 angle (e.g., 350° to 10°) return (normalizedPointAngle >= normalizedTheta1 || normalizedPointAngle <= normalizedTheta2); } }; return prelimNeighbors.filter(cell => isPointInArc(cell.location[0], cell.location[1], this.position.x, this.position.y, range, this.rotation.asRadians() - (fov.asRadians() / 2), this.rotation.asRadians() + (fov.asRadians() / 2), world.width)); } /** * Gets the cell in front of the agent according to its current * position and rotation, and world boundary conditions. */ getCellInFront(world) { const dx = Math.cos(this.rotation.asRadians()); const dy = Math.sin(this.rotation.asRadians()); const x = Math.round(this.position.x + dx); const y = Math.round(this.position.y + dy); return world.getCellNearest([x, y]); } /** * Gets the cell in front and to the left of the agent according to its current * position and rotation, and world boundary conditions. */ getCellInFrontAndLeft(world) { const dx = Math.cos(this.rotation.asRadians() + Math.PI / 4); const dy = Math.sin(this.rotation.asRadians() + Math.PI / 4); const x = Math.round(this.position.x + dx); const y = Math.round(this.position.y + dy); return world.getCellNearest([x, y]); } /** * Gets the cell in front and to the right of the agent according to its current * position and rotation, and world boundary conditions. */ getCellInFrontAndRight(world) { const dx = Math.cos(this.rotation.asRadians() - Math.PI / 4); const dy = Math.sin(this.rotation.asRadians() - Math.PI / 4); const x = Math.round(this.position.x + dx); const y = Math.round(this.position.y + dy); return world.getCellNearest([x, y]); } /** * Serializes an agent as a JSON object. */ toJSON() { const propNames = Object.getOwnPropertyNames(this); const json = {}; propNames.forEach((key) => { if (this[key] instanceof Agent) { return; } if (this[key] instanceof Set) { json[key] = Array.from(this[key]); return; } if (this[key] instanceof Vector2) { json[key] = this[key].components; return; } if (this[key] instanceof RandomGenerator) { return this[key].seed; } if (this[key] instanceof Map) { json[key] = Array.from(this[key].entries()); return; } if (this[key] instanceof Angle) { json[key] = `${this[key].asDegrees()} deg`; return; } if (this[key] instanceof Color) { json[key] = this[key].toRGB(); return; } json[key] = this[key]; }); return json; } } __decorate([ Trait(), __metadata("design:type", Number) ], Agent.prototype, "metabolism", void 0); __decorate([ Trait(), __metadata("design:type", Number) ], Agent.prototype, "reproductionThreshold", void 0); __decorate([ Trait(), __metadata("design:type", Number) ], Agent.prototype, "sightRange", void 0); __decorate([ Trait(), __metadata("design:type", Angle /** * The radius of the agent, measured in cells. */ ) ], Agent.prototype, "fov", void 0); __decorate([ Trait(), __metadata("design:type", Number) ], Agent.prototype, "radius", void 0); __decorate([ Trait(), __metadata("design:type", Color /** * The agent's stroke color when rendered. */ ) ], Agent.prototype, "color", void 0); __decorate([ Trait(), __metadata("design:type", Color /** * The shape of the agent when rendered. */ ) ], Agent.prototype, "strokeColor", void 0); __decorate([ Trait(), __metadata("design:type", String) ], Agent.prototype, "style", void 0); __decorate([ Trait(), __metadata("design:type", Number) ], Agent.prototype, "traitMutationRate", void 0); __decorate([ Trait(), __metadata("design:type", Number) ], Agent.prototype, "traitCrossoverRate", void 0); //# sourceMappingURL=Agent.js.map