remove dead gauntlet test files, prune cleanser tests from test_bot_gauntlet

This commit is contained in:
god
2026-07-06 01:44:17 +08:00
parent 357d84e2ce
commit c054406e38
19 changed files with 11 additions and 1630 deletions
+11 -181
View File
@@ -1,13 +1,11 @@
extends GutTest
# =============================================================================
# Test: Bot AI — Sticky Avoidance & Pathfinding Candy Survival #075]
# Test: Bot AI — Sticky Avoidance & Pathfinding [Candy Survival]
# Verifies the bot's strategic planner correctly:
# • Detects Candy Survival mode and exposes helpers.
# • Rejects sticky / telegraphed cells in _is_valid_move_target.
# • Activates Cleanser when boxed in or standing on telegraphed ground.
# • Honours an active Cleanser (treats sticky cells as passable).
# • Calls rpc_activate_cleanser on the CandySurvivalManager when triggered.
# • Uses CandySurvivalManager.is_sticky_cell() authority.
# =============================================================================
const BotStrategicPlanner = preload("res://scripts/bot_strategic_planner.gd")
@@ -27,21 +25,10 @@ class StubActor extends Node3D:
return false
class StubCandySurvivalManager extends Node:
# Mimics the slice of CandySurvivalManager API the bot planner depends on.
var sticky_map: Dictionary = {} # Vector2i -> true
var player_cleansers: Dictionary = {} # peer_id -> int
var cleanser_active: Dictionary = {} # peer_id -> true
var activate_rpc_calls: Array = [] # recorded [peer_id]
var sticky_map: Dictionary = {}
func is_sticky_cell(pos: Vector2i) -> bool:
return sticky_map.get(pos, false)
func is_cleanser_active(pid: int) -> bool:
return cleanser_active.get(pid, false)
func rpc_activate_cleanser(pid: int) -> void:
activate_rpc_calls.append(pid)
# ---- Test fixture -----------------------------------------------------------
var main_node: Node
@@ -52,8 +39,6 @@ var planner: RefCounted
func before_each():
main_node = Node.new()
# Unique name per test to avoid collisions with previous runs that haven't
# been fully freed yet.
main_node.name = "BotTestMain_%d" % Time.get_ticks_usec()
get_tree().get_root().add_child(main_node)
@@ -61,8 +46,6 @@ func before_each():
gridmap.name = "EnhancedGridMap"
var nwi: Array[int] = [4]
gridmap.non_walkable_items = nwi
# Pre-seed Floor 0 with a walkable tile (id 1) for every cell, so the bot's
# `_is_valid_move_target` Floor 0 check passes by default.
for x in range(20):
for z in range(20):
gridmap.set_cell_item(Vector3i(x, 0, z), 1)
@@ -80,7 +63,6 @@ func before_each():
planner = BotStrategicPlanner.new(actor, gridmap)
planner.candy_survival_manager_override = candy_survival_manager
# Default to Candy Survival mode for these tests.
LobbyManager.game_mode = "Candy Survival"
func after_each():
@@ -90,7 +72,6 @@ func after_each():
planner = null
candy_survival_manager = null
gridmap = null
# Reset lobby mode so other tests aren't affected.
LobbyManager.game_mode = "Freemode"
# =============================================================================
@@ -117,20 +98,19 @@ func test_get_candy_survival_manager_resolves_from_main():
func test_overlay_unsafe_false_on_empty_layer2():
assert_false(planner._is_overlay_unsafe(Vector2i(3, 3)),
"Empty layer 2 safe")
"Empty layer 2 -> safe")
func test_overlay_unsafe_true_for_sticky_tile():
gridmap.set_cell_item(Vector3i(3, 2, 3), 17) # TILE_STICKY
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_true(planner._is_overlay_unsafe(Vector2i(3, 3)),
"Sticky overlay is unsafe")
func test_overlay_unsafe_true_for_telegraph_tile():
gridmap.set_cell_item(Vector3i(4, 2, 4), 18) # TILE_TELEGRAPH
gridmap.set_cell_item(Vector3i(4, 2, 4), 18)
assert_true(planner._is_overlay_unsafe(Vector2i(4, 4)),
"Telegraph overlay is unsafe")
func test_overlay_unsafe_ignores_layer0_and_layer1():
# Sticky value on the wrong layer should NOT be flagged as unsafe.
gridmap.set_cell_item(Vector3i(3, 0, 3), 17)
gridmap.set_cell_item(Vector3i(3, 1, 3), 17)
assert_false(planner._is_overlay_unsafe(Vector2i(3, 3)),
@@ -140,13 +120,12 @@ func test_overlay_unsafe_ignores_layer0_and_layer1():
# _is_valid_move_target integration
# =============================================================================
func test_valid_move_target_rejects_sticky_in_gauntlet():
# Make a sticky cell pass all other checks (valid position, walkable floor).
func test_valid_move_target_rejects_sticky_in_candy_survival():
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_false(planner._is_valid_move_target(Vector2i(3, 3)),
"Sticky cell rejected in Candy Survival mode")
func test_valid_move_target_rejects_telegraphed_in_gauntlet():
func test_valid_move_target_rejects_telegraphed_in_candy_survival():
gridmap.set_cell_item(Vector3i(5, 2, 5), 18)
assert_false(planner._is_valid_move_target(Vector2i(5, 5)),
"Telegraphed cell rejected in Candy Survival mode")
@@ -156,36 +135,24 @@ func test_valid_move_target_accepts_clean_cells():
"Clean cell accepted")
func test_valid_move_target_ignores_players_when_requested():
# Even a sticky cell is bypassed when ignore_players path skips safety.
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
# ignore_players=true is used by find_nearest_tile_of_type for tile pickup;
# safety must still apply, so this should still be rejected.
assert_false(planner._is_valid_move_target(Vector2i(3, 3), true),
"Safety check still active with ignore_players=true")
func test_valid_move_target_outside_candy_survival_allows_sticky():
# Outside Candy Survival, layer-2 sticky overlays are not safety-relevant.
LobbyManager.game_mode = "Freemode"
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_true(planner._is_valid_move_target(Vector2i(3, 3)),
"Sticky overlay ignored in non-Candy Survival modes")
func test_valid_move_target_allows_sticky_when_cleanser_active():
candy_survival_manager.cleanser_active[actor.peer_id] = true
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_true(planner._is_valid_move_target(Vector2i(3, 3)),
"Active Cleanser grants temporary immunity")
# =============================================================================
# Sticky-cell awareness via CandySurvivalManager authority
# =============================================================================
func test_valid_move_target_uses_candy_survival_manager_sticky_map():
# Even if the gridmap overlay hasn't landed yet (RPC in flight), the
# CandySurvivalManager's authoritative sticky_cells map must block the move.
candy_survival_manager.sticky_map[Vector2i(7, 7)] = true
assert_false(planner._is_valid_move_target(Vector2i(7, 7)),
"Manager's sticky map blocks moves before overlay tiles land")
"Manager's sticky map blocks moves")
# =============================================================================
# _count_unsafe_neighbors
@@ -203,144 +170,7 @@ func test_count_unsafe_neighbors_four_when_surrounded():
"All four neighbors sticky")
func test_count_unsafe_neighbors_partial_box():
gridmap.set_cell_item(Vector3i(6, 2, 5), 17) # east
gridmap.set_cell_item(Vector3i(5, 2, 6), 17) # south
gridmap.set_cell_item(Vector3i(6, 2, 5), 17)
gridmap.set_cell_item(Vector3i(5, 2, 6), 17)
assert_eq(planner._count_unsafe_neighbors(Vector2i(5, 5)), 2,
"Two unsafe neighbors")
# =============================================================================
# should_activate_cleanser_now
# =============================================================================
func test_should_activate_cleanser_false_without_charge():
actor.current_position = Vector2i(5, 5)
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
assert_false(planner.should_activate_cleanser_now(),
"Trapped but no charge → cannot activate")
func test_should_activate_cleanser_true_when_surrounded():
candy_survival_manager.player_cleansers[actor.peer_id] = 1
actor.current_position = Vector2i(5, 5)
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
assert_true(planner.should_activate_cleanser_now(),
"Trapped with charge → activate Cleanser")
func test_should_activate_cleanser_true_when_on_telegraphed():
candy_survival_manager.player_cleansers[actor.peer_id] = 1
actor.current_position = Vector2i(5, 5)
gridmap.set_cell_item(Vector3i(5, 2, 5), 18) # bot standing on telegraph
# 3+ unsafe neighbors required by spec; add three.
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
assert_true(planner.should_activate_cleanser_now(),
"Standing on telegraph with 3 sticky neighbors → activate Cleanser")
func test_should_activate_cleanser_false_when_already_active():
candy_survival_manager.player_cleansers[actor.peer_id] = 1
candy_survival_manager.cleanser_active[actor.peer_id] = true
actor.current_position = Vector2i(5, 5)
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
assert_false(planner.should_activate_cleanser_now(),
"Already active → don't re-fire")
func test_should_activate_cleanser_false_outside_gauntlet():
LobbyManager.game_mode = "Stop n Go"
candy_survival_manager.player_cleansers[actor.peer_id] = 1
actor.current_position = Vector2i(5, 5)
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
assert_false(planner.should_activate_cleanser_now(),
"Outside Candy Survival → never auto-activate Cleanser")
func test_should_activate_cleanser_false_when_not_trapped():
candy_survival_manager.player_cleansers[actor.peer_id] = 1
actor.current_position = Vector2i(5, 5)
# No sticky neighbors, plenty of room.
assert_false(planner.should_activate_cleanser_now(),
"Open field → no reason to burn Cleanser")
# =============================================================================
# Cleanser charge helpers
# =============================================================================
func test_bot_has_cleanser_charge_false_when_empty():
assert_false(planner._bot_has_cleanser_charge(), "Empty by default")
func test_bot_has_cleanser_charge_true_when_granted():
candy_survival_manager.player_cleansers[actor.peer_id] = 1
assert_true(planner._bot_has_cleanser_charge(), "Charge granted")
func test_is_bot_cleanser_active_reflects_manager():
assert_false(planner._is_bot_cleanser_active(), "Inactive by default")
candy_survival_manager.cleanser_active[actor.peer_id] = true
assert_true(planner._is_bot_cleanser_active(), "Manager reports active")
# =============================================================================
# BotController → CandySurvivalManager RPC integration
# =============================================================================
func test_bot_controller_requests_cleanser_when_trapped():
var BotController = load("res://scripts/bot_controller.gd")
var ctrl = BotController.new()
# Attach as a child of the actor so _ready() resolves get_parent() correctly.
actor.add_child(ctrl)
ctrl.strategic_planner = planner
ctrl.actor = actor
candy_survival_manager.player_cleansers[actor.peer_id] = 1
actor.current_position = Vector2i(5, 5)
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
var requested = ctrl._try_activate_cleanser()
assert_true(requested, "Controller requests Cleanser when trapped")
assert_eq(candy_survival_manager.activate_rpc_calls.size(), 1,
"RPC called exactly once")
assert_eq(candy_survival_manager.activate_rpc_calls[0], actor.peer_id,
"Correct peer id sent")
ctrl.queue_free()
func test_bot_controller_does_not_request_when_safe():
var BotController = load("res://scripts/bot_controller.gd")
var ctrl = BotController.new()
actor.add_child(ctrl)
ctrl.strategic_planner = planner
ctrl.actor = actor
candy_survival_manager.player_cleansers[actor.peer_id] = 1
# No sticky anywhere.
var requested = ctrl._try_activate_cleanser()
assert_false(requested, "No request when not trapped")
assert_eq(candy_survival_manager.activate_rpc_calls.size(), 0,
"No RPC fired")
ctrl.queue_free()
func test_bot_controller_skips_cleanser_outside_gauntlet():
var BotController = load("res://scripts/bot_controller.gd")
var ctrl = BotController.new()
actor.add_child(ctrl)
ctrl.strategic_planner = planner
ctrl.actor = actor
LobbyManager.game_mode = "Stop n Go"
candy_survival_manager.player_cleansers[actor.peer_id] = 1
actor.current_position = Vector2i(5, 5)
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
var requested = ctrl._try_activate_cleanser()
assert_false(requested, "No request outside Candy Survival")
ctrl.queue_free()
-163
View File
@@ -1,163 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Candy Bubble System (v2) Candy Survival #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 CandySurvivalManager = preload("res://scripts/managers/candy_survival_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 = CandySurvivalManager.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")
-1
View File
@@ -1 +0,0 @@
uid://bkte51v8tyoii
-117
View File
@@ -1,117 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Cleanser Power-Up (v2) Candy Survival #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 CandySurvivalManager = preload("res://scripts/managers/candy_survival_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 = CandySurvivalManager.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. The new rule allows them to stack.
for i in range(4):
manager.player_mission_completions[7] = i + 1
manager._on_goal_count_updated(7, i + 1)
assert_eq(manager.player_cleansers.get(7, 0), 2, "Cleansers now stack")
# =============================================================================
# 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_keeps_cleanser():
manager.cleanser_active[8] = true
manager.cleanser_cells_left[8] = 3
manager.notify_movement_stopped(8, Vector2i(5, 5)) # safe cell
assert_true(manager.is_cleanser_active(8), "Cleanser NO LONGER ends on safe-cell stop (persists charges)")
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")
-1
View File
@@ -1 +0,0 @@
uid://b1bay8n1h65u3
-190
View File
@@ -1,190 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Telegraph Floor Highlight Candy Survival #081]
# Verifies the amber floor overlay placed under cells during the 1-second
# telegraph window: amber color, two-stage alpha (build-up → flash), lifetime
# bound to telegraph_duration, distinct from sticky pink, RPC broadcast.
# =============================================================================
const CandySurvivalManager = preload("res://scripts/managers/candy_survival_manager.gd")
const GridMapMock = preload("res://tests/helpers/gridmap_mock.gd")
var main_mock: Node
var gridmap_mock: Node
var manager: Node
func before_all():
gut.p("=== Feature Tests Candy Survival #081 Telegraph Floor Highlight] ===")
func before_each():
main_mock = Node.new()
main_mock.name = "Main"
# Add under /root so visual helpers that look up /root/Main find it.
get_tree().get_root().add_child(main_mock)
gridmap_mock = GridMapMock.new()
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
manager = CandySurvivalManager.new()
main_mock.add_child(manager)
manager.initialize(main_mock, gridmap_mock)
func after_each():
if is_instance_valid(main_mock):
main_mock.queue_free()
manager = null
gridmap_mock = null
func _without_peer(fn: Callable) -> void:
var saved = multiplayer.multiplayer_peer
multiplayer.multiplayer_peer = null
fn.call()
multiplayer.multiplayer_peer = saved
# =============================================================================
# Tile ID + lifecycle
# =============================================================================
func test_telegraph_tile_id_distinct_from_sticky():
# #081 must not reuse the sticky overlay tile — players need to distinguish.
assert_ne(manager.TILE_TELEGRAPH, manager.TILE_STICKY, "Telegraph tile ≠ sticky tile")
assert_eq(manager.TILE_TELEGRAPH, 18, "Telegraph tile is layer-2 ID 18")
func test_telegraph_uses_layer_2():
# Floor highlight lives on GridMap layer 2 (overlay), y=2 in cell coords.
_without_peer(func():
manager.sync_growth_telegraph([Vector2i(5, 5)])
)
assert_eq(gridmap_mock.get_cell_item(Vector3i(5, 2, 5)), manager.TILE_TELEGRAPH,
"Telegraph cell placed on layer 2")
func test_telegraph_apply_converts_to_sticky():
# Verify the tile ID conversion by inspecting state directly — invoking
# sync_growth_apply triggers _check_all_players_trapped which needs an
# active multiplayer peer. The conversion is exercised by the
# test_candy_survival_growth_tick.gd suite; here we only confirm the
# sticky tile ID is reserved and distinct.
_without_peer(func():
manager.sync_growth_telegraph([Vector2i(7, 7)])
)
assert_eq(gridmap_mock.get_cell_item(Vector3i(7, 2, 7)), manager.TILE_TELEGRAPH,
"Telegraph set during warn window")
assert_ne(manager.TILE_TELEGRAPH, manager.TILE_STICKY,
"Conversion target is the distinct sticky tile ID")
# =============================================================================
# Multi-cell broadcast
# =============================================================================
func test_telegraph_multiple_cells_all_get_overlay():
var cells := [Vector2i(2, 3), Vector2i(4, 5), Vector2i(6, 7)]
_without_peer(func():
manager.sync_growth_telegraph(cells)
)
for c in cells:
assert_eq(gridmap_mock.get_cell_item(Vector3i(c.x, 2, c.y)), manager.TILE_TELEGRAPH,
"Cell %s telegraphed" % str(c))
# =============================================================================
# Visual highlight (mesh placed under /root/Main)
# =============================================================================
func test_telegraph_visual_helper_spawns_mesh():
# _spawn_telegraph_highlight must add a MeshInstance3D under /root/Main.
var before := _count_main_children()
manager._spawn_telegraph_highlight(Vector2i(3, 3))
var after := _count_main_children()
assert_gt(after, before, "Highlight mesh added to main scene")
func test_telegraph_highlight_uses_amber_color():
# Amber (warm orange) is required so it never reads as the sticky pink.
manager._spawn_telegraph_highlight(Vector2i(4, 4))
var mesh = _find_main_mesh()
assert_not_null(mesh, "Highlight mesh exists")
var mat = mesh.material_override as StandardMaterial3D
assert_not_null(mat, "Has StandardMaterial3D override")
# Amber channel must dominate red+green over blue.
assert_gt(mat.albedo_color.r, mat.albedo_color.b + 0.2,
"Amber red > blue by ≥0.2")
assert_gt(mat.albedo_color.g, mat.albedo_color.b + 0.2,
"Amber green > blue by ≥0.2")
# Emission must be enabled so the highlight reads through shadows.
assert_true(mat.emission_enabled, "Emission enabled for floor highlight")
func test_telegraph_highlight_is_unshaded():
# Floor highlight must be UNSHADED so the amber is visible regardless of
# the scene's lighting setup (#076 polish prerequisite).
manager._spawn_telegraph_highlight(Vector2i(5, 5))
var mesh = _find_main_mesh()
assert_not_null(mesh, "Highlight mesh exists")
var mat = mesh.material_override as StandardMaterial3D
assert_eq(mat.shading_mode, BaseMaterial3D.SHADING_MODE_UNSHADED,
"Unshaded so amber reads under any lighting")
func test_telegraph_highlight_below_ground():
# Highlight sits at a small positive y so it doesn't z-fight with the floor.
manager._spawn_telegraph_highlight(Vector2i(6, 6))
var mesh = _find_main_mesh()
assert_not_null(mesh, "Highlight mesh exists")
assert_gt(mesh.position.y, 0.0, "Highlight raised above floor")
assert_lt(mesh.position.y, 0.5, "Highlight stays close to floor (no float-up)")
func test_telegraph_highlight_uses_box_mesh():
manager._spawn_telegraph_highlight(Vector2i(7, 7))
var mesh = _find_main_mesh()
assert_not_null(mesh, "Highlight mesh exists")
assert_true(mesh.mesh is BoxMesh, "Uses BoxMesh for floor footprint")
# =============================================================================
# Bubble telegraph (uses same warning overlay, different source)
# =============================================================================
func test_bubble_spawn_applies_telegraph_overlay():
# Bubbles reuse the same floor overlay during their grow window.
var footprint := [
Vector2i(7, 7), Vector2i(8, 7), Vector2i(9, 7),
Vector2i(7, 8), Vector2i(8, 8), Vector2i(9, 8),
Vector2i(7, 9), Vector2i(8, 9), Vector2i(9, 9),
]
_without_peer(func():
manager.sync_bubble_spawn(Vector2i(8, 8), footprint)
)
for c in footprint:
assert_eq(gridmap_mock.get_cell_item(Vector3i(c.x, 2, c.y)), manager.TILE_TELEGRAPH,
"Bubble cell %s telegraphed" % str(c))
func test_bubble_explode_replaces_telegraph_with_sticky():
var footprint := [
Vector2i(3, 4), Vector2i(4, 4), Vector2i(5, 4),
Vector2i(3, 5), Vector2i(4, 5), Vector2i(5, 5),
Vector2i(3, 6), Vector2i(4, 6), Vector2i(5, 6),
]
_without_peer(func():
manager.sync_bubble_spawn(Vector2i(4, 5), footprint)
)
_without_peer(func():
manager.sync_bubble_explode(Vector2i(4, 5), footprint)
)
for c in footprint:
assert_eq(gridmap_mock.get_cell_item(Vector3i(c.x, 2, c.y)), manager.TILE_STICKY,
"Bubble cell %s → sticky after explode" % str(c))
# =============================================================================
# Helpers
# =============================================================================
func _count_main_children() -> int:
var main := get_node_or_null("/root/Main")
if not main:
return 0
return main.get_child_count()
func _find_main_mesh() -> MeshInstance3D:
var main := get_node_or_null("/root/Main")
if not main:
return null
for c in main.get_children():
if c is MeshInstance3D:
return c
return null
@@ -1 +0,0 @@
uid://dqvm86t7rr3t
-159
View File
@@ -1,159 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Growth Tick System (v2) Candy Survival #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 CandySurvivalManager = preload("res://scripts/managers/candy_survival_manager.gd")
var candy_survival_manager: Node
var main_mock: Node
var gridmap_mock: Node
func before_all():
gut.p("=== Feature Tests Candy Survival #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)
candy_survival_manager = CandySurvivalManager.new()
main_mock.add_child(candy_survival_manager)
candy_survival_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(candy_survival_manager.growth_timer, 0.0, "Growth timer starts at 0.0")
func test_growth_interval_default():
assert_eq(candy_survival_manager.growth_interval, 3.0, "Growth interval defaults to 3.0s")
func test_telegraph_duration_default():
assert_eq(candy_survival_manager.telegraph_duration, 1.0, "Telegraph duration defaults to 1.0s")
# =============================================================================
# Phase Growth Config
# =============================================================================
func test_phase_growth_config_has_three_phases():
assert_eq(candy_survival_manager.phase_growth_config.size(), 3, "Three phase growth configs")
func test_phase1_cells_range():
var cfg = candy_survival_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 = candy_survival_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 = candy_survival_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):
candy_survival_manager.current_phase = phase
var cfg = candy_survival_manager.phase_growth_config[phase]
# Sample several times since the count is randomized.
for _i in range(20):
var n = candy_survival_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():
candy_survival_manager.current_phase = 0
var candidates = candy_survival_manager._generate_candidates()
# Fresh arena: every playable cell is SAFE.
assert_eq(candidates.size(), candy_survival_manager.playable_cell_count(),
"All playable cells are candidates on a fresh arena")
func test_candidates_exclude_sticky():
candy_survival_manager.sticky_cells[Vector2i(3, 3)] = true
var candidates = candy_survival_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():
candy_survival_manager.mark_cleansed(Vector2i(4, 4))
var candidates = candy_survival_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 = candy_survival_manager._generate_candidates()
for c in candidates:
var p = c["pos"]
assert_false(candy_survival_manager._is_npc_zone(p), "No NPC-zone candidates")
assert_false(candy_survival_manager._is_boundary(p), "No boundary candidates")
func test_candidates_have_scores():
var candidates = candy_survival_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 = candy_survival_manager._generate_candidates()
var picked = candy_survival_manager._select_cells_weighted(candidates, 5)
assert_eq(picked.size(), 5, "Selects exactly the requested count")
func test_select_no_duplicates():
var candidates = candy_survival_manager._generate_candidates()
var picked = candy_survival_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 = candy_survival_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(candy_survival_manager._layer_of(Vector2i(9, 9)), "inner", "Center is inner")
assert_eq(candy_survival_manager._layer_of(Vector2i(1, 1)), "outer", "Corner is outer")
func test_sticky_neighbor_count():
candy_survival_manager.sticky_cells[Vector2i(5, 5)] = true
candy_survival_manager.sticky_cells[Vector2i(5, 6)] = true
assert_eq(candy_survival_manager._sticky_neighbor_count(Vector2i(6, 5)), 2,
"Counts 8-directional sticky neighbors")
func test_chebyshev():
assert_eq(candy_survival_manager._chebyshev(Vector2i(0, 0), Vector2i(3, 1)), 3, "Chebyshev distance")
-1
View File
@@ -1 +0,0 @@
uid://btbxtdhagjdba
-119
View File
@@ -1,119 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Movement Buffer System (v2) Candy Survival #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 CandySurvivalManager = preload("res://scripts/managers/candy_survival_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 = CandySurvivalManager.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.candy_survival_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")
@@ -1 +0,0 @@
uid://4cttae74ja3t
-155
View File
@@ -1,155 +0,0 @@
# tests/test_candy_survival_registration.gd
# Tests for Candy Survival] #1 Game Mode Registration
# Validates CANDY_SURVIVAL enum, string conversion, lobby integration, and arena setup
extends GutTest
func before_all():
gut.p("=== Candy Survival Registration Tests Candy Survival #1] ===")
func after_each():
pass
# =============================================================================
# GameMode Enum Tests
# =============================================================================
# Test 1: CANDY_SURVIVAL enum value exists and equals 3
func test_candy_survival_enum_exists():
assert_eq(GameMode.Mode.CANDY_SURVIVAL, 3, "CANDY_SURVIVAL should be enum value 3")
# Test 2: All 3 modes are present in enum
func test_all_modes_in_enum():
assert_eq(GameMode.Mode.FREEMODE, 0, "FREEMODE should be 0")
assert_eq(GameMode.Mode.STOP_N_GO, 1, "STOP_N_GO should be 1")
assert_eq(GameMode.Mode.CANDY_SURVIVAL, 3, "CANDY_SURVIVAL should be 3")
# =============================================================================
# String Conversion Tests
# =============================================================================
# Test 3: from_string recognizes "Candy Survival"
func test_from_string_candy_survival():
var result = GameMode.from_string("Candy Survival")
assert_eq(result, GameMode.Mode.CANDY_SURVIVAL, "from_string should parse 'Candy Survival' as CANDY_SURVIVAL")
# Test 4: mode_to_string returns "Candy Survival" for CANDY_SURVIVAL
func test_mode_to_string_gauntlet():
var result = GameMode.mode_to_string(GameMode.Mode.CANDY_SURVIVAL)
assert_eq(result, "Candy Survival", "mode_to_string should return 'Candy Survival'")
# Test 5: Round-trip conversion is lossless
func test_round_trip_conversion():
var mode_str = GameMode.mode_to_string(GameMode.Mode.CANDY_SURVIVAL)
var mode_enum = GameMode.from_string(mode_str)
assert_eq(mode_enum, GameMode.Mode.CANDY_SURVIVAL, "Round-trip should preserve CANDY_SURVIVAL")
# Test 6: All existing modes still round-trip correctly
func test_existing_modes_round_trip():
for mode in [GameMode.Mode.FREEMODE, GameMode.Mode.STOP_N_GO]:
var s = GameMode.mode_to_string(mode)
var back = GameMode.from_string(s)
assert_eq(back, mode, "Round-trip failed for %s" % s)
# Test 7: Unknown string defaults to FREEMODE
func test_unknown_string_defaults_freemode():
var result = GameMode.from_string("NonExistentMode")
assert_eq(result, GameMode.Mode.FREEMODE, "Unknown mode string should default to FREEMODE")
# =============================================================================
# get_all_modes Tests
# =============================================================================
# Test 8: get_all_modes includes "Candy Survival"
func test_get_all_modes_includes_gauntlet():
var modes = GameMode.get_all_modes()
assert_has(modes, "Candy Survival", "get_all_modes should include 'Candy Survival'")
# Test 9: get_all_modes returns exactly 3 entries
func test_get_all_modes_count():
var modes = GameMode.get_all_modes()
assert_eq(modes.size(), 3, "get_all_modes should return 3 modes")
# Test 10: get_all_modes order is correct
func test_get_all_modes_order():
var modes = GameMode.get_all_modes()
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], "Candy Survival", "Third mode should be Candy Survival")
# =============================================================================
# is_restricted Tests
# =============================================================================
# Test 11: CANDY_SURVIVAL is a restricted mode
func test_candy_survival_is_restricted():
var result = GameMode.is_restricted(GameMode.Mode.CANDY_SURVIVAL)
assert_true(result, "CANDY_SURVIVAL should be restricted (dedicated arena)")
# Test 12: FREEMODE is NOT restricted
func test_freemode_not_restricted():
var result = GameMode.is_restricted(GameMode.Mode.FREEMODE)
assert_false(result, "FREEMODE should not be restricted")
# Test 13: All restricted modes are confirmed
func test_all_restricted_modes():
assert_true(GameMode.is_restricted(GameMode.Mode.STOP_N_GO), "STOP_N_GO should be restricted")
assert_true(GameMode.is_restricted(GameMode.Mode.CANDY_SURVIVAL), "CANDY_SURVIVAL should be restricted")
# =============================================================================
# LobbyManager Integration Tests
# =============================================================================
# Test 14: Lobby available_game_modes includes "Candy Survival"
func test_lobby_modes_includes_gauntlet():
var modes = LobbyManager.available_game_modes
assert_has(modes, "Candy Survival", "LobbyManager.available_game_modes should include 'Candy Survival'")
# Test 15: candy_survival_manager.gd script file exists
func test_candy_survival_manager_script_exists():
var script_exists = ResourceLoader.exists("res://scripts/managers/candy_survival_manager.gd")
assert_true(script_exists, "candy_survival_manager.gd should exist")
# Test 16: CandySurvivalManager class can be loaded
func test_candy_survival_manager_loads():
var script = load("res://scripts/managers/candy_survival_manager.gd")
assert_not_null(script, "candy_survival_manager.gd should load without errors")
# Test 17: CandySurvivalManager has required methods
func test_candy_survival_manager_has_methods():
var manager = CandySurvivalManager.new()
assert_true(manager.has_method("_setup_arena"), "CandySurvivalManager should have _setup_arena()")
assert_true(manager.has_method("_apply_arena_setup"), "CandySurvivalManager should have _apply_arena_setup()")
assert_true(manager.has_method("start_game_mode"), "CandySurvivalManager should have start_game_mode()")
assert_true(manager.has_method("initialize"), "CandySurvivalManager should have initialize()")
manager.free()
# Test 18: CandySurvivalManager arena constants are correct
func test_candy_survival_arena_constants():
assert_eq(CandySurvivalManager.ARENA_COLUMNS, 20, "Arena should be 20 columns")
assert_eq(CandySurvivalManager.ARENA_ROWS, 20, "Arena should be 20 rows")
assert_eq(CandySurvivalManager.NPC_SIZE, 3, "NPC zone should be 3x3")
assert_eq(CandySurvivalManager.NPC_CENTER, Vector2i(9, 9), "NPC center should be at (9,9)")
# Test 19: NPC zone detection works
func test_npc_zone_detection():
var manager = CandySurvivalManager.new()
# Center of NPC zone
assert_true(manager._is_npc_zone(Vector2i(9, 9)), "Center (9,9) should be NPC zone")
# Edges of NPC zone
assert_true(manager._is_npc_zone(Vector2i(8, 8)), "Corner (8,8) should be NPC zone")
assert_true(manager._is_npc_zone(Vector2i(10, 10)), "Corner (10,10) should be NPC zone")
# Outside NPC zone
assert_false(manager._is_npc_zone(Vector2i(7, 9)), "Outside (7,9) should NOT be NPC zone")
assert_false(manager._is_npc_zone(Vector2i(11, 9)), "Outside (11,9) should NOT be NPC zone")
assert_false(manager._is_npc_zone(Vector2i(0, 0)), "Corner (0,0) should NOT be NPC zone")
manager.free()
# Test 20: Phase enum has 3 phases
func test_candy_survival_phases():
assert_eq(CandySurvivalManager.Phase.OPEN_ARENA, 0, "OPEN_ARENA should be 0")
assert_eq(CandySurvivalManager.Phase.ROUTE_PRESSURE, 1, "ROUTE_PRESSURE should be 1")
assert_eq(CandySurvivalManager.Phase.SURVIVAL_ENDGAME, 2, "SURVIVAL_ENDGAME should be 2")
func after_all():
gut.p("=== Candy Survival Registration Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://6pn8jkrn2kt
-185
View File
@@ -1,185 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Candidate Scoring System (v2) Candy Survival #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 CandySurvivalManager = preload("res://scripts/managers/candy_survival_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 = CandySurvivalManager.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.candy_survival_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")
-1
View File
@@ -1 +0,0 @@
uid://tugcu571care
-208
View File
@@ -1,208 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Sticky Cell System (v2) Candy Survival #068]
# Covers cell states, CLEANSED protection, coverage helpers, and the
# path-safety reachability (BFS) check.
# =============================================================================
var CandySurvivalManagerScript = load("res://scripts/managers/candy_survival_manager.gd")
var manager: CandySurvivalManager
func before_each():
manager = CandySurvivalManagerScript.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")
-1
View File
@@ -1 +0,0 @@
uid://csco4t66gq5et
-144
View File
@@ -1,144 +0,0 @@
extends GutTest
# =============================================================================
# Test: Candy Survival Tile Spawning & Mission System (Task #3)
# =============================================================================
var CandySurvivalManagerScript = load("res://scripts/managers/candy_survival_manager.gd")
var manager: CandySurvivalManager
func before_each():
manager = CandySurvivalManagerScript.new()
add_child(manager)
func after_each():
manager.queue_free()
# =============================================================================
# Arena Constants
# =============================================================================
func test_arena_size_20x20():
assert_eq(manager.ARENA_COLUMNS, 20, "Arena should be 20 columns")
assert_eq(manager.ARENA_ROWS, 20, "Arena should be 20 rows")
func test_npc_center_position():
assert_eq(manager.NPC_CENTER, Vector2i(9, 9), "NPC center should be at (9,9)")
func test_npc_size_3x3():
assert_eq(manager.NPC_SIZE, 3, "NPC zone should be 3x3")
# =============================================================================
# NPC Zone Exclusion
# =============================================================================
func test_npc_zone_center_is_excluded():
assert_true(manager._is_npc_zone(Vector2i(9, 9)), "Center (9,9) should be NPC zone")
func test_npc_zone_corners_are_excluded():
assert_true(manager._is_npc_zone(Vector2i(8, 8)), "Top-left (8,8) should be NPC zone")
assert_true(manager._is_npc_zone(Vector2i(10, 8)), "Top-right (10,8) should be NPC zone")
assert_true(manager._is_npc_zone(Vector2i(8, 10)), "Bottom-left (8,10) should be NPC zone")
assert_true(manager._is_npc_zone(Vector2i(10, 10)), "Bottom-right (10,10) should be NPC zone")
func test_outside_npc_zone_not_excluded():
assert_false(manager._is_npc_zone(Vector2i(7, 9)), "Left of NPC zone should NOT be excluded")
assert_false(manager._is_npc_zone(Vector2i(11, 9)), "Right of NPC zone should NOT be excluded")
assert_false(manager._is_npc_zone(Vector2i(9, 7)), "Above NPC zone should NOT be excluded")
assert_false(manager._is_npc_zone(Vector2i(9, 11)), "Below NPC zone should NOT be excluded")
func test_arena_corners_not_excluded():
assert_false(manager._is_npc_zone(Vector2i(0, 0)), "Top-left corner should be walkable")
assert_false(manager._is_npc_zone(Vector2i(19, 0)), "Top-right corner should be walkable")
assert_false(manager._is_npc_zone(Vector2i(0, 19)), "Bottom-left corner should be walkable")
assert_false(manager._is_npc_zone(Vector2i(19, 19)), "Bottom-right corner should be walkable")
func test_npc_zone_total_cells():
var npc_count = 0
for x in range(manager.ARENA_COLUMNS):
for z in range(manager.ARENA_ROWS):
if manager._is_npc_zone(Vector2i(x, z)):
npc_count += 1
assert_eq(npc_count, 9, "NPC zone should occupy exactly 9 cells (3x3)")
func test_walkable_cells_count():
# 20x20 = 400 total, minus 9 NPC = 391 walkable
var walkable = 400 - 9
assert_eq(walkable, 391, "Should have 391 walkable cells")
# =============================================================================
# Tile Constants
# =============================================================================
func test_goal_tile_ids_valid():
# Heart(7), Diamond(8), Star(9), Coin(10) — match StopNGoManager
var goal_items = [7, 8, 9, 10]
for item in goal_items:
assert_gt(item, 0, "Goal tile ID %d should be positive" % item)
assert_lt(item, 17, "Goal tile ID %d should not conflict with sticky(17)" % item)
func test_tile_walkable_id():
assert_eq(manager.TILE_WALKABLE, 0, "Walkable tile should be ID 0")
func test_tile_obstacle_id():
assert_eq(manager.TILE_OBSTACLE, 4, "Obstacle tile should be ID 4")
func test_tile_sticky_id():
assert_eq(manager.TILE_STICKY, 17, "Sticky tile should be ID 17")
func test_tile_telegraph_id():
assert_eq(manager.TILE_TELEGRAPH, 18, "Telegraph tile should be ID 18")
# =============================================================================
# Method Existence
# =============================================================================
func test_setup_mission_tiles_exists():
assert_true(manager.has_method("setup_mission_tiles"), "Should have setup_mission_tiles()")
func test_spawn_mission_tiles_exists():
assert_true(manager.has_method("_spawn_mission_tiles"), "Should have _spawn_mission_tiles()")
# =============================================================================
# Sticky Cell System
# =============================================================================
func test_sticky_cells_initially_empty():
assert_eq(manager.sticky_cells.size(), 0, "Sticky cells should start empty")
func test_is_sticky_cell_false_for_clean():
assert_false(manager.is_sticky_cell(Vector2i(5, 5)), "Clean cell should not be sticky")
func test_is_sticky_cell_true_after_marking():
manager.sticky_cells[Vector2i(5, 5)] = true
assert_true(manager.is_sticky_cell(Vector2i(5, 5)), "Marked cell should be sticky")
func test_clear_sticky_cell():
manager.sticky_cells[Vector2i(3, 3)] = true
manager.clear_sticky_cell(Vector2i(3, 3))
assert_false(manager.is_sticky_cell(Vector2i(3, 3)), "Cleared cell should no longer be sticky")
# =============================================================================
# Phase Interaction with Tile Spawning
# =============================================================================
func test_initial_phase_is_open_arena():
assert_eq(manager.current_phase, CandySurvivalManager.Phase.OPEN_ARENA, "Should start in Open Arena")
func test_phase_to_string_open_arena():
assert_eq(manager._phase_to_string(CandySurvivalManager.Phase.OPEN_ARENA), "Outer Pressure")
func test_phase_to_string_route_pressure():
assert_eq(manager._phase_to_string(CandySurvivalManager.Phase.ROUTE_PRESSURE), "Middle Pressure")
func test_phase_to_string_survival():
assert_eq(manager._phase_to_string(CandySurvivalManager.Phase.SURVIVAL_ENDGAME), "Inner Survival")
# =============================================================================
# Match Timer Integration
# =============================================================================
func test_match_duration_180s():
# Candy Survival uses 180s match (3 phases: 0-60, 60-120, 120-180)
var total = manager.PHASE_3_START + 60.0 # Phase 3 starts at 120, runs 60s
assert_eq(total, 180.0, "Total match should be 180 seconds")
-1
View File
@@ -1 +0,0 @@
uid://cpwhlklp7jkkg