feat(stdlib): add Grid.* — tile geometry + A* pathfinding over the tilemap (#24)
Grid.* operates on the Map tilemap (Map.size/Map.row): a cell is passable unless
it is out of bounds or holds the caller's `wall` tile (a char code, e.g. '#'), so
any impassable glyph works. Everything is integer and deterministic.
- Grid.line(x0,y0,x1,y1) -> []Cell Bresenham line cells (LOS/raycast base)
- Grid.blocked(x,y,wall) -> bool the shared passability test
- Grid.line_of_sight(x0,y0,x1,y1,wall) unobstructed straight line?
- Grid.flood(x,y,wall) -> []Cell 4-connected reachable region (BFS)
- Grid.a_star(x0,y0,x1,y1,wall) -> []Cell shortest 4-connected path (A*,
Manhattan heuristic), empty if unreachable
The engine (runtime/native/grid.ludic, ~150 lines of Ludic, C-free) is spliced
into a game via core.ludic since it reads the tilemap runtime; returned Cell
slices are ordinary Ludic slices (`len` / `[i]`; each cell has `.x` `.y`).
Pathfinding lives under Grid rather than a `Path` namespace — that name is
already the filesystem-paths library (#10).
Verified against Python references: a 1500-case fuzzer over random maps agrees
exactly on A* path length (optimal, == BFS), flood-fill count, and line-of-sight.
examples/library/grid.ludic asserts the behaviour and is wired into `x test`
(now 61 passed); docs: a Grid section + 5 per-symbol pages, inventory/coverage
green. Seed reseeded; the C-free bootstrap fixpoint holds.
Scope: this lands the Grid.*/pathfinding half of #24. The ECS Query.* helpers
(count/first, and nearest/within which want a runtime spatial index) remain the
tracked follow-up the issue calls out as blocked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b798e3024e
commit
07e5a20c0e
13 changed files with 4852 additions and 3835 deletions
7
docs/language/grid/_section.md
Normal file
7
docs/language/grid/_section.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
id: grid
|
||||||
|
title: Grid
|
||||||
|
order: 27
|
||||||
|
---
|
||||||
|
|
||||||
|
Tile geometry and pathfinding over the <a href="map"><code>Map</code></a> tilemap. A cell is passable unless it is out of bounds or holds the caller's <code>wall</code> tile (a char code, e.g. <code>'#'</code>), so any impassable glyph works. Everything is integer and deterministic — same map and query reproduce the same path every run. <a href="grid-line"><code>Grid.line</code></a>/<a href="grid-flood"><code>Grid.flood</code></a>/<a href="grid-a_star"><code>Grid.a_star</code></a> return <code>Cell</code> slices (index them with <code>len</code> / <code>[i]</code>; each cell has <code>.x</code> and <code>.y</code>).
|
||||||
28
docs/language/grid/grid-a_star.md
Normal file
28
docs/language/grid/grid-a_star.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
id: grid-a_star
|
||||||
|
name: Grid.a_star
|
||||||
|
category: grid
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Grid.a_star
|
||||||
|
sig: Grid.a_star(x0, y0, x1, y1, wall) -> []Cell
|
||||||
|
tip: The shortest 4-connected path between two cells (A*), or an empty list.
|
||||||
|
order: 5
|
||||||
|
ns: Grid
|
||||||
|
member: a_star
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the shortest path from <code>(x0, y0)</code> to <code>(x1, y1)</code> over passable (non-<code>wall</code>) cells, 4-connected with uniform step cost, as a <code>Cell</code> slice from start to goal inclusive — an <strong>A*</strong> search with a Manhattan heuristic. Empty if the goal is unreachable (or start/goal is a wall). (Named under <code>Grid</code> rather than <code>Path</code>, which is the filesystem-paths library.)
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x0`, `y0` — the start cell
|
||||||
|
- `x1`, `y1` — the goal cell
|
||||||
|
- `wall` — the impassable tile char, e.g. `'#'`
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler H phase Update {
|
||||||
|
let path = Grid.a_star(1, 1, 20, 12, '#')
|
||||||
|
if len(path) > 0 { print(len(path)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
26
docs/language/grid/grid-blocked.md
Normal file
26
docs/language/grid/grid-blocked.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
id: grid-blocked
|
||||||
|
name: Grid.blocked
|
||||||
|
category: grid
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Grid.blocked
|
||||||
|
sig: Grid.blocked(x, y, wall) -> bool
|
||||||
|
tip: True if the cell is out of bounds or holds the wall tile.
|
||||||
|
order: 2
|
||||||
|
ns: Grid
|
||||||
|
member: blocked
|
||||||
|
---
|
||||||
|
|
||||||
|
Whether <code>(x, y)</code> blocks movement — <code>true</code> if it is out of bounds or its tile equals <code>wall</code>. The passability test the other Grid functions share.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x`, `y` — the cell
|
||||||
|
- `wall` — the impassable tile char, e.g. `'#'`
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler H phase Update {
|
||||||
|
if Grid.blocked(0, 0, '#') { print(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
27
docs/language/grid/grid-flood.md
Normal file
27
docs/language/grid/grid-flood.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: grid-flood
|
||||||
|
name: Grid.flood
|
||||||
|
category: grid
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Grid.flood
|
||||||
|
sig: Grid.flood(x, y, wall) -> []Cell
|
||||||
|
tip: Every passable cell reachable from (x,y), 4-connected, in BFS order.
|
||||||
|
order: 4
|
||||||
|
ns: Grid
|
||||||
|
member: flood
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns every passable cell reachable from <code>(x, y)</code> by 4-connected steps (up/down/left/right over non-<code>wall</code> cells), in breadth-first order — for connectivity checks, filling a room, or measuring an enclosed area. Empty if the start itself is blocked.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x`, `y` — the seed cell
|
||||||
|
- `wall` — the impassable tile char
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler H phase Update {
|
||||||
|
let region = Grid.flood(2, 2, '#')
|
||||||
|
print(len(region))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
27
docs/language/grid/grid-line.md
Normal file
27
docs/language/grid/grid-line.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: grid-line
|
||||||
|
name: Grid.line
|
||||||
|
category: grid
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Grid.line
|
||||||
|
sig: Grid.line(x0, y0, x1, y1) -> []Cell
|
||||||
|
tip: Every cell a straight line from (x0,y0) to (x1,y1) crosses (Bresenham).
|
||||||
|
order: 1
|
||||||
|
ns: Grid
|
||||||
|
member: line
|
||||||
|
---
|
||||||
|
|
||||||
|
Returns the cells a straight line from <code>(x0, y0)</code> to <code>(x1, y1)</code> passes through, endpoints included, using integer Bresenham — the basis of line-of-sight, ray-casting, and drawing on the grid.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x0`, `y0` — the start cell
|
||||||
|
- `x1`, `y1` — the end cell
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler H phase Update {
|
||||||
|
let cells = Grid.line(0, 0, 5, 3)
|
||||||
|
print(len(cells))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
27
docs/language/grid/grid-line_of_sight.md
Normal file
27
docs/language/grid/grid-line_of_sight.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
id: grid-line_of_sight
|
||||||
|
name: Grid.line_of_sight
|
||||||
|
category: grid
|
||||||
|
kind: namespace-method
|
||||||
|
tokens: Grid.line_of_sight
|
||||||
|
sig: Grid.line_of_sight(x0, y0, x1, y1, wall) -> bool
|
||||||
|
tip: True if the straight line between two cells crosses no wall.
|
||||||
|
order: 3
|
||||||
|
ns: Grid
|
||||||
|
member: line_of_sight
|
||||||
|
---
|
||||||
|
|
||||||
|
Whether an unobstructed straight line connects <code>(x0, y0)</code> and <code>(x1, y1)</code> — <code>true</code> when no cell it crosses (endpoints included) is a <code>wall</code>. Use it for visibility, aggro checks, and cover.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `x0`, `y0` — the viewer cell
|
||||||
|
- `x1`, `y1` — the target cell
|
||||||
|
- `wall` — the impassable tile char
|
||||||
|
|
||||||
|
```ludic
|
||||||
|
program Demo {
|
||||||
|
handler H phase Update {
|
||||||
|
if Grid.line_of_sight(1, 1, 8, 1, '#') { print(1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
62
examples/library/grid.ludic
Normal file
62
examples/library/grid.ludic
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# grid.ludic — Grid.* tile geometry and pathfinding over the Map tilemap. A
|
||||||
|
# tiny map with a wall maze; each assertion that holds prints its number, so a
|
||||||
|
# full run prints: 1 2 3 4 5 6 7 8 9 10 11 12 13
|
||||||
|
# Pathfinding is A* (runtime/native/grid.ludic), 4-connected, `'#'` = wall.
|
||||||
|
program Grid {
|
||||||
|
property Tag { v: int = 0 }
|
||||||
|
model Marker { Tag }
|
||||||
|
|
||||||
|
handler Boot phase Start {
|
||||||
|
# 01234567
|
||||||
|
Map.size(8, 5)
|
||||||
|
Map.row(0, "########")
|
||||||
|
Map.row(1, "#......#")
|
||||||
|
Map.row(2, "#.####.#")
|
||||||
|
Map.row(3, "#....#.#")
|
||||||
|
Map.row(4, "########")
|
||||||
|
|
||||||
|
# --- Bresenham line ---
|
||||||
|
let hl = Grid.line(1, 1, 6, 1)
|
||||||
|
if len(hl) == 6 { print(1) }
|
||||||
|
let dl = Grid.line(0, 0, 3, 3)
|
||||||
|
if len(dl) == 4 { print(2) }
|
||||||
|
|
||||||
|
# --- line of sight (a clear corridor vs one crossing a wall) ---
|
||||||
|
if Grid.line_of_sight(1, 1, 6, 1, '#') { print(3) }
|
||||||
|
if not Grid.line_of_sight(1, 1, 4, 3, '#') { print(4) }
|
||||||
|
|
||||||
|
# --- flood fill: every floor cell reachable from (1,1) ---
|
||||||
|
if len(Grid.flood(1, 1, '#')) == 13 { print(5) }
|
||||||
|
|
||||||
|
# --- A* shortest path around the maze ---
|
||||||
|
let p = Grid.a_star(1, 1, 6, 3, '#')
|
||||||
|
if len(p) == 8 { print(6) }
|
||||||
|
if p[0].x == 1 and p[0].y == 1 { print(7) }
|
||||||
|
let last = p[len(p) - 1]
|
||||||
|
if last.x == 6 and last.y == 3 { print(8) }
|
||||||
|
|
||||||
|
# the path is contiguous (each step 4-adjacent) and never crosses a wall
|
||||||
|
var ok = true
|
||||||
|
var i = 1
|
||||||
|
while i < len(p) {
|
||||||
|
let dx = p[i].x - p[i - 1].x
|
||||||
|
let dy = p[i].y - p[i - 1].y
|
||||||
|
var ad = dx
|
||||||
|
if ad < 0 { ad = 0 - ad }
|
||||||
|
var ay = dy
|
||||||
|
if ay < 0 { ay = 0 - ay }
|
||||||
|
if ad + ay != 1 { ok = false }
|
||||||
|
if Grid.blocked(p[i].x, p[i].y, '#') { ok = false }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
if ok { print(9) }
|
||||||
|
|
||||||
|
# --- unreachable / blocked cases ---
|
||||||
|
if len(Grid.a_star(1, 1, 3, 2, '#')) == 0 { print(10) } # (3,2) is a wall
|
||||||
|
if Grid.blocked(3, 2, '#') { print(11) }
|
||||||
|
if not Grid.blocked(1, 1, '#') { print(12) }
|
||||||
|
if Grid.blocked(0 - 1, 0, '#') { print(13) } # out of bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
handler Run phase Update { quit() }
|
||||||
|
}
|
||||||
|
|
@ -397,6 +397,7 @@ import "inflate.ludic"
|
||||||
import "image.ludic"
|
import "image.ludic"
|
||||||
import "truetype.ludic"
|
import "truetype.ludic"
|
||||||
import "ui.ludic"
|
import "ui.ludic"
|
||||||
|
import "grid.ludic"
|
||||||
|
|
||||||
# ---- tilemap --------------------------------------------------------------
|
# ---- tilemap --------------------------------------------------------------
|
||||||
# A character grid the game paints with map_row() and reads with tile(). Stored
|
# A character grid the game paints with map_row() and reads with tile(). Stored
|
||||||
|
|
|
||||||
174
runtime/native/grid.ludic
Normal file
174
runtime/native/grid.ludic
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
# ============================================================================
|
||||||
|
# grid.ludic — grid geometry and pathfinding over the tilemap, in Ludic.
|
||||||
|
#
|
||||||
|
# The Grid.* / Path.* namespaces (see emit_call.ludic) operate on the tilemap
|
||||||
|
# that Map.size/Map.row set up (rt_map / rt_tile in core.ludic). A cell is
|
||||||
|
# passable unless it is out of bounds or holds the caller's `wall` tile — a
|
||||||
|
# single char code, e.g. '#', so any impassable glyph works. Everything is
|
||||||
|
# integer and deterministic: same map + same query -> same path, every run.
|
||||||
|
#
|
||||||
|
# ludicc splices this file into a game via core.ludic (it needs the tilemap), so
|
||||||
|
# it links only where the tilemap does. Returned cell lists are ordinary Ludic
|
||||||
|
# slices — index them with `len` / `[i]` (Ludic's `for` iterates ranges, not
|
||||||
|
# collections).
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
property Cell { x: int = 0, y: int = 0 }
|
||||||
|
|
||||||
|
function grid_abs(v: int) -> int { if v < 0 { return 0 - v }; return v }
|
||||||
|
function grid_in_bounds(x: int, y: int) -> bool { return x >= 0 and y >= 0 and x < rt_mapw and y < rt_maph }
|
||||||
|
# a cell blocks movement if it is out of bounds or holds the `wall` tile.
|
||||||
|
function grid_blocked(x: int, y: int, wall: int) -> bool {
|
||||||
|
if not grid_in_bounds(x, y) { return true }
|
||||||
|
return rt_tile(x, y) == wall
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bresenham line from (x0,y0) to (x1,y1), inclusive — every cell it crosses.
|
||||||
|
function grid_line(x0: int, y0: int, x1: int, y1: int) -> []Cell {
|
||||||
|
let out = new []Cell
|
||||||
|
var x = x0
|
||||||
|
var y = y0
|
||||||
|
let dx = grid_abs(x1 - x0)
|
||||||
|
let dy = grid_abs(y1 - y0)
|
||||||
|
var sx = 0 - 1
|
||||||
|
if x0 < x1 { sx = 1 }
|
||||||
|
var sy = 0 - 1
|
||||||
|
if y0 < y1 { sy = 1 }
|
||||||
|
var err = dx - dy
|
||||||
|
while true {
|
||||||
|
let c = new Cell
|
||||||
|
c.x = x; c.y = y
|
||||||
|
push(out, c)
|
||||||
|
if x == x1 and y == y1 { break }
|
||||||
|
let e2 = 2 * err
|
||||||
|
if e2 > 0 - dy { err = err - dy; x = x + sx }
|
||||||
|
if e2 < dx { err = err + dx; y = y + sy }
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
# line of sight: true if the straight line hits no `wall` cell (endpoints incl).
|
||||||
|
function grid_line_of_sight(x0: int, y0: int, x1: int, y1: int, wall: int) -> bool {
|
||||||
|
let cells = grid_line(x0, y0, x1, y1)
|
||||||
|
var i = 0
|
||||||
|
while i < len(cells) {
|
||||||
|
if grid_blocked(cells[i].x, cells[i].y, wall) { return false }
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4-connected flood fill: every passable cell reachable from (sx,sy), BFS order.
|
||||||
|
function grid_flood(sx: int, sy: int, wall: int) -> []Cell {
|
||||||
|
let out = new []Cell
|
||||||
|
if grid_blocked(sx, sy, wall) { return out }
|
||||||
|
let w = rt_mapw
|
||||||
|
let n = w * rt_maph
|
||||||
|
let seen = bytes(n)
|
||||||
|
var i = 0
|
||||||
|
while i < n { seen[i] = 0; i = i + 1 }
|
||||||
|
let qx = words(n)
|
||||||
|
let qy = words(n)
|
||||||
|
var head = 0
|
||||||
|
var tail = 0
|
||||||
|
qx[tail] = sx; qy[tail] = sy; tail = tail + 1
|
||||||
|
seen[sy * w + sx] = 1
|
||||||
|
while head < tail {
|
||||||
|
let cx = qx[head]
|
||||||
|
let cy = qy[head]
|
||||||
|
head = head + 1
|
||||||
|
let c = new Cell
|
||||||
|
c.x = cx; c.y = cy
|
||||||
|
push(out, c)
|
||||||
|
var dir = 0
|
||||||
|
while dir < 4 {
|
||||||
|
var nx = cx
|
||||||
|
var ny = cy
|
||||||
|
if dir == 0 { nx = cx + 1 }
|
||||||
|
if dir == 1 { nx = cx - 1 }
|
||||||
|
if dir == 2 { ny = cy + 1 }
|
||||||
|
if dir == 3 { ny = cy - 1 }
|
||||||
|
if grid_in_bounds(nx, ny) and seen[ny * w + nx] == 0 and not grid_blocked(nx, ny, wall) {
|
||||||
|
seen[ny * w + nx] = 1
|
||||||
|
qx[tail] = nx; qy[tail] = ny; tail = tail + 1
|
||||||
|
}
|
||||||
|
dir = dir + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
# A* shortest path over the 4-connected grid, uniform step cost, Manhattan
|
||||||
|
# heuristic. Returns the path start..goal inclusive, or an empty list if the
|
||||||
|
# goal is unreachable (or start/goal is a wall). The open set is a linear scan —
|
||||||
|
# ample for a tilemap (<= 96x64), and the heuristic keeps it near-optimal work.
|
||||||
|
function path_a_star(x0: int, y0: int, x1: int, y1: int, wall: int) -> []Cell {
|
||||||
|
let out = new []Cell
|
||||||
|
if grid_blocked(x0, y0, wall) or grid_blocked(x1, y1, wall) { return out }
|
||||||
|
let w = rt_mapw
|
||||||
|
let n = w * rt_maph
|
||||||
|
let INF = 1000000000
|
||||||
|
let g = words(n) # cost from start (INF = unvisited)
|
||||||
|
let came = words(n) # parent cell index (-1 = none)
|
||||||
|
let inopen = bytes(n)
|
||||||
|
let closed = bytes(n)
|
||||||
|
var i = 0
|
||||||
|
while i < n { g[i] = INF; came[i] = 0 - 1; inopen[i] = 0; closed[i] = 0; i = i + 1 }
|
||||||
|
let start = y0 * w + x0
|
||||||
|
let goal = y1 * w + x1
|
||||||
|
g[start] = 0
|
||||||
|
inopen[start] = 1
|
||||||
|
var found = false
|
||||||
|
while true {
|
||||||
|
var best = 0 - 1
|
||||||
|
var bestf = INF
|
||||||
|
i = 0
|
||||||
|
while i < n {
|
||||||
|
if inopen[i] == 1 {
|
||||||
|
let cx = i - (i / w) * w
|
||||||
|
let cy = i / w
|
||||||
|
let f = g[i] + grid_abs(cx - x1) + grid_abs(cy - y1)
|
||||||
|
if f < bestf { bestf = f; best = i }
|
||||||
|
}
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
if best < 0 { break }
|
||||||
|
if best == goal { found = true; break }
|
||||||
|
inopen[best] = 0
|
||||||
|
closed[best] = 1
|
||||||
|
let cx = best - (best / w) * w
|
||||||
|
let cy = best / w
|
||||||
|
var dir = 0
|
||||||
|
while dir < 4 {
|
||||||
|
var nx = cx
|
||||||
|
var ny = cy
|
||||||
|
if dir == 0 { nx = cx + 1 }
|
||||||
|
if dir == 1 { nx = cx - 1 }
|
||||||
|
if dir == 2 { ny = cy + 1 }
|
||||||
|
if dir == 3 { ny = cy - 1 }
|
||||||
|
if grid_in_bounds(nx, ny) and not grid_blocked(nx, ny, wall) {
|
||||||
|
let ni = ny * w + nx
|
||||||
|
if closed[ni] == 0 {
|
||||||
|
let ng = g[best] + 1
|
||||||
|
if ng < g[ni] { g[ni] = ng; came[ni] = best; inopen[ni] = 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir = dir + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if not found { return out }
|
||||||
|
# reconstruct goal..start, then reverse into out
|
||||||
|
let rev = new []Cell
|
||||||
|
var cur = goal
|
||||||
|
while cur >= 0 {
|
||||||
|
let c = new Cell
|
||||||
|
c.x = cur - (cur / w) * w
|
||||||
|
c.y = cur / w
|
||||||
|
push(rev, c)
|
||||||
|
if cur == start { break }
|
||||||
|
cur = came[cur]
|
||||||
|
}
|
||||||
|
var k = len(rev) - 1
|
||||||
|
while k >= 0 { push(out, rev[k]); k = k - 1 }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
@ -203,6 +203,18 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
||||||
if (meth == "end") { bare = "regex_end"; push(labels, "match"); push(labels, "n") }
|
if (meth == "end") { bare = "regex_end"; push(labels, "match"); push(labels, "n") }
|
||||||
if (meth == "ok") { bare = "regex_ok"; push(labels, "match") }
|
if (meth == "ok") { bare = "regex_ok"; push(labels, "match") }
|
||||||
}
|
}
|
||||||
|
# Grid.* — tile geometry and pathfinding over the Map tilemap, from
|
||||||
|
# runtime/native/grid.ludic (spliced with core.ludic). `wall` is the impassable
|
||||||
|
# tile char, e.g. '#'. line/flood/a_star return []Cell slices. (Pathfinding
|
||||||
|
# lives under Grid rather than a `Path` namespace — that name is the filesystem
|
||||||
|
# paths library.)
|
||||||
|
if (ns == "Grid") {
|
||||||
|
if (meth == "line") { bare = "grid_line"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1") }
|
||||||
|
if (meth == "blocked") { bare = "grid_blocked"; push(labels, "x"); push(labels, "y"); push(labels, "wall") }
|
||||||
|
if (meth == "line_of_sight") { bare = "grid_line_of_sight"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1"); push(labels, "wall") }
|
||||||
|
if (meth == "flood") { bare = "grid_flood"; push(labels, "x"); push(labels, "y"); push(labels, "wall") }
|
||||||
|
if (meth == "a_star") { bare = "path_a_star"; push(labels, "x0"); push(labels, "y0"); push(labels, "x1"); push(labels, "y1"); push(labels, "wall") }
|
||||||
|
}
|
||||||
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
|
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
|
||||||
reorder_named(e, labels)
|
reorder_named(e, labels)
|
||||||
let id = node(E_ID); id.s = bare; e.a = id
|
let id = node(E_ID); id.s = bare; e.a = id
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -501,5 +501,12 @@
|
||||||
"regex-start",
|
"regex-start",
|
||||||
"regex-end",
|
"regex-end",
|
||||||
"regex-ok"
|
"regex-ok"
|
||||||
|
],
|
||||||
|
"grid": [
|
||||||
|
"grid-line",
|
||||||
|
"grid-blocked",
|
||||||
|
"grid-line_of_sight",
|
||||||
|
"grid-flood",
|
||||||
|
"grid-a_star"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ function cmd_test() -> int {
|
||||||
feat_case("library/uuid", "", "1 2 3 4 5 6 7 8 9 10", "uuid.ludic (Uuid v4/v7 format, version/variant, parse/equals)")
|
feat_case("library/uuid", "", "1 2 3 4 5 6 7 8 9 10", "uuid.ludic (Uuid v4/v7 format, version/variant, parse/equals)")
|
||||||
feat_case("library/noise", "", "1 2 3 4 5 6 7 8 9 10 11", "noise.ludic (Noise value/perlin/simplex/fbm/cellular determinism + range)")
|
feat_case("library/noise", "", "1 2 3 4 5 6 7 8 9 10 11", "noise.ludic (Noise value/perlin/simplex/fbm/cellular determinism + range)")
|
||||||
feat_case("library/regex", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "regex.ludic (Regex match/find/groups/classes/quantifiers/replace + linear-time safety)")
|
feat_case("library/regex", "", "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18", "regex.ludic (Regex match/find/groups/classes/quantifiers/replace + linear-time safety)")
|
||||||
|
feat_case("library/grid", "", "1 2 3 4 5 6 7 8 9 10 11 12 13", "grid.ludic (Grid line/flood/line_of_sight + A* pathfinding over the tilemap)")
|
||||||
feat_case("library/logging", "", "0 5 2 1", "logging.ludic (Log levels, set_level/level threshold, structured fields)")
|
feat_case("library/logging", "", "0 5 2 1", "logging.ludic (Log levels, set_level/level threshold, structured fields)")
|
||||||
# Os known-folders/arch and Fs.list read the BSD utsname/dirent layout, so
|
# Os known-folders/arch and Fs.list read the BSD utsname/dirent layout, so
|
||||||
# their asserted values are macOS-specific; skip off Darwin (see is_darwin).
|
# their asserted values are macOS-specific; skip off Darwin (see is_darwin).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue