200 lines
9.7 KiB
GDScript
200 lines
9.7 KiB
GDScript
extends SceneTree
|
|
|
|
const WorldServiceType = preload("res://scripts/world/world_service.gd")
|
|
const DigestType = preload("res://scripts/core/simulation_digest.gd")
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
print("S1: World Service")
|
|
_test_bounds_zones_and_footprints()
|
|
_test_occupancy_validation_and_release()
|
|
_test_walkability_excavation_and_revisions()
|
|
_test_connected_support_validation()
|
|
_test_snapshot_round_trip_and_atomic_restore()
|
|
_test_snapshot_order_is_deterministic()
|
|
if _failures == 0:
|
|
print("PASS: %d checks" % _checks)
|
|
else:
|
|
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
|
quit(_failures)
|
|
|
|
|
|
func _test_bounds_zones_and_footprints() -> void:
|
|
var world = WorldServiceType.new()
|
|
_expect(world.reset(_bounds(-2, 2, -2, 2)), "dictionary bounds are accepted")
|
|
_expect(world.is_in_bounds(Vector3i(-2, -2, 0)), "FRONT minimum corner is in bounds")
|
|
_expect(world.is_in_bounds(Vector3i(2, 2, 1)), "BACK maximum corner is in bounds")
|
|
_expect(not world.is_in_bounds(Vector3i(0, 0, 2)), "unknown layer is out of bounds")
|
|
_expect(not world.is_in_bounds(Vector3i(3, 0, 0)), "x outside bounds is rejected")
|
|
_expect(world.get_zone(Vector3i(0, 1, 0)) == world.ZONE_ABOVE_GROUND, "positive y is above ground")
|
|
_expect(world.get_zone(0) == world.ZONE_BOUNDARY, "zero y is boundary")
|
|
_expect(world.get_zone(Vector2i(0, -1)) == world.ZONE_UNDERGROUND, "negative y is underground")
|
|
|
|
var footprint := world.cells_for_footprint(Vector3i(-1, -1, 1), Vector2i(2, 2))
|
|
_expect(
|
|
footprint == [
|
|
Vector3i(-1, -1, 1), Vector3i(0, -1, 1),
|
|
Vector3i(-1, 0, 1), Vector3i(0, 0, 1),
|
|
],
|
|
"rectangular footprint has stable row-major ordering"
|
|
)
|
|
_expect(world.cells_for_footprint(Vector3i.ZERO, Vector2i.ZERO).is_empty(), "empty footprint is rejected")
|
|
var before := world.get_bounds()
|
|
_expect(not world.reset(Rect2i(0, 0, 0, 2)), "invalid reset is rejected")
|
|
_expect(world.get_bounds() == before, "failed reset preserves the current world")
|
|
|
|
|
|
func _test_occupancy_validation_and_release() -> void:
|
|
var world = WorldServiceType.new()
|
|
world.reset(_bounds(-3, 3, -2, 2))
|
|
var room_cells := world.cells_for_footprint(Vector3i(0, 0, 0), Vector2i(2, 1))
|
|
_expect(world.occupy(&"room-b", room_cells, &"facility_small"), "valid entity occupies its footprint")
|
|
_expect(world.get_occupant(Vector3i(0, 0, 0)) == &"room-b", "cell points back to occupant")
|
|
_expect(world.get_occupant_kind(Vector3i(1, 0, 0)) == &"facility_small", "occupant kind is retained")
|
|
_expect(world.get_entity_cells(&"room-b") == room_cells, "entity points to canonical cells")
|
|
|
|
var returned_cells := world.get_entity_cells(&"room-b")
|
|
returned_cells.clear()
|
|
_expect(world.get_entity_cells(&"room-b") == room_cells, "returned cell arrays cannot mutate world state")
|
|
|
|
var overlap := world.validate_occupancy([Vector3i(1, 0, 0)], &"room-a")
|
|
_expect(not overlap[&"ok"] and overlap[&"errors"].has(world.ERROR_OCCUPIED), "overlapping occupation reports OCCUPIED")
|
|
var revision_before_failure: int = world.revision
|
|
_expect(not world.occupy(&"room-a", [Vector3i(1, 0, 0)], &"facility"), "overlapping occupy is atomic")
|
|
_expect(world.revision == revision_before_failure, "failed occupy does not change revision")
|
|
_expect(not world.occupy(&"room-b", [Vector3i(2, 0, 0)], &"facility"), "same entity cannot occupy twice")
|
|
|
|
var malformed := world.validate_occupancy([
|
|
Vector3i(0, 1, 0),
|
|
Vector3i(0, 1, 0),
|
|
Vector3i(0, 1, 1),
|
|
Vector3i(9, 1, 0),
|
|
Vector2i.ZERO,
|
|
])
|
|
_expect(malformed[&"errors"].has(world.ERROR_DUPLICATE_CELL), "duplicate cell is reported")
|
|
_expect(malformed[&"errors"].has(world.ERROR_LAYER_MISMATCH), "layer crossing is reported")
|
|
_expect(malformed[&"errors"].has(world.ERROR_OUT_OF_BOUNDS), "out-of-bounds cell is reported")
|
|
_expect(malformed[&"errors"].has(world.ERROR_INVALID_CELL_TYPE), "non-Vector3i cell is reported")
|
|
|
|
_expect(world.release(&"room-b"), "release succeeds for existing entity")
|
|
_expect(world.get_occupant(Vector3i(0, 0, 0)).is_empty(), "release clears cell-side occupation")
|
|
_expect(world.get_entity_cells(&"room-b").is_empty(), "release clears entity-side occupation")
|
|
_expect(not world.release(&"room-b"), "double release is rejected")
|
|
|
|
|
|
func _test_walkability_excavation_and_revisions() -> void:
|
|
var world = WorldServiceType.new()
|
|
world.reset(_bounds(-1, 1, -2, 2))
|
|
var path_cell := Vector3i(0, 1, 0)
|
|
var cave_front := Vector3i(0, -1, 0)
|
|
var cave_back := Vector3i(0, -1, 1)
|
|
_expect(not world.is_walkable(path_cell), "walkability defaults to false")
|
|
_expect(world.set_walkable(path_cell, true), "walkability can be enabled")
|
|
_expect(world.is_walkable(path_cell), "enabled walkability is readable")
|
|
_expect(world.dirty_revision == world.revision, "dirty revision exposes the current world revision")
|
|
var walk_revision: int = world.revision
|
|
_expect(world.set_walkable(path_cell, true), "same walkability is a successful no-op")
|
|
_expect(world.revision == walk_revision, "walkability no-op does not dirty world")
|
|
|
|
_expect(world.is_excavated(Vector3i(0, 0, 0)), "boundary is usable without excavation")
|
|
_expect(not world.is_excavated(cave_front), "underground defaults to unexcavated")
|
|
_expect(world.set_excavated(cave_front, true), "underground spatial cell can be excavated")
|
|
_expect(world.is_excavated(cave_front) and world.is_excavated(cave_back), "excavation is shared by FRONT and BACK")
|
|
_expect(world.excavation_revision == world.revision, "excavation tracks its dirty revision")
|
|
_expect(world.get_dirty_cells().has(cave_front) and world.get_dirty_cells().has(cave_back), "both layers become dirty")
|
|
var dirty := world.consume_dirty_cells()
|
|
_expect(not dirty.is_empty() and world.get_dirty_cells().is_empty(), "dirty cells can be consumed deterministically")
|
|
_expect(not world.set_walkable(Vector3i(99, 0, 0), true), "out-of-bounds walkability mutation is rejected")
|
|
|
|
|
|
func _test_connected_support_validation() -> void:
|
|
var world = WorldServiceType.new()
|
|
world.reset(_bounds(-5, 5, 0, 2))
|
|
_expect(world.set_support(Vector3i(0, 0, 0), 3), "explicit support capacity is stored")
|
|
_expect(world.occupy(&"existing-room", [Vector3i(1, 0, 0)], &"facility_small"), "existing structural load is stored")
|
|
var supported := world.validate_support([Vector3i(2, 0, 0), Vector3i(3, 0, 0)], 1)
|
|
_expect(supported[&"ok"], "component capacity covers existing and proposed load")
|
|
_expect(supported[&"required_weight"] == 3, "support report includes existing structural load")
|
|
_expect(supported[&"available_capacity"] == 3, "support report exposes finite capacity")
|
|
|
|
var overloaded := world.validate_support([
|
|
Vector3i(2, 0, 0), Vector3i(3, 0, 0), Vector3i(4, 0, 0),
|
|
], 1)
|
|
_expect(not overloaded[&"ok"] and overloaded[&"errors"].has(world.ERROR_UNSUPPORTED), "overloaded component is rejected")
|
|
_expect(overloaded[&"unsupported_cells"].size() == 3, "all proposed cells in failed component are identified")
|
|
|
|
_expect(world.set_support(Vector3i(-2, 0, 1), 0, true), "infinite structural anchor is stored")
|
|
var anchored := world.validate_support([Vector3i(-1, 0, 1), Vector3i(0, 0, 1)], 99)
|
|
_expect(anchored[&"ok"] and anchored[&"available_capacity"] == -1, "connected anchor supports any finite weight")
|
|
_expect(not world.validate_support([Vector3i(0, 0, 0)], 0)[&"ok"], "non-positive proposed weight is rejected")
|
|
|
|
|
|
func _test_snapshot_round_trip_and_atomic_restore() -> void:
|
|
var source = WorldServiceType.new()
|
|
source.reset(_bounds(-3, 3, -3, 3))
|
|
source.occupy(&"warehouse", [Vector3i(1, 0, 1), Vector3i(0, 0, 1)], &"facility_medium")
|
|
source.set_walkable(Vector3i(-1, 0, 0), true)
|
|
source.set_excavated(Vector3i(2, -2, 0), true)
|
|
source.set_excavated(Vector3i(-2, 1, 0), false)
|
|
source.set_support(Vector3i(0, 0, 0), 5, false)
|
|
source.set_support(Vector3i(-1, 0, 1), 0, true)
|
|
var snapshot := source.create_snapshot()
|
|
|
|
var restored = WorldServiceType.new()
|
|
restored.reset(Rect2i(0, 0, 1, 1))
|
|
_expect(restored.restore_snapshot(snapshot), "valid world snapshot restores")
|
|
_expect(restored.create_snapshot() == snapshot, "world snapshot round trip is exact")
|
|
_expect(DigestType.compute(restored.create_snapshot()) == DigestType.compute(snapshot), "round trip preserves deterministic digest")
|
|
|
|
var corrupt: Dictionary = snapshot.duplicate(true)
|
|
corrupt[&"occupancies"].append({
|
|
&"entity_id": &"overlap",
|
|
&"kind": &"facility",
|
|
&"cells": [Vector3i(0, 0, 1)],
|
|
})
|
|
var before_failed_restore := restored.create_snapshot()
|
|
_expect(not restored.restore_snapshot(corrupt), "overlapping snapshot is rejected")
|
|
_expect(restored.create_snapshot() == before_failed_restore, "failed restore is fully atomic")
|
|
|
|
|
|
func _test_snapshot_order_is_deterministic() -> void:
|
|
var first = WorldServiceType.new()
|
|
var second = WorldServiceType.new()
|
|
first.reset(_bounds(-2, 2, -1, 1))
|
|
second.reset(_bounds(-2, 2, -1, 1))
|
|
first.occupy(&"z-entity", [Vector3i(1, 0, 0)], &"decoration")
|
|
first.occupy(&"a-entity", [Vector3i(-1, 0, 0)], &"decoration")
|
|
second.occupy(&"a-entity", [Vector3i(-1, 0, 0)], &"decoration")
|
|
second.occupy(&"z-entity", [Vector3i(1, 0, 0)], &"decoration")
|
|
first.set_walkable(Vector3i(1, 1, 1), true)
|
|
first.set_walkable(Vector3i(-1, -1, 0), true)
|
|
second.set_walkable(Vector3i(-1, -1, 0), true)
|
|
second.set_walkable(Vector3i(1, 1, 1), true)
|
|
_expect(first.create_snapshot() == second.create_snapshot(), "snapshot ignores Dictionary insertion order")
|
|
_expect(DigestType.compute(first.create_snapshot()) == DigestType.compute(second.create_snapshot()), "equivalent worlds have identical digest")
|
|
|
|
|
|
func _bounds(min_x: int, max_x: int, min_y: int, max_y: int) -> Dictionary:
|
|
return {
|
|
&"min_x": min_x,
|
|
&"max_x": max_x,
|
|
&"min_y": min_y,
|
|
&"max_y": max_y,
|
|
}
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
print(" ✓ %s" % label)
|
|
return
|
|
_failures += 1
|
|
push_error(" ✗ %s" % label)
|