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