UNPKG

hexagrid

Version:

Hexagonal grid with fast lookup

294 lines (238 loc) 8.13 kB
import Hex from "./hex"; import { Direction } from "./direction"; import { CubeCoordinates, CubeCoordinatesPartial } from "./cubecoordinates"; export default class Grid<HexType extends Hex<any> = Hex<any>> { private hexes: Map<string, HexType> = new Map(); get size(): number { return this.hexes.size; } constructor(...hexes: HexType[]) { this.push(...hexes); } /** * Merge other grids into the current grid * * If any hex in the new grids overlap with hexes in the current grid, * the older hexes are overwritten, similarly to what happens with `Object.assign`. * @param grids grids to merge into the current grid */ merge(...grids: Array<Grid<HexType>>): Grid<HexType> { const [thisHexes, ...otherHexes] = [this, ...grids].map(grid => Array.from(grid.values())); this.hexes.clear(); this.push(...thisHexes.concat(...otherHexes)); return this; } /** * Adds a bunch of hexes to the grid * @param hexes */ push(...hexes: HexType[]) { for (const hex of hexes) { this.hexes.set(`${hex.q}x${hex.r}`, hex); } } get(coord: CubeCoordinatesPartial): HexType | undefined { return this.hexes.get(`${coord.q}x${coord.r}`); } neighbour(coord: CubeCoordinatesPartial, direction: Direction) { return this.get(CubeCoordinates.translated(coord, direction)); } neighbours(center: CubeCoordinatesPartial, directions: number = Direction.all): HexType[] { const ret = <HexType[]> []; for (const direction of Direction.list()) { if (direction & directions) { const hex = this.get(CubeCoordinates.translated(center, direction)); if ( hex ) { ret.push(hex); } } } return ret; } /** * Get the list of hexes forming the shortest path between two hexes (included) * * Using A* * */ path(coord1: CubeCoordinatesPartial, coord2: CubeCoordinatesPartial): HexType[] | undefined { const hex1 = this.get(coord1); const hex2 = this.get(coord2); if (!hex1 || !hex2) { return undefined; } // const testPath = this.easyPath(coord1, coord2); // if (testPath) { // return testPath; // } const destCoord = hex2.toString(); interface PathTo { [coord: string]: HexType[]; } const allPaths: PathTo = {}; const toExpand: {[minDist: number]: PathTo} = {}; let toExpandNext: HexType[][] = []; const addPath = (path: HexType[]) => { const [last] = path.slice(-1); const minDist = CubeCoordinates.distance(last, hex2); toExpand[minDist] = toExpand[minDist] || {}; toExpand[minDist][last.toString()] = path; allPaths[last.toString()] = path; }; const readyNextIteration = () => { let minDistance = Number.POSITIVE_INFINITY; for (const key of Object.keys(toExpand)) { if (+key < minDistance) { minDistance = +key; } } if (minDistance < Number.POSITIVE_INFINITY) { toExpandNext = Object.values(toExpand[minDistance]); delete toExpand[minDistance]; } else { toExpandNext = []; } }; addPath([hex1]); readyNextIteration(); while (!(destCoord in allPaths) && toExpandNext.length > 0) { for (const path of toExpandNext) { const hex = path[path.length - 1]; for (const neighbour of this.neighbours(hex)) { if (allPaths[neighbour.toString()]) { continue; } addPath([...path, neighbour]); } } readyNextIteration(); } return allPaths[destCoord]; } /** * Shortest path between two coordinates, stopping when obstacle * @param hex1 * @param hex2 */ easyPath(coord1: CubeCoordinatesPartial, coord2: CubeCoordinatesPartial): HexType[] | undefined { const hex1 = this.get(coord1); const hex2 = this.get(coord2); if (!hex1 || !hex2) { return undefined; } const path = [hex1]; let currentHex: HexType | undefined = hex1; while (currentHex.q !== hex2.q || currentHex.r !== hex2.r) { currentHex = this.neighbour(currentHex, CubeCoordinates.direction(currentHex, hex2)); if (!currentHex) { return undefined; } path.push(currentHex); } return path; } /** * Distance between two hexes. -1 if not possible * */ distance(hex1: CubeCoordinatesPartial, hex2: CubeCoordinatesPartial) { const path = this.path(hex1, hex2); return (path || []).length - 1; } /** * Removes a hex by its coordinates. Returns whether there * was a hex removed * * @param q * @param r */ remove({q, r}: CubeCoordinatesPartial): boolean { return this.hexes.delete(`${q}x${r}`); } /** * Rotates the whole grid X times to the left, relative to center. * * Each rotation is 60° * * @param times * @param center The origin if not given */ rotateLeft(times: number = 1, center?: CubeCoordinates): Grid<HexType> { this.hexes.forEach(hex => hex.rotateLeft(times, center)); this.recalibrate(); return this; } /** * Rotates the whole grid X times to the right, relative to center. * * Each rotation is 60° * * @param times * @param center The origin if not given */ rotateRight(times: number = 1, center?: CubeCoordinates): Grid<HexType> { this.hexes.forEach(hex => hex.rotateRight(times, center)); this.recalibrate(); return this; } /** * Separate the hexes given into groups. * * Each hex in a group can travel through to other * members of its group by going through only members * of its group. * * @param hexes */ groups(hexes: HexType[]) { const hexSet = new Set(hexes); const groups: Array<Set<HexType>> = []; for (const hex of hexes) { // If the hex is already in a group if ((() => { for (const group of groups) { if (group.has(hex)) { return true; } } })()) { continue; } const newGroup = new Set([hex]); let toExplore = new Set([hex]); let nextToExplore = new Set<HexType>(); groups.push(newGroup); while (toExplore.size > 0) { for (const hex of toExplore) { for (const nb of this.neighbours(hex)) { if (newGroup.has(nb)) { continue; } if (!hexSet.has(nb)) { continue; } newGroup.add(nb); nextToExplore.add(nb); } } toExplore = nextToExplore; nextToExplore = new Set(); } } return groups; } /** * Makes sure the underlying storage of Hexes is coherent, if * any of their coordinates was changed since they were added */ recalibrate(): Grid<HexType> { const array = Array.from(this.values()); this.hexes.clear(); this.push(...array); return this; } values(): IterableIterator<HexType> { return this.hexes.values(); } toJSON(): HexType[] { return Array.from(this.values()); } }