Add headless regression tests for Phase 4b features
- Implement test for popup anchor behavior in rule-builder menus to ensure consistent anchor positioning during menu transitions. - Create tests for stage logic, including mode transitions, toolbar visibility, and status bar updates. - Add terrain drag-painting tests to verify correct block placement behavior and conflict handling. - Introduce walk waypoint tests to check for arrival conditions and position stability after navigation.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
# test_phase4b_logic.gd
|
||||
# Headless sanity checks for Phase 4b pure logic (no rendering):
|
||||
# 1. sandbox_theme.json parses and drives the stage's accent/popup/grid values.
|
||||
# 2. Bresenham cell pathing matches hand-computed staircase runs.
|
||||
# 3. AABB -> grid-cell rasterization covers the expected cells.
|
||||
# 4. The three-state occupancy query (empty / same-type skip / conflicting).
|
||||
#
|
||||
# Run with:
|
||||
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase4b_logic.gd --path .
|
||||
#
|
||||
# Prints PASS/FAIL per assertion and exits 0 on all PASS, 1 on any FAIL.
|
||||
|
||||
extends SceneTree
|
||||
|
||||
const STAGE_SCENE := preload("res://scenes/sandbox_stage.tscn")
|
||||
const STAGE_SPAWNER := preload("res://scripts/stage_spawner.gd")
|
||||
const TERRAIN_UTILS := preload("res://scripts/terrain_utils.gd")
|
||||
const PROP_BLOCK := preload("res://scripts/prop_block.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("")
|
||||
print("========================================================")
|
||||
print(" PHASE 4b LOGIC TEST (headless)")
|
||||
print("========================================================")
|
||||
var stage: Node2D = STAGE_SCENE.instantiate()
|
||||
root.add_child(stage)
|
||||
|
||||
_test_theme(stage)
|
||||
_test_bresenham(stage)
|
||||
_test_rasterize(stage)
|
||||
_test_classify(stage)
|
||||
|
||||
stage.queue_free()
|
||||
print("--------------------------------------------------------")
|
||||
if _failures == 0:
|
||||
print("RESULT: ALL PASSED (%d assertions, 0 failures)" % _checks)
|
||||
quit(0)
|
||||
else:
|
||||
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
|
||||
quit(1)
|
||||
|
||||
|
||||
func _test_theme(stage: Node2D) -> void:
|
||||
print("")
|
||||
print("--- Theme loading ---")
|
||||
_check(stage._theme_grid_default == 15.0,
|
||||
"theme grid.snap_size seeds default grid (%.1f)" % stage._theme_grid_default)
|
||||
_check(stage._action_popup_font_size == 24,
|
||||
"theme action_popup_font_size == 24 (got %d)" % stage._action_popup_font_size)
|
||||
_check(stage._accent_edit.to_html(false) == "22c6ff",
|
||||
"theme edit_accent parses to 22c6ff (got %s)" % stage._accent_edit.to_html(false))
|
||||
_check(stage._accent_direct.to_html(false) == "ffb300",
|
||||
"theme direct_accent parses to ffb300 (got %s)" % stage._accent_direct.to_html(false))
|
||||
|
||||
|
||||
func _test_bresenham(stage: Node2D) -> void:
|
||||
print("")
|
||||
print("--- Bresenham pathing ---")
|
||||
# Horizontal run: (0,0)->(3,0) yields 4 cells on the row.
|
||||
var cells: Array[Vector2i] = stage._bresenham_cells(Vector2i(0, 0), Vector2i(3, 0))
|
||||
_check(cells.size() == 4, "horizontal (0,0)->(3,0) has 4 cells (got %d)" % cells.size())
|
||||
_check(_cells_on_row(cells, 0), "horizontal run stays on row y=0")
|
||||
# Diagonal: (0,0)->(3,3) yields exactly 4 staircase cells.
|
||||
var diag: Array[Vector2i] = stage._bresenham_cells(Vector2i(0, 0), Vector2i(3, 3))
|
||||
_check(diag.size() == 4, "diagonal (0,0)->(3,3) has 4 cells (got %d)" % diag.size())
|
||||
_check(_is_staircase(diag), "diagonal run is a monotonic staircase")
|
||||
# Single cell: anchor == target yields one cell.
|
||||
var single: Array[Vector2i] = stage._bresenham_cells(Vector2i(2, 2), Vector2i(2, 2))
|
||||
_check(single.size() == 1, "anchor==target yields 1 cell (got %d)" % single.size())
|
||||
|
||||
|
||||
func _test_rasterize(stage: Node2D) -> void:
|
||||
print("")
|
||||
print("--- AABB rasterization ---")
|
||||
# A single 16x16 cell at the origin maps to exactly one cell.
|
||||
var one: Array[Vector2i] = stage._rasterize_aabb_to_cells(Rect2(0.0, 0.0, 16.0, 16.0))
|
||||
_check(one.size() == 1 and one[0] == Vector2i(0, 0),
|
||||
"16x16 AABB at origin rasterizes to cell (0,0)")
|
||||
# A ground block spans 200x32 centered at origin; with grid 16 that touches
|
||||
# 14 columns (x -7..6) x 2 rows (y -1..0) = 28 cells.
|
||||
var aabb := Rect2(Vector2(-100.0, -16.0), Vector2(200.0, 32.0))
|
||||
var cells: Array[Vector2i] = stage._rasterize_aabb_to_cells(aabb)
|
||||
_check(cells.size() == 28, "ground AABB rasterizes to 28 cells (got %d)" % cells.size())
|
||||
_check(cells.has(Vector2i(-7, -1)) and cells.has(Vector2i(6, 0)),
|
||||
"ground rasterization covers its corner cells")
|
||||
# Empty / degenerate AABB yields no cells.
|
||||
_check(stage._rasterize_aabb_to_cells(Rect2()).is_empty(), "empty AABB yields no cells")
|
||||
|
||||
|
||||
func _test_classify(stage: Node2D) -> void:
|
||||
print("")
|
||||
print("--- Three-state occupancy query ---")
|
||||
# Spawn two ground blocks at distinct cells into the World; classify.
|
||||
var world: Node2D = stage._world
|
||||
var ground_a := TERRAIN_UTILS.spawn_block(world, PackedVector2Array([
|
||||
Vector2(-100, -16), Vector2(100, -16), Vector2(100, 16), Vector2(-100, 16),
|
||||
]), STAGE_SPAWNER.TERRAIN_GRID_SIZE)
|
||||
ground_a.spawn_id = "ground"
|
||||
ground_a.position = stage._terrain_cell_center(Vector2i(0, 0))
|
||||
stage._rebuild_grid_cells()
|
||||
|
||||
stage.set_placement_mode("ground")
|
||||
var center_cell := Vector2i(0, 0)
|
||||
var far_cell := Vector2i(20, 20)
|
||||
_check(stage._classify_cell(center_cell) == 2,
|
||||
"same-type overlap classified as skip (2)")
|
||||
_check(stage._classify_cell(far_cell) == 1,
|
||||
"empty cell classified as empty (1)")
|
||||
stage.set_placement_mode("")
|
||||
|
||||
# A conflicting object (a crate with real polygon geometry) overlapping a
|
||||
# cell returns 3.
|
||||
var crate := PROP_BLOCK.new()
|
||||
crate.name = "Crate"
|
||||
crate.polygon_points = PackedVector2Array([
|
||||
Vector2(-24, -24), Vector2(24, -24), Vector2(24, 24), Vector2(-24, 24),
|
||||
])
|
||||
crate.position = stage._terrain_cell_center(Vector2i(0, 0))
|
||||
world.add_child(crate)
|
||||
stage._rebuild_grid_cells()
|
||||
stage.set_placement_mode("ground")
|
||||
_check(stage._classify_cell(center_cell) == 3,
|
||||
"conflicting object classified as blocked (3)")
|
||||
stage.set_placement_mode("")
|
||||
|
||||
crate.queue_free()
|
||||
ground_a.queue_free()
|
||||
|
||||
|
||||
func _cells_on_row(cells: Array[Vector2i], y: int) -> bool:
|
||||
for c: Vector2i in cells:
|
||||
if c.y != y:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _is_staircase(cells: Array[Vector2i]) -> bool:
|
||||
for i: int in range(1, cells.size()):
|
||||
var d := cells[i] - cells[i - 1]
|
||||
if absi(d.x) != 1 or absi(d.y) != 1:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: " + message)
|
||||
else:
|
||||
_failures += 1
|
||||
print("FAIL: " + message)
|
||||
Reference in New Issue
Block a user