rename gauntlet→candy_survival + rewrite Candy Survival per boss design

- Rename enum GAUNTLET→CANDY_SURVIVAL, all gauntlet_*→candy_survival_*
- Rename files: gauntlet_manager→candy_survival_manager, candy_cannon→candy_survival_npc, gauntlet.tscn→candy_survival.tscn
- Rewrite candy_survival_manager.gd: blueprints, candy stack, Mekton delivery, Sugar Rush, knock/ghost charges, sticky-as-wall
- Update player_movement_manager.gd: smack→knock system, ghost integration
- All Candy Survival issues (#54-57, #65-70) retitled per boss design
- Shared managers (goals_cycle, goal, player_race, playerboard, turn) untouched
This commit is contained in:
god
2026-07-06 01:28:39 +08:00
parent 114748a54f
commit 528e22875d
33 changed files with 731 additions and 2181 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
extends Node
# Minimal EnhancedGridMap stand-in for Gauntlet headless tests. Records
# Minimal EnhancedGridMap stand-in for Candy Survival headless tests. Records
# set_cell_item calls so lifecycle tests can run the local sync path without a
# real GridMap. Only the surface the manager touches is implemented.
+2 -2
View File
@@ -1,7 +1,7 @@
extends Node
# Minimal "Main" stand-in for Gauntlet headless tests. Provides the RPC methods
# the GauntletManager calls on its main_scene so calls resolve without the full
# Minimal "Main" stand-in for Candy Survival headless tests. Provides the RPC methods
# the CandySurvivalManager calls on its main_scene so calls resolve without the full
# game scene. Methods are no-ops that just need to exist + be rpc-tagged.
@rpc("any_peer", "call_local", "reliable")
+50 -50
View File
@@ -1,13 +1,13 @@
extends GutTest
# =============================================================================
# Test: Bot AI — Sticky Avoidance & Pathfinding [Gauntlet #075]
# Test: Bot AI — Sticky Avoidance & Pathfinding Candy Survival #075]
# Verifies the bot's strategic planner correctly:
# • Detects Gauntlet mode and exposes helpers.
# • 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 GauntletManager when triggered.
# • Calls rpc_activate_cleanser on the CandySurvivalManager when triggered.
# =============================================================================
const BotStrategicPlanner = preload("res://scripts/bot_strategic_planner.gd")
@@ -26,8 +26,8 @@ class StubActor extends Node3D:
func is_position_occupied(_p: Vector2i) -> bool:
return false
class StubGauntletManager extends Node:
# Mimics the slice of GauntletManager API the bot planner depends on.
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
@@ -46,7 +46,7 @@ class StubGauntletManager extends Node:
var main_node: Node
var gridmap: Node
var gauntlet_manager: StubGauntletManager
var candy_survival_manager: StubCandySurvivalManager
var actor: StubActor
var planner: RefCounted
@@ -68,9 +68,9 @@ func before_each():
gridmap.set_cell_item(Vector3i(x, 0, z), 1)
main_node.add_child(gridmap)
gauntlet_manager = StubGauntletManager.new()
gauntlet_manager.name = "GauntletManager"
main_node.add_child(gauntlet_manager)
candy_survival_manager = StubCandySurvivalManager.new()
candy_survival_manager.name = "CandySurvivalManager"
main_node.add_child(candy_survival_manager)
actor = StubActor.new()
actor.enhanced_gridmap = gridmap
@@ -78,17 +78,17 @@ func before_each():
main_node.add_child(actor)
planner = BotStrategicPlanner.new(actor, gridmap)
planner.gauntlet_manager_override = gauntlet_manager
planner.candy_survival_manager_override = candy_survival_manager
# Default to Gauntlet mode for these tests.
LobbyManager.game_mode = "Candy Pump Survival"
# Default to Candy Survival mode for these tests.
LobbyManager.game_mode = "Candy Survival"
func after_each():
if is_instance_valid(main_node):
main_node.queue_free()
actor = null
planner = null
gauntlet_manager = null
candy_survival_manager = null
gridmap = null
# Reset lobby mode so other tests aren't affected.
LobbyManager.game_mode = "Freemode"
@@ -97,19 +97,19 @@ func after_each():
# Mode detection
# =============================================================================
func test_is_gauntlet_mode_true_when_set():
assert_true(planner.is_gauntlet_mode(), "Detects Gauntlet via LobbyManager")
func test_is_candy_survival_mode_true_when_set():
assert_true(planner.is_candy_survival_mode(), "Detects Candy Survival via LobbyManager")
func test_is_gauntlet_mode_false_in_other_modes():
func test_is_candy_survival_mode_false_in_other_modes():
LobbyManager.game_mode = "Stop n Go"
assert_false(planner.is_gauntlet_mode(), "Stop n Go is not Gauntlet")
assert_false(planner.is_candy_survival_mode(), "Stop n Go is not Candy Survival")
LobbyManager.game_mode = "Freemode"
assert_false(planner.is_gauntlet_mode(), "Freemode is not Gauntlet")
assert_false(planner.is_candy_survival_mode(), "Freemode is not Candy Survival")
func test_get_gauntlet_manager_resolves_from_main():
var gm = planner._get_gauntlet_manager()
assert_not_null(gm, "Resolves GauntletManager under /root/Main")
assert_eq(gm, gauntlet_manager, "Same instance as the one we added")
func test_get_candy_survival_manager_resolves_from_main():
var gm = planner._get_candy_survival_manager()
assert_not_null(gm, "Resolves CandySurvivalManager under /root/Main")
assert_eq(gm, candy_survival_manager, "Same instance as the one we added")
# =============================================================================
# Overlay detection
@@ -134,7 +134,7 @@ func test_overlay_unsafe_ignores_layer0_and_layer1():
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)),
"Only layer 2 overlay matters for Gauntlet safety")
"Only layer 2 overlay matters for Candy Survival safety")
# =============================================================================
# _is_valid_move_target integration
@@ -144,12 +144,12 @@ func test_valid_move_target_rejects_sticky_in_gauntlet():
# Make a sticky cell pass all other checks (valid position, walkable floor).
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_false(planner._is_valid_move_target(Vector2i(3, 3)),
"Sticky cell rejected in Gauntlet mode")
"Sticky cell rejected in Candy Survival mode")
func test_valid_move_target_rejects_telegraphed_in_gauntlet():
gridmap.set_cell_item(Vector3i(5, 2, 5), 18)
assert_false(planner._is_valid_move_target(Vector2i(5, 5)),
"Telegraphed cell rejected in Gauntlet mode")
"Telegraphed cell rejected in Candy Survival mode")
func test_valid_move_target_accepts_clean_cells():
assert_true(planner._is_valid_move_target(Vector2i(8, 8)),
@@ -163,27 +163,27 @@ func test_valid_move_target_ignores_players_when_requested():
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_gauntlet_allows_sticky():
# Outside Gauntlet, layer-2 sticky overlays are not safety-relevant.
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-Gauntlet modes")
"Sticky overlay ignored in non-Candy Survival modes")
func test_valid_move_target_allows_sticky_when_cleanser_active():
gauntlet_manager.cleanser_active[actor.peer_id] = true
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 GauntletManager authority
# Sticky-cell awareness via CandySurvivalManager authority
# =============================================================================
func test_valid_move_target_uses_gauntlet_manager_sticky_map():
func test_valid_move_target_uses_candy_survival_manager_sticky_map():
# Even if the gridmap overlay hasn't landed yet (RPC in flight), the
# GauntletManager's authoritative sticky_cells map must block the move.
gauntlet_manager.sticky_map[Vector2i(7, 7)] = true
# 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")
@@ -221,7 +221,7 @@ func test_should_activate_cleanser_false_without_charge():
"Trapped but no charge → cannot activate")
func test_should_activate_cleanser_true_when_surrounded():
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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
@@ -230,7 +230,7 @@ func test_should_activate_cleanser_true_when_surrounded():
"Trapped with charge → activate Cleanser")
func test_should_activate_cleanser_true_when_on_telegraphed():
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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.
@@ -241,8 +241,8 @@ func test_should_activate_cleanser_true_when_on_telegraphed():
"Standing on telegraph with 3 sticky neighbors → activate Cleanser")
func test_should_activate_cleanser_false_when_already_active():
gauntlet_manager.player_cleansers[actor.peer_id] = 1
gauntlet_manager.cleanser_active[actor.peer_id] = true
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
@@ -252,16 +252,16 @@ func test_should_activate_cleanser_false_when_already_active():
func test_should_activate_cleanser_false_outside_gauntlet():
LobbyManager.game_mode = "Stop n Go"
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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 Gauntlet → never auto-activate Cleanser")
"Outside Candy Survival → never auto-activate Cleanser")
func test_should_activate_cleanser_false_when_not_trapped():
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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(),
@@ -275,16 +275,16 @@ 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():
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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")
gauntlet_manager.cleanser_active[actor.peer_id] = true
candy_survival_manager.cleanser_active[actor.peer_id] = true
assert_true(planner._is_bot_cleanser_active(), "Manager reports active")
# =============================================================================
# BotController → GauntletManager RPC integration
# BotController → CandySurvivalManager RPC integration
# =============================================================================
func test_bot_controller_requests_cleanser_when_trapped():
@@ -295,7 +295,7 @@ func test_bot_controller_requests_cleanser_when_trapped():
ctrl.strategic_planner = planner
ctrl.actor = actor
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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
@@ -303,9 +303,9 @@ func test_bot_controller_requests_cleanser_when_trapped():
var requested = ctrl._try_activate_cleanser()
assert_true(requested, "Controller requests Cleanser when trapped")
assert_eq(gauntlet_manager.activate_rpc_calls.size(), 1,
assert_eq(candy_survival_manager.activate_rpc_calls.size(), 1,
"RPC called exactly once")
assert_eq(gauntlet_manager.activate_rpc_calls[0], actor.peer_id,
assert_eq(candy_survival_manager.activate_rpc_calls[0], actor.peer_id,
"Correct peer id sent")
ctrl.queue_free()
@@ -317,11 +317,11 @@ func test_bot_controller_does_not_request_when_safe():
ctrl.strategic_planner = planner
ctrl.actor = actor
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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(gauntlet_manager.activate_rpc_calls.size(), 0,
assert_eq(candy_survival_manager.activate_rpc_calls.size(), 0,
"No RPC fired")
ctrl.queue_free()
@@ -334,13 +334,13 @@ func test_bot_controller_skips_cleanser_outside_gauntlet():
ctrl.actor = actor
LobbyManager.game_mode = "Stop n Go"
gauntlet_manager.player_cleansers[actor.peer_id] = 1
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 Gauntlet")
assert_false(requested, "No request outside Candy Survival")
ctrl.queue_free()
+3 -3
View File
@@ -1,14 +1,14 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Candy Bubble System (v2) [Gauntlet #082]
# 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 GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
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
@@ -20,7 +20,7 @@ func before_each():
gridmap_mock = GridMapMock.new()
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
manager = GauntletManager.new()
manager = CandySurvivalManager.new()
main_mock.add_child(manager)
manager.initialize(main_mock, gridmap_mock)
manager.current_phase = 0
+3 -3
View File
@@ -1,13 +1,13 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Cleanser Power-Up (v2) [Gauntlet #072]
# 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 GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
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
@@ -20,7 +20,7 @@ func before_each():
gridmap_mock = GridMapMock.new()
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
manager = GauntletManager.new()
manager = CandySurvivalManager.new()
main_mock.add_child(manager)
manager.initialize(main_mock, gridmap_mock)
manager.current_phase = 0
+5 -5
View File
@@ -1,13 +1,13 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Telegraph Floor Highlight [Gauntlet #081]
# 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 GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
const CandySurvivalManager = preload("res://scripts/managers/candy_survival_manager.gd")
const GridMapMock = preload("res://tests/helpers/gridmap_mock.gd")
var main_mock: Node
@@ -15,7 +15,7 @@ var gridmap_mock: Node
var manager: Node
func before_all():
gut.p("=== Feature Tests [Gauntlet #081 Telegraph Floor Highlight] ===")
gut.p("=== Feature Tests Candy Survival #081 Telegraph Floor Highlight] ===")
func before_each():
main_mock = Node.new()
@@ -26,7 +26,7 @@ func before_each():
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
manager = GauntletManager.new()
manager = CandySurvivalManager.new()
main_mock.add_child(manager)
manager.initialize(main_mock, gridmap_mock)
@@ -63,7 +63,7 @@ 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_gauntlet_growth_tick.gd suite; here we only confirm 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)])
+39 -39
View File
@@ -1,19 +1,19 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Growth Tick System (v2) [Gauntlet #067]
# 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 GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
var gauntlet_manager: Node
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 [Gauntlet #067 Growth Tick] ===")
gut.p("=== Feature Tests Candy Survival #067 Growth Tick] ===")
func before_each():
main_mock = Node.new()
@@ -22,9 +22,9 @@ func before_each():
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
gauntlet_manager = GauntletManager.new()
main_mock.add_child(gauntlet_manager)
gauntlet_manager.initialize(main_mock, gridmap_mock)
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:
@@ -38,43 +38,43 @@ func after_all():
# =============================================================================
func test_growth_timer_starts_zero():
assert_eq(gauntlet_manager.growth_timer, 0.0, "Growth timer starts at 0.0")
assert_eq(candy_survival_manager.growth_timer, 0.0, "Growth timer starts at 0.0")
func test_growth_interval_default():
assert_eq(gauntlet_manager.growth_interval, 3.0, "Growth interval defaults to 3.0s")
assert_eq(candy_survival_manager.growth_interval, 3.0, "Growth interval defaults to 3.0s")
func test_telegraph_duration_default():
assert_eq(gauntlet_manager.telegraph_duration, 1.0, "Telegraph duration defaults to 1.0s")
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(gauntlet_manager.phase_growth_config.size(), 3, "Three phase growth configs")
assert_eq(candy_survival_manager.phase_growth_config.size(), 3, "Three phase growth configs")
func test_phase1_cells_range():
var cfg = gauntlet_manager.phase_growth_config[0]
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 = gauntlet_manager.phase_growth_config[1]
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 = gauntlet_manager.phase_growth_config[2]
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):
gauntlet_manager.current_phase = phase
var cfg = gauntlet_manager.phase_growth_config[phase]
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 = gauntlet_manager._cells_this_tick()
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"]])
@@ -83,15 +83,15 @@ func test_cells_this_tick_in_phase_range():
# =============================================================================
func test_candidates_are_all_safe_cells():
gauntlet_manager.current_phase = 0
var candidates = gauntlet_manager._generate_candidates()
candy_survival_manager.current_phase = 0
var candidates = candy_survival_manager._generate_candidates()
# Fresh arena: every playable cell is SAFE.
assert_eq(candidates.size(), gauntlet_manager.playable_cell_count(),
assert_eq(candidates.size(), candy_survival_manager.playable_cell_count(),
"All playable cells are candidates on a fresh arena")
func test_candidates_exclude_sticky():
gauntlet_manager.sticky_cells[Vector2i(3, 3)] = true
var candidates = gauntlet_manager._generate_candidates()
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):
@@ -99,8 +99,8 @@ func test_candidates_exclude_sticky():
assert_false(found, "Sticky cells are excluded from candidates")
func test_candidates_exclude_cleansed():
gauntlet_manager.mark_cleansed(Vector2i(4, 4))
var candidates = gauntlet_manager._generate_candidates()
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):
@@ -108,14 +108,14 @@ func test_candidates_exclude_cleansed():
assert_false(found, "Cleansed cells are excluded from candidates (regrowth protection)")
func test_candidates_exclude_npc_and_boundary():
var candidates = gauntlet_manager._generate_candidates()
var candidates = candy_survival_manager._generate_candidates()
for c in candidates:
var p = c["pos"]
assert_false(gauntlet_manager._is_npc_zone(p), "No NPC-zone candidates")
assert_false(gauntlet_manager._is_boundary(p), "No boundary candidates")
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 = gauntlet_manager._generate_candidates()
var candidates = candy_survival_manager._generate_candidates()
assert_true(candidates.size() > 0, "Has candidates")
assert_true(candidates[0].has("score"), "Candidate carries a score")
@@ -124,13 +124,13 @@ func test_candidates_have_scores():
# =============================================================================
func test_select_count_respected():
var candidates = gauntlet_manager._generate_candidates()
var picked = gauntlet_manager._select_cells_weighted(candidates, 5)
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 = gauntlet_manager._generate_candidates()
var picked = gauntlet_manager._select_cells_weighted(candidates, 10)
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")
@@ -138,7 +138,7 @@ func test_select_no_duplicates():
func test_select_capped_at_pool_size():
var small = [{"pos": Vector2i(2, 2), "score": 1.0}, {"pos": Vector2i(2, 3), "score": 1.0}]
var picked = gauntlet_manager._select_cells_weighted(small, 10)
var picked = candy_survival_manager._select_cells_weighted(small, 10)
assert_eq(picked.size(), 2, "Cannot select more than pool size")
# =============================================================================
@@ -146,14 +146,14 @@ func test_select_capped_at_pool_size():
# =============================================================================
func test_layer_classification():
assert_eq(gauntlet_manager._layer_of(Vector2i(9, 9)), "inner", "Center is inner")
assert_eq(gauntlet_manager._layer_of(Vector2i(1, 1)), "outer", "Corner is outer")
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():
gauntlet_manager.sticky_cells[Vector2i(5, 5)] = true
gauntlet_manager.sticky_cells[Vector2i(5, 6)] = true
assert_eq(gauntlet_manager._sticky_neighbor_count(Vector2i(6, 5)), 2,
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(gauntlet_manager._chebyshev(Vector2i(0, 0), Vector2i(3, 1)), 3, "Chebyshev distance")
assert_eq(candy_survival_manager._chebyshev(Vector2i(0, 0), Vector2i(3, 1)), 3, "Chebyshev distance")
+4 -4
View File
@@ -1,13 +1,13 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Movement Buffer System (v2) [Gauntlet #083]
# 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 GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
const CandySurvivalManager = preload("res://scripts/managers/candy_survival_manager.gd")
var manager
var main_mock: Node
var gridmap_mock: Node
@@ -18,7 +18,7 @@ func before_each():
gridmap_mock = Node.new()
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
manager = GauntletManager.new()
manager = CandySurvivalManager.new()
main_mock.add_child(manager)
manager.initialize(main_mock, gridmap_mock)
manager.current_phase = 0
@@ -61,7 +61,7 @@ func test_buffer_penalty_far_is_zero():
func test_buffer_penalty_lifts_in_final_window():
manager._register_buffer(Vector2i(6, 6), 40.0)
manager.elapsed_time = manager.gauntlet_round_duration() - 5.0 # within final 30s
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():
+54 -54
View File
@@ -1,11 +1,11 @@
# tests/test_gauntlet_registration.gd
# Tests for [Gauntlet] #1 Game Mode Registration
# Validates GAUNTLET enum, string conversion, lobby integration, and arena setup
# 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("=== Gauntlet Registration Tests [Gauntlet #1] ===")
gut.p("=== Candy Survival Registration Tests Candy Survival #1] ===")
func after_each():
pass
@@ -14,35 +14,35 @@ func after_each():
# GameMode Enum Tests
# =============================================================================
# Test 1: GAUNTLET enum value exists and equals 3
func test_gauntlet_enum_exists():
assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be enum value 3")
# 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.GAUNTLET, 3, "GAUNTLET should be 3")
assert_eq(GameMode.Mode.CANDY_SURVIVAL, 3, "CANDY_SURVIVAL should be 3")
# =============================================================================
# String Conversion Tests
# =============================================================================
# Test 3: from_string recognizes "Candy Pump Survival"
func test_from_string_candy_cannon():
var result = GameMode.from_string("Candy Pump Survival")
assert_eq(result, GameMode.Mode.GAUNTLET, "from_string should parse 'Candy Pump Survival' as GAUNTLET")
# 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 Pump Survival" for GAUNTLET
# 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.GAUNTLET)
assert_eq(result, "Candy Pump Survival", "mode_to_string should return 'Candy Pump Survival'")
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.GAUNTLET)
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.GAUNTLET, "Round-trip should preserve GAUNTLET")
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():
@@ -60,10 +60,10 @@ func test_unknown_string_defaults_freemode():
# get_all_modes Tests
# =============================================================================
# Test 8: get_all_modes includes "Candy Pump Survival"
# 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 Pump Survival", "get_all_modes should include 'Candy Pump Survival'")
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():
@@ -75,16 +75,16 @@ 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 Pump Survival", "Third mode should be Candy Pump Survival")
assert_eq(modes[2], "Candy Survival", "Third mode should be Candy Survival")
# =============================================================================
# is_restricted Tests
# =============================================================================
# Test 11: GAUNTLET is a restricted mode
func test_gauntlet_is_restricted():
var result = GameMode.is_restricted(GameMode.Mode.GAUNTLET)
assert_true(result, "GAUNTLET should be restricted (dedicated arena)")
# 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():
@@ -94,46 +94,46 @@ func test_freemode_not_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.GAUNTLET), "GAUNTLET 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 Pump Survival"
# 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 Pump Survival", "LobbyManager.available_game_modes should include 'Candy Pump Survival'")
assert_has(modes, "Candy Survival", "LobbyManager.available_game_modes should include 'Candy Survival'")
# Test 15: gauntlet_manager.gd script file exists
func test_gauntlet_manager_script_exists():
var script_exists = ResourceLoader.exists("res://scripts/managers/gauntlet_manager.gd")
assert_true(script_exists, "gauntlet_manager.gd should exist")
# 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: GauntletManager class can be loaded
func test_gauntlet_manager_loads():
var script = load("res://scripts/managers/gauntlet_manager.gd")
assert_not_null(script, "gauntlet_manager.gd should load without errors")
# 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: GauntletManager has required methods
func test_gauntlet_manager_has_methods():
var manager = GauntletManager.new()
assert_true(manager.has_method("_setup_arena"), "GauntletManager should have _setup_arena()")
assert_true(manager.has_method("_apply_arena_setup"), "GauntletManager should have _apply_arena_setup()")
assert_true(manager.has_method("start_game_mode"), "GauntletManager should have start_game_mode()")
assert_true(manager.has_method("initialize"), "GauntletManager should have initialize()")
# 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: GauntletManager arena constants are correct
func test_gauntlet_arena_constants():
assert_eq(GauntletManager.ARENA_COLUMNS, 20, "Arena should be 20 columns")
assert_eq(GauntletManager.ARENA_ROWS, 20, "Arena should be 20 rows")
assert_eq(GauntletManager.NPC_SIZE, 3, "NPC zone should be 3x3")
assert_eq(GauntletManager.NPC_CENTER, Vector2i(9, 9), "NPC center should be at (9,9)")
# 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 = GauntletManager.new()
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
@@ -146,10 +146,10 @@ func test_npc_zone_detection():
manager.free()
# Test 20: Phase enum has 3 phases
func test_gauntlet_phases():
assert_eq(GauntletManager.Phase.OPEN_ARENA, 0, "OPEN_ARENA should be 0")
assert_eq(GauntletManager.Phase.ROUTE_PRESSURE, 1, "ROUTE_PRESSURE should be 1")
assert_eq(GauntletManager.Phase.SURVIVAL_ENDGAME, 2, "SURVIVAL_ENDGAME should be 2")
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("=== Gauntlet Registration Tests Complete ===")
gut.p("=== Candy Survival Registration Tests Complete ===")
+4 -4
View File
@@ -1,13 +1,13 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Candidate Scoring System (v2) [Gauntlet #073]
# 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 GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
const CandySurvivalManager = preload("res://scripts/managers/candy_survival_manager.gd")
var manager
var main_mock: Node
var gridmap_mock: Node
@@ -18,7 +18,7 @@ func before_each():
gridmap_mock = Node.new()
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
manager = GauntletManager.new()
manager = CandySurvivalManager.new()
main_mock.add_child(manager)
manager.initialize(main_mock, gridmap_mock)
manager.current_phase = 0
@@ -94,7 +94,7 @@ func test_player_pressure_under_player_penalized():
assert_eq(manager._score_player_pressure(Vector2i(5, 5), players), -50.0, "Under player (early) = -50")
func test_player_pressure_under_player_final_window():
manager.elapsed_time = manager.gauntlet_round_duration() - 5.0 # within final 30s
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")
+4 -4
View File
@@ -1,16 +1,16 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Sticky Cell System (v2) [Gauntlet #068]
# 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 GauntletManagerScript = load("res://scripts/managers/gauntlet_manager.gd")
var manager: GauntletManager
var CandySurvivalManagerScript = load("res://scripts/managers/candy_survival_manager.gd")
var manager: CandySurvivalManager
func before_each():
manager = GauntletManagerScript.new()
manager = CandySurvivalManagerScript.new()
add_child(manager)
func after_each():
+9 -9
View File
@@ -1,14 +1,14 @@
extends GutTest
# =============================================================================
# Test: Gauntlet Tile Spawning & Mission System (Task #3)
# Test: Candy Survival Tile Spawning & Mission System (Task #3)
# =============================================================================
var GauntletManagerScript = load("res://scripts/managers/gauntlet_manager.gd")
var manager: GauntletManager
var CandySurvivalManagerScript = load("res://scripts/managers/candy_survival_manager.gd")
var manager: CandySurvivalManager
func before_each():
manager = GauntletManagerScript.new()
manager = CandySurvivalManagerScript.new()
add_child(manager)
func after_each():
@@ -123,22 +123,22 @@ func test_clear_sticky_cell():
# =============================================================================
func test_initial_phase_is_open_arena():
assert_eq(manager.current_phase, GauntletManager.Phase.OPEN_ARENA, "Should start in 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(GauntletManager.Phase.OPEN_ARENA), "Outer Pressure")
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(GauntletManager.Phase.ROUTE_PRESSURE), "Middle 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(GauntletManager.Phase.SURVIVAL_ENDGAME), "Inner Survival")
assert_eq(manager._phase_to_string(CandySurvivalManager.Phase.SURVIVAL_ENDGAME), "Inner Survival")
# =============================================================================
# Match Timer Integration
# =============================================================================
func test_match_duration_180s():
# Gauntlet uses 180s match (3 phases: 0-60, 60-120, 120-180)
# 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")