remove dead gauntlet test files, prune cleanser tests from test_bot_gauntlet
This commit is contained in:
+11
-181
@@ -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()
|
||||
@@ -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 +0,0 @@
|
||||
uid://bkte51v8tyoii
|
||||
@@ -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 +0,0 @@
|
||||
uid://b1bay8n1h65u3
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
uid://btbxtdhagjdba
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
uid://6pn8jkrn2kt
|
||||
@@ -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 +0,0 @@
|
||||
uid://tugcu571care
|
||||
@@ -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 +0,0 @@
|
||||
uid://csco4t66gq5et
|
||||
@@ -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 +0,0 @@
|
||||
uid://cpwhlklp7jkkg
|
||||
Reference in New Issue
Block a user