117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
package dt
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
func scatter(w, h int, seed uint32) []bool {
|
|
seeds := make([]bool, w*h)
|
|
for i := range seeds {
|
|
seed = seed*1664525 + 1013904223
|
|
seeds[i] = seed>>20&7 == 0
|
|
}
|
|
seeds[0] = true // guarantee at least one
|
|
return seeds
|
|
}
|
|
|
|
// brute is the definition: the smallest squared distance to any seed, with dx measured the short way round
|
|
// when the grid is a cylinder.
|
|
func brute(seeds []bool, w, h, x, y int, wrapX bool) float64 {
|
|
best := math.Inf(1)
|
|
for sy := 0; sy < h; sy++ {
|
|
for sx := 0; sx < w; sx++ {
|
|
if !seeds[sy*w+sx] {
|
|
continue
|
|
}
|
|
dx := float64(x - sx)
|
|
if wrapX {
|
|
if d := math.Abs(dx); d > float64(w)/2 {
|
|
dx = float64(w) - d
|
|
}
|
|
}
|
|
dy := float64(y - sy)
|
|
if d := dx*dx + dy*dy; d < best {
|
|
best = d
|
|
}
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
|
|
func check(t *testing.T, w, h int, wrapX bool) {
|
|
t.Helper()
|
|
seeds := scatter(w, h, 99)
|
|
d2, near := Transform(seeds, w, h, wrapX)
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
want := brute(seeds, w, h, x, y, wrapX)
|
|
i := y*w + x
|
|
if math.Abs(float64(d2[i])-want) > 1e-3 {
|
|
t.Fatalf("wrap=%v cell (%d,%d): d2 %g, brute force %g", wrapX, x, y, d2[i], want)
|
|
}
|
|
// The feature index must be a seed, and it must be one at exactly that distance.
|
|
n := int(near[i])
|
|
if n < 0 || !seeds[n] {
|
|
t.Fatalf("wrap=%v cell (%d,%d): nearest %d is not a seed", wrapX, x, y, n)
|
|
}
|
|
got := brute(onlyAt(w, h, n), w, h, x, y, wrapX)
|
|
if math.Abs(got-want) > 1e-3 {
|
|
t.Fatalf("wrap=%v cell (%d,%d): nearest seed %d is at %g, not %g", wrapX, x, y, n, got, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func onlyAt(w, h, i int) []bool {
|
|
s := make([]bool, w*h)
|
|
s[i] = true
|
|
return s
|
|
}
|
|
|
|
// The one test the coastal pass rests on. Everything there is written in terms of "how far is this cell from
|
|
// the waterline and which stretch does it belong to", so a distance transform that is subtly wrong would not
|
|
// fail loudly - it would put the shelf break in slightly the wrong place everywhere. The transform is exact,
|
|
// so the comparison is against an exhaustive search and the tolerance is float32 rounding.
|
|
func TestMatchesBruteForce(t *testing.T) { check(t, 41, 37, false) }
|
|
|
|
// And the same on a cylinder, which is what a planet is. The failure this catches is a shelf that stops dead
|
|
// at the seam.
|
|
func TestMatchesBruteForceOnACylinder(t *testing.T) { check(t, 41, 37, true) }
|
|
|
|
// A seed on one edge must be found from the other edge, and by the short way round.
|
|
func TestWrapFindsTheSeedAcrossTheSeam(t *testing.T) {
|
|
const w, h = 9, 3
|
|
seeds := make([]bool, w*h)
|
|
seeds[h/2*w+0] = true // one seed, at column 0 of the middle row
|
|
|
|
d2, near := Transform(seeds, w, h, true)
|
|
// Column 8 is one step from column 0 the short way round, eight steps the long way.
|
|
if got := d2[h/2*w+8]; math.Abs(float64(got)-1) > 1e-6 {
|
|
t.Errorf("d2 at column 8 = %g, want 1", got)
|
|
}
|
|
if got := near[h/2*w+8]; got != int32(h/2*w) {
|
|
t.Errorf("near at column 8 = %d, want %d", got, h/2*w)
|
|
}
|
|
// The far side of the cylinder is four steps away either way.
|
|
if got := d2[h/2*w+4]; math.Abs(float64(got)-16) > 1e-6 {
|
|
t.Errorf("d2 at column 4 = %g, want 16", got)
|
|
}
|
|
// Without the wrap the same grid gives eight.
|
|
d2f, _ := Transform(seeds, w, h, false)
|
|
if got := d2f[h/2*w+8]; math.Abs(float64(got)-64) > 1e-6 {
|
|
t.Errorf("unwrapped d2 at column 8 = %g, want 64", got)
|
|
}
|
|
}
|
|
|
|
func TestNoSeedsAtAll(t *testing.T) {
|
|
const w, h = 5, 4
|
|
seeds := make([]bool, w*h)
|
|
_, near := Transform(seeds, w, h, true)
|
|
for i, n := range near {
|
|
if n != -1 {
|
|
t.Fatalf("cell %d reports a nearest seed %d on an empty grid", i, n)
|
|
}
|
|
}
|
|
}
|