feat: add Candy Cannon Survival game mode with collectible tiles
Version bump to 2.3.6. New game mode features 20×20 arena with central cannon obstacle, three escalating phases (Open Arena, Route Pressure, Survival), and collectible tiles (Hearts, Diamonds, Stars, Coins) with pattern-matching missions. Players dodge candy volleys while completing collection goals. Updated export paths and version strings across all platforms (Windows, Android, Web, Linux).
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# tests/test_gauntlet_registration.gd
|
||||
# Tests for [Gauntlet] #1 Game Mode Registration
|
||||
# Validates GAUNTLET enum, string conversion, lobby integration, and arena setup
|
||||
|
||||
extends GutTest
|
||||
|
||||
func before_all():
|
||||
gut.p("=== Gauntlet Registration Tests [Gauntlet #1] ===")
|
||||
|
||||
func after_each():
|
||||
pass
|
||||
|
||||
# =============================================================================
|
||||
# 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 2: All 4 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.TEKTON_DOORS, 2, "TEKTON_DOORS should be 2")
|
||||
assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be 3")
|
||||
|
||||
# =============================================================================
|
||||
# String Conversion Tests
|
||||
# =============================================================================
|
||||
|
||||
# Test 3: from_string recognizes "Candy Cannon Survival"
|
||||
func test_from_string_candy_cannon():
|
||||
var result = GameMode.from_string("Candy Cannon Survival")
|
||||
assert_eq(result, GameMode.Mode.GAUNTLET, "from_string should parse 'Candy Cannon Survival' as GAUNTLET")
|
||||
|
||||
# Test 4: mode_to_string returns "Candy Cannon Survival" for GAUNTLET
|
||||
func test_mode_to_string_gauntlet():
|
||||
var result = GameMode.mode_to_string(GameMode.Mode.GAUNTLET)
|
||||
assert_eq(result, "Candy Cannon Survival", "mode_to_string should return 'Candy Cannon 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_enum = GameMode.from_string(mode_str)
|
||||
assert_eq(mode_enum, GameMode.Mode.GAUNTLET, "Round-trip should preserve GAUNTLET")
|
||||
|
||||
# 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, GameMode.Mode.TEKTON_DOORS]:
|
||||
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 Cannon Survival"
|
||||
func test_get_all_modes_includes_gauntlet():
|
||||
var modes = GameMode.get_all_modes()
|
||||
assert_has(modes, "Candy Cannon Survival", "get_all_modes should include 'Candy Cannon Survival'")
|
||||
|
||||
# Test 9: get_all_modes returns exactly 4 entries
|
||||
func test_get_all_modes_count():
|
||||
var modes = GameMode.get_all_modes()
|
||||
assert_eq(modes.size(), 4, "get_all_modes should return 4 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], "Tekton Doors", "Third mode should be Tekton Doors")
|
||||
assert_eq(modes[3], "Candy Cannon Survival", "Fourth mode should be Candy Cannon 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 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.TEKTON_DOORS), "TEKTON_DOORS should be restricted")
|
||||
assert_true(GameMode.is_restricted(GameMode.Mode.GAUNTLET), "GAUNTLET should be restricted")
|
||||
|
||||
# =============================================================================
|
||||
# LobbyManager Integration Tests
|
||||
# =============================================================================
|
||||
|
||||
# Test 14: Lobby available_game_modes includes "Candy Cannon Survival"
|
||||
func test_lobby_modes_includes_gauntlet():
|
||||
var modes = LobbyManager.available_game_modes
|
||||
assert_has(modes, "Candy Cannon Survival", "LobbyManager.available_game_modes should include 'Candy Cannon 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 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 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()")
|
||||
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 19: NPC zone detection works
|
||||
func test_npc_zone_detection():
|
||||
var manager = GauntletManager.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_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 after_all():
|
||||
gut.p("=== Gauntlet Registration Tests Complete ===")
|
||||
@@ -0,0 +1,144 @@
|
||||
extends GutTest
|
||||
|
||||
# =============================================================================
|
||||
# Test: Gauntlet Tile Spawning & Mission System (Task #3)
|
||||
# =============================================================================
|
||||
|
||||
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()
|
||||
|
||||
# =============================================================================
|
||||
# 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, GauntletManager.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), "Open Arena")
|
||||
|
||||
func test_phase_to_string_route_pressure():
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.ROUTE_PRESSURE), "Route Pressure")
|
||||
|
||||
func test_phase_to_string_survival():
|
||||
assert_eq(manager._phase_to_string(GauntletManager.Phase.SURVIVAL_ENDGAME), "Survival!")
|
||||
|
||||
# =============================================================================
|
||||
# Match Timer Integration
|
||||
# =============================================================================
|
||||
|
||||
func test_match_duration_180s():
|
||||
# Gauntlet 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")
|
||||
Reference in New Issue
Block a user