From a79be39173fa786d7949c0eb3d1748aa56aa01bc Mon Sep 17 00:00:00 2001 From: Abbe Keultjes Date: Fri, 22 Jul 2022 22:20:13 +0200 Subject: [PATCH] feat(grid): various changes, mainly traverse() and update() methods With the transduce() function as the main way of traversing/updating hexes, Grid's traverse() and update() are convenience methods that internally use transduce(). The added value of grid.traverse() is that it returns hexes limited to those present in the grid. The added value of grid.update() is that it always "merges" transformed hexes (optionally supplied by a traverser or iterable) into the grid. It also automatically limits hexes to those present in the grid (passed transformers are only called with existing hexes). Finally, the way a grid is created is improved. The constructor now also accepts a grid (which will be cloned) or an iterable of hex coordinates. --- src/grid/constants.ts | 4 ++ src/grid/grid.ts | 144 +++++++++++++++++++++++++----------------- 2 files changed, 89 insertions(+), 59 deletions(-) create mode 100644 src/grid/constants.ts diff --git a/src/grid/constants.ts b/src/grid/constants.ts new file mode 100644 index 00000000..24788c2a --- /dev/null +++ b/src/grid/constants.ts @@ -0,0 +1,4 @@ +// copied from https://github.com/dphilipson/transducist/blob/master/src/propertyNames.ts +export const INIT = '@@transducer/init' +export const RESULT = '@@transducer/result' +export const STEP = '@@transducer/step' diff --git a/src/grid/grid.ts b/src/grid/grid.ts index 8a16699a..542f1e77 100644 --- a/src/grid/grid.ts +++ b/src/grid/grid.ts @@ -1,21 +1,20 @@ -import { transduce, Transducer } from 'transducist' +import { filter, map, toArray, Transducer, Transformer } from 'transducist' import { createHex, Hex, HexCoordinates, Point, pointToCube } from '../hex' import { isFunction } from '../utils' +import { INIT, RESULT, STEP } from './constants' import { concat, distance } from './functions' +import { transduce } from './transduce' import { Traverser } from './types' export class Grid implements Iterable { - static fromIterable(hexes: Map): Grid - static fromIterable(hexes: Iterable): Grid - static fromIterable(iterable: Map | Iterable): Grid { - const iterator = iterable instanceof Map ? iterable.values() : iterable[Symbol.iterator]() - const firstHex = iterator.next().value + static fromIterable(hexes: Iterable): Grid { + const firstHex: T = hexes[Symbol.iterator]().next().value if (!firstHex) { - throw new Error(`Can't create grid from empty iterable: ${iterable}`) + throw new Error(`Can't create grid from empty iterable: ${hexes}`) } - return new Grid(Object.getPrototypeOf(firstHex), iterable) + return new Grid(Object.getPrototypeOf(firstHex), hexes) } get [Symbol.toStringTag]() { @@ -23,32 +22,42 @@ export class Grid implements Iterable { } [Symbol.iterator]() { - return this.#hexes.values() + return this.hexes.values() } - createHex = (coordinates?: HexCoordinates): T => createHex(this.hexPrototype, coordinates) - - #hexes = new Map() + readonly hexPrototype: T + readonly hexes = new Map() + // arrow function to avoid having to call it like so: this.createHex.bind(this) + readonly createHex = (coordinates?: HexCoordinates): T => createHex(this.hexPrototype, coordinates) + constructor(hexPrototype: T) constructor(hexPrototype: T, traversers: Traverser | Traverser[]) - constructor(hexPrototype: T, hexes: Map) - constructor(hexPrototype: T, hexes: Iterable) - constructor(public hexPrototype: T, input: Traverser | Traverser[] | Map | Iterable) { - if (input instanceof Map) { - this.#hexes = new Map(input) - } else if (this.#isTraverser(input)) { - this.#setHexes(input(this.createHex)) - } else if (Array.isArray(input) && this.#isTraverser(input[0])) { - this.#setHexes(concat(input)(this.createHex)) - } else { - this.#setHexes(input as Iterable) + constructor(hexPrototype: T, hexLikes: Iterable) + constructor(grid: Grid) + constructor( + hexPrototypeOrGrid: T | Grid, + input: Traverser | Traverser[] | Iterable = [], + ) { + if (hexPrototypeOrGrid instanceof Grid) { + this.hexPrototype = hexPrototypeOrGrid.hexPrototype + this.setHexes(hexPrototypeOrGrid) + return } - // todo: throw error when all if's fail? + + this.hexPrototype = hexPrototypeOrGrid + this.setHexes(this.#getHexesFromIterableOrTraversers(input)) } getHex(coordinates: HexCoordinates): T | undefined { const hex = this.createHex(coordinates) - return this.#hexes.get(hex.toString()) + return this.hexes.get(hex.toString()) + } + + setHexes(hexes: Iterable): this { + for (const hex of hexes) { + this.hexes.set(hex.toString(), hex) + } + return this } reduce(reducer: (previousHex: T, currentHex: T) => T): T @@ -82,7 +91,7 @@ export class Grid implements Iterable { // return new Map(this.#hexes) // } toMap(): Map { - return new Map(this.#hexes) + return new Map(this.hexes) } toObject(): Record { @@ -93,40 +102,40 @@ export class Grid implements Iterable { return obj } - // enabling stopWhenOutOfBounds can improve performance significantly - traverse(traversers: Traverser | Traverser[], { stopWhenOutOfBounds = false } = {}): T[] { - const hexes: T[] = [] - - for (const hex of concat(traversers)(this.createHex)) { - const existingHex = this.getHex(hex) - if (existingHex) { - hexes.push(existingHex) - } else if (stopWhenOutOfBounds) { - return hexes - } - } - - return hexes + // todo: should probably be a generator, because it's output will be used for transducing + traverse(traversers: Traverser | Traverser[]): T[] { + return transduce( + concat(traversers)(this.createHex), + // todo: move to grid/transformers + [map((hex) => this.getHex(hex)), filter(Boolean)], + toArray(), + ) } clone(): Grid { - const clonedHexes = new Map() - for (const hex of this) { - clonedHexes.set(hex.toString(), hex.clone()) + return new Grid(this) + } + + update(transformers: Transducer | Transducer[]): this + update(transformers: Transducer | Transducer[], hexes: Iterable): this + update(transformers: Transducer | Transducer[], traversers: Traverser | Traverser[]): this + update( + transformers: Transducer | Transducer[], + hexesOrTraversers: Grid | Iterable | Traverser | Traverser[] = this, + ): this { + if (hexesOrTraversers === this) { + transduce(hexesOrTraversers as Grid, transformers, this.#toGridReducer) + return this } - return new Grid(this.hexPrototype, clonedHexes) - } - - update(transformer: Transducer, iterable: Iterable = this): this { - transduce(iterable, transformer, { - // todo: move strings to constants.ts - ['@@transducer/init']: () => this.#hexes, - ['@@transducer/result']: (grid) => grid, - ['@@transducer/step']: (_, hex) => { - this.#hexes.set(hex.toString(), hex) - return this.#hexes - }, - }) + + transduce( + this.#getHexesFromIterableOrTraversers(hexesOrTraversers), + // automatically limit to hexes in grid (unless hexes is already those in the grid) + // todo: move to grid/transformers + [map((hex) => this.getHex(hex)), filter(Boolean)].concat(transformers), + this.#toGridReducer, + ) + return this } @@ -138,13 +147,30 @@ export class Grid implements Iterable { return distance(this.hexPrototype, from, to) } - #setHexes(iterable: Iterable) { - for (const hex of iterable) { - this.#hexes.set(hex.toString(), hex) + *#getHexesFromIterableOrTraversers( + input: Iterable | Traverser | Traverser[], + ): Generator { + const hexLikes = this.#isTraverser(input) + ? input(this.createHex) + : Array.isArray(input) && this.#isTraverser(input[0]) + ? concat(input)(this.createHex) + : (input as Iterable) + + for (const hexLike of hexLikes) { + yield this.createHex(hexLike) } } #isTraverser(value: unknown): value is Traverser { return isFunction>(value) } + + readonly #toGridReducer: Transformer = { + [INIT]: () => this, + [RESULT]: () => this, + [STEP]: (_, hex) => { + this.hexes.set(hex.toString(), hex) + return this + }, + } }