extends Node2D signal score_changed(points: int) signal grid_state_loaded(grid_size: Vector2i, tile_types: int) enum GameState { WAITING, # Waiting for player input SELECTING, # First tile selected SWAPPING, # Animating tile swap PROCESSING # Processing matches and cascades } var GRID_SIZE := Vector2i(8, 8) var TILE_TYPES := 5 const TILE_SCENE := preload("res://scenes/game/gameplays/tile.tscn") # Safety constants const MAX_GRID_SIZE := 15 const MAX_TILE_TYPES := 10 const MAX_CASCADE_ITERATIONS := 20 const MIN_GRID_SIZE := 3 const MIN_TILE_TYPES := 3 # Layout constants (replacing magic numbers) const SCREEN_WIDTH_USAGE := 0.8 # Use 80% of screen width const SCREEN_HEIGHT_USAGE := 0.7 # Use 70% of screen height const GRID_LEFT_MARGIN := 50.0 const GRID_TOP_MARGIN := 50.0 # Timing constants const CASCADE_WAIT_TIME := 0.1 const SWAP_ANIMATION_TIME := 0.3 const TILE_DROP_WAIT_TIME := 0.2 var grid := [] var tile_size: float = 48.0 var grid_offset: Vector2 var current_state: GameState = GameState.WAITING var selected_tile: Node2D = null var cursor_position: Vector2i = Vector2i(0, 0) var keyboard_navigation_enabled: bool = false var grid_initialized: bool = false var instance_id: String func _ready(): # Generate instance ID instance_id = "Match3_%d" % get_instance_id() if grid_initialized: DebugManager.log_warn("[%s] Match3 _ready() called multiple times, skipping initialization" % instance_id, "Match3") return DebugManager.log_debug("[%s] Match3 _ready() started" % instance_id, "Match3") grid_initialized = true # Calculate grid layout _calculate_grid_layout() # Try to load saved state, otherwise use default var loaded_saved_state = await load_saved_state() if not loaded_saved_state: DebugManager.log_info("No saved state found, using default grid initialization", "Match3") _initialize_grid() else: DebugManager.log_info("Successfully loaded saved grid state", "Match3") DebugManager.log_debug("Match3 _ready() completed, calling debug structure check", "Match3") # Notify UI that grid state is loaded grid_state_loaded.emit(GRID_SIZE, TILE_TYPES) # Debug: Check scene tree structure call_deferred("_debug_scene_structure") func _calculate_grid_layout(): var viewport_size = get_viewport().get_visible_rect().size var available_width = viewport_size.x * SCREEN_WIDTH_USAGE var available_height = viewport_size.y * SCREEN_HEIGHT_USAGE # Calculate tile size based on available space var max_tile_width = available_width / GRID_SIZE.x var max_tile_height = available_height / GRID_SIZE.y tile_size = min(max_tile_width, max_tile_height) # Align grid to left side with margins var total_grid_height = tile_size * GRID_SIZE.y grid_offset = Vector2( GRID_LEFT_MARGIN, (viewport_size.y - total_grid_height) / 2 + GRID_TOP_MARGIN ) func _initialize_grid(): # Create gem pool for current tile types var gem_indices = [] for i in range(TILE_TYPES): gem_indices.append(i) for y in range(GRID_SIZE.y): grid.append([]) for x in range(GRID_SIZE.x): var tile = TILE_SCENE.instantiate() var tile_position = grid_offset + Vector2(x, y) * tile_size tile.position = tile_position tile.grid_position = Vector2i(x, y) add_child(tile) # Set gem types for this tile tile.set_active_gem_types(gem_indices) # Set tile type after adding to scene tree var new_type = randi() % TILE_TYPES tile.tile_type = new_type # Connect tile signals tile.tile_selected.connect(_on_tile_selected) DebugManager.log_debug("Created tile at grid(%d,%d) world_pos(%s) with type %d" % [x, y, tile_position, new_type], "Match3") grid[y].append(tile) func _has_match_at(pos: Vector2i) -> bool: # Bounds and null checks if not _is_valid_grid_position(pos): return false if pos.y >= grid.size() or pos.x >= grid[pos.y].size(): DebugManager.log_error("Grid array bounds exceeded at (%d,%d)" % [pos.x, pos.y], "Match3") return false var tile = grid[pos.y][pos.x] if not tile or not is_instance_valid(tile): return false # Check if tile has required properties if not "tile_type" in tile: DebugManager.log_warn("Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y], "Match3") return false var matches_horizontal = _get_match_line(pos, Vector2i(1, 0)) if matches_horizontal.size() >= 3: return true var matches_vertical = _get_match_line(pos, Vector2i(0, 1)) return matches_vertical.size() >= 3 # Check for any matches on the board func _check_for_matches() -> bool: for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): if _has_match_at(Vector2i(x, y)): return true return false func _get_match_line(start: Vector2i, dir: Vector2i) -> Array: # Validate input parameters if not _is_valid_grid_position(start): DebugManager.log_error("Invalid start position for match line: (%d,%d)" % [start.x, start.y], "Match3") return [] if abs(dir.x) + abs(dir.y) != 1 or (dir.x != 0 and dir.y != 0): DebugManager.log_error("Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y], "Match3") return [] # Check grid bounds and tile validity if start.y >= grid.size() or start.x >= grid[start.y].size(): return [] var start_tile = grid[start.y][start.x] if not start_tile or not is_instance_valid(start_tile): return [] if not "tile_type" in start_tile: return [] var line = [start_tile] var type = start_tile.tile_type # Check both directions with safety limits for offset in [1, -1]: var current = start + dir * offset var steps = 0 while steps < GRID_SIZE.x + GRID_SIZE.y and _is_valid_grid_position(current): if current.y >= grid.size() or current.x >= grid[current.y].size(): break var neighbor = grid[current.y][current.x] if not neighbor or not is_instance_valid(neighbor): break if not "tile_type" in neighbor or neighbor.tile_type != type: break line.append(neighbor) current += dir * offset steps += 1 return line func _clear_matches(): # Check grid integrity if not _validate_grid_integrity(): DebugManager.log_error("Grid integrity check failed in _clear_matches", "Match3") return var match_groups := [] var processed_tiles := {} for y in range(GRID_SIZE.y): if y >= grid.size(): continue for x in range(GRID_SIZE.x): if x >= grid[y].size(): continue # Safe tile access with validation var tile = grid[y][x] if not tile or not is_instance_valid(tile): continue if processed_tiles.has(tile): continue var pos = Vector2i(x, y) var horiz = _get_match_line(pos, Vector2i(1, 0)) var vert = _get_match_line(pos, Vector2i(0, 1)) if horiz.size() >= 3: match_groups.append(horiz) for match_tile in horiz: if is_instance_valid(match_tile): processed_tiles[match_tile] = true if vert.size() >= 3: match_groups.append(vert) for match_tile in vert: if is_instance_valid(match_tile): processed_tiles[match_tile] = true # Calculate total score from all matches # Formula: 3 gems = 3 points, 4 gems = 6 points, 5 gems = 8 points, etc. # Pattern: n gems = n + (n-2) points for n >= 3, but 3 gems = exactly 3 points var total_score := 0 for match_group in match_groups: var match_size = match_group.size() var match_score: int if match_size == 3: match_score = 3 # Exactly 3 points for 3 gems else: match_score = match_size + max(0, match_size - 2) # n + (n-2) for n >= 4 total_score += match_score print("Debug: Match of ", match_size, " gems = ", match_score, " points") # Remove duplicates from all matches combined var to_clear := [] var unique_dict := {} for match_group in match_groups: for tile in match_group: unique_dict[tile] = true to_clear = unique_dict.keys() # Fixed: Only proceed if there are matches to clear if to_clear.size() == 0: return # Emit score before clearing tiles if total_score > 0: print("Debug: Total score for this turn: ", total_score) score_changed.emit(total_score) # Safe tile removal with validation for tile in to_clear: if not is_instance_valid(tile): continue # Validate tile has grid_position property if not "grid_position" in tile: DebugManager.log_warn("Tile missing grid_position during removal", "Match3") tile.queue_free() continue var tile_pos = tile.grid_position # Validate grid position before clearing reference if _is_valid_grid_position(tile_pos) and tile_pos.y < grid.size() and tile_pos.x < grid[tile_pos.y].size(): grid[tile_pos.y][tile_pos.x] = null else: DebugManager.log_warn("Invalid grid position during tile removal: (%d,%d)" % [tile_pos.x, tile_pos.y], "Match3") tile.queue_free() # Wait one frame for nodes to be queued properly await get_tree().process_frame _drop_tiles() await get_tree().create_timer(TILE_DROP_WAIT_TIME).timeout _fill_empty_cells() func _drop_tiles(): var moved = true while moved: moved = false for x in range(GRID_SIZE.x): # Fixed: Start from GRID_SIZE.y - 1 to avoid out of bounds for y in range(GRID_SIZE.y - 1, -1, -1): var tile = grid[y][x] # Fixed: Check bounds before accessing y + 1 if tile and y + 1 < GRID_SIZE.y and not grid[y + 1][x]: grid[y + 1][x] = tile grid[y][x] = null tile.grid_position = Vector2i(x, y + 1) # You can animate position here using Tween for smooth drop: # tween.interpolate_property(tile, "position", tile.position, grid_offset + Vector2(x, y + 1) * tile_size, 0.2) tile.position = grid_offset + Vector2(x, y + 1) * tile_size moved = true func _fill_empty_cells(): # Safety check for grid integrity if not _validate_grid_integrity(): DebugManager.log_error("Grid integrity check failed in _fill_empty_cells", "Match3") return # Create gem pool for current tile types var gem_indices = [] for i in range(TILE_TYPES): gem_indices.append(i) var tiles_created = 0 for y in range(GRID_SIZE.y): if y >= grid.size(): DebugManager.log_error("Grid row %d does not exist" % y, "Match3") continue for x in range(GRID_SIZE.x): if x >= grid[y].size(): DebugManager.log_error("Grid column %d does not exist in row %d" % [x, y], "Match3") continue if not grid[y][x]: var tile = TILE_SCENE.instantiate() if not tile: DebugManager.log_error("Failed to instantiate tile at (%d,%d)" % [x, y], "Match3") continue tile.grid_position = Vector2i(x, y) tile.position = grid_offset + Vector2(x, y) * tile_size add_child(tile) # Set gem types for this tile with error handling if tile.has_method("set_active_gem_types"): tile.set_active_gem_types(gem_indices) else: DebugManager.log_warn("Tile missing set_active_gem_types method", "Match3") # Set random tile type with bounds checking if TILE_TYPES > 0: tile.tile_type = randi() % TILE_TYPES else: DebugManager.log_error("TILE_TYPES is 0, cannot set tile type", "Match3") tile.queue_free() continue grid[y][x] = tile # Connect tile signals for new tiles with error handling if tile.has_signal("tile_selected"): tile.tile_selected.connect(_on_tile_selected) else: DebugManager.log_warn("Tile missing tile_selected signal", "Match3") tiles_created += 1 DebugManager.log_debug("Created %d new tiles" % tiles_created, "Match3") # Enhanced cascade protection with better limits await get_tree().create_timer(CASCADE_WAIT_TIME).timeout var iteration = 0 while _check_for_matches() and iteration < MAX_CASCADE_ITERATIONS: _clear_matches() await get_tree().create_timer(CASCADE_WAIT_TIME).timeout iteration += 1 if iteration >= MAX_CASCADE_ITERATIONS: DebugManager.log_warn("Maximum cascade iterations reached (%d), stopping to prevent infinite loop" % MAX_CASCADE_ITERATIONS, "Match3") # Save grid state after cascades complete save_current_state() func regenerate_grid(): # Validate grid size before regeneration if GRID_SIZE.x < MIN_GRID_SIZE or GRID_SIZE.y < MIN_GRID_SIZE or GRID_SIZE.x > MAX_GRID_SIZE or GRID_SIZE.y > MAX_GRID_SIZE: DebugManager.log_error("Invalid grid size for regeneration: %dx%d" % [GRID_SIZE.x, GRID_SIZE.y], "Match3") return if TILE_TYPES < 3 or TILE_TYPES > MAX_TILE_TYPES: DebugManager.log_error("Invalid tile types count for regeneration: %d" % TILE_TYPES, "Match3") return # Use time-based seed to ensure different patterns each time var new_seed = Time.get_ticks_msec() seed(new_seed) DebugManager.log_debug("Regenerating grid with seed: " + str(new_seed), "Match3") # Safe tile cleanup with improved error handling var children_to_remove = [] var removed_count = 0 for child in get_children(): if not is_instance_valid(child): continue # More robust tile detection if child.has_method("get_script") and child.get_script(): var script_path = child.get_script().resource_path if script_path == "res://scenes/game/gameplays/tile.gd": children_to_remove.append(child) removed_count += 1 elif "grid_position" in child: # Fallback detection children_to_remove.append(child) removed_count += 1 DebugManager.log_debug("Found %d tile children to remove" % removed_count, "Match3") # First clear grid array references to prevent access to nodes being freed for y in range(grid.size()): if y < grid.size() and grid[y] and grid[y] is Array: for x in range(grid[y].size()): if x < grid[y].size(): grid[y][x] = null # Clear the grid array safely grid.clear() # Remove all found tile children using queue_free for safe cleanup for child in children_to_remove: if is_instance_valid(child): child.queue_free() # Wait for nodes to be properly freed from the scene tree await get_tree().process_frame await get_tree().process_frame # Extra frame for complex hierarchies # Recalculate grid layout before regenerating tiles _calculate_grid_layout() # Regenerate the grid with safety checks _initialize_grid() func set_tile_types(new_count: int): # Input validation if new_count < 3: DebugManager.log_error("Tile types count too low: %d (minimum 3)" % new_count, "Match3") return if new_count > MAX_TILE_TYPES: DebugManager.log_error("Tile types count too high: %d (maximum %d)" % [new_count, MAX_TILE_TYPES], "Match3") return if new_count == TILE_TYPES: DebugManager.log_debug("Tile types count unchanged, skipping regeneration", "Match3") return DebugManager.log_debug("Changing tile types from %d to %d" % [TILE_TYPES, new_count], "Match3") TILE_TYPES = new_count # Regenerate grid with new tile types (gem pool is updated in regenerate_grid) await regenerate_grid() func set_grid_size(new_size: Vector2i): # Comprehensive input validation if new_size.x < MIN_GRID_SIZE or new_size.y < MIN_GRID_SIZE: DebugManager.log_error("Grid size too small: %dx%d (minimum %dx%d)" % [new_size.x, new_size.y, MIN_GRID_SIZE, MIN_GRID_SIZE], "Match3") return if new_size.x > MAX_GRID_SIZE or new_size.y > MAX_GRID_SIZE: DebugManager.log_error("Grid size too large: %dx%d (maximum %dx%d)" % [new_size.x, new_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE], "Match3") return if new_size == GRID_SIZE: DebugManager.log_debug("Grid size unchanged, skipping regeneration", "Match3") return DebugManager.log_debug("Changing grid size from %s to %s" % [GRID_SIZE, new_size], "Match3") GRID_SIZE = new_size # Regenerate grid with new size await regenerate_grid() func reset_all_visual_states() -> void: # Debug function to reset all tile visual states DebugManager.log_debug("Resetting all tile visual states", "Match3") for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): if grid[y][x] and grid[y][x].has_method("force_reset_visual_state"): grid[y][x].force_reset_visual_state() # Reset game state selected_tile = null current_state = GameState.WAITING keyboard_navigation_enabled = false func _debug_scene_structure() -> void: DebugManager.log_debug("=== Scene Structure Debug ===", "Match3") DebugManager.log_debug("Match3 node children count: %d" % get_child_count(), "Match3") DebugManager.log_debug("Match3 global position: %s" % global_position, "Match3") DebugManager.log_debug("Match3 scale: %s" % scale, "Match3") # Check if grid is properly initialized if not grid or grid.size() == 0: DebugManager.log_error("Grid not initialized when debug structure called", "Match3") return # Check tiles var tile_count = 0 for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): if y < grid.size() and x < grid[y].size() and grid[y][x]: tile_count += 1 DebugManager.log_debug("Created %d tiles out of %d expected" % [tile_count, GRID_SIZE.x * GRID_SIZE.y], "Match3") # Check first tile in detail if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]: var first_tile = grid[0][0] DebugManager.log_debug("First tile global position: %s" % first_tile.global_position, "Match3") DebugManager.log_debug("First tile local position: %s" % first_tile.position, "Match3") # Check parent chain var current_node = self var depth = 0 while current_node and depth < 10: DebugManager.log_debug("Parent level %d: %s (type: %s)" % [depth, current_node.name, current_node.get_class()], "Match3") current_node = current_node.get_parent() depth += 1 func _input(event: InputEvent) -> void: # Debug key to reset all visual states if event.is_action_pressed("action_east") and DebugManager.is_debug_enabled(): reset_all_visual_states() return if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: return # Handle keyboard/gamepad navigation if event.is_action_pressed("move_up"): _move_cursor(Vector2i.UP) keyboard_navigation_enabled = true elif event.is_action_pressed("move_down"): _move_cursor(Vector2i.DOWN) keyboard_navigation_enabled = true elif event.is_action_pressed("move_left"): _move_cursor(Vector2i.LEFT) keyboard_navigation_enabled = true elif event.is_action_pressed("move_right"): _move_cursor(Vector2i.RIGHT) keyboard_navigation_enabled = true elif event.is_action_pressed("action_south"): if keyboard_navigation_enabled: _select_tile_at_cursor() func _move_cursor(direction: Vector2i) -> void: # Input validation for direction vector if abs(direction.x) > 1 or abs(direction.y) > 1: DebugManager.log_error("Invalid cursor direction vector: " + str(direction), "Match3") return if direction.x != 0 and direction.y != 0: DebugManager.log_error("Diagonal cursor movement not supported: " + str(direction), "Match3") return # Validate grid integrity before cursor operations if not _validate_grid_integrity(): DebugManager.log_error("Grid integrity check failed in cursor movement", "Match3") return var old_pos = cursor_position var new_pos = cursor_position + direction # Bounds checking new_pos.x = clamp(new_pos.x, 0, GRID_SIZE.x - 1) new_pos.y = clamp(new_pos.y, 0, GRID_SIZE.y - 1) if new_pos != cursor_position: # Safe access to old tile var old_tile = _safe_grid_access(old_pos) if old_tile and "is_selected" in old_tile and "is_highlighted" in old_tile: if not old_tile.is_selected: old_tile.is_highlighted = false DebugManager.log_debug("Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y], "Match3") cursor_position = new_pos # Safe access to new tile var new_tile = _safe_grid_access(cursor_position) if new_tile and "is_selected" in new_tile and "is_highlighted" in new_tile: if not new_tile.is_selected: new_tile.is_highlighted = true func _select_tile_at_cursor() -> void: # Validate cursor position and grid integrity if not _is_valid_grid_position(cursor_position): DebugManager.log_warn("Invalid cursor position for selection: (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") return var tile = _safe_grid_access(cursor_position) if tile: DebugManager.log_debug("Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") _on_tile_selected(tile) else: DebugManager.log_warn("No valid tile at cursor position (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") func _on_tile_selected(tile: Node2D) -> void: if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: DebugManager.log_debug("Tile selection ignored - game busy (state: %s)" % [GameState.keys()[current_state]], "Match3") return DebugManager.log_debug("Tile selected at (%d,%d), gem type: %d" % [tile.grid_position.x, tile.grid_position.y, tile.tile_type], "Match3") if current_state == GameState.WAITING: # First tile selection _select_tile(tile) elif current_state == GameState.SELECTING: if tile == selected_tile: # Deselect current tile DebugManager.log_debug("Same tile clicked - deselecting", "Match3") _deselect_tile() else: # Attempt to swap with selected tile DebugManager.log_debug("Attempting swap between (%d,%d) and (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y, tile.grid_position.x, tile.grid_position.y], "Match3") _attempt_swap(selected_tile, tile) func _select_tile(tile: Node2D) -> void: selected_tile = tile tile.is_selected = true current_state = GameState.SELECTING DebugManager.log_debug("Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3") func _deselect_tile() -> void: if selected_tile and is_instance_valid(selected_tile): # Safe access to tile properties if "grid_position" in selected_tile: DebugManager.log_debug("Deselecting tile at (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y], "Match3") else: DebugManager.log_debug("Deselecting tile (no grid position available)", "Match3") # Safe property access for selection state if "is_selected" in selected_tile: selected_tile.is_selected = false # Handle cursor highlighting for keyboard navigation if keyboard_navigation_enabled: # Clear highlighting on selected tile if "is_highlighted" in selected_tile: selected_tile.is_highlighted = false # Re-highlight cursor position if it's different from the deselected tile var cursor_tile = _safe_grid_access(cursor_position) if cursor_tile and "is_highlighted" in cursor_tile: cursor_tile.is_highlighted = true DebugManager.log_debug("Restored cursor highlighting at (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") else: # For mouse navigation, just clear highlighting if "is_highlighted" in selected_tile: selected_tile.is_highlighted = false selected_tile = null current_state = GameState.WAITING func _are_adjacent(tile1: Node2D, tile2: Node2D) -> bool: if not tile1 or not tile2: return false var pos1 = tile1.grid_position var pos2 = tile2.grid_position var diff = abs(pos1.x - pos2.x) + abs(pos1.y - pos2.y) return diff == 1 func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void: if not _are_adjacent(tile1, tile2): DebugManager.log_debug("Tiles are not adjacent, cannot swap", "Match3") return DebugManager.log_debug("Starting swap animation: (%d,%d)[type:%d] <-> (%d,%d)[type:%d]" % [tile1.grid_position.x, tile1.grid_position.y, tile1.tile_type, tile2.grid_position.x, tile2.grid_position.y, tile2.tile_type], "Match3") current_state = GameState.SWAPPING await _swap_tiles(tile1, tile2) # Check if swap creates matches if _has_match_at(tile1.grid_position) or _has_match_at(tile2.grid_position): DebugManager.log_info("Valid swap created matches at (%d,%d) or (%d,%d)" % [tile1.grid_position.x, tile1.grid_position.y, tile2.grid_position.x, tile2.grid_position.y], "Match3") _deselect_tile() current_state = GameState.PROCESSING _clear_matches() await get_tree().create_timer(0.3).timeout current_state = GameState.WAITING else: # Invalid swap - revert DebugManager.log_debug("No matches created, reverting swap", "Match3") await _swap_tiles(tile1, tile2) # Swap back DebugManager.log_debug("Swap reverted successfully", "Match3") _deselect_tile() current_state = GameState.WAITING func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void: if not tile1 or not tile2: DebugManager.log_error("Cannot swap tiles - one or both tiles are null", "Match3") return # Update grid positions var pos1 = tile1.grid_position var pos2 = tile2.grid_position DebugManager.log_debug("Swapping tile positions: (%d,%d) -> (%d,%d), (%d,%d) -> (%d,%d)" % [pos1.x, pos1.y, pos2.x, pos2.y, pos2.x, pos2.y, pos1.x, pos1.y], "Match3") tile1.grid_position = pos2 tile2.grid_position = pos1 # Update grid array grid[pos1.y][pos1.x] = tile2 grid[pos2.y][pos2.x] = tile1 # Animate position swap var target_pos1 = grid_offset + Vector2(pos2.x, pos2.y) * tile_size var target_pos2 = grid_offset + Vector2(pos1.x, pos1.y) * tile_size DebugManager.log_trace("Animating tile movement over 0.3 seconds", "Match3") var tween = create_tween() tween.set_parallel(true) tween.tween_property(tile1, "position", target_pos1, SWAP_ANIMATION_TIME) tween.tween_property(tile2, "position", target_pos2, SWAP_ANIMATION_TIME) await tween.finished DebugManager.log_trace("Tile swap animation completed", "Match3") func serialize_grid_state() -> Array: # Convert the current grid to a serializable 2D array DebugManager.log_info("Starting serialization: grid.size()=%d, GRID_SIZE=(%d,%d)" % [grid.size(), GRID_SIZE.x, GRID_SIZE.y], "Match3") if grid.size() == 0: DebugManager.log_error("Grid array is empty during serialization!", "Match3") return [] var serialized_grid = [] var valid_tiles = 0 var null_tiles = 0 for y in range(GRID_SIZE.y): var row = [] for x in range(GRID_SIZE.x): if y < grid.size() and x < grid[y].size() and grid[y][x]: row.append(grid[y][x].tile_type) valid_tiles += 1 # Only log first few for brevity if valid_tiles <= 5: DebugManager.log_debug("Serializing tile (%d,%d): type %d" % [x, y, grid[y][x].tile_type], "Match3") else: row.append(-1) # Invalid/empty tile null_tiles += 1 # Only log first few nulls for brevity if null_tiles <= 5: DebugManager.log_debug("Serializing tile (%d,%d): NULL/empty (-1)" % [x, y], "Match3") serialized_grid.append(row) DebugManager.log_info("Serialized grid state: %dx%d grid, %d valid tiles, %d null tiles" % [GRID_SIZE.x, GRID_SIZE.y, valid_tiles, null_tiles], "Match3") return serialized_grid func get_active_gem_types() -> Array: # Get active gem types from the first available tile if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]: return grid[0][0].active_gem_types.duplicate() else: # Fallback to default var default_types = [] for i in range(TILE_TYPES): default_types.append(i) return default_types func save_current_state(): # Save complete game state var grid_layout = serialize_grid_state() var active_gems = get_active_gem_types() DebugManager.log_info("Saving match3 state: size(%d,%d), %d tile types, %d active gems" % [GRID_SIZE.x, GRID_SIZE.y, TILE_TYPES, active_gems.size()], "Match3") SaveManager.save_grid_state(GRID_SIZE, TILE_TYPES, active_gems, grid_layout) func load_saved_state() -> bool: # Check if there's a saved grid state if not SaveManager.has_saved_grid(): DebugManager.log_info("No saved grid state found, using default generation", "Match3") return false var saved_state = SaveManager.get_saved_grid_state() # Restore grid settings var saved_size = Vector2i(saved_state.grid_size.x, saved_state.grid_size.y) TILE_TYPES = saved_state.tile_types_count var saved_gems = saved_state.active_gem_types var saved_layout = saved_state.grid_layout DebugManager.log_info("[%s] Loading saved grid state: size(%d,%d), %d tile types, layout_size=%d" % [instance_id, saved_size.x, saved_size.y, TILE_TYPES, saved_layout.size()], "Match3") # Debug: Print first few rows of loaded layout for y in range(min(3, saved_layout.size())): var row_str = "" for x in range(min(8, saved_layout[y].size())): row_str += str(saved_layout[y][x]) + " " DebugManager.log_debug("Loading row %d: %s" % [y, row_str], "Match3") # Validate saved data if saved_layout.size() != saved_size.y: DebugManager.log_error("Saved grid layout height mismatch: expected %d, got %d" % [saved_size.y, saved_layout.size()], "Match3") return false if saved_layout.size() > 0 and saved_layout[0].size() != saved_size.x: DebugManager.log_error("Saved grid layout width mismatch: expected %d, got %d" % [saved_size.x, saved_layout[0].size()], "Match3") return false # Apply the saved settings var old_size = GRID_SIZE GRID_SIZE = saved_size # Recalculate layout if size changed if old_size != saved_size: DebugManager.log_info("Grid size changed from %s to %s, recalculating layout" % [old_size, saved_size], "Match3") _calculate_grid_layout() await _restore_grid_from_layout(saved_layout, saved_gems) DebugManager.log_info("Successfully loaded saved grid state", "Match3") return true func _restore_grid_from_layout(grid_layout: Array, active_gems: Array): DebugManager.log_info("[%s] Starting grid restoration: layout_size=%d, active_gems=%s" % [instance_id, grid_layout.size(), active_gems], "Match3") # Clear ALL existing tile children, not just ones in grid array # This ensures no duplicate layers are created var all_tile_children = [] for child in get_children(): if child.has_method("get_script") and child.get_script(): var script_path = child.get_script().resource_path if script_path == "res://scenes/game/gameplays/tile.gd": all_tile_children.append(child) DebugManager.log_debug("Found %d existing tile children to remove" % all_tile_children.size(), "Match3") # Remove all found tile children for child in all_tile_children: child.queue_free() # Clear existing grid array for y in range(grid.size()): if grid[y] and grid[y] is Array: for x in range(grid[y].size()): grid[y][x] = null # Clear grid array grid.clear() # Wait for nodes to be freed await get_tree().process_frame # Restore grid from saved layout for y in range(GRID_SIZE.y): grid.append([]) for x in range(GRID_SIZE.x): var tile = TILE_SCENE.instantiate() var tile_position = grid_offset + Vector2(x, y) * tile_size tile.position = tile_position tile.grid_position = Vector2i(x, y) add_child(tile) # Set gem types for this tile tile.set_active_gem_types(active_gems) # Set the saved tile type var saved_tile_type = grid_layout[y][x] DebugManager.log_debug("Setting tile (%d,%d): saved_type=%d, TILE_TYPES=%d" % [x, y, saved_tile_type, TILE_TYPES], "Match3") if saved_tile_type >= 0 and saved_tile_type < TILE_TYPES: tile.tile_type = saved_tile_type DebugManager.log_debug("✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3") else: # Fallback for invalid tile types tile.tile_type = randi() % TILE_TYPES DebugManager.log_error("✗ Invalid saved tile type %d at (%d,%d), using random %d" % [saved_tile_type, x, y, tile.tile_type], "Match3") # Connect tile signals tile.tile_selected.connect(_on_tile_selected) grid[y].append(tile) DebugManager.log_info("Completed grid restoration: %d tiles restored" % [GRID_SIZE.x * GRID_SIZE.y], "Match3") # Safety and validation helper functions func _is_valid_grid_position(pos: Vector2i) -> bool: return pos.x >= 0 and pos.y >= 0 and pos.x < GRID_SIZE.x and pos.y < GRID_SIZE.y func _validate_grid_integrity() -> bool: # Check if grid array structure is valid if not grid is Array: DebugManager.log_error("Grid is not an array", "Match3") return false if grid.size() != GRID_SIZE.y: DebugManager.log_error("Grid height mismatch: %d vs %d" % [grid.size(), GRID_SIZE.y], "Match3") return false for y in range(grid.size()): if not grid[y] is Array: DebugManager.log_error("Grid row %d is not an array" % y, "Match3") return false if grid[y].size() != GRID_SIZE.x: DebugManager.log_error("Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), GRID_SIZE.x], "Match3") return false return true func _safe_grid_access(pos: Vector2i) -> Node2D: # Safe grid access with comprehensive bounds checking if not _is_valid_grid_position(pos): return null if pos.y >= grid.size() or pos.x >= grid[pos.y].size(): DebugManager.log_warn("Grid bounds exceeded: (%d,%d)" % [pos.x, pos.y], "Match3") return null var tile = grid[pos.y][pos.x] if not tile or not is_instance_valid(tile): return null return tile func _safe_tile_access(tile: Node2D, property: String): # Safe property access on tiles if not tile or not is_instance_valid(tile): return null if not property in tile: DebugManager.log_warn("Tile missing property: %s" % property, "Match3") return null return tile.get(property)