extends GutTest # ============================================================================= # Test: Gauntlet Sticky Cell System (v2) [Gauntlet #068] # Covers cell states, CLEANSED protection, coverage helpers, and the # path-safety reachability (BFS) check. # ============================================================================= var GauntletManagerScript = load("res://scripts/managers/gauntlet_manager.gd") var manager: GauntletManager func before_each(): manager = GauntletManagerScript.new() add_child(manager) func after_each(): manager.queue_free() # ============================================================================= # CellState enum # ============================================================================= func test_cellstate_enum_has_six_states(): assert_eq(manager.CellState.size(), 6, "CellState should have 6 states") func test_cellstate_values(): assert_eq(manager.CellState.SAFE, 0, "SAFE should be 0") assert_eq(manager.CellState.TELEGRAPHED, 1, "TELEGRAPHED should be 1") assert_eq(manager.CellState.STICKY, 2, "STICKY should be 2") assert_eq(manager.CellState.BUBBLE_GROWING, 3, "BUBBLE_GROWING should be 3") assert_eq(manager.CellState.BLOCKED, 4, "BLOCKED should be 4") assert_eq(manager.CellState.CLEANSED, 5, "CLEANSED should be 5") # ============================================================================= # cell_state() classification # ============================================================================= func test_interior_cell_is_safe(): assert_eq(manager.cell_state(Vector2i(3, 3)), manager.CellState.SAFE, "Open interior cell is SAFE") func test_npc_zone_is_blocked(): assert_eq(manager.cell_state(Vector2i(9, 9)), manager.CellState.BLOCKED, "NPC zone is BLOCKED") func test_boundary_is_blocked(): assert_eq(manager.cell_state(Vector2i(0, 5)), manager.CellState.BLOCKED, "Boundary is BLOCKED") assert_eq(manager.cell_state(Vector2i(19, 5)), manager.CellState.BLOCKED, "Far boundary is BLOCKED") func test_sticky_cell_state(): manager.sticky_cells[Vector2i(4, 4)] = true assert_eq(manager.cell_state(Vector2i(4, 4)), manager.CellState.STICKY, "Sticky cell is STICKY") func test_telegraphed_cell_state(): manager.telegraphed_cells[Vector2i(5, 5)] = 1.0 assert_eq(manager.cell_state(Vector2i(5, 5)), manager.CellState.TELEGRAPHED, "Telegraphed cell is TELEGRAPHED") func test_cleansed_cell_state(): manager.mark_cleansed(Vector2i(6, 6)) assert_eq(manager.cell_state(Vector2i(6, 6)), manager.CellState.CLEANSED, "Cleansed cell is CLEANSED") func test_sticky_takes_priority_over_cleansed(): # A cell that is both should report STICKY (active hazard wins). manager.sticky_cells[Vector2i(7, 7)] = true manager.cleansed_cells[Vector2i(7, 7)] = 5.0 assert_eq(manager.cell_state(Vector2i(7, 7)), manager.CellState.STICKY, "Sticky wins over cleansed") # ============================================================================= # CLEANSED protection lifecycle # ============================================================================= func test_mark_cleansed_sets_protection_time(): manager.mark_cleansed(Vector2i(3, 4)) assert_true(manager.is_cleansed_cell(Vector2i(3, 4)), "Cell should be cleansed") assert_almost_eq(manager.cleansed_cells[Vector2i(3, 4)], manager.CLEANSED_PROTECTION_TIME, 0.001, "Protection time set") func test_clear_sticky_marks_cleansed(): manager.sticky_cells[Vector2i(4, 5)] = true manager.clear_sticky_cell(Vector2i(4, 5)) assert_false(manager.is_sticky_cell(Vector2i(4, 5)), "Sticky removed") assert_true(manager.is_cleansed_cell(Vector2i(4, 5)), "Cleared cell becomes cleansed") func test_tick_cleansed_decays_and_expires(): manager.mark_cleansed(Vector2i(5, 6)) manager._tick_cleansed_cells(manager.CLEANSED_PROTECTION_TIME + 0.1) assert_false(manager.is_cleansed_cell(Vector2i(5, 6)), "Protection expires after full duration") func test_tick_cleansed_partial_decay_keeps_cell(): manager.mark_cleansed(Vector2i(5, 7)) manager._tick_cleansed_cells(1.0) assert_true(manager.is_cleansed_cell(Vector2i(5, 7)), "Cell still protected after partial tick") # ============================================================================= # Coverage helpers (v2 target 70-75%) # ============================================================================= func test_coverage_targets(): assert_almost_eq(manager.COVERAGE_TARGET_MIN, 0.70, 0.001, "Min coverage 70%") assert_almost_eq(manager.COVERAGE_TARGET_MAX, 0.75, 0.001, "Max coverage 75%") func test_playable_cell_count(): # 20x20 = 400, minus 76 boundary cells, minus 9 NPC zone = 315 assert_eq(manager.playable_cell_count(), 315, "Playable cells = 315") func test_coverage_ratio_zero_when_empty(): assert_almost_eq(manager.coverage_ratio(), 0.0, 0.001, "No sticky cells = 0 coverage") func test_coverage_ratio_scales(): var playable := manager.playable_cell_count() # Fill ~half the playable cells with arbitrary distinct keys. var half := int(playable / 2.0) for i in range(half): manager.sticky_cells[Vector2i(1000 + i, 0)] = true assert_almost_eq(manager.coverage_ratio(), float(half) / float(playable), 0.001, "Coverage tracks ratio") func test_coverage_reached_threshold(): var playable := manager.playable_cell_count() var needed := int(ceil(playable * manager.COVERAGE_TARGET_MIN)) for i in range(needed): manager.sticky_cells[Vector2i(2000 + i, 0)] = true assert_true(manager.is_coverage_reached(), "Coverage reached at >=70%") func test_coverage_not_reached_below_threshold(): manager.sticky_cells[Vector2i(2, 2)] = true assert_false(manager.is_coverage_reached(), "One sticky cell is below target") # ============================================================================= # Path safety: passability + reachability (BFS) # ============================================================================= func test_passable_interior(): assert_true(manager._is_cell_passable(Vector2i(3, 3)), "Open interior is passable") func test_not_passable_boundary_or_npc(): assert_false(manager._is_cell_passable(Vector2i(0, 0)), "Boundary not passable") assert_false(manager._is_cell_passable(Vector2i(9, 9)), "NPC zone not passable") func test_not_passable_sticky(): manager.sticky_cells[Vector2i(3, 3)] = true assert_false(manager._is_cell_passable(Vector2i(3, 3)), "Sticky cell not passable") func test_extra_sticky_blocks_passability(): var extra := {Vector2i(4, 4): true} assert_false(manager._is_cell_passable(Vector2i(4, 4), extra), "Hypothetical sticky blocks") assert_true(manager._is_cell_passable(Vector2i(5, 5), extra), "Other cells still passable") func test_open_arena_has_large_safe_region(): # From an open interior cell, flood fill should easily exceed the minimum. var n := manager._reachable_safe_cells(Vector2i(3, 3), {}, 50) assert_true(n >= 50, "Open arena reaches the search cap") func test_player_has_safe_region_when_open(): assert_true(manager._player_has_safe_region(Vector2i(3, 3), {}), "Open cell has safe region") func test_fully_boxed_player_has_no_safe_region(): # Box in the cell at (3,3) on all 4 sides with hypothetical sticky. var extra := { Vector2i(2, 3): true, Vector2i(4, 3): true, Vector2i(3, 2): true, Vector2i(3, 4): true, } assert_false(manager._player_has_safe_region(Vector2i(3, 3), extra), "Boxed-in player has no safe region") func test_reachable_zero_when_start_blocked(): manager.sticky_cells[Vector2i(3, 3)] = true assert_eq(manager._reachable_safe_cells(Vector2i(3, 3), {}, 10), 0, "Blocked start reaches nothing") # ============================================================================= # Sticky entry → per-player slow (v2: no hard trap, no global time_scale) # ============================================================================= # Minimal stand-in for a Player that records apply_slow_effect calls. class SlowSpyPlayer: extends Node var slow_calls: Array = [] func apply_slow_effect(duration: float = 3.0) -> void: slow_calls.append(duration) func test_apply_sticky_slow_calls_player_slow(): var spy := SlowSpyPlayer.new() add_child(spy) # Force the local-call branch (no networked rpc) for a deterministic unit test. var saved_peer = multiplayer.multiplayer_peer multiplayer.multiplayer_peer = null manager.apply_sticky_slow(spy) multiplayer.multiplayer_peer = saved_peer assert_eq(spy.slow_calls.size(), 1, "Sticky slow invokes apply_slow_effect once") assert_almost_eq(spy.slow_calls[0], manager.STICKY_SLOW_DURATION, 0.001, "Slows for STICKY_SLOW_DURATION") spy.queue_free() func test_apply_sticky_slow_does_not_trap(): var spy := SlowSpyPlayer.new() spy.set("peer_id", 42) add_child(spy) var saved_peer = multiplayer.multiplayer_peer multiplayer.multiplayer_peer = null manager.apply_sticky_slow(spy) multiplayer.multiplayer_peer = saved_peer assert_false(manager.trapped_players.has(42), "Sticky slow never adds to trapped_players") spy.queue_free() func test_apply_sticky_slow_safe_without_method(): # A node lacking apply_slow_effect must not crash the call. var plain := Node.new() add_child(plain) manager.apply_sticky_slow(plain) # should no-op assert_true(true, "apply_sticky_slow tolerates players without the method") plain.queue_free() func test_sticky_slow_duration_is_positive(): assert_true(manager.STICKY_SLOW_DURATION > 0.0, "Sticky slow duration is a positive number")