174 lines
5.7 KiB
Text
174 lines
5.7 KiB
Text
# ============================================================================
|
|
# 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 -v }; return v }
|
|
function grid_in_bounds(rt_core_st: RtCoreState, x: int, y: int) -> bool { return x >= 0 and y >= 0 and x < rt_core_st.rt_mapw and y < rt_core_st.rt_maph }
|
|
# a cell blocks movement if it is out of bounds or holds the `wall` tile.
|
|
function grid_blocked(rt_core_st: RtCoreState, x: int, y: int, wall: int) -> bool {
|
|
if not grid_in_bounds(rt_core_st, x, y) { return true }
|
|
return rt_tile(rt_core_st, 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 = -1
|
|
if x0 < x1 { sx = 1 }
|
|
var sy = -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 > -dy { err -= dy; x += sx }
|
|
if e2 < dx { err += dx; y += sy }
|
|
}
|
|
return out
|
|
}
|
|
|
|
# line of sight: true if the straight line hits no `wall` cell (endpoints incl).
|
|
function grid_line_of_sight(rt_core_st: RtCoreState, 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(rt_core_st, cells[i].x, cells[i].y, wall) { return false }
|
|
i += 1
|
|
}
|
|
return true
|
|
}
|
|
|
|
# 4-connected flood fill: every passable cell reachable from (sx,sy), BFS order.
|
|
function grid_flood(rt_core_st: RtCoreState, sx: int, sy: int, wall: int) -> []Cell {
|
|
let out = new []Cell
|
|
if grid_blocked(rt_core_st, sx, sy, wall) { return out }
|
|
let w = rt_core_st.rt_mapw
|
|
let n = w * rt_core_st.rt_maph
|
|
let seen = bytes(n)
|
|
var i = 0
|
|
while i < n { seen[i] = 0; i += 1 }
|
|
let qx = words(n)
|
|
let qy = words(n)
|
|
var head = 0
|
|
var tail = 0
|
|
qx[tail] = sx; qy[tail] = sy; tail += 1
|
|
seen[sy * w + sx] = 1
|
|
while head < tail {
|
|
let cx = qx[head]
|
|
let cy = qy[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(rt_core_st, nx, ny) and seen[ny * w + nx] == 0 and not grid_blocked(rt_core_st, nx, ny, wall) {
|
|
seen[ny * w + nx] = 1
|
|
qx[tail] = nx; qy[tail] = ny; tail += 1
|
|
}
|
|
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(rt_core_st: RtCoreState, x0: int, y0: int, x1: int, y1: int, wall: int) -> []Cell {
|
|
let out = new []Cell
|
|
if grid_blocked(rt_core_st, x0, y0, wall) or grid_blocked(rt_core_st, x1, y1, wall) { return out }
|
|
let w = rt_core_st.rt_mapw
|
|
let n = w * rt_core_st.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] = -1; inopen[i] = 0; closed[i] = 0; 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 = -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 += 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(rt_core_st, nx, ny) and not grid_blocked(rt_core_st, 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 += 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 -= 1 }
|
|
return out
|
|
}
|