Files
stickman/tests/test_phase4b_terrain.gd
T
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

121 lines
4.6 KiB
GDScript

# test_phase4b_terrain.gd
# Headless checks for Phase 4b terrain drag-painting batch commit semantics
# (spec §2.6.4/2.6.5 + §5.5/5.6):
# 1. Dragging across an EMPTY open region commits one block per path cell.
# 2. Dragging over an existing same-type block commits ZERO new blocks
# (same-type overlap skip; advisory dictionary, no double-create).
# 3. A cell occupied by a conflicting prop is skipped while its empty
# neighbours still spawn (Case C).
# 4. A drag never leaves ghost terrain behind in the World.
#
# Run with:
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase4b_terrain.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 TERRAIN_BLOCK := preload("res://scripts/terrain_block.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("")
print("========================================================")
print(" PHASE 4b TERRAIN DRAG COMMIT TEST (headless)")
print("========================================================")
var stage: Node2D = STAGE_SCENE.instantiate()
root.add_child(stage)
var world: Node2D = stage._world
# --- Empty-region drag: 3 path cells -> 3 blocks ---
var before := _terrain_count(world)
stage.set_mode(stage.StageMode.EDIT)
stage.set_placement_mode("ground")
stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(0, 0)))
stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(2, 0)))
_check(stage._drag_cells.size() == 3, "path has 3 cells (got %d)" % stage._drag_cells.size())
stage._commit_terrain_drag()
var after_empty := _terrain_count(world)
_check(after_empty - before == 3, "empty drag committed 3 blocks (got %d)" % (after_empty - before))
# --- Same-type overlap drag: over a region now occupied by ground -> 0 ---
var b2 := _terrain_count(world)
stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(0, 0)))
stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(2, 0)))
stage._commit_terrain_drag()
var a2 := _terrain_count(world)
_check(a2 - b2 == 0, "same-type overlap drag committed 0 blocks (got %d)" % (a2 - b2))
# --- Conflicting-object skip: put a crate in a far empty region, drag over it ---
# With block stride (Ground = 200x32), the crate's 48x48 AABB covers exactly
# one block cell (70,0), not a 16-px span.
var crate := _spawn_crate(world, stage._terrain_cell_center(Vector2i(70, 0)))
stage._rebuild_grid_cells()
var b3 := _terrain_count(world)
# Drag cells (65,0)..(75,0): the middle cell (70,0) conflict-red, edges empty.
stage._begin_terrain_drag(stage._terrain_cell_center(Vector2i(65, 0)))
stage._update_terrain_drag(stage._terrain_cell_center(Vector2i(75, 0)))
_check(stage._drag_cells.size() == 11, "conflict drag path has 11 cells (got %d)" % stage._drag_cells.size())
var cls_edge: int = stage._classify_cell(Vector2i(65, 0))
var cls_mid: int = stage._classify_cell(Vector2i(70, 0))
_check(cls_mid == 3, "crate cell classified conflict (3) (got %d)" % cls_mid)
_check(cls_edge == 1, "edge cell (65,0) empty (1) (got %d)" % cls_edge)
stage._commit_terrain_drag()
var a3 := _terrain_count(world)
# Path length 11 minus the single crate cell that gets skipped = 10 commits.
_check(a3 - b3 == 10, "conflict drag committed only empty cells (expected 10, got %d)" % (a3 - b3))
# --- No ghost terrain leaked into World ---
var ghost_blocks := 0
for child: Node in world.get_children():
if child is TERRAIN_BLOCK:
ghost_blocks += 1
_check(ghost_blocks == a3, "World contains only committed blocks (got %d, expected %d)" % [ghost_blocks, a3])
crate.queue_free()
stage.queue_free()
await process_frame
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 _spawn_crate(world: Node2D, pos: Vector2) -> Node2D:
var crate = preload("res://scripts/prop_block.gd").new()
crate.name = "Crate"
crate.polygon_points = PackedVector2Array([
Vector2(-24, -24), Vector2(24, -24), Vector2(24, 24), Vector2(-24, 24),
])
crate.position = pos
world.add_child(crate)
return crate
func _terrain_count(world: Node2D) -> int:
var n := 0
for child: Node in world.get_children():
if child is TERRAIN_BLOCK:
n += 1
return n
func _check(condition: bool, message: String) -> void:
_checks += 1
if condition:
print("PASS: " + message)
else:
_failures += 1
print("FAIL: " + message)