extends Node # PlayerboardManager - Handles all playerboard operations including grab, put, arrange var player: Node3D var enhanced_gridmap: Node # Playerboard state var selected_gridmap_position = Vector2i(-1, -1) var selected_playerboard_slot = -1 var targeted_playerboard_slot = -1 # 0-based indices corresponding to User's 1-based request: 1,5,6,10,11,15,16,20,21,22,23,24,25 const HIDDEN_SLOTS = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24] func is_valid_playerboard_slot(slot_index: int) -> bool: return not (slot_index in HIDDEN_SLOTS) func initialize(p_player: Node3D, p_gridmap: Node): player = p_player enhanced_gridmap = p_gridmap func _normalize_tile(tile: int) -> int: """Normal tiles 7-10 are goals. 11-14 are powerups and not goals.""" return tile # ============================================================================= # GRAB Operations # ============================================================================= func grab_item(grid_position: Vector2i) -> bool: var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true if not enhanced_gridmap: print("[Grab] Failed for %s: enhanced_gridmap is null" % player.name) return false if not has_ap: print("[Grab] Failed for %s: no AP (%d)" % [player.name, player.action_points]) return false if player.get("is_frozen"): print("[Grab] Failed for %s: player is frozen" % player.name) return false var cell = Vector3i(grid_position.x, 1, grid_position.y) var item = enhanced_gridmap.get_cell_item(cell) # Validate adjacency (unless it's current position) if grid_position != player.current_position: var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0) var is_adjacent = false for neighbor in neighbors: if neighbor.position == grid_position: is_adjacent = true break if not is_adjacent: print("[Grab] Failed for %s: %s is not adjacent to current %s" % [player.name, grid_position, player.current_position]) return false if item == -1: print("[Grab] Failed for %s: no item at %s Layer 1" % [player.name, grid_position]) return false # === AUTO-ARRANGE LOGIC (Client-side pre-check) === # If item is powerup (11-14), we don't need a slot. var is_powerup = (item >= 11 and item <= 14) var target_slot = -1 if not is_powerup: target_slot = find_best_goal_slot_for_item(item) if target_slot == -1: print("[Grab] Failed for %s: No valid slot found for item %d." % [player.name, item]) return false # no space if not player.is_multiplayer_authority(): print("[Grab] Failed for %s: not authority" % player.name) return false print("[Grab] %s SUCCESS! Grabbing item %d at %s into slot %d" % [player.name, item, grid_position, target_slot]) # Play pickup animation (synced across network) if player.is_multiplayer_authority() and player.has_method("sync_pickup_animation"): player.rpc("sync_pickup_animation") # === Optimistic Local Update (immediate visual feedback) === # Apply changes locally first, server will validate/sync enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately # === Power-Up Consumption (Instant Unlock) === # IDs 11-14 are Ability Power-Ups. They are consumed on pickup, not placed on board. if item >= 11 and item <= 14: var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") if special_tiles_manager: special_tiles_manager.add_powerup_from_item(item) SfxManager.play("pick_up_power_tile") # Animation for powerup? # ... # Skip adding to playerboard. Just consume. else: # Normal Tile: Add to playerboard player.playerboard[target_slot] = item # Stop n Go Mission Progress var sng_main = player.get_tree().root.get_node_or_null("Main") if sng_main: var sng_manager = sng_main.get_node_or_null("StopNGoManager") if sng_manager and sng_manager.has_method("update_mission_progress"): sng_manager.update_mission_progress(player.name.to_int(), item) # Update UI immediately for responsiveness # Update UI immediately for responsiveness var main = player.get_tree().get_root().get_node_or_null("Main") if main and main.ui_manager and not (player.is_bot or player.is_in_group("Bots")): main.ui_manager.update_playerboard_ui() # Check if goal is completed after grabbing _check_goal_completion() # === Server Sync === if multiplayer.is_server(): # HOST/SERVER: Broadcast to all clients main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) # Use main's RPC which properly looks up player by ID on each client var peer_id = player.name.to_int() main.rpc("sync_playerboard", peer_id, player.playerboard) player.has_performed_action = true player.consume_action_points(1) player.rpc("force_action_state_none") else: # CLIENT: Send RPC request to server for validation player.rpc_id(1, "request_server_grab", grid_position, cell.x, cell.y, cell.z, item) return true # Action applied locally func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): var main = player.get_tree().get_root().get_node_or_null("Main") if not main: push_error("Server: Main node not found.") return false var server_gridmap = main.get_node("EnhancedGridMap") if not server_gridmap: push_error("Server: EnhancedGridMap not found.") return false # 1. Server-side Validation var server_item = server_gridmap.get_cell_item(cell) # Check if item is still there if server_item != item_id: print("Server: Item mismatch or already taken. Server has ", server_item) _force_sync_to_client(cell, server_item) return false # Check action points if player.action_points <= 0: print("Server: Player has no action points.") _force_sync_to_client(cell, server_item) return false # Check adjacency if grid_pos != player.current_position: var neighbors = server_gridmap.get_neighbors(player.current_position, 0) if not neighbors.any(func(n): return n.position == grid_pos): print("Server: Player is not adjacent to item.") _force_sync_to_client(cell, server_item) return false # 2. Server-side Auto-Arrange var is_powerup = (item_id >= 11 and item_id <= 14) var target_slot = -1 if not is_powerup: target_slot = find_best_goal_slot_for_item(item_id) if target_slot == -1: print("Server: Player has no valid slot for item.") _force_sync_to_client(cell, server_item) return false # 3. Server Executes the Action # 3a. Update gridmap (using Main's RPC, which has authority) main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) # 3b. Update playerboard state (on this server-side instance) if is_powerup: var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") if special_tiles_manager: special_tiles_manager.add_powerup_from_item(item_id) # Do not add to playerboard else: player.playerboard[target_slot] = item_id # Stop n Go Mission Progress var sng_manager = main.get_node_or_null("StopNGoManager") if sng_manager and sng_manager.has_method("update_mission_progress"): sng_manager.update_mission_progress(player.name.to_int(), item_id) # 3c. Broadcast the new playerboard state to all clients var peer_id = player.name.to_int() main.rpc("sync_playerboard", peer_id, player.playerboard) # 3d. Check if goal is completed (SERVER-SIDE - this triggers goal regeneration for clients!) # Logic only runs if board changed, but theoretically powerup pickup shouldn't trigger goal if not is_powerup: _check_goal_completion() # 3e. Consume action points player.has_performed_action = true player.consume_action_points(1) # 3f. Reset the UI for the player who acted player.rpc("force_action_state_none") # 4. Check if we need to respawn tiles (Scarcity Logic) # "during no more tiles" -> Refill if floor is empty _check_and_refill_grid_if_needed(server_gridmap) return true func _check_and_refill_grid_if_needed(server_gridmap: Node): # Check if there are any items left on floor 1 var has_items = false var item_list = server_gridmap.mesh_library.get_item_list() if server_gridmap.mesh_library else [] # Iterate used cells to check for ANY item on floor 1 var used_cells = server_gridmap.get_used_cells() for cell in used_cells: if cell.y == 1: # Floor 1 has_items = true break if not has_items: if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS): # Tekton Doors handles its own wall-aware refill in PortalModeManager return print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...") # Call randomize_floor on floor 1 using ScarcityController # ScarcityController is a global class, so we can pass its static function as a Callable server_gridmap.randomize_floor(1, ScarcityController.get_random_tile_id) # We need to sync the ENTIRE floor to clients. # EnhancedGridMap doesn't have a "Sync Floor" RPC built-in to Main, only single cells or array update. # Main.gd has sync_grid_item which handles single cells. # Ideally we'd loop and sync all, or add a method to Main to sync floor. # Loop is okay for 10x10 (100 RPCs is a bit much but acceptable for a rare event). var main = player.get_tree().get_root().get_node_or_null("Main") if main: for x in range(server_gridmap.columns): for z in range(server_gridmap.rows): var item = server_gridmap.get_cell_item(Vector3i(x, 1, z)) main.rpc("sync_grid_item", x, 1, z, item) func _force_sync_to_client(cell: Vector3i, server_item: int): """Force a sync of the specific cell and playerboard to the client who initiated the failed action.""" # Only meaningful if we are server if not multiplayer.is_server(): return var main = player.get_tree().get_root().get_node_or_null("Main") if not main: return # Determine client peer ID from player name (standard convention) # Note: Bots are ID 1, so we don't need to sync special for them (local function calls) # But for Clients... var peer_id = player.name.to_int() if peer_id == 1: # Server/Bot return # Sync the Grid Item (which they thought they took) main.rpc_id(peer_id, "sync_grid_item", cell.x, cell.y, cell.z, server_item) # Sync their Playerboard (which they thought they updated) main.rpc_id(peer_id, "sync_playerboard", peer_id, player.playerboard) print("Server: Forced sync to client %d due to action failure." % peer_id) func bot_try_grab_item() -> bool: if not enhanced_gridmap or player.action_points <= 0: return false # First check current position var current_cell = Vector3i(player.current_position.x, 1, player.current_position.y) var item = enhanced_gridmap.get_cell_item(current_cell) if item != -1: # Find empty slot that is NOT hidden var empty_slot = -1 for i in range(player.playerboard.size()): if player.playerboard[i] == -1 and not (i in HIDDEN_SLOTS): empty_slot = i break if empty_slot != -1: if player.is_multiplayer_authority(): # Convert Holo (11-14) if item >= 11 and item <= 14: item = item - 4 # Inventory Add if item >= 7 and item <= 10: var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") if special_tiles_manager: special_tiles_manager.add_powerup_from_item(item) player.playerboard[empty_slot] = item var main = player.get_tree().get_root().get_node_or_null("Main") if main: main.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1) main.rpc("sync_playerboard", player.name.to_int(), player.playerboard) player.has_performed_action = true player.action_points -= 1 _check_goal_completion() return true # Check adjacent cells if nothing at current position var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0) for neighbor in neighbors: if neighbor.is_walkable: var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y) item = enhanced_gridmap.get_cell_item(cell) if item != -1: # Find empty slot that is NOT hidden var empty_slot = -1 for i in range(player.playerboard.size()): if player.playerboard[i] == -1 and not (i in HIDDEN_SLOTS): empty_slot = i break if empty_slot != -1: if player.is_multiplayer_authority(): player.playerboard[empty_slot] = item var main = player.get_tree().get_root().get_node_or_null("Main") if main: main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) main.rpc("sync_playerboard", player.name.to_int(), player.playerboard) player.has_performed_action = true player.action_points -= 1 return true return false # ============================================================================= # PUT Operations # ============================================================================= func auto_put_item() -> bool: # Check AP only if in turn-based mode var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true if not enhanced_gridmap or not has_ap or player.is_bot or player.is_in_group("Bots") or player.get("is_frozen"): return false # Step 1: Find empty adjacent (or current) grid cells var valid_put_positions = [] var current_cell_3d = Vector3i(player.current_position.x, 1, player.current_position.y) if enhanced_gridmap.get_cell_item(current_cell_3d) == -1: valid_put_positions.append(player.current_position) var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0) for neighbor in neighbors: if neighbor.is_walkable: var pos = neighbor.position var cell_3d = Vector3i(pos.x, 1, pos.y) if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos): # TEKTON DOORS: Avoid portal doors var is_on_portal = false if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS): var doors = get_tree().get_nodes_in_group("PortalDoors") for door in doors: var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position)) if Vector2i(door_grid.x, door_grid.z) == pos: is_on_portal = true break if not is_on_portal: valid_put_positions.append(pos) if valid_put_positions.is_empty(): return false # Step 2: Find a tile that should NOT be on the board (prioritize non-goal items) var put_slot = -1 # Count how many times each goal item is needed in the goals var goal_counts = {} for i in range(3): for j in range(3): var g = player.goals[i * 3 + j] if g != -1: goal_counts[g] = goal_counts.get(g, 0) + 1 # Priority 1: Find items that are NOT in goals at all (junk tiles) for i in range(player.playerboard.size()): var current_item = player.playerboard[i] if current_item == -1: continue # Normalize item for goal comparison (holo tiles 11-14 = normal tiles 7-10) var normalized_item = _normalize_tile(current_item) # Item is not in goals at all → definitely junk, put this first if not normalized_item in player.goals: put_slot = i break # Priority 2: Find items outside central 3x3 OR items in central 3x3 that don't match goal position if put_slot == -1: for i in range(player.playerboard.size()): var board_item = player.playerboard[i] if board_item == -1: continue var row = i / 5 var col = i % 5 # If it's outside the central 3x3, it's potentially movable if row < 1 or row > 3 or col < 1 or col > 3: put_slot = i break else: # It's inside central 3x3 - check if it matches the corresponding goal position var goal_row = row - 1 var goal_col = col - 1 var expected_goal = player.goals[goal_row * 3 + goal_col] # Normalize for comparison (holo tiles 11-14 = normal tiles 7-10) var normalized_board = _normalize_tile(board_item) # If the item doesn't match what should be in this goal position, it's misplaced if normalized_board != expected_goal and expected_goal != -1: put_slot = i break # Priority 3: Find excess goal items (we have more than needed in central area) if put_slot == -1: for i in range(player.playerboard.size()): var current_item = player.playerboard[i] if current_item == -1: continue if not current_item in player.goals: continue # Count how many of this item we have in the correct central positions var correctly_placed_count = 0 for goal_row in range(3): for goal_col in range(3): if player.goals[goal_row * 3 + goal_col] == current_item: var board_idx = (goal_row + 1) * 5 + (goal_col + 1) if player.playerboard[board_idx] == current_item: correctly_placed_count += 1 # Count total of this item on the board var total_count = 0 for board_item in player.playerboard: if board_item == current_item: total_count += 1 # If we have all needed copies already correctly placed, extras can go if correctly_placed_count >= goal_counts.get(current_item, 0) and total_count > correctly_placed_count: put_slot = i break if put_slot == -1: return false # Nothing suitable to put # Step 3: Perform the put with optimistic local update var target_pos = valid_put_positions[0] var item = player.playerboard[put_slot] var cell = Vector3i(target_pos.x, 1, target_pos.y) if player.is_multiplayer_authority(): # Play put animation (synced across network) if player.is_multiplayer_authority() and player.has_method("sync_put_animation"): player.rpc("sync_put_animation") # === Optimistic Local Update (immediate visual feedback) === enhanced_gridmap.set_cell_item(cell, item) # Add item to grid visually immediately player.playerboard[put_slot] = -1 # Remove from playerboard immediately # Update UI immediately for responsiveness var main = player.get_tree().get_root().get_node_or_null("Main") if main and main.ui_manager and not (player.is_bot or player.is_in_group("Bots")): main.ui_manager.update_playerboard_ui() # === Server Sync === player.rpc("sync_grid_item", cell.x, cell.y, cell.z, item) player.rpc("sync_playerboard", player.playerboard) player.has_performed_action = true player.consume_action_points(1) return true # ============================================================================= # ARRANGE Operations # ============================================================================= # Arrangement mode has been removed along with ActionMenu. pass # ============================================================================= # Helper Functions # ============================================================================= func find_best_goal_slot_for_item(item: int) -> int: if item == -1: return -1 # Normalize item - treat holo tiles (11-14) the same as normal tiles (7-10) var normalized_item = _normalize_tile(item) # Convert goals to 2D (3x3) var goals_2d = [] for i in range(3): var row = [] for j in range(3): row.append(player.goals[i * 3 + j]) goals_2d.append(row) # Search for where this item should go in the central 3x3 (mapped to 5x5 board) for i in range(3): for j in range(3): if goals_2d[i][j] == normalized_item: var board_row = i + 1 # offset to center in 5x5 var board_col = j + 1 var slot_index = board_row * 5 + board_col # Ensure this slot is not hidden (though 3x3 center should be safe, best to check) if slot_index in HIDDEN_SLOTS: continue if player.playerboard[slot_index] == -1: # only if empty return slot_index # No ideal goal slot? Look for any empty slot that is NOT hidden AND NOT reserved for Goals # Goal slots are row 2-3, col 1-3 (indices: 11,12,13, 16,17,18) matching User request # Rows 1 (6,7,8) are now storage slots var reserved_goal_slots = [11, 12, 13, 16, 17, 18] for i in range(player.playerboard.size()): if player.playerboard[i] == -1: if i in HIDDEN_SLOTS: continue if i in reserved_goal_slots: continue # Skip if it's a goal slot (reserved) return i return -1 func find_best_put_candidate() -> Dictionary: # Convert goals to 2D (3x3) var goals_2d = [] for i in range(3): var row = [] for j in range(3): row.append(player.goals[i * 3 + j]) goals_2d.append(row) # Convert playerboard to 2D (5x5) var board_2d = [] for i in range(5): var row = [] for j in range(5): row.append(player.playerboard[i * 5 + j]) board_2d.append(row) # Step 1: Find misplaced or extra goal-matching items var candidate_items = [] for board_i in range(5): for board_j in range(5): var item = board_2d[board_i][board_j] if item == -1: continue var board_idx = board_i * 5 + board_j # Is this item part of the goals? if item not in player.goals: continue # Is it already in the correct central position? var is_in_correct_central_spot = false if board_i in [1, 2, 3] and board_j in [1, 2, 3]: var goal_i = board_i - 1 var goal_j = board_j - 1 if goals_2d[goal_i][goal_j] == item: is_in_correct_central_spot = true if not is_in_correct_central_spot: candidate_items.append({ "slot": board_idx, "item": item }) # Step 2: Find valid adjacent empty grid cells var valid_cells = [] # Check current position var current_cell_3d = Vector3i(player.current_position.x, 1, player.current_position.y) if enhanced_gridmap.get_cell_item(current_cell_3d) == -1: valid_cells.append(player.current_position) # Check neighbors var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0) for neighbor in neighbors: if neighbor.is_walkable: var pos = neighbor.position var cell_3d = Vector3i(pos.x, 1, pos.y) if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos): valid_cells.append(pos) if valid_cells.is_empty() or candidate_items.is_empty(): return {} # Step 3: Prefer to put an item that *completes* a missing goal for goal_i in range(3): for goal_j in range(3): var needed_item = goals_2d[goal_i][goal_j] if needed_item == -1: continue # Check if central spot is empty var board_i = goal_i + 1 var board_j = goal_j + 1 var central_slot = board_i * 5 + board_j if player.playerboard[central_slot] == -1: # Look for this item in candidate_items for cand in candidate_items: if cand.item == needed_item: if not valid_cells.is_empty(): return { "slot_index": cand.slot, "grid_position": valid_cells[0] # pick first valid cell } # Fallback: just put any candidate item return { "slot_index": candidate_items[0].slot, "grid_position": valid_cells[0] } func get_adjacent_playerboard_slots(slot_index) -> Array: var adjacent = [] var row = slot_index / 5 var col = slot_index % 5 if row > 0: adjacent.append(slot_index - 5) if row < 4: adjacent.append(slot_index + 5) if col > 0: adjacent.append(slot_index - 1) if col < 4: adjacent.append(slot_index + 1) return adjacent func is_valid_arrangement_slot(from_slot: int, to_slot: int) -> bool: var from_row = int(from_slot / 5) var from_col = from_slot % 5 var to_row = int(to_slot / 5) var to_col = to_slot % 5 var row_diff = abs(from_row - to_row) var col_diff = abs(from_col - to_col) return (row_diff == 1 and col_diff == 0) or (row_diff == 0 and col_diff == 1) func has_item_at_current_position() -> bool: var current_cell = Vector3i(player.current_position.x, 1, player.current_position.y) return enhanced_gridmap.get_cell_item(current_cell) != -1 func has_items_in_playerboard() -> bool: return player.playerboard.any(func(item): return item != -1) func playerboard_is_full() -> bool: # Check if any NON-HIDDEN slot is empty for i in range(player.playerboard.size()): if not (i in HIDDEN_SLOTS) and player.playerboard[i] == -1: return false return true # Slot selection management func select_playerboard_slot(slot_index: int): selected_playerboard_slot = slot_index _update_playerboard_slot_visual(slot_index) _highlight_adjacent_playerboard_slots() func deselect_playerboard_slot(): var old_selected = selected_playerboard_slot selected_playerboard_slot = -1 if old_selected != -1: _update_playerboard_slot_visual(old_selected) untarget_playerboard_slot() _highlight_adjacent_playerboard_slots() func target_playerboard_slot(slot_index: int): if targeted_playerboard_slot != -1: untarget_playerboard_slot() targeted_playerboard_slot = slot_index _update_playerboard_slot_visual(slot_index) func untarget_playerboard_slot(): if targeted_playerboard_slot != -1: var old_targeted = targeted_playerboard_slot targeted_playerboard_slot = -1 _update_playerboard_slot_visual(old_targeted) func can_move_to_target_playerboard_slot() -> bool: if selected_playerboard_slot == -1 or targeted_playerboard_slot == -1 or selected_playerboard_slot == targeted_playerboard_slot: return false var adjacent_slots = get_adjacent_playerboard_slots(selected_playerboard_slot) return adjacent_slots.has(targeted_playerboard_slot) func _update_playerboard_slot_visual(slot_index: int): var main = player.get_tree().get_root().get_node_or_null("Main") if not main or not main.playerboard_ui or (player.is_bot or player.is_in_group("Bots")): return var slot = main.playerboard_ui.get_child(slot_index) if slot: if slot.get_child_count() > 0: slot.get_child(0).visible = slot_index == selected_playerboard_slot if slot.get_child_count() > 1: slot.get_child(1).visible = slot_index == targeted_playerboard_slot if slot.get_child_count() > 2: slot.get_child(2).visible = selected_playerboard_slot != -1 and get_adjacent_playerboard_slots(selected_playerboard_slot).has(slot_index) func _highlight_adjacent_playerboard_slots(): var main = player.get_tree().get_root().get_node_or_null("Main") if not main or not main.playerboard_ui or (player.is_bot or player.is_in_group("Bots")): return for i in range(25): var slot = main.playerboard_ui.get_child(i) if slot.get_child_count() > 2: slot.get_child(2).hide() if selected_playerboard_slot != -1: var adjacent_slots = get_adjacent_playerboard_slots(selected_playerboard_slot) for adj_slot in adjacent_slots: var slot = main.playerboard_ui.get_child(adj_slot) if slot.get_child_count() > 2: slot.get_child(2).show() # ============================================================================= # Goal Completion Check # ============================================================================= func _check_goal_completion(): """Check if playerboard matches goals and trigger completion rewards.""" if not player.race_manager: return # Check if the pattern matches if player.race_manager.check_pattern_match(): print("[PlayerboardManager] Goal completed for player %s!" % player.name) # Level up boost difficulty on goal completion var powerup_manager = player.get_node_or_null("PowerUpManager") if powerup_manager: powerup_manager.add_goal_completion_reward() # Trigger screen shake for goal completion if player.is_multiplayer_authority() and player.has_method("trigger_screen_shake"): player.trigger_screen_shake("goal") # Notify GoalsCycleManager for scoring var main = player.get_tree().get_root().get_node_or_null("Main") if main: var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager") if goals_cycle_manager: goals_cycle_manager.on_goal_completed(player, goals_cycle_manager.get_time_remaining()) else: # Fallback if manager not initialized yet NotificationManager.send_message(player, NotificationManager.MESSAGES.GOAL_COMPLETED, NotificationManager.MessageType.GOAL) func clear_and_convert_to_score() -> int: """Clear playerboard and return score for matching tiles.""" var matching_score = 0 var goals = player.goals var playerboard = player.playerboard # Check center 3x3 of playerboard against goals for i in range(3): for j in range(3): var goal_idx = i * 3 + j var board_idx = (i + 1) * 5 + (j + 1) # Center 3x3 in 5x5 board if goal_idx < goals.size() and board_idx < playerboard.size(): if goals[goal_idx] != -1 and playerboard[board_idx] == goals[goal_idx]: matching_score += 10 # 10 points per matching tile # Clear playerboard player.playerboard.fill(-1) return matching_score