feat(nav): 17.3 - a map in square tiles (nav_tiled, nav_tile_build; a tile must be a whole number of cells or its seams never join, and the shim refuses one that is not; 64-bit polygon refs for 16384 tiles), ground as a NavGround with boulders as convex footprints, one save format for one tile or many, and nav_random_near - a reachable point from the caller's seed, bit-identical on the Mac and the PC. 10 tests on both

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 15:42:37 +03:00
parent 9b8606483d
commit e6893f58be
16 changed files with 436 additions and 109 deletions

View file

@ -0,0 +1,61 @@
# fakes/meadow.ludic - ground built by hand for ludic.nav's tests: n metre squares a side, flat at
# y = 0; river 1 crosses x 18..22 with a ford at z 30..34, river 2 without one, river 0 is none
function ground(n: int, river: int) -> NavGround {
let g = new NavGround
g.nv = (n + 1) * (n + 1)
g.v = floats(g.nv * 3)
for j in 0 .. n + 1 {
for i in 0 .. n + 1 {
let o = (j * (n + 1) + i) * 3
g.v[o] = float(i)
g.v[o + 2] = float(j)
}
}
g.nt = n * n * 2
g.t = new []int
g.area = buffer(g.nt)
for j in 0 .. n {
for i in 0 .. n {
let a = j * (n + 1) + i
push(g.t, a)
push(g.t, a + n + 1)
push(g.t, a + 1)
push(g.t, a + 1)
push(g.t, a + n + 1)
push(g.t, a + n + 2)
var k = 1
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
g.area[(j * n + i) * 2] = k
g.area[(j * n + i) * 2 + 1] = k
}
}
return g
}
# a post r wide at (x, z), from under the ground to over a walker's head
function post_at(x: float, z: float, r: float) -> []float {
let c = floats(5)
c[0] = x
c[1] = -1.0
c[2] = z
c[3] = r
c[4] = 4.0
return c
}
# a square footprint 2h across at (x, z), as a boulder's is written: n, the corners, ymin, ymax
function square_at(x: float, z: float, h: float) -> []float {
let f = floats(11)
f[0] = 4.0
f[1] = x - h
f[2] = z - h
f[3] = x + h
f[4] = z - h
f[5] = x + h
f[6] = z + h
f[7] = x - h
f[8] = z + h
f[9] = -1.0
f[10] = 3.0
return f
}

View file

@ -3,47 +3,15 @@
# the ford, stops at the bank when there is none, and a saved mesh answers as the built one did.
import "ludic.nav"
import "ludic.base"
import "fakes/meadow.ludic"
program NavTest {
numbers float
# the meadow, flat at y = 0; river 1 has a ford at z 30..34, river 2 none, river 0 is no river
function meadow(nav_st: mut NavState, kind: int, river: int, post: bool) -> bool {
let n = 40
let v = floats((n + 1) * (n + 1) * 3)
for j in 0 .. n + 1 {
for i in 0 .. n + 1 {
let o = (j * (n + 1) + i) * 3
v[o] = float(i)
v[o + 1] = 0.0
v[o + 2] = float(j)
}
}
let t = new []int
let area = buffer(n * n * 2)
for j in 0 .. n {
for i in 0 .. n {
let a = j * (n + 1) + i
push(t, a)
push(t, a + n + 1)
push(t, a + 1)
push(t, a + 1)
push(t, a + n + 1)
push(t, a + n + 2)
var k = 1
if river > 0 and i >= 18 and i < 22 and not (river == 1 and j >= 30 and j < 34) { k = 0 }
area[(j * n + i) * 2] = k
area[(j * n + i) * 2 + 1] = k
}
}
let cyl = floats(5)
cyl[0] = 20.0
cyl[1] = -1.0
cyl[2] = 20.0
cyl[3] = 2.0
cyl[4] = 4.0
var nc = 0
if post { nc = 1 }
return nav_build(nav_st, kind, v, (n + 1) * (n + 1), t, n * n * 2, area, cyl, nc, new NavConfig)
let g = ground(40, river)
if post { g.cyl = post_at(20.0, 20.0, 2.0) }
if post { g.nc = 1 }
return nav_build(nav_st, kind, g, new NavConfig)
}
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
@ -92,6 +60,31 @@ program NavTest {
expect_eq(nav_path(nav_st, NAV_LARGE, 5.0, 0.0, 5.0, 10.0, 0.0, 10.0), -1)
}
test "a random point near is always one a walker there can reach, and a seed is always the same point" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 2, false))
for seed in 0 .. 50 {
expect(nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, seed))
expect(nav_near_x(nav_st) < 18.0)
}
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
let x7 = nav_near_x(nav_st)
let z7 = nav_near_z(nav_st)
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 8)
expect(Math.abs(nav_near_x(nav_st) - x7) + Math.abs(nav_near_z(nav_st) - z7) > 0.01)
nav_random_near(nav_st, NAV_PERSON, 5.0, 0.0, 10.0, 30.0, 7)
expect_eq(nav_near_x(nav_st), x7)
expect_eq(nav_near_z(nav_st), z7)
}
test "a boulder's footprint is walked round" (nav_st: mut NavState) {
let g = ground(40, 0)
g.foot = square_at(20.0, 20.0, 3.0)
g.nf = len(g.foot)
expect(nav_build(nav_st, NAV_PERSON, g, new NavConfig))
expect(not nav_straight(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 20.0))
expect(nav_path(nav_st, NAV_PERSON, 5.0, 0.0, 20.0, 35.0, 0.0, 20.0) >= 3)
}
test "a mesh saved and loaded answers as the built one did" (nav_st: mut NavState) {
expect(meadow(nav_st, NAV_PERSON, 1, true))
let path = Os.temp_dir() + "/ludic_nav_test.navmesh"

View file

@ -0,0 +1,66 @@
# tiles_test.ludic - ludic.nav's map in tiles: a 128 m meadow as four 64 m tiles, one of them under
# water. A path crosses the tiles' seams, the water's tile has no polygons and nothing inside it is
# a way's end, and the set saved and loaded answers as the built one did.
import "ludic.nav"
import "ludic.base"
import "fakes/meadow.ludic"
program TilesTest {
numbers float
# four tiles over ground(128), the far corner's squares (x and z past 64) under water when wet
function map(nav_st: mut NavState, wet: bool) -> int {
let g = ground(128, 0)
if wet {
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2] = 0 } }
for j in 64 .. 128 { for i in 64 .. 128 { g.area[(j * 128 + i) * 2 + 1] = 0 } }
}
g.cyl = post_at(96.0, 20.0, 2.0)
g.nc = 1
expect(nav_tiled(nav_st, NAV_PERSON, 0.0, 0.0, 64.0, 16, 4096))
var empty = 0
for tz in 0 .. 2 {
for tx in 0 .. 2 {
let p = nav_tile_build(nav_st, NAV_PERSON, tx, tz, g, new NavConfig)
expect(p >= 0)
if p == 0 { empty += 1 }
}
}
return empty
}
function last_x(nav_st: NavState) -> float { return nav_corner_x(nav_st, nav_corners(nav_st) - 1) }
function last_z(nav_st: NavState) -> float { return nav_corner_z(nav_st, nav_corners(nav_st) - 1) }
test "a path crosses the tiles' seams and goes round the post in the far tile" (nav_st: mut NavState) {
expect_eq(map(nav_st, false), 0)
expect(nav_polygons(nav_st, NAV_PERSON) > 0)
let n = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
expect(n >= 3)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_x(nav_st) - 120.0) < 0.1)
let d = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 120.0, 0.0, 120.0)
expect(d >= 2)
expect(not nav_partial(nav_st, NAV_PERSON))
}
test "a tile under water has no polygons: a way into it is none, the shore beside it is reached" (nav_st: mut NavState) {
expect_eq(map(nav_st, true), 1)
expect(not nav_nearest(nav_st, NAV_PERSON, 100.0, 0.0, 100.0))
expect_eq(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 70.0, 0.0, 70.0), -1)
expect(nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 10.0, 62.0, 0.0, 70.0) >= 2)
expect(not nav_partial(nav_st, NAV_PERSON))
expect(Math.abs(last_z(nav_st) - 70.0) < 0.1)
}
test "the tiles saved and loaded answer as the built ones did" (nav_st: mut NavState) {
map(nav_st, true)
let path = Os.temp_dir() + "/ludic_nav_tiles.navmesh"
expect(nav_save_file(nav_st, NAV_PERSON, path))
expect(nav_load_file(nav_st, NAV_LARGE, path))
expect_eq(nav_polygons(nav_st, NAV_LARGE), nav_polygons(nav_st, NAV_PERSON))
let a = nav_path(nav_st, NAV_PERSON, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
let ax = last_x(nav_st)
let b = nav_path(nav_st, NAV_LARGE, 10.0, 0.0, 20.0, 120.0, 0.0, 20.0)
expect_eq(a, b)
expect_near(last_x(nav_st), ax, 0.0001)
}
}