@xtia/grid 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -21
- package/lib/grid.d.ts +5 -6
- package/lib/grid.js +17 -22
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# `Grid`
|
|
2
2
|
|
|
3
|
+
**Alpha**: Some API details may change
|
|
4
|
+
|
|
3
5
|
## Summary
|
|
4
6
|
|
|
5
7
|
A mutable 2D grid with efficient region views, spatial operations, and seamless interoperability with [Pipe2D](https://github.com/tiadrop/pipe2d). Store, edit, and transform 2D data with an ergonomic API designed for practical use.
|
|
@@ -46,14 +48,14 @@ const grid = Grid.from(source);
|
|
|
46
48
|
|
|
47
49
|
## Regions
|
|
48
50
|
|
|
49
|
-
Use `grid.region(x, y, w, h)` to define a subgrid. The subgrid is
|
|
51
|
+
Use `grid.region(x, y, w, h)` to define a subgrid. The subgrid is **zero-copy view** into the parent; changes to the subgrid affect the parent and vice-versa.
|
|
50
52
|
|
|
51
53
|
```ts
|
|
52
54
|
// game map with terrain
|
|
53
|
-
const world = Grid.solid(100, 100,
|
|
55
|
+
const world = Grid.solid(100, 100, "grass"); // Grid<string>
|
|
54
56
|
|
|
55
57
|
// add a lake
|
|
56
|
-
world.region(30, 30, 20, 20).fill(
|
|
58
|
+
world.region(30, 30, 20, 20).fill("water");
|
|
57
59
|
|
|
58
60
|
// add mountains around the lake with a circular mask
|
|
59
61
|
const centre = world.cells.get(40, 40);
|
|
@@ -61,7 +63,7 @@ const mask = (x: number, y: number) => {
|
|
|
61
63
|
const dist = Math.hypot(x - centre.x, y - centre.y);
|
|
62
64
|
return dist > 12 && dist < 18; // mountain ring
|
|
63
65
|
};
|
|
64
|
-
world.fill(
|
|
66
|
+
world.fill("mountain", mask);
|
|
65
67
|
|
|
66
68
|
// get a view of just the interesting area
|
|
67
69
|
const lakeRegion = world.region(25, 25, 30, 30);
|
|
@@ -75,16 +77,15 @@ const lakeGrid = Grid.from(lakeRegion);
|
|
|
75
77
|
`grid.map(read, write)` creates a **view** that reads and writes the parent through the provided transformation functions.
|
|
76
78
|
|
|
77
79
|
```ts
|
|
78
|
-
const sprites = ["grass.png", "water.png", "mountain.png", "forest.png"];
|
|
79
80
|
const spriteMap = world.map(
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
type => type + ".png",
|
|
82
|
+
sprite => sprite.replace(/\.*/, '')
|
|
82
83
|
);
|
|
83
84
|
|
|
84
|
-
world.set(0, 0,
|
|
85
|
+
world.set(0, 0, "mountain"); // modify the parent
|
|
85
86
|
console.log(spriteMap.get(0, 0)); // read the transformed view: "mountain.png"
|
|
86
87
|
spriteMap.set(1, 0, "forest.png"); // modify the view
|
|
87
|
-
console.log(world.get(1, 0)); // read the parent:
|
|
88
|
+
console.log(world.get(1, 0)); // read the parent: "forest"
|
|
88
89
|
```
|
|
89
90
|
|
|
90
91
|
For one-way read-mapping, use `grid.pipe` - a [Pipe2D](https://github.com/tiadrop/pipe2d) view into the grid's data.
|
|
@@ -95,9 +96,9 @@ For one-way read-mapping, use `grid.pipe` - a [Pipe2D](https://github.com/tiadro
|
|
|
95
96
|
|
|
96
97
|
```ts
|
|
97
98
|
const topLeft = world.cells.get(0, 0);
|
|
98
|
-
console.log(topLeft.value); //
|
|
99
|
+
console.log(topLeft.value); // "mountain"
|
|
99
100
|
|
|
100
|
-
topLeft.value =
|
|
101
|
+
topLeft.value = "forest; // modifies the underlying data
|
|
101
102
|
console.log(spriteMap.get(0, 0)); // now "forest.png";
|
|
102
103
|
|
|
103
104
|
// navigate by offset
|
|
@@ -109,44 +110,63 @@ Cells provide methods for locational utilities such as pathfinding and visibilit
|
|
|
109
110
|
|
|
110
111
|
Cells are unique to, owned by, and coordinated relative to the view that provided them. Their pathfinding and visibility mapping features are unaware of space outside of their view's bounds.
|
|
111
112
|
|
|
112
|
-
###
|
|
113
|
+
### (Everybody needs good) Neighbours
|
|
114
|
+
|
|
115
|
+
`cell.getNeighbours(includeDiagonals?)` returns an array of Cells:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// derive a display number for clicked cells in Minesweeper
|
|
119
|
+
const numOfAdjacentMines = clickedCell.getNeighbours(true)
|
|
120
|
+
.filter(cell => cell.value.isMine)
|
|
121
|
+
.length;
|
|
113
122
|
```
|
|
114
|
-
|
|
123
|
+
|
|
124
|
+
### Path finding
|
|
125
|
+
```ts
|
|
126
|
+
const costs = {
|
|
127
|
+
grass: 1,
|
|
128
|
+
water: Infinity,
|
|
129
|
+
mountain: Infinity,
|
|
130
|
+
forest: 2
|
|
131
|
+
};
|
|
115
132
|
|
|
116
133
|
const start = world.cells.get(5, 5);
|
|
117
134
|
const destination = world.cells.get(90, 90);
|
|
118
135
|
const path = start.findPath(
|
|
119
|
-
destination,
|
|
136
|
+
destination, // or simply [90, 90]
|
|
120
137
|
(cell) => costs[cell.value]
|
|
121
|
-
); // Cell<
|
|
138
|
+
); // Cell<string>[]
|
|
122
139
|
```
|
|
123
140
|
|
|
124
141
|
### Visibility mapping
|
|
125
142
|
|
|
126
143
|
```ts
|
|
144
|
+
// createVisibilityMap(isClear);
|
|
127
145
|
const visibility = start.createVisibilityMap(
|
|
128
|
-
cell => cell.value
|
|
146
|
+
cell => cell.value === "grass" // anything except grass blocks vision
|
|
129
147
|
);
|
|
130
148
|
|
|
131
|
-
// any 2d source, including visibility maps, can be used as a mask
|
|
149
|
+
// any 2d source, including visibility maps, can be used as a mask for paste/fill operations
|
|
132
150
|
// paste(source: Source2D<T>, mask?: Source2D<boolean>)
|
|
133
|
-
screenGrid.paste(spriteMap, visibility
|
|
151
|
+
screenGrid.paste(spriteMap, visibility);
|
|
134
152
|
|
|
135
153
|
// using pipe2d's convenience
|
|
136
|
-
const mountainCells = world.cells.toFlatArrayXY().filter(c => c.value ===
|
|
154
|
+
const mountainCells = world.cells.toFlatArrayXY().filter(c => c.value === "mountain");
|
|
137
155
|
```
|
|
138
156
|
|
|
139
157
|
### Reusing path data
|
|
140
158
|
|
|
141
|
-
Use `cell.getPathMap(costFunc)` to create a Pipe2D of optimal paths from the parent cell. The grid space is explored once to produce an internal traversal map, and paths to individual cells are constructed
|
|
159
|
+
Use `cell.getPathMap(costFunc)` to create a Pipe2D of optimal paths from the parent cell. The grid space is explored once to produce an internal traversal map, and paths to individual cells are constructed from that data (and cached) when that pipe is queried.
|
|
142
160
|
|
|
143
161
|
```ts
|
|
144
|
-
const pathMap = start.getPathMap(c => costs[c]);
|
|
162
|
+
const pathMap = start.getPathMap(c => costs[c.value]);
|
|
145
163
|
|
|
146
164
|
const pathToCentre = pathMap.get(50, 50);
|
|
147
165
|
const pathToCorner = pathMap.get(99, 99);
|
|
148
166
|
```
|
|
149
167
|
|
|
168
|
+
Unlike visibility maps, which are lazily evaluated according to the supplied `isWall` function *when the map is queried*, path maps necessarily reflect the underlying data *when the map is created*. This distinction is hinted through the `create*` vs `get*` naming.
|
|
169
|
+
|
|
150
170
|
## The storage layer
|
|
151
171
|
|
|
152
172
|
`Grid` itself is an interface for reading and writing 2D data. `GridBase` - a subclass of `Grid` - maintains the actual storage of such data.
|
package/lib/grid.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ type PathFindOptions<T> = {
|
|
|
18
18
|
type CostMapOptions<T> = PathFindOptions<T> & {
|
|
19
19
|
stopAt?: [number, number] | Cell<T>;
|
|
20
20
|
};
|
|
21
|
+
type VisibilityOptions = {
|
|
22
|
+
maxDistance?: number;
|
|
23
|
+
boundariesVisible?: boolean;
|
|
24
|
+
};
|
|
21
25
|
export declare enum TraversalType {
|
|
22
26
|
cardinal = 0,
|
|
23
27
|
diagonal = 1,
|
|
@@ -27,11 +31,6 @@ type CostFunc<T> = (cell: Cell<T>, context: {
|
|
|
27
31
|
traversalType: TraversalType;
|
|
28
32
|
from: Cell<T>;
|
|
29
33
|
}) => number;
|
|
30
|
-
export declare enum Visibility {
|
|
31
|
-
visible = 0,
|
|
32
|
-
hidden = 1,
|
|
33
|
-
wall = 2
|
|
34
|
-
}
|
|
35
34
|
export declare class Cell<T> {
|
|
36
35
|
readonly x: number;
|
|
37
36
|
readonly y: number;
|
|
@@ -49,7 +48,7 @@ export declare class Cell<T> {
|
|
|
49
48
|
x: number;
|
|
50
49
|
y: number;
|
|
51
50
|
} | [number, number]): IterableIterator<Cell<T>>;
|
|
52
|
-
createVisibilityMap(
|
|
51
|
+
createVisibilityMap(isClear: (cell: Cell<T>) => boolean, options?: VisibilityOptions): Pipe2D<boolean>;
|
|
53
52
|
}
|
|
54
53
|
export declare class Grid<T> {
|
|
55
54
|
readonly width: number;
|
package/lib/grid.js
CHANGED
|
@@ -6,12 +6,6 @@ export var TraversalType;
|
|
|
6
6
|
TraversalType[TraversalType["diagonal"] = 1] = "diagonal";
|
|
7
7
|
TraversalType[TraversalType["shortcut"] = 2] = "shortcut";
|
|
8
8
|
})(TraversalType || (TraversalType = {}));
|
|
9
|
-
export var Visibility;
|
|
10
|
-
(function (Visibility) {
|
|
11
|
-
Visibility[Visibility["visible"] = 0] = "visible";
|
|
12
|
-
Visibility[Visibility["hidden"] = 1] = "hidden";
|
|
13
|
-
Visibility[Visibility["wall"] = 2] = "wall";
|
|
14
|
-
})(Visibility || (Visibility = {}));
|
|
15
9
|
export class Cell {
|
|
16
10
|
constructor(x, y, gridView) {
|
|
17
11
|
this.x = x;
|
|
@@ -175,26 +169,27 @@ export class Cell {
|
|
|
175
169
|
}
|
|
176
170
|
}
|
|
177
171
|
}
|
|
178
|
-
createVisibilityMap(
|
|
172
|
+
createVisibilityMap(isClear, options = {}) {
|
|
173
|
+
var _a, _b;
|
|
174
|
+
const maxDistance = (_a = options.maxDistance) !== null && _a !== void 0 ? _a : Infinity;
|
|
175
|
+
const boundariesVisible = (_b = options.boundariesVisible) !== null && _b !== void 0 ? _b : true;
|
|
179
176
|
return new Pipe2D(this.gridView.width, this.gridView.height, (x, y) => {
|
|
180
|
-
if (
|
|
181
|
-
return
|
|
182
|
-
const distance = getDistance(this, { x, y });
|
|
183
|
-
if (distance > maxDistance) {
|
|
184
|
-
return Visibility.hidden;
|
|
177
|
+
if (!isClear(this)) {
|
|
178
|
+
return x === this.x && y === this.y ? boundariesVisible : false;
|
|
185
179
|
}
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (cell.x === x && cell.y === y) {
|
|
192
|
-
return Visibility.wall;
|
|
193
|
-
}
|
|
194
|
-
return Visibility.hidden;
|
|
180
|
+
if (maxDistance < Infinity) {
|
|
181
|
+
if (getDistance(this, { x, y }) > maxDistance) {
|
|
182
|
+
return false;
|
|
195
183
|
}
|
|
196
184
|
}
|
|
197
|
-
|
|
185
|
+
for (const cell of this.getLineTo({ x, y })) {
|
|
186
|
+
if (isClear(cell))
|
|
187
|
+
continue;
|
|
188
|
+
return cell.x === x && cell.y === y
|
|
189
|
+
? boundariesVisible
|
|
190
|
+
: false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
198
193
|
});
|
|
199
194
|
}
|
|
200
195
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Grid, type GridBase, type Cell, TraversalType
|
|
1
|
+
export { Grid, type GridBase, type Cell, TraversalType } from "./grid.js";
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Grid, TraversalType
|
|
1
|
+
export { Grid, TraversalType } from "./grid.js";
|
package/package.json
CHANGED