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 = (location: [number, number]) => T export type CellGridConstructor = { width: number, height?: number, cellFactory: CellFactory, boundaryCondition?: BoundaryCondition, } export default class CellGrid { 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 { const { height = width, boundaryCondition = 'finite' } = options ?? {} return new CellGrid({ width, height, boundaryCondition, cellFactory: (location: [number, number]) => new Cell({ location }) }) } constructor(opts: CellGridConstructor) { 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(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(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(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) } } }