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
203 lines (173 loc) • 5.73 kB
text/typescript
import Cell from '../entities/Cell'
import Agent from '../entities/Agent'
import { Numbers } from '../main'
import { Tuple } from 'numbers/Vector2'
export type BoundaryCondition = 'finite' | 'periodic'
export type CellFactory<T extends Cell> = (location: [number, number]) => T
export type CellGridConstructor<T extends Cell> = {
width: number,
height?: number,
cellFactory: CellFactory<T>,
boundaryCondition?: BoundaryCondition,
}
export default class CellGrid<T extends Cell> {
public cells: T[][] = []
public width: number = 0
public height: number = 0
public boundaryCondition: BoundaryCondition = 'finite'
public static default(
width: number,
options?: {
height?: number,
boundaryCondition?: BoundaryCondition
}
): CellGrid<Cell> {
const { height = width, boundaryCondition = 'finite' } = options ?? {}
return new CellGrid({
width,
height,
boundaryCondition,
cellFactory: (location: [number, number]) => new Cell({ location })
})
}
constructor(opts: CellGridConstructor<T>) {
const {
width,
height = width,
cellFactory,
boundaryCondition = 'finite'
} = opts
this.width = width
this.height = height
this.boundaryCondition = boundaryCondition
for (let x = 0; x < width; x++) {
this.cells[x] = []
for (let y = 0; y < height; y++) {
this.cells[x][y] = cellFactory([x, y])
}
}
}
public *[Symbol.iterator]() {
for (const row of this.cells) {
for (const cell of row) {
yield cell
}
}
}
public map<U>(callback: (cell: T, i: number) => U): U[] {
let i = 0
const values = []
for (const row of this.cells) {
for (const cell of row) {
values.push(callback(cell, i))
i++
}
}
return values
}
public forEach(callback: (cell: T) => void) {
for (const row of this.cells) {
for (const cell of row) {
callback(cell)
}
}
}
public filter(callback: (cell: T) => boolean): T[] {
const filtered: T[] = []
for (const row of this.cells) {
for (const cell of row) {
if (callback(cell)) {
filtered.push(cell)
}
}
}
return filtered
}
public reduce<U>(callback: (accumulator: U, cell: T) => U, initialValue: U): U {
let accumulator = initialValue
for (const row of this.cells) {
for (const cell of row) {
accumulator = callback(accumulator, cell)
}
}
return accumulator
}
/**
* Gets the cell at the set's [x,y] location.
* Returns undefined if the location is out of bounds.
*/
public getCell(location: [number, number]): T | undefined {
if (!this.isInBounds(location)) {
// throw new Error(`Location ${location} is out of bounds.`)
return undefined
}
const cell = this.cells[location[0]]?.[location[1]]
return cell
}
/**
* Gets the cell nearest to the given location.
*
* There are two boundary conditions:
* - **FINITE** Returns undefined if the location is out of bounds.
* - **PERIODIC** Returns the cell at the periodic location.
*/
public getCellNearest(location: [number, number]): T | undefined {
if (this.boundaryCondition === 'finite' ) {
if (!this.isInBounds(location)) {
return undefined
}
const x = Math.round(location[0])
const y = Math.round(location[1])
return this.getCell([x, y])
}
if (this.boundaryCondition === 'periodic' ) {
let x = location[0] % (this.width - 1)
let y = location[1] % (this.height - 1)
if (x < 0) {
x += this.width - 1
}
if (y < 0) {
y += this.height - 1
}
x = Math.round(x)
y = Math.round(y)
return this.getCell([x, y])
}
throw new Error('Invalid boundary condition')
}
/**
* Picks a cell at random from the cell set.
*/
public random(rng: Numbers.RandomGenerator): T {
const x = rng.uniformInt(0, this.width - 1)
const y = rng.uniformInt(0, this.width - 1)
return this.cells[x][y]
}
/**
* Picks N random cells from the cell set without replacement
*/
public randomSample(rng: Numbers.RandomGenerator, N: number): T[] {
const cells = [...this.cells.flat()]
const randomCells: T[] = []
for (let i = 0; i < N; i++) {
const index = rng.uniformInt(0, cells.length - 1)
randomCells.push(cells[index])
cells.splice(index, 1)
}
return randomCells
}
/**
* Returns true if the given location is within the grid bounds.
*/
public isInBounds(location: [number, number]): boolean {
const [x, y] = location.map(Math.floor)
return x >= 0 && x < this.width && y >= 0 && y < this.height
}
public insertAgent<T extends Agent>(agent: T): void {
const location = agent.position.components.map(Math.floor) as Tuple
if (this.isInBounds(location)) {
const cell = this.getCell(location)
cell.tenantAgents.add(agent)
}
}
}