From caa74b3ee178aa87ac5b9e772ccf0b87dbc33a75 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 6 Oct 2026 21:47:51 +0200 Subject: [PATCH] test(sites): geometric contract and integration checks for the terrain showcase Co-Authored-By: Claude Fable 5.1 --- tests/contract/run_site_content_tests.gd | 110 +++++++++++++++++- .../run_site_terrain_integration.gd | 43 ++++++- 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/tests/contract/run_site_content_tests.gd b/tests/contract/run_site_content_tests.gd index 0c171cf..e914dda 100644 --- a/tests/contract/run_site_content_tests.gd +++ b/tests/contract/run_site_content_tests.gd @@ -13,19 +13,37 @@ const DiveAccessibilityAdjustments = preload( const DiveRulesResolver = preload("res://game/domain/rules/dive_rules_resolver.gd") const SITE_PATH := "res://content/sites/zone1_vigilant.tres" +const SHOWCASE_PATH := "res://content/sites/terrain_showcase.tres" +# Showcase probe positions (docs/briefs/terrain-varied-landforms.md ยง5). The canyon +# transect crosses its first leg at right angles; the crevasse transect is the +# z = -120 row between the two buttes. +const SHOWCASE_MESA_TOP := Vector2(-80.0, -90.0) +const SHOWCASE_CANYON_MID := Vector2(-130.0, -135.0) +const SHOWCASE_CANYON_AXIS := Vector2(40.0, -30.0) +const SHOWCASE_CREVASSE_ROW_Z := -120.0 +const SHOWCASE_CREVASSE_SHELF_X := 118.0 +const SHOWCASE_CREVASSE_FLOOR_X := 124.0 +const SHOWCASE_BOULDER_FIELD_CENTRE := Vector2(-120.0, 110.0) +const SHOWCASE_BOULDER_FIELD_RADIUS_M := 75.0 +const SHOWCASE_WEST_SPUR := Vector2(80.0, 120.0) +const SHOWCASE_CHANNEL := Vector2(125.0, 120.0) +const SHOWCASE_EAST_SPUR := Vector2(170.0, 125.0) +const STEEP_WALL_RISE_PER_M := 1.7 # tan(60 degrees) +const LEDGE_RISE_PER_M := 0.35 const ROUTE_CLEARANCE_M := 3.0 const MINIMUM_LANDMARK_SPACING_M := 60.0 const MAXIMUM_ANCHOR_TO_WRECK_M := 320.0 const MAXIMUM_WRECK_SPACING_M := 260.0 const HULL_HALF_LENGTH_M := 14.0 const HULL_SEABED_TOLERANCE_M := 2.0 -const TEST_COUNT := 34 +const TEST_COUNT := 52 var _failures: Array[String] = [] func _init() -> void: _test_zone1_content() + _test_showcase_content() if _failures.is_empty(): print("Site content contract tests: %d passed, 0 failed." % TEST_COUNT) @@ -132,6 +150,96 @@ func _test_zone1_content() -> void: _expect(_hull_lies_along_seabed(field, targets[0]), "the target hull lies along the groove floor") +func _test_showcase_content() -> void: + var definition := load(SHOWCASE_PATH) as DiveSiteDefinition + _expect(definition != null, "showcase definition loads") + if definition == null: + return + _expect(definition.validation_error().is_empty(), "showcase definition is valid: %s" % definition.validation_error()) + var field := definition.height_field() + if field == null: + return + _expect(field.cell_size_m() == 1.0 and field.columns() == 513 and field.rows() == 513, "showcase grid is 513 x 513 cells of 1 m") + _expect(definition.playable_bounds_m.is_equal_approx(Rect2(-220.0, -220.0, 440.0, 440.0)), "showcase playable zone is 440 x 440 m") + _expect(field.minimum_elevation_m() >= -80.5 and field.maximum_elevation_m() <= -18.5, "showcase terrain stays between 19 m and 80 m deep") + var anchors := definition.placements_of_kind(SitePlacement.Kind.VESSEL_ANCHOR) + _expect(anchors.size() == 1 and definition.placements.size() == 1, "showcase has a single placement, the mooring beacon") + var spawn := definition.spawn_position_m + _expect(spawn.y - field.elevation_at(spawn.x, spawn.z) >= 10.0, "showcase spawn has at least 10 m of water under the keel") + + # NW: stratified canyon through the mesa. + var mesa_top := field.elevation_at(SHOWCASE_MESA_TOP.x, SHOWCASE_MESA_TOP.y) + var canyon_floor := field.elevation_at(SHOWCASE_CANYON_MID.x, SHOWCASE_CANYON_MID.y) + _expect(mesa_top - canyon_floor >= 18.0, "canyon floor is at least 18 m below the mesa top (%.1f m)" % (mesa_top - canyon_floor)) + var axis := SHOWCASE_CANYON_AXIS.normalized() + var lowest := INF + var highest := -INF + for along in range(-5, 6): + var point := SHOWCASE_CANYON_MID + axis * float(along) + var elevation := field.elevation_at(point.x, point.y) + lowest = minf(lowest, elevation) + highest = maxf(highest, elevation) + _expect(highest - lowest <= 1.0, "canyon floor is flat within 1 m over 10 m along the axis") + var normal := Vector2(-axis.y, axis.x) + var transect := PackedFloat32Array() + for offset in range(-30, 31): + var point := SHOWCASE_CANYON_MID + normal * float(offset) + transect.append(field.elevation_at(point.x, point.y)) + var steepest := 0.0 + var ledge_pairs := 0 + for index in range(transect.size() - 1): + var rise := absf(transect[index + 1] - transect[index]) + steepest = maxf(steepest, rise) + var in_wall := transect[index] > canyon_floor + 1.0 and transect[index] < mesa_top - 1.0 + if in_wall and rise < LEDGE_RISE_PER_M: + ledge_pairs += 1 + _expect(steepest >= STEEP_WALL_RISE_PER_M, "canyon wall is steeper than 60 degrees somewhere (%.1f m per m)" % steepest) + _expect(ledge_pairs >= 2, "canyon wall carries at least two ledges (%d flat metres)" % ledge_pairs) + + # NE: crevasse between the buttes. + var shelf := field.elevation_at(SHOWCASE_CREVASSE_SHELF_X, SHOWCASE_CREVASSE_ROW_Z) + var crevasse_floor := field.elevation_at(SHOWCASE_CREVASSE_FLOOR_X, SHOWCASE_CREVASSE_ROW_Z) + _expect(shelf - crevasse_floor >= 16.0, "crevasse floor is at least 16 m below the rim (%.1f m)" % (shelf - crevasse_floor)) + var deep_cells := 0 + for x in range(100, 146): + if field.elevation_at(float(x), SHOWCASE_CREVASSE_ROW_Z) < shelf - 10.0: + deep_cells += 1 + _expect(deep_cells >= 3 and deep_cells <= 8, "crevasse is 3 to 8 m wide at 10 m down (%d m)" % deep_cells) + + # SW: boulder field on sand. + var rock_cells := 0 + var disc_cells := 0 + var radius := int(SHOWCASE_BOULDER_FIELD_RADIUS_M) + for z in range(-radius, radius + 1): + for x in range(-radius, radius + 1): + var point := SHOWCASE_BOULDER_FIELD_CENTRE + Vector2(x, z) + if point.distance_to(SHOWCASE_BOULDER_FIELD_CENTRE) > SHOWCASE_BOULDER_FIELD_RADIUS_M: + continue + disc_cells += 1 + if field.material_at(point.x, point.y) == SonarMaterial.Value.ROCK: + rock_cells += 1 + var rock_fraction := float(rock_cells) / float(disc_cells) + _expect(rock_fraction >= 0.25 and rock_fraction <= 0.6, "boulder field is 25 to 60 %% rock (%.0f %%)" % (rock_fraction * 100.0)) + _expect(field.material_at(SHOWCASE_BOULDER_FIELD_CENTRE.x + 100.0, SHOWCASE_BOULDER_FIELD_CENTRE.y) == SonarMaterial.Value.SAND, "the sand plain beside the boulder field is sand") + + # SE: two spurs and a sand channel. + var west_spur := field.elevation_at(SHOWCASE_WEST_SPUR.x, SHOWCASE_WEST_SPUR.y) + var east_spur := field.elevation_at(SHOWCASE_EAST_SPUR.x, SHOWCASE_EAST_SPUR.y) + var channel := field.elevation_at(SHOWCASE_CHANNEL.x, SHOWCASE_CHANNEL.y) + _expect(west_spur - channel >= 5.0 and east_spur - channel >= 5.0, "the channel runs at least 5 m below both spurs") + _expect( + field.material_at(SHOWCASE_WEST_SPUR.x, SHOWCASE_WEST_SPUR.y) == SonarMaterial.Value.ROCK + and field.material_at(SHOWCASE_EAST_SPUR.x, SHOWCASE_EAST_SPUR.y) == SonarMaterial.Value.ROCK, + "both spur summits are rock" + ) + _expect(field.material_at(SHOWCASE_CHANNEL.x, SHOWCASE_CHANNEL.y + 30.0) == SonarMaterial.Value.SAND, "the channel floor is sand") + + # S: escarpment. + var above := field.elevation_at(0.0, 190.0) + var below := field.elevation_at(0.0, 250.0) + _expect(above - below >= 20.0, "the escarpment drops at least 20 m (%.1f m)" % (above - below)) + + func _rests_on_terrain(field: TerrainHeightField, placement: SitePlacement) -> bool: var seabed := field.elevation_at(placement.position_m.x, placement.position_m.z) var bottom := placement.position_m.y - placement.footprint_m.y * 0.5 diff --git a/tests/integration/run_site_terrain_integration.gd b/tests/integration/run_site_terrain_integration.gd index 67b45bc..30caa0b 100644 --- a/tests/integration/run_site_terrain_integration.gd +++ b/tests/integration/run_site_terrain_integration.gd @@ -16,8 +16,9 @@ const ReferenceBathymetryAcquisition = preload( const DIVE_SITE_SCENE = preload("res://game/world/sites/dive_site.tscn") const ZONE1_SITE_PATH := "res://content/sites/zone1_vigilant.tres" +const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres" const GROOVE_PROBE_OFFSET_M := 25.0 -const TEST_COUNT := 31 +const TEST_COUNT := 36 var _failures: Array[String] = [] @@ -30,6 +31,7 @@ func _run() -> void: await _test_actor_matches_field() await _test_dive_site_scene() await _test_zone1_site() + await _test_showcase_site() var exit_code := 0 if _failures.is_empty(): @@ -218,6 +220,45 @@ func _test_zone1_site() -> void: site.queue_free() +func _test_showcase_site() -> void: + await process_frame + var definition := load(SHOWCASE_SITE_PATH) as DiveSiteDefinition + _expect(definition != null, "showcase site definition loads") + if definition == null: + return + var started_usec := Time.get_ticks_usec() + var site := DIVE_SITE_SCENE.instantiate() as DiveSite + root.add_child(site) + _expect(site.configure(definition).is_empty(), "dive site scene accepts the Jardin des Formes") + var configure_ms := float(Time.get_ticks_usec() - started_usec) / 1000.0 + _expect(configure_ms < 3000.0, "showcase configures under the 3 s load budget (%.0f ms)" % configure_ms) + await physics_frame + await physics_frame + var space := site.get_world_3d().direct_space_state + var terrain := site.terrain() + var canyon_floor := _terrain_height(space, terrain, -130.0, -135.0, []) + var mesa_top := _terrain_height(space, terrain, -80.0, -90.0, []) + _expect(mesa_top - canyon_floor >= 18.0, "collision rays find the canyon under the mesa (%.1f m vs %.1f m)" % [canyon_floor, mesa_top]) + + # Sensor above the canyon looking along its first leg (the acquisition probes along + # local +Z): rock walls and sand floor. + var sensor := Transform3D(Basis.IDENTITY, Vector3(-130.0, -15.0, -135.0)) + sensor.basis = Basis.looking_at(-Vector3(40.0, 0.0, -30.0).normalized(), Vector3.UP) + var acquisition := ReferenceBathymetryAcquisition.new(site.get_world_3d()) + var request := SonarCaptureRequest.new( + &"scan", &"session", sensor, 5.0, 95.0, deg_to_rad(76.0), deg_to_rad(64.0), 49, 31 + ) + var result = acquisition.capture(request) + var materials: Dictionary = {} + for sample in result.samples: + materials[sample.material] = true + _expect( + result.accepted and materials.has(SonarMaterial.Value.ROCK) and materials.has(SonarMaterial.Value.SAND), + "a scan along the canyon reads rock walls and a sand floor" + ) + site.queue_free() + + ## Seabed height under (x, z), or NAN (every comparison fails) when the ray misses the terrain actor. func _terrain_height( space: PhysicsDirectSpaceState3D,