refactor: enhance test framework with automated resource tracking and scripted error capture capabilities

This commit is contained in:
2026-06-26 09:40:17 +08:00
parent 948a99cf90
commit 00f9d98f4b
58 changed files with 3594 additions and 1289 deletions
+208
View File
@@ -0,0 +1,208 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Sticky Cell System (v2) [Gauntlet #068]
# Covers cell states, CLEANSED protection, coverage helpers, and the
# path-safety reachability (BFS) check.
# =============================================================================
var GauntletManagerScript = load("res://scripts/managers/gauntlet_manager.gd")
var manager: GauntletManager
func before_each():
manager = GauntletManagerScript.new()
add_child(manager)
func after_each():
manager.queue_free()
# =============================================================================
# CellState enum
# =============================================================================
func test_cellstate_enum_has_six_states():
assert_eq(manager.CellState.size(), 6, "CellState should have 6 states")
func test_cellstate_values():
assert_eq(manager.CellState.SAFE, 0, "SAFE should be 0")
assert_eq(manager.CellState.TELEGRAPHED, 1, "TELEGRAPHED should be 1")
assert_eq(manager.CellState.STICKY, 2, "STICKY should be 2")
assert_eq(manager.CellState.BUBBLE_GROWING, 3, "BUBBLE_GROWING should be 3")
assert_eq(manager.CellState.BLOCKED, 4, "BLOCKED should be 4")
assert_eq(manager.CellState.CLEANSED, 5, "CLEANSED should be 5")
# =============================================================================
# cell_state() classification
# =============================================================================
func test_interior_cell_is_safe():
assert_eq(manager.cell_state(Vector2i(3, 3)), manager.CellState.SAFE, "Open interior cell is SAFE")
func test_npc_zone_is_blocked():
assert_eq(manager.cell_state(Vector2i(9, 9)), manager.CellState.BLOCKED, "NPC zone is BLOCKED")
func test_boundary_is_blocked():
assert_eq(manager.cell_state(Vector2i(0, 5)), manager.CellState.BLOCKED, "Boundary is BLOCKED")
assert_eq(manager.cell_state(Vector2i(19, 5)), manager.CellState.BLOCKED, "Far boundary is BLOCKED")
func test_sticky_cell_state():
manager.sticky_cells[Vector2i(4, 4)] = true
assert_eq(manager.cell_state(Vector2i(4, 4)), manager.CellState.STICKY, "Sticky cell is STICKY")
func test_telegraphed_cell_state():
manager.telegraphed_cells[Vector2i(5, 5)] = 1.0
assert_eq(manager.cell_state(Vector2i(5, 5)), manager.CellState.TELEGRAPHED, "Telegraphed cell is TELEGRAPHED")
func test_cleansed_cell_state():
manager.mark_cleansed(Vector2i(6, 6))
assert_eq(manager.cell_state(Vector2i(6, 6)), manager.CellState.CLEANSED, "Cleansed cell is CLEANSED")
func test_sticky_takes_priority_over_cleansed():
# A cell that is both should report STICKY (active hazard wins).
manager.sticky_cells[Vector2i(7, 7)] = true
manager.cleansed_cells[Vector2i(7, 7)] = 5.0
assert_eq(manager.cell_state(Vector2i(7, 7)), manager.CellState.STICKY, "Sticky wins over cleansed")
# =============================================================================
# CLEANSED protection lifecycle
# =============================================================================
func test_mark_cleansed_sets_protection_time():
manager.mark_cleansed(Vector2i(3, 4))
assert_true(manager.is_cleansed_cell(Vector2i(3, 4)), "Cell should be cleansed")
assert_almost_eq(manager.cleansed_cells[Vector2i(3, 4)], manager.CLEANSED_PROTECTION_TIME, 0.001, "Protection time set")
func test_clear_sticky_marks_cleansed():
manager.sticky_cells[Vector2i(4, 5)] = true
manager.clear_sticky_cell(Vector2i(4, 5))
assert_false(manager.is_sticky_cell(Vector2i(4, 5)), "Sticky removed")
assert_true(manager.is_cleansed_cell(Vector2i(4, 5)), "Cleared cell becomes cleansed")
func test_tick_cleansed_decays_and_expires():
manager.mark_cleansed(Vector2i(5, 6))
manager._tick_cleansed_cells(manager.CLEANSED_PROTECTION_TIME + 0.1)
assert_false(manager.is_cleansed_cell(Vector2i(5, 6)), "Protection expires after full duration")
func test_tick_cleansed_partial_decay_keeps_cell():
manager.mark_cleansed(Vector2i(5, 7))
manager._tick_cleansed_cells(1.0)
assert_true(manager.is_cleansed_cell(Vector2i(5, 7)), "Cell still protected after partial tick")
# =============================================================================
# Coverage helpers (v2 target 70-75%)
# =============================================================================
func test_coverage_targets():
assert_almost_eq(manager.COVERAGE_TARGET_MIN, 0.70, 0.001, "Min coverage 70%")
assert_almost_eq(manager.COVERAGE_TARGET_MAX, 0.75, 0.001, "Max coverage 75%")
func test_playable_cell_count():
# 20x20 = 400, minus 76 boundary cells, minus 9 NPC zone = 315
assert_eq(manager.playable_cell_count(), 315, "Playable cells = 315")
func test_coverage_ratio_zero_when_empty():
assert_almost_eq(manager.coverage_ratio(), 0.0, 0.001, "No sticky cells = 0 coverage")
func test_coverage_ratio_scales():
var playable := manager.playable_cell_count()
# Fill ~half the playable cells with arbitrary distinct keys.
var half := int(playable / 2.0)
for i in range(half):
manager.sticky_cells[Vector2i(1000 + i, 0)] = true
assert_almost_eq(manager.coverage_ratio(), float(half) / float(playable), 0.001, "Coverage tracks ratio")
func test_coverage_reached_threshold():
var playable := manager.playable_cell_count()
var needed := int(ceil(playable * manager.COVERAGE_TARGET_MIN))
for i in range(needed):
manager.sticky_cells[Vector2i(2000 + i, 0)] = true
assert_true(manager.is_coverage_reached(), "Coverage reached at >=70%")
func test_coverage_not_reached_below_threshold():
manager.sticky_cells[Vector2i(2, 2)] = true
assert_false(manager.is_coverage_reached(), "One sticky cell is below target")
# =============================================================================
# Path safety: passability + reachability (BFS)
# =============================================================================
func test_passable_interior():
assert_true(manager._is_cell_passable(Vector2i(3, 3)), "Open interior is passable")
func test_not_passable_boundary_or_npc():
assert_false(manager._is_cell_passable(Vector2i(0, 0)), "Boundary not passable")
assert_false(manager._is_cell_passable(Vector2i(9, 9)), "NPC zone not passable")
func test_not_passable_sticky():
manager.sticky_cells[Vector2i(3, 3)] = true
assert_false(manager._is_cell_passable(Vector2i(3, 3)), "Sticky cell not passable")
func test_extra_sticky_blocks_passability():
var extra := {Vector2i(4, 4): true}
assert_false(manager._is_cell_passable(Vector2i(4, 4), extra), "Hypothetical sticky blocks")
assert_true(manager._is_cell_passable(Vector2i(5, 5), extra), "Other cells still passable")
func test_open_arena_has_large_safe_region():
# From an open interior cell, flood fill should easily exceed the minimum.
var n := manager._reachable_safe_cells(Vector2i(3, 3), {}, 50)
assert_true(n >= 50, "Open arena reaches the search cap")
func test_player_has_safe_region_when_open():
assert_true(manager._player_has_safe_region(Vector2i(3, 3), {}), "Open cell has safe region")
func test_fully_boxed_player_has_no_safe_region():
# Box in the cell at (3,3) on all 4 sides with hypothetical sticky.
var extra := {
Vector2i(2, 3): true, Vector2i(4, 3): true,
Vector2i(3, 2): true, Vector2i(3, 4): true,
}
assert_false(manager._player_has_safe_region(Vector2i(3, 3), extra), "Boxed-in player has no safe region")
func test_reachable_zero_when_start_blocked():
manager.sticky_cells[Vector2i(3, 3)] = true
assert_eq(manager._reachable_safe_cells(Vector2i(3, 3), {}, 10), 0, "Blocked start reaches nothing")
# =============================================================================
# Sticky entry → per-player slow (v2: no hard trap, no global time_scale)
# =============================================================================
# Minimal stand-in for a Player that records apply_slow_effect calls.
class SlowSpyPlayer:
extends Node
var slow_calls: Array = []
func apply_slow_effect(duration: float = 3.0) -> void:
slow_calls.append(duration)
func test_apply_sticky_slow_calls_player_slow():
var spy := SlowSpyPlayer.new()
add_child(spy)
# Force the local-call branch (no networked rpc) for a deterministic unit test.
var saved_peer = multiplayer.multiplayer_peer
multiplayer.multiplayer_peer = null
manager.apply_sticky_slow(spy)
multiplayer.multiplayer_peer = saved_peer
assert_eq(spy.slow_calls.size(), 1, "Sticky slow invokes apply_slow_effect once")
assert_almost_eq(spy.slow_calls[0], manager.STICKY_SLOW_DURATION, 0.001, "Slows for STICKY_SLOW_DURATION")
spy.queue_free()
func test_apply_sticky_slow_does_not_trap():
var spy := SlowSpyPlayer.new()
spy.set("peer_id", 42)
add_child(spy)
var saved_peer = multiplayer.multiplayer_peer
multiplayer.multiplayer_peer = null
manager.apply_sticky_slow(spy)
multiplayer.multiplayer_peer = saved_peer
assert_false(manager.trapped_players.has(42), "Sticky slow never adds to trapped_players")
spy.queue_free()
func test_apply_sticky_slow_safe_without_method():
# A node lacking apply_slow_effect must not crash the call.
var plain := Node.new()
add_child(plain)
manager.apply_sticky_slow(plain) # should no-op
assert_true(true, "apply_sticky_slow tolerates players without the method")
plain.queue_free()
func test_sticky_slow_duration_is_positive():
assert_true(manager.STICKY_SLOW_DURATION > 0.0, "Sticky slow duration is a positive number")