extends GutTest # ============================================================================= # Test: Gauntlet Movement Buffer System (v2) [Gauntlet #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") 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 = 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() # ============================================================================= # 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.gauntlet_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")