Skip to content

Commit

Permalink
feat(grid): grids can now be created/traversed with a single traverse…
Browse files Browse the repository at this point in the history
…r or an array of traversers

This makes combining traversers a lot more intuitive. Also improved typing of createHexPrototype.
  • Loading branch information
flauwekeul committed Apr 22, 2021
1 parent e56ced8 commit 890ce96
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 14 deletions.
4 changes: 2 additions & 2 deletions playground/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ const hexPrototype = createHexPrototype<CustomHex>({
const store = new Map<string, CustomHex>()
const grid = Grid.of(hexPrototype, rectangle({ start: { q: 0, r: 0 }, width: 10, height: 10 }), store)
.each(setStore())
.traverse(at({ q: 9, r: 0 }), move(CompassDirection.SE, 4), move(CompassDirection.SW, 4))
.traverse([at({ q: 9, r: 0 }), move(CompassDirection.SE, 4), move(CompassDirection.SW, 4)])
.filter(inStore())
.each((hex) => {
hex.svg = render(hex)
// console.log(hex)
})
.run()
console.log(store, grid.store)
console.log(grid)

const amount = 10
createSuite().add('Grid', function () {
Expand Down
174 changes: 174 additions & 0 deletions src/grid/grid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { cloneHex, createHex, createHexPrototype, Hex, toString } from '../hex'
import { Grid } from './grid'
import { at } from './traversers'

const hexPrototype = createHexPrototype()

describe('creation', () => {
test('accepts a single traverser', () => {
const traverser = jest.fn(() => [])
new Grid(hexPrototype, traverser).run()

expect(traverser).toBeCalledWith(createHex(hexPrototype), expect.any(Function))
})

test('accepts multiple traversers', () => {
const callback = jest.fn()
const grid = new Grid(hexPrototype, [at({ q: 1, r: 2 }), at({ q: 3, r: 4 })]).run(callback)

expect(callback.mock.calls).toEqual([
[createHex(hexPrototype, { q: 1, r: 2 }), grid],
[createHex(hexPrototype, { q: 3, r: 4 }), grid],
])
})

test(`accepts a store that's cloned`, () => {
const hex = createHex(hexPrototype)
const store = new Map([[hex.toString(), hex]])
const grid = new Grid(hexPrototype, null, store)

expect(grid.store).toEqual(store)
expect(grid.store).not.toBe(store)
})
})

test('implements toStringTag', () => {
expect(`${new Grid(hexPrototype)}`).toBe('[object Grid]')
})

test('can be iterated', () => {
const coordinates = { q: 1, r: 2 }
const grid = new Grid(hexPrototype, [at(coordinates)])

for (const hex of grid) {
expect(hex).toMatchObject(coordinates)
}
})

describe('getHex()', () => {
test('returns a hex from the store when present in the store', () => {
const coordinates = { q: 1, r: 2 }
const hex = createHex(hexPrototype, coordinates)
const store = new Map([[toString(hex), hex]])
const grid = new Grid(hexPrototype, null, store)

expect(grid.getHex(coordinates)).toBe(hex)
})

test('calls toString() on the hex prototype so that the user can control how a hex is looked up in the store', () => {
const customPrototype = createHexPrototype({
toString() {
return `${this.q}|${this.r}`
},
})
const coordinates = { q: 1, r: 2 }
const hex = createHex(customPrototype, coordinates)
const store = new Map([['1|2', hex]])
const grid = new Grid(customPrototype, null, store)

expect(grid.getHex(coordinates)).toBe(hex)
})

test('returns a new hex when not present in the store', () => {
const coordinates = { q: 1, r: 2 }
const hex = createHex(hexPrototype, coordinates)
const grid = new Grid(hexPrototype)

expect(grid.getHex(coordinates)).toMatchObject(hex)
expect(grid.getHex(coordinates)).not.toBe(hex)
})

test('calls clone() on the hex prototype so that a user can control how a new hex is created', () => {
const customPrototype = createHexPrototype<{ custom: string } & Hex>({
clone(newProps) {
return cloneHex(this, { ...newProps, custom: 'custom' })
},
})
const grid = new Grid(customPrototype)
const hex = grid.getHex()

expect(hex.custom).toBe('custom')
})
})

describe('each()', () => {
test('iterates over each hex from the previous iterator/traverser', () => {
const callback = jest.fn()
const grid1 = new Grid(hexPrototype, [at({ q: 1, r: 2 }), at({ q: 3, r: 4 })]).each(callback).run()
expect(callback.mock.calls).toEqual([
[createHex(hexPrototype, { q: 1, r: 2 }), grid1],
[createHex(hexPrototype, { q: 3, r: 4 }), grid1],
])

callback.mockReset()

const grid2 = new Grid(hexPrototype, [at({ q: 1, r: 2 }), at({ q: 3, r: 4 })])
.traverse([at({ q: 5, r: 6 })]) // 👈 now the last traverser
.each(callback)
.run()
expect(callback.mock.calls).toEqual([[createHex(hexPrototype, { q: 5, r: 6 }), grid2]])
})

test(`can add hexes to the grid's store`, () => {
const store = new Map<string, Hex>()
const grid = new Grid(hexPrototype, [at({ q: 1, r: 2 }), at({ q: 3, r: 4 })], store)
.each((hex, grid) => {
grid.store?.set(hex.toString(), hex)
})
.run()

expect(grid.store).toEqual(
new Map([
['1,2', createHex(hexPrototype, { q: 1, r: 2 })],
['3,4', createHex(hexPrototype, { q: 3, r: 4 })],
]),
)
})
})

describe('filter()', () => {
test('filters hexes', () => {
const callback = jest.fn()
const grid = new Grid(hexPrototype, [at({ q: 1, r: 1 }), at({ q: 2, r: 2 }), at({ q: 3, r: 3 })])
.filter((hex) => hex.q !== 2)
.run(callback)

expect(callback.mock.calls).toEqual([
[createHex(hexPrototype, { q: 1, r: 1 }), grid],
[createHex(hexPrototype, { q: 3, r: 3 }), grid],
])
})
})

describe('takeWhile()', () => {
test('stops when the passed predicate returns false', () => {
const callback = jest.fn()
const grid = new Grid(hexPrototype, [at({ q: 1, r: 1 }), at({ q: 2, r: 2 }), at({ q: 3, r: 3 })])
.takeWhile((hex) => hex.q !== 2)
.run(callback)

expect(callback.mock.calls).toEqual([[createHex(hexPrototype, { q: 1, r: 1 }), grid]])
})
})

describe('run()', () => {
test('runs all iterators recursively and returns itself', () => {
const eachCallback = jest.fn()
const filterCallback = jest.fn((hex) => hex.q > 1)
const runCallback = jest.fn()
const grid = new Grid(hexPrototype, [at({ q: 1, r: 2 }), at({ q: 3, r: 4 })])
.each(eachCallback)
.filter(filterCallback)

expect(eachCallback).not.toBeCalled()

const result = grid.run(runCallback)

expect(result).toBe(grid)
expect(eachCallback.mock.calls).toEqual([
[createHex(hexPrototype, { q: 1, r: 2 }), grid],
[createHex(hexPrototype, { q: 3, r: 4 }), grid],
])
expect(runCallback.mock.calls).toEqual([[createHex(hexPrototype, { q: 3, r: 4 }), grid]])
})
})
23 changes: 12 additions & 11 deletions src/grid/grid.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createHex, Hex, HexCoordinates } from '../hex'
import { ensureArray } from '../utils'
import { Callback, Traverser } from './types'

export class Grid<T extends Hex> {
static of<T extends Hex>(hexPrototype: T, traverser?: Traverser<T> | null, store?: Map<string, T>) {
static of<T extends Hex>(hexPrototype: T, traverser?: Traverser<T> | Traverser<T>[] | null, store?: Map<string, T>) {
return new Grid<T>(hexPrototype, traverser, store)
}

Expand All @@ -12,12 +13,16 @@ export class Grid<T extends Hex> {

store?: Map<string, T>

// todo: add getters for hexes and cursor? Then [Symbol.iterator] can be removed. Also, hexes should be stored as a Map?
// then: what's the purpose of passing a store?
private _getPrevHexState: GetHexState<T> = () => ({ hexes: [], cursor: null })

constructor(public hexPrototype: T, traverser?: Traverser<T> | null, store?: Map<string, T>) {
constructor(public hexPrototype: T, traverser?: Traverser<T> | Traverser<T>[] | null, store?: Map<string, T>) {
if (traverser) {
this._getPrevHexState = () => {
const hexes = Array.from(traverser(this.getHex(), this.getHex.bind(this)))
const hexes = ensureArray(traverser).flatMap<T>((traverser) =>
Array.from(traverser(this.getHex(), this.getHex.bind(this))),
)
return { hexes, cursor: hexes[hexes.length - 1] }
}
}
Expand Down Expand Up @@ -85,16 +90,12 @@ export class Grid<T extends Hex> {
return this._clone(takeWhile)
}

traverse(...traversers: Traverser<T>[]) {
if (traversers.length === 0) {
return this
}

traverse(traversers: Traverser<T>[] | Traverser<T>) {
const traverse: GetHexState<T> = (currentGrid) => {
const nextHexes: T[] = []
let cursor = this._getPrevHexState(currentGrid).cursor ?? this.getHex()

for (const traverser of traversers) {
for (const traverser of ensureArray(traversers)) {
for (const nextCursor of traverser(cursor, this.getHex.bind(this))) {
cursor = nextCursor
nextHexes.push(cursor)
Expand All @@ -107,9 +108,9 @@ export class Grid<T extends Hex> {
return this._clone(traverse)
}

run(until?: Callback<T, void>) {
run(callback?: Callback<T, void>) {
for (const hex of this._getPrevHexState(this).hexes) {
until && until(hex, this)
callback && callback(hex, this)
}
return this
}
Expand Down
1 change: 1 addition & 0 deletions src/grid/traversers/concat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Traverser } from '../types'
*
* @param traversers One or more traversers to be combined into a single new traverser
*/
// todo: this is probably obsolete
export const concat = <T extends Hex>(...traversers: Traverser<T>[]): Traverser<T> => (cursor, getHex) => {
const result: T[] = []
let _cursor = cursor
Expand Down
2 changes: 1 addition & 1 deletion src/hex/functions/createHexPrototype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const defaultHexSettings: HexSettings = {
offset: -1,
}

export const createHexPrototype = <T extends Hex>(options?: T | Partial<HexPrototypeOptions>): T => {
export const createHexPrototype = <T extends Hex>(options?: Partial<T | HexPrototypeOptions>): T => {
// pseudo private property
const s = new WeakMap()

Expand Down
1 change: 1 addition & 0 deletions src/utils/ensureArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ensureArray = <T>(value: T | T[]): T[] => (Array.isArray(value) ? value : [value])
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ensureArray'
export * from './isAxial'
export * from './isCube'
export * from './isFunction'
Expand Down

0 comments on commit 890ce96

Please sign in to comment.