@xtia/grid 0.0.3 → 0.0.5

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 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.
@@ -20,7 +22,7 @@ npm i @xtia/grid
20
22
  ```
21
23
 
22
24
  ```ts
23
- import { Grid } from "@xtia/grid";
25
+ import { Grid } from "@xtia/grid"; // ~5.3kb gzipped
24
26
 
25
27
  // initialise a 30x20 Grid<number> of 0's
26
28
  const numGrid = Grid.solid(30, 20, 0);
@@ -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 a **view** into the parent; changes to the subgrid affect the parent and vice-versa.
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, 0); // 0 = grass
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(1); // 1 = water
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(2, mask); // 2 = mountain
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);
@@ -72,85 +74,125 @@ const lakeGrid = Grid.from(lakeRegion);
72
74
 
73
75
  ## Transformation Layers
74
76
 
75
- `grid.map(read, write)` creates a **view** that reads and writes the parent through the provided transformation functions.
77
+ `grid.map(read, write)` creates a **zero-copy 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
- n => sprites[n],
81
- s => sprites.indexOf(s)
81
+ type => type + ".png",
82
+ sprite => sprite.replace(/\..*/, '')
82
83
  );
83
84
 
84
- world.set(0, 0, 2); // modify the parent
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: 3
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.
91
92
 
93
+ ```ts
94
+ // create a one-way, player-centred sprite map
95
+ const viewport = world.pipe
96
+ .oob("mountain")
97
+ .map(t => t + ".png")
98
+ .crop(playerX - 5, playerY - 5, 10, 10);
99
+
100
+ // or get a list of locations of cells with mountains
101
+ const mountainCells = world.cells.toFlatArrayXY()
102
+ .filter(cell => cell.value === "mountain")
103
+ .map(cell => [cell.x, cell.y]);
104
+
105
+ ```
106
+
92
107
  ## Cell interface
93
108
 
94
109
  `grid.cells` provides a Pipe2D, for convenient transformation, of live-view interfaces relating to positions in the grid.
95
110
 
96
111
  ```ts
97
112
  const topLeft = world.cells.get(0, 0);
98
- console.log(topLeft.value); // 2
113
+ console.log(topLeft.value); // "mountain"
99
114
 
100
- topLeft.value = 3; // modifies the underlying data
115
+ topLeft.value = "forest"; // modifies the underlying data
101
116
  console.log(spriteMap.get(0, 0)); // now "forest.png";
102
117
 
103
118
  // navigate by offset
104
119
  const adjacentCell = topLeft.look(1, 0);
105
- console.log(adjacentCell.x, adjacentCell.y); // 1, 0
120
+ console.log(adjacentCell?.x, adjacentCell?.y); // 1, 0
106
121
  ```
107
122
 
108
123
  Cells provide methods for locational utilities such as pathfinding and visibility mapping.
109
124
 
110
125
  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
126
 
112
- ### Path finding
127
+ ### (Everybody needs good) Neighbours
128
+
129
+ `cell.getNeighbours(includeDiagonals?)` returns an array of Cells:
130
+
131
+ ```ts
132
+ // derive a display number for clicked cells in Minesweeper
133
+ const numOfAdjacentMines = clickedCell.getNeighbours(true)
134
+ .filter(cell => cell.value.isMine)
135
+ .length;
113
136
  ```
114
- const costs = [1, Infinity, Infinity, 2]; // grass=1, water/mountain=impassable, forest=2
137
+
138
+ ### Path finding
139
+ ```ts
140
+ const costs = {
141
+ grass: 1,
142
+ water: Infinity,
143
+ mountain: Infinity,
144
+ forest: 2
145
+ };
115
146
 
116
147
  const start = world.cells.get(5, 5);
117
148
  const destination = world.cells.get(90, 90);
118
149
  const path = start.findPath(
119
- destination,
150
+ destination, // or simply [90, 90]
120
151
  (cell) => costs[cell.value]
121
- ); // Cell<number>[]
152
+ ); // Cell<string>[]
122
153
  ```
123
154
 
124
155
  ### Visibility mapping
125
156
 
126
157
  ```ts
158
+ // createVisibilityMap(isClear);
127
159
  const visibility = start.createVisibilityMap(
128
- cell => cell.value == 0 // anything except grass blocks vision
160
+ cell => cell.value === "grass" // anything except grass blocks vision
129
161
  );
130
162
 
131
- // any 2d source, including visibility maps, can be used as a mask
163
+ // any 2d source, including visibility maps, can be used as a mask for paste/fill operations
132
164
  // paste(source: Source2D<T>, mask?: Source2D<boolean>)
133
- screenGrid.paste(spriteMap, visibility.map(v => v != Visibility.hidden));
134
-
135
- // using pipe2d's convenience
136
- const mountainCells = world.cells.toFlatArrayXY().filter(c => c.value === 2);
165
+ screenGrid.paste(spriteMap, visibility);
137
166
  ```
138
167
 
139
168
  ### Reusing path data
140
169
 
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 form that data (and cached) when that pipe is queried.
170
+ 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
171
 
143
172
  ```ts
144
- const pathMap = start.getPathMap(c => costs[c]);
173
+ const pathMap = start.getPathMap(c => costs[c.value]);
145
174
 
146
175
  const pathToCentre = pathMap.get(50, 50);
147
176
  const pathToCorner = pathMap.get(99, 99);
148
177
  ```
149
178
 
179
+ 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.
180
+
150
181
  ## The storage layer
151
182
 
152
183
  `Grid` itself is an interface for reading and writing 2D data. `GridBase` - a subclass of `Grid` - maintains the actual storage of such data.
153
184
 
154
185
  Although the Grid factory methods (`Grid.solid()`, `Grid.from()`, `Grid.init()`) return a `GridBase<T>`, the mental model is that we're simply working with `Grid`s, therefore those factory methods live on `Grid`. The only API distinction is that `GridBase` provides a 'change' event, via `grid.on("change", handler)`.
155
186
 
156
- You can perform batched updates, holding off the 'change' event until the batch process concludes, with `grid.batchUpdate(callback)`.
187
+ You can perform batched updates, holding off the 'change' event until the batch process concludes, with `grid.batchUpdate(callback)`.
188
+
189
+ ## Save and load
190
+
191
+ Pipe2D makes it easy to save and restore grid data:
192
+
193
+ ```ts
194
+ const saved = world.pipe.toFlatArrayXY(); // ["mountain", "forest", "grass", ...]
195
+
196
+ const restoredPipe = Pipe2D.fromFlatArrayXY(saved);
197
+ const restored = Grid.from(restoredPipe);
198
+ ```
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(isWall: (cell: Cell<T>) => boolean, maxDistance?: number): Pipe2D<Visibility>;
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;
@@ -138,7 +132,7 @@ export class Cell {
138
132
  return path;
139
133
  });
140
134
  }
141
- return this.gridView.cells.map((c => { var _a; return (_a = costs.get(c)) !== null && _a !== void 0 ? _a : Infinity; }))
135
+ return this.gridView.cells.map(costs, () => Infinity)
142
136
  .strict();
143
137
  }
144
138
  *getLineTo(target) {
@@ -175,26 +169,27 @@ export class Cell {
175
169
  }
176
170
  }
177
171
  }
178
- createVisibilityMap(isWall, maxDistance = Infinity) {
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 (isWall(this))
181
- return Visibility.hidden;
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
- for (const cell of this.getLineTo({ x, y })) {
187
- if (isWall(cell)) {
188
- if (cell.x === this.x && cell.y === this.y) {
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
- return Visibility.visible;
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, Visibility } from "./grid.js";
1
+ export { Grid, type GridBase, type Cell, TraversalType } from "./grid.js";
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- export { Grid, TraversalType, Visibility } from "./grid.js";
1
+ export { Grid, TraversalType } from "./grid.js";
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.3",
2
+ "version": "0.0.5",
3
3
  "description": "2D store and utilities, with zero-copy regions and transform layers, pathfinding, Pipe2D interop",
4
4
  "name": "@xtia/grid",
5
5
  "exports": {