Files
tekton/tests/test_gauntlet_bubble.gd
T

164 lines
7.0 KiB
GDScript

extends GutTest
# =============================================================================
# Test: Gauntlet Candy Bubble System (v2) [Gauntlet #082]
# Covers bubble-specific scoring components, phase budgets, anti-stacking,
# the 3x3 blast footprint, and the grow→explode lifecycle.
# Runs headless (no multiplayer peer): elapsed_time = 0 so the final-30s window
# is inactive unless a test sets it directly.
# =============================================================================
const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd")
const GridMapMock = preload("res://tests/helpers/gridmap_mock.gd")
var manager
var main_mock: Node
var gridmap_mock: Node
func before_each():
main_mock = Node.new()
add_child(main_mock)
gridmap_mock = GridMapMock.new()
gridmap_mock.name = "EnhancedGridMap"
main_mock.add_child(gridmap_mock)
manager = GauntletManager.new()
main_mock.add_child(manager)
manager.initialize(main_mock, gridmap_mock)
manager.current_phase = 0
func after_each():
if main_mock:
main_mock.queue_free()
# Run a callable with the multiplayer peer detached so manager code takes the
# local (non-rpc) sync path — deterministic for headless lifecycle tests.
func _without_peer(fn: Callable) -> void:
var saved = multiplayer.multiplayer_peer
multiplayer.multiplayer_peer = null
fn.call()
multiplayer.multiplayer_peer = saved
# =============================================================================
# Phase budget
# =============================================================================
func test_bubble_budget_per_phase():
manager.current_phase = 0
assert_eq(manager._bubble_budget_for_phase(), 0, "Phase 1 → 0 bubbles")
manager.current_phase = 1
assert_eq(manager._bubble_budget_for_phase(), 2, "Phase 2 → 2 bubbles")
manager.current_phase = 2
assert_eq(manager._bubble_budget_for_phase(), 3, "Phase 3 → 3 bubbles")
func test_phase_change_resets_counter():
manager.bubbles_this_phase = 2
manager._start_phase(manager.Phase.SURVIVAL_ENDGAME)
assert_eq(manager.bubbles_this_phase, 0, "Per-phase bubble count resets on phase change")
# =============================================================================
# Blast footprint (3x3, clipped)
# =============================================================================
func test_blast_is_3x3_in_open_area():
var cells = manager._bubble_blast_cells(Vector2i(14, 14))
assert_eq(cells.size(), 9, "Open-area bubble blast is 3x3 = 9 cells")
func test_blast_clips_npc_zone():
# Center adjacent to the NPC zone (9,9) clips blocked cells out.
var cells = manager._bubble_blast_cells(Vector2i(7, 9))
assert_true(cells.size() < 9, "Blast near NPC zone is clipped below 9")
for c in cells:
assert_false(manager._is_npc_zone(c), "No blast cell lands in NPC zone")
# =============================================================================
# Scoring components
# =============================================================================
func test_bubble_camping_thresholds():
var region: Vector2i = manager._region_of(Vector2i(8, 8))
manager._camp_tracking[1] = {"region": region, "time": 6.0}
assert_eq(manager._bubble_score_camping(Vector2i(8, 8)), 40.0, ">5s = +40")
manager._camp_tracking[1]["time"] = 9.0
assert_eq(manager._bubble_score_camping(Vector2i(8, 8)), 60.0, ">8s = +60")
func test_bubble_player_cluster():
var players = [Vector2i(5, 5), Vector2i(6, 6)]
assert_eq(manager._bubble_score_player_cluster(Vector2i(5, 6), players), 20.0, "2 nearby players = +20")
assert_eq(manager._bubble_score_player_cluster(Vector2i(15, 15), players), 0.0, "No nearby players = 0")
func test_bubble_direct_hit_penalty():
var players = [Vector2i(5, 5)]
assert_eq(manager._bubble_score_direct_hit(Vector2i(5, 5), players), -60.0, "Directly under player = -60")
assert_eq(manager._bubble_score_direct_hit(Vector2i(8, 8), players), 0.0, "Not under player = 0")
func test_bubble_recent_penalty():
manager.recent_bubble_positions = [Vector2i(14, 14)]
assert_eq(manager._bubble_score_recent(Vector2i(11, 11)), -50.0, "Near recent bubble = -50")
assert_eq(manager._bubble_score_recent(Vector2i(2, 2)), 0.0, "Far from recent bubble = 0")
func test_bubble_untouched_area():
# Open arena around (10,10) → large reachable region → +30.
assert_eq(manager._bubble_score_untouched_area(Vector2i(14, 14)), 30.0, "Large untouched area = +30")
func test_bubble_full_score_is_finite():
var s = manager._calculate_bubble_score(Vector2i(8, 8), [])
assert_true(is_finite(s), "Full bubble score is finite")
# =============================================================================
# Spawn lifecycle
# =============================================================================
func test_spawn_bubble_marks_growing_cells():
_without_peer(func():
manager._spawn_bubble(Vector2i(14, 14))
)
assert_eq(manager.bubbles_this_phase, 1, "Phase counter increments")
assert_eq(manager.bubbles_total, 1, "Round counter increments")
assert_eq(manager.active_bubbles.size(), 1, "One active bubble")
assert_true(manager.bubble_cells.has(Vector2i(14, 14)), "Center marked BUBBLE_GROWING")
assert_eq(manager.cell_state(Vector2i(14, 14)), manager.CellState.BUBBLE_GROWING, "cell_state reports BUBBLE_GROWING")
func test_spawn_bubble_records_recent_position():
_without_peer(func():
manager._spawn_bubble(Vector2i(14, 14))
)
assert_true(manager.recent_bubble_positions.has(Vector2i(14, 14)), "Center remembered for anti-stacking")
func test_recent_positions_capped():
_without_peer(func():
for i in range(manager.BUBBLE_RECENT_MEMORY + 3):
manager._spawn_bubble(Vector2i(2 + i, 15))
)
assert_eq(manager.recent_bubble_positions.size(), manager.BUBBLE_RECENT_MEMORY, "Recent memory capped")
# =============================================================================
# Explosion
# =============================================================================
func test_update_bubbles_explodes_after_grow_duration():
_without_peer(func():
manager._spawn_bubble(Vector2i(14, 14))
manager._update_bubbles(manager.BUBBLE_GROW_DURATION + 0.1)
)
assert_eq(manager.active_bubbles.size(), 0, "Bubble removed after exploding")
assert_true(manager.sticky_cells.has(Vector2i(14, 14)), "Center became sticky")
assert_false(manager.bubble_cells.has(Vector2i(14, 14)), "BUBBLE_GROWING cleared on explode")
func test_update_bubbles_waits_for_timer():
_without_peer(func():
manager._spawn_bubble(Vector2i(14, 14))
manager._update_bubbles(manager.BUBBLE_GROW_DURATION * 0.5)
)
assert_eq(manager.active_bubbles.size(), 1, "Bubble still growing before timer elapses")
assert_false(manager.sticky_cells.has(Vector2i(14, 14)), "No sticky yet mid-grow")
func test_explode_creates_3x3_sticky():
_without_peer(func():
manager._explode_bubble(Vector2i(14, 14), manager._bubble_blast_cells(Vector2i(14, 14)))
)
var sticky_in_blast := 0
for dx in range(-1, 2):
for dz in range(-1, 2):
if manager.sticky_cells.has(Vector2i(14 + dx, 14 + dz)):
sticky_in_blast += 1
assert_eq(sticky_in_blast, 9, "Explosion creates a full 3x3 sticky area")