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