Files
Ransom-s-wake/tests/contract/run_site_content_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

298 lines
15 KiB
GDScript

extends SceneTree
const SonarMaterial = preload("res://game/domain/sonar/sonar_material.gd")
const DiveSiteDefinition = preload("res://game/domain/sites/dive_site_definition.gd")
const TerrainHeightField = preload("res://game/domain/sites/terrain_height_field.gd")
const SitePlacement = preload("res://game/domain/sites/site_placement.gd")
const SiteRoute = preload("res://game/domain/sites/site_route.gd")
const DiveDifficultyProfile = preload("res://game/domain/rules/dive_difficulty_profile.gd")
const DiveUpgradeEffects = preload("res://game/domain/rules/dive_upgrade_effects.gd")
const DiveAccessibilityAdjustments = preload(
"res://game/domain/rules/dive_accessibility_adjustments.gd"
)
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 second leg at right angles; the crevasse transect is the
# z = -120 row between the two buttes, compared with the open plateau on the same row;
# the escarpment is probed across its step band at x = 0, where the crest is at z = 217.
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_PLAIN_X := 0.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 SHOWCASE_ESCARPMENT_CREST_Z := 217.0
const SHOWCASE_ESCARPMENT_BAND_M := 14.0 # softness 6 m plus 8 m of outline noise
const STEEP_WALL_RISE_PER_M := 1.73 # 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 := 54
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)
quit(0)
else:
for failure: String in _failures:
push_error(failure)
print("Site content contract tests: %d failed." % _failures.size())
quit(1)
func _test_zone1_content() -> void:
var definition := load(SITE_PATH) as DiveSiteDefinition
_expect(definition != null, "zone 1 definition loads")
if definition == null:
return
_expect(definition.validation_error().is_empty(), "zone 1 definition is valid: %s" % definition.validation_error())
var field := definition.height_field()
if field == null:
return
var rules = DiveRulesResolver.new().resolve(
DiveDifficultyProfile.standard(),
DiveUpgradeEffects.new(),
DiveAccessibilityAdjustments.new()
)
var certified_m: float = rules.certified_depth_m
var critical_m: float = rules.critical_depth_m
_expect(definition.playable_bounds_m.is_equal_approx(Rect2(-300.0, -300.0, 600.0, 600.0)), "playable zone is 600 x 600 m")
_expect(field.minimum_elevation_m() >= -88.5 and field.maximum_elevation_m() <= -19.5, "terrain stays between 20 m and 88 m deep")
_expect(field.cell_size_m() == 1.5 and field.columns() == 513 and field.rows() == 513, "terrain grid is 513 x 513 cells of 1.5 m")
var targets := definition.placements_of_kind(SitePlacement.Kind.WRECK_TARGET)
var decoys := definition.placements_of_kind(SitePlacement.Kind.WRECK_DECOY)
var items := definition.placements_of_kind(SitePlacement.Kind.SECONDARY_ITEM)
var anchors := definition.placements_of_kind(SitePlacement.Kind.VESSEL_ANCHOR)
var debris := definition.placements_of_kind(SitePlacement.Kind.DEBRIS)
var landmarks := definition.placements_of_kind(SitePlacement.Kind.LANDMARK)
_expect(targets.size() == 1, "exactly one target wreck")
_expect(decoys.size() == 2, "exactly two decoy wrecks")
_expect(items.size() == 1, "exactly one secondary item")
_expect(anchors.size() == 1, "exactly one vessel anchor")
_expect(debris.size() >= 3, "a debris trail of at least three pieces")
_expect(landmarks.size() >= 4, "at least four named landmarks")
for wreck: SitePlacement in targets + decoys:
_expect(-wreck.position_m.y <= certified_m, "%s lies above the certified depth" % wreck.placement_id)
_expect(_rests_on_terrain(field, wreck), "%s rests on or in the seabed" % wreck.placement_id)
if items.size() == 1:
var item_depth := -items[0].position_m.y
_expect(item_depth > certified_m and item_depth <= critical_m, "secondary item is between 100 %% and 115 %% of the certified depth (%.1f m)" % item_depth)
_expect(_rests_on_terrain(field, items[0]), "secondary item rests on the seabed")
for piece: SitePlacement in debris:
if -piece.position_m.y < 45.0 or -piece.position_m.y > 66.0 or not _rests_on_terrain(field, piece):
_failures.append("debris %s is off the slope or floating" % piece.placement_id)
_expect(true, "debris pieces checked individually")
if anchors.size() == 1:
var seabed_under_anchor := -field.elevation_at(anchors[0].position_m.x, anchors[0].position_m.z)
_expect(seabed_under_anchor >= 20.0 and seabed_under_anchor <= 30.0, "vessel anchors over 20-30 m of seabed")
var spawn := definition.spawn_position_m
_expect(spawn.y - field.elevation_at(spawn.x, spawn.z) >= 8.0, "spawn has at least 8 m of water under the keel")
_expect(is_equal_approx(-spawn.y, 10.0), "spawn is 10 m deep")
var safe := definition.route(&"safe_ridge")
var risky := definition.route(&"risky_groove")
_expect(safe != null and safe.maximum_depth_m == 32.0, "safe route is declared with a 32 m ceiling")
_expect(risky != null and risky.maximum_depth_m == 48.0, "risky route is declared with a 48 m ceiling")
if safe != null and risky != null and targets.size() == 1:
_expect(_route_clears_terrain(field, safe), "safe route clears the terrain by %.0f m" % ROUTE_CLEARANCE_M)
_expect(_route_clears_terrain(field, risky), "risky route clears the terrain by %.0f m" % ROUTE_CLEARANCE_M)
var target := targets[0].position_m
_expect(
_xz_distance(safe.waypoints_m[-1], target) <= 25.0 and _xz_distance(risky.waypoints_m[-1], target) <= 25.0,
"both routes end within 25 m of the target"
)
_expect(_route_length(risky) < _route_length(safe) * 0.9, "risky route is shorter than the safe route")
var spaced := true
for i in landmarks.size():
for j in range(i + 1, landmarks.size()):
if _xz_distance(landmarks[i].position_m, landmarks[j].position_m) < MINIMUM_LANDMARK_SPACING_M:
spaced = false
_expect(spaced, "landmarks are at least 60 m apart")
var wrecks := targets + decoys
if anchors.size() == 1:
var anchor := anchors[0].position_m
var farthest := 0.0
for wreck: SitePlacement in wrecks:
farthest = maxf(farthest, _xz_distance(anchor, wreck.position_m))
_expect(farthest <= MAXIMUM_ANCHOR_TO_WRECK_M, "every wreck lies within 320 m of the mooring (farthest %.0f m)" % farthest)
var widest := 0.0
for i in wrecks.size():
for j in range(i + 1, wrecks.size()):
widest = maxf(widest, _xz_distance(wrecks[i].position_m, wrecks[j].position_m))
_expect(widest <= MAXIMUM_WRECK_SPACING_M, "every pair of wrecks lies within 260 m (widest %.0f m)" % widest)
if targets.size() == 1:
var target_xz := Vector2(targets[0].position_m.x, targets[0].position_m.z)
var spawn_xz := Vector2(spawn.x, spawn.z)
var yaw := definition.spawn_yaw_radians
var spawn_forward := Vector2(-sin(yaw), -cos(yaw))
_expect(spawn_forward.dot(target_xz - spawn_xz) > 0.0, "the spawn faces the target wreck")
_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() <= -19.0, "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))
_expect(field.material_at(SHOWCASE_CANYON_MID.x, SHOWCASE_CANYON_MID.y) == SonarMaterial.Value.SAND, "canyon floor is sand through the rock mesa")
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 ledges := 0
var on_ledge := false
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
# A ledge is a run of flat metres inside the wall, separated from the next by a riser.
if in_wall and rise < LEDGE_RISE_PER_M:
if not on_ledge:
ledges += 1
on_ledge = true
else:
on_ledge = false
_expect(steepest >= STEEP_WALL_RISE_PER_M, "canyon wall is steeper than 60 degrees somewhere (%.2f m per m)" % steepest)
_expect(ledges >= 2, "canyon walls carry at least two distinct ledges (%d)" % ledges)
# NE: crevasse between the buttes.
var plain := field.elevation_at(SHOWCASE_CREVASSE_PLAIN_X, SHOWCASE_CREVASSE_ROW_Z)
var crevasse_floor := field.elevation_at(SHOWCASE_CREVASSE_FLOOR_X, SHOWCASE_CREVASSE_ROW_Z)
_expect(plain - crevasse_floor >= 16.0, "crevasse floor is at least 16 m below the open plateau (%.1f m)" % (plain - crevasse_floor))
_expect(field.material_at(SHOWCASE_CREVASSE_FLOOR_X, SHOWCASE_CREVASSE_ROW_Z) == SonarMaterial.Value.SAND, "crevasse floor is sand")
var deep_cells := 0
for x in range(100, 146):
if field.elevation_at(float(x), SHOWCASE_CREVASSE_ROW_Z) < plain - 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, measured across its step band only so the slope below cannot
# stand in for a missing step (the slope alone would give about 7 m here).
var above := field.elevation_at(0.0, SHOWCASE_ESCARPMENT_CREST_Z - SHOWCASE_ESCARPMENT_BAND_M)
var below := field.elevation_at(0.0, SHOWCASE_ESCARPMENT_CREST_Z + SHOWCASE_ESCARPMENT_BAND_M)
_expect(above - below >= 18.0, "the escarpment steps down at least 18 m across its crest band (%.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
return bottom >= seabed - placement.footprint_m.y and bottom <= seabed + 1.0
func _hull_lies_along_seabed(field: TerrainHeightField, hull: SitePlacement) -> bool:
var centre := Vector2(hull.position_m.x, hull.position_m.z)
var long_axis := Vector2(cos(hull.yaw_radians), -sin(hull.yaw_radians))
var centre_seabed := field.elevation_at(centre.x, centre.y)
for side: float in [-1.0, 1.0]:
var end := centre + long_axis * HULL_HALF_LENGTH_M * side
if absf(field.elevation_at(end.x, end.y) - centre_seabed) > HULL_SEABED_TOLERANCE_M:
return false
return true
func _route_clears_terrain(field: TerrainHeightField, route: SiteRoute) -> bool:
for i in range(route.waypoints_m.size() - 1):
var a := route.waypoints_m[i]
var b := route.waypoints_m[i + 1]
var steps := maxi(int(ceilf(a.distance_to(b) / 2.0)), 1)
for step in range(steps + 1):
var point := a.lerp(b, float(step) / float(steps))
if point.y - field.elevation_at(point.x, point.z) < ROUTE_CLEARANCE_M:
return false
return true
func _route_length(route: SiteRoute) -> float:
var length := 0.0
for i in range(route.waypoints_m.size() - 1):
length += route.waypoints_m[i].distance_to(route.waypoints_m[i + 1])
return length
func _xz_distance(a: Vector3, b: Vector3) -> float:
return Vector2(a.x, a.z).distance_to(Vector2(b.x, b.z))
func _expect(condition: bool, label: String) -> void:
if not condition:
_failures.append(label)