en esta app de python hay un error ya que no existe board_to_tuple. Corrigelo de la forma mas sencilla posible : ""import pygame
import sys
import random
from collections import deque
import heapq
import threading
from functools import lru_cache

# Configuración de Pygame
pygame.init()

# Constantes
TILE_SIZE = 100
GRID_SIZE = 4
WINDOW_HEIGHT = TILE_SIZE * GRID_SIZE + 60  # Espacio extra para el botón
WINDOW_WIDTH = TILE_SIZE * GRID_SIZE
FPS = 30  # Frames por segundo
STEP_DELAY = 100  # Milisegundos entre movimientos al animar la solución

# Colores
BACKGROUND_COLOR = (255, 255, 255)
TILE_COLOR = (0, 102, 204)
TEXT_COLOR = (255, 255, 255)
BUTTON_COLOR = (0, 204, 102)
BUTTON_SOLVING_COLOR = (255, 0, 0)  # Rojo para cuando está resolviendo

# Inicializar pantalla
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption('Puzzle 15')

# Fuente
font = pygame.font.Font(None, 72)
button_font = pygame.font.Font(None, 48)

# Variables globales para manejar la resolución
solving = False
solution = []
solution_step = 0
last_update_time = 0

def create_initial_board():
    while True:
        numbers = list(range(1, GRID_SIZE * GRID_SIZE))
        numbers.append(0)
        random.shuffle(numbers)
        board = [numbers[i:i + GRID_SIZE] for i in range(0, len(numbers), GRID_SIZE)]
        if is_solvable(board):
            print("Tablero inicial generado y es resoluble.")
            return board
        else:
            print("Tablero generado no es resoluble, generando uno nuevo.")

def is_solvable(board):
    flattened = [num for row in board for num in row if num != 0]
    inversions = 0
    for i in range(len(flattened)):
        for j in range(i + 1, len(flattened)):
            if flattened[i] > flattened[j]:
                inversions += 1

    empty_row, _ = find_empty_tile(board)

    if GRID_SIZE % 2 == 1:
        return inversions % 2 == 0
    else:
        empty_row_from_bottom = GRID_SIZE - empty_row
        if empty_row_from_bottom % 2 == 0:
            return inversions % 2 == 1
        else:
            return inversions % 2 == 0

def draw_board(board, solve_button, button_text_display):
    screen.fill(BACKGROUND_COLOR)
    for row in range(GRID_SIZE):
        for col in range(GRID_SIZE):
            value = board[row][col]
            if value != 0:
                x = col * TILE_SIZE
                y = row * TILE_SIZE
                pygame.draw.rect(screen, TILE_COLOR, (x, y, TILE_SIZE, TILE_SIZE))
                text = font.render(str(value), True, TEXT_COLOR)
                text_rect = text.get_rect(center=(x + TILE_SIZE // 2, y + TILE_SIZE // 2))
                screen.blit(text, text_rect)
    
    # Dibujar botón de resolver
    pygame.draw.rect(screen, solve_button['color'], solve_button['rect'])
    button_text = button_font.render(button_text_display, True, TEXT_COLOR)
    button_rect = button_text.get_rect(center=solve_button['rect'].center)
    screen.blit(button_text, button_rect)

def find_empty_tile(board):
    for row in range(GRID_SIZE):
        for col in range(GRID_SIZE):
            if board[row][col] == 0:
                return (row, col)
    return None

def is_valid_move(empty_tile, click_pos):
    empty_row, empty_col = empty_tile
    click_row, click_col = click_pos
    return (abs(empty_row - click_row) == 1 and empty_col == click_col) or (abs(empty_col - click_col) == 1 and empty_row == click_row)

def swap_tiles(board, tile1, tile2):
    board[tile1[0]][tile1[1]], board[tile2[0]][tile2[1]] = board[tile2[0]][tile2[1]], board[tile1[0]][tile1[1]]

def is_solved(board):
    correct = list(range(1, GRID_SIZE * GRID_SIZE)) + [0]
    flat_board = sum(board, [])
    return correct == flat_board

def get_neighbors(position):
    row, col = position
    neighbors = [
        (row + 1, col), (row - 1, col),
        (row, col + 1), (row, col - 1)
    ]
    valid_neighbors = [(r, c) for r, c in neighbors if 0 <= r < GRID_SIZE and 0 <= c < GRID_SIZE]
    return valid_neighbors

@lru_cache(maxsize=None)
def manhattan_distance_cached(board_state):
    """
    Calcula la distancia Manhattan con Linear Conflict para un estado del tablero.
    Usa caché para evitar recalcular estados ya evaluados.
    """
    distance = 0
    linear_conflict = 0

    board = [list(row) for row in board_state]

    for row in range(GRID_SIZE):
        for col in range(GRID_SIZE):
            value = board[row][col]
            if value != 0:
                target_row, target_col = divmod(value - 1, GRID_SIZE)
                distance += abs(row - target_row) + abs(col - target_col)

                # Verificar conflictos en filas
                if row == target_row:
                    for other_col in range(col + 1, GRID_SIZE):
                        other_value = board[row][other_col]
                        if other_value != 0:
                            other_target_row, other_target_col = divmod(other_value - 1, GRID_SIZE)
                            if other_target_row == row and other_target_col < target_col:
                                linear_conflict += 1

                # Verificar conflictos en columnas
                if col == target_col:
                    for other_row in range(row + 1, GRID_SIZE):
                        other_value = board[other_row][col]
                        if other_value != 0:
                            other_target_row, other_target_col = divmod(other_value - 1, GRID_SIZE)
                            if other_target_col == col and other_target_row < target_row:
                                linear_conflict += 1

    return distance + 2 * linear_conflict

def a_star_solve(start_board, total_estimated_cost):
    def board_to_tuple(board):
        return tuple(tuple(row) for row in board)

    start_state = board_to_tuple(start_board)
    goal_state = board_to_tuple([[i + 1 if i < GRID_SIZE * GRID_SIZE - 1 else 0 for i in range(j * GRID_SIZE, (j + 1) * GRID_SIZE)] for j in range(GRID_SIZE)])

    open_set = []
    heapq.heappush(open_set, (0, start_state))
    came_from = {}
    g_score = {start_state: 0}
    f_score = {start_state: manhattan_distance_cached(start_state)}
    closed_set = set()

    while open_set:
        current_f, current_state = heapq.heappop(open_set)
        current_board = [list(row) for row in current_state]

        if current_state == goal_state:
            print("Solución encontrada.")
            return reconstruct_path(came_from, current_state)

        closed_set.add(current_state)

        empty_tile = find_empty_tile(current_board)
        for neighbor_pos in get_neighbors(empty_tile):
            new_board = [row[:] for row in current_board]
            swap_tiles(new_board, empty_tile, neighbor_pos)
            neighbor_state = board_to_tuple(new_board)
            tentative_g_score = g_score[current_state] + 1

            if neighbor_state in closed_set:
                continue

            if neighbor_state not in g_score or tentative_g_score < g_score[neighbor_state]:
                came_from[neighbor_state] = current_state
                g_score[neighbor_state] = tentative_g_score
                f_score_value = tentative_g_score + manhattan_distance_cached(neighbor_state)
                f_score[neighbor_state] = f_score_value
                heapq.heappush(open_set, (f_score_value, neighbor_state))

        # Actualizar solving_progress
        solving_progress = min(99, int((g_score[current_state] / total_estimated_cost) * 100))
    
    print("No se encontró solución.")
    return None

def reconstruct_path(came_from, current):
    total_path = [current]
    while current in came_from:
        current = came_from[current]
        total_path.append(current)
    return total_path[::-1]

def solve_puzzle(board):
    global solution, solving, solving_progress
    print("Iniciando resolución en hilo separado.")
    start_state = board_to_tuple(board)
    initial_estimate = manhattan_distance_cached(start_state)
    total_estimated_cost = initial_estimate * 3  # Factor de seguridad
    solution_path = a_star_solve(board, total_estimated_cost)
    if solution_path:
        solution = solution_path
        print(f"Solución con {len(solution)} pasos encontrada.")
        solving_progress = 100  # Establecer progreso al 100% cuando se encuentra la solución
    else:
        solution = []
        print("No se encontró solución.")
    solving = False  # Indica que se ha terminado de resolver

def main():
    global solving, solution, solution_step, last_update_time
    clock = pygame.time.Clock()
    board = create_initial_board()
    solve_button_rect = pygame.Rect(0, WINDOW_HEIGHT - 60, WINDOW_WIDTH, 60)
    solve_button = {
        'rect': solve_button_rect,
        'color': BUTTON_COLOR
    }
    button_text_display = "Resolver"

    # Variables para la animación de la solución
    solution = []
    solution_step = 0
    last_update_time = 0

    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                print("Saliendo del juego.")
                pygame.quit()
                sys.exit()
            elif event.type == pygame.MOUSEBUTTONDOWN and not solving:
                mouse_pos = pygame.mouse.get_pos()
                if solve_button['rect'].collidepoint(mouse_pos):
                    # Iniciar resolución
                    print("Botón 'Resolver' clickeado.")
                    solving = True
                    button_text_display = "Resolviendo 0%"
                    solve_button['color'] = BUTTON_SOLVING_COLOR
                    solution = []
                    solution_step = 0
                    last_update_time = pygame.time.get_ticks()
                    # Iniciar hilo para resolver
                    solve_thread = threading.Thread(target=solve_puzzle, args=(board,))
                    solve_thread.start()
                else:
                    # Manejar movimiento de las fichas
                    click_row, click_col = mouse_pos[1] // TILE_SIZE, mouse_pos[0] // TILE_SIZE
                    empty_tile = find_empty_tile(board)
                    if is_valid_move(empty_tile, (click_row, click_col)):
                        print(f"Mover ficha desde ({click_row}, {click_col}) a ({empty_tile[0]}, {empty_tile[1]}).")
                        swap_tiles(board, empty_tile, (click_row, click_col))
        
        current_time = pygame.time.get_ticks()

        # Si el hilo de resolución ha terminado y hay una solución, empezar a animarla
        if not solving and solution:
            print("Hilo de resolución completado. Iniciando animación de la solución.")
            # Iniciar la animación
            solving = True
            solution_step = 0
            last_update_time = current_time
        elif solving and solution:
            if solution_step < len(solution):
                if current_time - last_update_time >= STEP_DELAY:
                    # Aplicar el siguiente paso de la solución
                    current_state = solution[solution_step]
                    for i in range(GRID_SIZE):
                        board[i] = list(current_state[i])
                    solution_step += 1
                    # Actualizar el porcentaje de progreso
                    progress = int((solution_step / len(solution)) * 100)
                    button_text_display = f"Resolviendo {progress}%"
                    last_update_time = current_time
                    print(f"Animando paso {solution_step}/{len(solution)} ({progress}%).")
            else:
                # Animación completada
                print("Animación de solución completada.")
                solving = False
                button_text_display = "Resolver"
                solve_button['color'] = BUTTON_COLOR
                solution = []
                solution_step = 0

        # Dibujar el tablero y el botón
        draw_board(board, solve_button, button_text_display)
        pygame.display.flip()
        clock.tick(FPS)

if __name__ == '__main__':
    main()""