Skip to content

Commit

Permalink
feat(grid): rename HexCache to CoordinatesCache and expand API, add t…
Browse files Browse the repository at this point in the history
…oString() to hex

The CoordinatesCache uses hex coordinates as keys in its internal map (hence the name). It has an
API very similar to JS's native Map (and Set) class. Traversing a grid once using CoordinatesCache
decreases performance about ~40% compared to using no cache. The turning point where caching starts
improving performance is at around traversing a ~300 hex grid twice (or a ~130 hex grid thrice). The
main purpose of the cache isn't improving performance (maybe I shouldn't call it a cache then?), but
keeping state (hexes).
  • Loading branch information
flauwekeul committed Apr 22, 2021
1 parent 64ec33b commit 6a0ca15
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 35 deletions.
64 changes: 64 additions & 0 deletions src/cache/coordinatesCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { toString } from '../hex/functions'
import { Hex, HexCoordinates } from '../hex/types'

export interface Cache<T> {
readonly last: T | undefined
readonly size: number
clear(): void
delete(id: unknown): boolean
forEach(fn: (item: T, id: unknown, cache: this) => void): void
get(id: unknown): T | undefined
has(id: unknown): boolean
set(id: unknown): this
}

// todo: experiment with cache that serializes and deserializes hexes (users should probably implement it)
// todo: add "dummy cache" that doesn't cache (for the purpose of performance)
export class CoordinatesCache<T extends Hex> implements Cache<T> {
static of<T extends Hex>(hexes?: T[]) {
return new CoordinatesCache(hexes)
}

private cache = new Map<string, T>()

get last() {
return Array.from(this.cache.values()).pop()
}

get size() {
return this.cache.size
}

constructor(hexes: T[] = []) {
this.cache = new Map(hexes.map((hex) => [hex.toString(), hex]))
}

clear() {
this.cache.clear()
}

delete(coordinates: string | HexCoordinates) {
return this.cache.delete(stringifyCoordinates(coordinates))
}

forEach(fn: (hex: T, id: string, cache: this) => void) {
this.cache.forEach((hex, id) => fn(hex, id, this))
}

get(coordinates: string | HexCoordinates) {
return this.cache.get(stringifyCoordinates(coordinates))
}

has(coordinates: string | HexCoordinates) {
return this.cache.has(stringifyCoordinates(coordinates))
}

set(hex: T) {
this.cache.set(stringifyCoordinates(hex), hex)
return this
}
}

function stringifyCoordinates(coordinates: string | HexCoordinates) {
return typeof coordinates === 'string' ? coordinates : toString(coordinates)
}
1 change: 1 addition & 0 deletions src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './coordinatesCache'
30 changes: 15 additions & 15 deletions src/grid/grid.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { CoordinatesCache } from '../cache'
import { CompassDirection } from '../compass'
import { createHex, Hex, HexCoordinates } from '../hex'
import { HexCache } from '../hexCache'
import { neighborOf } from './functions'
import { rectangle, RectangleOptions } from './traversers'
import { GetOrCreateHexFn, Traverser } from './types'
import { forEach, map } from './utils'

// todo: add from() static method that only accepts hexes and creates a grid by picking the prototype and traverser (traverser is just: `() => hexes`)
// todo: add from() static method that only accepts a cache and creates a grid by picking the prototype and traverser (traverser is just: `() => cache`)
export class Grid<T extends Hex> {
static of<T extends Hex>(hexPrototype: T, hexes?: HexCache<T>, traverser?: InternalTraverser<T>) {
return new Grid(hexPrototype, hexes, traverser)
static of<T extends Hex>(hexPrototype: T, cache?: CoordinatesCache<T>, traverser?: InternalTraverser<T>) {
return new Grid(hexPrototype, cache, traverser)
}

constructor(
public hexPrototype: T,
// todo: default to a no-op cache that does nothing?
public hexes = new HexCache<T>(),
public cache = new CoordinatesCache<T>(),
private traverser: InternalTraverser<T> = infiniteTraverser,
) {}

Expand All @@ -25,17 +25,17 @@ export class Grid<T extends Hex> {
}
}

// it doesn't take a hexPrototype and hexes because it doesn't need to copy those
// it doesn't take a hexPrototype and cache because it doesn't need to copy those
copy(traverser = this.traverser) {
// bind(this) in case the traverser is a "regular" (generator) function
return Grid.of(this.hexPrototype, this.hexes, traverser.bind(this))
return Grid.of(this.hexPrototype, this.cache, traverser.bind(this))
}

each(fn: (hex: T) => void) {
return this.copy(() => forEach(fn)(this.traverser()))
}

// todo: use this.hexes
// todo: use this.cache
map(fn: (hex: T) => T) {
return this.copy(() => map(fn)(this.traverser()))
}
Expand Down Expand Up @@ -70,30 +70,30 @@ export class Grid<T extends Hex> {
this.traverser()
// todo: private method/property?
const getOrCreateHex: GetOrCreateHexFn<T> = (coordinates) =>
// todo: use Map for faster finding (also for `this.hexes.items.some()`)?
this.hexes.items.find((hex) => hex.equals(coordinates)) ?? createHex(this.hexPrototype).copy(coordinates) // copy to enable users to make custom hexes
let cursor: T = this.hexes.items[this.hexes.items.length - 1] || createHex(this.hexPrototype).copy() // copy to enable users to make custom hexes
this.cache.get(coordinates) ?? createHex(this.hexPrototype).copy(coordinates) // copy to enable users to make custom hexes
// todo: don't start at last hex and/or make it configurable?
let cursor: T = this.cache.last || createHex(this.hexPrototype).copy() // copy to enable users to make custom hexes

for (const traverser of traversers) {
for (const nextCursor of traverser(cursor, getOrCreateHex)) {
cursor = nextCursor
if (hasTraversedBefore && !this.hexes.items.some((prevHex) => prevHex.equals(cursor))) {
// return early when traversing outside previously made grid
if (hasTraversedBefore && !this.cache.has(cursor)) {
return result // todo: or continue? or make this configurable?
}
this.cache.set(cursor)
result.push(cursor)
}
}

// cache hexes
this.hexes.items = result
return result
}

return this.copy(nextTraverse)
}

// todo: maybe remove this method?
// todo: use this.hexes
// todo: use this.cache
neighborOf(hex: T, direction: CompassDirection) {
return neighborOf(hex, direction)
}
Expand Down
4 changes: 4 additions & 0 deletions src/hex/functions/createHexPrototype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { hexToOffset } from './hexToOffset'
import { hexToPoint } from './hexToPoint'
import { isFlat } from './isFlat'
import { isPointy } from './isPointy'
import { toString } from './toString'
import { width } from './width'

export interface HexPrototypeOptions {
Expand Down Expand Up @@ -98,6 +99,9 @@ export const createHexPrototype = <T extends Hex>(customPrototype?: T | Partial<
toPoint() {
return hexToPoint(this)
},
toString() {
return toString(this)
},
// todo: add to docs that any of the above methods will be overwritten when present in customPrototype
...customPrototype,
} as T & HexPrototypeOptions
Expand Down
1 change: 1 addition & 0 deletions src/hex/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export * from './isFlat'
export * from './isHex'
export * from './isPointy'
export * from './offsetToAxial'
export * from './toString'
export * from './width'
3 changes: 3 additions & 0 deletions src/hex/functions/toString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { HexCoordinates } from '../types'

export const toString = ({ q, r }: HexCoordinates) => `${q},${r}`
1 change: 1 addition & 0 deletions src/hex/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface DefaultHexPrototype extends HexSettings {
// todo: about 80% sure the newProps type works (it's used in more places, if it works: maybe make it a separate type?)
copy(this: this, newProps?: Partial<this> | HexCoordinates): this
toPoint(this: this): Point
toString(this: this): string
}

export interface Hex extends DefaultHexPrototype, AxialCoordinates {}
19 changes: 0 additions & 19 deletions src/hexCache/hexCache.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/hexCache/index.ts

This file was deleted.

0 comments on commit 6a0ca15

Please sign in to comment.