refactor: enhance test framework with automated resource tracking and scripted error capture capabilities
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
extends Node
|
||||
|
||||
# Minimal EnhancedGridMap stand-in for Gauntlet headless tests. Records
|
||||
# set_cell_item calls so lifecycle tests can run the local sync path without a
|
||||
# real GridMap. Only the surface the manager touches is implemented.
|
||||
|
||||
var cell_size := Vector3(1, 1, 1)
|
||||
var cells: Dictionary = {} # Vector3i -> item id
|
||||
var astar_inits := 0
|
||||
|
||||
func set_cell_item(pos: Vector3i, item: int, _orientation: int = 0) -> void:
|
||||
if item == -1:
|
||||
cells.erase(pos)
|
||||
else:
|
||||
cells[pos] = item
|
||||
|
||||
func get_cell_item(pos: Vector3i) -> int:
|
||||
return cells.get(pos, -1)
|
||||
|
||||
func initialize_astar() -> void:
|
||||
astar_inits += 1
|
||||
|
||||
func update_grid_data() -> void:
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7ihsm80fbyb5
|
||||
@@ -0,0 +1,9 @@
|
||||
extends Node
|
||||
|
||||
# Minimal "Main" stand-in for Gauntlet headless tests. Provides the RPC methods
|
||||
# the GauntletManager calls on its main_scene so calls resolve without the full
|
||||
# game scene. Methods are no-ops that just need to exist + be rpc-tagged.
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func sync_grid_item(_x: int, _y: int, _z: int, _item: int) -> void:
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://ca04jq87bj3ap
|
||||
@@ -0,0 +1,163 @@
|
||||
extends GutTest
|
||||
|
||||
# =============================================================================
|
||||
# Test: Gauntlet Candy Bubble System (v2) [Gauntlet #082]
|
||||
# Covers bubble-specific scoring components, phase budgets, anti-stacking,
|
||||
# the 3x3 blast footprint, and the grow→explode lifecycle.
|
||||
# Runs headless (no multiplayer peer): elapsed_time = 0 so the final-30s window
|
||||
# is inactive unless a test sets it directly.
|
||||
# =============================================================================
|
||||
|
||||
const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
|
||||
const GridMapMock = preload("res://tests/helpers/gridmap_mock.gd")
|
||||
var manager
|
||||
var main_mock: Node
|
||||
var gridmap_mock: Node
|
||||
|
||||
func before_each():
|
||||
main_mock = Node.new()
|
||||
add_child(main_mock)
|
||||
gridmap_mock = GridMapMock.new()
|
||||
gridmap_mock.name = "EnhancedGridMap"
|
||||
main_mock.add_child(gridmap_mock)
|
||||
manager = GauntletManager.new()
|
||||
main_mock.add_child(manager)
|
||||
manager.initialize(main_mock, gridmap_mock)
|
||||
manager.current_phase = 0
|
||||
|
||||
func after_each():
|
||||
if main_mock:
|
||||
main_mock.queue_free()
|
||||
|
||||
# Run a callable with the multiplayer peer detached so manager code takes the
|
||||
# local (non-rpc) sync path — deterministic for headless lifecycle tests.
|
||||
func _without_peer(fn: Callable) -> void:
|
||||
var saved = multiplayer.multiplayer_peer
|
||||
multiplayer.multiplayer_peer = null
|
||||
fn.call()
|
||||
multiplayer.multiplayer_peer = saved
|
||||
|
||||
# =============================================================================
|
||||
# Phase budget
|
||||
# =============================================================================
|
||||
|
||||
func test_bubble_budget_per_phase():
|
||||
manager.current_phase = 0
|
||||
assert_eq(manager._bubble_budget_for_phase(), 0, "Phase 1 → 0 bubbles")
|
||||
manager.current_phase = 1
|
||||
assert_eq(manager._bubble_budget_for_phase(), 2, "Phase 2 → 2 bubbles")
|
||||
manager.current_phase = 2
|
||||
assert_eq(manager._bubble_budget_for_phase(), 3, "Phase 3 → 3 bubbles")
|
||||
|
||||
func test_phase_change_resets_counter():
|
||||
manager.bubbles_this_phase = 2
|
||||
manager._start_phase(manager.Phase.SURVIVAL_ENDGAME)
|
||||
assert_eq(manager.bubbles_this_phase, 0, "Per-phase bubble count resets on phase change")
|
||||
|
||||
# =============================================================================
|
||||
# Blast footprint (3x3, clipped)
|
||||
# =============================================================================
|
||||
|
||||
func test_blast_is_3x3_in_open_area():
|
||||
var cells = manager._bubble_blast_cells(Vector2i(14, 14))
|
||||
assert_eq(cells.size(), 9, "Open-area bubble blast is 3x3 = 9 cells")
|
||||
|
||||
func test_blast_clips_npc_zone():
|
||||
# Center adjacent to the NPC zone (9,9) clips blocked cells out.
|
||||
var cells = manager._bubble_blast_cells(Vector2i(7, 9))
|
||||
assert_true(cells.size() < 9, "Blast near NPC zone is clipped below 9")
|
||||
for c in cells:
|
||||
assert_false(manager._is_npc_zone(c), "No blast cell lands in NPC zone")
|
||||
|
||||
# =============================================================================
|
||||
# Scoring components
|
||||
# =============================================================================
|
||||
|
||||
func test_bubble_camping_thresholds():
|
||||
var region: Vector2i = manager._region_of(Vector2i(8, 8))
|
||||
manager._camp_tracking[1] = {"region": region, "time": 6.0}
|
||||
assert_eq(manager._bubble_score_camping(Vector2i(8, 8)), 40.0, ">5s = +40")
|
||||
manager._camp_tracking[1]["time"] = 9.0
|
||||
assert_eq(manager._bubble_score_camping(Vector2i(8, 8)), 60.0, ">8s = +60")
|
||||
|
||||
func test_bubble_player_cluster():
|
||||
var players = [Vector2i(5, 5), Vector2i(6, 6)]
|
||||
assert_eq(manager._bubble_score_player_cluster(Vector2i(5, 6), players), 20.0, "2 nearby players = +20")
|
||||
assert_eq(manager._bubble_score_player_cluster(Vector2i(15, 15), players), 0.0, "No nearby players = 0")
|
||||
|
||||
func test_bubble_direct_hit_penalty():
|
||||
var players = [Vector2i(5, 5)]
|
||||
assert_eq(manager._bubble_score_direct_hit(Vector2i(5, 5), players), -60.0, "Directly under player = -60")
|
||||
assert_eq(manager._bubble_score_direct_hit(Vector2i(8, 8), players), 0.0, "Not under player = 0")
|
||||
|
||||
func test_bubble_recent_penalty():
|
||||
manager.recent_bubble_positions = [Vector2i(14, 14)]
|
||||
assert_eq(manager._bubble_score_recent(Vector2i(11, 11)), -50.0, "Near recent bubble = -50")
|
||||
assert_eq(manager._bubble_score_recent(Vector2i(2, 2)), 0.0, "Far from recent bubble = 0")
|
||||
|
||||
func test_bubble_untouched_area():
|
||||
# Open arena around (10,10) → large reachable region → +30.
|
||||
assert_eq(manager._bubble_score_untouched_area(Vector2i(14, 14)), 30.0, "Large untouched area = +30")
|
||||
|
||||
func test_bubble_full_score_is_finite():
|
||||
var s = manager._calculate_bubble_score(Vector2i(8, 8), [])
|
||||
assert_true(is_finite(s), "Full bubble score is finite")
|
||||
|
||||
# =============================================================================
|
||||
# Spawn lifecycle
|
||||
# =============================================================================
|
||||
|
||||
func test_spawn_bubble_marks_growing_cells():
|
||||
_without_peer(func():
|
||||
manager._spawn_bubble(Vector2i(14, 14))
|
||||
)
|
||||
assert_eq(manager.bubbles_this_phase, 1, "Phase counter increments")
|
||||
assert_eq(manager.bubbles_total, 1, "Round counter increments")
|
||||
assert_eq(manager.active_bubbles.size(), 1, "One active bubble")
|
||||
assert_true(manager.bubble_cells.has(Vector2i(14, 14)), "Center marked BUBBLE_GROWING")
|
||||
assert_eq(manager.cell_state(Vector2i(14, 14)), manager.CellState.BUBBLE_GROWING, "cell_state reports BUBBLE_GROWING")
|
||||
|
||||
func test_spawn_bubble_records_recent_position():
|
||||
_without_peer(func():
|
||||
manager._spawn_bubble(Vector2i(14, 14))
|
||||
)
|
||||
assert_true(manager.recent_bubble_positions.has(Vector2i(14, 14)), "Center remembered for anti-stacking")
|
||||
|
||||
func test_recent_positions_capped():
|
||||
_without_peer(func():
|
||||
for i in range(manager.BUBBLE_RECENT_MEMORY + 3):
|
||||
manager._spawn_bubble(Vector2i(2 + i, 15))
|
||||
)
|
||||
assert_eq(manager.recent_bubble_positions.size(), manager.BUBBLE_RECENT_MEMORY, "Recent memory capped")
|
||||
|
||||
# =============================================================================
|
||||
# Explosion
|
||||
# =============================================================================
|
||||
|
||||
func test_update_bubbles_explodes_after_grow_duration():
|
||||
_without_peer(func():
|
||||
manager._spawn_bubble(Vector2i(14, 14))
|
||||
manager._update_bubbles(manager.BUBBLE_GROW_DURATION + 0.1)
|
||||
)
|
||||
assert_eq(manager.active_bubbles.size(), 0, "Bubble removed after exploding")
|
||||
assert_true(manager.sticky_cells.has(Vector2i(14, 14)), "Center became sticky")
|
||||
assert_false(manager.bubble_cells.has(Vector2i(14, 14)), "BUBBLE_GROWING cleared on explode")
|
||||
|
||||
func test_update_bubbles_waits_for_timer():
|
||||
_without_peer(func():
|
||||
manager._spawn_bubble(Vector2i(14, 14))
|
||||
manager._update_bubbles(manager.BUBBLE_GROW_DURATION * 0.5)
|
||||
)
|
||||
assert_eq(manager.active_bubbles.size(), 1, "Bubble still growing before timer elapses")
|
||||
assert_false(manager.sticky_cells.has(Vector2i(14, 14)), "No sticky yet mid-grow")
|
||||
|
||||
func test_explode_creates_3x3_sticky():
|
||||
_without_peer(func():
|
||||
manager._explode_bubble(Vector2i(14, 14), manager._bubble_blast_cells(Vector2i(14, 14)))
|
||||
)
|
||||
var sticky_in_blast := 0
|
||||
for dx in range(-1, 2):
|
||||
for dz in range(-1, 2):
|
||||
if manager.sticky_cells.has(Vector2i(14 + dx, 14 + dz)):
|
||||
sticky_in_blast += 1
|
||||
assert_eq(sticky_in_blast, 9, "Explosion creates a full 3x3 sticky area")
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkte51v8tyoii
|
||||
@@ -1,41 +0,0 @@
|
||||
extends GutTest
|
||||
|
||||
const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
|
||||
var gauntlet_manager: Node
|
||||
var main_mock: Node
|
||||
var gridmap_mock: Node
|
||||
|
||||
func before_all():
|
||||
gut.p("=== Feature Tests [Gauntlet #4 Cannon Timer] ===")
|
||||
|
||||
func before_each():
|
||||
main_mock = Node.new()
|
||||
add_child(main_mock)
|
||||
gridmap_mock = Node.new()
|
||||
gridmap_mock.name = "EnhancedGridMap"
|
||||
main_mock.add_child(gridmap_mock)
|
||||
|
||||
gauntlet_manager = GauntletManager.new()
|
||||
main_mock.add_child(gauntlet_manager)
|
||||
gauntlet_manager.initialize(main_mock, gridmap_mock)
|
||||
|
||||
func test_cannon_timer_initialization():
|
||||
assert_eq(gauntlet_manager.cannon_timer, 0.0, "Timer should start at 0.0 before phase starts")
|
||||
|
||||
# Manually start phase to setup interval
|
||||
gauntlet_manager.current_phase = 0 # GauntletManager.Phase.OPEN_ARENA
|
||||
var config = gauntlet_manager.phase_configs[0]
|
||||
gauntlet_manager.cannon_interval = config["interval"]
|
||||
gauntlet_manager.cannon_timer = gauntlet_manager.cannon_interval
|
||||
|
||||
assert_eq(gauntlet_manager.cannon_timer, 5.0, "Timer should initialize to Phase 1 interval (5.0)")
|
||||
|
||||
func test_volley_size_configuration():
|
||||
assert_eq(gauntlet_manager.phase_configs[0]["volley"], 5, "Phase 1 volley size should be 5")
|
||||
|
||||
func after_each():
|
||||
if main_mock:
|
||||
main_mock.queue_free()
|
||||
|
||||
func after_all():
|
||||
gut.p("=== Feature Tests Complete ===")
|
||||
@@ -1 +0,0 @@
|
||||
uid://ct0psnc84v1sy
|
||||
@@ -0,0 +1,115 @@
|
||||
extends GutTest
|
||||
|
||||
# =============================================================================
|
||||
# Test: Gauntlet Cleanser Power-Up (v2) [Gauntlet #072]
|
||||
# Covers grant cadence (every 2 missions, max 1), 5-cell immunity lifecycle,
|
||||
# sticky clearing, stun-blocked activation, and the safe-stop early termination.
|
||||
# Runs headless; uses a GridMap mock so clear_sticky_cell can run locally.
|
||||
# =============================================================================
|
||||
|
||||
const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
|
||||
const GridMapMock = preload("res://tests/helpers/gridmap_mock.gd")
|
||||
const MainMock = preload("res://tests/helpers/main_mock.gd")
|
||||
var manager
|
||||
var main_mock: Node
|
||||
var gridmap_mock: Node
|
||||
|
||||
func before_each():
|
||||
main_mock = MainMock.new()
|
||||
add_child(main_mock)
|
||||
gridmap_mock = GridMapMock.new()
|
||||
gridmap_mock.name = "EnhancedGridMap"
|
||||
main_mock.add_child(gridmap_mock)
|
||||
manager = GauntletManager.new()
|
||||
main_mock.add_child(manager)
|
||||
manager.initialize(main_mock, gridmap_mock)
|
||||
manager.current_phase = 0
|
||||
|
||||
func after_each():
|
||||
if main_mock:
|
||||
main_mock.queue_free()
|
||||
|
||||
func _without_peer(fn: Callable) -> void:
|
||||
var saved = multiplayer.multiplayer_peer
|
||||
multiplayer.multiplayer_peer = null
|
||||
fn.call()
|
||||
multiplayer.multiplayer_peer = saved
|
||||
|
||||
# =============================================================================
|
||||
# Grant cadence: every 2 missions, inventory max 1
|
||||
# =============================================================================
|
||||
|
||||
func test_no_cleanser_after_one_mission():
|
||||
manager._on_goal_count_updated(7, 1)
|
||||
assert_eq(manager.player_cleansers.get(7, 0), 0, "No cleanser after 1 mission")
|
||||
|
||||
func test_cleanser_granted_after_two_missions():
|
||||
manager._on_goal_count_updated(7, 1)
|
||||
manager._on_goal_count_updated(7, 2)
|
||||
assert_eq(manager.player_cleansers.get(7, 0), 1, "Cleanser granted after 2 missions")
|
||||
|
||||
func test_cleanser_inventory_capped_at_one():
|
||||
# Four missions would be two grants, but inventory caps at 1 (not consumed).
|
||||
for i in range(4):
|
||||
manager._on_goal_count_updated(7, i + 1)
|
||||
assert_eq(manager.player_cleansers.get(7, 0), 1, "Inventory never exceeds 1")
|
||||
|
||||
# =============================================================================
|
||||
# Activation / immunity lifecycle
|
||||
# =============================================================================
|
||||
|
||||
func test_use_cleanser_cell_decrements_until_exhausted():
|
||||
manager.cleanser_active[3] = true
|
||||
manager.cleanser_cells_left[3] = manager.CLEANSER_MAX_CELLS
|
||||
# First 4 uses keep it active...
|
||||
for i in range(manager.CLEANSER_MAX_CELLS - 1):
|
||||
assert_true(manager.use_cleanser_cell(3), "Still active on use %d" % (i + 1))
|
||||
# ...the 5th use exhausts it.
|
||||
assert_false(manager.use_cleanser_cell(3), "Exhausted after 5th cell")
|
||||
assert_false(manager.is_cleanser_active(3), "Deactivated after 5th cell")
|
||||
|
||||
func test_use_cleanser_cell_when_inactive_returns_false():
|
||||
assert_false(manager.use_cleanser_cell(99), "Inactive cleanser use returns false")
|
||||
|
||||
func test_deactivate_clears_state():
|
||||
manager.cleanser_active[5] = true
|
||||
manager.cleanser_cells_left[5] = 3
|
||||
manager.deactivate_cleanser(5)
|
||||
assert_false(manager.is_cleanser_active(5), "Deactivated")
|
||||
assert_false(manager.cleanser_cells_left.has(5), "Cells-left cleared")
|
||||
|
||||
# =============================================================================
|
||||
# Sticky clearing
|
||||
# =============================================================================
|
||||
|
||||
func test_clear_sticky_cell_removes_and_protects():
|
||||
manager.sticky_cells[Vector2i(4, 4)] = true
|
||||
_without_peer(func():
|
||||
manager.clear_sticky_cell(Vector2i(4, 4))
|
||||
)
|
||||
assert_false(manager.is_sticky_cell(Vector2i(4, 4)), "Sticky removed")
|
||||
assert_true(manager.is_cleansed_cell(Vector2i(4, 4)), "Cleared cell gets regrowth protection")
|
||||
# Layer-2 overlay cleared (mock records -1 = erased).
|
||||
assert_eq(gridmap_mock.get_cell_item(Vector3i(4, 2, 4)), -1, "Layer-2 sticky overlay cleared")
|
||||
|
||||
# =============================================================================
|
||||
# Safe-stop early termination (#072 acceptance: ends when stopping on safe cell)
|
||||
# =============================================================================
|
||||
|
||||
func test_stop_on_safe_cell_ends_cleanser():
|
||||
manager.cleanser_active[8] = true
|
||||
manager.cleanser_cells_left[8] = 3
|
||||
manager.notify_movement_stopped(8, Vector2i(5, 5)) # safe cell
|
||||
assert_false(manager.is_cleanser_active(8), "Cleanser ends on safe-cell stop")
|
||||
|
||||
func test_stop_on_sticky_cell_keeps_cleanser():
|
||||
manager.sticky_cells[Vector2i(6, 6)] = true
|
||||
manager.cleanser_active[8] = true
|
||||
manager.cleanser_cells_left[8] = 3
|
||||
manager.notify_movement_stopped(8, Vector2i(6, 6)) # still on sticky
|
||||
assert_true(manager.is_cleanser_active(8), "Cleanser persists while still on sticky")
|
||||
|
||||
func test_notify_stop_noop_without_cleanser():
|
||||
# Should not crash or change anything when the player has no cleanser.
|
||||
manager.notify_movement_stopped(123, Vector2i(5, 5))
|
||||
assert_false(manager.is_cleanser_active(123), "No cleanser → no-op")
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1bay8n1h65u3
|
||||
@@ -0,0 +1,159 @@
|
||||
extends GutTest
|
||||
|
||||
# =============================================================================
|
||||
# Test: Gauntlet Growth Tick System (v2) [Gauntlet #067]
|
||||
# Replaces the old cannon-timer test. Covers growth timing, phase configs,
|
||||
# candidate generation, cells-per-tick ranges, weighted selection, and
|
||||
# cleansed-cell exclusion.
|
||||
# =============================================================================
|
||||
|
||||
const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
|
||||
var gauntlet_manager: Node
|
||||
var main_mock: Node
|
||||
var gridmap_mock: Node
|
||||
|
||||
func before_all():
|
||||
gut.p("=== Feature Tests [Gauntlet #067 Growth Tick] ===")
|
||||
|
||||
func before_each():
|
||||
main_mock = Node.new()
|
||||
add_child(main_mock)
|
||||
gridmap_mock = Node.new()
|
||||
gridmap_mock.name = "EnhancedGridMap"
|
||||
main_mock.add_child(gridmap_mock)
|
||||
|
||||
gauntlet_manager = GauntletManager.new()
|
||||
main_mock.add_child(gauntlet_manager)
|
||||
gauntlet_manager.initialize(main_mock, gridmap_mock)
|
||||
|
||||
func after_each():
|
||||
if main_mock:
|
||||
main_mock.queue_free()
|
||||
|
||||
func after_all():
|
||||
gut.p("=== Feature Tests Complete ===")
|
||||
|
||||
# =============================================================================
|
||||
# Growth Timing
|
||||
# =============================================================================
|
||||
|
||||
func test_growth_timer_starts_zero():
|
||||
assert_eq(gauntlet_manager.growth_timer, 0.0, "Growth timer starts at 0.0")
|
||||
|
||||
func test_growth_interval_default():
|
||||
assert_eq(gauntlet_manager.growth_interval, 3.0, "Growth interval defaults to 3.0s")
|
||||
|
||||
func test_telegraph_duration_default():
|
||||
assert_eq(gauntlet_manager.telegraph_duration, 1.0, "Telegraph duration defaults to 1.0s")
|
||||
|
||||
# =============================================================================
|
||||
# Phase Growth Config
|
||||
# =============================================================================
|
||||
|
||||
func test_phase_growth_config_has_three_phases():
|
||||
assert_eq(gauntlet_manager.phase_growth_config.size(), 3, "Three phase growth configs")
|
||||
|
||||
func test_phase1_cells_range():
|
||||
var cfg = gauntlet_manager.phase_growth_config[0]
|
||||
assert_eq(cfg["cells_min"], 4, "Phase 1 min 4 cells/tick")
|
||||
assert_eq(cfg["cells_max"], 6, "Phase 1 max 6 cells/tick")
|
||||
|
||||
func test_phase2_cells_range():
|
||||
var cfg = gauntlet_manager.phase_growth_config[1]
|
||||
assert_eq(cfg["cells_min"], 6, "Phase 2 min 6 cells/tick")
|
||||
assert_eq(cfg["cells_max"], 8, "Phase 2 max 8 cells/tick")
|
||||
|
||||
func test_phase3_cells_range():
|
||||
var cfg = gauntlet_manager.phase_growth_config[2]
|
||||
assert_eq(cfg["cells_min"], 8, "Phase 3 min 8 cells/tick")
|
||||
assert_eq(cfg["cells_max"], 10, "Phase 3 max 10 cells/tick")
|
||||
|
||||
func test_cells_this_tick_in_phase_range():
|
||||
for phase in range(3):
|
||||
gauntlet_manager.current_phase = phase
|
||||
var cfg = gauntlet_manager.phase_growth_config[phase]
|
||||
# Sample several times since the count is randomized.
|
||||
for _i in range(20):
|
||||
var n = gauntlet_manager._cells_this_tick()
|
||||
assert_true(n >= cfg["cells_min"] and n <= cfg["cells_max"],
|
||||
"Phase %d cells/tick %d within [%d,%d]" % [phase, n, cfg["cells_min"], cfg["cells_max"]])
|
||||
|
||||
# =============================================================================
|
||||
# Candidate Generation
|
||||
# =============================================================================
|
||||
|
||||
func test_candidates_are_all_safe_cells():
|
||||
gauntlet_manager.current_phase = 0
|
||||
var candidates = gauntlet_manager._generate_candidates()
|
||||
# Fresh arena: every playable cell is SAFE.
|
||||
assert_eq(candidates.size(), gauntlet_manager.playable_cell_count(),
|
||||
"All playable cells are candidates on a fresh arena")
|
||||
|
||||
func test_candidates_exclude_sticky():
|
||||
gauntlet_manager.sticky_cells[Vector2i(3, 3)] = true
|
||||
var candidates = gauntlet_manager._generate_candidates()
|
||||
var found := false
|
||||
for c in candidates:
|
||||
if c["pos"] == Vector2i(3, 3):
|
||||
found = true
|
||||
assert_false(found, "Sticky cells are excluded from candidates")
|
||||
|
||||
func test_candidates_exclude_cleansed():
|
||||
gauntlet_manager.mark_cleansed(Vector2i(4, 4))
|
||||
var candidates = gauntlet_manager._generate_candidates()
|
||||
var found := false
|
||||
for c in candidates:
|
||||
if c["pos"] == Vector2i(4, 4):
|
||||
found = true
|
||||
assert_false(found, "Cleansed cells are excluded from candidates (regrowth protection)")
|
||||
|
||||
func test_candidates_exclude_npc_and_boundary():
|
||||
var candidates = gauntlet_manager._generate_candidates()
|
||||
for c in candidates:
|
||||
var p = c["pos"]
|
||||
assert_false(gauntlet_manager._is_npc_zone(p), "No NPC-zone candidates")
|
||||
assert_false(gauntlet_manager._is_boundary(p), "No boundary candidates")
|
||||
|
||||
func test_candidates_have_scores():
|
||||
var candidates = gauntlet_manager._generate_candidates()
|
||||
assert_true(candidates.size() > 0, "Has candidates")
|
||||
assert_true(candidates[0].has("score"), "Candidate carries a score")
|
||||
|
||||
# =============================================================================
|
||||
# Weighted Selection
|
||||
# =============================================================================
|
||||
|
||||
func test_select_count_respected():
|
||||
var candidates = gauntlet_manager._generate_candidates()
|
||||
var picked = gauntlet_manager._select_cells_weighted(candidates, 5)
|
||||
assert_eq(picked.size(), 5, "Selects exactly the requested count")
|
||||
|
||||
func test_select_no_duplicates():
|
||||
var candidates = gauntlet_manager._generate_candidates()
|
||||
var picked = gauntlet_manager._select_cells_weighted(candidates, 10)
|
||||
var seen := {}
|
||||
for p in picked:
|
||||
assert_false(seen.has(p), "No duplicate selections")
|
||||
seen[p] = true
|
||||
|
||||
func test_select_capped_at_pool_size():
|
||||
var small = [{"pos": Vector2i(2, 2), "score": 1.0}, {"pos": Vector2i(2, 3), "score": 1.0}]
|
||||
var picked = gauntlet_manager._select_cells_weighted(small, 10)
|
||||
assert_eq(picked.size(), 2, "Cannot select more than pool size")
|
||||
|
||||
# =============================================================================
|
||||
# Scoring Helpers
|
||||
# =============================================================================
|
||||
|
||||
func test_layer_classification():
|
||||
assert_eq(gauntlet_manager._layer_of(Vector2i(9, 9)), "inner", "Center is inner")
|
||||
assert_eq(gauntlet_manager._layer_of(Vector2i(1, 1)), "outer", "Corner is outer")
|
||||
|
||||
func test_sticky_neighbor_count():
|
||||
gauntlet_manager.sticky_cells[Vector2i(5, 5)] = true
|
||||
gauntlet_manager.sticky_cells[Vector2i(5, 6)] = true
|
||||
assert_eq(gauntlet_manager._sticky_neighbor_count(Vector2i(6, 5)), 2,
|
||||
"Counts 8-directional sticky neighbors")
|
||||
|
||||
func test_chebyshev():
|
||||
assert_eq(gauntlet_manager._chebyshev(Vector2i(0, 0), Vector2i(3, 1)), 3, "Chebyshev distance")
|
||||
@@ -0,0 +1 @@
|
||||
uid://btbxtdhagjdba
|
||||
@@ -0,0 +1,119 @@
|
||||
extends GutTest
|
||||
|
||||
# =============================================================================
|
||||
# Test: Gauntlet Movement Buffer System (v2) [Gauntlet #083]
|
||||
# Hidden, decaying safe-corridor penalties layered onto candidate scoring.
|
||||
# Runs headless; elapsed_time = 0 so the final-30s window is inactive unless a
|
||||
# test sets elapsed_time directly.
|
||||
# =============================================================================
|
||||
|
||||
const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
|
||||
var manager
|
||||
var main_mock: Node
|
||||
var gridmap_mock: Node
|
||||
|
||||
func before_each():
|
||||
main_mock = Node.new()
|
||||
add_child(main_mock)
|
||||
gridmap_mock = Node.new()
|
||||
gridmap_mock.name = "EnhancedGridMap"
|
||||
main_mock.add_child(gridmap_mock)
|
||||
manager = GauntletManager.new()
|
||||
main_mock.add_child(manager)
|
||||
manager.initialize(main_mock, gridmap_mock)
|
||||
manager.current_phase = 0
|
||||
|
||||
func after_each():
|
||||
if main_mock:
|
||||
main_mock.queue_free()
|
||||
|
||||
# =============================================================================
|
||||
# Registration
|
||||
# =============================================================================
|
||||
|
||||
func test_register_buffer_sets_phase_base_penalty():
|
||||
manager._register_buffer(Vector2i(5, 5), 40.0)
|
||||
assert_true(manager.movement_buffers.has(Vector2i(5, 5)), "Buffer registered")
|
||||
assert_almost_eq(manager.movement_buffers[Vector2i(5, 5)]["penalty"], 40.0, 0.001, "Full penalty stored")
|
||||
|
||||
func test_register_buffer_keeps_strongest():
|
||||
manager._register_buffer(Vector2i(5, 5), 20.0)
|
||||
manager._register_buffer(Vector2i(5, 5), 40.0)
|
||||
assert_almost_eq(manager.movement_buffers[Vector2i(5, 5)]["penalty"], 40.0, 0.001, "Keeps the stronger penalty")
|
||||
manager._register_buffer(Vector2i(5, 5), 10.0)
|
||||
assert_almost_eq(manager.movement_buffers[Vector2i(5, 5)]["penalty"], 40.0, 0.001, "Weaker refresh does not lower it")
|
||||
|
||||
# =============================================================================
|
||||
# Penalty lookup (inside / adjacent / none / final-window)
|
||||
# =============================================================================
|
||||
|
||||
func test_buffer_penalty_inside_is_full_negative():
|
||||
manager._register_buffer(Vector2i(6, 6), 40.0)
|
||||
assert_almost_eq(manager._buffer_penalty_at(Vector2i(6, 6)), -40.0, 0.001, "Inside buffer = full negative")
|
||||
|
||||
func test_buffer_penalty_adjacent_is_half():
|
||||
manager._register_buffer(Vector2i(6, 6), 40.0)
|
||||
assert_almost_eq(manager._buffer_penalty_at(Vector2i(7, 6)), -20.0, 0.001, "Adjacent buffer = half penalty")
|
||||
|
||||
func test_buffer_penalty_far_is_zero():
|
||||
manager._register_buffer(Vector2i(6, 6), 40.0)
|
||||
assert_eq(manager._buffer_penalty_at(Vector2i(15, 15)), 0.0, "Far from buffer = 0")
|
||||
|
||||
func test_buffer_penalty_lifts_in_final_window():
|
||||
manager._register_buffer(Vector2i(6, 6), 40.0)
|
||||
manager.elapsed_time = manager.gauntlet_round_duration() - 5.0 # within final 30s
|
||||
assert_eq(manager._buffer_penalty_at(Vector2i(6, 6)), 0.0, "Final window lifts buffers")
|
||||
|
||||
func test_buffer_penalty_empty_is_zero():
|
||||
assert_eq(manager._buffer_penalty_at(Vector2i(6, 6)), 0.0, "No buffers = 0")
|
||||
|
||||
# =============================================================================
|
||||
# Time decay (−25% every 5s)
|
||||
# =============================================================================
|
||||
|
||||
func test_decay_reduces_penalty_after_interval():
|
||||
manager._register_buffer(Vector2i(5, 5), 40.0)
|
||||
manager._decay_movement_buffers(manager.BUFFER_DECAY_INTERVAL) # one full step
|
||||
assert_almost_eq(manager.movement_buffers[Vector2i(5, 5)]["penalty"], 30.0, 0.001, "−25% after one interval")
|
||||
|
||||
func test_decay_waits_for_full_interval():
|
||||
manager._register_buffer(Vector2i(5, 5), 40.0)
|
||||
manager._decay_movement_buffers(manager.BUFFER_DECAY_INTERVAL * 0.5) # not yet
|
||||
assert_almost_eq(manager.movement_buffers[Vector2i(5, 5)]["penalty"], 40.0, 0.001, "No decay before interval elapses")
|
||||
|
||||
func test_decay_prunes_faded_buffers():
|
||||
manager._register_buffer(Vector2i(5, 5), manager.BUFFER_MIN_PENALTY + 0.5)
|
||||
manager._decay_movement_buffers(manager.BUFFER_DECAY_INTERVAL)
|
||||
assert_false(manager.movement_buffers.has(Vector2i(5, 5)), "Faded buffer pruned below BUFFER_MIN_PENALTY")
|
||||
|
||||
# =============================================================================
|
||||
# Phase-change decay (−50%)
|
||||
# =============================================================================
|
||||
|
||||
func test_phase_change_halves_buffers():
|
||||
manager._register_buffer(Vector2i(5, 5), 40.0)
|
||||
manager._start_phase(manager.Phase.ROUTE_PRESSURE)
|
||||
assert_almost_eq(manager.movement_buffers[Vector2i(5, 5)]["penalty"], 20.0, 0.001, "Phase change halves penalty")
|
||||
|
||||
# =============================================================================
|
||||
# Scoring integration
|
||||
# =============================================================================
|
||||
|
||||
func test_score_movement_buffer_uses_detected_corridor():
|
||||
# With no players, the proximity floor is inert; a registered buffer still bites.
|
||||
manager._register_buffer(Vector2i(5, 5), 40.0)
|
||||
assert_almost_eq(manager._score_movement_buffer(Vector2i(5, 5)), -40.0, 0.001, "Score reflects buffer penalty")
|
||||
|
||||
func test_score_movement_buffer_zero_without_buffers_or_players():
|
||||
assert_eq(manager._score_movement_buffer(Vector2i(5, 5)), 0.0, "No buffers, no players = 0")
|
||||
|
||||
# =============================================================================
|
||||
# Scale helper
|
||||
# =============================================================================
|
||||
|
||||
func test_scale_all_buffers_prunes_and_scales():
|
||||
manager._register_buffer(Vector2i(1, 1), 40.0)
|
||||
manager._register_buffer(Vector2i(2, 2), manager.BUFFER_MIN_PENALTY + 0.1)
|
||||
manager._scale_all_buffers(0.5)
|
||||
assert_almost_eq(manager.movement_buffers[Vector2i(1, 1)]["penalty"], 20.0, 0.001, "Scaled by 0.5")
|
||||
assert_false(manager.movement_buffers.has(Vector2i(2, 2)), "Below-min entry pruned")
|
||||
@@ -0,0 +1 @@
|
||||
uid://4cttae74ja3t
|
||||
@@ -29,15 +29,15 @@ func test_all_modes_in_enum():
|
||||
# String Conversion Tests
|
||||
# =============================================================================
|
||||
|
||||
# Test 3: from_string recognizes "Candy Cannon Survival"
|
||||
# Test 3: from_string recognizes "Candy Pump Survival"
|
||||
func test_from_string_candy_cannon():
|
||||
var result = GameMode.from_string("Candy Cannon Survival")
|
||||
assert_eq(result, GameMode.Mode.GAUNTLET, "from_string should parse 'Candy Cannon Survival' as GAUNTLET")
|
||||
var result = GameMode.from_string("Candy Pump Survival")
|
||||
assert_eq(result, GameMode.Mode.GAUNTLET, "from_string should parse 'Candy Pump Survival' as GAUNTLET")
|
||||
|
||||
# Test 4: mode_to_string returns "Candy Cannon Survival" for GAUNTLET
|
||||
# Test 4: mode_to_string returns "Candy Pump Survival" for GAUNTLET
|
||||
func test_mode_to_string_gauntlet():
|
||||
var result = GameMode.mode_to_string(GameMode.Mode.GAUNTLET)
|
||||
assert_eq(result, "Candy Cannon Survival", "mode_to_string should return 'Candy Cannon Survival'")
|
||||
assert_eq(result, "Candy Pump Survival", "mode_to_string should return 'Candy Pump Survival'")
|
||||
|
||||
# Test 5: Round-trip conversion is lossless
|
||||
func test_round_trip_conversion():
|
||||
@@ -61,10 +61,10 @@ func test_unknown_string_defaults_freemode():
|
||||
# get_all_modes Tests
|
||||
# =============================================================================
|
||||
|
||||
# Test 8: get_all_modes includes "Candy Cannon Survival"
|
||||
# Test 8: get_all_modes includes "Candy Pump Survival"
|
||||
func test_get_all_modes_includes_gauntlet():
|
||||
var modes = GameMode.get_all_modes()
|
||||
assert_has(modes, "Candy Cannon Survival", "get_all_modes should include 'Candy Cannon Survival'")
|
||||
assert_has(modes, "Candy Pump Survival", "get_all_modes should include 'Candy Pump Survival'")
|
||||
|
||||
# Test 9: get_all_modes returns exactly 4 entries
|
||||
func test_get_all_modes_count():
|
||||
@@ -77,7 +77,7 @@ func test_get_all_modes_order():
|
||||
assert_eq(modes[0], "Freemode", "First mode should be Freemode")
|
||||
assert_eq(modes[1], "Stop n Go", "Second mode should be Stop n Go")
|
||||
assert_eq(modes[2], "Tekton Doors", "Third mode should be Tekton Doors")
|
||||
assert_eq(modes[3], "Candy Cannon Survival", "Fourth mode should be Candy Cannon Survival")
|
||||
assert_eq(modes[3], "Candy Pump Survival", "Fourth mode should be Candy Pump Survival")
|
||||
|
||||
# =============================================================================
|
||||
# is_restricted Tests
|
||||
@@ -103,10 +103,10 @@ func test_all_restricted_modes():
|
||||
# LobbyManager Integration Tests
|
||||
# =============================================================================
|
||||
|
||||
# Test 14: Lobby available_game_modes includes "Candy Cannon Survival"
|
||||
# Test 14: Lobby available_game_modes includes "Candy Pump Survival"
|
||||
func test_lobby_modes_includes_gauntlet():
|
||||
var modes = LobbyManager.available_game_modes
|
||||
assert_has(modes, "Candy Cannon Survival", "LobbyManager.available_game_modes should include 'Candy Cannon Survival'")
|
||||
assert_has(modes, "Candy Pump Survival", "LobbyManager.available_game_modes should include 'Candy Pump Survival'")
|
||||
|
||||
# Test 15: gauntlet_manager.gd script file exists
|
||||
func test_gauntlet_manager_script_exists():
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
extends GutTest
|
||||
|
||||
# =============================================================================
|
||||
# Test: Gauntlet Candidate Scoring System (v2) [Gauntlet #073]
|
||||
# Covers each score component, camping accumulation, and full-formula
|
||||
# composition. Runs headless (no multiplayer peer), so elapsed_time = 0 and
|
||||
# the final-30s window is inactive unless a test sets elapsed_time directly.
|
||||
# =============================================================================
|
||||
|
||||
const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
|
||||
var manager
|
||||
var main_mock: Node
|
||||
var gridmap_mock: Node
|
||||
|
||||
func before_each():
|
||||
main_mock = Node.new()
|
||||
add_child(main_mock)
|
||||
gridmap_mock = Node.new()
|
||||
gridmap_mock.name = "EnhancedGridMap"
|
||||
main_mock.add_child(gridmap_mock)
|
||||
manager = GauntletManager.new()
|
||||
main_mock.add_child(manager)
|
||||
manager.initialize(main_mock, gridmap_mock)
|
||||
manager.current_phase = 0
|
||||
|
||||
func after_each():
|
||||
if main_mock:
|
||||
main_mock.queue_free()
|
||||
|
||||
# =============================================================================
|
||||
# LayerPriority
|
||||
# =============================================================================
|
||||
|
||||
func test_layer_priority_matches_phase_weights():
|
||||
manager.current_phase = 0
|
||||
# Outer ring cell (corner-ish) gets the phase-0 outer weight (+60).
|
||||
assert_eq(manager._score_layer_priority(Vector2i(2, 2)), 60.0, "Phase 0 outer = +60")
|
||||
# Inner cell near center gets phase-0 inner weight (-40).
|
||||
assert_eq(manager._score_layer_priority(Vector2i(9, 8)), -40.0, "Phase 0 inner = -40")
|
||||
|
||||
func test_layer_priority_phase3_inner():
|
||||
manager.current_phase = 2
|
||||
assert_eq(manager._score_layer_priority(Vector2i(9, 8)), 60.0, "Phase 2 inner = +60")
|
||||
|
||||
# =============================================================================
|
||||
# StickyNeighbor
|
||||
# =============================================================================
|
||||
|
||||
func test_sticky_neighbor_score_scales():
|
||||
assert_eq(manager._score_sticky_neighbor(Vector2i(5, 5)), 0.0, "No neighbors = 0")
|
||||
manager.sticky_cells[Vector2i(5, 6)] = true
|
||||
assert_eq(manager._score_sticky_neighbor(Vector2i(5, 5)), 8.0, "One neighbor = +8")
|
||||
|
||||
func test_sticky_neighbor_score_capped():
|
||||
# Surround (5,5) on all 8 sides → 8 * 8 = 64, capped at 64.
|
||||
for dx in range(-1, 2):
|
||||
for dz in range(-1, 2):
|
||||
if dx == 0 and dz == 0:
|
||||
continue
|
||||
manager.sticky_cells[Vector2i(5 + dx, 5 + dz)] = true
|
||||
assert_eq(manager._score_sticky_neighbor(Vector2i(5, 5)), 64.0, "Capped at +64")
|
||||
|
||||
# =============================================================================
|
||||
# InwardPressure
|
||||
# =============================================================================
|
||||
|
||||
func test_inward_pressure_higher_near_center():
|
||||
manager.current_phase = 2
|
||||
var near = manager._score_inward_pressure(Vector2i(8, 8)) # close to center
|
||||
var far = manager._score_inward_pressure(Vector2i(1, 1)) # far corner
|
||||
assert_true(near > far, "Inward pressure stronger near center")
|
||||
|
||||
func test_inward_pressure_phase_scaling():
|
||||
# Same cell, later phase => higher inward pressure ceiling.
|
||||
var pos := Vector2i(8, 8)
|
||||
manager.current_phase = 0
|
||||
var p0 = manager._score_inward_pressure(pos)
|
||||
manager.current_phase = 2
|
||||
var p2 = manager._score_inward_pressure(pos)
|
||||
assert_true(p2 > p0, "Later phase pushes inward harder")
|
||||
|
||||
# =============================================================================
|
||||
# PlayerPressure
|
||||
# =============================================================================
|
||||
|
||||
func test_player_pressure_ring():
|
||||
# 3 cells from a player → +20.
|
||||
var players = [Vector2i(5, 5)]
|
||||
assert_eq(manager._score_player_pressure(Vector2i(8, 5), players), 20.0, "2-4 cells away = +20")
|
||||
|
||||
func test_player_pressure_under_player_penalized():
|
||||
var players = [Vector2i(5, 5)]
|
||||
# elapsed_time 0, round 180 → not final window → directly under = -50.
|
||||
assert_eq(manager._score_player_pressure(Vector2i(5, 5), players), -50.0, "Under player (early) = -50")
|
||||
|
||||
func test_player_pressure_under_player_final_window():
|
||||
manager.elapsed_time = manager.gauntlet_round_duration() - 5.0 # within final 30s
|
||||
var players = [Vector2i(5, 5)]
|
||||
assert_eq(manager._score_player_pressure(Vector2i(5, 5), players), 10.0, "Under player (final) = +10")
|
||||
|
||||
func test_player_pressure_no_players():
|
||||
assert_eq(manager._score_player_pressure(Vector2i(5, 5), []), 0.0, "No players = 0")
|
||||
|
||||
# =============================================================================
|
||||
# ClusterGrowth
|
||||
# =============================================================================
|
||||
|
||||
func test_cluster_growth_none():
|
||||
assert_eq(manager._score_cluster_growth(Vector2i(5, 5)), 0.0, "No sticky neighbors = 0")
|
||||
|
||||
func test_cluster_growth_expand():
|
||||
manager.sticky_cells[Vector2i(5, 6)] = true
|
||||
assert_eq(manager._score_cluster_growth(Vector2i(5, 5)), 15.0, "Expanding cluster = +15")
|
||||
|
||||
func test_cluster_growth_connect():
|
||||
manager.sticky_cells[Vector2i(4, 5)] = true
|
||||
manager.sticky_cells[Vector2i(6, 5)] = true
|
||||
manager.sticky_cells[Vector2i(5, 6)] = true
|
||||
assert_eq(manager._score_cluster_growth(Vector2i(5, 5)), 25.0, "Connecting clusters = +25")
|
||||
|
||||
# =============================================================================
|
||||
# CampingPressure
|
||||
# =============================================================================
|
||||
|
||||
func test_camp_region_grouping():
|
||||
assert_eq(manager._region_of(Vector2i(0, 0)), Vector2i(0, 0), "Cells 0-3 → region 0")
|
||||
assert_eq(manager._region_of(Vector2i(5, 7)), Vector2i(1, 1), "Cells 4-7 → region 1")
|
||||
|
||||
func test_camping_pressure_thresholds():
|
||||
var region: Vector2i = manager._region_of(Vector2i(8, 8))
|
||||
manager._camp_tracking[1] = {"region": region, "time": 6.0}
|
||||
assert_eq(manager._score_camping_pressure(Vector2i(8, 8)), 20.0, ">5s = +20")
|
||||
manager._camp_tracking[1]["time"] = 9.0
|
||||
assert_eq(manager._score_camping_pressure(Vector2i(8, 8)), 40.0, ">8s = +40")
|
||||
manager._camp_tracking[1]["time"] = 11.0
|
||||
assert_eq(manager._score_camping_pressure(Vector2i(8, 8)), 60.0, ">10s = +60")
|
||||
|
||||
func test_camping_pressure_none():
|
||||
assert_eq(manager._score_camping_pressure(Vector2i(8, 8)), 0.0, "No camping = 0")
|
||||
|
||||
# =============================================================================
|
||||
# Repetition
|
||||
# =============================================================================
|
||||
|
||||
func test_repetition_penalty():
|
||||
manager._last_tick_cells = [Vector2i(5, 5)]
|
||||
assert_eq(manager._score_repetition(Vector2i(5, 6)), -30.0, "Adjacent to last tick = -30")
|
||||
assert_eq(manager._score_repetition(Vector2i(15, 15)), 0.0, "Far from last tick = 0")
|
||||
|
||||
# =============================================================================
|
||||
# PathSafety (soft penalty)
|
||||
# =============================================================================
|
||||
|
||||
func test_path_safety_no_players_no_penalty():
|
||||
assert_eq(manager._score_path_safety(Vector2i(5, 5)), 0.0, "No players = no penalty")
|
||||
|
||||
# =============================================================================
|
||||
# Camp tracking accumulation
|
||||
# =============================================================================
|
||||
|
||||
func test_camp_time_for_region_picks_max():
|
||||
var region := Vector2i(1, 1)
|
||||
manager._camp_tracking[1] = {"region": region, "time": 3.0}
|
||||
manager._camp_tracking[2] = {"region": region, "time": 7.0}
|
||||
assert_almost_eq(manager._camp_time_for_region(region), 7.0, 0.001, "Longest camp time wins")
|
||||
|
||||
# =============================================================================
|
||||
# Full formula composition
|
||||
# =============================================================================
|
||||
|
||||
func test_full_score_runs_and_is_finite():
|
||||
var s = manager._calculate_candidate_score(Vector2i(5, 5), [])
|
||||
assert_true(is_finite(s), "Full score is a finite number")
|
||||
|
||||
func test_full_score_rewards_sticky_cluster():
|
||||
# A cell hugging an existing cluster should generally beat an isolated one.
|
||||
# Average several samples to wash out RandomNoise (±20).
|
||||
manager.sticky_cells[Vector2i(5, 6)] = true
|
||||
manager.sticky_cells[Vector2i(6, 5)] = true
|
||||
var clustered := 0.0
|
||||
var isolated := 0.0
|
||||
for _i in range(40):
|
||||
clustered += manager._calculate_candidate_score(Vector2i(5, 5), [])
|
||||
isolated += manager._calculate_candidate_score(Vector2i(15, 15), [])
|
||||
assert_true(clustered > isolated, "Clustered cell scores higher on average")
|
||||
@@ -0,0 +1 @@
|
||||
uid://tugcu571care
|
||||
@@ -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")
|
||||
@@ -0,0 +1 @@
|
||||
uid://csco4t66gq5et
|
||||
@@ -126,13 +126,13 @@ func test_initial_phase_is_open_arena():
|
||||
assert_eq(manager.current_phase, GauntletManager.Phase.OPEN_ARENA, "Should start in Open Arena")
|
||||
|
||||
func test_phase_to_string_open_arena():
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.OPEN_ARENA), "Open Arena")
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.OPEN_ARENA), "Outer Pressure")
|
||||
|
||||
func test_phase_to_string_route_pressure():
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.ROUTE_PRESSURE), "Route Pressure")
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.ROUTE_PRESSURE), "Middle Pressure")
|
||||
|
||||
func test_phase_to_string_survival():
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.SURVIVAL_ENDGAME), "Survival!")
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.SURVIVAL_ENDGAME), "Inner Survival")
|
||||
|
||||
# =============================================================================
|
||||
# Match Timer Integration
|
||||
|
||||
Reference in New Issue
Block a user