import Cell from '../entities/Cell' import Agent from '../entities/Agent' import RandomGenerator from '../numbers/RandomGenerator' import CellGrid from './CellGrid' export default class AgentSet { private agents: Set = new Set() /** * Generates an AgentSet from an array of agents. */ public static fromArray(agents: T[]) { const agentSet = new AgentSet() for (const agent of agents) { agentSet.add(agent) } return agentSet } /** * Generates an AgentSet from a Set of agents. */ public static fromSet(agents: Set) { const agentSet = new AgentSet() for (const agent of agents) { agentSet.add(agent) } return agentSet } /** * Generates an AgentSet from a factory function. * The factory function is called `count` times with an sequential index * and a non-sequential random seed. The function should return an Agent. */ public static fromFactory( count: number, agentFactory: (index: number, randomSeed: number) => T, options?: { randomSeed?: number, } ) { const randomSeed = options?.randomSeed ?? Date.now() ^ (Math.random() * 0x100000000) const rng = new RandomGenerator(randomSeed) const agentSet = new AgentSet() for (let i = 0; i < count; i++) { rng.jump() const seed = rng.uniformInt(0, 0x100000000) agentSet.add(agentFactory(i, seed)) } return agentSet } constructor() {} public *[Symbol.iterator]() { for (const agent of this.agents) { yield agent } } public toArray(): T[] { return [...this.agents] } /** * Runs the 'act' method on all agents in the set. */ public step( world: CellGrid, agentSets: { [key: string]: AgentSet }, tick: number ): void { for (const agent of this.agents) { agent.act(world, agentSets, tick) } } /** * Inserts an agent into the set. * Optionally inserts the agent into the grid. */ public add(agent: T, world?: CellGrid) { this.agents.add(agent) if (world) { world.insertAgent(agent) } } /** * Removes an agent from the set. */ public remove(agent: T) { this.agents.delete(agent) } /** * Iterates over all agents in the set. */ public forEach(callback: (agent: T, i: number) => void) { let i = 0 for (const agent of this.agents) { callback(agent, i) i++ } } /** * Maps a callback function over all agents in the set. */ public map(callback: (agent: T, i: number) => U): U[] { let i = 0 const values = [] for (const agent of this.agents) { values.push(callback(agent, i)) i++ } return values } /** * Gets the ith agent in the set. */ public get(index: number): T | undefined { return [...this.agents][index] } /* * Reduces the set of agents to a single value via a callback function. */ public reduce(callback: (accumulator: U, agent: T) => U, initialValue: U): U { return [...this.agents].reduce(callback, initialValue) } /** * Finds the Agent with the minimum value in the set of agents. * * Returns undefined if the set is empty. */ public minimizeBy(fn: (agent: T) => number): T { if (this.agents.size === 0) { return undefined } return [...this.agents].reduce((a, b) => fn(a) < fn(b) ? a : b) } /** * Finds the Agent with the maximum value in the set of agents. * * Returns undefined if the set is empty. * * @since 1.0.0 */ public maximizeBy(fn: (agent: T) => number): T { if (this.agents.size === 0) { return undefined } return [...this.agents].reduce((a, b) => fn(a) > fn(b) ? a : b) } /** * Returns the number of agents in the set. */ public get size() { return this.agents.size } /** * Removes all dead agents from the set. * Note: This is handled automatically by the model. */ public cullDeadAgents() { this.agents = new Set([...this.agents].filter(agent => agent.isAlive)) } /** * Gets the agent at a location on a grid. */ public getAgentAt(location: [number, number]): T | undefined { return [...this.agents].find(agent => agent.position[0] === location[0] && agent.position[1] === location[1]) } /** * Gets the nearest agent to a location. Returns undefined if the set is empty. */ public getNearestAgentAt( world: CellGrid, location: [number, number], options?: { metric?: 'manhattan'|'euclidean' } ): T | undefined { if (this.agents.size === 0) { return undefined } const metric = options?.metric || 'euclidean' let nearestAgent: T | undefined let nearestDistance = Infinity for (const agent of this.agents) { const distance = this.distance(world, location, agent.position.components, metric) if (distance < nearestDistance) { nearestDistance = distance nearestAgent = agent } } return nearestAgent } public filter(predicate: (agent: T) => boolean): AgentSet { return AgentSet.fromArray([...this.agents].filter(predicate)) } public findFirst(predicate: (agent: T) => boolean): T | undefined { return [...this.agents].find(predicate) } public sortBy(compareFn: (a: T, b: T) => number): AgentSet { return AgentSet.fromArray([...this.agents].sort(compareFn)) } public slice(start: number, end: number): AgentSet { return AgentSet.fromArray([...this.agents].slice(start, end)) } /** * Gets all agents within a certain range of a location. */ public getWithinRange( world: CellGrid, location: [number, number], range: number, options?: { metric?: 'manhattan'|'euclidean' } ): AgentSet { const metric = options?.metric || 'euclidean' return AgentSet.fromArray( [...this.agents] .filter(agent => this.distance(world, location, agent.position.components, metric) <= range) ) } /** * Gets the mean position of all agents in the set. */ public getCentroid(): [number, number] { const sum = this.reduce((acc, agent) => { acc[0] += agent.position.x acc[1] += agent.position.y return acc }, [0, 0]) return [sum[0] / this.size, sum[1] / this.size] } /** * Picks a single agent at random from the set. * Optionally provide a list of agents to exclude from the selection. * * Returns undefined if the set is empty. */ public random( rng: RandomGenerator, options?: { exclude?: T[], } ): T | undefined { const exclude = options?.exclude || [] const candidates = [...this.agents].filter(agent => !exclude.includes(agent)) if (candidates.length === 0) { return undefined } return rng.pickRandom(candidates) } /** * Picks a random sample (without replacement) of agents from the set. * Optionally provide a list of agents to exclude from the sample. */ public randomSample( rng: RandomGenerator, count: number, options?: { exclude?: T[], } ): AgentSet { const exclude = options?.exclude || [] const candidates = [...this.agents].filter(agent => !exclude.includes(agent)) const sample = rng.pickRandomArray(candidates, count) return AgentSet.fromArray(sample) } private distance( world: CellGrid, location1: [number, number], location2: [number, number], metric: 'manhattan'|'euclidean', ): number { if (world.boundaryCondition === 'finite') { if (metric === 'euclidean') { return Math.sqrt((location1[0] - location2[0]) ** 2 + (location1[1] - location2[1]) ** 2) } else { return Math.abs(location1[0] - location2[0]) + Math.abs(location1[1] - location2[1]) } } if (world.boundaryCondition === 'periodic') { const dx = Math.abs(location1[0] - location2[0]) const dy = Math.abs(location1[1] - location2[1]) const x = Math.min(dx, world.width - dx) const y = Math.min(dy, world.height - dy) if (metric === 'euclidean') { return Math.sqrt(x ** 2 + y ** 2) } else { return x + y } } } }