straywild/tests/terrain_chunk_generator_validation.gd

216 lines
5.6 KiB
GDScript

extends SceneTree
const CATALOG: TerrainChunkCatalog = preload(
"res://world/generation/chunks/terrain_chunk_catalog.tres"
)
const EXPECTED_IDS: Array[String] = [
"chunk_0000",
"chunk_0001",
"chunk_0002",
"chunk_0003",
"chunk_0004",
"chunk_spawn",
]
const REQUIRED_IDS: Array[String] = [
"chunk_spawn",
"chunk_0001",
"chunk_0002",
"chunk_0003",
"chunk_0004",
]
const TEST_SEED := 13001
var _failures: Array[String] = []
func _initialize() -> void:
call_deferred(&"_run")
func _run() -> void:
_validate_catalog()
_validate_profiles()
_validate_generation()
_finish()
func _validate_catalog() -> void:
for error: String in CATALOG.validation_errors():
_check(false, error)
_check(
CATALOG.definitions.size() == EXPECTED_IDS.size(),
"Catalog must contain the authored chunks and spawn definition.",
)
for stable_id: String in EXPECTED_IDS:
var definition := CATALOG.definition_for_id(StringName(stable_id))
_check(definition != null, "Catalog is missing %s." % stable_id)
if definition == null:
continue
_check(
definition.packed_scene != null,
"%s must reference an imported GLB." % stable_id,
)
func _validate_profiles() -> void:
for definition: TerrainChunkDefinition in CATALOG.definitions:
var variants := TerrainChunkAnalyzer.create_variants(
definition,
CATALOG.chunk_size,
)
_check(
not variants.is_empty(),
"%s must produce at least one rotation variant."
% definition.stable_id,
)
for variant: TerrainChunkVariant in variants:
_check(
variant.edge_profiles.size() == 4,
"%s must expose four edge profiles." % variant.stable_key(),
)
for profile: TerrainChunkEdgeProfile in variant.edge_profiles:
_check(
not profile.points.is_empty(),
"%s has an empty edge profile." % variant.stable_key(),
)
func _validate_generation() -> void:
var first := _make_generator()
root.add_child(first)
var first_solved := first.generate()
_check(first_solved, "Generator must solve the prototype 5x5 grid.")
if not first_solved:
first.free()
return
var first_keys := first.placement_keys()
_check(first_keys.size() == 25, "Generator must place exactly 25 chunks.")
for stable_id: String in REQUIRED_IDS:
_check(
_placements_contain(first_keys, stable_id),
"Generated diagnostic must contain %s." % stable_id,
)
_check(
_all_neighbor_edges_match(first),
"Every generated neighboring edge must match.",
)
_check(
_count_placements(first_keys, "chunk_spawn") == 1,
"Generated layouts must contain exactly one spawn chunk.",
)
_check(
first_keys[12].begins_with("chunk_spawn@"),
"The spawn chunk must occupy the center cell.",
)
var second := _make_generator()
root.add_child(second)
var second_solved := second.generate()
_check(second_solved, "Repeated deterministic generation must solve.")
if not second_solved:
first.free()
second.free()
return
_check(
second.placement_keys() == first_keys,
"The same seed and catalog must produce the same layout.",
)
first.free()
second.free()
for seed_offset: int in range(1, 6):
var generator := _make_generator()
generator.generation_seed = TEST_SEED + seed_offset
root.add_child(generator)
var solved := generator.generate()
_check(
solved,
"Generator must solve seed %d." % generator.generation_seed,
)
if solved:
var keys := generator.placement_keys()
for stable_id: String in REQUIRED_IDS:
_check(
_placements_contain(keys, stable_id),
"Seed %d must contain %s."
% [generator.generation_seed, stable_id],
)
_check(
_all_neighbor_edges_match(generator),
"Seed %d has a mismatched neighboring edge."
% generator.generation_seed,
)
generator.free()
func _make_generator() -> TerrainChunkGenerator:
var generator := TerrainChunkGenerator.new()
generator.catalog = CATALOG
generator.grid_size = Vector2i(5, 5)
generator.generation_seed = TEST_SEED
generator.generate_on_ready = false
generator.build_collision = false
generator.force_center_chunk_id = &"chunk_spawn"
generator.required_chunk_ids = PackedStringArray(REQUIRED_IDS)
generator.maximum_backtracks = 100000
return generator
func _placements_contain(keys: PackedStringArray, stable_id: String) -> bool:
for key: String in keys:
if key.begins_with(stable_id + "@"):
return true
return false
func _count_placements(keys: PackedStringArray, stable_id: String) -> int:
var count := 0
for key: String in keys:
if key.begins_with(stable_id + "@"):
count += 1
return count
func _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
for index: int in generator._placements.size():
var coordinate := Vector2i(
index % generator.grid_size.x,
index / generator.grid_size.x,
)
var current: TerrainChunkVariant = generator._placements[index]
if coordinate.x > 0:
var west: TerrainChunkVariant = generator._placements[index - 1]
if not generator._edges_are_compatible(
current,
TerrainChunkTopology.Edge.WEST,
west,
TerrainChunkTopology.Edge.EAST,
):
return false
if coordinate.y > 0:
var north: TerrainChunkVariant = (
generator._placements[index - generator.grid_size.x]
)
if not generator._edges_are_compatible(
current,
TerrainChunkTopology.Edge.NORTH,
north,
TerrainChunkTopology.Edge.SOUTH,
):
return false
return true
func _check(condition: bool, message: String) -> void:
if not condition:
_failures.append(message)
func _finish() -> void:
if _failures.is_empty():
print("Terrain chunk generator validation: PASS")
quit(0)
return
for failure: String in _failures:
printerr("Terrain chunk generator validation: ", failure)
quit(1)