Skip to content

Commit

Permalink
feat(grid): use single Compass enum, make move() accept ambiguous dir…
Browse files Browse the repository at this point in the history
…ections, improve grid.rectangle

A single compass with 8 directions is simpler, even though 2 directions are ambiguous (E and W for
pointy hexes and S and N for flat hexes). The lib should solve this ambiguity. The move() traverser
does this by converting to offset coordinates (only for these ambiguous directions). Finally,
grid.rectangle() now uses grid.traverse(), removing some duplication.
  • Loading branch information
flauwekeul committed Apr 22, 2021
1 parent abf047f commit 2bf8d1e
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 63 deletions.
28 changes: 13 additions & 15 deletions playground/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { at, createHex, createHexPrototype, Grid, Hex, move, PointyCompassDirection, repeat } from '../dist'
import { createHex, createHexPrototype, Grid, Hex } from '../dist'
import { createSuite } from './benchmark'
import { render } from './render'

Expand All @@ -9,29 +9,27 @@ interface CustomHex extends Hex {

const hexPrototype = createHexPrototype<CustomHex>({
dimensions: 30,
// orientation: Orientation.FLAT,
custom: 'custom', // fixme: adding `orientation: 'flat'` makes this an error, adding `orientation: Orientation.FLAT` doesn't
origin: (hexPrototype) => ({ x: hexPrototype.width * -0.5, y: hexPrototype.height * -0.5 }),
})
// const hex = createHex(hexPrototype, { q: 4, r: 3 })

Grid.of(hexPrototype)
.traverse(
at({ q: 0, r: 0 }),
repeat(
2,
move(PointyCompassDirection.E, 4),
move(PointyCompassDirection.SE),
move(PointyCompassDirection.W, 4),
move(PointyCompassDirection.SW),
),
move(PointyCompassDirection.E, 4),
)
.rectangle({ width: 10, height: 10 })
.rectangle({ start: { q: 0, r: 3 }, width: 5, height: 5 })
// .rectangleFromOpposingCorners({ q: 0, r: 3 }, { q: 3, r: 6 })
.each((hex) => {
hex.svg = render(createHex(hexPrototype, hex))
// console.log(hex)
})
.run()

const grid = Grid.of(hexPrototype)
createSuite().add('', function () {})
createSuite()
.add('rectangle', function () {
const grid = Grid.of(hexPrototype)
grid.rectangle({ start: { q: 1, r: 2 }, width: 5, height: 5 }).run()
})
.add('rectangleFromOpposingCorners', function () {
const grid = Grid.of(hexPrototype)
grid.rectangleFromOpposingCorners({ q: 1, r: 2 }, { q: 3, r: 6 }).run()
})
2 changes: 2 additions & 0 deletions src/grid/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AxialCoordinates, CubeCoordinates } from '../hex'

// fixme: compass has 8 directions, this should too?
export const DIRECTION_COORDINATES: AxialCoordinates[] = [
{ q: 1, r: 0 },
{ q: 0, r: 1 },
Expand All @@ -9,6 +10,7 @@ export const DIRECTION_COORDINATES: AxialCoordinates[] = [
{ q: 1, r: -1 },
]

// fixme: compass has 8 directions, this should too?
export const RECTANGLE_DIRECTIONS = [
['q', 'r', 's'],
['r', 'q', 's'],
Expand Down
4 changes: 2 additions & 2 deletions src/grid/functions/at.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HexCoordinates } from '../../hex'
import { DefaultHexPrototype, HexCoordinates } from '../../hex'
import { Traverser } from '../types'

export const at = (cursor: HexCoordinates): Traverser => () => [cursor]
export const at = <T extends DefaultHexPrototype>(cursor: HexCoordinates): Traverser<T> => () => [cursor]

export const start = at
39 changes: 35 additions & 4 deletions src/grid/functions/move.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import { HexCoordinates } from '../../hex'
import { DefaultHexPrototype, HexCoordinates } from '../../hex'
import { offsetFromZero } from '../../utils'
import { DIRECTION_COORDINATES } from '../constants'
import { CompassDirection, Traverser } from '../types'
import { Compass, Traverser } from '../types'

// todo: also accept a string and/or number for direction?
export const move = (direction: CompassDirection, times = 1): Traverser => {
export const move = <T extends DefaultHexPrototype>(direction: Compass, times = 1): Traverser<T> => {
const { q, r } = DIRECTION_COORDINATES[direction]
return (cursor) => {

return (cursor, hexPrototype) => {
const result: HexCoordinates[] = []
const relativeOffset = (coordinate: number) => offsetFromZero(hexPrototype.offset, coordinate)

// todo: refactor, move ifs inside single for loop
if (hexPrototype.isPointy && (direction === Compass.S || direction === Compass.N)) {
for (let i = 1; i <= times; i++) {
const cursorCol = cursor.q - relativeOffset(cursor.r)
const cursorRow = cursor.r
const addCol = i * q - relativeOffset(i * r)
const addRow = i * r
const _q = cursorCol + relativeOffset(cursorRow) + addCol
const _r = cursorRow + addRow
result.push({ q: _q, r: _r })
}
return result
}

if (hexPrototype.isFlat && (direction === Compass.E || direction === Compass.W)) {
for (let i = 1; i <= times; i++) {
const cursorCol = cursor.q
const cursorRow = cursor.r - relativeOffset(cursor.q)
const addCol = i * q
const addRow = i * r - relativeOffset(i * q)
const _q = cursorCol + addCol
const _r = cursorRow + relativeOffset(cursorCol) + addRow
result.push({ q: _q, r: _r })
}
return result
}

for (let i = 1; i <= times; i++) {
result.push({ q: cursor.q + q * i, r: cursor.r + r * i })
}
Expand Down
4 changes: 2 additions & 2 deletions src/grid/functions/rectangle.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { createHex, CubeCoordinates, Hex } from '../../hex'
import { offsetFromZero } from '../../utils'
import { RECTANGLE_DIRECTIONS } from '../constants'
import { FlatCompassDirection, PointyCompassDirection, RectangleOptions } from '../types'
import { Compass, RectangleOptions } from '../types'

export const rectangle = <T extends Hex>(
hexPrototype: T,
{
width,
height,
start = { q: 0, r: 0 },
direction = hexPrototype.isPointy ? PointyCompassDirection.E : FlatCompassDirection.S,
direction = hexPrototype.isPointy ? Compass.E : Compass.SE,
}: RectangleOptions,
) => {
const result: T[] = []
Expand Down
9 changes: 6 additions & 3 deletions src/grid/functions/repeat.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { HexCoordinates } from '../../hex'
import { DefaultHexPrototype, HexCoordinates } from '../../hex'
import { Traverser } from '../types'

// todo: looks a lot like Grid.traverse()
export const repeat = (amount: number, ...traversers: Traverser[]): Traverser => (cursor) => {
export const repeat = <T extends DefaultHexPrototype>(amount: number, ...traversers: Traverser<T>[]): Traverser<T> => (
cursor,
hexPrototype,
) => {
const result: HexCoordinates[] = []
let _cursor = cursor

for (let i = 0; i < amount; i++) {
for (const traverser of traversers) {
for (const nextCursor of traverser(_cursor)) {
for (const nextCursor of traverser(_cursor, hexPrototype)) {
_cursor = nextCursor
result.push(_cursor)
}
Expand Down
56 changes: 34 additions & 22 deletions src/grid/grid.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { createHex, CubeCoordinates, equals, Hex, HexCoordinates } from '../hex'
import { offsetFromZero } from '../utils'
import { RECTANGLE_DIRECTIONS } from './constants'
import { at, move, repeat } from './functions'
import { FlatCompassDirection, PointyCompassDirection, RectangleOptions, Traverser } from './types'
import { Compass, RectangleOptions, Traverser } from './types'

interface InternalTraverser<T extends Hex> {
(this: Grid<T>): Iterable<T>
Expand Down Expand Up @@ -63,42 +62,55 @@ export class Grid<T extends Hex> {
width,
height,
start = { q: 0, r: 0 },
direction = this.hexPrototype.isPointy ? PointyCompassDirection.E : FlatCompassDirection.S,
direction = this.hexPrototype.isPointy ? Compass.E : Compass.SE,
}: RectangleOptions) {
const _start: CubeCoordinates = { q: start.q, r: start.r, s: -start.q - start.r }
const [firstCoordinate, secondCoordinate, thirdCoordinate] = RECTANGLE_DIRECTIONS[direction]
const [firstStop, secondStop] = this.hexPrototype.isPointy ? [width, height] : [height, width]

// todo: duplication in traverse()
const rectangle: InternalTraverser<T> = () => {
const result: T[] = []
const hasTraversedBefore = this.traverser !== infiniteTraverser
const previousHexes = [...this.traverser()]
let cursor: CubeCoordinates = previousHexes[previousHexes.length - 1] || { q: 0, r: 0 }

const relativeOffset = (coordinate: number) => offsetFromZero(this.hexPrototype.offset, coordinate)
const traverser: Traverser<T> = (cursor) => {
const result: HexCoordinates[] = []
let _cursor = cursor
for (let second = 0; second < secondStop; second++) {
const secondOffset = offsetFromZero(this.hexPrototype.offset, second)

const secondOffset = relativeOffset(second)
for (let first = -secondOffset; first < firstStop - secondOffset; first++) {
const nextCursor: unknown = {
[firstCoordinate]: first + _start[firstCoordinate],
[secondCoordinate]: second + _start[secondCoordinate],
[thirdCoordinate]: -first - second + _start[thirdCoordinate],
}
cursor = nextCursor as CubeCoordinates
if (hasTraversedBefore && !previousHexes.some((prevCoords) => equals(prevCoords, cursor))) {
return result // todo: or continue? or make this configurable?
}
result.push(createHex(this.hexPrototype, cursor))
_cursor = nextCursor as CubeCoordinates
result.push(_cursor)
}
}

return result
}
return this.clone(rectangle)
return this.traverse(traverser)
}

// fixme: when topLeft > bottomRight
// todo: is it okay to never pass a direction? Probably, but maybe add option to determine if row or col is traversed first
// todo: have a single rectangle method that either takes {width, height} or {topLeft, bottomRight}?
rectangleFromOpposingCorners(topLeft: HexCoordinates, bottomRight: HexCoordinates) {
const { isPointy, offset } = this.hexPrototype
const relativeOffset = (coordinate: number) => offsetFromZero(offset, coordinate)

if (isPointy) {
const topLeftCol = topLeft.q + relativeOffset(topLeft.r)
const bottomRightCol = bottomRight.q + relativeOffset(bottomRight.r)
const height = Math.abs(topLeft.r - bottomRight.r) + 1
const width = Math.abs(topLeftCol - bottomRightCol) + (height % 2)
return this.rectangle({ width, height, start: topLeft })
}

const topLeftRow = topLeft.r + relativeOffset(topLeft.q)
const bottomRightRow = bottomRight.r + relativeOffset(bottomRight.q)
const height = Math.abs(topLeftRow - bottomRightRow) + 1
const width = Math.abs(topLeft.q - bottomRight.q) + (height % 2)
return this.rectangle({ width, height, start: topLeft })
}

traverse(...traversers: Traverser[]) {
traverse(...traversers: Traverser<T>[]) {
if (traversers.length === 0) {
return this // or clone()? bottomRightdo: when to return clone and when not?
}
Expand All @@ -110,7 +122,7 @@ export class Grid<T extends Hex> {
let cursor: HexCoordinates = previousHexes[previousHexes.length - 1] || { q: 0, r: 0 }

for (const traverser of traversers) {
for (const nextCursor of traverser(cursor)) {
for (const nextCursor of traverser(cursor, this.hexPrototype)) {
cursor = nextCursor
if (hasTraversedBefore && !previousHexes.some((prevCoords) => equals(prevCoords, cursor))) {
return result // todo: or continue? or make this configurable?
Expand Down
21 changes: 6 additions & 15 deletions src/grid/types.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import { HexCoordinates } from '../hex'
import { DefaultHexPrototype, HexCoordinates } from '../hex'

export enum PointyCompassDirection {
export enum Compass {
E,
SE,
SW,
W,
NW,
NE,
}

export enum FlatCompassDirection {
SE,
S,
SW,
W,
NW,
N,
NE,
}

export type CompassDirection = PointyCompassDirection | FlatCompassDirection

export interface Traverser {
(cursor: HexCoordinates): Iterable<HexCoordinates>
export interface Traverser<T extends DefaultHexPrototype> {
(cursor: HexCoordinates, hexPrototype: T): Iterable<HexCoordinates>
}

export interface RectangleOptions {
width: number
height: number
start?: HexCoordinates
direction?: CompassDirection
direction?: Compass
}
1 change: 1 addition & 0 deletions src/utils/offsetFromZero.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// todo: rename (also rename offset)?
// todo: change to https://www.redblobgames.com/grids/hexagons/#conversions-offset
export const offsetFromZero = (offset: number, distance: number) => (distance + offset * (distance & 1)) >> 1

0 comments on commit 2bf8d1e

Please sign in to comment.