Files
stickman/tests/test_phase4b_logic.gd
ryan 1f91f3d2e5 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.
2026-09-04 15:08:08 -04:00

160 lines
5.9 KiB
GDScript

# 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)