From 50e707df8fe08ca7c93150f089f3e5056b7e88c0 Mon Sep 17 00:00:00 2001 From: Abbe Keultjes Date: Sun, 2 May 2021 18:30:56 +0200 Subject: [PATCH] feat(grid): add rays() traverser This first version is very basic and only accepts at/start, a length for the rays, a rotation and an updateRay callback that's called with each ray and is meant to remove hexes from the ray to create a field of view. --- src/grid/traversers/index.ts | 1 + src/grid/traversers/rays.test.ts | 176 +++++++++++++++++++++++++++++++ src/grid/traversers/rays.ts | 36 +++++++ 3 files changed, 213 insertions(+) create mode 100644 src/grid/traversers/rays.test.ts create mode 100644 src/grid/traversers/rays.ts diff --git a/src/grid/traversers/index.ts b/src/grid/traversers/index.ts index 3898e70c..1c39e428 100644 --- a/src/grid/traversers/index.ts +++ b/src/grid/traversers/index.ts @@ -1,6 +1,7 @@ export * from './add' export * from './branch' export * from './line' +export * from './rays' export * from './rectangle' export * from './ring' export * from './spiral' diff --git a/src/grid/traversers/rays.test.ts b/src/grid/traversers/rays.test.ts new file mode 100644 index 00000000..a917052d --- /dev/null +++ b/src/grid/traversers/rays.test.ts @@ -0,0 +1,176 @@ +import { createHex, createHexPrototype, Hex } from '../../hex' +import { rays } from './rays' + +const hexPrototype = createHexPrototype() +const cursor = createHex(hexPrototype, { q: 1, r: 2 }) +const getHex = jest.fn((coordinates) => createHex(hexPrototype, coordinates)) + +describe('when called with only length', () => { + test('returns a traverser that returns all (unique) hexes around the cursor with radius length', () => { + const result = [...rays({ length: 2 })(cursor, getHex)] + expect(result).toMatchObject([ + { q: 1, r: 1 }, + { q: 1, r: 0 }, + { q: 2, r: 1 }, + { q: 2, r: 0 }, + { q: 3, r: 0 }, + { q: 2, r: 2 }, + { q: 3, r: 1 }, + { q: 3, r: 2 }, + { q: 2, r: 3 }, + { q: 1, r: 3 }, + { q: 1, r: 4 }, + { q: 0, r: 4 }, + { q: 0, r: 3 }, + { q: -1, r: 4 }, + { q: -1, r: 3 }, + { q: 0, r: 2 }, + { q: -1, r: 2 }, + { q: 0, r: 1 }, + ]) + }) +}) + +describe('when called with start and length', () => { + test('returns a traverser that returns all (unique) hexes around and including the start with radius length', () => { + const result = [...rays({ start: [3, 2], length: 2 })(cursor, getHex)] + expect(result).toMatchObject([ + { q: 3, r: 2 }, + { q: 3, r: 1 }, + { q: 3, r: 0 }, + { q: 4, r: 1 }, + { q: 4, r: 0 }, + { q: 5, r: 0 }, + { q: 4, r: 2 }, + { q: 5, r: 1 }, + { q: 5, r: 2 }, + { q: 4, r: 3 }, + { q: 3, r: 3 }, + { q: 3, r: 4 }, + { q: 2, r: 4 }, + { q: 2, r: 3 }, + { q: 1, r: 4 }, + { q: 1, r: 3 }, + { q: 2, r: 2 }, + { q: 1, r: 2 }, + { q: 2, r: 1 }, + ]) + }) +}) + +describe('when called with at and length', () => { + test('returns a traverser that returns all (unique) hexes around the at with radius length', () => { + const result = [...rays({ at: [1, 4], length: 2 })(cursor, getHex)] + expect(result).toMatchObject([ + { q: 1, r: 3 }, + { q: 1, r: 2 }, + { q: 2, r: 3 }, + { q: 2, r: 2 }, + { q: 3, r: 2 }, + { q: 2, r: 4 }, + { q: 3, r: 3 }, + { q: 3, r: 4 }, + { q: 2, r: 5 }, + { q: 1, r: 5 }, + { q: 1, r: 6 }, + { q: 0, r: 6 }, + { q: 0, r: 5 }, + { q: -1, r: 6 }, + { q: -1, r: 5 }, + { q: 0, r: 4 }, + { q: -1, r: 4 }, + { q: 0, r: 3 }, + ]) + }) +}) + +describe('updateRay callback', () => { + test('is called with each ray', () => { + const updateRay = jest.fn((ray) => ray.filter((hex) => hex.q < 1)) + const result = [...rays({ length: 2, updateRay })(cursor, getHex)] + + expect(updateRay.mock.calls).toMatchObject([ + [ + [ + { q: 1, r: 1 }, + { q: 1, r: 0 }, + ], + ], + [ + [ + { q: 2, r: 1 }, + { q: 2, r: 0 }, + ], + ], + [ + [ + { q: 2, r: 1 }, + { q: 3, r: 0 }, + ], + ], + [ + [ + { q: 2, r: 2 }, + { q: 3, r: 1 }, + ], + ], + [ + [ + { q: 2, r: 2 }, + { q: 3, r: 2 }, + ], + ], + [ + [ + { q: 2, r: 2 }, + { q: 2, r: 3 }, + ], + ], + [ + [ + { q: 1, r: 3 }, + { q: 1, r: 4 }, + ], + ], + [ + [ + { q: 1, r: 3 }, + { q: 0, r: 4 }, + ], + ], + [ + [ + { q: 0, r: 3 }, + { q: -1, r: 4 }, + ], + ], + [ + [ + { q: 0, r: 3 }, + { q: -1, r: 3 }, + ], + ], + [ + [ + { q: 0, r: 2 }, + { q: -1, r: 2 }, + ], + ], + [ + [ + { q: 0, r: 2 }, + { q: 0, r: 1 }, + ], + ], + ]) + expect(result).toMatchObject([ + { q: 0, r: 4 }, + { q: 0, r: 3 }, + { q: -1, r: 4 }, + { q: -1, r: 3 }, + { q: 0, r: 2 }, + { q: -1, r: 2 }, + { q: 0, r: 1 }, + ]) + }) +}) diff --git a/src/grid/traversers/rays.ts b/src/grid/traversers/rays.ts new file mode 100644 index 00000000..6b25f671 --- /dev/null +++ b/src/grid/traversers/rays.ts @@ -0,0 +1,36 @@ +import { assertCubeCoordinates, CubeCoordinates, Hex } from '../../hex' +import { RotationLike, StartOrAt, Traverser } from '../types' +import { line, LineBetweenOptions } from './line' +import { ring } from './ring' + +// todo: +// - add option for first ray () +export const rays = ({ + at, + start, + length, + rotation, + updateRay = (_) => _, +}: RaysOptions): Traverser => { + return (cursor, getHex) => { + const firstCoordinates = at ?? start ?? cursor + const { q, r, s } = assertCubeCoordinates(firstCoordinates, cursor) + // todo: make this configurable: either a direction or end of line? + const ringStart: CubeCoordinates = { q, r: r - length, s: s + length } + + return ring({ center: firstCoordinates, start: ringStart, rotation })(cursor, getHex) + .reduce((uniqueHexes, through) => { + const ray = line({ at, start, through } as LineBetweenOptions)(cursor, getHex) + updateRay(ray).forEach((hex) => uniqueHexes.set(hex.toString(), hex)) + return uniqueHexes + }, new Map()) + .values() + } +} + +export type RaysOptions = StartOrAt & { + length: number + // todo: add arc option + rotation?: RotationLike + updateRay?: (hexesInRay: T[]) => T[] +}