Una forma sencilla y rápida de aprender JAVA, observando y deduciendo cómo se comporta el lenguaje a través de ejemplos prácticos.

Archivo del blog

miércoles, 11 de junio de 2025

Proyecto Ajedrez II: Finalización del proyecto v1.

Llevamos el proyecto Ajedrez (ChessMate) al siguiente nivel con nuevas funcionalidades. Ahora, el motor de juego pasa de simples jugadas aleatorias a un sistema más sólido. Esta nueva versión incorpora una interfaz gráfica mejorada y con una "IA clásica" (de momento sin uso de redes neuronales), herramientas para guardar, revisar y visualizar partidas.

Características Principales

    Interfaz Gráfica y Jugabilidad:
        Tablero Interactivo: Permite mover las piezas mediante arrastrar y soltar (drag & drop) o con dos clics (origen y destino).
        Visualización Profesional: Utiliza una fuente personalizada para renderizar las piezas, dándole un aspecto limpio y clásico.
        Ayuda Visual: Resalta la pieza seleccionada y todos sus movimientos legales posibles, distinguiendo entre movimientos normales y capturas.
        Funciones de Usabilidad: Incluye un modo de concentración (oculta paneles laterales), la capacidad de rotar el tablero, atajos de teclado y soporte para pantalla completa.
        Pantalla de Bienvenida (Splash Screen): Muestra una pantalla de carga profesional al iniciar la aplicación.

    Inteligencia Artificial (IA):
        Algoritmo Avanzado: Implementa un motor de búsqueda Negamax con poda Alfa-Beta, una técnica eficiente y estándar en los motores de ajedrez para explorar el árbol de jugadas.
        Función de Evaluación Sofisticada: Su "inteligencia" se basa en una función que puntúa las posiciones considerando múltiples factores:
            Valor material de las piezas.
            Control del centro del tablero.
            Seguridad del Rey.
            Estructura de peones (doblados, aislados, pasados).
            Movilidad y actividad de las piezas.
        Dificultad Ajustable: El nivel de la IA (Fácil, Normal, Difícil, etc.) se controla ajustando la profundidad de búsqueda del algoritmo.

    Gestión de Partidas y PGN:
        Carga y Guardado: Capacidad completa para leer y escribir archivos .pgn.
        Visor/Revisor de Partidas: Al cargar un PGN o al finalizar una partida, se activa un modo de revisión con controles para navegar movimiento a movimiento (inicio, fin, siguiente, anterior) y una opción de reproducción automática.

    Cumplimiento de Reglas del Ajedrez:
        Reglas Especiales: Maneja correctamente todas las reglas especiales del ajedrez: enroque, captura al paso, y promoción de peones (permitiendo al usuario elegir la pieza).
        Detección de Tablas (Empate): Detecta correctamente los diferentes tipos de tablas: rey ahogado, regla de los 50 movimientos, triple repetición de posición y material insuficiente para dar jaque mate.


Organización de las Clases del Proyecto

1. Núcleo del Juego y Lógica (El Modelo)

Estas clases forman el "cerebro" del ajedrez. No tienen conocimiento de la interfaz gráfica y podrían funcionar en una aplicación de consola.

    MotorJuego.java: El Orquestador Central. Es la clase más importante de la lógica. Gestiona el flujo de la partida, los turnos, implementa el algoritmo de la IA (Negamax), y determina el estado del juego (jaque mate, tablas, etc.).
    Tablero.java: El Estado del Mundo. Representa el tablero de ajedrez, mantiene la posición de todas las piezas y gestiona las reglas complejas como los derechos de enroque y la captura al paso. Es la fuente de la verdad para cualquier posición.
    Pieza.java: El Prototipo de Pieza. Es la clase abstracta de la que heredan todas las demás piezas. Define el comportamiento común (ser blanca/negra, haber movido) y obliga a implementar la lógica de movimiento.
        Rey.java: Implementa la lógica de movimiento del Rey, incluyendo la validación del enroque.
        Dama.java: Implementa el movimiento combinado de Torre y Alfil.
        Torre.java: Implementa el movimiento ortogonal.
        Alfil.java: Implementa el movimiento diagonal.
        Caballo.java: Implementa el movimiento en "L".
        Peon.java: Implementa la lógica más compleja: avance de uno o dos pasos, captura diagonal, promoción y captura al paso.
    Movimiento.java: La Acción. Encapsula toda la información de un solo movimiento (origen, destino, pieza movida, pieza capturada) y contiene la lógica para ejecutarlo sobre el tablero.
    EstadoJuego.java: El Diccionario de Estados. Un enum que define todos los posibles estados de una partida (turno de las blancas, jaque, jaque mate, empate por ahogado, etc.), permitiendo una comunicación clara desde el motor hacia la interfaz.
    Posicion.java: La Coordenada. Un record simple pero fundamental que representa una casilla del tablero (ej. "e4"). Es usado por todas las demás clases del núcleo.


2. Interfaz Gráfica y Presentación (La Vista)

Estas clases son responsables de todo lo que el usuario ve en la pantalla.

    AjedrezGrafico.java: La Ventana Principal. Es el JFrame que contiene y organiza todos los demás componentes visuales. Actúa como el controlador principal, conectando las acciones del usuario (clics en botones) con la lógica del MotorJuego.
    PanelTablero.java: El Corazón Visual. Es el JPanel personalizado que dibuja el tablero, las piezas, los resaltados de movimientos y gestiona la interacción directa del usuario con el tablero (clics, arrastrar y soltar).
    SplashScreen.java: La Primera Impresión. Muestra una pantalla de bienvenida profesional mientras la aplicación se carga.
    ConfiguracionDialog.java: Permite al usuario configurar las opciones de la partida (modo de juego, dificultad de la IA).
    PromocionDialog.java: Se muestra cuando un peón llega al final del tablero, permitiendo al usuario elegir a qué pieza promocionar.
    ElegirLadoDialog.java: Permite al jugador elegir si quiere jugar con las piezas blancas o negras al inicio de una partida.
    InformacionDialog.java: Muestra información sobre la aplicación, como los atajos de teclado y detalles técnicos.
    ConfirmacionDialog.java: Un diálogo genérico para pedir confirmación al usuario antes de realizar acciones importantes (como salir o finalizar una partida).


3. Componentes de Soporte a la Interfaz (Ayudantes de la Vista)

Clases más pequeñas que realizan tareas muy específicas para mejorar la interfaz de usuario.

    GuiUtils.java: El Ayudante de Estilo. Una clase de utilidad para tareas repetitivas de la GUI, como cargar fuentes personalizadas y aplicar estilos consistentes a los botones.
    HistorialMovimientoRenderer (clase interna en AjedrezGrafico): Personaliza la apariencia de la tabla que muestra el historial de movimientos.



Código Java (MotorJuego.java):

package ajedrez_gui;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ajedrez_gui.AjedrezGrafico.ModoJuego;

public class MotorJuego {

    private static final Logger LOGGER = Logger.getLogger(MotorJuego.class.getName());

    private Tablero tablero;
    private boolean turnoBlancas;
    private int contador50Movimientos;
    private Map<String, Integer> historialPosiciones;
    private List<String> historialNotacion;
    private Movimiento movimientoPendientePromocion;
    private EstadoJuego estadoActual;
    private Tablero tableroAntesPromocion;
    private boolean turnoBlancasAntesPromocion;
    private int contador50MovimientosAntesPromocion;
    private Map<String, Integer> historialPosicionesAntesPromocion;
    private List<String> historialNotacionAntesPromocion;

    private int profundidadBusqueda = 4;

    private static final int VALOR_PEON = 100;
    private static final int VALOR_CABALLO = 320;
    private static final int VALOR_ALFIL = 330;
    private static final int VALOR_TORRE = 500;
    private static final int VALOR_DAMA = 900;
    private static final int VALOR_REY = 20000;
    private static final int CENTRO_PEON_PUNTOS = 10;
    private static final int CENTRO_PIEZA_MENOR_PUNTOS = 10;
    private static final int SEMI_CENTRO_PUNTOS = 5;
    private static final int DESARROLLO_PIEZA_MENOR_PUNTOS = 10;
    private static final int MOVILIDAD_POR_MOVIMIENTO_PUNTOS = 1;
    private static final int ESCUDO_PEON_REY_PUNTOS = 8;
    private static final int REY_EXPUESTO_PENALIZACION = -30;
    private static final int PEON_PASADO_PUNTOS_BASE = 20;
    private static final int PEON_AISLADO_PENALIZACION = -10;
    private static final int PEON_DOBLADO_PENALIZACION = -15;
    private static final int TORRE_COLUMNA_ABIERTA_PUNTOS = 15;
    private static final int TORRE_COLUMNA_SEMIABIERTA_PUNTOS = 10;
    private static final int TORRE_SEPTIMA_FILA_PUNTOS = 10;

    public MotorJuego() {
        iniciarNuevaPartida();
    }

    public final void iniciarNuevaPartida() {
        tablero = new Tablero();
        turnoBlancas = true;
        contador50Movimientos = 0;
        historialPosiciones = new HashMap<>();
        historialNotacion = new ArrayList<>();
        movimientoPendientePromocion = null;

        tableroAntesPromocion = null;
        turnoBlancasAntesPromocion = false;
        contador50MovimientosAntesPromocion = 0;
        historialPosicionesAntesPromocion = null;
        historialNotacionAntesPromocion = null;

        registrarPosicionActual();
        actualizarEstadoJuego(true);
        LOGGER.log(Level.INFO, "MotorJuego: Nueva partida iniciada. Estado: " + estadoActual);
    }

    public void setProfundidadBusqueda(int profundidad) {
        if (profundidad < 1) {
            LOGGER.log(Level.WARNING, "Intento de establecer profundidad de búsqueda inválida: " + profundidad + ". Usando 1.");
            this.profundidadBusqueda = 1;
        } else {
            this.profundidadBusqueda = profundidad;
        }
        LOGGER.log(Level.INFO, "MotorJuego: Profundidad de búsqueda establecida a " + this.profundidadBusqueda);
    }

    public int getProfundidadBusquedaActual() {
        return this.profundidadBusqueda;
    }

    public Tablero getTablero() {
        return tablero;
    }

    public boolean esTurnoBlancas() {
        return turnoBlancas;
    }

    public EstadoJuego getEstadoActual() {
        return estadoActual;
    }

    public List<String> getHistorialNotacion() {
        return new ArrayList<>(historialNotacion);
    }

    public List<Movimiento> getMovimientosLegales() {
        if (estadoActual != null && (estadoActual.esFinDePartida() || estadoActual == EstadoJuego.PROMOCION_REQUIERIDA)) {
            return Collections.emptyList();
        }
        return tablero.generarMovimientosLegales(turnoBlancas);
    }

    public EstadoJuego intentarMover(Posicion origen, Posicion destino) {
        if (estadoActual != null && estadoActual.esFinDePartida() && estadoActual != EstadoJuego.PROMOCION_REQUIERIDA) {
            LOGGER.log(Level.WARNING, "MotorJuego: Intento de mover en estado de fin de partida: " + estadoActual);
            return estadoActual;
        }
        if (estadoActual == EstadoJuego.PROMOCION_REQUIERIDA && (movimientoPendientePromocion == null || !movimientoPendientePromocion.getDestino().equals(origen))) {
            LOGGER.log(Level.WARNING, "MotorJuego: Intento de mover mientras se requiere promoción.");
            return EstadoJuego.PROMOCION_REQUIERIDA;
        }

        if (origen == null || destino == null || !origen.esValida() || !destino.esValida()) {
            LOGGER.log(Level.WARNING, "MotorJuego: Coordenadas inválidas: " + origen + "->" + destino);
            return EstadoJuego.MOVIMIENTO_INVALIDO;
        }

        Pieza piezaEnOrigen = tablero.obtenerPieza(origen);
        if (piezaEnOrigen == null || piezaEnOrigen.esBlanca() != turnoBlancas) {
            LOGGER.log(Level.INFO, "MotorJuego: Intento de mover pieza incorrecta o casilla vacía: " + origen);
            return EstadoJuego.MOVIMIENTO_INVALIDO;
        }

        List<Movimiento> movimientosLegales = tablero.generarMovimientosLegales(turnoBlancas);
        Movimiento movimientoSeleccionado = encontrarMovimientoEnLista(movimientosLegales, origen, destino);

        if (movimientoSeleccionado == null) {
            LOGGER.log(Level.INFO, "MotorJuego: Movimiento ilegal: " + origen + "->" + destino);
            return EstadoJuego.MOVIMIENTO_INVALIDO;
        }

        Pieza piezaMovidaOriginal = movimientoSeleccionado.getPiezaMovida();
        Pieza piezaCapturada = movimientoSeleccionado.getPiezaCapturada();

        if (movimientoSeleccionado.esPromocion()) {
            LOGGER.log(Level.INFO, "MotorJuego: Movimiento legal requiere promoción: " + origen + "->" + destino);
            guardarEstadoAntesPromocion();
            movimientoSeleccionado.ejecutar();
            tablero.setEnPassant(movimientoSeleccionado.obtenerNuevoEnPassant());
            this.movimientoPendientePromocion = movimientoSeleccionado;
            estadoActual = EstadoJuego.PROMOCION_REQUIERIDA;
            return estadoActual;
        } else {
            movimientoSeleccionado.ejecutar();
            tablero.setEnPassant(movimientoSeleccionado.obtenerNuevoEnPassant());
            boolean turnoDelQueMovio = this.turnoBlancas;
            this.turnoBlancas = !this.turnoBlancas;
            finalizarTurno(movimientoSeleccionado, piezaMovidaOriginal, piezaCapturada, null, turnoDelQueMovio);
            return estadoActual;
        }
    }

    private void guardarEstadoAntesPromocion() {
        tableroAntesPromocion = tablero.copiar();
        turnoBlancasAntesPromocion = turnoBlancas;
        contador50MovimientosAntesPromocion = contador50Movimientos;
        historialPosicionesAntesPromocion = new HashMap<>(historialPosiciones);
        historialNotacionAntesPromocion = new ArrayList<>(historialNotacion);
        LOGGER.log(Level.FINE, "Estado del motor guardado antes de promoción. Turno del que promociona: " + (turnoBlancasAntesPromocion ? "Blancas" : "Negras"));
    }

    private void restaurarEstadoAntesPromocion() {
        if (tableroAntesPromocion != null) {
            tablero = tableroAntesPromocion;
            turnoBlancas = turnoBlancasAntesPromocion;
            contador50Movimientos = contador50MovimientosAntesPromocion;
            historialPosiciones = historialPosicionesAntesPromocion;
            historialNotacion = historialNotacionAntesPromocion;
            movimientoPendientePromocion = null;
            tableroAntesPromocion = null;
            historialPosicionesAntesPromocion = null;
            historialNotacionAntesPromocion = null;
            actualizarEstadoJuego(false);
            LOGGER.log(Level.INFO, "Estado del motor restaurado después de cancelación de promoción. Nuevo estado: " + estadoActual);
        } else {
            LOGGER.log(Level.WARNING, "Intento de restaurar estado antes de promoción, pero no hay estado guardado.");
        }
    }

    public void cancelarPromocionPendiente() {
        if (estadoActual == EstadoJuego.PROMOCION_REQUIERIDA && movimientoPendientePromocion != null) {
            LOGGER.log(Level.INFO, "Cancelando promoción pendiente.");
            restaurarEstadoAntesPromocion();
        } else {
            LOGGER.log(Level.WARNING, "cancelarPromocionPendiente llamado en estado incorrecto.");
        }
    }

    public EstadoJuego completarPromocion(char tipoPieza) {
        if (estadoActual != EstadoJuego.PROMOCION_REQUIERIDA || movimientoPendientePromocion == null) {
            LOGGER.log(Level.SEVERE, "MotorJuego: completarPromocion llamado incorrectamente. Estado: " + estadoActual);
            if (tableroAntesPromocion != null) {
                restaurarEstadoAntesPromocion();
                return EstadoJuego.ERROR_INTERNO;
            }
            return estadoActual;
        }

        boolean esPiezaPromocionadaBlanca = turnoBlancasAntesPromocion;

        Posicion destinoPromocion = movimientoPendientePromocion.getDestino();
        char piezaCharMayus = Character.toUpperCase(tipoPieza);

        if (piezaCharMayus != 'R' && piezaCharMayus != 'B' && piezaCharMayus != 'N' && piezaCharMayus != 'Q') {
            LOGGER.log(Level.WARNING, "MotorJuego: Pieza de promoción inválida: " + tipoPieza + ". Cancelando promoción.");
            cancelarPromocionPendiente();
            return EstadoJuego.MOVIMIENTO_INVALIDO;
        }

        Pieza nuevaPieza = switch (piezaCharMayus) {
            case 'R' ->
                new Torre(esPiezaPromocionadaBlanca);
            case 'B' ->
                new Alfil(esPiezaPromocionadaBlanca);
            case 'N' ->
                new Caballo(esPiezaPromocionadaBlanca);
            case 'Q' ->
                new Dama(esPiezaPromocionadaBlanca);
            default ->
                throw new IllegalStateException("Tipo de pieza de promoción inesperado: " + piezaCharMayus);
        };

        tablero.setPieza(destinoPromocion, nuevaPieza);
        LOGGER.log(Level.INFO, "MotorJuego: Peón en " + destinoPromocion + " promocionado a "
                + nuevaPieza.getClass().getSimpleName() + " (" + piezaCharMayus + ")"
                + " de color " + (esPiezaPromocionadaBlanca ? "BLANCO" : "NEGRO"));

        boolean turnoDelQuePromociono = this.turnoBlancasAntesPromocion;
        this.turnoBlancas = !this.turnoBlancasAntesPromocion;

        finalizarTurno(
                movimientoPendientePromocion,
                movimientoPendientePromocion.getPiezaMovida(),
                movimientoPendientePromocion.getPiezaCapturada(),
                piezaCharMayus,
                turnoDelQuePromociono
        );

        movimientoPendientePromocion = null;
        tableroAntesPromocion = null;
        historialPosicionesAntesPromocion = null;
        historialNotacionAntesPromocion = null;

        return estadoActual;
    }

    private void finalizarTurno(Movimiento movimientoRealizado, Pieza piezaMovidaOriginal, Pieza piezaCapturada, Character promocionChar, boolean turnoDelJugadorQueMovio) {
        boolean oponenteEnJaque = tablero.estaEnJaque(this.turnoBlancas);
        List<Movimiento> movimientosOponente = tablero.generarMovimientosLegales(this.turnoBlancas);
        boolean esJaqueMate = oponenteEnJaque && movimientosOponente.isEmpty();

        String sufijoNotacion = "";
        if (esJaqueMate) {
            sufijoNotacion = "#";
        } else if (oponenteEnJaque) {
            sufijoNotacion = "+";
        }

        if (piezaMovidaOriginal instanceof Peon || piezaCapturada != null) {
            contador50Movimientos = 0;
            historialPosiciones.clear();
            LOGGER.log(Level.FINE, "Contador de 50 movimientos y historial FEN reseteados.");
        } else {
            contador50Movimientos++;
        }

        String notacionFinal;
        if (movimientoRealizado.esEnroqueCorto()) {
            notacionFinal = "O-O" + sufijoNotacion;
        } else if (movimientoRealizado.esEnroqueLargo()) {
            notacionFinal = "O-O-O" + sufijoNotacion;
        } else {
            Posicion origen = movimientoRealizado.getOrigen();
            Posicion destino = movimientoRealizado.getDestino();
            String promoStr = (promocionChar != null) ? "=" + Character.toUpperCase(promocionChar) : "";
            notacionFinal = origen.toString() + destino.toString() + promoStr + sufijoNotacion;
        }

        historialNotacion.add(notacionFinal);
        LOGGER.log(Level.INFO, "MotorJuego: Movimiento registrado: " + notacionFinal + " por " + (turnoDelJugadorQueMovio ? "Blancas" : "Negras"));

        registrarPosicionActual();
        actualizarEstadoJuego(false);
    }

    private void actualizarEstadoJuego(boolean esInicioPartida) {
        List<Movimiento> movimientosLegales = tablero.generarMovimientosLegales(turnoBlancas);
        boolean reyActualEnJaque = tablero.estaEnJaque(turnoBlancas);

        if (movimientosLegales.isEmpty()) {
            estadoActual = reyActualEnJaque
                    ? (turnoBlancas ? EstadoJuego.JAQUEMATE_GANA_NEGRAS : EstadoJuego.JAQUEMATE_GANA_BLANCAS)
                    : EstadoJuego.AHOGADO;
        } else if (contador50Movimientos >= 100) {
            estadoActual = EstadoJuego.EMPATE_50_MOV;
        } else if (historialPosiciones.getOrDefault(tablero.getPosicionHash(turnoBlancas), 0) >= 3) {
            estadoActual = EstadoJuego.EMPATE_TRIPLE_REP;
        } else if (tablero.esMaterialInsuficiente()) {
            estadoActual = EstadoJuego.EMPATE_MATERIAL_INSUF;
        } else {
            if (esInicioPartida && turnoBlancas) {
                estadoActual = EstadoJuego.TURNO_BLANCAS;
            } else {
                estadoActual = reyActualEnJaque
                        ? (turnoBlancas ? EstadoJuego.JAQUE_BLANCAS : EstadoJuego.JAQUE_NEGRAS)
                        : (turnoBlancas ? EstadoJuego.TURNO_BLANCAS : EstadoJuego.TURNO_NEGRAS);
            }
        }
        LOGGER.log(Level.INFO, "MotorJuego: Nuevo estado calculado: " + estadoActual + " (Turno para: " + (turnoBlancas ? "Blancas" : "Negras") + ")");
    }

    private Movimiento encontrarMovimientoEnLista(List<Movimiento> movimientos, Posicion origen, Posicion destino) {
        if (movimientos == null || origen == null || destino == null) {
            return null;
        }
        for (var mov : movimientos) {
            if (mov.getOrigen().equals(origen) && mov.getDestino().equals(destino)) {
                return mov;
            }
        }
        return null;
    }

    private void registrarPosicionActual() {
        String hash = tablero.getPosicionHash(turnoBlancas);
        historialPosiciones.put(hash, historialPosiciones.getOrDefault(hash, 0) + 1);
        LOGGER.log(Level.FINER, "Posición registrada para hash: " + hash + " Count: " + historialPosiciones.get(hash) + " Turno: " + (turnoBlancas ? "B" : "N"));
    }

    public EstadoJuego realizarMovimientoCPU() {
        if (estadoActual.esFinDePartida()) {
            LOGGER.log(Level.WARNING, "CPU intentó mover en estado de fin de partida: " + estadoActual);
            return estadoActual;
        }

        List<Movimiento> movimientosLegales = getMovimientosLegales();
        if (movimientosLegales.isEmpty()) {
            LOGGER.log(Level.WARNING, "MotorJuego WARN: CPU (" + (turnoBlancas ? "Blancas" : "Negras") + ") sin movimientos legales. Estado: " + estadoActual);
            actualizarEstadoJuego(false);
            return estadoActual;
        }

        LOGGER.log(Level.INFO, "MotorJuego: CPU (Turno: " + (turnoBlancas ? "Blancas" : "Negras")
                + ") calculando mejor movimiento con Negamax (Profundidad: " + profundidadBusqueda + ")");

        Movimiento mejorMovimiento = null;
        int mejorValor = Integer.MIN_VALUE;

        movimientosLegales.sort(Comparator
                .comparing((Movimiento mov) -> mov.getPiezaCapturada() != null || mov.esCapturaEnPassant(), Comparator.reverseOrder())
                .thenComparing(Movimiento::esPromocion, Comparator.reverseOrder())
        );

        boolean turnoCpuOriginal = this.turnoBlancas;

        for (Movimiento mov : movimientosLegales) {
            Tablero simulatedTablero = this.tablero.copiar();
            int simulatedContador50 = this.contador50Movimientos;
            Map<String, Integer> simulatedHistorialPos = new HashMap<>(this.historialPosiciones);

            Movimiento movSimulado = new Movimiento(simulatedTablero, mov.getOrigen(), mov.getDestino());
            Pieza piezaMovidaOriginalSim = simulatedTablero.obtenerPieza(movSimulado.getOrigen());
            Pieza piezaCapturadaSim = movSimulado.getPiezaCapturada();

            movSimulado.ejecutar();
            simulatedTablero.setEnPassant(movSimulado.obtenerNuevoEnPassant());

            if (movSimulado.esPromocion()) {
                simulatedTablero.setPieza(movSimulado.getDestino(), new Dama(turnoCpuOriginal));
            }

            if (piezaMovidaOriginalSim instanceof Peon || piezaCapturadaSim != null) {
                simulatedContador50 = 0;
                simulatedHistorialPos.clear();
            } else {
                simulatedContador50++;
            }

            boolean turnoParaSiguienteNegamax = !turnoCpuOriginal;
            String simulatedHash = simulatedTablero.getPosicionHash(turnoParaSiguienteNegamax);
            simulatedHistorialPos.put(simulatedHash, simulatedHistorialPos.getOrDefault(simulatedHash, 0) + 1);

            int valorMovimiento = -negamax(simulatedTablero, turnoParaSiguienteNegamax, simulatedContador50, simulatedHistorialPos,
                    profundidadBusqueda - 1, -Integer.MAX_VALUE, Integer.MAX_VALUE);

            LOGGER.log(Level.FINER, "MotorJuego: Movimiento " + mov + " evaluado dengan valor: " + valorMovimiento);

            if (valorMovimiento > mejorValor) {
                mejorValor = valorMovimiento;
                mejorMovimiento = mov;
            }
        }

        if (mejorMovimiento == null) {
            LOGGER.log(Level.SEVERE, "MotorJuego: Error CRÍTICO: Negamax no encontró ningún movimiento aunque había opciones.");
            mejorMovimiento = movimientosLegales.isEmpty() ? null : movimientosLegales.get(0);
            if (mejorMovimiento == null) {
                return estadoActual;
            }
        }

        LOGGER.log(Level.INFO, "MotorJuego: Mejor movimiento encontrado por Negamax: " + mejorMovimiento + " con valor: " + mejorValor);

        intentarMover(mejorMovimiento.getOrigen(), mejorMovimiento.getDestino());

        if (this.estadoActual == EstadoJuego.PROMOCION_REQUIERIDA) {
            LOGGER.log(Level.INFO, "MotorJuego: CPU (movimiento " + mejorMovimiento + ") necesita completar promoción.");
            completarPromocion('Q');
        }
        return estadoActual;
    }

    private int negamax(Tablero currentTablero, boolean esTurnoBlancasActual, int currentContador50,
            Map<String, Integer> currentHistorialPos,
            int depth, int alpha, int beta) {

        String currentHashForRepetition = currentTablero.getPosicionHash(esTurnoBlancasActual);
        if (currentHistorialPos.getOrDefault(currentHashForRepetition, 0) >= 3) {
            return 0;
        }
        if (currentContador50 >= 100) {
            return 0;
        }
        if (currentTablero.esMaterialInsuficiente()) {
            return 0;
        }

        if (depth == 0) {
            return evaluarPosicion(currentTablero, currentContador50, currentHistorialPos, esTurnoBlancasActual);
        }

        List<Movimiento> movimientosLegales = currentTablero.generarMovimientosLegales(esTurnoBlancasActual);

        if (movimientosLegales.isEmpty()) {
            return evaluarPosicion(currentTablero, currentContador50, currentHistorialPos, esTurnoBlancasActual);
        }

        movimientosLegales.sort(Comparator
                .comparing((Movimiento mov) -> mov.getPiezaCapturada() != null || mov.esCapturaEnPassant(), Comparator.reverseOrder())
                .thenComparing(Movimiento::esPromocion, Comparator.reverseOrder())
        );

        int maxValor = Integer.MIN_VALUE;

        for (Movimiento mov : movimientosLegales) {
            Tablero nextTablero = currentTablero.copiar();
            int nextContador50 = currentContador50;
            Map<String, Integer> nextHistorialPos = new HashMap<>(currentHistorialPos);

            Movimiento movSimulado = new Movimiento(nextTablero, mov.getOrigen(), mov.getDestino());
            Pieza piezaMovidaOriginalSim = nextTablero.obtenerPieza(movSimulado.getOrigen());
            Pieza piezaCapturadaSim = movSimulado.getPiezaCapturada();

            movSimulado.ejecutar();
            nextTablero.setEnPassant(movSimulado.obtenerNuevoEnPassant());

            if (movSimulado.esPromocion()) {
                nextTablero.setPieza(movSimulado.getDestino(), new Dama(esTurnoBlancasActual));
            }

            if (piezaMovidaOriginalSim instanceof Peon || piezaCapturadaSim != null) {
                nextContador50 = 0;
                nextHistorialPos.clear();
            } else {
                nextContador50++;
            }

            boolean turnoParaSiguienteRecursion = !esTurnoBlancasActual;
            String nextHash = nextTablero.getPosicionHash(turnoParaSiguienteRecursion);
            nextHistorialPos.put(nextHash, nextHistorialPos.getOrDefault(nextHash, 0) + 1);

            int valorActual = -negamax(nextTablero, turnoParaSiguienteRecursion, nextContador50, nextHistorialPos,
                    depth - 1, -beta, -alpha);

            if (valorActual > maxValor) {
                maxValor = valorActual;
            }
            if (maxValor > alpha) {
                alpha = maxValor;
            }
            if (alpha >= beta) {
                LOGGER.log(Level.FINEST, "Poda Alfa-Beta en profundidad " + depth + ". Alpha: " + alpha + ", Beta: " + beta + ". Cortando.");
                break;
            }
        }
        return maxValor;
    }

    private int evaluarPosicion(Tablero tableroEval, int contador50Mov, Map<String, Integer> historialPos, boolean esTurnoBlancasEval) {
        List<Movimiento> movimientosLegalesParaJugadorActual = tableroEval.generarMovimientosLegales(esTurnoBlancasEval);
        boolean reyDelJugadorActualEnJaque = tableroEval.estaEnJaque(esTurnoBlancasEval);

        if (movimientosLegalesParaJugadorActual.isEmpty()) {
            if (reyDelJugadorActualEnJaque) {
                return -(VALOR_REY - (profundidadBusqueda * 100));
            } else {
                return 0;
            }
        }

        int puntuacionMaterial = 0;
        int puntuacionPosicionalBlancas = 0;
        int puntuacionPosicionalNegras = 0;

        Posicion posReyBlanco = tableroEval.encontrarRey(true);
        Posicion posReyNegro = tableroEval.encontrarRey(false);

        if (posReyBlanco != null) {
            puntuacionPosicionalBlancas += evaluarSeguridadRey(posReyBlanco, true, tableroEval);
        }
        if (posReyNegro != null) {
            puntuacionPosicionalNegras += evaluarSeguridadRey(posReyNegro, false, tableroEval);
        }

        puntuacionPosicionalBlancas += evaluarMovilidad(tableroEval.generarMovimientosLegales(true));
        puntuacionPosicionalNegras += evaluarMovilidad(tableroEval.generarMovimientosLegales(false));

        for (int r = 0; r < Tablero.TAMANO_TABLERO; r++) {
            for (int c = 0; c < Tablero.TAMANO_TABLERO; c++) {
                Posicion pos = new Posicion(r, c);
                Pieza pieza = tableroEval.obtenerPieza(pos);

                if (pieza != null) {
                    int valorPiezaBase = switch (pieza) {
                        case Peon p ->
                            VALOR_PEON;
                        case Caballo k ->
                            VALOR_CABALLO;
                        case Alfil b ->
                            VALOR_ALFIL;
                        case Torre t ->
                            VALOR_TORRE;
                        case Dama q ->
                            VALOR_DAMA;
                        case Rey k ->
                            0;
                    };

                    if (pieza.esBlanca()) {
                        puntuacionMaterial += valorPiezaBase;
                        puntuacionPosicionalBlancas += evaluarPosicionPieza(pieza, pos, tableroEval, true, posReyBlanco);
                    } else {
                        puntuacionMaterial -= valorPiezaBase;
                        puntuacionPosicionalNegras += evaluarPosicionPieza(pieza, pos, tableroEval, false, posReyNegro);
                    }
                }
            }
        }

        int puntuacionTotalPerspectivaBlanca = puntuacionMaterial + (puntuacionPosicionalBlancas - puntuacionPosicionalNegras);
        return esTurnoBlancasEval ? puntuacionTotalPerspectivaBlanca : -puntuacionTotalPerspectivaBlanca;
    }

    private int evaluarPosicionPieza(Pieza pieza, Posicion pos, Tablero tablero, boolean esBlanca, Posicion posReyAmigo) {
        int puntuacion = 0;
        puntuacion += evaluarControlCentro(pieza, pos);
        if (pieza instanceof Caballo || pieza instanceof Alfil) {
            puntuacion += evaluarDesarrollo((pieza instanceof Caballo ? 'N' : 'B'), pos, esBlanca);
        }
        if (pieza instanceof Peon peon) {
            puntuacion += evaluarEstructuraPeon(peon, pos, tablero);
        }
        if (pieza instanceof Torre torre) {
            puntuacion += evaluarActividadTorre(torre, pos, tablero);
        }
        return puntuacion;
    }

    private int evaluarControlCentro(Pieza pieza, Posicion pos) {
        int puntos = 0;
        int r = pos.x();
        int c = pos.y();
        if ((r == 3 || r == 4) && (c == 3 || c == 4)) {
            puntos += (pieza instanceof Peon) ? CENTRO_PEON_PUNTOS : CENTRO_PIEZA_MENOR_PUNTOS;
        } else if (((r >= 2 && r <= 5) && (c >= 2 && c <= 5))) {
            puntos += SEMI_CENTRO_PUNTOS;
        }
        return puntos;
    }

    private int evaluarDesarrollo(char tipoPieza, Posicion pos, boolean esBlanca) {
        boolean enCasillaInicial = false;
        if (tipoPieza == 'N') {
            if (esBlanca && (pos.equals(new Posicion(Tablero.FILA_INICIAL_BLANCAS, 1)) || pos.equals(new Posicion(Tablero.FILA_INICIAL_BLANCAS, 6)))) {
                enCasillaInicial = true;
            }
            if (!esBlanca && (pos.equals(new Posicion(Tablero.FILA_INICIAL_NEGRAS, 1)) || pos.equals(new Posicion(Tablero.FILA_INICIAL_NEGRAS, 6)))) {
                enCasillaInicial = true;
            }
        } else if (tipoPieza == 'B') {
            if (esBlanca && (pos.equals(new Posicion(Tablero.FILA_INICIAL_BLANCAS, 2)) || pos.equals(new Posicion(Tablero.FILA_INICIAL_BLANCAS, 5)))) {
                enCasillaInicial = true;
            }
            if (!esBlanca && (pos.equals(new Posicion(Tablero.FILA_INICIAL_NEGRAS, 2)) || pos.equals(new Posicion(Tablero.FILA_INICIAL_NEGRAS, 5)))) {
                enCasillaInicial = true;
            }
        }
        return !enCasillaInicial ? DESARROLLO_PIEZA_MENOR_PUNTOS : 0;
    }

    private int evaluarMovilidad(List<Movimiento> movimientos) {
        return movimientos.size() * MOVILIDAD_POR_MOVIMIENTO_PUNTOS;
    }

    private int evaluarSeguridadRey(Posicion posRey, boolean esBlanco, Tablero tablero) {
        if (posRey == null) {
            return 0;
        }
        int puntuacionSeguridad = 0;
        int peonesEscudo = 0;
        int direccionAvancePeonAmigo = esBlanco ? 1 : -1;

        Posicion[] casillasEscudoRelativas = {
            new Posicion(direccionAvancePeonAmigo, -1),
            new Posicion(direccionAvancePeonAmigo, 0),
            new Posicion(direccionAvancePeonAmigo, 1)
        };

        for (Posicion rel : casillasEscudoRelativas) {
            Posicion posEscudo = new Posicion(posRey.x() + rel.x(), posRey.y() + rel.y());
            if (posEscudo.esValida()) {
                Pieza p = tablero.obtenerPieza(posEscudo);
                if (p instanceof Peon && p.esBlanca() == esBlanco) {
                    peonesEscudo++;
                }
            }
        }
        puntuacionSeguridad += peonesEscudo * ESCUDO_PEON_REY_PUNTOS;

        boolean columnaTotalmenteAbierta = true;
        boolean columnaSemiAbiertaParaEnemigo = true;

        for (int r = 0; r < Tablero.TAMANO_TABLERO; ++r) {
            Pieza pEnCol = tablero.obtenerPieza(new Posicion(r, posRey.y()));
            if (pEnCol instanceof Peon) {
                columnaTotalmenteAbierta = false;
                if (pEnCol.esBlanca() == esBlanco) {
                    columnaSemiAbiertaParaEnemigo = false;
                }
            }
        }

        if (columnaTotalmenteAbierta || columnaSemiAbiertaParaEnemigo) {
            for (int r = 0; r < Tablero.TAMANO_TABLERO; r++) {
                Pieza pAtacante = tablero.obtenerPieza(new Posicion(r, posRey.y()));
                if (pAtacante != null && pAtacante.esBlanca() != esBlanco
                        && (pAtacante instanceof Torre || pAtacante instanceof Dama)) {
                    if (pAtacante.esCaminoLibreRecto(new Posicion(r, posRey.y()), posRey, tablero)) {
                        puntuacionSeguridad += REY_EXPUESTO_PENALIZACION;
                    }
                }
            }
        }

        if (peonesEscudo == 0 && (columnaTotalmenteAbierta || columnaSemiAbiertaParaEnemigo)) {
            puntuacionSeguridad += REY_EXPUESTO_PENALIZACION / 2;
        }
        return puntuacionSeguridad;
    }

    private int evaluarEstructuraPeon(Peon peon, Posicion pos, Tablero tablero) {
        int puntuacion = 0;
        boolean esBlanco = peon.esBlanca();
        int r = pos.x();
        int c = pos.y();
        int direccionAvance = esBlanco ? 1 : -1;

        boolean esPasado = true;
        for (int filaScan = r + direccionAvance; (esBlanco ? (filaScan < Tablero.TAMANO_TABLERO) : (filaScan >= 0)); filaScan += direccionAvance) {
            for (int colOffset = -1; colOffset <= 1; colOffset++) {
                int colScan = c + colOffset;
                if (colScan >= 0 && colScan < Tablero.TAMANO_TABLERO) {
                    Pieza pEnemigo = tablero.obtenerPieza(new Posicion(filaScan, colScan));
                    if (pEnemigo instanceof Peon && pEnemigo.esBlanca() != esBlanco) {
                        esPasado = false;
                        break;
                    }
                }
            }
            if (!esPasado) {
                break;
            }
        }
        if (esPasado) {
            int filasAvanzadas = esBlanco ? r - Tablero.FILA_PEONES_BLANCOS : Tablero.FILA_PEONES_NEGROS - r;
            puntuacion += PEON_PASADO_PUNTOS_BASE + (filasAvanzadas * (filasAvanzadas + 1));
        }

        boolean aislado = true;
        for (int colOffset = -1; colOffset <= 1; colOffset += 2) {
            int colAdyacente = c + colOffset;
            if (colAdyacente >= 0 && colAdyacente < Tablero.TAMANO_TABLERO) {
                for (int filaScan = 0; filaScan < Tablero.TAMANO_TABLERO; filaScan++) {
                    Pieza pAmigo = tablero.obtenerPieza(new Posicion(filaScan, colAdyacente));
                    if (pAmigo instanceof Peon && pAmigo.esBlanca() == esBlanco) {
                        aislado = false;
                        break;
                    }
                }
            }
            if (!aislado) {
                break;
            }
        }
        if (aislado) {
            puntuacion += PEON_AISLADO_PENALIZACION;
        }

        boolean doblado = false;
        for (int filaScan = 0; filaScan < Tablero.TAMANO_TABLERO; filaScan++) {
            if (filaScan == r) {
                continue;
            }
            Pieza pAmigoEnColumna = tablero.obtenerPieza(new Posicion(filaScan, c));
            if (pAmigoEnColumna instanceof Peon && pAmigoEnColumna.esBlanca() == esBlanco) {
                doblado = true;
                break;
            }
        }
        if (doblado) {
            puntuacion += PEON_DOBLADO_PENALIZACION;
        }

        return puntuacion;
    }

    private int evaluarActividadTorre(Torre torre, Posicion pos, Tablero tablero) {
        int puntuacion = 0;
        int rPos = pos.x();
        int cPos = pos.y();

        boolean columnaTotalmenteAbierta = true;
        boolean columnaSemiAbiertaParaTorre = true;
        for (int rScan = 0; rScan < Tablero.TAMANO_TABLERO; rScan++) {
            Pieza pEnColumna = tablero.obtenerPieza(new Posicion(rScan, cPos));
            if (pEnColumna instanceof Peon) {
                columnaTotalmenteAbierta = false;
                if (pEnColumna.esBlanca() == torre.esBlanca()) {
                    columnaSemiAbiertaParaTorre = false;
                }
            }
        }
        if (columnaTotalmenteAbierta) {
            puntuacion += TORRE_COLUMNA_ABIERTA_PUNTOS;
        } else if (columnaSemiAbiertaParaTorre) {
            puntuacion += TORRE_COLUMNA_SEMIABIERTA_PUNTOS;
        }

        int filaAtaqueOponente = torre.esBlanca() ? Tablero.FILA_PEONES_NEGROS : Tablero.FILA_PEONES_BLANCOS;
        if (rPos == filaAtaqueOponente) {
            puntuacion += TORRE_SEPTIMA_FILA_PUNTOS;
        }
        return puntuacion;
    }

    public List<String> parsearMovimientosDesdePGN(List<String> lineasPGN) throws IllegalArgumentException {
        List<String> movimientosPartida = new ArrayList<>();
        StringBuilder sbMovimientos = new StringBuilder();
        boolean dentroDeComentarioMultiLinea = false;

        for (String linea : lineasPGN) {
            linea = linea.trim();
            if (linea.isEmpty() || linea.startsWith("[")) {
                continue;
            }

            linea = linea.replaceAll("\\{.*?\\}", "");
            linea = linea.replaceAll(";.*", "");

            for (char c : linea.toCharArray()) {
                if (c == '(') {
                    dentroDeComentarioMultiLinea = true;
                    continue;
                }
                if (c == ')') {
                    dentroDeComentarioMultiLinea = false;
                    continue;
                }
                if (!dentroDeComentarioMultiLinea) {
                    sbMovimientos.append(c);
                }
            }
            if (!dentroDeComentarioMultiLinea) {
                sbMovimientos.append(" ");
            }
        }

        String textoMovimientos = sbMovimientos.toString().trim();
        textoMovimientos = textoMovimientos.replaceAll("\\s*(1-0|0-1|1/2-1/2|\\*)$", "").trim();
        textoMovimientos = textoMovimientos.replaceAll("\\b\\d+\\.{1,3}\\s*", " ");
        textoMovimientos = textoMovimientos.replaceAll("\\s+", " ").trim();

        if (textoMovimientos.isEmpty()) {
            return movimientosPartida;
        }

        String[] tokens = textoMovimientos.split("\\s+");
        for (String token : tokens) {
            token = token.trim();
            if (!token.isEmpty()) {
                movimientosPartida.add(token);
            }
        }

        if (movimientosPartida.isEmpty() && !lineasPGN.stream().anyMatch(l -> l.trim().startsWith("["))) {
            LOGGER.warning("PGN parseado no produjo movimientos, pero PGN no parecía vacío o solo cabeceras.");
        }
        return movimientosPartida;
    }

    public String generarPGN(ModoJuego modoJuego, boolean humanoJuegaBlancas) {
        StringBuilder pgn = new StringBuilder();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd");
        SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
        Date currentDate = new Date();

        pgn.append("[Event \"Partida ChessMate\"]\n");
        pgn.append("[Site \"Local Game\"]\n");
        pgn.append("[Date \"").append(dateFormat.format(currentDate)).append("\"]\n");
        pgn.append("[Round \"-\"]\n");

        String whitePlayerName, blackPlayerName;
        String dificultadStr = "(Nivel " + profundidadBusqueda + ")";

        switch (modoJuego) {
            case HUMANO_VS_CPU:
                whitePlayerName = humanoJuegaBlancas ? "Humano" : "ChessMate CPU " + dificultadStr;
                blackPlayerName = humanoJuegaBlancas ? "ChessMate CPU " + dificultadStr : "Humano";
                break;
            case CPU_VS_CPU:
                whitePlayerName = "ChessMate CPU Blancas " + dificultadStr;
                blackPlayerName = "ChessMate CPU Negras " + dificultadStr;
                break;
            case HUMANO_VS_HUMANO:
            default:
                whitePlayerName = "Jugador Blancas";
                blackPlayerName = "Jugador Negras";
                break;
        }
        pgn.append("[White \"").append(whitePlayerName).append("\"]\n");
        pgn.append("[Black \"").append(blackPlayerName).append("\"]\n");

        String resultadoPGN = "*";
        if (estadoActual != null && estadoActual.esFinDePartida()) {
            if (estadoActual == EstadoJuego.JAQUEMATE_GANA_BLANCAS) {
                resultadoPGN = "1-0";
            } else if (estadoActual == EstadoJuego.JAQUEMATE_GANA_NEGRAS) {
                resultadoPGN = "0-1";
            } else if (estadoActual.esEmpate()) {
                resultadoPGN = "1/2-1/2";
            }
        }
        pgn.append("[Result \"").append(resultadoPGN).append("\"]\n");

        pgn.append("[Time \"").append(timeFormat.format(currentDate)).append("\"]\n");
        pgn.append("[WhiteElo \"?\"]\n");
        pgn.append("[BlackElo \"?\"]\n");
        pgn.append("[Annotator \"ChessMate Aplicación\"]\n");

        Tablero tableroInicial = new Tablero();
        pgn.append("[FEN \"").append(tableroInicial.getPosicionHash(true)).append("\"]\n");

        pgn.append("\n");

        List<String> movimientos = getHistorialNotacion();
        int numeroTurno = 1;
        for (int i = 0; i < movimientos.size(); i++) {
            if (i % 2 == 0) {
                pgn.append(numeroTurno).append(". ");
            }
            pgn.append(movimientos.get(i));

            if (i % 2 != 0 || i == movimientos.size() - 1) {
                pgn.append(" ");
                if (numeroTurno % 7 == 0 && i < movimientos.size() - 1 && movimientos.size() > 14) {
                    pgn.append("\n");
                }
            } else {
                pgn.append(" ");
            }

            if (i % 2 != 0) {
                numeroTurno++;
            }
        }
        if (!pgn.toString().trim().endsWith(resultadoPGN)) {
            pgn.append(resultadoPGN);
        }
        return pgn.toString().trim();
    }
}


Código Java (Tablero.java):

package ajedrez_gui;

import java.util.ArrayList;
import java.util.List;

public final class Tablero {

    private final Pieza[][] casillas; 
    public static final int TAMANO_TABLERO = 8;
    
    public static final int FILA_INICIAL_BLANCAS = 0; 
    public static final int FILA_PEONES_BLANCOS = 1; 
    public static final int FILA_PEONES_NEGROS = 6; 
    public static final int FILA_INICIAL_NEGRAS = 7; 
    
    private boolean puedeEnrocarBlancoCorto;
    private boolean puedeEnrocarBlancoLargo;
    private boolean puedeEnrocarNegroCorto;
    private boolean puedeEnrocarNegroLargo;
    
    private Posicion enPassantTarget;
    
    public Tablero() {
        casillas = new Pieza[TAMANO_TABLERO][TAMANO_TABLERO];
        iniciarPosicionEstandar();
    }
    
    private Tablero(Pieza[][] casillasExistentes, boolean bCorto, boolean bLargo, boolean nCorto, boolean nLargo, Posicion epTarget) {
        this.casillas = new Pieza[TAMANO_TABLERO][TAMANO_TABLERO];
        for (int i = 0; i < TAMANO_TABLERO; i++) {
            for (int j = 0; j < TAMANO_TABLERO; j++) {                
                this.casillas[i][j] = (casillasExistentes[i][j] != null) ? casillasExistentes[i][j].copiar() : null;
            }
        }
        
        this.puedeEnrocarBlancoCorto = bCorto;
        this.puedeEnrocarBlancoLargo = bLargo;
        this.puedeEnrocarNegroCorto = nCorto;
        this.puedeEnrocarNegroLargo = nLargo;
        this.enPassantTarget = epTarget; 
    }
    
    public Posicion getEnPassant() {
        return enPassantTarget;
    }

    public void setEnPassant(Posicion nuevoEnPassant) {
        this.enPassantTarget = nuevoEnPassant;
    }
    
    public void iniciarPosicionEstandar() {
        
        for (int i = 0; i < TAMANO_TABLERO; i++) {
            for (int j = 0; j < TAMANO_TABLERO; j++) {
                casillas[i][j] = null;
            }
        }
        
        casillas[FILA_INICIAL_BLANCAS][0] = new Torre(true);  
        casillas[FILA_INICIAL_BLANCAS][1] = new Caballo(true); 
        casillas[FILA_INICIAL_BLANCAS][2] = new Alfil(true);  
        casillas[FILA_INICIAL_BLANCAS][3] = new Dama(true);   
        casillas[FILA_INICIAL_BLANCAS][4] = new Rey(true);    
        casillas[FILA_INICIAL_BLANCAS][5] = new Alfil(true);  
        casillas[FILA_INICIAL_BLANCAS][6] = new Caballo(true); 
        casillas[FILA_INICIAL_BLANCAS][7] = new Torre(true);  
        for (int j = 0; j < TAMANO_TABLERO; j++) {
            casillas[FILA_PEONES_BLANCOS][j] = new Peon(true); 
        }
        
        casillas[FILA_INICIAL_NEGRAS][0] = new Torre(false);  
        casillas[FILA_INICIAL_NEGRAS][1] = new Caballo(false); 
        casillas[FILA_INICIAL_NEGRAS][2] = new Alfil(false);  
        casillas[FILA_INICIAL_NEGRAS][3] = new Dama(false);   
        casillas[FILA_INICIAL_NEGRAS][4] = new Rey(false);    
        casillas[FILA_INICIAL_NEGRAS][5] = new Alfil(false);  
        casillas[FILA_INICIAL_NEGRAS][6] = new Caballo(false); 
        casillas[FILA_INICIAL_NEGRAS][7] = new Torre(false);  
        for (int j = 0; j < TAMANO_TABLERO; j++) {
            casillas[FILA_PEONES_NEGROS][j] = new Peon(false); 
        }
        
        inicializarEnroque();
        enPassantTarget = null;
    }
    
    public Pieza obtenerPieza(Posicion pos) {
        return (pos != null && pos.esValida()) ? casillas[pos.x()][pos.y()] : null;
    }
    
    public void setPieza(Posicion pos, Pieza pieza) {
        if (pos != null && pos.esValida()) {
            casillas[pos.x()][pos.y()] = pieza;
        } else {
            System.err.println("Error: Intento de setPieza en posición inválida: " + pos);
        }
    }
    
    public Posicion encontrarRey(boolean esBlanco) {
        for (int i = 0; i < TAMANO_TABLERO; i++) {
            for (int j = 0; j < TAMANO_TABLERO; j++) {
                Pieza p = casillas[i][j];
                
                if (p instanceof Rey rey && rey.esBlanca() == esBlanco) {
                    return new Posicion(i, j);
                }
            }
        }        
        System.err.println("Error Crítico: No se encontró el rey " + (esBlanco ? "blanco" : "negro") + "!");
        return null; 
    }

    
    public boolean estaAtacada(Posicion pos, boolean atacadoPorBlancas) {
        if (pos == null || !pos.esValida()) {
            return false;
        }
        for (int i = 0; i < TAMANO_TABLERO; i++) {
            for (int j = 0; j < TAMANO_TABLERO; j++) {
                Posicion origenAtacante = new Posicion(i, j);
                Pieza piezaAtacante = obtenerPieza(origenAtacante);                
                if (piezaAtacante != null && piezaAtacante.esBlanca() == atacadoPorBlancas) {                    
                    if (piezaAtacante.esMovimientoBasicoValido(origenAtacante, pos, this)) {
                        if (piezaAtacante instanceof Peon) {                            
                            if (pos.y() != origenAtacante.y()) {
                                return true; 
                            }                            
                        } else {                            
                            return true;
                        }
                    }
                }
            }
        }
        return false; 
    }
    
    public boolean estaEnJaque(boolean reyBlanco) {
        Posicion posRey = encontrarRey(reyBlanco);
        if (posRey == null) {            
            System.err.println("Advertencia: Rey no encontrado al verificar jaque para " + (reyBlanco ? "blancas." : "negras."));
            return true;
        }
        
        return estaAtacada(posRey, !reyBlanco);
    }
    
    public List<Movimiento> generarMovimientosLegales(boolean paraBlancas) {
        List<Movimiento> movimientosLegales = new ArrayList<>();

        for (int x1 = 0; x1 < TAMANO_TABLERO; x1++) {
            for (int y1 = 0; y1 < TAMANO_TABLERO; y1++) {
                Posicion origen = new Posicion(x1, y1);
                Pieza pieza = obtenerPieza(origen);
                
                if (pieza != null && pieza.esBlanca() == paraBlancas) {
                    for (int x2 = 0; x2 < TAMANO_TABLERO; x2++) {
                        for (int y2 = 0; y2 < TAMANO_TABLERO; y2++) {
                            Posicion destino = new Posicion(x2, y2);
                            if (origen.equals(destino)) {
                                continue; 
                            }                           
                            
                            Movimiento movPotencial = new Movimiento(this, origen, destino);
                            
                            if (pieza.esMovimientoBasicoValido(origen, destino, this)) {
                                
                                Tablero copiaTablero = this.copiar();
                                
                                Pieza piezaEnCopiaOrigen = copiaTablero.obtenerPieza(origen);
                                if (piezaEnCopiaOrigen != null) {                                    
                                    Movimiento movSimulado = new Movimiento(copiaTablero, origen, destino);
                                    movSimulado.ejecutar();                                     
                                    copiaTablero.setEnPassant(movSimulado.obtenerNuevoEnPassant());                                    
                                    if (!copiaTablero.estaEnJaque(paraBlancas)) {                                        
                                        movimientosLegales.add(movPotencial);
                                    }                                    
                                } else {
                                    System.err.println("Error simulación: Pieza desapareció en copia?");
                                }
                            }
                        }
                    }
                }
            }
        }
        return movimientosLegales;
    }
    
    private void inicializarEnroque() {
        puedeEnrocarBlancoCorto = true;
        puedeEnrocarBlancoLargo = true;
        puedeEnrocarNegroCorto = true;
        puedeEnrocarNegroLargo = true;
    }

    public boolean puedeEnrocar(boolean esBlanco, boolean ladoCorto) {
        if (esBlanco) {
            return ladoCorto ? puedeEnrocarBlancoCorto : puedeEnrocarBlancoLargo;
        } else {
            return ladoCorto ? puedeEnrocarNegroCorto : puedeEnrocarNegroLargo;
        }
    }
    
    public void actualizarEnroquePorMovimiento(Posicion origen, Pieza piezaMovida) {
        if (piezaMovida == null || !origen.esValida()) {
            return;
        }
        
        if (piezaMovida instanceof Rey) {
            if (piezaMovida.esBlanca()) {
                invalidarEnroqueBlanco();
            } else {
                invalidarEnroqueNegro();
            }
        } else if (piezaMovida instanceof Torre torre) {
            
            Posicion TR_BLANCA_LARGA = new Posicion(FILA_INICIAL_BLANCAS, 0); 
            Posicion TR_BLANCA_CORTA = new Posicion(FILA_INICIAL_BLANCAS, 7); 
            Posicion TR_NEGRA_LARGA = new Posicion(FILA_INICIAL_NEGRAS, 0); 
            Posicion TR_NEGRA_CORTA = new Posicion(FILA_INICIAL_NEGRAS, 7); 

            if (torre.esBlanca()) {
                if (origen.equals(TR_BLANCA_LARGA)) {
                    puedeEnrocarBlancoLargo = false;
                } else if (origen.equals(TR_BLANCA_CORTA)) {
                    puedeEnrocarBlancoCorto = false;
                }
            } else {
                if (origen.equals(TR_NEGRA_LARGA)) {
                    puedeEnrocarNegroLargo = false;
                } else if (origen.equals(TR_NEGRA_CORTA)) {
                    puedeEnrocarNegroCorto = false;
                }
            }
        }
    }
    
    public void actualizarEnroquePorCapturaTorre(Posicion posTorreCapturada) {
        if (posTorreCapturada == null || !posTorreCapturada.esValida()) {
            return;
        }
        
        Posicion TR_BLANCA_LARGA = new Posicion(FILA_INICIAL_BLANCAS, 0); 
        Posicion TR_BLANCA_CORTA = new Posicion(FILA_INICIAL_BLANCAS, 7); 
        Posicion TR_NEGRA_LARGA = new Posicion(FILA_INICIAL_NEGRAS, 0); 
        Posicion TR_NEGRA_CORTA = new Posicion(FILA_INICIAL_NEGRAS, 7); 

        if (posTorreCapturada.equals(TR_BLANCA_LARGA)) {
            puedeEnrocarBlancoLargo = false;
        } else if (posTorreCapturada.equals(TR_BLANCA_CORTA)) {
            puedeEnrocarBlancoCorto = false;
        } else if (posTorreCapturada.equals(TR_NEGRA_LARGA)) {
            puedeEnrocarNegroLargo = false;
        } else if (posTorreCapturada.equals(TR_NEGRA_CORTA)) {
            puedeEnrocarNegroCorto = false;
        }
    }

    public void invalidarEnroqueBlanco() {
        puedeEnrocarBlancoCorto = false;
        puedeEnrocarBlancoLargo = false;
    }

    public void invalidarEnroqueNegro() {
        puedeEnrocarNegroCorto = false;
        puedeEnrocarNegroLargo = false;
    }
    
    public Tablero copiar() {
        return new Tablero(this.casillas,
                this.puedeEnrocarBlancoCorto, this.puedeEnrocarBlancoLargo,
                this.puedeEnrocarNegroCorto, this.puedeEnrocarNegroLargo,
                this.enPassantTarget);
    }
    
    public String getPosicionHash(boolean turnoActualBlancas) {
        var sb = new StringBuilder();
        
        for (int i = TAMANO_TABLERO - 1; i >= 0; i--) {
            int casillasVacias = 0;
            for (int j = 0; j < TAMANO_TABLERO; j++) {
                Pieza p = casillas[i][j];
                if (p == null) {
                    casillasVacias++;
                } else {
                    if (casillasVacias > 0) {
                        sb.append(casillasVacias);
                    }
                    casillasVacias = 0;                    
                    char piezaChar = switch (p) {
                        case Rey r ->
                            'K';
                        case Dama q ->
                            'Q';
                        case Torre t ->
                            'R';
                        case Alfil b ->
                            'B';
                        case Caballo k ->
                            'N'; 
                        case Peon pawn ->
                            'P';
                        
                    };
                    sb.append(p.esBlanca() ? piezaChar : Character.toLowerCase(piezaChar));
                }
            }
            if (casillasVacias > 0) {
                sb.append(casillasVacias);
            }
            if (i > 0) {
                sb.append('/'); 
            }
        }
        
        sb.append(turnoActualBlancas ? " w" : " b");        
        sb.append(" ");
        String enroques = "";
        if (puedeEnrocarBlancoCorto) {
            enroques += "K";
        }
        if (puedeEnrocarBlancoLargo) {
            enroques += "Q";
        }
        if (puedeEnrocarNegroCorto) {
            enroques += "k";
        }
        if (puedeEnrocarNegroLargo) {
            enroques += "q";
        }
        sb.append(enroques.isEmpty() ? "-" : enroques);        
        sb.append(" ");
        sb.append(enPassantTarget != null ? enPassantTarget.toString() : "-");        
        
        return sb.toString();
    }
    
    public boolean esMaterialInsuficiente() {
        int caballosBlancos = 0, alfilesBlancosCasillaClara = 0, alfilesBlancosCasillaOscura = 0;
        int caballosNegros = 0, alfilesNegrosCasillaClara = 0, alfilesNegrosCasillaOscura = 0;
        boolean hayPiezasMayoresOPeones = false;

        for (int i = 0; i < TAMANO_TABLERO; i++) {
            for (int j = 0; j < TAMANO_TABLERO; j++) {
                Pieza p = casillas[i][j];
                if (p == null || p instanceof Rey) {
                    continue; 
                }
                
                if (p instanceof Peon || p instanceof Dama || p instanceof Torre) {
                    hayPiezasMayoresOPeones = true;
                    break; 
                }

                boolean esCasillaClara = (i + j) % 2 != 0; 

                if (p.esBlanca()) {
                    if (p instanceof Caballo) {
                        caballosBlancos++;
                    } else if (p instanceof Alfil) {
                        if (esCasillaClara) {
                            alfilesBlancosCasillaClara++;
                        } else {
                            alfilesBlancosCasillaOscura++;
                        }
                    }
                } else { 
                    if (p instanceof Caballo) {
                        caballosNegros++;
                    } else if (p instanceof Alfil) {
                        if (esCasillaClara) {
                            alfilesNegrosCasillaClara++;
                        } else {
                            alfilesNegrosCasillaOscura++;
                        }
                    }
                }
            }
            if (hayPiezasMayoresOPeones) {
                break;
            }
        }
        
        if (hayPiezasMayoresOPeones) {
            return false;
        }
        
        int alfilesBlancos = alfilesBlancosCasillaClara + alfilesBlancosCasillaOscura;
        int alfilesNegros = alfilesNegrosCasillaClara + alfilesNegrosCasillaOscura;
        int totalPiezasMenoresBlancas = caballosBlancos + alfilesBlancos;
        int totalPiezasMenoresNegras = caballosNegros + alfilesNegros;
        
        if (totalPiezasMenoresBlancas == 0 && totalPiezasMenoresNegras == 0) {
            return true;
        }
        
        if ((totalPiezasMenoresBlancas == 1 && totalPiezasMenoresNegras == 0)
                || (totalPiezasMenoresBlancas == 0 && totalPiezasMenoresNegras == 1)) {
            return true;
        }
        
        if (caballosBlancos == 0 && caballosNegros == 0 && alfilesBlancos == 1 && alfilesNegros == 1) {
            boolean ambosClaros = alfilesBlancosCasillaClara == 1 && alfilesNegrosCasillaClara == 1;
            boolean ambosOscuros = alfilesBlancosCasillaOscura == 1 && alfilesNegrosCasillaOscura == 1;
            if (ambosClaros || ambosOscuros) {
                return true;
            }
        }
        
        return false;
    }
    
    public void mostrarTableroConsola() {
        System.out.println("\n   a b c d e f g h");
        System.out.println("  +-----------------+");
        for (int i = TAMANO_TABLERO - 1; i >= 0; i--) { 
            System.out.print((i + 1) + " | ");
            for (int j = 0; j < TAMANO_TABLERO; j++) { 
                Pieza p = casillas[i][j];
                System.out.print((p == null ? "." : p.toString()) + " "); 
            }
            System.out.println("| " + (i + 1));
        }
        System.out.println("  +-----------------+");
        System.out.println("   a b c d e f g h\n");
    }
}


Código Java (Pieza.java):

package ajedrez_gui;

public abstract sealed class Pieza permits Alfil, Caballo, Dama, Peon, Rey, Torre {

    protected final boolean esBlanca; 
    protected boolean haMovido;      

    public Pieza(boolean esBlanca) {
        this.esBlanca = esBlanca;
        this.haMovido = false;
    }
    
    public boolean esBlanca() {
        return esBlanca;
    }

    public boolean haMovido() {
        return haMovido;
    }

    public void setMovida(boolean movida) {
        this.haMovido = movida;
    }
    
    public abstract boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero);
    
    protected boolean esCasillaVaciaOEnemiga(Posicion pos, Tablero tablero) {
        if (!pos.esValida()) {
            return false;
        }
        Pieza piezaDestino = tablero.obtenerPieza(pos);
        return piezaDestino == null || piezaDestino.esBlanca() != this.esBlanca;
    }
    
    protected boolean esCaminoLibreRecto(Posicion origen, Posicion destino, Tablero tablero) {
        int dx = Integer.compare(destino.x(), origen.x());
        int dy = Integer.compare(destino.y(), origen.y());
        if (dx != 0 && dy != 0) {
            return false; 
        }
        if (dx == 0 && dy == 0) {
            return true;  
        }
        int x = origen.x() + dx;
        int y = origen.y() + dy;
        while (x != destino.x() || y != destino.y()) {
            Posicion actual = new Posicion(x, y);
            
            if (tablero.obtenerPieza(actual) != null) {
                return false; 
            }
            x += dx;
            y += dy;
        }
        return true; 
    }
    
    protected boolean esCaminoLibreDiagonal(Posicion origen, Posicion destino, Tablero tablero) {
        int dx_total = destino.x() - origen.x();
        int dy_total = destino.y() - origen.y();
        if (Math.abs(dx_total) != Math.abs(dy_total) || dx_total == 0) {
            return false; 
        }
        int dx_step = Integer.compare(dx_total, 0);
        int dy_step = Integer.compare(dy_total, 0);
        int x = origen.x() + dx_step;
        int y = origen.y() + dy_step;

        while (x != destino.x() || y != destino.y()) {
            Posicion actual = new Posicion(x, y);
            
            if (tablero.obtenerPieza(actual) != null) {
                return false; 
            }
            x += dx_step;
            y += dy_step;
        }
        return true; 
    }
    
    public abstract Pieza copiar();
    
    @Override
    public abstract String toString();
}


Código Java (Rey.java):

package ajedrez_gui;

public final class Rey extends Pieza {

    public Rey(boolean esBlanca) {
        super(esBlanca);
    }

    @Override
    public boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero) {
        if (!destino.esValida()) {
            return false;
        }

        int dx_abs = Math.abs(destino.x() - origen.x());
        int dy_abs = Math.abs(destino.y() - origen.y());
        
        if (dx_abs <= 1 && dy_abs <= 1 && (dx_abs != 0 || dy_abs != 0)) {
            return esCasillaVaciaOEnemiga(destino, tablero);
        }        
        
        if (!this.haMovido && dx_abs == 0 && dy_abs == 2) {            
            boolean ladoCorto = destino.y() > origen.y();            
            return validarEnroqueCompleto(origen, ladoCorto, tablero);
        }

        return false; 
    }

    
    private boolean validarEnroqueCompleto(Posicion posRey, boolean ladoCorto, Tablero tablero) {
        
        if (!tablero.puedeEnrocar(this.esBlanca, ladoCorto)) {
            return false;
        }
        
        if (tablero.estaEnJaque(this.esBlanca)) {
            return false;
        }

        int fila = posRey.x();
        int colRey = posRey.y();
        int direccion = ladoCorto ? 1 : -1; 
        int colTorre = ladoCorto ? 7 : 0; 

        
        int inicio = Math.min(colRey, colTorre) + 1;
        int fin = Math.max(colRey, colTorre);
        for (int col = inicio; col < fin; col++) {
            if (tablero.obtenerPieza(new Posicion(fila, col)) != null) {
                return false; 
            }
        }
        
        Posicion casillaPaso1 = new Posicion(fila, colRey + direccion);
        Posicion casillaPaso2 = new Posicion(fila, colRey + 2 * direccion); 
        if (tablero.estaAtacada(casillaPaso1, !this.esBlanca)
                || tablero.estaAtacada(casillaPaso2, !this.esBlanca)) {
            return false; 
        }
        
        Posicion posTorre = new Posicion(fila, colTorre);
        Pieza piezaTorre = tablero.obtenerPieza(posTorre);
        if (!(piezaTorre instanceof Torre) || piezaTorre.esBlanca() != this.esBlanca || piezaTorre.haMovido()) {
            
            return false; 
        }

        return true; 
    }

    @Override
    public Pieza copiar() {
        var copia = new Rey(this.esBlanca);
        copia.setMovida(this.haMovido);
        return copia;
    }

    @Override
    public String toString() {
        
        return esBlanca() ? "K" : "k";
    }
}


Código Java (Dama.java):

package ajedrez_gui;

public final class Dama extends Pieza {

    public Dama(boolean esBlanca) {
        super(esBlanca);
    }

    @Override
    public boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero) {
        if (!destino.esValida() || origen.equals(destino)) {
            return false;
        }
        int dx = destino.x() - origen.x();
        int dy = destino.y() - origen.y();
        int dxAbs = Math.abs(dx);
        int dyAbs = Math.abs(dy);
        
        boolean esMovimientoRecto = (dx == 0 && dy != 0) || (dx != 0 && dy == 0);        
        boolean esMovimientoDiagonal = dxAbs == dyAbs; 

        if (esMovimientoRecto) {
            return esCaminoLibreRecto(origen, destino, tablero) && esCasillaVaciaOEnemiga(destino, tablero);
        } else if (esMovimientoDiagonal) {
            return esCaminoLibreDiagonal(origen, destino, tablero) && esCasillaVaciaOEnemiga(destino, tablero);
        }

        return false; 
    }

    @Override
    public Pieza copiar() {
        var copia = new Dama(this.esBlanca);
        copia.setMovida(this.haMovido);
        return copia;
    }

    @Override
    public String toString() {
        
        return esBlanca() ? "Q" : "q";
    }
}


Código Java (Torre.java):

package ajedrez_gui;

public final class Torre extends Pieza {

    public Torre(boolean esBlanca) {
        super(esBlanca);
    }

    @Override
    public boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero) {
        if (!destino.esValida() || origen.equals(destino)) {
            return false;
        }
        
        boolean esMovimientoRecto = (origen.x() == destino.x() && origen.y() != destino.y())
                || (origen.x() != destino.x() && origen.y() == destino.y());

        if (esMovimientoRecto) {
            
            return esCaminoLibreRecto(origen, destino, tablero) && esCasillaVaciaOEnemiga(destino, tablero);
        }
        return false;
    }

    @Override
    public Pieza copiar() {
        var copia = new Torre(this.esBlanca);
        copia.setMovida(this.haMovido);
        return copia;
    }

    @Override
    public String toString() {
        
        return esBlanca() ? "R" : "r";
    }
}


Código Java (Alfil.java):

package ajedrez_gui;

public final class Alfil extends Pieza {

    public Alfil(boolean esBlanca) {
        super(esBlanca);
    }

    @Override
    public boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero) {
        if (!destino.esValida() || origen.equals(destino)) {
            return false;
        }
        
        int dxAbs = Math.abs(destino.x() - origen.x());
        int dyAbs = Math.abs(destino.y() - origen.y());
        
        if (dxAbs == dyAbs) {
            
            return esCaminoLibreDiagonal(origen, destino, tablero) && esCasillaVaciaOEnemiga(destino, tablero);
        }
        return false;
    }

    @Override
    public Pieza copiar() {
        var copia = new Alfil(this.esBlanca);
        copia.setMovida(this.haMovido);
        return copia;
    }

    @Override
    public String toString() {
        
        return esBlanca() ? "B" : "b";
    }
}


Código Java (Caballo.java):

package ajedrez_gui;

public final class Caballo extends Pieza {

    public Caballo(boolean esBlanca) {
        super(esBlanca);
    }

    @Override
    public boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero) {
        if (!destino.esValida()) {
            return false;
        }
        
        int dxAbs = Math.abs(destino.x() - origen.x());
        int dyAbs = Math.abs(destino.y() - origen.y());

        if ((dxAbs == 2 && dyAbs == 1) || (dxAbs == 1 && dyAbs == 2)) {
            
            return esCasillaVaciaOEnemiga(destino, tablero);
        }
        return false;
    }

    @Override
    public Pieza copiar() {
        var copia = new Caballo(this.esBlanca);
        copia.setMovida(this.haMovido);
        return copia;
    }

    @Override
    public String toString() {
        
        return esBlanca() ? "N" : "n";
    }
}


Código Java (Peon.java):

package ajedrez_gui;

public final class Peon extends Pieza {

    public Peon(boolean esBlanca) {
        super(esBlanca);
    }

    @Override
    public boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero) {
        if (!destino.esValida()) {
            return false;
        }

        int direccion = esBlanca() ? 1 : -1; 
        int dx = destino.x() - origen.x(); 
        int dy = destino.y() - origen.y(); 
        Pieza piezaDestino = tablero.obtenerPieza(destino);
        
        if (dx == direccion && dy == 0 && piezaDestino == null) {
            return true;
        }
        
        boolean enFilaInicial = (esBlanca() && origen.x() == Tablero.FILA_PEONES_BLANCOS)
                || (!esBlanca() && origen.x() == Tablero.FILA_PEONES_NEGROS);
        Posicion posIntermedia = new Posicion(origen.x() + direccion, origen.y());
        if (dx == 2 * direccion && dy == 0 && enFilaInicial
                && tablero.obtenerPieza(posIntermedia) == null && piezaDestino == null) {
            return true;
        }
        
        if (dx == direccion && Math.abs(dy) == 1 && piezaDestino != null && piezaDestino.esBlanca() != this.esBlanca()) {
            return true;
        }
        
        Posicion enPassantTarget = tablero.getEnPassant();
        if (enPassantTarget != null && destino.equals(enPassantTarget)
                && dx == direccion && Math.abs(dy) == 1) {            
            Posicion peonCapturadoPos = new Posicion(origen.x(), destino.y());
            Pieza peonACapturar = tablero.obtenerPieza(peonCapturadoPos);
            if (peonACapturar instanceof Peon && peonACapturar.esBlanca() != this.esBlanca()) {
                return true; 
            }
        }

        return false; 
    }

    @Override
    public Pieza copiar() {
        var copia = new Peon(this.esBlanca);
        copia.setMovida(this.haMovido);
        return copia;
    }

    @Override
    public String toString() {
        
        return esBlanca() ? "P" : "p";
    }
}


Código Java (Movimiento.java):

package ajedrez_gui;

public class Movimiento {

    private final Tablero tablero; 
    private final Posicion origen;
    private final Posicion destino;
    private final Pieza piezaMovida;
    private final Pieza piezaCapturada;
    private final boolean esEnroqueCorto;
    private final boolean esEnroqueLargo;
    private final boolean esPromocion;
    private final boolean esCapturaEnPassant;
    private final Posicion enPassantPrevio; 
    
    private record DetallesMovimiento(
            Pieza piezaCapturada,
            boolean esEnroqueCorto,
            boolean esEnroqueLargo,
            boolean esPromocion,
            boolean esCapturaEnPassant
            ) {

    }
    
    public Movimiento(Tablero tablero, Posicion origen, Posicion destino) {
        this.tablero = tablero;
        this.origen = origen;
        this.destino = destino;
        this.piezaMovida = tablero.obtenerPieza(origen);
        this.enPassantPrevio = tablero.getEnPassant();

        DetallesMovimiento detalles = determinarDetallesMovimiento(tablero, origen, destino, piezaMovida, enPassantPrevio);
        this.piezaCapturada = detalles.piezaCapturada();
        this.esEnroqueCorto = detalles.esEnroqueCorto();
        this.esEnroqueLargo = detalles.esEnroqueLargo();
        this.esPromocion = detalles.esPromocion();
        this.esCapturaEnPassant = detalles.esCapturaEnPassant();
    }
    
    private Movimiento(Tablero tablero, Posicion origen, Posicion destino, Pieza piezaMovida, Pieza piezaCapturada,
            boolean esEnroqueCorto, boolean esEnroqueLargo, boolean esPromocion, boolean esCapturaEnPassant,
            Posicion enPassantPrevio) {
        this.tablero = tablero;
        this.origen = origen;
        this.destino = destino;
        this.piezaMovida = piezaMovida;
        this.piezaCapturada = piezaCapturada;
        this.esEnroqueCorto = esEnroqueCorto;
        this.esEnroqueLargo = esEnroqueLargo;
        this.esPromocion = esPromocion;
        this.esCapturaEnPassant = esCapturaEnPassant;
        this.enPassantPrevio = enPassantPrevio;
    }
    
    private static DetallesMovimiento determinarDetallesMovimiento(
            Tablero tableroActual, Posicion origen, Posicion destino,
            Pieza piezaMovida, Posicion enPassantTargetActual) {

        Pieza capturada = null;
        boolean enroqueC = false, enroqueL = false, promocion = false, capturaEP = false;

        if (piezaMovida == null) {
            return new DetallesMovimiento(null, false, false, false, false);
        }
        
        if (piezaMovida instanceof Rey) {
            int dy = destino.y() - origen.y();
            int dx = destino.x() - origen.x();
            if (dx == 0 && Math.abs(dy) == 2) { 
                enroqueC = dy == 2;
                enroqueL = dy == -2;
                capturada = null;
            } else { 
                capturada = tableroActual.obtenerPieza(destino);
            }
        } else if (piezaMovida instanceof Peon peon) {
            int filaPromocion = peon.esBlanca() ? Tablero.FILA_INICIAL_NEGRAS : Tablero.FILA_INICIAL_BLANCAS;
            if (destino.x() == filaPromocion) {
                promocion = true;
                capturada = tableroActual.obtenerPieza(destino); 
            }

            int dx_peon = destino.x() - origen.x();
            int dy_peon_abs = Math.abs(destino.y() - origen.y());
            
            if (enPassantTargetActual != null && destino.equals(enPassantTargetActual) && dy_peon_abs == 1) {
                int dirCorrecta = peon.esBlanca() ? 1 : -1;
                if (dx_peon == dirCorrecta) {
                    Posicion peonCapturadoPos = new Posicion(origen.x(), destino.y());
                    Pieza peonACapturar = tableroActual.obtenerPieza(peonCapturadoPos);
                    if (peonACapturar instanceof Peon && peonACapturar.esBlanca() != peon.esBlanca()) {
                        capturaEP = true;
                        capturada = peonACapturar; 
                    }
                }
            }
            
            if (!capturaEP && dy_peon_abs == 1 && dx_peon == (peon.esBlanca() ? 1 : -1)) {
                Pieza piezaEnDestino = tableroActual.obtenerPieza(destino);
                
                if (piezaEnDestino != null && !promocion) {
                    capturada = piezaEnDestino;
                }
            }
        } else { 
            capturada = tableroActual.obtenerPieza(destino);
        }

        if (enroqueC || enroqueL) {
            capturada = null; 
        }
        return new DetallesMovimiento(capturada, enroqueC, enroqueL, promocion, capturaEP);
    }
    
    public void ejecutar() {
        if (piezaMovida == null || !origen.esValida() || !destino.esValida()) {
            System.err.println("Movimiento.ejecutar(): Intento inválido.");
            return;
        }
        
        tablero.actualizarEnroquePorMovimiento(origen, piezaMovida);
        Pieza piezaEnDestinoAntes = tablero.obtenerPieza(destino);
        if (piezaEnDestinoAntes instanceof Torre) {
            tablero.actualizarEnroquePorCapturaTorre(destino);
        }
        
        tablero.setPieza(destino, piezaMovida);
        tablero.setPieza(origen, null);
        
        if (esEnroqueCorto || esEnroqueLargo) {
            int fila = origen.x();
            int torreOrigenY = esEnroqueCorto ? 7 : 0;
            int torreDestinoY = esEnroqueCorto ? 5 : 3;
            Posicion torreOrigenPos = new Posicion(fila, torreOrigenY);
            Posicion torreDestinoPos = new Posicion(fila, torreDestinoY);
            Pieza torre = tablero.obtenerPieza(torreOrigenPos);
            tablero.setPieza(torreDestinoPos, torre);
            tablero.setPieza(torreOrigenPos, null);
            if (torre != null) {
                torre.setMovida(true); 
            }            
        } else if (esCapturaEnPassant) {
            Posicion posPeonCapturado = new Posicion(origen.x(), destino.y());
            tablero.setPieza(posPeonCapturado, null);             
        }        
        piezaMovida.setMovida(true);        
    }
    
    public Posicion obtenerNuevoEnPassant() {        
        if (piezaMovida instanceof Peon && Math.abs(destino.x() - origen.x()) == 2) {
            int epFila = (origen.x() + destino.x()) / 2;
            int epColumna = origen.y();
            return new Posicion(epFila, epColumna);
        }
        return null; 
    }
    
    public Posicion getOrigen() {
        return origen;
    }

    public Posicion getDestino() {
        return destino;
    }

    public Pieza getPiezaMovida() {
        return piezaMovida;
    }

    public Pieza getPiezaCapturada() {
        return piezaCapturada;
    }

    public boolean esEnroque() {
        return esEnroqueCorto || esEnroqueLargo;
    }

    public boolean esEnroqueCorto() {
        return esEnroqueCorto;
    }

    public boolean esEnroqueLargo() {
        return esEnroqueLargo;
    }

    public boolean esPromocion() {
        return esPromocion;
    }

    public boolean esCapturaEnPassant() {
        return esCapturaEnPassant;
    }

    public Posicion getEnPassantPrevio() {
        return enPassantPrevio;
    }
    
    public Movimiento copiar() {
        return new Movimiento(this.tablero, this.origen, this.destino,
                this.piezaMovida, this.piezaCapturada,
                this.esEnroqueCorto, this.esEnroqueLargo, this.esPromocion, this.esCapturaEnPassant,
                this.enPassantPrevio);
    }
    
    @Override
    public String toString() {
        if (esEnroqueCorto) {
            return "O-O";
        }
        if (esEnroqueLargo) {
            return "O-O-O";
        }

        String piezaStr = (piezaMovida != null) ? piezaMovida.toString() : "?";
        String origenStr = (origen != null) ? origen.toString() : "??";
        String destinoStr = (destino != null) ? destino.toString() : "??";
        String capturaStr = (piezaCapturada != null) ? "x" : "-";
        String promoStr = esPromocion ? "=?" : "";
        String epStr = esCapturaEnPassant ? " e.p." : "";

        return piezaStr.toUpperCase() + origenStr + capturaStr + destinoStr + promoStr + epStr;
    }
}


Código Java (EstadoJuego.java):

package ajedrez_gui;

public enum EstadoJuego {
    
    TURNO_BLANCAS("TURNO BLANCAS"),
    TURNO_NEGRAS("TURNO NEGRAS"),    
    JAQUE_BLANCAS("JAQUE!"), 
    JAQUE_NEGRAS("JAQUE!"),     
    JAQUEMATE_GANA_NEGRAS("JAQUEMATE!!"), 
    JAQUEMATE_GANA_BLANCAS("JAQUEMATE!!"),     
    AHOGADO("EMPATE AHOGADO"),
    EMPATE_50_MOV("EMPATE 50 MOV"),
    EMPATE_TRIPLE_REP("EMPATE X3 REP"),
    EMPATE_MATERIAL_INSUF("EMPATE MATERIAL"),    
    PROMOCION_REQUIERIDA("PROMOCION PEON"), 
    MOVIMIENTO_INVALIDO("MOV NO VALIDO"), 
    ERROR_INTERNO("ERROR INTERNO"), 
    EN_CURSO("En curso");               

    private final String mensaje; 

    EstadoJuego(String mensaje) {        
        if (mensaje.length() > 16) {
            System.err.println("Advertencia: Mensaje de estado > 16 chars: " + mensaje);
        }
        this.mensaje = mensaje;
    }
    
    public String getMensaje() {
        return mensaje;
    }
    
    public boolean esFinDePartida() {
        return switch (this) {
            case JAQUEMATE_GANA_BLANCAS, JAQUEMATE_GANA_NEGRAS, AHOGADO, EMPATE_50_MOV, EMPATE_TRIPLE_REP, EMPATE_MATERIAL_INSUF ->
                true;
            default ->
                false;
        };
    }
    
    public boolean esJaqueMate() {
        return this == JAQUEMATE_GANA_BLANCAS || this == JAQUEMATE_GANA_NEGRAS;
    }
    
    public boolean esEmpate() {
        return switch (this) {
            case AHOGADO, EMPATE_50_MOV, EMPATE_TRIPLE_REP, EMPATE_MATERIAL_INSUF ->
                true;
            default ->
                false;
        };
    }
    
    public boolean esJaque() {
        return this == JAQUE_BLANCAS || this == JAQUE_NEGRAS;
    }
}


Código Java (Posicion.java):

package ajedrez_gui;

public record Posicion(int x, int y) {  
   
    public boolean esValida() {
        return x >= 0 && x < Tablero.TAMANO_TABLERO && y >= 0 && y < Tablero.TAMANO_TABLERO;
    }
    
    @Override
    public String toString() {
        if (!esValida()) {
            return "??";
        }
        char file = (char) ('a' + y); 
        char rank = (char) ('1' + x); 
        return "" + file + rank;
    }
    
    public static Posicion desdeNotacion(String notacionAlgebraica) {
        if (notacionAlgebraica == null || !notacionAlgebraica.matches("^[a-hA-H][1-8]$")) {
            
            return null;
        }
        String lowerNotacion = notacionAlgebraica.toLowerCase();
        int y = lowerNotacion.charAt(0) - 'a'; 
        int x = Character.getNumericValue(lowerNotacion.charAt(1)) - 1;         
        if (x < 0 || x >= Tablero.TAMANO_TABLERO || y < 0 || y >= Tablero.TAMANO_TABLERO) {
            
            return null;
        }
        return new Posicion(x, y);
    }    
}


Código Java (AjedrezGrafico.java):

package ajedrez_gui;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.UIManager;
import java.util.logging.Level;
import java.util.logging.Logger;

public class AjedrezGrafico extends JFrame {

    private static final Logger LOGGER = Logger.getLogger(AjedrezGrafico.class.getName());

    private JPanel panelLateralIzquierdo;
    private JButton botonLateralJugar;
    private JButton botonLateralConfigurar;
    private JButton botonLateralGuardarPGN;
    private JButton botonLateralCargarPGN;
    private JButton botonLateralPausaReanudar;
    private JButton botonLateralInformacion;

    private PanelTablero panelTablero;
    private JTextField campoMovimiento;
    private JLabel labelEstadoInfo;

    private JTable tablaHistorial;
    private DefaultTableModel modeloTablaHistorial;
    private JScrollPane scrollTablaHistorial;

    private JPanel panelDerecho;
    private JLabel labelIntroduce;

    private JPanel panelControlesPgn;
    private JButton botonPgnInicio;
    private JButton botonPgnAnterior;
    private JButton botonPgnPlayPause;
    private JButton botonPgnSiguiente;
    private JButton botonPgnFin;

    private MotorJuego motor;
    private boolean rotarTablero = false;
    private boolean partidaEnCurso = false;
    private boolean isPaused = false;
    private boolean isFullScreenMode = false;
    private boolean isConcentrationMode = false;
    private javax.swing.Timer timerMensajeError;
    private SwingWorker<EstadoJuego, Void> cpuMoveWorker;

    private boolean isReplayingPgn = false;
    private boolean isPgnLoadedMode = false;
    private javax.swing.Timer pgnReplayTimer;
    private List<String> pgnLoadedMovements;
    private int currentPgnDisplayIndex;
    private static final int PGN_REPLAY_DELAY_MS = 700;

    private boolean isRevisandoPartidaActual = false;
    private boolean isAutoReplayPartidaActual = false;
    private javax.swing.Timer replayPartidaActualTimer;
    private List<String> movimientosPartidaActualRevisar;
    private int currentPartidaActualRevisarIndex;
    private static final int PARTIDA_ACTUAL_REPLAY_DELAY_MS = 700;

    private Posicion selectedPiecePos = null;
    private List<Movimiento> currentLegalMoves = null;

    private boolean cfgJugarComoBlancas = DEFAULT_JUGAR_COMO_BLANCAS;
    private ModoJuego cfgModoJuego = DEFAULT_MODO_JUEGO;
    private NivelDificultad cfgNivelDificultad = DEFAULT_NIVEL_DIFICULTAD;

    public enum NivelDificultad {
        FACIL("Fácil", 2), NORMAL("Normal", 3), DIFICIL("Difícil", 4), EXPERTO("Experto", 5);
        private final String displayName;
        private final int profundidad;

        NivelDificultad(String displayName, int profundidad) {
            this.displayName = displayName;
            this.profundidad = profundidad;
        }

        public int getProfundidad() {
            return profundidad;
        }

        @Override
        public String toString() {
            return displayName;
        }
    }

    private static final boolean DEFAULT_JUGAR_COMO_BLANCAS = true;
    private static final ModoJuego DEFAULT_MODO_JUEGO = ModoJuego.HUMANO_VS_CPU;
    private static final NivelDificultad DEFAULT_NIVEL_DIFICULTAD = NivelDificultad.NORMAL;
    private static final int PRIMER_MOV_CPU_DELAY_MS = 500;
    private static final Font FONT_BOTON_LATERAL = new Font("Tahoma", Font.BOLD, 16);
    private static final Dimension TAMANO_BOTON_LATERAL = new Dimension(180, 55);
    private static final Font FONT_BOTON_PGN_NAV = new Font("Tahoma", Font.BOLD, 14);
    private static final Dimension TAMANO_BOTON_PGN_NAV = new Dimension(60, 45);

    private static final Color COLOR_FONDO_BARRA_LATERAL = new Color(200, 200, 220);
    private static final int ESPACIADO_BOTONES_LATERALES = 10;

    private Font baseFontEstado;
    private Font baseFontHistorial;
    private Font baseFontIntroduce;
    private Font baseFontCampoMovimiento;
    private Font baseFontCoordenadasTablero;
    private float initialWindowHeight;
    private int baseRowHeightHistorial;

    private static final int MAIN_BORDER_GAP = 0;
    private static final int CENTER_PANEL_HGAP = 0;
    private static final int RIGHT_PANEL_BORDER_TOP = 5;
    private static final int RIGHT_PANEL_BORDER_LEFT = 5;
    private static final int RIGHT_PANEL_BORDER_BOTTOM = 5;
    private static final int RIGHT_PANEL_BORDER_RIGHT = 10;
    private static final int ESTADO_LABEL_BORDER_PADDING = 5;
    private static final int ESTADO_LABEL_H_PADDING = 8;
    private static final int INTRO_LABEL_H_PADDING = 5;
    private static final int INTRO_LABEL_V_PADDING = 3;
    private static final int INPUT_FIELD_BASE_HEIGHT = 45;
    private static final int RIGID_AREA_SMALL_HEIGHT = 5;
    private static final int RIGID_AREA_BOTTOM_MARGIN_HEIGHT = 15;
    private static final String STATUS_PAUSADO_TEXT = "PAUSADO";
    private static final String STATUS_LISTO_TEXT = "-- CHESSMATE --";
    private static final String STATUS_NAVEGANDO_PGN_TEXT = "NAVEGANDO PGN";
    private static final String STATUS_REPRODUCIENDO_PGN_TEXT = "REPRO PGN...";
    private static final String STATUS_REVISANDO_PARTIDA_TEXT = "REVISANDO PARTIDA";
    private static final String STATUS_REPRO_PARTIDA_ACTUAL_TEXT = "REPRO PARTIDA...";

    private static final String ERROR_FORMAT_TEXT = "ERROR FORMATO";
    private static final String ERROR_COORD_TEXT = "ERROR COORDENADA";
    private static final String ERROR_MOVE_TEXT = "MOVIMIENTO ILEGAL";
    private static final String ERROR_PROMO_CANCEL_TEXT = "PROMOCION CANCELADA";
    private static final String ERROR_PGN_LOAD_TEXT = "ERROR CARGANDO PGN";
    private static final int ERROR_MESSAGE_DURATION_MS = 1800;

    public enum ModoJuego {
        HUMANO_VS_CPU("Humano vs CPU"), CPU_VS_CPU("CPU vs CPU"), HUMANO_VS_HUMANO("Humano vs Humano");
        private final String displayName;

        ModoJuego(String name) {
            this.displayName = name;
        }

        @Override
        public String toString() {
            return displayName;
        }
    }

    private final Color COLOR_ESTADO_NORMAL = Color.BLACK;
    private final Color COLOR_FONDO_ESTADO = new Color(210, 210, 210);
    private final Color COLOR_FONDO_PANEL_DERECHO = new Color(190, 190, 190);
    private final Color COLOR_BORDE_HISTORIAL = Color.BLUE.darker();
    private static final Color COLOR_FILA_ALTERNADA_HISTORIAL = new Color(238, 238, 238);
    private static final Color COLOR_CABECERA_HISTORIAL_FONDO = new Color(200, 200, 200);

    private static final int BASE_ANCHO_MIN_PANEL_DERECHO = 320;
    private static final int BASE_ANCHO_PREF_PANEL_DERECHO = 380;
    private static final int HISTORIAL_COL_ID_BASE_WIDTH = 55;

    public AjedrezGrafico() {
        super("ChessMate IA");
        motor = new MotorJuego();
        motor.setProfundidadBusqueda(DEFAULT_NIVEL_DIFICULTAD.getProfundidad());
        initComponents();
        configurarTimersYReplay();
        configurarEstadoInicialUI();
        this.setMinimumSize(new Dimension(1000, 700));
        this.pack();
        this.setLocationRelativeTo(null);
        setupDynamicFontScaling();
    }

    private float getCurrentScaleFactor() {
        if (initialWindowHeight <= 0) {
            return 1.0f;
        }
        float currentHeight = this.getHeight();
        return Math.max(0.65f, currentHeight / initialWindowHeight);
    }

    private void initComponents() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout(MAIN_BORDER_GAP, MAIN_BORDER_GAP));
        var contentPane = (JPanel) getContentPane();
        contentPane.setBorder(BorderFactory.createEmptyBorder(MAIN_BORDER_GAP, MAIN_BORDER_GAP, MAIN_BORDER_GAP, MAIN_BORDER_GAP));
        contentPane.setBackground(COLOR_FONDO_PANEL_DERECHO);

        baseFontEstado = new Font("Courier New", Font.BOLD, 20);
        baseFontHistorial = new Font("Monospaced", Font.BOLD, 20);
        baseFontIntroduce = new Font("Tahoma", Font.PLAIN, 14);
        baseFontCampoMovimiento = new Font("Courier New", Font.BOLD, 28);
        baseFontCoordenadasTablero = new Font("Courier New", Font.BOLD, 18);

        FontMetrics fmBaseHistorial = getFontMetrics(baseFontHistorial);
        baseRowHeightHistorial = fmBaseHistorial.getHeight() + 2;

        panelLateralIzquierdo = new JPanel();
        panelLateralIzquierdo.setLayout(new BoxLayout(panelLateralIzquierdo, BoxLayout.Y_AXIS));
        panelLateralIzquierdo.setBackground(COLOR_FONDO_BARRA_LATERAL);
        panelLateralIzquierdo.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        botonLateralJugar = new JButton("JUGAR");
        botonLateralConfigurar = new JButton("CONFIGURAR");
        botonLateralGuardarPGN = new JButton("GUARDAR PGN");
        botonLateralCargarPGN = new JButton("CARGAR PGN");
        botonLateralPausaReanudar = new JButton("PAUSAR");
        botonLateralInformacion = new JButton("INFORMACIÓN");

        JButton[] botonesLaterales = {botonLateralJugar, botonLateralConfigurar, botonLateralGuardarPGN, botonLateralCargarPGN, botonLateralPausaReanudar, botonLateralInformacion};
        for (JButton btn : botonesLaterales) {
            GuiUtils.applyButtonStyle(btn, FONT_BOTON_LATERAL, TAMANO_BOTON_LATERAL);
            btn.setAlignmentX(Component.CENTER_ALIGNMENT);
            panelLateralIzquierdo.add(btn);
            panelLateralIzquierdo.add(Box.createRigidArea(new Dimension(0, ESPACIADO_BOTONES_LATERALES)));
        }
        add(panelLateralIzquierdo, BorderLayout.WEST);

        var panelCentro = new JPanel(new BorderLayout(CENTER_PANEL_HGAP, 0));
        panelCentro.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        panelCentro.setOpaque(true);
        panelCentro.setBackground(COLOR_FONDO_PANEL_DERECHO);
        panelTablero = new PanelTablero(this::handleClickOnBoard, this::handleDragAndDropMove, baseFontCoordenadasTablero);
        panelDerecho = new JPanel();
        panelDerecho.setLayout(new GridBagLayout());
        panelDerecho.setOpaque(true);
        panelDerecho.setBackground(COLOR_FONDO_PANEL_DERECHO);
        panelDerecho.setBorder(BorderFactory.createEmptyBorder(RIGHT_PANEL_BORDER_TOP, RIGHT_PANEL_BORDER_LEFT, RIGHT_PANEL_BORDER_BOTTOM, RIGHT_PANEL_BORDER_RIGHT));

        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = 0;
        labelEstadoInfo = new JLabel(" ", SwingConstants.CENTER);
        labelEstadoInfo.setFont(baseFontEstado);
        labelEstadoInfo.setForeground(COLOR_ESTADO_NORMAL);
        labelEstadoInfo.setOpaque(true);
        labelEstadoInfo.setBackground(COLOR_FONDO_ESTADO);
        Border bordeEstado = BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.GRAY, 1),
                BorderFactory.createEmptyBorder(ESTADO_LABEL_BORDER_PADDING, ESTADO_LABEL_H_PADDING, ESTADO_LABEL_BORDER_PADDING, ESTADO_LABEL_H_PADDING));
        labelEstadoInfo.setBorder(bordeEstado);
        labelEstadoInfo.setMinimumSize(new Dimension(100, 0));
        gbc.gridy = 0;
        gbc.weightx = 1.0;
        gbc.weighty = 0.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.insets = new Insets(0, 0, RIGID_AREA_SMALL_HEIGHT, 0);
        panelDerecho.add(labelEstadoInfo, gbc);

        modeloTablaHistorial = new DefaultTableModel(new Object[]{"ID", "BLANCAS", "NEGRAS"}, 0) {
            @Override
            public boolean isCellEditable(int r, int c) {
                return false;
            }
        };
        tablaHistorial = new JTable(modeloTablaHistorial) {
            @Override
            public Component prepareRenderer(TableCellRenderer renderer, int row, int col) {
                Component c = super.prepareRenderer(renderer, row, col);
                if (!isRowSelected(row)) {
                    c.setBackground(row % 2 == 0 ? Color.WHITE : COLOR_FILA_ALTERNADA_HISTORIAL);
                }

                boolean resaltarEsteMovimiento = false;
                if (isPgnLoadedMode && col > 0) {
                    int moveNumberInRow = (row * 2) + (col - 1);
                    if (moveNumberInRow == currentPgnDisplayIndex - 1) {
                        resaltarEsteMovimiento = true;
                    }
                } else if (isRevisandoPartidaActual && col > 0) {
                    int moveNumberInRow = (row * 2) + (col - 1);
                    if (moveNumberInRow == currentPartidaActualRevisarIndex - 1) {
                        resaltarEsteMovimiento = true;
                    }
                }

                if (resaltarEsteMovimiento) {
                    c.setBackground(Color.CYAN.darker());
                    c.setForeground(Color.WHITE);
                } else {
                    c.setForeground(Color.BLACK);
                }
                return c;
            }
        };
        tablaHistorial.setFont(baseFontHistorial);
        tablaHistorial.setRowHeight(baseRowHeightHistorial);
        JTableHeader header = tablaHistorial.getTableHeader();
        header.setFont(baseFontHistorial.deriveFont(Font.BOLD));
        header.setBackground(COLOR_CABECERA_HISTORIAL_FONDO);
        header.setForeground(Color.BLACK);
        header.setReorderingAllowed(false);
        ((DefaultTableCellRenderer) header.getDefaultRenderer()).setHorizontalAlignment(SwingConstants.CENTER);
        tablaHistorial.setFillsViewportHeight(true);
        tablaHistorial.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        tablaHistorial.setShowGrid(true);
        tablaHistorial.setGridColor(Color.GRAY.brighter());
        tablaHistorial.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));

        HistorialMovimientoRenderer movimientoRenderer = new HistorialMovimientoRenderer(baseFontHistorial);
        tablaHistorial.getColumnModel().getColumn(1).setCellRenderer(movimientoRenderer);
        tablaHistorial.getColumnModel().getColumn(2).setCellRenderer(movimientoRenderer);
        DefaultTableCellRenderer idRenderer = new DefaultTableCellRenderer();
        idRenderer.setHorizontalAlignment(JLabel.CENTER);
        tablaHistorial.getColumnModel().getColumn(0).setCellRenderer(idRenderer);

        setHistorialColumnWidths(1.0f);

        scrollTablaHistorial = new JScrollPane(tablaHistorial);
        scrollTablaHistorial.getViewport().setBackground(Color.WHITE);
        scrollTablaHistorial.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        scrollTablaHistorial.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollTablaHistorial.setBorder(BorderFactory.createLineBorder(COLOR_BORDE_HISTORIAL, 1));
        gbc.gridy = 1;
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.insets = new Insets(0, 0, RIGID_AREA_SMALL_HEIGHT, 0);
        panelDerecho.add(scrollTablaHistorial, gbc);

        panelControlesPgn = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 2));
        panelControlesPgn.setBackground(COLOR_FONDO_PANEL_DERECHO);
        botonPgnInicio = new JButton("<<");
        botonPgnAnterior = new JButton("<");
        botonPgnPlayPause = new JButton(">");
        botonPgnSiguiente = new JButton(">");
        botonPgnFin = new JButton(">>");
        JButton[] botonesPgn = {botonPgnInicio, botonPgnAnterior, botonPgnPlayPause, botonPgnSiguiente, botonPgnFin};
        for (JButton btn : botonesPgn) {
            GuiUtils.applyButtonStyle(btn, FONT_BOTON_PGN_NAV, TAMANO_BOTON_PGN_NAV);
            panelControlesPgn.add(btn);
        }
        panelControlesPgn.setVisible(false);
        gbc.gridy = 2;
        gbc.weighty = 0.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.insets = new Insets(RIGID_AREA_SMALL_HEIGHT, 0, RIGID_AREA_SMALL_HEIGHT, 0);
        panelDerecho.add(panelControlesPgn, gbc);

        labelIntroduce = new JLabel("Escribe Movimiento:");
        labelIntroduce.setVisible(false);
        gbc.gridy = 3;
        gbc.weighty = 0.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.insets = new Insets(0, 0, 0, 0);
        panelDerecho.add(labelIntroduce, gbc);
        campoMovimiento = new JTextField();
        campoMovimiento.setVisible(false);
        campoMovimiento.setEnabled(false);
        if (campoMovimiento.getDocument() instanceof AbstractDocument doc) {
            doc.setDocumentFilter(new UppercaseDocumentFilter());
        }
        gbc.gridy = 4;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.insets = new Insets(0, 0, RIGID_AREA_BOTTOM_MARGIN_HEIGHT, 0);
        panelDerecho.add(campoMovimiento, gbc);

        panelCentro.add(panelTablero, BorderLayout.CENTER);
        panelCentro.add(panelDerecho, BorderLayout.EAST);
        add(panelCentro, BorderLayout.CENTER);

        botonLateralJugar.addActionListener(e -> mostrarDialogoElegirLado());
        botonLateralConfigurar.addActionListener(e -> mostrarDialogoConfiguracion());
        botonLateralPausaReanudar.addActionListener(e -> togglePausa());
        botonLateralGuardarPGN.addActionListener(e -> guardarPartidaPGN());
        botonLateralCargarPGN.addActionListener(e -> cargarPartidaPGN());
        botonLateralInformacion.addActionListener(e -> mostrarDialogoInformacion());

        botonPgnInicio.addActionListener(e -> {
            if (isPgnLoadedMode) {
                irAlInicioPgn();
            } else if (isRevisandoPartidaActual) {
                irAlInicioPartidaActualRevisar();
            }
        });

        botonPgnAnterior.addActionListener(e -> {
            if (isPgnLoadedMode) {
                procesarMovimientoPgnNavegacion(false);
            } else if (isRevisandoPartidaActual) {
                procesarMovimientoPartidaActualRevisar(false);
            }
        });

        botonPgnPlayPause.addActionListener(e -> {
            if (isPgnLoadedMode) {
                togglePgnPlayPause();
            } else if (isRevisandoPartidaActual) {
                toggleReplayPartidaActual();
            }
        });

        botonPgnSiguiente.addActionListener(e -> {
            if (isPgnLoadedMode) {
                procesarMovimientoPgnNavegacion(true);
            } else if (isRevisandoPartidaActual) {
                procesarMovimientoPartidaActualRevisar(true);
            }
        });

        botonPgnFin.addActionListener(e -> {
            if (isPgnLoadedMode) {
                irAlFinPgn();
            } else if (isRevisandoPartidaActual) {
                irAlFinPartidaActualRevisar();
            }
        });

        configurarTeclaEscAbandono();

        InputMap rootInputMap = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap rootActionMap = getRootPane().getActionMap();

        rootInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F9, 0), "f9");
        rootActionMap.put("f9", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                toggleRotarTablero();
            }
        });

        rootInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0), "f10");
        rootActionMap.put("f10", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                toggleConcentrationMode();
            }
        });

        rootInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0), "f11");
        rootActionMap.put("f11", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                toggleMaximize();
            }
        });

        rootInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_F12, 0), "f12");
        rootActionMap.put("f12", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                toggleFullScreen();
            }
        });
    }

    private void toggleMaximize() {
        if (isFullScreenMode) {
            toggleFullScreen();
        }

        int state = getExtendedState();
        if ((state & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH) {
            setExtendedState(state & ~JFrame.MAXIMIZED_BOTH);
        } else {
            setExtendedState(state | JFrame.MAXIMIZED_BOTH);
        }

        revalidate();
        repaint();
    }

    private void setHistorialColumnWidths(float scaleFactor) {
        Font scaledFont = tablaHistorial.getFont();
        FontMetrics fm = tablaHistorial.getFontMetrics(scaledFont);

        int charWidthFor10Chars = fm.stringWidth("  Nf3-g5#+");

        TableCellRenderer rendComp = tablaHistorial.getColumnModel().getColumn(1).getCellRenderer();
        int padding = 0;
        if (rendComp instanceof HistorialMovimientoRenderer rend) {
            padding = rend.getHorizontalPadding() * 2;
        } else {
            padding = (int) (6 * scaleFactor);
        }
        int movColWidth = charWidthFor10Chars + padding;

        int idColWidth = (int) (HISTORIAL_COL_ID_BASE_WIDTH * scaleFactor);

        TableColumn idColumn = tablaHistorial.getColumnModel().getColumn(0);
        idColumn.setPreferredWidth(idColWidth);
        idColumn.setMinWidth(Math.max(40, (int) (idColWidth * 0.8f)));
        idColumn.setMaxWidth((int) (idColWidth * 1.5f));

        TableColumn blancasColumn = tablaHistorial.getColumnModel().getColumn(1);
        blancasColumn.setPreferredWidth(movColWidth);
        blancasColumn.setMinWidth(Math.max(110, (int) (movColWidth * 0.9f)));

        TableColumn negrasColumn = tablaHistorial.getColumnModel().getColumn(2);
        negrasColumn.setPreferredWidth(movColWidth);
        negrasColumn.setMinWidth(Math.max(110, (int) (movColWidth * 0.9f)));

        tablaHistorial.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
    }

    private void toggleConcentrationMode() {
        isConcentrationMode = !isConcentrationMode;
        panelLateralIzquierdo.setVisible(!isConcentrationMode);
        panelDerecho.setVisible(!isConcentrationMode);

        actualizarControlesUI();

        this.revalidate();
        this.repaint();
        LOGGER.log(Level.INFO, "Modo Concentración: " + (isConcentrationMode ? "Activado" : "Desactivado"));
    }

    private void toggleRotarTablero() {
        this.rotarTablero = !this.rotarTablero;
        if (panelTablero != null) {
            panelTablero.setRotado(this.rotarTablero);
        }
        panelTablero.clearPieceSelection();
        selectedPiecePos = null;
        currentLegalMoves = null;
    }

    private void handleClickOnBoard(Posicion clickedPos) {
        if (isReplayingPgn || isPgnLoadedMode || isRevisandoPartidaActual || isAutoReplayPartidaActual) {
            return;
        }
        if (clickedPos == null) {
            if (this.selectedPiecePos != null) {
                this.selectedPiecePos = null;
                this.currentLegalMoves = null;
                panelTablero.clearPieceSelection();
            }
            return;
        }
        if (!partidaEnCurso || !esTurnoDelHumano() || isPaused || motor.getEstadoActual().esFinDePartida() || motor.getEstadoActual() == EstadoJuego.PROMOCION_REQUIERIDA) {
            if (panelTablero.hasVisualSelection()) {
                panelTablero.clearPieceSelection();
            }
            return;
        }
        Pieza piezaEnPosicion = motor.getTablero().obtenerPieza(clickedPos);

        if (this.selectedPiecePos != null && this.currentLegalMoves != null) {
            if (this.currentLegalMoves.stream().anyMatch(m -> m.getOrigen().equals(this.selectedPiecePos) && m.getDestino().equals(clickedPos))) {
                handleDragAndDropMove(this.selectedPiecePos, clickedPos);
                return;
            } else {
                this.selectedPiecePos = null;
                this.currentLegalMoves = null;
                panelTablero.clearPieceSelection();
            }
        }

        if (piezaEnPosicion != null && piezaEnPosicion.esBlanca() == motor.esTurnoBlancas()) {
            this.selectedPiecePos = clickedPos;
            this.currentLegalMoves = motor.getMovimientosLegales();
            List<Posicion> validDests = this.currentLegalMoves.stream()
                    .filter(m -> m.getOrigen().equals(this.selectedPiecePos))
                    .map(Movimiento::getDestino)
                    .collect(Collectors.toList());
            panelTablero.setPieceSelection(this.selectedPiecePos, validDests);
        } else {
            this.selectedPiecePos = null;
            this.currentLegalMoves = null;
            panelTablero.clearPieceSelection();
        }
    }

    private void handleDragAndDropMove(Posicion origen, Posicion destino) {
        if (isReplayingPgn || isPgnLoadedMode || isRevisandoPartidaActual || isAutoReplayPartidaActual) {
            return;
        }
        if (!partidaEnCurso || !esTurnoDelHumano() || isPaused || motor.getEstadoActual().esFinDePartida() || motor.getEstadoActual() == EstadoJuego.PROMOCION_REQUIERIDA) {
            return;
        }
        EstadoJuego resultado = motor.intentarMover(origen, destino);
        if (resultado != EstadoJuego.MOVIMIENTO_INVALIDO) {
            this.selectedPiecePos = null;
            this.currentLegalMoves = null;
        }

        switch (resultado) {
            case MOVIMIENTO_INVALIDO:
                mostrarErrorUsuario(ERROR_MOVE_TEXT);
                if (this.selectedPiecePos != null && this.currentLegalMoves != null) {
                    List<Posicion> validDests = this.currentLegalMoves.stream()
                            .filter(m -> m.getOrigen().equals(this.selectedPiecePos))
                            .map(Movimiento::getDestino)
                            .collect(Collectors.toList());
                    panelTablero.setPieceSelection(this.selectedPiecePos, validDests);
                } else {
                    panelTablero.clearPieceSelection();
                }
                break;
            case PROMOCION_REQUIERIDA:
                actualizarEstadoInfo(resultado);
                panelTablero.clearPieceSelection();
                char pProm = promocionarPeonGrafico();
                if (pProm == 0) {
                    motor.cancelarPromocionPendiente();
                    mostrarErrorUsuario(ERROR_PROMO_CANCEL_TEXT);
                    actualizarUIPostMovimiento(motor.getEstadoActual());
                    return;
                }
                EstadoJuego rProm = motor.completarPromocion(pProm);
                actualizarUIPostMovimiento(rProm);
                if (rProm.esFinDePartida()) {
                    finalizarPartida();
                } else if (esTurnoDeLaCPU() && !isPaused && motor.getEstadoActual() != EstadoJuego.PROMOCION_REQUIERIDA) {
                    iniciarMovimientoCPU();
                }
                break;
            case ERROR_INTERNO:
                mostrarErrorUsuario("ERROR INTERNO MOTOR");
                panelTablero.clearPieceSelection();
                break;
            default:
                actualizarUIPostMovimiento(resultado);
                if (resultado.esFinDePartida()) {
                    finalizarPartida();
                } else if (esTurnoDeLaCPU() && !isPaused && motor.getEstadoActual() != EstadoJuego.PROMOCION_REQUIERIDA) {
                    iniciarMovimientoCPU();
                }
                break;
        }
        actualizarControlesUI();
    }

    private void setupDynamicFontScaling() {
        SwingUtilities.invokeLater(() -> {
            if (this.getHeight() > 0) {
                this.initialWindowHeight = this.getHeight();
            } else {
                Dimension ps = this.getPreferredSize();
                this.initialWindowHeight = ps.height > 0 ? ps.height : 700;
            }
            if (panelTablero != null) {
                panelTablero.setInitialPanelHeight(initialWindowHeight);
                panelTablero.updateScaleFactor(getCurrentScaleFactor());
            }
            updateDynamicUIElements();
        });
        this.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                updateDynamicUIElements();
                if (panelTablero != null) {
                    panelTablero.updateScaleFactor(getCurrentScaleFactor());
                    panelTablero.repaint();
                }
            }
        });
    }

    private void updateDynamicUIElements() {
        if (initialWindowHeight <= 0) {
            if (this.getHeight() > 100) {
                initialWindowHeight = this.getHeight();
            } else {
                return;
            }
        }
        float scaleFactor = getCurrentScaleFactor();

        float newEstadoFontSize = baseFontEstado.getSize2D() * scaleFactor;
        labelEstadoInfo.setFont(baseFontEstado.deriveFont(Font.BOLD, newEstadoFontSize));
        FontMetrics fmEstado = labelEstadoInfo.getFontMetrics(labelEstadoInfo.getFont());
        int newEstadoLabelHeight = fmEstado.getHeight() + (ESTADO_LABEL_BORDER_PADDING * 2);
        labelEstadoInfo.setPreferredSize(new Dimension(labelEstadoInfo.getPreferredSize().width, newEstadoLabelHeight));

        Font scaledHistorialFont = baseFontHistorial.deriveFont(baseFontHistorial.getSize2D() * scaleFactor);
        tablaHistorial.setFont(scaledHistorialFont);
        JTableHeader header = tablaHistorial.getTableHeader();
        if (header != null) {
            header.setFont(scaledHistorialFont);
        }
        tablaHistorial.setRowHeight(Math.max(1, (int) (baseRowHeightHistorial * scaleFactor)));

        TableCellRenderer rend = tablaHistorial.getColumnModel().getColumn(1).getCellRenderer();
        if (rend instanceof HistorialMovimientoRenderer r) {
            r.updateFontAndPadding(scaledHistorialFont);
            tablaHistorial.getColumnModel().getColumn(2).setCellRenderer(r);
        }
        setHistorialColumnWidths(scaleFactor);

        float newIntroduceFontSize = baseFontIntroduce.getSize2D() * scaleFactor;
        labelIntroduce.setFont(baseFontIntroduce.deriveFont(Font.PLAIN, newIntroduceFontSize));
        float newCampoMovimientoFontSize = baseFontCampoMovimiento.getSize2D() * scaleFactor;
        campoMovimiento.setFont(baseFontCampoMovimiento.deriveFont(Font.BOLD, newCampoMovimientoFontSize));
        FontMetrics fmCampo = campoMovimiento.getFontMetrics(campoMovimiento.getFont());
        int newCampoMovimientoHeight = fmCampo.getHeight() + campoMovimiento.getInsets().top + campoMovimiento.getInsets().bottom;
        newCampoMovimientoHeight = Math.max(newCampoMovimientoHeight, (int) (INPUT_FIELD_BASE_HEIGHT * scaleFactor));
        campoMovimiento.setPreferredSize(new Dimension(campoMovimiento.getPreferredSize().width, newCampoMovimientoHeight));
        campoMovimiento.setMaximumSize(new Dimension(Integer.MAX_VALUE, newCampoMovimientoHeight));

        SwingUtilities.invokeLater(() -> {
            if (panelTablero != null && panelTablero.getHeight() > 0 && panelDerecho != null && !isConcentrationMode) {
                int tableroHeight = panelTablero.getHeight();
                int scaledAnchoMin = (int) (BASE_ANCHO_MIN_PANEL_DERECHO * Math.max(1.0f, scaleFactor * 0.9f));
                int scaledAnchoPref = (int) (BASE_ANCHO_PREF_PANEL_DERECHO * Math.max(1.0f, scaleFactor * 0.9f));
                panelDerecho.setMinimumSize(new Dimension(scaledAnchoMin, tableroHeight));
                panelDerecho.setPreferredSize(new Dimension(scaledAnchoPref, tableroHeight));
                panelDerecho.revalidate();
            }
            if (panelControlesPgn != null) {
                Dimension scaledPgnButtonSize = new Dimension(
                        (int) (TAMANO_BOTON_PGN_NAV.width * Math.max(0.8f, scaleFactor)),
                        (int) (TAMANO_BOTON_PGN_NAV.height * Math.max(0.8f, scaleFactor))
                );
                Font scaledPgnButtonFont = FONT_BOTON_PGN_NAV.deriveFont(FONT_BOTON_PGN_NAV.getStyle(), FONT_BOTON_PGN_NAV.getSize2D() * Math.max(0.8f, scaleFactor));
                for (Component comp : panelControlesPgn.getComponents()) {
                    if (comp instanceof JButton btn) {
                        GuiUtils.applyButtonStyle(btn, scaledPgnButtonFont, scaledPgnButtonSize);
                    }
                }
                panelControlesPgn.revalidate();
            }
        });
        revalidate();
        repaint();
    }

    private void configurarTimersYReplay() {
        timerMensajeError = new Timer(ERROR_MESSAGE_DURATION_MS, e -> restaurarEstadoInfoPostError());
        timerMensajeError.setRepeats(false);

        pgnReplayTimer = new Timer(PGN_REPLAY_DELAY_MS, e -> {
            if (isPgnLoadedMode && isReplayingPgn) {
                procesarMovimientoPgnNavegacion(true);
            } else {
                pgnReplayTimer.stop();
            }
        });
        pgnReplayTimer.setRepeats(true);

        replayPartidaActualTimer = new Timer(PARTIDA_ACTUAL_REPLAY_DELAY_MS, e -> {
            if (isRevisandoPartidaActual && isAutoReplayPartidaActual) {
                procesarMovimientoPartidaActualRevisar(true);
            } else {
                replayPartidaActualTimer.stop();
            }
        });
        replayPartidaActualTimer.setRepeats(true);
    }

    private void configurarEstadoInicialUI() {
        partidaEnCurso = false;
        isPaused = false;

        isReplayingPgn = false;
        isPgnLoadedMode = false;
        if (pgnReplayTimer != null && pgnReplayTimer.isRunning()) {
            pgnReplayTimer.stop();
        }
        pgnLoadedMovements = null;
        currentPgnDisplayIndex = 0;

        isRevisandoPartidaActual = false;
        isAutoReplayPartidaActual = false;
        if (replayPartidaActualTimer != null && replayPartidaActualTimer.isRunning()) {
            replayPartidaActualTimer.stop();
        }
        movimientosPartidaActualRevisar = null;
        currentPartidaActualRevisarIndex = 0;

        motor.iniciarNuevaPartida();
        if (panelTablero != null) {
            panelTablero.setTablero(motor.getTablero());
            panelTablero.setRotado(this.rotarTablero);
            panelTablero.setEstadoJuego(EstadoJuego.EN_CURSO);
            panelTablero.clearPieceSelection();
            panelTablero.setTurnoActual(motor.esTurnoBlancas());
            panelTablero.repaint();
        }
        this.selectedPiecePos = null;
        this.currentLegalMoves = null;
        this.rotarTablero = false;

        actualizarHistorialDisplay(0);
        actualizarEstadoInfo(STATUS_LISTO_TEXT);
        campoMovimiento.setText("");
        stopAllTimers();
        cancelCPUMoveWorker();
        restaurarConfiguracionDefault();
        actualizarControlesUI();
    }

    private void stopAllTimers() {
        if (timerMensajeError != null && timerMensajeError.isRunning()) {
            timerMensajeError.stop();
        }
        if (pgnReplayTimer != null && pgnReplayTimer.isRunning()) {
            pgnReplayTimer.stop();
        }

        if (replayPartidaActualTimer != null && replayPartidaActualTimer.isRunning()) {
            replayPartidaActualTimer.stop();
        }
    }

    private void cancelCPUMoveWorker() {
        if (cpuMoveWorker != null && !cpuMoveWorker.isDone()) {
            cpuMoveWorker.cancel(true);
        }
        cpuMoveWorker = null;
    }

    private void restaurarConfiguracionDefault() {
        cfgJugarComoBlancas = DEFAULT_JUGAR_COMO_BLANCAS;
        cfgModoJuego = DEFAULT_MODO_JUEGO;
        cfgNivelDificultad = DEFAULT_NIVEL_DIFICULTAD;
        motor.setProfundidadBusqueda(cfgNivelDificultad.getProfundidad());
        this.rotarTablero = false;
        if (panelTablero != null) {
            panelTablero.setRotado(false);
            panelTablero.repaint();
        }
    }

    private void mostrarDialogoElegirLado() {
        if (partidaEnCurso || isPgnLoadedMode || isRevisandoPartidaActual) {
            return;
        }
        var dialog = new ElegirLadoDialog(this, this.cfgJugarComoBlancas, getCurrentScaleFactor());
        dialog.setVisible(true);
        if (dialog.seDebeEmpezar()) {
            this.cfgJugarComoBlancas = dialog.getLadoSeleccionadoBlancas();
            boolean debeRotar = !cfgJugarComoBlancas && (cfgModoJuego == ModoJuego.HUMANO_VS_CPU || cfgModoJuego == ModoJuego.CPU_VS_CPU);
            this.rotarTablero = debeRotar;
            if (panelTablero != null) {
                panelTablero.setRotado(this.rotarTablero);
                panelTablero.repaint();
            }
            iniciarPartida();
        }
    }

    private void mostrarDialogoConfirmarFinalizar() {
        if (isPgnLoadedMode || isRevisandoPartidaActual || !partidaEnCurso || motor.getEstadoActual().esFinDePartida()) {
            return;
        }

        var dialog = new ConfirmacionDialog(this,
                "Confirmar Finalización",
                "¿Seguro que quieres Finalizar?",
                "Sí",
                "No",
                getCurrentScaleFactor());
        dialog.setVisible(true);

        if (dialog.isConfirmado()) {
            finalizarPartida();
        } else if (esTurnoDelHumano() && !isPaused) {
            panelTablero.setTurnoActual(motor.esTurnoBlancas());
            panelTablero.setEstadoJuego(motor.getEstadoActual());
            panelTablero.setTablero(motor.getTablero());
            panelTablero.clearPieceSelection();
            panelTablero.repaint();
        }
    }

    private void mostrarDialogoConfirmarCierreApp() {
        var dialog = new ConfirmacionDialog(this,
                "Confirmar Salida",
                "¿Abandonar la Aplicación?",
                "Sí",
                "No",
                getCurrentScaleFactor());
        dialog.setVisible(true);
        if (dialog.isConfirmado()) {
            System.exit(0);
        }
    }

    private void mostrarDialogoConfiguracion() {
        if (isPgnLoadedMode || isRevisandoPartidaActual) {
            return;
        }
        boolean estPausCvC = false;
        if (partidaEnCurso && cfgModoJuego == ModoJuego.CPU_VS_CPU && !isPaused) {
            togglePausa();
            estPausCvC = true;
        } else if (partidaEnCurso && esTurnoDeLaCPU() && !isPaused) {
            cancelCPUMoveWorker();
        }
        var dialog = new ConfiguracionDialog(this, true, cfgModoJuego, cfgNivelDificultad, getCurrentScaleFactor());
        dialog.setVisible(true);

        if (dialog.seAplicaronCambios()) {
            cfgModoJuego = dialog.getModoSeleccionado();
            cfgNivelDificultad = dialog.getNivelDificultadSeleccionado();
            motor.setProfundidadBusqueda(cfgNivelDificultad.getProfundidad());

            boolean debeRotar = !cfgJugarComoBlancas && (cfgModoJuego == ModoJuego.HUMANO_VS_CPU || cfgModoJuego == ModoJuego.CPU_VS_CPU);
            if (this.rotarTablero != debeRotar) {
                this.rotarTablero = debeRotar;
                if (panelTablero != null) {
                    panelTablero.setRotado(this.rotarTablero);
                    panelTablero.repaint();
                }
            }

            if (partidaEnCurso) {
                finalizarPartida();
            } else {
                actualizarEstadoInfo(STATUS_LISTO_TEXT);
                actualizarControlesUI();
            }
        } else {
            if (estPausCvC && partidaEnCurso) {
                togglePausa();
            } else if (partidaEnCurso && esTurnoDeLaCPU() && !isPaused) {
                iniciarMovimientoCPU();
            }
        }
        actualizarControlesUI();
    }

    private void mostrarDialogoInformacion() {
        boolean pgnReplayEstabaCorriendo = false;
        boolean partidaActualReplayEstabaCorriendo = false;

        if (isReplayingPgn && pgnReplayTimer != null && pgnReplayTimer.isRunning()) {
            pgnReplayTimer.stop();
            pgnReplayEstabaCorriendo = true;
        }
        if (isAutoReplayPartidaActual && replayPartidaActualTimer != null && replayPartidaActualTimer.isRunning()) {
            replayPartidaActualTimer.stop();
            partidaActualReplayEstabaCorriendo = true;
        }

        InformacionDialog infoDialog = new InformacionDialog(this, getCurrentScaleFactor());
        infoDialog.setVisible(true);

        if (pgnReplayEstabaCorriendo) {
            pgnReplayTimer.start();
        }
        if (partidaActualReplayEstabaCorriendo) {
            replayPartidaActualTimer.start();
        }

        if (!pgnReplayEstabaCorriendo && !partidaActualReplayEstabaCorriendo) {
            if (isPgnLoadedMode) {
                actualizarEstadoInfo(STATUS_NAVEGANDO_PGN_TEXT);
            } else if (isRevisandoPartidaActual) {
                String baseMsg = STATUS_REVISANDO_PARTIDA_TEXT;
                if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()
                        && movimientosPartidaActualRevisar != null
                        && currentPartidaActualRevisarIndex == movimientosPartidaActualRevisar.size()) {
                    baseMsg = motor.getEstadoActual().getMensaje() + " - " + baseMsg;
                }
                actualizarEstadoInfo(baseMsg.toUpperCase());
            } else if (partidaEnCurso) {
                if (isPaused) {
                    actualizarEstadoInfo(STATUS_PAUSADO_TEXT);
                } else {
                    actualizarEstadoInfo(motor.getEstadoActual());
                }
            } else {
                if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()) {
                    actualizarEstadoInfo(motor.getEstadoActual());
                } else {
                    actualizarEstadoInfo(STATUS_LISTO_TEXT);
                }
            }
        }

        SwingUtilities.invokeLater(() -> {
            this.requestFocusInWindow();
            this.repaint();
        });
    }

    private void iniciarPartida() {
        finalizarModoPgn();
        finalizarModoRevisarPartidaActual();

        stopAllTimers();
        motor.setProfundidadBusqueda(cfgNivelDificultad.getProfundidad());
        cancelCPUMoveWorker();

        partidaEnCurso = true;
        isPaused = false;

        movimientosPartidaActualRevisar = null;
        currentPartidaActualRevisarIndex = 0;

        motor.iniciarNuevaPartida();

        if (panelTablero != null) {
            panelTablero.setRotado(this.rotarTablero);
            panelTablero.setTablero(motor.getTablero());
            panelTablero.setEstadoJuego(EstadoJuego.EN_CURSO);
            panelTablero.clearPieceSelection();
            panelTablero.setTurnoActual(motor.esTurnoBlancas());
            panelTablero.repaint();
        }
        this.selectedPiecePos = null;
        this.currentLegalMoves = null;

        actualizarHistorialDisplay(0);
        actualizarEstadoInfo(motor.getEstadoActual());
        campoMovimiento.setText("");
        actualizarControlesUI();

        if (esTurnoDeLaCPU() && !motor.getEstadoActual().esFinDePartida() && !isPaused && motor.getEstadoActual() != EstadoJuego.PROMOCION_REQUIERIDA) {
            iniciarMovimientoCPU();
        }
    }

    private void finalizarPartida() {
        if (isPgnLoadedMode) {
            return;
        }

        stopAllTimers();
        cancelCPUMoveWorker();

        partidaEnCurso = false;
        isPaused = false;

        if (panelTablero != null) {
            panelTablero.setEstadoJuego(motor.getEstadoActual().esFinDePartida() ? motor.getEstadoActual() : EstadoJuego.EN_CURSO);
            panelTablero.clearPieceSelection();
            panelTablero.repaint();
        }
        this.selectedPiecePos = null;
        this.currentLegalMoves = null;

        if (motor != null && !motor.getHistorialNotacion().isEmpty()) {
            isRevisandoPartidaActual = true;
            isAutoReplayPartidaActual = false;
            movimientosPartidaActualRevisar = new ArrayList<>(motor.getHistorialNotacion());
            currentPartidaActualRevisarIndex = movimientosPartidaActualRevisar.size();

            String mensajeFinJuego = motor.getEstadoActual().esFinDePartida() ? motor.getEstadoActual().getMensaje() : "PARTIDA FINALIZADA";
            actualizarEstadoInfo(mensajeFinJuego + " - " + STATUS_REVISANDO_PARTIDA_TEXT.toUpperCase());
        } else {
            isRevisandoPartidaActual = false;
            if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()) {
                actualizarEstadoInfo(motor.getEstadoActual());
            } else {
                actualizarEstadoInfo(STATUS_LISTO_TEXT);
            }
        }

        campoMovimiento.setText("");
        actualizarHistorialDisplay(0);
        actualizarControlesUI();
    }

    private void actualizarControlesUI() {
        boolean juegoTerminadoOficialmente = (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida());
        boolean partidaActivaReal = partidaEnCurso && !juegoTerminadoOficialmente;

        boolean navegacionPgnActiva = isPgnLoadedMode && pgnLoadedMovements != null && !pgnLoadedMovements.isEmpty();
        boolean navegacionPartidaActualActiva = isRevisandoPartidaActual && movimientosPartidaActualRevisar != null && !movimientosPartidaActualRevisar.isEmpty();

        boolean puedeInteractuarTablero = partidaActivaReal && !isPaused && esTurnoDelHumano()
                && !navegacionPgnActiva && !navegacionPartidaActualActiva
                && !isAutoReplayPartidaActual && !isReplayingPgn;

        botonLateralJugar.setEnabled((!partidaActivaReal || juegoTerminadoOficialmente) && !navegacionPgnActiva && !navegacionPartidaActualActiva);

        boolean mostrarPausa = (cfgModoJuego == ModoJuego.CPU_VS_CPU) && partidaActivaReal
                && !isReplayingPgn && !isAutoReplayPartidaActual;
        botonLateralPausaReanudar.setVisible(mostrarPausa);
        if (mostrarPausa) {
            botonLateralPausaReanudar.setText(isPaused ? "REANUDAR" : "PAUSAR");
        }

        botonLateralConfigurar.setEnabled((!partidaActivaReal || juegoTerminadoOficialmente) && !navegacionPgnActiva && !navegacionPartidaActualActiva);
        botonLateralCargarPGN.setEnabled(!partidaActivaReal && !isReplayingPgn && !isAutoReplayPartidaActual && !isRevisandoPartidaActual);

        boolean mostrarPanelControles = !isConcentrationMode && (navegacionPgnActiva || navegacionPartidaActualActiva);
        panelControlesPgn.setVisible(mostrarPanelControles);

        if (navegacionPgnActiva) {
            botonPgnAnterior.setEnabled(currentPgnDisplayIndex > 0 && !isReplayingPgn);
            botonPgnSiguiente.setEnabled(currentPgnDisplayIndex < pgnLoadedMovements.size() && !isReplayingPgn);
            botonPgnInicio.setEnabled(currentPgnDisplayIndex > 0 && !isReplayingPgn);
            botonPgnFin.setEnabled(currentPgnDisplayIndex < pgnLoadedMovements.size() && !isReplayingPgn);
            botonPgnPlayPause.setText(isReplayingPgn ? "||" : ">");
            botonPgnPlayPause.setEnabled(true);
        } else if (navegacionPartidaActualActiva) {
            boolean hayMovimientos = !movimientosPartidaActualRevisar.isEmpty();
            botonPgnAnterior.setEnabled(hayMovimientos && currentPartidaActualRevisarIndex > 0 && !isAutoReplayPartidaActual);
            botonPgnSiguiente.setEnabled(hayMovimientos && currentPartidaActualRevisarIndex < movimientosPartidaActualRevisar.size() && !isAutoReplayPartidaActual);
            botonPgnInicio.setEnabled(hayMovimientos && currentPartidaActualRevisarIndex > 0 && !isAutoReplayPartidaActual);
            botonPgnFin.setEnabled(hayMovimientos && currentPartidaActualRevisarIndex < movimientosPartidaActualRevisar.size() && !isAutoReplayPartidaActual);
            botonPgnPlayPause.setText(isAutoReplayPartidaActual ? "||" : ">");
            boolean puedeReproducir = hayMovimientos && currentPartidaActualRevisarIndex < movimientosPartidaActualRevisar.size();
            botonPgnPlayPause.setEnabled(hayMovimientos && (isAutoReplayPartidaActual || puedeReproducir));
        } else {
            botonPgnAnterior.setEnabled(false);
            botonPgnSiguiente.setEnabled(false);
            botonPgnInicio.setEnabled(false);
            botonPgnFin.setEnabled(false);
            botonPgnPlayPause.setText(">");
            botonPgnPlayPause.setEnabled(false);
        }

        labelIntroduce.setVisible(false);
        campoMovimiento.setVisible(false);
        campoMovimiento.setEnabled(false);

        panelTablero.setMouseInteractionEnabled(puedeInteractuarTablero);
        panelTablero.setTurnoActual(motor.esTurnoBlancas());

        if (!puedeInteractuarTablero) {
            panelTablero.clearPieceSelection();
            selectedPiecePos = null;
            currentLegalMoves = null;
        }

        boolean hayAlgoQueGuardar = motor != null && (!motor.getHistorialNotacion().isEmpty() || juegoTerminadoOficialmente)
                && !isPgnLoadedMode && !isReplayingPgn
                && !isAutoReplayPartidaActual;
        botonLateralGuardarPGN.setEnabled(hayAlgoQueGuardar);
        botonLateralInformacion.setEnabled(!isReplayingPgn && !isAutoReplayPartidaActual);

        if (panelLateralIzquierdo != null) {
            panelLateralIzquierdo.revalidate();
            panelLateralIzquierdo.repaint();
        }
        if (panelDerecho != null) {
            panelDerecho.revalidate();
            panelDerecho.repaint();
        }
    }

    private void toggleFullScreen() {
        dispose();
        if (isFullScreenMode) {
            setUndecorated(false);
        } else {
            setUndecorated(true);
        }
        isFullScreenMode = !isFullScreenMode;
        if (isFullScreenMode) {
            setExtendedState(JFrame.MAXIMIZED_BOTH);
        } else {
            setExtendedState(JFrame.NORMAL);
        }
        setVisible(true);
        SwingUtilities.invokeLater(() -> {
            revalidate();
            repaint();
            updateDynamicUIElements();
            if (panelTablero != null) {
                panelTablero.updateScaleFactor(getCurrentScaleFactor());
                panelTablero.repaint();
            }
        });
    }

    private void togglePausa() {
        if (isReplayingPgn || isPgnLoadedMode || isRevisandoPartidaActual || isAutoReplayPartidaActual) {
            return;
        }
        if (!partidaEnCurso || motor.getEstadoActual().esFinDePartida() || cfgModoJuego != ModoJuego.CPU_VS_CPU) {
            return;
        }
        isPaused = !isPaused;
        if (isPaused) {
            cancelCPUMoveWorker();
            actualizarEstadoInfo(STATUS_PAUSADO_TEXT);
        } else {
            actualizarEstadoInfo(motor.getEstadoActual());
            if (esTurnoDeLaCPU()) {
                iniciarMovimientoCPU();
            }
        }
        actualizarControlesUI();
    }

    private boolean esTurnoDelHumano() {
        if (!partidaEnCurso || motor.getEstadoActual().esFinDePartida() || isPaused || isReplayingPgn || isPgnLoadedMode || isRevisandoPartidaActual || isAutoReplayPartidaActual) {
            return false;
        }
        boolean turnoActualBlancas = motor.esTurnoBlancas();
        return switch (cfgModoJuego) {
            case HUMANO_VS_HUMANO ->
                true;
            case HUMANO_VS_CPU ->
                turnoActualBlancas == cfgJugarComoBlancas;
            case CPU_VS_CPU ->
                false;
        };
    }

    private boolean esTurnoDeLaCPU() {
        if (!partidaEnCurso || motor.getEstadoActual().esFinDePartida() || isPaused || isReplayingPgn || isPgnLoadedMode || isRevisandoPartidaActual || isAutoReplayPartidaActual) {
            return false;
        }
        boolean turnoActualBlancas = motor.esTurnoBlancas();
        return switch (cfgModoJuego) {
            case HUMANO_VS_HUMANO ->
                false;
            case HUMANO_VS_CPU ->
                turnoActualBlancas != cfgJugarComoBlancas;
            case CPU_VS_CPU ->
                true;
        };
    }

    private void iniciarMovimientoCPU() {
        if (isReplayingPgn || isPgnLoadedMode || isRevisandoPartidaActual || isAutoReplayPartidaActual) {
            return;
        }
        if (!partidaEnCurso || !esTurnoDeLaCPU() || isPaused || motor.getEstadoActual().esFinDePartida() || motor.getEstadoActual() == EstadoJuego.PROMOCION_REQUIERIDA) {
            actualizarControlesUI();
            return;
        }
        cancelCPUMoveWorker();

        int delay = (motor.getHistorialNotacion().isEmpty()) ? PRIMER_MOV_CPU_DELAY_MS : 0;

        cpuMoveWorker = new SwingWorker<>() {
            @Override
            protected EstadoJuego doInBackground() throws Exception {
                if (delay > 0) {
                    Thread.sleep(delay);
                }
                
                return motor.realizarMovimientoCPU();
            }

            @Override
            protected void done() {
                if (isCancelled()) {
                    return;
                }
                EstadoJuego res;
                try {
                    res = get();
                } catch (Exception e) {
                    LOGGER.log(Level.SEVERE, "Error en SwingWorker de CPU", e);
                    res = EstadoJuego.ERROR_INTERNO;
                }

                actualizarUIPostMovimiento(res);

                if (res.esFinDePartida()) {
                    finalizarPartida();
                } else if (cfgModoJuego == ModoJuego.CPU_VS_CPU && !isPaused && res != EstadoJuego.PROMOCION_REQUIERIDA) {
                    iniciarMovimientoCPU();
                } else {
                    actualizarControlesUI();
                }
                cpuMoveWorker = null;
            }
        };
        cpuMoveWorker.execute();
    }

    private void actualizarUIPostMovimiento(EstadoJuego estadoFinal) {
        SwingUtilities.invokeLater(() -> {
            if (panelTablero != null) {
                panelTablero.setTablero(motor.getTablero());
                panelTablero.setEstadoJuego(estadoFinal);
                panelTablero.clearPieceSelection();
                panelTablero.setTurnoActual(motor.esTurnoBlancas());
                panelTablero.repaint();
            }

            actualizarHistorialDisplay(0);
            actualizarEstadoInfo(estadoFinal);
        });
    }

    private char promocionarPeonGrafico() {
        boolean peonEsBlanco = !motor.esTurnoBlancas();
        var dialog = new PromocionDialog(this, peonEsBlanco, getCurrentScaleFactor());
        dialog.setVisible(true);
        return dialog.isSeleccionRealizada() ? dialog.getPiezaSeleccionada() : 0;
    }

    private void actualizarHistorialDisplay(int maxMovesToShowIgnored) {
        List<String> notacionCompleta = Collections.emptyList();
        int indiceMovimientoActual = -1;

        if (isPgnLoadedMode && pgnLoadedMovements != null) {
            notacionCompleta = pgnLoadedMovements;
            indiceMovimientoActual = currentPgnDisplayIndex - 1;
        } else if (isRevisandoPartidaActual && movimientosPartidaActualRevisar != null) {
            notacionCompleta = movimientosPartidaActualRevisar;
            indiceMovimientoActual = currentPartidaActualRevisarIndex - 1;
        } else if (motor != null) {
            notacionCompleta = motor.getHistorialNotacion();
        }

        modeloTablaHistorial.setRowCount(0);
        int turnoNumero = 1;
        for (int i = 0; i < notacionCompleta.size(); i += 2) {
            String blancas = notacionCompleta.get(i);
            String negras = (i + 1 < notacionCompleta.size()) ? notacionCompleta.get(i + 1) : "";
            modeloTablaHistorial.addRow(new Object[]{String.format("%03d", turnoNumero++), blancas, negras});
        }

        final int finalIndiceMovimientoActual = indiceMovimientoActual;

        SwingUtilities.invokeLater(() -> {
            if (tablaHistorial.getRowCount() > 0) {
                int rowToScroll = -1;
                if (finalIndiceMovimientoActual >= 0) {
                    rowToScroll = finalIndiceMovimientoActual / 2;
                }

                if (rowToScroll != -1 && rowToScroll < tablaHistorial.getRowCount()) {
                    int colToScroll = (finalIndiceMovimientoActual % 2 == 0) ? 1 : 2;
                    tablaHistorial.scrollRectToVisible(tablaHistorial.getCellRect(rowToScroll, colToScroll, true));
                } else {
                    tablaHistorial.scrollRectToVisible(tablaHistorial.getCellRect(tablaHistorial.getRowCount() - 1, 0, true));
                }
            }
            tablaHistorial.repaint();
        });
    }

    private void mostrarErrorUsuario(String msg) {
        if (timerMensajeError.isRunning()) {
            timerMensajeError.stop();
        }
        actualizarEstadoInfo(msg);
        timerMensajeError.setInitialDelay(ERROR_MESSAGE_DURATION_MS);
        timerMensajeError.start();
    }

    private void restaurarEstadoInfoPostError() {
        if (!timerMensajeError.isRunning()) {
            if (isReplayingPgn) {
                actualizarEstadoInfo(STATUS_REPRODUCIENDO_PGN_TEXT);
            } else if (isPgnLoadedMode) {
                actualizarEstadoInfo(STATUS_NAVEGANDO_PGN_TEXT);
            } else if (isAutoReplayPartidaActual) {
                actualizarEstadoInfo(STATUS_REPRO_PARTIDA_ACTUAL_TEXT);
            } else if (isRevisandoPartidaActual) {
                String baseMsg = STATUS_REVISANDO_PARTIDA_TEXT;
                if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()
                        && movimientosPartidaActualRevisar != null
                        && currentPartidaActualRevisarIndex == movimientosPartidaActualRevisar.size()) {
                    baseMsg = motor.getEstadoActual().getMensaje() + " - " + baseMsg;
                }
                actualizarEstadoInfo(baseMsg.toUpperCase());
            } else if (!partidaEnCurso) {
                if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()) {
                    actualizarEstadoInfo(motor.getEstadoActual());
                } else {
                    actualizarEstadoInfo(STATUS_LISTO_TEXT);
                }
            } else if (isPaused) {
                actualizarEstadoInfo(STATUS_PAUSADO_TEXT);
            } else {
                actualizarEstadoInfo(motor.getEstadoActual());
            }
        }
    }

    private void actualizarEstadoInfo(EstadoJuego e) {
        if (e == null && !isPgnLoadedMode && !isRevisandoPartidaActual && !partidaEnCurso) {
            actualizarEstadoInfo(STATUS_LISTO_TEXT);
            return;
        }

        if (isReplayingPgn) {
            actualizarEstadoInfo(STATUS_REPRODUCIENDO_PGN_TEXT);
        } else if (isPgnLoadedMode) {
            actualizarEstadoInfo(STATUS_NAVEGANDO_PGN_TEXT);
        } else if (isAutoReplayPartidaActual) {
            actualizarEstadoInfo(STATUS_REPRO_PARTIDA_ACTUAL_TEXT);
        } else if (isRevisandoPartidaActual) {
            String baseMsg = STATUS_REVISANDO_PARTIDA_TEXT;
            if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()
                    && movimientosPartidaActualRevisar != null
                    && currentPartidaActualRevisarIndex == movimientosPartidaActualRevisar.size()) {
                baseMsg = motor.getEstadoActual().getMensaje() + " - " + baseMsg;
            }
            actualizarEstadoInfo(baseMsg.toUpperCase());
        } else if (isPaused && partidaEnCurso && (motor.getEstadoActual() == null || !motor.getEstadoActual().esFinDePartida())) {
            actualizarEstadoInfo(STATUS_PAUSADO_TEXT);
        } else if (!partidaEnCurso && (motor.getEstadoActual() == null || !motor.getEstadoActual().esFinDePartida())) {
            actualizarEstadoInfo(STATUS_LISTO_TEXT);
        } else if (e != null) {
            actualizarEstadoInfo(e.getMensaje());
        } else {
            actualizarEstadoInfo(STATUS_LISTO_TEXT);
        }
    }

    private void actualizarEstadoInfo(String t) {
        final String tf = (t == null ? "" : t).toUpperCase();
        SwingUtilities.invokeLater(() -> {
            labelEstadoInfo.setForeground(COLOR_ESTADO_NORMAL);
            labelEstadoInfo.setText(tf);
        });
    }

    private static class UppercaseDocumentFilter extends DocumentFilter {

        @Override
        public void insertString(FilterBypass fb, int o, String s, AttributeSet a) throws BadLocationException {
            if (s != null) {
                fb.insertString(o, s.toUpperCase(), a);
            }
        }

        @Override
        public void replace(FilterBypass fb, int o, int l, String t, AttributeSet a) throws BadLocationException {
            if (t != null) {
                fb.replace(o, l, t.toUpperCase(), a);
            }
        }
    }

    private void configurarTeclaEscAbandono() {
        InputMap im = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap am = getRootPane().getActionMap();
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "esc");
        am.put("esc", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {

                if (isReplayingPgn) {
                    togglePgnPlayPause();
                    return;
                }
                if (isAutoReplayPartidaActual) {
                    toggleReplayPartidaActual();
                    return;
                }

                if (isPgnLoadedMode || isRevisandoPartidaActual) {
                    var dialog = new ConfirmacionDialog(AjedrezGrafico.this,
                            "Salir del Modo Revisión",
                            "¿Desea salir del modo Revisión?",
                            "Sí", "No",
                            getCurrentScaleFactor());
                    dialog.setVisible(true);
                    if (dialog.isConfirmado()) {
                        if (isPgnLoadedMode) {
                            finalizarModoPgn();
                        } else {
                            finalizarModoRevisarPartidaActual();
                        }
                    }
                    return;
                }

                if (partidaEnCurso && (motor.getEstadoActual() == null || !motor.getEstadoActual().esFinDePartida())) {
                    mostrarDialogoConfirmarFinalizar();
                } else {
                    mostrarDialogoConfirmarCierreApp();
                }
            }
        });
    }

    private void guardarPartidaPGN() {
        boolean juegoTerminadoOficialmente = (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida());
        if (isPgnLoadedMode || isReplayingPgn || isAutoReplayPartidaActual) {
            JOptionPane.showMessageDialog(this, "No se puede guardar mientras se navega un PGN o se reproduce una partida.", "Guardar PGN", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        if (motor == null || (motor.getHistorialNotacion().isEmpty() && !juegoTerminadoOficialmente)) {
            JOptionPane.showMessageDialog(this, "No hay movimientos para guardar o la partida no ha terminado.", "Guardar PGN", JOptionPane.INFORMATION_MESSAGE);
            return;
        }

        JFileChooser fc = new JFileChooser();
        fc.setDialogTitle("Guardar PGN");
        fc.setFileFilter(new FileNameExtensionFilter("PGN (*.pgn)", "pgn"));
        fc.setSelectedFile(new File("PartidaAjedrez_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".pgn"));
        if (fc.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
            File f = fc.getSelectedFile();
            if (!f.getName().toLowerCase().endsWith(".pgn")) {
                f = new File(f.getParentFile(), f.getName() + ".pgn");
            }
            try (FileWriter w = new FileWriter(f)) {
                w.write(motor.generarPGN(cfgModoJuego, cfgJugarComoBlancas));
                JOptionPane.showMessageDialog(this, "PGN guardado:\n" + f.getAbsolutePath(), "Guardado", JOptionPane.INFORMATION_MESSAGE);
            } catch (IOException ex) {
                JOptionPane.showMessageDialog(this, "Error guardando PGN: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    private void cargarPartidaPGN() {
        String advertenciaMsg = null;
        if (partidaEnCurso) {
            advertenciaMsg = "Finalice la partida actual antes de cargar una nueva.";
        } else if (isPgnLoadedMode) {
            advertenciaMsg = "Ya hay un PGN cargado. Finalice la navegación actual primero.";
        } else if (isRevisandoPartidaActual) {
            advertenciaMsg = "Salga del modo de revisión actual antes de cargar un PGN.";
        }

        if (advertenciaMsg != null) {
            JOptionPane.showMessageDialog(this, advertenciaMsg, "Cargar PGN", JOptionPane.WARNING_MESSAGE);
            return;
        }

        JFileChooser fc = new JFileChooser();
        fc.setDialogTitle("Cargar Partida PGN");
        fc.setFileFilter(new FileNameExtensionFilter("Archivos PGN (*.pgn)", "pgn"));

        if (fc.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
            File archivoPgn = fc.getSelectedFile();
            try {
                List<String> lineasPgn = new ArrayList<>();
                try (BufferedReader reader = new BufferedReader(new FileReader(archivoPgn))) {
                    String linea;
                    while ((linea = reader.readLine()) != null) {
                        lineasPgn.add(linea);
                    }
                }

                List<String> movimientosParseados = motor.parsearMovimientosDesdePGN(lineasPgn);

                if (movimientosParseados.isEmpty()) {
                    mostrarErrorUsuario("PGN SIN MOVIMIENTOS");
                    LOGGER.log(Level.WARNING, "El PGN cargado no contenía movimientos parseables.");
                } else {
                    LOGGER.log(Level.INFO, "PGN cargado con " + movimientosParseados.size() + " movimientos. Preparando para navegación.");
                    prepararNavegacionPGN(movimientosParseados);
                }

            } catch (IOException ex) {
                mostrarErrorUsuario(ERROR_PGN_LOAD_TEXT);
                LOGGER.log(Level.SEVERE, "Error al leer el archivo PGN: " + archivoPgn.getAbsolutePath(), ex);
            } catch (IllegalArgumentException ex) {
                mostrarErrorUsuario("ERROR FORMATO PGN");
                LOGGER.log(Level.SEVERE, "Error al parsear PGN: " + ex.getMessage(), ex);
            }
        }
    }

    private void prepararNavegacionPGN(List<String> movimientos) {
        if (movimientos == null || movimientos.isEmpty()) {
            return;
        }
        finalizarModoRevisarPartidaActual();
        finalizarModoPgn();

        isPgnLoadedMode = true;
        pgnLoadedMovements = new ArrayList<>(movimientos);
        currentPgnDisplayIndex = 0;

        motor.iniciarNuevaPartida();
        panelTablero.setTablero(motor.getTablero());
        panelTablero.setEstadoJuego(EstadoJuego.EN_CURSO);
        panelTablero.clearPieceSelection();
        panelTablero.repaint();

        actualizarHistorialDisplay(0);
        actualizarEstadoInfo(STATUS_NAVEGANDO_PGN_TEXT);
        actualizarControlesUI();
    }

    private void procesarMovimientoPgnNavegacion(boolean avanzar) {
        if (!isPgnLoadedMode || pgnLoadedMovements == null || pgnLoadedMovements.isEmpty()) {
            return;
        }
        if (isReplayingPgn && !avanzar) {
            togglePgnPlayPause();
            return;
        }

        int nuevoIndice = currentPgnDisplayIndex;
        if (avanzar) {
            if (currentPgnDisplayIndex < pgnLoadedMovements.size()) {
                nuevoIndice++;
            } else {
                if (isReplayingPgn) {
                    togglePgnPlayPause();
                }
                return;
            }
        } else {
            if (currentPgnDisplayIndex > 0) {
                nuevoIndice--;
            } else {
                return;
            }
        }

        motor.iniciarNuevaPartida();
        for (int i = 0; i < nuevoIndice; i++) {
            String movimientoPGN = pgnLoadedMovements.get(i);
            MovimientoParseadoReplay movParseado = parsearMovimientoReplay(movimientoPGN);
            if (movParseado == null) {
                LOGGER.log(Level.SEVERE, "Navegación PGN: Error parseando movimiento '" + movimientoPGN + "' al reaplicar.");
                finalizarModoPgn();
                mostrarErrorUsuario("ERROR MOV PGN");
                return;
            }
            EstadoJuego res = motor.intentarMover(movParseado.origen(), movParseado.destino());
            if (res == EstadoJuego.PROMOCION_REQUIERIDA) {
                if (movParseado.piezaPromocion() == 0) {
                    LOGGER.log(Level.SEVERE, "Navegación PGN: Promoción requerida pero no especificada para " + movimientoPGN);
                    motor.cancelarPromocionPendiente();
                    finalizarModoPgn();
                    mostrarErrorUsuario("ERROR PROMO PGN");
                    return;
                }
                motor.completarPromocion(movParseado.piezaPromocion());
            } else if (res == EstadoJuego.MOVIMIENTO_INVALIDO || res == EstadoJuego.ERROR_INTERNO) {
                LOGGER.log(Level.SEVERE, "Navegación PGN: Movimiento inválido/error al reaplicar: " + movimientoPGN + " (" + res + ")");
                finalizarModoPgn();
                mostrarErrorUsuario("ERROR MOV PGN");
                return;
            }
        }

        currentPgnDisplayIndex = nuevoIndice;
        actualizarUIPostMovimiento(motor.getEstadoActual());
        actualizarControlesUI();

        if (isReplayingPgn && currentPgnDisplayIndex >= pgnLoadedMovements.size()) {
            togglePgnPlayPause();
        }
    }

    private void togglePgnPlayPause() {
        if (!isPgnLoadedMode || pgnLoadedMovements == null || pgnLoadedMovements.isEmpty()) {
            return;
        }
        isReplayingPgn = !isReplayingPgn;
        if (isReplayingPgn) {
            actualizarEstadoInfo(STATUS_REPRODUCIENDO_PGN_TEXT);
            if (currentPgnDisplayIndex >= pgnLoadedMovements.size()) {
                irAlInicioPgn();
            }
            pgnReplayTimer.start();
        } else {
            pgnReplayTimer.stop();
            actualizarEstadoInfo(STATUS_NAVEGANDO_PGN_TEXT);
        }
        actualizarControlesUI();
    }

    private void irAlInicioPgn() {
        if (!isPgnLoadedMode) {
            return;
        }
        if (isReplayingPgn) {
            isReplayingPgn = false;
            pgnReplayTimer.stop();
        }

        currentPgnDisplayIndex = 0;
        motor.iniciarNuevaPartida();
        actualizarUIPostMovimiento(motor.getEstadoActual());
        actualizarControlesUI();
    }

    private void irAlFinPgn() {
        if (!isPgnLoadedMode || pgnLoadedMovements == null || pgnLoadedMovements.isEmpty()) {
            return;
        }
        if (isReplayingPgn) {
            isReplayingPgn = false;
            pgnReplayTimer.stop();
        }

        int targetIndex = pgnLoadedMovements.size();

        motor.iniciarNuevaPartida();
        for (int i = 0; i < targetIndex; i++) {
            String movimientoPGN = pgnLoadedMovements.get(i);
            MovimientoParseadoReplay movParseado = parsearMovimientoReplay(movimientoPGN);
            if (movParseado == null) {
                LOGGER.log(Level.SEVERE, "Navegación PGN (Fin): Error parseando movimiento '" + movimientoPGN + "'.");
                finalizarModoPgn();
                mostrarErrorUsuario("ERROR MOV PGN");
                return;
            }
            EstadoJuego res = motor.intentarMover(movParseado.origen(), movParseado.destino());
            if (res == EstadoJuego.PROMOCION_REQUIERIDA) {
                if (movParseado.piezaPromocion() == 0) {
                    LOGGER.log(Level.SEVERE, "Navegación PGN (Fin): Promoción sin pieza para " + movimientoPGN);
                    motor.cancelarPromocionPendiente();
                    finalizarModoPgn();
                    mostrarErrorUsuario("ERROR PROMO PGN");
                    return;
                }
                motor.completarPromocion(movParseado.piezaPromocion());
            } else if (res == EstadoJuego.MOVIMIENTO_INVALIDO || res == EstadoJuego.ERROR_INTERNO) {
                LOGGER.log(Level.SEVERE, "Navegación PGN (Fin): Movimiento inválido/error " + movimientoPGN + " (" + res + ")");
                finalizarModoPgn();
                mostrarErrorUsuario("ERROR MOV PGN");
                return;
            }
        }
        currentPgnDisplayIndex = targetIndex;
        actualizarUIPostMovimiento(motor.getEstadoActual());
        actualizarControlesUI();
    }

    private void finalizarModoPgn() {
        if (pgnReplayTimer != null && pgnReplayTimer.isRunning()) {
            pgnReplayTimer.stop();
        }
        isPgnLoadedMode = false;
        isReplayingPgn = false;
        pgnLoadedMovements = null;
        currentPgnDisplayIndex = 0;

        if (isRevisandoPartidaActual) {
            motor.iniciarNuevaPartida();
            for (int i = 0; i < currentPartidaActualRevisarIndex; i++) {
                if (movimientosPartidaActualRevisar != null && i < movimientosPartidaActualRevisar.size()) {
                    String mov = movimientosPartidaActualRevisar.get(i);
                    MovimientoParseadoReplay mp = parsearMovimientoReplay(mov);
                    if (mp != null) {
                        EstadoJuego r = motor.intentarMover(mp.origen(), mp.destino());
                        if (r == EstadoJuego.PROMOCION_REQUIERIDA && mp.piezaPromocion() != 0) {
                            motor.completarPromocion(mp.piezaPromocion());
                        }
                    }
                }
            }
            actualizarUIPostMovimiento(motor.getEstadoActual());
            actualizarEstadoInfo(STATUS_REVISANDO_PARTIDA_TEXT);
        } else if (partidaEnCurso) {
            finalizarPartida();
        } else {
            motor.iniciarNuevaPartida();
            actualizarUIPostMovimiento(motor.getEstadoActual());
            actualizarEstadoInfo(STATUS_LISTO_TEXT);
        }

        actualizarHistorialDisplay(0);
        actualizarControlesUI();
    }

    private void finalizarModoRevisarPartidaActual() {
        if (replayPartidaActualTimer != null && replayPartidaActualTimer.isRunning()) {
            replayPartidaActualTimer.stop();
        }
        isRevisandoPartidaActual = false;
        isAutoReplayPartidaActual = false;

        if (partidaEnCurso) {
            actualizarEstadoInfo(motor.getEstadoActual());
        } else if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()) {
            actualizarEstadoInfo(motor.getEstadoActual());
        } else {
            actualizarEstadoInfo(STATUS_LISTO_TEXT);
        }
        actualizarControlesUI();
    }

    private void procesarMovimientoPartidaActualRevisar(boolean avanzar) {
        if (!isRevisandoPartidaActual || movimientosPartidaActualRevisar == null || movimientosPartidaActualRevisar.isEmpty()) {
            return;
        }

        if (isAutoReplayPartidaActual && !avanzar) {
            toggleReplayPartidaActual();
            return;
        }

        int nuevoIndice = currentPartidaActualRevisarIndex;
        if (avanzar) {
            if (currentPartidaActualRevisarIndex < movimientosPartidaActualRevisar.size()) {
                nuevoIndice++;
            } else {
                if (isAutoReplayPartidaActual) {
                    toggleReplayPartidaActual();
                }
                return;
            }
        } else {
            if (currentPartidaActualRevisarIndex > 0) {
                nuevoIndice--;
            } else {
                return;
            }
        }

        motor.iniciarNuevaPartida();
        for (int i = 0; i < nuevoIndice; i++) {
            String movimientoPGN = movimientosPartidaActualRevisar.get(i);
            MovimientoParseadoReplay movParseado = parsearMovimientoReplay(movimientoPGN);

            if (movParseado == null) {
                LOGGER.log(Level.SEVERE, "Revisión Partida Actual: Error parseando movimiento '" + movimientoPGN + "' al reaplicar.");
                finalizarModoRevisarPartidaActual();
                mostrarErrorUsuario("ERROR MOV REVISIÓN");
                return;
            }
            EstadoJuego res = motor.intentarMover(movParseado.origen(), movParseado.destino());
            if (res == EstadoJuego.PROMOCION_REQUIERIDA) {
                if (movParseado.piezaPromocion() == 0) {
                    LOGGER.log(Level.SEVERE, "Revisión Partida Actual: Promoción requerida pero no especificada para " + movimientoPGN);
                    motor.cancelarPromocionPendiente();
                    finalizarModoRevisarPartidaActual();
                    mostrarErrorUsuario("ERROR PROMO REVISIÓN");
                    return;
                }
                motor.completarPromocion(movParseado.piezaPromocion());
            } else if (res == EstadoJuego.MOVIMIENTO_INVALIDO || res == EstadoJuego.ERROR_INTERNO) {
                LOGGER.log(Level.SEVERE, "Revisión Partida Actual: Movimiento inválido/error al reaplicar: " + movimientoPGN + " (" + res + ")");
                finalizarModoRevisarPartidaActual();
                mostrarErrorUsuario("ERROR MOV REVISIÓN");
                return;
            }
        }

        currentPartidaActualRevisarIndex = nuevoIndice;
        actualizarUIPostMovimiento(motor.getEstadoActual());
        actualizarControlesUI();

        if (isAutoReplayPartidaActual && currentPartidaActualRevisarIndex >= movimientosPartidaActualRevisar.size()) {
            toggleReplayPartidaActual();
        }
    }

    private void toggleReplayPartidaActual() {
        if (!isRevisandoPartidaActual || movimientosPartidaActualRevisar == null || movimientosPartidaActualRevisar.isEmpty()) {
            return;
        }
        isAutoReplayPartidaActual = !isAutoReplayPartidaActual;

        if (isAutoReplayPartidaActual) {
            actualizarEstadoInfo(STATUS_REPRO_PARTIDA_ACTUAL_TEXT);
            if (currentPartidaActualRevisarIndex >= movimientosPartidaActualRevisar.size()) {
                irAlInicioPartidaActualRevisar();
            }
            replayPartidaActualTimer.start();
        } else {
            replayPartidaActualTimer.stop();
            String baseMsg = STATUS_REVISANDO_PARTIDA_TEXT;
            if (motor.getEstadoActual() != null && motor.getEstadoActual().esFinDePartida()
                    && currentPartidaActualRevisarIndex == movimientosPartidaActualRevisar.size()) {
                baseMsg = motor.getEstadoActual().getMensaje() + " - " + baseMsg;
            }
            actualizarEstadoInfo(baseMsg.toUpperCase());
        }
        actualizarControlesUI();
    }

    private void irAlInicioPartidaActualRevisar() {
        if (!isRevisandoPartidaActual) {
            return;
        }
        if (isAutoReplayPartidaActual) {
            isAutoReplayPartidaActual = false;
            replayPartidaActualTimer.stop();
        }
        currentPartidaActualRevisarIndex = 0;
        motor.iniciarNuevaPartida();
        actualizarUIPostMovimiento(motor.getEstadoActual());
        actualizarControlesUI();
    }

    private void irAlFinPartidaActualRevisar() {
        if (!isRevisandoPartidaActual || movimientosPartidaActualRevisar == null || movimientosPartidaActualRevisar.isEmpty()) {
            return;
        }
        if (isAutoReplayPartidaActual) {
            isAutoReplayPartidaActual = false;
            replayPartidaActualTimer.stop();
        }
        int targetIndex = movimientosPartidaActualRevisar.size();
        motor.iniciarNuevaPartida();
        for (int i = 0; i < targetIndex; i++) {
            String movimientoPGN = movimientosPartidaActualRevisar.get(i);
            MovimientoParseadoReplay movParseado = parsearMovimientoReplay(movimientoPGN);
            if (movParseado == null) {
                LOGGER.log(Level.SEVERE, "Revisión Partida (Fin): Error parseando '" + movimientoPGN + "'.");
                finalizarModoRevisarPartidaActual();
                mostrarErrorUsuario("ERROR MOV REVISIÓN");
                return;
            }
            EstadoJuego res = motor.intentarMover(movParseado.origen(), movParseado.destino());
            if (res == EstadoJuego.PROMOCION_REQUIERIDA) {
                if (movParseado.piezaPromocion() == 0) {
                    LOGGER.log(Level.SEVERE, "Revisión Partida (Fin): Promoción sin pieza para " + movimientoPGN);
                    motor.cancelarPromocionPendiente();
                    finalizarModoRevisarPartidaActual();
                    mostrarErrorUsuario("ERROR PROMO REVISIÓN");
                    return;
                }
                motor.completarPromocion(movParseado.piezaPromocion());
            } else if (res == EstadoJuego.MOVIMIENTO_INVALIDO || res == EstadoJuego.ERROR_INTERNO) {
                LOGGER.log(Level.SEVERE, "Revisión Partida (Fin): Movimiento inválido/error " + movimientoPGN + " (" + res + ")");
                finalizarModoRevisarPartidaActual();
                mostrarErrorUsuario("ERROR MOV REVISIÓN");
                return;
            }
        }
        currentPartidaActualRevisarIndex = targetIndex;
        actualizarUIPostMovimiento(motor.getEstadoActual());
        actualizarControlesUI();
    }

    private record MovimientoParseadoReplay(Posicion origen, Posicion destino, char piezaPromocion) {

    }

    private MovimientoParseadoReplay parsearMovimientoReplay(String movimientoPGN) {
        if (movimientoPGN == null || movimientoPGN.trim().isEmpty()) {
            return null;
        }
        movimientoPGN = movimientoPGN.trim();

        boolean esTurnoBlancasQueMueven = motor.esTurnoBlancas();

        if (movimientoPGN.equalsIgnoreCase("O-O") || movimientoPGN.equalsIgnoreCase("0-0")) {
            Posicion origenRey = esTurnoBlancasQueMueven ? Posicion.desdeNotacion("e1") : Posicion.desdeNotacion("e8");
            Posicion destinoRey = esTurnoBlancasQueMueven ? Posicion.desdeNotacion("g1") : Posicion.desdeNotacion("g8");
            if (origenRey == null || destinoRey == null) {
                return null;
            }
            return new MovimientoParseadoReplay(origenRey, destinoRey, (char) 0);
        }
        if (movimientoPGN.equalsIgnoreCase("O-O-O") || movimientoPGN.equalsIgnoreCase("0-0-0")) {
            Posicion origenRey = esTurnoBlancasQueMueven ? Posicion.desdeNotacion("e1") : Posicion.desdeNotacion("e8");
            Posicion destinoRey = esTurnoBlancasQueMueven ? Posicion.desdeNotacion("c1") : Posicion.desdeNotacion("c8");
            if (origenRey == null || destinoRey == null) {
                return null;
            }
            return new MovimientoParseadoReplay(origenRey, destinoRey, (char) 0);
        }

        String origenStr, destinoStr;
        char piezaPromocion = 0;

        movimientoPGN = movimientoPGN.replaceAll("[+#?!]$", "");

        int idxIgual = movimientoPGN.indexOf('=');
        if (idxIgual != -1) {
            if (idxIgual + 1 < movimientoPGN.length()) {
                piezaPromocion = Character.toUpperCase(movimientoPGN.charAt(idxIgual + 1));
            }
            movimientoPGN = movimientoPGN.substring(0, idxIgual);
        }

        if (movimientoPGN.matches("^[a-h][1-8][a-h][1-8]$")) {
            origenStr = movimientoPGN.substring(0, 2);
            destinoStr = movimientoPGN.substring(2, 4);
            Posicion origen = Posicion.desdeNotacion(origenStr);
            Posicion destino = Posicion.desdeNotacion(destinoStr);
            if (origen != null && destino != null) {
                return new MovimientoParseadoReplay(origen, destino, piezaPromocion);
            }
        }

        if (movimientoPGN.matches("^[a-h][1-8]$")) {
            Posicion destino = Posicion.desdeNotacion(movimientoPGN);
            if (destino != null) {
                List<Movimiento> posiblesMovs = motor.getMovimientosLegales().stream()
                        .filter(m -> m.getPiezaMovida() instanceof Peon
                        && m.getDestino().equals(destino)
                        && m.getPiezaMovida().esBlanca() == esTurnoBlancasQueMueven)
                        .collect(Collectors.toList());

                if (posiblesMovs.size() == 1) {
                    return new MovimientoParseadoReplay(posiblesMovs.get(0).getOrigen(), destino, piezaPromocion);
                } else if (posiblesMovs.size() > 1) {
                    LOGGER.log(Level.WARNING, "Replay Parser: Ambiguo para movimiento de peón (formato 'e4'): " + movimientoPGN + ". Múltiples orígenes.");
                } else {
                    LOGGER.log(Level.WARNING, "Replay Parser: No se encontró origen para movimiento de peón (formato 'e4'): " + movimientoPGN);
                }
            }
        }
        LOGGER.log(Level.FINE, "Replay Parser: Movimiento '" + movimientoPGN + "' no es formato 'e2e4', 'O-O', 'O-O-O' o 'e4' simple. Se intentará como SAN genérico con limitaciones.");
        return null;
    }

    public static void main(String[] args) {
        LOGGER.setLevel(Level.INFO);
        try {
            UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
            UIManager.put("Button.paintBorders", true);
            UIManager.put("Component.arc", 5);
            UIManager.put("ProgressBar.arc", 5);
            UIManager.put("TextComponent.arc", 5);
        } catch (Exception ex) {
            try {
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } catch (Exception e) {
                try {
                    UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
                } catch (Exception ex2) {
                    LOGGER.log(Level.SEVERE, "Error setting L&F", ex2);
                }
            }
        }
        final SplashScreen splash = new SplashScreen();
        splash.startProgress(1300);
        SwingUtilities.invokeLater(() -> splash.setVisible(true));
        new Timer(2300, e -> {
            splash.dispose();
            SwingUtilities.invokeLater(() -> new AjedrezGrafico().setVisible(true));
        }) {
            {
                setRepeats(false);
                start();
            }
        };
    }

    private static class HistorialMovimientoRenderer extends DefaultTableCellRenderer {

        private Font currentScaledFont;
        private final int MAX_CHARS = 12;
        private int horizontalPaddingPx;

        public HistorialMovimientoRenderer(Font baseFont) {
            updateFontAndPadding(baseFont);
            setHorizontalAlignment(JLabel.LEFT);
        }

        public void updateFontAndPadding(Font newScaledFont) {
            this.currentScaledFont = newScaledFont;
            FontMetrics fm = getFontMetrics(newScaledFont);
            this.horizontalPaddingPx = Math.max(3, fm.charWidth('M') / 2 + 1);
            setBorder(new EmptyBorder(2, horizontalPaddingPx, 2, horizontalPaddingPx));
        }

        public int getHorizontalPadding() {
            return horizontalPaddingPx;
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value,
                boolean isSelected, boolean hasFocus, int row, int column) {
            JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

            String sValue = (value == null) ? "" : value.toString();
            String texto;

            if (sValue.isEmpty()) {
                texto = "";
            } else {
                texto = "  " + sValue;
            }

            if (texto.length() > MAX_CHARS) {
                String baseMov = sValue;
                if (baseMov.length() > (MAX_CHARS - 2)) {
                    baseMov = baseMov.substring(0, MAX_CHARS - 2);
                }
                texto = "  " + baseMov;
            }

            label.setText(texto);
            label.setFont(currentScaledFont);

            if (!isSelected) {
                if (label.getBackground() != Color.CYAN.darker()) {
                    label.setBackground(row % 2 == 0 ? Color.WHITE : COLOR_FILA_ALTERNADA_HISTORIAL);
                }
            }
            return label;
        }
    }
}


Código Java (PanelTablero.java):

package ajedrez_gui;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.AffineTransform;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

import java.awt.Cursor;
import javax.swing.JPanel;

public class PanelTablero extends JPanel {

    private static final Logger LOGGER = Logger.getLogger(PanelTablero.class.getName());

    private Tablero tableroJuego;
    private final int TAMANO_PANEL_PREFERIDO_BASE = 550;
    private final int CASILLAS = 8;
    private final int ESPACIO_COORD_BASE = 8;

    private static final String RUTA_FUENTE_PIEZAS = "/ajedrez_gui/assets/CASEFONT.TTF";
    private static final String FUENTE_FALLBACK_POR_DEFECTO = "Dialog";
    private final Font FONT_PIEZAS_BASE;
    private Font baseFontCoordenadas;

    private final Color BACKGROUND_COLOR = new Color(190, 190, 190);
    private final Color SQUARE_COLOR_DARK = Color.GRAY;
    private final Color COLOR_COORDENADAS = Color.BLACK;
    private final Color COLOR_CONTORNO = Color.BLACK;
    private final Color COLOR_PIEZA_BLANCA = Color.WHITE;
    private final Color COLOR_PIEZA_NEGRA = Color.BLACK;

    private static final Color HIGHLIGHT_SELECTION_COLOR = new Color(255, 255, 0, 100);
    private static final Color HIGHLIGHT_POSSIBLE_MOVE_COLOR = new Color(0, 255, 0, 100);
    private static final Color HIGHLIGHT_CAPTURE_COLOR = new Color(255, 0, 0, 100);
    private static final float HIGHLIGHT_STROKE_WIDTH = 3f;

    private boolean rotado = false;
    private EstadoJuego estadoActualDelJuego = EstadoJuego.EN_CURSO;
    private boolean isMouseInteractionEnabled = true;
    private boolean isWhiteTurn = true;

    private Posicion selectedPos = null; 
    private List<Posicion> validDestinations = new ArrayList<>(); 
    private Point dragPoint = null;
    private Pieza draggedPiece = null;
    private Point dragOffset = null;

    private final Consumer<Posicion> clickCallback;
    private final BiConsumer<Posicion, Posicion> dragDropCallback;

    private float currentScaleFactor = 1.0f;
    private float initialPanelHeightForScaling;

    public PanelTablero(Consumer<Posicion> clickHandler, BiConsumer<Posicion, Posicion> dragDropHandler, Font baseFontCoordenadas) {
        this.clickCallback = clickHandler;
        this.dragDropCallback = dragDropHandler;
        this.baseFontCoordenadas = baseFontCoordenadas;

        FONT_PIEZAS_BASE = GuiUtils.cargarFuenteBaseDesdeRecurso(RUTA_FUENTE_PIEZAS, FUENTE_FALLBACK_POR_DEFECTO);
        this.setPreferredSize(new Dimension(TAMANO_PANEL_PREFERIDO_BASE, TAMANO_PANEL_PREFERIDO_BASE));
        this.setBackground(BACKGROUND_COLOR);
        this.tableroJuego = null;

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (!isMouseInteractionEnabled || PanelTablero.this.draggedPiece != null) {
                    return;
                }
                int squareSize = getSquareSize();
                if (squareSize <= 0) {
                    return;
                }

                Posicion clickedPosLogic = getBoardPosFromMouse(e.getX(), e.getY(), squareSize);
                PanelTablero.this.selectedPos = clickedPosLogic;

                if (clickedPosLogic == null) {
                    clickCallback.accept(null);
                    return;
                }

                Pieza pieceAtClickedPos = tableroJuego != null ? tableroJuego.obtenerPieza(clickedPosLogic) : null;
                boolean canSelectOrDrag = (pieceAtClickedPos != null && pieceAtClickedPos.esBlanca() == isWhiteTurn);

                if (canSelectOrDrag) {
                    PanelTablero.this.draggedPiece = pieceAtClickedPos;
                    Rectangle rectCasilla = getGraphicalSquareRect(clickedPosLogic, squareSize);
                    PanelTablero.this.dragOffset = new Point(e.getX() - rectCasilla.x, e.getY() - rectCasilla.y);
                    PanelTablero.this.dragPoint = e.getPoint();
                } else {
                    PanelTablero.this.draggedPiece = null;
                }
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                Posicion originOfDragAtPress = PanelTablero.this.selectedPos;
                Pieza pieceBeingDraggedNow = PanelTablero.this.draggedPiece;

                clearInternalDragState();

                if (!isMouseInteractionEnabled) {
                    repaint();
                    return;
                }

                int squareSize = getSquareSize();
                if (squareSize <= 0) {
                    repaint();
                    return;
                }
                Posicion releasedPosLogic = getBoardPosFromMouse(e.getX(), e.getY(), squareSize);

                if (pieceBeingDraggedNow != null && originOfDragAtPress != null) {
                    if (releasedPosLogic != null && !originOfDragAtPress.equals(releasedPosLogic)) {
                        dragDropCallback.accept(originOfDragAtPress, releasedPosLogic);
                    } else {
                        clickCallback.accept(originOfDragAtPress);
                    }
                } else if (originOfDragAtPress != null) {
                    clickCallback.accept(originOfDragAtPress);
                } else if (releasedPosLogic != null) {
                    clickCallback.accept(releasedPosLogic);
                } else {
                    clickCallback.accept(null);
                }
                repaint();
            }

            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.isConsumed() || (PanelTablero.this.dragPoint != null && PanelTablero.this.dragPoint.distance(e.getPoint()) > 5)) {
                    e.consume();
                    return;
                }
                if (!isMouseInteractionEnabled) {
                    return;
                }
                int squareSize = getSquareSize();
                if (squareSize <= 0) {
                    return;
                }
                Posicion clickedPosLogic = getBoardPosFromMouse(e.getX(), e.getY(), squareSize);
                clickCallback.accept(clickedPosLogic);
            }
        });

        addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                if (!isMouseInteractionEnabled || PanelTablero.this.draggedPiece == null) {
                    return;
                }
                PanelTablero.this.dragPoint = e.getPoint();
                e.consume();
                repaint();
            }
        });
    }

    public void setInitialPanelHeight(float height) {
        this.initialPanelHeightForScaling = height;
    }

    public void updateScaleFactor(float newScaleFactor) {
        if (Math.abs(this.currentScaleFactor - newScaleFactor) > 0.01f) {
            this.currentScaleFactor = newScaleFactor;
        }
    }

    private int getSquareSize() {
        Font currentCoordsFont = baseFontCoordenadas.deriveFont(baseFontCoordenadas.getSize2D() * currentScaleFactor);
        FontMetrics fmCoord = getFontMetrics(currentCoordsFont);
        int scaledEspacioCoord = Math.max(1, (int) (ESPACIO_COORD_BASE * currentScaleFactor));
        int paddingInferior = fmCoord.getHeight() + scaledEspacioCoord;
        int paddingIzquierdo = fmCoord.stringWidth("8") + scaledEspacioCoord;
        int paddingSuperior = fmCoord.getAscent() / 2 + scaledEspacioCoord / 2;
        int anchoPanel = getWidth();
        int altoPanel = getHeight();
        int anchoTableroEfectivo = anchoPanel - paddingIzquierdo - scaledEspacioCoord;
        int altoTableroEfectivo = altoPanel - paddingSuperior - paddingInferior;
        int tamanoTableroDibujo = Math.min(anchoTableroEfectivo, altoTableroEfectivo);
        if (tamanoTableroDibujo < CASILLAS) {
            tamanoTableroDibujo = CASILLAS;
        }
        return tamanoTableroDibujo / CASILLAS;
    }

    private Posicion getBoardPosFromMouse(int mouseX, int mouseY, int squareSize) {
        if (squareSize <= 0) {
            return null;
        }
        Font currentCoordsFont = baseFontCoordenadas.deriveFont(baseFontCoordenadas.getSize2D() * currentScaleFactor);
        FontMetrics fmCoord = getFontMetrics(currentCoordsFont);
        int scaledEspacioCoord = Math.max(1, (int) (ESPACIO_COORD_BASE * currentScaleFactor));
        int paddingInferior = fmCoord.getHeight() + scaledEspacioCoord;
        int paddingIzquierdo = fmCoord.stringWidth("8") + scaledEspacioCoord;
        int paddingSuperior = fmCoord.getAscent() / 2 + scaledEspacioCoord / 2;
        int anchoPanel = getWidth();
        int altoPanel = getHeight();
        int anchoTableroEfectivo = anchoPanel - paddingIzquierdo - scaledEspacioCoord;
        int altoTableroEfectivo = altoPanel - paddingSuperior - paddingInferior;
        int tamanoTableroReal = squareSize * CASILLAS;
        int offsetXTablero = paddingIzquierdo + (anchoTableroEfectivo - tamanoTableroReal) / 2;
        int offsetYTablero = paddingSuperior + (altoTableroEfectivo - tamanoTableroReal) / 2;
        int col = (mouseX - offsetXTablero) / squareSize;
        int row = (mouseY - offsetYTablero) / squareSize;
        if (col < 0 || col >= CASILLAS || row < 0 || row >= CASILLAS) {
            return null;
        }
        int logicRow, logicCol;
        if (rotado) {
            logicRow = row;
            logicCol = (CASILLAS - 1) - col;
        } else {
            logicRow = (CASILLAS - 1) - row;
            logicCol = col;
        }
        return new Posicion(logicRow, logicCol);
    }

    private Rectangle getGraphicalSquareRect(Posicion logicPos, int squareSize) {
        if (squareSize <= 0) {
            return new Rectangle(0, 0, 0, 0);
        }
        Font currentCoordsFont = baseFontCoordenadas.deriveFont(baseFontCoordenadas.getSize2D() * currentScaleFactor);
        FontMetrics fmCoord = getFontMetrics(currentCoordsFont);
        int scaledEspacioCoord = Math.max(1, (int) (ESPACIO_COORD_BASE * currentScaleFactor));
        int paddingInferior = fmCoord.getHeight() + scaledEspacioCoord;
        int paddingIzquierdo = fmCoord.stringWidth("8") + scaledEspacioCoord;
        int paddingSuperior = fmCoord.getAscent() / 2 + scaledEspacioCoord / 2;
        int anchoPanel = getWidth();
        int altoPanel = getHeight();
        int anchoTableroEfectivo = anchoPanel - paddingIzquierdo - scaledEspacioCoord;
        int altoTableroEfectivo = altoPanel - paddingSuperior - paddingInferior;
        int tamanoTableroReal = squareSize * CASILLAS;
        int offsetXTablero = paddingIzquierdo + (anchoTableroEfectivo - tamanoTableroReal) / 2;
        int offsetYTablero = paddingSuperior + (altoTableroEfectivo - tamanoTableroReal) / 2;
        int rowGraf, colGraf;
        if (rotado) {
            rowGraf = logicPos.x();
            colGraf = (CASILLAS - 1) - logicPos.y();
        } else {
            rowGraf = (CASILLAS - 1) - logicPos.x();
            colGraf = logicPos.y();
        }
        return new Rectangle(offsetXTablero + colGraf * squareSize, offsetYTablero + rowGraf * squareSize, squareSize, squareSize);
    }

    public void setRotado(boolean rotado) {
        if (this.rotado != rotado) {
            this.rotado = rotado;
            this.repaint();
        }
    }

    public void setTablero(Tablero tablero) {
        this.tableroJuego = tablero;
        this.repaint();
    }

    public void setEstadoJuego(EstadoJuego estado) {
        EstadoJuego nuevo = (estado != null) ? estado : EstadoJuego.EN_CURSO;
        if (this.estadoActualDelJuego != nuevo) {
            this.estadoActualDelJuego = nuevo;
            this.repaint();
        }
    }

    public void setMouseInteractionEnabled(boolean enabled) {
        this.isMouseInteractionEnabled = enabled;
        this.setCursor(enabled ? Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) : Cursor.getDefaultCursor());
    }

    public void setTurnoActual(boolean isWhiteTurn) {
        this.isWhiteTurn = isWhiteTurn;
    }

    public void setPieceSelection(Posicion selectedPos, List<Posicion> validDestinations) {
        this.selectedPos = selectedPos;
        this.validDestinations = (validDestinations != null) ? new ArrayList<>(validDestinations) : new ArrayList<>();
        this.repaint();
    }

    public void clearPieceSelection() {
        this.selectedPos = null;
        this.validDestinations.clear();
        clearInternalDragState();
        this.repaint();
    }

    private void clearInternalDragState() {
        this.draggedPiece = null;
        this.dragPoint = null;
        this.dragOffset = null;
    }

    public boolean hasVisualSelection() {
        return this.selectedPos != null || (this.validDestinations != null && !this.validDestinations.isEmpty());
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (!(g instanceof Graphics2D g2)) {
            return;
        }
        setupAntialiasing(g2);
        int squareSize = getSquareSize();
        if (squareSize <= 0) {
            g2.setColor(Color.RED);
            g2.drawString("Panel pequeño", 10, 20);
            return;
        }
        Font currentCoordsFont = baseFontCoordenadas.deriveFont(baseFontCoordenadas.getSize2D() * currentScaleFactor);
        FontMetrics fmCoord = getFontMetrics(currentCoordsFont);
        int scaledEspacioCoord = Math.max(1, (int) (ESPACIO_COORD_BASE * currentScaleFactor));
        int paddingInferior = fmCoord.getHeight() + scaledEspacioCoord;
        int paddingIzquierdo = fmCoord.stringWidth("8") + scaledEspacioCoord;
        int paddingSuperior = fmCoord.getAscent() / 2 + scaledEspacioCoord / 2;
        int anchoPanel = getWidth();
        int altoPanel = getHeight();
        int anchoTableroEfectivo = anchoPanel - paddingIzquierdo - scaledEspacioCoord;
        int altoTableroEfectivo = altoPanel - paddingSuperior - paddingInferior;
        int tamanoTableroReal = squareSize * CASILLAS;
        int offsetXTablero = paddingIzquierdo + (anchoTableroEfectivo - tamanoTableroReal) / 2;
        int offsetYTablero = paddingSuperior + (altoTableroEfectivo - tamanoTableroReal) / 2;
        dibujarTablero(g2, offsetXTablero, offsetYTablero, squareSize);
        dibujarResaltados(g2, offsetXTablero, offsetYTablero, squareSize);
        if (tableroJuego != null) {
            dibujarPiezas(g2, offsetXTablero, offsetYTablero, squareSize);
        }
        if (draggedPiece != null && dragPoint != null && dragOffset != null) {
            drawDraggedPiece(g2, squareSize);
        }
        dibujarContorno(g2, offsetXTablero, offsetYTablero, tamanoTableroReal);
        dibujarCoordenadas(g2, offsetXTablero, offsetYTablero, squareSize, tamanoTableroReal, currentCoordsFont, fmCoord, scaledEspacioCoord);
    }

    private void setupAntialiasing(Graphics2D g2) {
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    }

    private void dibujarTablero(Graphics2D g2, int oX, int oY, int sS) {
        g2.setColor(SQUARE_COLOR_DARK);
        for (int j = 0; j < CASILLAS; j++) {
            for (int i = 0; i < CASILLAS; i++) {
                if ((i + j) % 2 != 0) {
                    g2.fillRect(oX + i * sS, oY + j * sS, sS, sS);
                }
            }
        }
    }

    private void dibujarResaltados(Graphics2D g2, int oX, int oY, int sS) {
        if (this.selectedPos != null && this.validDestinations != null) {
            Rectangle rS = getGraphicalSquareRect(this.selectedPos, sS);
            g2.setColor(HIGHLIGHT_SELECTION_COLOR);
            g2.fillRect(rS.x, rS.y, rS.width, rS.height);
            g2.setColor(HIGHLIGHT_SELECTION_COLOR.darker());
            g2.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH));
            g2.drawRect(rS.x, rS.y, rS.width, rS.height);
            for (Posicion d : validDestinations) {
                Rectangle rD = getGraphicalSquareRect(d, sS);
                Pieza pD = tableroJuego != null ? tableroJuego.obtenerPieza(d) : null;
                Color hC = (pD != null && pD.esBlanca() != isWhiteTurn) ? HIGHLIGHT_CAPTURE_COLOR : HIGHLIGHT_POSSIBLE_MOVE_COLOR;
                g2.setColor(hC);
                g2.fillRect(rD.x, rD.y, rD.width, rD.height);
                g2.setColor(hC.darker());
                g2.drawRect(rD.x, rD.y, rD.width, rD.height);
            }
            g2.setStroke(new BasicStroke(1f));
        }
    }

    private void dibujarPiezas(Graphics2D g2, int oX, int oY, int sS) {
        if (FONT_PIEZAS_BASE == null || tableroJuego == null) {
            return;
        }
        float pieceFontSize = (float) (sS * 0.80);
        Font fontPieces = FONT_PIEZAS_BASE.deriveFont(pieceFontSize);
        g2.setFont(fontPieces);
        for (int x = 0; x < Tablero.TAMANO_TABLERO; x++) {
            for (int y = 0; y < Tablero.TAMANO_TABLERO; y++) {
                Posicion pL = new Posicion(x, y);
                Pieza p = tableroJuego.obtenerPieza(pL);
                if (draggedPiece != null && pL.equals(PanelTablero.this.selectedPos) && dragPoint != null) {
                    continue;
                }
                if (p != null) {
                    String pC = obtenerCaracterPiezaFuente(p);
                    Rectangle rC = getGraphicalSquareRect(pL, sS);
                    g2.setColor(p.esBlanca() ? COLOR_PIEZA_BLANCA : COLOR_PIEZA_NEGRA);
                    boolean rotRey = false;
                    if (p instanceof Rey r && estadoActualDelJuego.esJaqueMate()) {
                        boolean perdedor = (estadoActualDelJuego == EstadoJuego.JAQUEMATE_GANA_NEGRAS && r.esBlanca()) || (estadoActualDelJuego == EstadoJuego.JAQUEMATE_GANA_BLANCAS && !r.esBlanca());
                        if (perdedor) {
                            rotRey = true;
                        }
                    }
                    if (rotRey) {
                        AffineTransform orig = g2.getTransform();
                        g2.rotate(Math.PI / 2.0, rC.getCenterX(), rC.getCenterY());
                        centrarTexto(g2, pC, rC, fontPieces);
                        g2.setTransform(orig);
                    } else {
                        centrarTexto(g2, pC, rC, fontPieces);
                    }
                }
            }
        }
    }

    private void drawDraggedPiece(Graphics2D g2, int sS) {
        if (draggedPiece == null || dragPoint == null || dragOffset == null) {
            return;
        }
        String pC = obtenerCaracterPiezaFuente(draggedPiece);
        Color col = draggedPiece.esBlanca() ? COLOR_PIEZA_BLANCA : COLOR_PIEZA_NEGRA;
        float pieceFontSize = (float) (sS * 0.80);
        Font fontPieces = FONT_PIEZAS_BASE.deriveFont(Font.BOLD, pieceFontSize);
        g2.setFont(fontPieces);
        int x = dragPoint.x - dragOffset.x;
        int y = dragPoint.y - dragOffset.y;
        Rectangle dR = new Rectangle(x, y, sS, sS);
        g2.setColor(col);
        centrarTexto(g2, pC, dR, fontPieces);
    }

    private void dibujarContorno(Graphics2D g2, int oX, int oY, int tTR) {
        Stroke s = new BasicStroke(Math.max(1f, 2f * currentScaleFactor));
        g2.setColor(COLOR_CONTORNO);
        g2.setStroke(s);
        g2.drawRect(oX, oY, tTR, tTR);
        g2.setStroke(new BasicStroke(1f));
    }

    private void dibujarCoordenadas(Graphics2D g2, int oXT, int oYT, int sS, int tTR, Font fontC, FontMetrics fm, int sEC) {
        if (fontC == null || fm == null) {
            return;
        }
        g2.setFont(fontC);
        g2.setColor(COLOR_COORDENADAS);
        int yL = oYT + tTR + sEC + fm.getAscent();
        for (int i = 0; i < CASILLAS; i++) {
            char lC = rotado ? (char) ('a' + (CASILLAS - 1 - i)) : (char) ('a' + i);
            String l = String.valueOf(lC);
            int xL = oXT + i * sS + (sS - fm.stringWidth(l)) / 2;
            g2.drawString(l.toUpperCase(), xL, yL);
        }
        int xN = oXT - sEC - fm.stringWidth("8");
        if (xN < sEC / 2) {
            xN = sEC / 2;
        }
        for (int j = 0; j < CASILLAS; j++) {
            int nI = rotado ? (j + 1) : (CASILLAS - j);
            String n = String.valueOf(nI);
            int yN = oYT + j * sS + (sS - fm.getHeight()) / 2 + fm.getAscent();
            g2.drawString(n, xN, yN);
        }
    }

    private void centrarTexto(Graphics g, String t, Rectangle r, Font f) {
        if (f == null) {
            f = g.getFont();
        }
        FontMetrics m = g.getFontMetrics(f);
        int x = r.x + (r.width - m.stringWidth(t)) / 2;
        int y = r.y + ((r.height - m.getHeight()) / 2) + m.getAscent();
        g.setFont(f);
        g.drawString(t, x, y);
    }

    private String obtenerCaracterPiezaFuente(Pieza p) {
        return switch (p) {
            case Rey r ->
                "l";
            case Dama q ->
                "w";
            case Torre t ->
                "t";
            case Alfil b ->
                "v";
            case Caballo k ->
                "m";
            case Peon pn ->
                "o";
            default ->
                "";
        };
    } 
}


Código Java (SplashScreen.java):

package ajedrez_gui;

import javax.swing.*;
import java.awt.*;
import java.net.URL;
import java.util.logging.Level; 
import java.util.logging.Logger; 

public class SplashScreen extends JWindow {

    private static final Logger LOGGER = Logger.getLogger(SplashScreen.class.getName());
    
    private static final int IMAGE_TARGET_WIDTH = 640;
    private static final int IMAGE_TARGET_HEIGHT = 320;
    
    private static final int PROGRESS_BAR_HEIGHT = 25;
    private static final int BAR_MARGIN_HORIZONTAL = 60; 
    private static final Color PROGRESS_BAR_COLOR = Color.GRAY;
    private static final int VERTICAL_OFFSET_FROM_CENTER = 120; 
    
    private static final int WINDOW_WIDTH = IMAGE_TARGET_WIDTH;
    private static final int WINDOW_HEIGHT = IMAGE_TARGET_HEIGHT;
    
    private static final String IMAGE_PATH = "/ajedrez_gui/assets/chessmate.png"; 

    private JProgressBar progressBar;
    private Timer progressTimer;
    private double currentProgress = 0; 

    public SplashScreen() {
        setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
        setLocationRelativeTo(null); 
        
        var layeredPane = new JLayeredPane();
        layeredPane.setPreferredSize(new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));
        
        var imageLabel = new JLabel();
        URL imageURL = getClass().getResource(IMAGE_PATH);
        if (imageURL != null) {
            var originalIcon = new ImageIcon(imageURL);
            
            Image scaledImage = originalIcon.getImage().getScaledInstance(
                    IMAGE_TARGET_WIDTH, IMAGE_TARGET_HEIGHT, Image.SCALE_SMOOTH);
            imageLabel.setIcon(new ImageIcon(scaledImage));
            LOGGER.log(Level.INFO, "SplashScreen: Imagen de splash cargada: " + IMAGE_PATH);
        } else {
            LOGGER.log(Level.SEVERE, "SplashScreen Error: No se pudo cargar imagen: " + IMAGE_PATH);
            
            imageLabel.setOpaque(true);
            imageLabel.setBackground(Color.DARK_GRAY);
            imageLabel.setForeground(Color.RED);
            imageLabel.setHorizontalAlignment(SwingConstants.CENTER);
            imageLabel.setText("Error cargando: " + IMAGE_PATH);
        }
        imageLabel.setBounds(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
        layeredPane.add(imageLabel, JLayeredPane.DEFAULT_LAYER); 
        
        progressBar = new JProgressBar(0, 100);
        progressBar.setValue(0);
        progressBar.setStringPainted(false); 
        progressBar.setBorderPainted(false); 
        progressBar.setForeground(PROGRESS_BAR_COLOR);
        progressBar.setOpaque(false); 
        
        int barWidth = WINDOW_WIDTH - (BAR_MARGIN_HORIZONTAL * 2);
        int barX = BAR_MARGIN_HORIZONTAL;
        int barY = (WINDOW_HEIGHT / 2) - (PROGRESS_BAR_HEIGHT / 2) + VERTICAL_OFFSET_FROM_CENTER;
        
        barY = Math.min(barY, WINDOW_HEIGHT - PROGRESS_BAR_HEIGHT - 5);

        progressBar.setBounds(barX, barY, barWidth, PROGRESS_BAR_HEIGHT);
        layeredPane.add(progressBar, JLayeredPane.PALETTE_LAYER); 

        setContentPane(layeredPane);
    }

    
    public void startProgress(int animationDurationMillis) {
        if (progressTimer != null && progressTimer.isRunning()) {
            progressTimer.stop();
        }
        currentProgress = 0;
        progressBar.setValue(0);

        if (animationDurationMillis <= 0) {
            animationDurationMillis = 100; 
        }
        
        int updates = 50; 
        int updateInterval = Math.max(10, animationDurationMillis / updates); 
        double incrementPerStep = 100.0 / (double) (animationDurationMillis / updateInterval); 

        progressTimer = new Timer(updateInterval, e -> {
            currentProgress += incrementPerStep;
            int progressToShow = Math.min((int) currentProgress, 100); 

            
            SwingUtilities.invokeLater(() -> progressBar.setValue(progressToShow));

            if (progressToShow >= 100) {
                ((Timer) e.getSource()).stop(); 
                LOGGER.log(Level.FINE, "SplashScreen: Animación de progreso completada.");
            }
        });

        progressTimer.setInitialDelay(0); 
        progressTimer.start();
        LOGGER.log(Level.INFO, "SplashScreen: Animación de progreso iniciada.");
    }
    
    @Override
    public void dispose() {
        if (progressTimer != null && progressTimer.isRunning()) {
            progressTimer.stop();
        }
        super.dispose();
        LOGGER.log(Level.INFO, "SplashScreen: Disposed.");
    }
}


Código Java (ConfiguracionDialog.java):

package ajedrez_gui;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import static ajedrez_gui.AjedrezGrafico.ModoJuego;
import static ajedrez_gui.AjedrezGrafico.NivelDificultad;

public class ConfiguracionDialog extends JDialog {

    private static final ModoJuego DEFAULT_MODO_JUEGO = ModoJuego.HUMANO_VS_CPU;
    private static final NivelDificultad DEFAULT_NIVEL_DIFICULTAD = NivelDificultad.NORMAL;

    private static final String DIALOG_TITLE = "Menú Configurar";
    private static final Font BASE_FONT_CUSTOM_TITLE = new Font("Tahoma", Font.BOLD, 14);
    private static final Color COLOR_CUSTOM_TITLE_BG = Color.LIGHT_GRAY;
    private static final Color COLOR_CUSTOM_TITLE_FG = Color.BLACK;
    private static final int BASE_CUSTOM_TITLE_BAR_HEIGHT = 30;
    private static final int BASE_CUSTOM_TITLE_PADDING = 5;
    private static final Font BASE_FONT_LABEL = new Font("Tahoma", Font.PLAIN, 14);
    private static final Font BASE_FONT_COMBO = new Font("Tahoma", Font.PLAIN, 14);
    private static final Font BASE_FONT_BUTTON = new Font("Tahoma", Font.PLAIN, 14);
    private static final Dimension BASE_BUTTON_SIZE = new Dimension(120, 30);

    private static final int FIXED_DIALOG_WIDTH_BASE = 460;
    private static final int FIXED_DIALOG_HEIGHT_BASE = 240; 

    private JComboBox<ModoJuego> comboModo;
    private JComboBox<NivelDificultad> comboNivelDificultad;
    private JButton botonAplicar;
    private JButton botonDefault;

    private ModoJuego modoSeleccionado;
    private NivelDificultad nivelDificultadSeleccionado;
    private boolean cambiosAplicados = false;
    private final float scaleFactor;

    public ConfiguracionDialog(Frame owner, boolean modal,
            ModoJuego modoActual,
            NivelDificultad nivelActual,
            float scaleFactor) {
        super(owner, modal);
        this.scaleFactor = scaleFactor > 0 ? scaleFactor : 1.0f;
        setUndecorated(true);

        this.modoSeleccionado = modoActual;
        this.nivelDificultadSeleccionado = nivelActual;

        initComponents();
        initListeners();
        setValoresActuales(modoActual, nivelActual);
        actualizarControlesDependientes();

        int dialogWidth = (int) (FIXED_DIALOG_WIDTH_BASE * this.scaleFactor);
        int dialogHeight = (int) (FIXED_DIALOG_HEIGHT_BASE * this.scaleFactor);

        dialogWidth = Math.max(dialogWidth, (int) (FIXED_DIALOG_WIDTH_BASE * 0.75f));
        dialogHeight = Math.max(dialogHeight, (int) (FIXED_DIALOG_HEIGHT_BASE * 0.75f));

        setSize(dialogWidth, dialogHeight);
        setMinimumSize(new Dimension(dialogWidth, dialogHeight));
        setMaximumSize(new Dimension(dialogWidth, dialogHeight));
        setResizable(false);

        setLocationRelativeTo(owner);
        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    }

    private void initComponents() {
        var mainPanel = new JPanel();
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
        mainPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
        mainPanel.setBackground(new Color(230, 230, 230));

        int scaledTitleBarHeight = (int) (BASE_CUSTOM_TITLE_BAR_HEIGHT * scaleFactor);
        int scaledTitlePadding = (int) (BASE_CUSTOM_TITLE_PADDING * scaleFactor);
        Font scaledCustomTitleFont = BASE_FONT_CUSTOM_TITLE.deriveFont(BASE_FONT_CUSTOM_TITLE.getSize() * scaleFactor);

        var titlePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, scaledTitlePadding));
        titlePanel.setBackground(COLOR_CUSTOM_TITLE_BG);
        titlePanel.setPreferredSize(new Dimension(0, scaledTitleBarHeight));
        titlePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, scaledTitleBarHeight));
        var titleLabel = new JLabel(DIALOG_TITLE);
        titleLabel.setFont(scaledCustomTitleFont);
        titleLabel.setForeground(COLOR_CUSTOM_TITLE_FG);
        titlePanel.add(titleLabel);
        mainPanel.add(titlePanel);

        var contentPanel = new JPanel();
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        int scaledPadding = (int) (15 * scaleFactor);
        contentPanel.setBorder(BorderFactory.createEmptyBorder(scaledPadding, (int) (20 * scaleFactor), scaledPadding, (int) (20 * scaleFactor)));
        contentPanel.setOpaque(false);

        var optionsPanel = new JPanel(new GridBagLayout());
        optionsPanel.setOpaque(false);
        var gbc = new GridBagConstraints();
        gbc.insets = new Insets((int) (10 * scaleFactor), (int) (5 * scaleFactor), (int) (10 * scaleFactor), (int) (5 * scaleFactor));
        gbc.anchor = GridBagConstraints.WEST;

        Font scaledLabelFont = BASE_FONT_LABEL.deriveFont(BASE_FONT_LABEL.getSize() * scaleFactor);
        Font scaledComboFont = BASE_FONT_COMBO.deriveFont(BASE_FONT_COMBO.getSize() * scaleFactor);

        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.weightx = 0.0;
        var labelModo = new JLabel("Modo:");
        labelModo.setFont(scaledLabelFont);
        optionsPanel.add(labelModo, gbc);
        gbc.gridx = 1;
        gbc.weightx = 1.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        comboModo = new JComboBox<>(ModoJuego.values());
        comboModo.setFont(scaledComboFont);
        optionsPanel.add(comboModo, gbc);
        gbc.fill = GridBagConstraints.NONE;

        gbc.gridx = 0;
        gbc.gridy++;
        gbc.weightx = 0.0;
        var labelNivelDificultad = new JLabel("Dificultad CPU:");
        labelNivelDificultad.setFont(scaledLabelFont);
        optionsPanel.add(labelNivelDificultad, gbc);
        gbc.gridx = 1;
        gbc.weightx = 1.0;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        comboNivelDificultad = new JComboBox<>(NivelDificultad.values());
        comboNivelDificultad.setFont(scaledComboFont);
        optionsPanel.add(comboNivelDificultad, gbc);
        gbc.fill = GridBagConstraints.NONE;

        optionsPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
        contentPanel.add(optionsPanel);
        contentPanel.add(Box.createVerticalGlue());

        var panelBotones = new JPanel(new FlowLayout(FlowLayout.CENTER, (int) (15 * scaleFactor), (int) (10 * scaleFactor)));
        panelBotones.setOpaque(false);
        botonAplicar = new JButton("Aceptar");
        botonDefault = new JButton("Restaurar");

        Font scaledButtonFont = BASE_FONT_BUTTON.deriveFont(BASE_FONT_BUTTON.getSize() * scaleFactor);
        Dimension scaledButtonSize = new Dimension((int) (BASE_BUTTON_SIZE.width * scaleFactor), (int) (BASE_BUTTON_SIZE.height * scaleFactor));

        GuiUtils.applyButtonStyle(botonAplicar, scaledButtonFont, scaledButtonSize);
        GuiUtils.applyButtonStyle(botonDefault, scaledButtonFont, scaledButtonSize);
        panelBotones.add(botonAplicar);
        panelBotones.add(botonDefault);
        panelBotones.setAlignmentX(Component.CENTER_ALIGNMENT);
        contentPanel.add(panelBotones);

        mainPanel.add(contentPanel);
        setContentPane(mainPanel);
    }

    private void initListeners() {
        comboModo.addItemListener(e -> {
            if (e.getStateChange() == ItemEvent.SELECTED) {
                actualizarControlesDependientes();
            }
        });
        botonAplicar.addActionListener(e -> aplicarCambios());
        botonDefault.addActionListener(e -> restaurarDefaults());
        getRootPane().registerKeyboardAction(e -> cancelar(),
                KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0),
                JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    private void setValoresActuales(ModoJuego modo, NivelDificultad nivel) {
        comboModo.setSelectedItem(modo != null ? modo : DEFAULT_MODO_JUEGO);
        comboNivelDificultad.setSelectedItem(nivel != null ? nivel : DEFAULT_NIVEL_DIFICULTAD);
    }

    private void actualizarControlesDependientes() {
        var modoSel = (ModoJuego) comboModo.getSelectedItem();
        if (modoSel == null) {
            return;
        }

        boolean habilitaOpcionesCPU = modoSel == ModoJuego.HUMANO_VS_CPU || modoSel == ModoJuego.CPU_VS_CPU;
        comboNivelDificultad.setEnabled(habilitaOpcionesCPU);
    }

    private void aplicarCambios() {
        this.modoSeleccionado = (ModoJuego) comboModo.getSelectedItem();
        this.nivelDificultadSeleccionado = comboNivelDificultad.isEnabled() ? (NivelDificultad) comboNivelDificultad.getSelectedItem() : DEFAULT_NIVEL_DIFICULTAD;
        this.cambiosAplicados = true;
        this.dispose();
    }

    private void restaurarDefaults() {
        setValoresActuales(DEFAULT_MODO_JUEGO, DEFAULT_NIVEL_DIFICULTAD);
        actualizarControlesDependientes();
    }

    private void cancelar() {
        this.cambiosAplicados = false;
        this.dispose();
    }

    public boolean seAplicaronCambios() {
        return cambiosAplicados;
    }

    public ModoJuego getModoSeleccionado() {
        return modoSeleccionado != null ? modoSeleccionado : DEFAULT_MODO_JUEGO;
    }

    public NivelDificultad getNivelDificultadSeleccionado() {
        return nivelDificultadSeleccionado != null ? nivelDificultadSeleccionado : DEFAULT_NIVEL_DIFICULTAD;
    }
}


Código Java (PromocionDialog.java):

package ajedrez_gui;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.logging.Level;
import java.util.logging.Logger;

public class PromocionDialog extends JDialog {

    private static final Logger LOGGER = Logger.getLogger(PromocionDialog.class.getName());

    private static final String DIALOG_TITLE = "Selecciona Pieza de Promoción";
    private static final Font BASE_FONT_CUSTOM_TITLE = new Font("Tahoma", Font.BOLD, 14);
    private static final Color COLOR_CUSTOM_TITLE_BG = Color.LIGHT_GRAY;
    private static final Color COLOR_CUSTOM_TITLE_FG = Color.BLACK;
    private static final int BASE_CUSTOM_TITLE_BAR_HEIGHT = 30;
    private static final int BASE_CUSTOM_TITLE_PADDING = 5;

    private static final String RUTA_FUENTE_PIEZAS = "/ajedrez_gui/assets/CASEFONT.TTF";
    private static final float BASE_FONT_SIZE_PIEZAS_DIALOG = 52f;
    private static final String FUENTE_FALLBACK_POR_DEFECTO = "Dialog";
    private static final String DAMA_CHAR = "w";
    private static final String TORRE_CHAR = "t";
    private static final String ALFIL_CHAR = "v";
    private static final String CABALLO_CHAR = "m";

    private static final Color COLOR_PIEZA_BLANCA_EN_DIALOGO = Color.BLACK;
    private static final Color COLOR_PIEZA_NEGRA_EN_DIALOGO = Color.WHITE;
    private static final Color COLOR_FONDO_PIEZA_BLANCA_DIALOGO = new Color(100, 100, 100);
    private static final Color COLOR_FONDO_PIEZA_NEGRA_DIALOGO = new Color(210, 210, 210);

    private static final Color BORDER_COLOR_NORMAL = Color.GRAY;
    private static final Color BORDER_COLOR_HOVER = Color.BLACK;
    private static final int BASE_BORDER_THICKNESS = 2;
    private static final int BASE_PADDING_PIEZA = 8;

    private JLabel labelDama;
    private JLabel labelTorre;
    private JLabel labelAlfil;
    private JLabel labelCaballo;
    private Font fontPiezasDialogo;
    private char piezaSeleccionada = 'Q';
    private boolean seleccionRealizada = false;
    private float scaleFactor;

    public PromocionDialog(Frame owner, boolean peonQuePromocionaEsBlanco, float scaleFactor) {
        super(owner, true);
        this.scaleFactor = scaleFactor > 0 ? scaleFactor : 1.0f;
        setUndecorated(true);

        float scaledPieceFontSize = BASE_FONT_SIZE_PIEZAS_DIALOG * this.scaleFactor;
        this.fontPiezasDialogo = GuiUtils.cargarFuenteDesdeRecurso(
                RUTA_FUENTE_PIEZAS, scaledPieceFontSize, FUENTE_FALLBACK_POR_DEFECTO);

        initComponents(peonQuePromocionaEsBlanco);
        initListeners();

        pack();
        setLocationRelativeTo(owner);
        setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
    }

    private void initComponents(boolean peonQuePromocionaEsBlanco) {
        var mainPanel = new JPanel();
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
        mainPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
        mainPanel.setBackground(new Color(230, 230, 230));

        int scaledTitleBarHeight = (int) (BASE_CUSTOM_TITLE_BAR_HEIGHT * scaleFactor);
        int scaledTitlePadding = (int) (BASE_CUSTOM_TITLE_PADDING * scaleFactor);
        Font scaledCustomTitleFont = BASE_FONT_CUSTOM_TITLE.deriveFont(BASE_FONT_CUSTOM_TITLE.getSize() * scaleFactor);

        if (isUndecorated()) {
            var titlePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, scaledTitlePadding));
            titlePanel.setBackground(COLOR_CUSTOM_TITLE_BG);
            titlePanel.setPreferredSize(new Dimension(0, scaledTitleBarHeight));
            titlePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, scaledTitleBarHeight));
            var titleLabel = new JLabel(DIALOG_TITLE);
            titleLabel.setFont(scaledCustomTitleFont);
            titleLabel.setForeground(COLOR_CUSTOM_TITLE_FG);
            titlePanel.add(titleLabel);
            mainPanel.add(titlePanel);
        }

        var contentPanel = new JPanel();
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        contentPanel.setBorder(BorderFactory.createEmptyBorder((int) (15 * scaleFactor), (int) (20 * scaleFactor), (int) (20 * scaleFactor), (int) (20 * scaleFactor)));
        contentPanel.setOpaque(false);

        var piecePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, (int) (12 * scaleFactor), (int) (5 * scaleFactor)));
        piecePanel.setOpaque(false);

        Color colorDeLasPiezasEnDialogo;
        Color fondoDeLasPiezasEnDialogo;

        if (peonQuePromocionaEsBlanco) {
            colorDeLasPiezasEnDialogo = COLOR_PIEZA_BLANCA_EN_DIALOGO;
            fondoDeLasPiezasEnDialogo = COLOR_FONDO_PIEZA_BLANCA_DIALOGO;
        } else {
            colorDeLasPiezasEnDialogo = COLOR_PIEZA_NEGRA_EN_DIALOGO;
            fondoDeLasPiezasEnDialogo = COLOR_FONDO_PIEZA_NEGRA_DIALOGO;
        }

        labelDama = createPieceLabel(DAMA_CHAR, colorDeLasPiezasEnDialogo, fondoDeLasPiezasEnDialogo);
        labelTorre = createPieceLabel(TORRE_CHAR, colorDeLasPiezasEnDialogo, fondoDeLasPiezasEnDialogo);
        labelAlfil = createPieceLabel(ALFIL_CHAR, colorDeLasPiezasEnDialogo, fondoDeLasPiezasEnDialogo);
        labelCaballo = createPieceLabel(CABALLO_CHAR, colorDeLasPiezasEnDialogo, fondoDeLasPiezasEnDialogo);

        piecePanel.add(labelDama);
        piecePanel.add(labelTorre);
        piecePanel.add(labelAlfil);
        piecePanel.add(labelCaballo);
        piecePanel.setAlignmentX(Component.CENTER_ALIGNMENT);

        contentPanel.add(Box.createVerticalGlue());
        contentPanel.add(piecePanel);
        contentPanel.add(Box.createVerticalGlue());

        mainPanel.add(contentPanel);
        setContentPane(mainPanel);
    }

    private JLabel createPieceLabel(String pieceChar, Color pieceColor, Color pieceBackground) {
        var label = new JLabel(pieceChar, SwingConstants.CENTER);
        if (fontPiezasDialogo != null) {
            label.setFont(fontPiezasDialogo);
        }
        label.setForeground(pieceColor);
        label.setBackground(pieceBackground);
        label.setOpaque(true);

        int scaledPaddingPiece = (int) (BASE_PADDING_PIEZA * scaleFactor);
        int scaledBorderThickness = Math.max(1, (int) (BASE_BORDER_THICKNESS * scaleFactor));

        Border padding = new EmptyBorder(scaledPaddingPiece, scaledPaddingPiece, scaledPaddingPiece, scaledPaddingPiece);
        Border lineBorderNormal = new LineBorder(BORDER_COLOR_NORMAL, scaledBorderThickness);
        label.setBorder(new CompoundBorder(lineBorderNormal, padding));
        label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        return label;
    }

    private void initListeners() {
        int scaledPaddingPiece = (int) (BASE_PADDING_PIEZA * scaleFactor);
        int scaledBorderThickness = Math.max(1, (int) (BASE_BORDER_THICKNESS * scaleFactor));

        Border padding = new EmptyBorder(scaledPaddingPiece, scaledPaddingPiece, scaledPaddingPiece, scaledPaddingPiece);
        Border lineBorderNormal = new LineBorder(BORDER_COLOR_NORMAL, scaledBorderThickness);
        Border lineBorderHover = new LineBorder(BORDER_COLOR_HOVER, scaledBorderThickness);
        Border compoundBorderNormal = new CompoundBorder(lineBorderNormal, padding);
        Border compoundBorderHover = new CompoundBorder(lineBorderHover, padding);

        MouseAdapter pieceListener = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                Object source = e.getSource();
                if (source == labelDama) {
                    piezaSeleccionada = 'Q';
                } else if (source == labelTorre) {
                    piezaSeleccionada = 'R';
                } else if (source == labelAlfil) {
                    piezaSeleccionada = 'B';
                } else if (source == labelCaballo) {
                    piezaSeleccionada = 'N';
                }
                seleccionRealizada = true;
                dispose();
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                ((JLabel) e.getSource()).setBorder(compoundBorderHover);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                ((JLabel) e.getSource()).setBorder(compoundBorderNormal);
            }
        };
        labelDama.addMouseListener(pieceListener);
        labelTorre.addMouseListener(pieceListener);
        labelAlfil.addMouseListener(pieceListener);
        labelCaballo.addMouseListener(pieceListener);

        getRootPane().registerKeyboardAction(e -> {
            LOGGER.log(Level.INFO, "PromocionDialog: ESC presionado. Cancelando.");
            seleccionRealizada = false;
            dispose();
        }, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    public boolean isSeleccionRealizada() {
        return seleccionRealizada;
    }

    public char getPiezaSeleccionada() {
        return piezaSeleccionada;
    }

}


Código Java (ElegirLadoDialog.java):

package ajedrez_gui;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

public class ElegirLadoDialog extends JDialog {

    private static final String DIALOG_TITLE = "Elige tu Bando ChessMate";

    private static final Font BASE_FONT_CUSTOM_TITLE = new Font("Tahoma", Font.BOLD, 14);
    private static final Color COLOR_CUSTOM_TITLE_BG = Color.LIGHT_GRAY;
    private static final Color COLOR_CUSTOM_TITLE_FG = Color.BLACK;
    private static final int BASE_CUSTOM_TITLE_BAR_HEIGHT = 30;
    private static final int BASE_CUSTOM_TITLE_PADDING = 5;

    private static final String RUTA_FUENTE_PIEZAS = "/ajedrez_gui/assets/CASEFONT.TTF";
    private static final float BASE_FONT_SIZE_PIEZAS_DIALOG = 58f;
    private static final String FUENTE_FALLBACK_POR_DEFECTO = "Dialog";
    private static final String REY_CHAR = "l";
    private static final Color COLOR_PIEZA_BLANCA = Color.WHITE;
    private static final Color COLOR_PIEZA_NEGRA = Color.BLACK;
    private static final Color COLOR_FONDO_REY = new Color(190, 190, 190);
    private static final Color BORDER_COLOR_NORMAL = Color.GRAY;
    private static final Color BORDER_COLOR_HOVER = Color.BLACK;
    private static final int BASE_BORDER_THICKNESS = 2;
    private static final int BASE_PADDING_REY = 10;

    private JLabel labelReyBlanco;
    private JLabel labelReyNegro;
    private Font fontPiezasDialogo;

    private boolean ladoSeleccionadoBlancas = true;
    private boolean empezarPartida = false;
    private float scaleFactor;

    public ElegirLadoDialog(Frame owner, boolean defaultEsBlancas, float scaleFactor) {
        super(owner, true);
        this.scaleFactor = scaleFactor > 0 ? scaleFactor : 1.0f;
        setUndecorated(true);

        float scaledPieceFontSize = BASE_FONT_SIZE_PIEZAS_DIALOG * this.scaleFactor;
        this.fontPiezasDialogo = GuiUtils.cargarFuenteDesdeRecurso(
                RUTA_FUENTE_PIEZAS,
                scaledPieceFontSize,
                FUENTE_FALLBACK_POR_DEFECTO
        );
        this.ladoSeleccionadoBlancas = defaultEsBlancas;

        initComponents();
        initListeners();

        pack();
        setLocationRelativeTo(owner);
        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    }

    private void initComponents() {
        var mainPanel = new JPanel();
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
        mainPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
        mainPanel.setBackground(new Color(230, 230, 230));

        int scaledTitleBarHeight = (int) (BASE_CUSTOM_TITLE_BAR_HEIGHT * scaleFactor);
        int scaledTitlePadding = (int) (BASE_CUSTOM_TITLE_PADDING * scaleFactor);
        Font scaledCustomTitleFont = BASE_FONT_CUSTOM_TITLE.deriveFont(BASE_FONT_CUSTOM_TITLE.getSize() * scaleFactor);

        var titlePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, scaledTitlePadding));
        titlePanel.setBackground(COLOR_CUSTOM_TITLE_BG);
        titlePanel.setPreferredSize(new Dimension(0, scaledTitleBarHeight));
        titlePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, scaledTitleBarHeight));
        var titleLabel = new JLabel(DIALOG_TITLE);
        titleLabel.setFont(scaledCustomTitleFont);
        titleLabel.setForeground(COLOR_CUSTOM_TITLE_FG);
        titlePanel.add(titleLabel);
        mainPanel.add(titlePanel);

        var contentPanel = new JPanel();
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        contentPanel.setBorder(BorderFactory.createEmptyBorder((int) (15 * scaleFactor), (int) (20 * scaleFactor), (int) (20 * scaleFactor), (int) (20 * scaleFactor)));
        contentPanel.setOpaque(false);

        var kingPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, (int) (40 * scaleFactor), (int) (10 * scaleFactor)));
        kingPanel.setOpaque(false);

        int scaledPaddingRey = (int) (BASE_PADDING_REY * scaleFactor);
        int scaledBorderThickness = Math.max(1, (int) (BASE_BORDER_THICKNESS * scaleFactor));

        Border padding = new EmptyBorder(scaledPaddingRey, scaledPaddingRey, scaledPaddingRey, scaledPaddingRey);
        Border lineBorderNormal = new LineBorder(BORDER_COLOR_NORMAL, scaledBorderThickness);
        Border compoundBorderNormal = new CompoundBorder(lineBorderNormal, padding);

        labelReyBlanco = createKingLabel(REY_CHAR, COLOR_PIEZA_BLANCA, COLOR_FONDO_REY, compoundBorderNormal);
        labelReyNegro = createKingLabel(REY_CHAR, COLOR_PIEZA_NEGRA, COLOR_FONDO_REY, compoundBorderNormal);
        kingPanel.add(labelReyBlanco);
        kingPanel.add(labelReyNegro);
        kingPanel.setAlignmentX(Component.CENTER_ALIGNMENT);

        contentPanel.add(Box.createVerticalGlue());
        contentPanel.add(kingPanel);
        contentPanel.add(Box.createVerticalGlue());

        mainPanel.add(contentPanel);
        setContentPane(mainPanel);
    }

    private JLabel createKingLabel(String text, Color foreground, Color background, Border border) {
        var label = new JLabel(text, SwingConstants.CENTER);
        if (fontPiezasDialogo != null) {
            label.setFont(fontPiezasDialogo);
        }
        label.setForeground(foreground);
        label.setBackground(background);
        label.setOpaque(true);
        label.setBorder(border);
        label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        return label;
    }

    private void initListeners() {
        int scaledPaddingRey = (int) (BASE_PADDING_REY * scaleFactor);
        int scaledBorderThickness = Math.max(1, (int) (BASE_BORDER_THICKNESS * scaleFactor));

        Border padding = new EmptyBorder(scaledPaddingRey, scaledPaddingRey, scaledPaddingRey, scaledPaddingRey);
        Border lineBorderNormal = new LineBorder(BORDER_COLOR_NORMAL, scaledBorderThickness);
        Border lineBorderHover = new LineBorder(BORDER_COLOR_HOVER, scaledBorderThickness);
        Border compoundBorderNormal = new CompoundBorder(lineBorderNormal, padding);
        Border compoundBorderHover = new CompoundBorder(lineBorderHover, padding);

        MouseAdapter reyListener = new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                ladoSeleccionadoBlancas = (e.getSource() == labelReyBlanco);
                empezarPartida = true;
                dispose();
            }

            @Override
            public void mouseEntered(MouseEvent e) {
                ((JLabel) e.getSource()).setBorder(compoundBorderHover);
            }

            @Override
            public void mouseExited(MouseEvent e) {
                ((JLabel) e.getSource()).setBorder(compoundBorderNormal);
            }
        };
        labelReyBlanco.addMouseListener(reyListener);
        labelReyNegro.addMouseListener(reyListener);
        getRootPane().registerKeyboardAction(e -> {
            empezarPartida = false;
            dispose();
        }, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    public boolean seDebeEmpezar() {
        return empezarPartida;
    }

    public boolean getLadoSeleccionadoBlancas() {
        return ladoSeleccionadoBlancas;
    }
}


Código Java (InformacionDialog.java):

package ajedrez_gui;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.text.SimpleDateFormat;
import java.util.Date;

public class InformacionDialog extends JDialog {

    private static final String CUSTOM_TITLE_BAR_TEXT = "Información - ChessMate IA";
    private static final String APP_NAME = "ChessMate IA";
    private static final String APP_VERSION = "1.0";
    private static final String DEV_ENVIRONMENT = "Java SE (Plataforma Standard Edition)";
    private static final String IDE_USED = "Apache NetBeans IDE";

    private static final Font BASE_FONT_CUSTOM_TITLE = new Font("Tahoma", Font.BOLD, 14);
    private static final Color COLOR_CUSTOM_TITLE_BG = Color.LIGHT_GRAY;
    private static final Color COLOR_CUSTOM_TITLE_FG = Color.BLACK;
    private static final int BASE_CUSTOM_TITLE_BAR_HEIGHT = 30;
    private static final int BASE_CUSTOM_TITLE_PADDING = 5;

    private static final Color COLOR_FONDO_DIALOGO_GENERAL = new Color(230, 230, 230);
    private static final Color COLOR_FONDO_PESTANA = new Color(238, 238, 242);
    private static final Color COLOR_TEXTO_PRINCIPAL = Color.BLACK;
    private static final Color COLOR_TEXTO_CABECERA_SECCION = new Color(10, 10, 10);
    private static final Color COLOR_SEPARADOR_LINEA = new Color(70, 70, 70); 

    private static final Font BASE_FONT_TAB_TITLE = new Font("Tahoma", Font.PLAIN, 12);    
    private static final Font BASE_FONT_SECTION_TITLE = new Font("Tahoma", Font.BOLD, 14); 
    private static final Font BASE_FONT_TEXT_NORMAL = new Font("Tahoma", Font.PLAIN, 12);

    private static final int FIXED_DIALOG_WIDTH_BASE = 460;
    private static final int FIXED_DIALOG_HEIGHT_BASE = 320; 

    private float scaleFactor;

    public InformacionDialog(Frame owner, float scaleFactor) {
        super(owner, true);
        this.scaleFactor = Math.max(0.85f, scaleFactor);
        setUndecorated(true);

        JPanel dialogPanel = initComponentsAndGetPanel();
        setContentPane(dialogPanel);

        int dialogWidth = (int) (FIXED_DIALOG_WIDTH_BASE * this.scaleFactor);
        int dialogHeight = (int) (FIXED_DIALOG_HEIGHT_BASE * this.scaleFactor);

        setSize(dialogWidth, dialogHeight);
        setMinimumSize(new Dimension(dialogWidth, dialogHeight));
        setMaximumSize(new Dimension(dialogWidth, dialogHeight));
        setResizable(false);
        setLocationRelativeTo(owner);
        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    }

    private JPanel initComponentsAndGetPanel() {
        JPanel dialogOuterPanel = new JPanel(new BorderLayout());
        dialogOuterPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));

        JPanel mainPanel = new JPanel();
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
        mainPanel.setBackground(COLOR_FONDO_DIALOGO_GENERAL);

        int scaledTitleBarHeight = (int) (BASE_CUSTOM_TITLE_BAR_HEIGHT * scaleFactor);
        int scaledTitlePadding = (int) (BASE_CUSTOM_TITLE_PADDING * scaleFactor);
        Font scaledCustomTitleFont = BASE_FONT_CUSTOM_TITLE.deriveFont(BASE_FONT_CUSTOM_TITLE.getSize2D() * scaleFactor);
        JPanel titlePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, scaledTitlePadding));
        titlePanel.setBackground(COLOR_CUSTOM_TITLE_BG);
        titlePanel.setPreferredSize(new Dimension(0, scaledTitleBarHeight));
        titlePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, scaledTitleBarHeight));
        JLabel dialogTitleLabel = new JLabel(CUSTOM_TITLE_BAR_TEXT);
        dialogTitleLabel.setFont(scaledCustomTitleFont);
        dialogTitleLabel.setForeground(COLOR_CUSTOM_TITLE_FG);
        titlePanel.add(dialogTitleLabel);
        mainPanel.add(titlePanel);

        JPanel tabsOnlyPanel = new JPanel(new BorderLayout());
        tabsOnlyPanel.setOpaque(true);
        tabsOnlyPanel.setBackground(COLOR_FONDO_DIALOGO_GENERAL);
        int contentPadding = (int) (8 * scaleFactor); 
        tabsOnlyPanel.setBorder(new EmptyBorder(contentPadding, contentPadding, contentPadding, contentPadding));

        JTabbedPane tabbedPane = new JTabbedPane();
        tabbedPane.setFont(BASE_FONT_TAB_TITLE.deriveFont(BASE_FONT_TAB_TITLE.getSize2D() * scaleFactor));
        tabbedPane.setBackground(COLOR_FONDO_DIALOGO_GENERAL.brighter());

        JPanel generalPanel = createTabPanel();
        addSectionTitleAndContent(generalPanel, "Sobre " + APP_NAME, this::addInfoGenerales);
        tabbedPane.addTab("General", generalPanel);

        JPanel atajosPanel = createTabPanel();
        addSectionTitleAndContent(atajosPanel, "Atajos de Teclado", this::addInfoAtajos);
        tabbedPane.addTab("Atajos", atajosPanel);

        JPanel tecnicaPanel = createTabPanel();
        addSectionTitleAndContent(tecnicaPanel, "Detalles Técnicos", this::addInfoTecnica);
        tabbedPane.addTab("Técnico", tecnicaPanel);

        tabsOnlyPanel.add(tabbedPane, BorderLayout.CENTER);
        mainPanel.add(tabsOnlyPanel);

        JPanel bottomButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
        bottomButtonPanel.setOpaque(true);
        bottomButtonPanel.setBackground(COLOR_FONDO_DIALOGO_GENERAL);
        bottomButtonPanel.setBorder(new EmptyBorder((int) (5 * scaleFactor), 0, (int) (8 * scaleFactor), (int) (8 * scaleFactor)));
        JButton closeButton = new JButton("Cerrar");
        GuiUtils.applyButtonStyle(closeButton,
                BASE_FONT_TEXT_NORMAL.deriveFont(Font.PLAIN, BASE_FONT_TEXT_NORMAL.getSize2D() * scaleFactor),
                new Dimension((int) (80 * scaleFactor), (int) (26 * scaleFactor))
        );
        closeButton.addActionListener(e -> dispose());
        bottomButtonPanel.add(closeButton);
        mainPanel.add(bottomButtonPanel);

        dialogOuterPanel.add(mainPanel, BorderLayout.CENTER);
        return dialogOuterPanel;
    }

    private JPanel createTabPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.setOpaque(true);
        panel.setBackground(COLOR_FONDO_PESTANA);
        int padding = (int) (10 * scaleFactor);
        panel.setBorder(new EmptyBorder(padding, padding, padding, padding));
        return panel;
    }

    private void addSectionTitleAndContent(JPanel panel, String title, java.util.function.Consumer<JPanel> contentAdder) {
        JLabel titleLabel = new JLabel(title);
        
        titleLabel.setFont(BASE_FONT_SECTION_TITLE.deriveFont(Font.BOLD, BASE_FONT_SECTION_TITLE.getSize2D() * scaleFactor));
        titleLabel.setForeground(COLOR_TEXTO_CABECERA_SECCION);
        titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
        panel.add(titleLabel);

        panel.add(Box.createRigidArea(new Dimension(0, (int) (4 * scaleFactor)))); 
        
        JSeparator separator = new JSeparator(SwingConstants.HORIZONTAL);
        separator.setForeground(COLOR_SEPARADOR_LINEA);
        separator.setBackground(COLOR_SEPARADOR_LINEA);
        separator.setAlignmentX(Component.LEFT_ALIGNMENT);
        
        int lineHeight = Math.max(2, (int) (3 * scaleFactor)); 

        separator.setMaximumSize(new Dimension(Integer.MAX_VALUE, lineHeight));
        separator.setPreferredSize(new Dimension(10, lineHeight));
        
        JPanel separatorPanel = new JPanel(new BorderLayout());
        separatorPanel.setOpaque(false);
        separatorPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, lineHeight));
        separatorPanel.setPreferredSize(new Dimension(10, lineHeight));
        separatorPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        
        separator.setOpaque(true);
        separator.setBorder(BorderFactory.createMatteBorder(0, 0, lineHeight, 0, COLOR_SEPARADOR_LINEA));

        separatorPanel.add(separator, BorderLayout.CENTER);
        panel.add(separatorPanel);

        panel.add(Box.createRigidArea(new Dimension(0, (int) (10 * scaleFactor)))); 
        contentAdder.accept(panel);
    }

    private void addKeyValue(JPanel panel, String key, String value) {
        JPanel linePanel = new JPanel();
        linePanel.setLayout(new BoxLayout(linePanel, BoxLayout.X_AXIS));
        linePanel.setOpaque(false);
        linePanel.setAlignmentX(Component.LEFT_ALIGNMENT);
        JLabel keyLabel = new JLabel("<html><b>" + key + ": </b></html>");
        keyLabel.setFont(BASE_FONT_TEXT_NORMAL.deriveFont(BASE_FONT_TEXT_NORMAL.getSize2D() * scaleFactor));
        keyLabel.setForeground(COLOR_TEXTO_PRINCIPAL);
        JLabel valueLabel = new JLabel(value);
        valueLabel.setFont(BASE_FONT_TEXT_NORMAL.deriveFont(BASE_FONT_TEXT_NORMAL.getSize2D() * scaleFactor));
        valueLabel.setForeground(COLOR_TEXTO_PRINCIPAL);
        linePanel.add(keyLabel);
        linePanel.add(valueLabel);
        linePanel.add(Box.createHorizontalGlue());
        panel.add(linePanel);
        panel.add(Box.createRigidArea(new Dimension(0, (int) (3 * scaleFactor))));
    }

    private void addInfoGenerales(JPanel panel) {
        addKeyValue(panel, "Aplicación", APP_NAME);
        addKeyValue(panel, "Versión", APP_VERSION);
        String fechaCompilacion = new SimpleDateFormat("dd MMMM yyyy").format(new Date());
        addKeyValue(panel, "Fecha", fechaCompilacion);
        addKeyValue(panel, "Desarrollador", "Magnumsoft (c) " + new SimpleDateFormat("yyyy").format(new Date()));
    }
    
    private void addInfoAtajos(JPanel panel) {
        addKeyValue(panel, "F9", "Rotar Tablero.");
        addKeyValue(panel, "F10", "Activar/Desactivar Modo Concentración.");
        addKeyValue(panel, "F11", "Maximizar / Restaurar Ventana.");
        addKeyValue(panel, "F12", "Activar/Desactivar Pantalla Completa.");
        
        addKeyValue(panel, "ESC", "Finaliza estado actual (juego, revisión, etc.).");
    }        

    private void addInfoTecnica(JPanel panel) {
        addKeyValue(panel, "Desarrollado en", DEV_ENVIRONMENT);
        addKeyValue(panel, "IDE Utilizado", IDE_USED);
    }
}


Código Java (ConfirmacionDialog.java):

package ajedrez_gui;

import javax.swing.*;
import java.awt.*;

public class ConfirmacionDialog extends JDialog {
    
    private static final Font BASE_FONT_CUSTOM_TITLE = new Font("Tahoma", Font.BOLD, 14);
    private static final Color COLOR_CUSTOM_TITLE_BG = Color.LIGHT_GRAY;
    private static final Color COLOR_CUSTOM_TITLE_FG = Color.BLACK;
    private static final int BASE_CUSTOM_TITLE_BAR_HEIGHT = 30;
    private static final int BASE_CUSTOM_TITLE_PADDING = 5;
    private static final Font BASE_FONT_TEXT = new Font("Tahoma", Font.BOLD, 16);
    private static final Font BASE_FONT_BUTTON = new Font("Tahoma", Font.PLAIN, 14);
    private static final Dimension BASE_BUTTON_SIZE = new Dimension(100, 30);

    private JButton botonAfirmativo;
    private JButton botonNegativo;
    private boolean confirmado = false;
    private final float scaleFactor;
    
    public ConfirmacionDialog(Frame owner, String titulo, String mensaje, String textoBotonAfirmativo, String textoBotonNegativo, float scaleFactor) {
        super(owner, true);
        this.scaleFactor = scaleFactor > 0 ? scaleFactor : 1.0f;
        setUndecorated(true);

        initComponents(titulo, mensaje, textoBotonAfirmativo, textoBotonNegativo);
        initListeners();

        pack();
        setLocationRelativeTo(owner);
        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    }

    private void initComponents(String titulo, String mensaje, String textoBotonAfirmativo, String textoBotonNegativo) {
        var mainPanel = new JPanel();
        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
        mainPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
        mainPanel.setBackground(new Color(230, 230, 230));

        int scaledTitleBarHeight = (int) (BASE_CUSTOM_TITLE_BAR_HEIGHT * scaleFactor);
        int scaledTitlePadding = (int) (BASE_CUSTOM_TITLE_PADDING * scaleFactor);
        Font scaledCustomTitleFont = BASE_FONT_CUSTOM_TITLE.deriveFont(BASE_FONT_CUSTOM_TITLE.getSize() * scaleFactor);

        var titlePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, scaledTitlePadding));
        titlePanel.setBackground(COLOR_CUSTOM_TITLE_BG);
        titlePanel.setPreferredSize(new Dimension(0, scaledTitleBarHeight));
        titlePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, scaledTitleBarHeight));

        var titleLabel = new JLabel(titulo); 
        titleLabel.setFont(scaledCustomTitleFont);
        titleLabel.setForeground(COLOR_CUSTOM_TITLE_FG);
        titlePanel.add(titleLabel);
        mainPanel.add(titlePanel);

        var contentPanel = new JPanel();
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        int scaledPadding = (int) (20 * scaleFactor);
        contentPanel.setBorder(BorderFactory.createEmptyBorder(scaledPadding, scaledPadding, scaledPadding, scaledPadding));
        contentPanel.setOpaque(false);

        Font scaledTextFont = BASE_FONT_TEXT.deriveFont(BASE_FONT_TEXT.getSize() * scaleFactor);
        var labelTexto = new JLabel(mensaje, SwingConstants.CENTER); 
        labelTexto.setFont(scaledTextFont);
        labelTexto.setAlignmentX(Component.CENTER_ALIGNMENT);
        contentPanel.add(labelTexto);

        contentPanel.add(Box.createRigidArea(new Dimension(0, (int) (30 * scaleFactor))));

        var panelBotones = new JPanel(new FlowLayout(FlowLayout.CENTER, (int) (25 * scaleFactor), (int) (10 * scaleFactor)));
        panelBotones.setOpaque(false);
        botonAfirmativo = new JButton(textoBotonAfirmativo); 
        botonNegativo = new JButton(textoBotonNegativo);   

        Font scaledButtonFont = BASE_FONT_BUTTON.deriveFont(BASE_FONT_BUTTON.getSize() * scaleFactor);
        Dimension scaledButtonSize = new Dimension((int) (BASE_BUTTON_SIZE.width * scaleFactor), (int) (BASE_BUTTON_SIZE.height * scaleFactor));

        GuiUtils.applyButtonStyle(botonAfirmativo, scaledButtonFont, scaledButtonSize);
        GuiUtils.applyButtonStyle(botonNegativo, scaledButtonFont, scaledButtonSize);
        panelBotones.add(botonAfirmativo);
        panelBotones.add(botonNegativo);
        panelBotones.setAlignmentX(Component.CENTER_ALIGNMENT);
        contentPanel.add(panelBotones);

        mainPanel.add(contentPanel);
        setContentPane(mainPanel);
    }

    private void initListeners() {
        botonAfirmativo.addActionListener(e -> {
            this.confirmado = true;
            this.dispose();
        });

        botonNegativo.addActionListener(e -> {
            this.confirmado = false;
            this.dispose();
        });

        getRootPane().registerKeyboardAction(e -> {
            confirmado = false;
            dispose();
        }, KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    public boolean isConfirmado() {
        return confirmado;
    }
}


Código Java (GuiUtils.java):

package ajedrez_gui;

import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;

public final class GuiUtils {    
    private GuiUtils() {
        throw new IllegalStateException("Clase de utilidad, no instanciable.");
    }
    
    public static Font cargarFuenteBaseDesdeRecurso(String resourcePath, String fallbackFontName) {
        Font customFont = null;
        try (InputStream is = GuiUtils.class.getResourceAsStream(resourcePath)) {
            if (is != null) {
                Font fontBase = Font.createFont(Font.TRUETYPE_FONT, is);
                GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
                ge.registerFont(fontBase); 
                customFont = fontBase; 
                System.out.println("Fuente base cargada: " + resourcePath);
            } else {
                System.err.println("Error: Recurso de fuente no encontrado: " + resourcePath);
            }
        } catch (FontFormatException e) {
            System.err.println("Error: Formato de fuente inválido: " + resourcePath + " (" + e.getMessage() + ")");
        } catch (IOException e) {
            System.err.println("Error I/O cargando fuente: " + resourcePath + " (" + e.getMessage() + ")");
        } catch (Exception e) { 
            System.err.println("Error inesperado cargando fuente: " + resourcePath);
            e.printStackTrace();
        }        
        if (customFont == null) {
            System.err.println("FALLBACK: Usando fuente '" + fallbackFontName + "' (base)");
            
            customFont = new Font(fallbackFontName, Font.PLAIN, 20);
        }
        return customFont;
    }
    
    public static Font cargarFuenteDesdeRecurso(String resourcePath, float size, String fallbackFontName) {
        Font baseFont = cargarFuenteBaseDesdeRecurso(resourcePath, fallbackFontName);
        if (baseFont != null) {
            return baseFont.deriveFont(Font.PLAIN, size);
        } else {            
            return new Font(fallbackFontName, Font.PLAIN, (int) size);
        }
    }
    
    public static void applyButtonStyle(JButton button, Font font, Dimension size) {
        if (button == null || font == null || size == null) {
            return;
        }
        button.setFont(font);
        button.setPreferredSize(size);
        button.setMinimumSize(size);
        button.setMaximumSize(size);
        button.setFocusable(false); 
    }
}


Resultado:




Con la tecnología de Blogger.