Files
Ransom-s-wake/tests/unit/run_site_baker_tests.gd
T
OlivierandClaude Fable 5.1 d91e6bd02f fix(tools): canyons impose a floor material through rock outcrops; tighten showcase contracts
Review findings: the mesa forced rock onto the canyon bed, two runners
declared the wrong TEST_COUNT, and the escarpment check could pass on the
slope alone. Canyon gains floor_material, the showcase is re-baked with sand
beds, the contract measures the step across the crest band, counts distinct
ledges, and probes the crevasse against the open plateau. Degenerate recipe
parameters now fail an assert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 22:00:24 +02:00

232 lines
12 KiB
GDScript

extends SceneTree
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const SiteTerrainBaker = preload("res://tools/site_terrain_baker.gd")
const VehicleTestRecipe = preload("res://tools/sites/vehicle_test_terrain_recipe.gd")
const Zone1Recipe = preload("res://tools/sites/zone1_vigilant_recipe.gd")
const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd")
const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res"
const TEST_COUNT := 44
var _failures: Array[String] = []
func _init() -> void:
_test_plane_and_blob()
_test_materials_and_determinism()
_test_modifiers_and_primitive_materials()
_test_zone1_artefact_is_stable()
_test_canyon()
_test_escarpment()
_test_outcrop()
_test_boulder_field()
_test_strata()
if _failures.is_empty():
print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT)
quit(0)
else:
for failure: String in _failures:
push_error(failure)
print("Site baker unit tests: %d failed." % _failures.size())
quit(1)
func _small_recipe() -> SiteTerrainBaker.Recipe:
var recipe := SiteTerrainBaker.Recipe.new()
recipe.columns = 21
recipe.rows = 21
recipe.cell_size_m = 2.0
recipe.origin_x_m = -20.0
recipe.origin_z_m = -20.0
recipe.floor_elevation_m = -80.0
recipe.rock_slope_degrees = 20.0
return recipe
func _test_plane_and_blob() -> void:
var recipe := _small_recipe()
recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.1, 0.0, 0.0))
var data := SiteTerrainBaker.bake(recipe)
_expect(data != null and data.validation_error().is_empty(), "plane recipe bakes valid data")
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -30.0), "plane depth at the reference point")
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 20)], -28.0), "plane slopes along +Z")
_expect(data.materials[data.index_of(10, 10)] == SonarMaterial.Value.SAND, "gentle plane is sand")
recipe.primitives.append(SiteTerrainBaker.Blob.new(Vector2(0.0, 0.0), 4.0, 4.0, 10.0))
data = SiteTerrainBaker.bake(recipe)
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -20.0), "blob adds its amplitude at the centre")
_expect(data.materials[data.index_of(12, 10)] == SonarMaterial.Value.ROCK, "steep blob flank is rock")
func _test_materials_and_determinism() -> void:
var recipe := _small_recipe()
recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.0, 0.0, 0.0))
recipe.primitives.append(SiteTerrainBaker.Roughness.new(1933, 0.05, 0.5))
recipe.material_patches.append(
SiteTerrainBaker.MaterialPatch.new(Vector2(10.0, 10.0), 3.0, SonarMaterial.Value.ROCK)
)
var first := SiteTerrainBaker.bake(recipe)
var second := SiteTerrainBaker.bake(recipe)
_expect(first.elevations_m == second.elevations_m, "same recipe bakes identical elevations")
_expect(first.materials == second.materials, "same recipe bakes identical materials")
_expect(first.materials[first.index_of(15, 15)] == SonarMaterial.Value.ROCK, "material patch overrides the slope rule")
var calibration := SiteTerrainBaker.bake(VehicleTestRecipe.recipe())
_expect(calibration != null and calibration.validation_error().is_empty(), "calibration recipe bakes valid data")
## Test doubles for the extension points: a modifier that lifts the field by one metre
## and a primitive that forces rock on a flat plane.
class Lift extends SiteTerrainBaker.Modifier:
func modify(_x_m: float, _z_m: float, elevation_m: float, _gradient: Vector2) -> float:
return elevation_m + 1.0
class GradientProbe extends SiteTerrainBaker.Modifier:
var gradient_at_centre := Vector2.ZERO
func modify(x_m: float, z_m: float, elevation_m: float, gradient: Vector2) -> float:
if is_zero_approx(x_m) and is_zero_approx(z_m):
gradient_at_centre = gradient
return elevation_m
class RockDisc extends SiteTerrainBaker.Primitive:
func material_at(x_m: float, z_m: float) -> int:
if Vector2(x_m, z_m).length() <= 5.0:
return SonarMaterial.Value.ROCK
return SiteTerrainBaker.NO_MATERIAL
func _test_modifiers_and_primitive_materials() -> void:
var recipe := _small_recipe()
recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.2, 0.0, 0.0, 0.0))
var probe := GradientProbe.new()
recipe.modifiers.append(probe)
recipe.modifiers.append(Lift.new())
var data := SiteTerrainBaker.bake(recipe)
_expect(is_equal_approx(data.elevations_m[data.index_of(10, 10)], -29.0), "modifier runs after the primitive sum")
_expect(probe.gradient_at_centre.is_equal_approx(Vector2(0.2, 0.0)), "modifier receives the local gradient (%s)" % probe.gradient_at_centre)
var flat := _small_recipe()
flat.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.0, 0.0, 0.0))
flat.primitives.append(RockDisc.new())
var flat_data := SiteTerrainBaker.bake(flat)
_expect(flat_data.materials[flat_data.index_of(10, 10)] == SonarMaterial.Value.ROCK, "primitive material wins over the slope rule")
_expect(flat_data.materials[flat_data.index_of(0, 0)] == SonarMaterial.Value.SAND, "primitive material is local")
func _test_canyon() -> void:
var canyon := SiteTerrainBaker.Canyon.new(
PackedVector2Array([Vector2(-20.0, 0.0), Vector2(0.0, 0.0), Vector2(0.0, 20.0)]),
4.0, 10.0, 1.0, 0, 0.0, SonarMaterial.Value.SAND
)
_expect(is_equal_approx(canyon.elevation_delta(-10.0, 0.0), -10.0), "canyon floor sits at depth on the first leg")
_expect(canyon.material_at(-10.0, 0.0) == SonarMaterial.Value.SAND, "canyon imposes its floor material on the flat floor")
_expect(canyon.material_at(-10.0, 4.0) == SiteTerrainBaker.NO_MATERIAL, "canyon leaves its walls to the slope rule")
_expect(is_equal_approx(canyon.elevation_delta(1.5, 10.0), -10.0), "canyon floor is flat across its width on the second leg")
_expect(is_equal_approx(canyon.elevation_delta(-10.0, 6.0), 0.0), "canyon stops past half width plus softness")
_expect(is_equal_approx(canyon.elevation_delta(15.0, -15.0), 0.0), "canyon is zero far from the path")
_expect(is_equal_approx(canyon.elevation_delta(0.0, 20.0), 0.0), "canyon closes at its last vertex")
_expect(canyon.elevation_delta(-10.0, 4.0) < -4.0 and canyon.elevation_delta(-10.0, 4.0) > -6.0, "canyon wall is half way down at half width")
var rough := SiteTerrainBaker.Canyon.new(
PackedVector2Array([Vector2(-200.0, 0.0), Vector2(200.0, 0.0)]), 8.0, 10.0, 2.0, 7, 3.0, SiteTerrainBaker.NO_MATERIAL
)
var wall_values: Dictionary = {}
for x in range(-100, 101, 10):
wall_values[snappedf(rough.elevation_delta(float(x), 8.0), 0.01)] = true
_expect(wall_values.size() > 3, "outline noise makes the canyon wall irregular along the path")
func _test_escarpment() -> void:
var cliff := SiteTerrainBaker.Escarpment.new(
PackedVector2Array([Vector2(-50.0, 0.0), Vector2(50.0, 0.0)]), 10.0, 2.0, 0.5, 0, 0.0
)
_expect(is_equal_approx(cliff.elevation_delta(0.0, -10.0), 0.0), "escarpment leaves the left (north) side alone")
_expect(is_equal_approx(cliff.elevation_delta(0.0, 2.0), -10.0), "escarpment drops by drop_m on the right (south) side")
_expect(is_equal_approx(cliff.elevation_delta(0.0, 12.0), -15.0), "escarpment keeps falling at slope_beyond")
func _test_outcrop() -> void:
var mesa := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 10.0, 6.0, 4.0, 0, 0.0, true)
_expect(is_equal_approx(mesa.elevation_delta(0.0, 0.0), 6.0), "outcrop top is flat at height_m")
_expect(is_equal_approx(mesa.elevation_delta(0.0, 8.0), 3.0), "outcrop flank is half way down in the middle of the wall")
_expect(is_equal_approx(mesa.elevation_delta(0.0, 25.0), 0.0), "outcrop is zero past its outline")
_expect(mesa.material_at(0.0, 0.0) == SonarMaterial.Value.ROCK, "rock_top makes the flat summit rock")
_expect(mesa.material_at(0.0, 25.0) == SiteTerrainBaker.NO_MATERIAL, "outcrop leaves the surroundings to the slope rule")
var sand_top := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 10.0, 6.0, 4.0, 0, 0.0, false)
_expect(sand_top.material_at(0.0, 0.0) == SiteTerrainBaker.NO_MATERIAL, "without rock_top the summit follows the slope rule")
var pit := SiteTerrainBaker.Outcrop.new(Vector2.ZERO, 10.0, 20.0, -5.0, 3.0, 0, 0.0, false)
_expect(is_equal_approx(pit.elevation_delta(0.0, 0.0), -5.0), "negative height digs a flat-bottomed pit")
func _test_boulder_field() -> void:
var first := SiteTerrainBaker.BoulderField.new(Vector2(50.0, -50.0), 30.0, 11, 20, 2.0, 4.0, 0.8)
var second := SiteTerrainBaker.BoulderField.new(Vector2(50.0, -50.0), 30.0, 11, 20, 2.0, 4.0, 0.8)
_expect(first.boulders() == second.boulders(), "same seed scatters the same boulders")
var all_inside := true
for boulder in first.boulders():
all_inside = all_inside and Vector2(boulder.x, boulder.y).distance_to(Vector2(50.0, -50.0)) <= 30.0
_expect(all_inside and first.boulders().size() == 20, "boulders stay inside the field disc")
var top := first.boulders()[0]
_expect(first.elevation_delta(top.x, top.y) >= top.z * 0.8 - 0.001, "a boulder centre is at least its own height")
_expect(first.material_at(top.x, top.y) == SonarMaterial.Value.ROCK and first.material_at(200.0, 200.0) == SiteTerrainBaker.NO_MATERIAL, "boulders are rock, the sand around is not forced")
var stacked := SiteTerrainBaker.BoulderField.new(Vector2.ZERO, 0.0, 3, 5, 2.0, 4.0, 1.0)
_expect(stacked.elevation_delta(0.0, 0.0) <= 4.0 + 0.001, "overlapping boulders combine by maximum, not by sum")
func _test_strata() -> void:
# A 45 degree plane sampled every 0.5 m: four cells per 2 m layer.
var recipe := _small_recipe()
recipe.columns = 41
recipe.rows = 41
recipe.cell_size_m = 0.5
recipe.origin_x_m = -10.0
recipe.origin_z_m = -10.0
recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 1.0, 0.0, 0.0, 0.0))
var plain := SiteTerrainBaker.bake(recipe)
recipe.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.9, 20.0, 35.0, Vector2.ZERO, 0.0))
var terraced := SiteTerrainBaker.bake(recipe)
var flat_cells := 0
var riser_cells := 0
for column in range(5, 35):
var rise := terraced.elevations_m[terraced.index_of(column + 1, 20)] - terraced.elevations_m[terraced.index_of(column, 20)]
if rise < 0.1:
flat_cells += 1
elif rise > 0.9:
riser_cells += 1
_expect(flat_cells >= 8 and riser_cells >= 4, "strata turn a steep slope into ledges and risers (%d flat, %d risers)" % [flat_cells, riser_cells])
_expect(is_equal_approx(terraced.elevations_m[terraced.index_of(20, 20)], plain.elevations_m[plain.index_of(20, 20)]), "strata keep the layer boundaries where they were")
var flat := _small_recipe()
flat.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.3, 0.0, 0.0, 0.0, 0.0))
flat.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.9, 20.0, 35.0, Vector2.ZERO, 0.0))
var flat_data := SiteTerrainBaker.bake(flat)
_expect(is_equal_approx(flat_data.elevations_m[flat_data.index_of(10, 10)], -30.3), "strata leave flat ground alone")
var identity := _small_recipe()
identity.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 1.0, 0.0, 0.0, 0.0))
var before := SiteTerrainBaker.bake(identity)
identity.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.0, 20.0, 35.0, Vector2.ZERO, 0.0))
var after := SiteTerrainBaker.bake(identity)
_expect(before.elevations_m == after.elevations_m, "sharpness 0 is the identity")
func _test_zone1_artefact_is_stable() -> void:
var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData
_expect(versioned != null, "versioned zone 1 terrain loads")
if versioned == null:
return
var baked := SiteTerrainBaker.bake(Zone1Recipe.recipe())
_expect(
baked.elevations_m == versioned.elevations_m and baked.materials == versioned.materials,
"zone 1 recipe still bakes the versioned artefact byte for byte"
)
func _expect(condition: bool, label: String) -> void:
if not condition:
_failures.append(label)