Files
Ransom-s-wake/tests/contract/run_site_content_tests.gd
T

286 lines
14 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 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 := 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)
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() <= -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
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)