Skip to content

Commit

Permalink
feat: remove transducers
Browse files Browse the repository at this point in the history
As it turns out, transducers don't perform a lot better compared to Array methods. They only perform
a little better (17% in a specific benchmark) when traversing a grid until the traverser "leaves the
grid". A transducer is able to shortcut the iteration. This seems a pretty rare use case and similar
functionality is implemented in the traverse() method (with the `bail` option). The examples are
updated to use the transducer-less API.
  • Loading branch information
flauwekeul committed Jul 29, 2022
1 parent f4b65f8 commit 072ead5
Show file tree
Hide file tree
Showing 27 changed files with 639 additions and 291 deletions.
16 changes: 6 additions & 10 deletions examples/a-star-path-finding/aStar.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { assertCubeCoordinates, AxialCoordinates, equals, Grid, Hex, HexCoordinates, ring, tap } from 'honeycomb-grid'
import { filter, remove } from 'transducist'
import { assertCubeCoordinates, AxialCoordinates, equals, Grid, Hex, HexCoordinates, ring } from 'honeycomb-grid'
import { TARGET_COORDINATES } from './index'
import { AStarOptions, PathData } from './types'

Expand Down Expand Up @@ -30,12 +29,10 @@ export function aStar<T extends Hex>({
targetFound = equals(current.coordinates, TARGET_COORDINATES)
if (targetFound) return backtrack(closed, grid)

grid.traverse(ring({ center: current.coordinates, radius: 1 }), [
filter(isPassable),
remove((hex) => isInList(closed, hex)),
// todo: this isn't allowed, because a transducer must always return T, but is that reasonable?
// map(tile => createPathData(tile))
tap((neighbor) => {
grid
.traverse(ring({ center: current.coordinates, radius: 1 }))
.filter((tile) => isPassable(tile) && !isInList(closed, tile))
.forEach((neighbor) => {
if (!isInList(open, neighbor)) {
const neighborPathData = createPathData(neighbor)
const nextG = current.g + neighborPathData.g
Expand All @@ -45,8 +42,7 @@ export function aStar<T extends Hex>({
}
open.push(neighborPathData)
}
}),
])
})
}

// no path found
Expand Down
48 changes: 25 additions & 23 deletions examples/a-star-path-finding/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Color, Polygon } from '@svgdotjs/svg.js'
import { createHexPrototype, Grid, rectangle, tap } from 'honeycomb-grid'
import { mapIndexed, remove } from 'transducist'
import { createHexPrototype, Grid, rectangle } from 'honeycomb-grid'
import { aStar } from './aStar'
import { getTileFill, render } from './render'
import { Tile } from './types'
Expand All @@ -19,19 +18,22 @@ const hexPrototype = createHexPrototype<Tile>({
return this.cost !== IMPASSABLE_COST
},
})
const grid = new Grid(hexPrototype, rectangle({ width: 24, height: 12 })).update([
tap((tile) => {
if (tile.equals(START_COORDINATES) || tile.equals(TARGET_COORDINATES)) {
tile.cost = 1
return
}
const grid = new Grid(hexPrototype, rectangle({ width: 24, height: 12 })).update((tiles) =>
tiles
.map((tile) => {
if (tile.equals(START_COORDINATES) || tile.equals(TARGET_COORDINATES)) {
tile.cost = 1
return tile
}

tile.cost = Math.random() > IMPASSABLE_CHANCE ? Math.floor(Math.random() * MAX_COST) : IMPASSABLE_COST
}),
tap((tile) => {
tile.svg = render(tile)
}),
])
tile.cost = Math.random() > IMPASSABLE_CHANCE ? Math.floor(Math.random() * MAX_COST) : IMPASSABLE_COST
return tile
})
.map((tile) => {
tile.svg = render(tile)
return tile
}),
)
const shortestPath = aStar<Tile>({
grid,
start: START_COORDINATES,
Expand All @@ -43,14 +45,14 @@ const shortestPath = aStar<Tile>({
const pathColor = new Color('#ff9').to('#993')

grid.update(
[
remove((tile) => tile.equals(START_COORDINATES) || tile.equals(TARGET_COORDINATES)),
mapIndexed((tile, i) => {
const polygon = tile.svg.findOne('polygon') as Polygon
const fill = getTileFill(tile, pathColor)
;(polygon.animate(undefined, i * 100) as any).fill(fill)
return tile
}),
],
(tiles) =>
tiles
.filter((tile) => !tile.equals(START_COORDINATES) && !tile.equals(TARGET_COORDINATES))
.map((tile, i) => {
const polygon = tile.svg.findOne('polygon') as Polygon
const fill = getTileFill(tile, pathColor)
;(polygon.animate(undefined, i * 100) as any).fill(fill)
return tile
}),
shortestPath,
)
54 changes: 25 additions & 29 deletions examples/line-of-sight/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@ import {
PartialCubeCoordinates,
repeatWith,
ring,
tap,
transduce,
Traverser,
TupleCoordinates,
} from 'honeycomb-grid'
import { filter, takeWhile, toArray } from 'transducist'
import { initialGameState, onUpdate, updateGameState } from './gameState'
import { renderMap, renderPlayer } from './render'
import { TILES } from './tiles'
Expand All @@ -38,7 +35,7 @@ const playerElement = renderPlayer(draw, hexPrototype.width, hexPrototype.height

onUpdate(['playerCoordinates'], ({ playerCoordinates }) => {
movePlayer(playerElement, playerCoordinates)
updateDiscoveredHexes(grid)
updateDiscoveredTiles(grid)
updateFieldOfView(grid, playerCoordinates)
})

Expand All @@ -51,34 +48,36 @@ draw.click((event: MouseEvent) => {

updateGameState(initialGameState)

function updateDiscoveredHexes(grid: Grid<Tile>) {
grid.update([
filter((tile) => tile.visibility === 'visible'),
tap((tile) => {
tile.element.first().addClass('discovered')
}),
])
function updateDiscoveredTiles(grid: Grid<Tile>) {
grid.update((tiles) =>
tiles
.filter((tile) => tile.visibility === 'visible')
.map((tile) => {
tile.element.first().addClass('discovered')
return tile
}),
)
}

function updateFieldOfView(grid: Grid<Tile>, start: HexCoordinates) {
grid.update(
[
tap((tile) => {
(tiles) =>
tiles.map((tile) => {
tile.visibility = 'visible'
tile.element.first().removeClass('discovered')
tile.element.first().removeClass('undiscovered')
return tile
}),
],
fieldOfView(start),
)
}

function fieldOfView(start: HexCoordinates): Traverser<Tile> {
const startHex = assertCubeCoordinates(grid.hexPrototype, start)
const startTile = assertCubeCoordinates(grid.hexPrototype, start)
return repeatWith(
ring({
center: start,
start: translate(startHex, { r: -config.viewDistanceInTiles }),
start: translate(startTile, { r: -config.viewDistanceInTiles }),
}),
lineOfSight(start),
{ includeSource: false },
Expand All @@ -87,19 +86,16 @@ function fieldOfView(start: HexCoordinates): Traverser<Tile> {

function lineOfSight(start: HexCoordinates): Traverser<Tile> {
return (_, stop) => {
// this state is needed to stop the ray *after* the tile with opaque terrain is found
let foundOpaqueTerrain = false
return transduce(
grid.traverse(line<Tile>({ start, stop })),
// todo: instead of keeping state like this, try making a reduce() transducer?
[
takeWhile(() => !foundOpaqueTerrain),
tap((tile) => {
foundOpaqueTerrain = tile.terrain.opaque
}),
],
toArray(),
)
const result: Tile[] = []
const sightLine = grid.traverse(line<Tile>({ start, stop }))

for (const tile of sightLine) {
result.push(tile)
// make sure the last tile is the opaque one
if (tile.terrain.opaque) return result
}

return result
}
}

Expand Down
7 changes: 4 additions & 3 deletions examples/line-of-sight/render.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Svg } from '@svgdotjs/svg.js'
import { Grid, tap } from 'honeycomb-grid'
import { Grid } from 'honeycomb-grid'
import { Tile } from './types'

export function renderMap(draw: Svg, grid: Grid<Tile>) {
grid.update(
tap((tile) => {
grid.update((tiles) =>
tiles.map((tile) => {
tile.element = renderTile(draw, tile)
return tile
}),
)
}
Expand Down

2 comments on commit 072ead5

@etodanik
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much simpler and nicer <3

@flauwekeul
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hold on, it's going to be simpler still 😄

Please sign in to comment.