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

domingo, 13 de abril de 2025

Proyecto Ajedrez I.2: Interfaz Gráfica Principal (GUI - Swing)

 Siguiendo con los posts anteriores relacionados con el ajedrez (Graficando Tablero Ajedrez, Ajedrez: Descripción de las reglas), avanzamos al siguiente nivel: la creación de la interfaz para más adelante hacer la implementación de las reglas básicas del ajedrez.

Por ahora, el motor de juego es básico. Sin embargo, la idea a futuro es poner un sistema de aprendizaje automático basado en una red neuronal artificial. Este sistema permitirá al motor aprender jugando partidas contra sí mismo de forma aleatoria al inicio. Con el tiempo, las jugadas irán mejorando y de esta forma la red neuronal se entrenará y se irá optimizando el motor de juego.

* Entorno de Desarrollo y Estructura de Archivos

    IDE: NetBeans 25
    SDK: OpenJDK 24 (o compatible)
    GUI: Java Swing
    Look & Feel: FlatLaf (Light)
    Fuente Piezas: CASEFONT.TTF

* Estructura de directorios sigue el estándar de NetBeans

Ajedrez/
├── build/           # Archivos compilados
├── dist/            # Archivos distribuibles (JAR)
├── nbproject/       # Archivos de configuración de NetBeans
├── src/             # Código fuente
│   └── ajedrez_gui/ # Paquete principal
│       ├── assets/  # Recursos (fuente, imágenes)
│       └── *.java   # Todas las clases .java
└── test/            # Código de pruebas (si lo hubiera)


* Características Principales

    Interfaz Gráfica Completa: Un tablero visualmente claro con coordenadas, un historial de movimientos en notación algebraica y un panel de estado que informa sobre el turno actual, jaques o el resultado final de la partida.

    Múltiples Modos de Juego:
        Humano vs. CPU
        Humano vs. Humano
        CPU vs. CPU: La máquina juega contra sí misma.

    Configuración Flexible: Antes de iniciar, puedes elegir jugar como Blancas o Negras. También puedes ajustar la "velocidad" (tiempo de reflexión simulado) de la CPU en los modos que la involucran.
    Entrada de Movimientos: Las jugadas se introducen mediante texto en formato de coordenadas (ej: "e2e4", "g1f3").
    Implementación de Reglas FIDE: Se han implementado las reglas básicas del ajedrez:
    Interacción Adicional:
        . Opción de pausar y reanudar las partidas en modo CPU vs. CPU.
        . Tecla ESC con doble función: confirma el abandono de la partida actual o, si no hay partida activa, confirma el cierre de toda la aplicación mediante diálogos personalizados.
        . Look and Feel moderno gracias a la biblioteca FlatLaf.
        . Una pantalla de bienvenida (Splash Screen) al iniciar.

* Reglas de Ajedrez Implementadas

El motor del juego (MotorJuego) y la lógica del tablero (Tablero) se encargan de validar y aplicar las siguientes reglas:

    Movimiento de Piezas: Todos los movimientos básicos y capturas de las seis tipos de piezas (Peón, Caballo, Alfil, Torre, Dama, Rey) están correctamente implementados.
    Movimiento Inicial del Peón: Los peones pueden avanzar dos casillas en su primer movimiento.
    Captura al Paso (En Passant): Implementada la regla especial de captura al paso para los peones.
    Promoción del Peón: Cuando un peón alcanza la última fila, se abre un diálogo para que el jugador humano elija a qué pieza promocionar (Dama, Torre, Alfil o Caballo). En los movimientos de la CPU, esta promociona automáticamente a Dama.
    Enroque: Se permite el enroque corto (O-O) y largo (O-O-O), validando todas las condiciones necesarias:
        .Ni el rey ni la torre implicada se han movido previamente.
        .Las casillas entre el rey y la torre están vacías.
        .El rey no está en jaque.
        .El rey no pasa por casillas atacadas por el oponente.
        .El rey no termina en una casilla atacada.
    Jaque: El sistema detecta cuándo un rey está amenazado.
    Jaque Mate: Se detecta correctamente la condición de Jaque Mate, finalizando la partida y declarando un ganador.
    Ahogado (Stalemate): Se detecta la condición de empate por Ahogado (rey no está en jaque, pero no tiene movimientos legales).
    Empate por Regla de 50 Movimientos: La partida termina en empate si transcurren 50 movimientos consecutivos de cada jugador sin mover un peón ni capturar una pieza.
    Empate por Triple Repetición: La partida termina en empate si la misma posición del tablero (incluyendo turno, derechos de enroque y posibilidad de captura al paso) se repite tres veces.
    Empate por Material Insuficiente: Se detectan las situaciones básicas de material insuficiente donde es imposible dar jaque mate (Rey vs. Rey, Rey vs. Rey+Caballo, Rey vs. Rey+Alfil, Rey+Alfil vs. Rey+Alfil si ambos alfiles están en casillas del mismo color).

* Estructura de la Aplicación

    1. Interfaz Gráfica Principal (GUI - Swing)

    AjedrezGrafico.java: Clase principal (JFrame). Contiene la ventana, la barra de herramientas superior, organiza los paneles principales (tablero, panel derecho), gestiona los eventos de botones, la entrada de texto, los temporizadores para la CPU y mensajes, y actúa como controlador general de la UI.
    PanelTablero.java: Componente JPanel responsable de dibujar el tablero, las piezas (usando una fuente TTF personalizada) y las coordenadas. Gestiona la rotación visual del tablero.
    SplashScreen.java: Ventana (JWindow) que se muestra al inicio con una imagen y una barra de progreso.
    GuiUtils.java: Clase de utilidad para cargar recursos (como la fuente de las piezas) y aplicar estilos consistentes a los componentes (botones).

    2. Ventanas de Diálogo (JDialogs - Swing)

    ConfiguracionDialog.java: Permite al usuario seleccionar el modo de juego y la velocidad de la CPU. Presenta una barra de título personalizada.
    ElegirLadoDialog.java: Permite al usuario elegir si jugar con las piezas Blancas o Negras. Utiliza la fuente de piezas para mostrar los reyes como botones.
    PromocionDialog.java: Aparece cuando un peón llega a la última fila, permitiendo al usuario seleccionar la pieza a la que promocionar.
    ConfirmarFinalizarDialog.java: Diálogo modal que aparece al pulsar ESC durante una partida para confirmar si se desea abandonar la partida actual.
    ConfirmarCierreAppDialog.java: Diálogo modal que aparece al pulsar ESC fuera de una partida para confirmar si se desea cerrar toda la aplicación.

    3. Lógica Central del Juego (Core Logic)

    MotorJuego.java: El "cerebro" del juego. Gestiona el estado actual (EstadoJuego), controla los turnos, interactúa con el Tablero para validar y ejecutar movimientos, mantiene el historial de jugadas (para UI y regla de triple repetición), implementa la lógica simple de la CPU (movimientos aleatorios) y determina el resultado de la partida.
    Tablero.java: Representa el tablero de 8x8. Mantiene la posición de todas las piezas, los derechos de enroque y la información de la casilla de captura al paso. Proporciona métodos cruciales como generarMovimientosLegales() (que incluye la validación anti-jaque), estaAtacada(), estaEnJaque(), esMaterialInsuficiente(), y getPosicionHash() (para la regla de repetición).
    Movimiento.java: Clase que encapsula una jugada, conteniendo información sobre la pieza movida, origen, destino, pieza capturada (si la hay), y si se trata de un movimiento especial como enroque, promoción o captura al paso.
    EstadoJuego.java: Un enum que define todos los posibles estados del juego (Turno Blancas, Jaque Negras, Jaque Mate Gana Blancas, Empate Ahogado, etc.), facilitando la gestión del flujo del juego y la comunicación con la UI.
    Posicion.java: Un record inmutable para representar las coordenadas (x, y) de una casilla en el tablero, con métodos útiles para validar y convertir a/desde notación algebraica.

    4. Representación de las Piezas (Pieces)

    Pieza.java: Clase base abstract sealed para todas las piezas. Define propiedades comunes (color, si se ha movido) y el método abstracto esMovimientoBasicoValido(), además de métodos helper para validar caminos y casillas.

    Alfil.java, Caballo.java, Dama.java, Peon.java, Rey.java, Torre.java: Clases final que heredan de Pieza e implementan la lógica específica de movimiento básico para cada tipo de pieza en el método esMovimientoBasicoValido(). Rey y Peon incluyen la lógica para sus movimientos especiales (enroque y avance doble/en passant, respectivamente).

Código Java 1 (AjedrezGrafico.java):

package ajedrez_gui;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.List;
import java.util.regex.Pattern;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class AjedrezGrafico extends JFrame {

    // --- Constantes ---
    private static final boolean DEFAULT_JUGAR_COMO_BLANCAS = true;
    private static final ModoJuego DEFAULT_MODO_JUEGO = ModoJuego.HUMANO_VS_CPU;
    private static final String DEFAULT_VELOCIDAD_CPU_STR = "Medio";
    private static final int DEFAULT_CPU_DELAY_MS = 500;
    private static final int SLOW_CPU_DELAY_MS = 1200;
    private static final int MIN_CPU_MOVE_DELAY_MS = 500;
    private static final int PRIMER_MOV_CPU_DELAY_MS = 500;

    private static final int TOOLBAR_HGAP = 10;
    private static final int TOOLBAR_VGAP = 5;
    private static final Font FONT_TOOLBAR_BUTTON = new Font("Tahoma", Font.PLAIN, 14);
    private static final Dimension TOOLBAR_BUTTON_SIZE = new Dimension(120, 30);

    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_HEIGHT = 35;
    private static final int ESTADO_LABEL_MAX_HEIGHT = 40;
    private static final int ESTADO_LABEL_MIN_HEIGHT = 30;
    private static final int ESTADO_LABEL_BORDER_PADDING = 5;
    private static final int ESTADO_LABEL_H_PADDING = 8;
    private static final int HISTORIAL_AREA_HEIGHT = 250;
    private static final int HISTORIAL_AREA_MIN_HEIGHT = 100;
    private static final int HISTORIAL_PANEL_PADDING = 15;
    private static final int INTRO_LABEL_H_PADDING = 5;
    private static final int INTRO_LABEL_V_PADDING = 3;
    private static final int INPUT_FIELD_HEIGHT = 70;
    private static final int RIGID_AREA_SMALL_HEIGHT = 5;
    private static final int RIGID_AREA_BOTTOM_MARGIN_HEIGHT = 15;

    private static final int HISTORIAL_ID_WIDTH = 3;
    private static final int HISTORIAL_MOVE_WIDTH = 8;
    private static final String HISTORIAL_HEADER_FORMAT = " %-" + HISTORIAL_ID_WIDTH + "s  %-" + HISTORIAL_MOVE_WIDTH + "s  %-" + HISTORIAL_MOVE_WIDTH + "s ";
    private static final String HISTORIAL_SEPARATOR_FORMAT = " %-" + HISTORIAL_ID_WIDTH + "s  %-" + HISTORIAL_MOVE_WIDTH + "s  %-" + HISTORIAL_MOVE_WIDTH + "s ";
    private static final String HISTORIAL_MOVE_FORMAT_FULL = " %0" + HISTORIAL_ID_WIDTH + "d  %-" + HISTORIAL_MOVE_WIDTH + "." + HISTORIAL_MOVE_WIDTH + "s  %-" + HISTORIAL_MOVE_WIDTH + "." + HISTORIAL_MOVE_WIDTH + "s";

    private static final int MAX_ESTADO_LABEL_LENGTH = 16;
    private static final String STATUS_PAUSED_TEXT = "PAUSADO";
    private static final String STATUS_LISTO_TEXT ="-- CHESSMATE --";
    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 int ERROR_MESSAGE_DURATION_MS = 1800;

    // --- CAMBIO: Constantes Ventana 3:2 ---
    private static final int TARGET_WINDOW_HEIGHT = 600; // Altura fija
    private static final int TARGET_WINDOW_WIDTH = TARGET_WINDOW_HEIGHT * 3 / 2; // = 900

    // Modo de Juego Enum
    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;
        }
    }

    // --- Componentes UI ---
    private PanelTablero panelTablero;
    private JTextField campoMovimiento;
    private JLabel labelEstadoInfo;
    private JTextArea areaHistorial;
    private JScrollPane scrollHistorial;
    private JPanel panelDerecho;
    private JButton botonJugar;
    private JButton botonConfigurar;
    private JButton botonPausaReanudar;

    // --- Estado del Juego y Configuración ---
    private MotorJuego motor;
    private boolean rotarTablero = false;
    private boolean partidaEnCurso = false;
    private boolean isPaused = false;
    private javax.swing.Timer timerCPU;
    private javax.swing.Timer timerMensajeError;
    private javax.swing.Timer timerDelayMovimientoCPU;
    private boolean esPrimerMovimientoCPUDePartida = false;

    private boolean cfgJugarComoBlancas = DEFAULT_JUGAR_COMO_BLANCAS;
    private ModoJuego cfgModoJuego = DEFAULT_MODO_JUEGO;
    private int cfgCpuDelay = DEFAULT_CPU_DELAY_MS;
    private String cfgVelocidadStr = DEFAULT_VELOCIDAD_CPU_STR;

    // --- Fuentes y Colores ---
    private final Font FONT_COURIER_BOLD_40 = new Font("Courier New", Font.BOLD, 40);
    private final Font FONT_COURIER_BOLD_20_ESTADO = new Font("Courier New", Font.BOLD, 20);
    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();

    // Ancho calculado del panel derecho
    private int anchoPanelDerechoCalculado = 260;

    public AjedrezGrafico() {
        motor = new MotorJuego();
        calcularAnchoPanelDerecho();

        // --- Ventana sin decoración ---
        setUndecorated(true);

        initComponents();
        configurarTimers();
        configurarEstadoInicialUI();
        this.setResizable(false); // No redimensionable

        // --- Establecer tamaño fijo 3:2 ---
        this.setSize(TARGET_WINDOW_WIDTH, TARGET_WINDOW_HEIGHT);

        this.setLocationRelativeTo(null); // Centrar en pantalla
    }

    private void calcularAnchoPanelDerecho() {
        var tempArea = new JTextArea();
        tempArea.setFont(FONT_COURIER_BOLD_20_ESTADO);
        FontMetrics fmHist = tempArea.getFontMetrics(FONT_COURIER_BOLD_20_ESTADO);
        String headerString = String.format(HISTORIAL_HEADER_FORMAT, "id", "BLANCAS", "NEGRAS");
        String separatorString = String.format(HISTORIAL_SEPARATOR_FORMAT, "---", "--------", "--------");
        String longestMoveExample = String.format(HISTORIAL_MOVE_FORMAT_FULL, 999, "Qb4xd4++", "Ra1-h1++");
        int contentWidth = Math.max(fmHist.stringWidth(headerString), fmHist.stringWidth(separatorString));
        contentWidth = Math.max(contentWidth, fmHist.stringWidth(longestMoveExample));
        anchoPanelDerechoCalculado = contentWidth + HISTORIAL_PANEL_PADDING * 2;
        anchoPanelDerechoCalculado = Math.max(anchoPanelDerechoCalculado, 260);
        System.out.println("Ancho calculado panel derecho (ancho mov=" + HISTORIAL_MOVE_WIDTH + "): " + anchoPanelDerechoCalculado);
    }

    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); // Color de fondo general

        // --- Barra de herramientas superior ---
        var toolBarSuperior = new JToolBar();
        toolBarSuperior.setFloatable(false);
        toolBarSuperior.setLayout(new FlowLayout(FlowLayout.LEFT, TOOLBAR_HGAP, TOOLBAR_VGAP));
        toolBarSuperior.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
        toolBarSuperior.setBackground(COLOR_FONDO_ESTADO);

        botonJugar = new JButton("Jugar");
        botonPausaReanudar = new JButton("Pausar");
        GuiUtils.applyButtonStyle(botonJugar, FONT_TOOLBAR_BUTTON, TOOLBAR_BUTTON_SIZE);
        GuiUtils.applyButtonStyle(botonPausaReanudar, FONT_TOOLBAR_BUTTON, TOOLBAR_BUTTON_SIZE);

        botonConfigurar = new JButton("Configurar");
        botonConfigurar.setToolTipText("Configurar Juego");
        GuiUtils.applyButtonStyle(botonConfigurar, FONT_TOOLBAR_BUTTON, TOOLBAR_BUTTON_SIZE);

        toolBarSuperior.add(botonJugar);
        toolBarSuperior.add(botonPausaReanudar);
        toolBarSuperior.add(botonConfigurar);

        add(toolBarSuperior, BorderLayout.NORTH);

        var panelCentro = new JPanel(new BorderLayout(CENTER_PANEL_HGAP, 0));
        panelCentro.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); // Sin borde extra
        panelCentro.setOpaque(true); // Volver a opaco
        panelCentro.setBackground(COLOR_FONDO_PANEL_DERECHO);

        // Crear el panel del tablero
        panelTablero = new PanelTablero();
        
        // Crear el panel derecho (con BoxLayout interno como antes)
        panelDerecho = new JPanel();
        panelDerecho.setLayout(new BoxLayout(panelDerecho, BoxLayout.Y_AXIS));
        panelDerecho.setPreferredSize(new Dimension(anchoPanelDerechoCalculado, 1)); // Ancho preferido, alto se ajusta
        panelDerecho.setBorder(BorderFactory.createEmptyBorder(
                RIGHT_PANEL_BORDER_TOP, RIGHT_PANEL_BORDER_LEFT,
                RIGHT_PANEL_BORDER_BOTTOM, RIGHT_PANEL_BORDER_RIGHT));
        panelDerecho.setBackground(COLOR_FONDO_PANEL_DERECHO);
        
        // --- Configurar contenido del panelDerecho ---
        labelEstadoInfo = new JLabel(" ", SwingConstants.CENTER);
        labelEstadoInfo.setFont(FONT_COURIER_BOLD_20_ESTADO);
        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.setAlignmentX(Component.CENTER_ALIGNMENT); // Para BoxLayout de panelDerecho
        labelEstadoInfo.setPreferredSize(new Dimension(0, ESTADO_LABEL_HEIGHT));
        labelEstadoInfo.setMaximumSize(new Dimension(Integer.MAX_VALUE, ESTADO_LABEL_MAX_HEIGHT));
        labelEstadoInfo.setMinimumSize(new Dimension(100, ESTADO_LABEL_MIN_HEIGHT));
        panelDerecho.add(labelEstadoInfo);

        areaHistorial = new JTextArea();
        areaHistorial.setBackground(Color.WHITE);
        areaHistorial.setForeground(Color.BLACK);
        areaHistorial.setFont(FONT_COURIER_BOLD_20_ESTADO);
        areaHistorial.setEditable(false);
        areaHistorial.setLineWrap(false);
        areaHistorial.setMargin(new Insets(5, 5, 5, 5));
        scrollHistorial = new JScrollPane(areaHistorial);
        scrollHistorial.getViewport().setBackground(Color.WHITE);
        scrollHistorial.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        scrollHistorial.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollHistorial.setAlignmentX(Component.CENTER_ALIGNMENT); // Para BoxLayout de panelDerecho
        scrollHistorial.setPreferredSize(new Dimension(0, HISTORIAL_AREA_HEIGHT));
        scrollHistorial.setBorder(BorderFactory.createLineBorder(COLOR_BORDE_HISTORIAL, 3));
        scrollHistorial.setMaximumSize(new Dimension(anchoPanelDerechoCalculado + 10, Integer.MAX_VALUE)); // Limitar ancho máximo
        scrollHistorial.setMinimumSize(new Dimension(100, HISTORIAL_AREA_MIN_HEIGHT));
        panelDerecho.add(scrollHistorial);

        panelDerecho.add(Box.createRigidArea(new Dimension(0, RIGID_AREA_SMALL_HEIGHT)));

        var labelIntroduce = new JLabel("Escribe Movimiento:");
        labelIntroduce.setForeground(Color.BLACK);
        labelIntroduce.setFont(FONT_TOOLBAR_BUTTON);
        labelIntroduce.setAlignmentX(Component.CENTER_ALIGNMENT); // Para BoxLayout de panelDerecho
        labelIntroduce.setBorder(BorderFactory.createEmptyBorder(
                INTRO_LABEL_V_PADDING, INTRO_LABEL_H_PADDING,
                INTRO_LABEL_V_PADDING, INTRO_LABEL_H_PADDING));
        panelDerecho.add(labelIntroduce);

        campoMovimiento = new JTextField();
        campoMovimiento.setBackground(Color.WHITE);
        campoMovimiento.setForeground(Color.BLACK);
        campoMovimiento.setCaretColor(Color.BLACK);
        campoMovimiento.setFont(FONT_COURIER_BOLD_40);
        campoMovimiento.setHorizontalAlignment(JTextField.CENTER);
        if (campoMovimiento.getDocument() instanceof AbstractDocument doc) {
            doc.setDocumentFilter(new UppercaseDocumentFilter());
        }
        campoMovimiento.setAlignmentX(Component.CENTER_ALIGNMENT); // Para BoxLayout de panelDerecho
        campoMovimiento.setPreferredSize(new Dimension(0, INPUT_FIELD_HEIGHT));
        campoMovimiento.setMaximumSize(new Dimension(anchoPanelDerechoCalculado, INPUT_FIELD_HEIGHT)); // Limitar ancho máximo
        campoMovimiento.setMinimumSize(new Dimension(100, INPUT_FIELD_HEIGHT));
        panelDerecho.add(campoMovimiento);

        panelDerecho.add(Box.createRigidArea(new Dimension(0, RIGID_AREA_BOTTOM_MARGIN_HEIGHT)));

        // --- Añadir componentes a panelCentro usando BorderLayout ---
        panelCentro.add(panelTablero, BorderLayout.CENTER); // Tablero al centro
        panelCentro.add(panelDerecho, BorderLayout.EAST);   // Panel derecho al este

        // Añadir panelCentro al centro del JFrame
        add(panelCentro, BorderLayout.CENTER);

        // --- Listeners ---
        botonJugar.addActionListener(e -> mostrarDialogoElegirLado());
        botonConfigurar.addActionListener(e -> mostrarDialogoConfiguracion());
        botonPausaReanudar.addActionListener(e -> togglePausa());
        campoMovimiento.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent e) {
                checkInputLength();
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
            }

            private void checkInputLength() {
                SwingUtilities.invokeLater(() -> {
                    if (partidaEnCurso && esTurnoDelHumano() && !isPaused
                            && motor.getEstadoActual() != EstadoJuego.PROMOCION_REQUIERIDA
                            && !motor.getEstadoActual().esFinDePartida()
                            && campoMovimiento.getText().length() == 4) {
                        procesarEntradaUsuario();
                    }
                });
            }
        });

        configurarTeclaEscAbandono();

    }

    private void configurarTimers() {
        timerCPU = new Timer(cfgCpuDelay, e -> {
            if (!timerDelayMovimientoCPU.isRunning()) {
                iniciarMovimientoCPUConDelay();
            }
        });
        timerCPU.setRepeats(true);

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

        timerDelayMovimientoCPU = new Timer(MIN_CPU_MOVE_DELAY_MS, e -> realizarMovimientoCPUConDelay());
        timerDelayMovimientoCPU.setRepeats(false);
    }

    private void configurarEstadoInicialUI() {
        partidaEnCurso = false;
        isPaused = false;
        esPrimerMovimientoCPUDePartida = false;
        motor.iniciarNuevaPartida();
        panelTablero.setTablero(motor.getTablero());
        panelTablero.setRotado(false);
        panelTablero.setEstadoJuego(EstadoJuego.EN_CURSO);
        this.rotarTablero = false;
        actualizarHistorialDisplay();
        actualizarEstadoInfo(STATUS_LISTO_TEXT);
        campoMovimiento.setText("");
        stopAllTimers();
        restaurarConfiguracionDefault();
        actualizarControlesUI();
    }

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

    private void restaurarConfiguracionDefault() {
        cfgJugarComoBlancas = DEFAULT_JUGAR_COMO_BLANCAS;
        cfgModoJuego = DEFAULT_MODO_JUEGO;
        cfgVelocidadStr = DEFAULT_VELOCIDAD_CPU_STR;
        cfgCpuDelay = DEFAULT_CPU_DELAY_MS;
        if (timerCPU != null) {
            timerCPU.setDelay(cfgCpuDelay);
        }
        this.rotarTablero = false;
        if (panelTablero != null) {
            panelTablero.setRotado(false);
            panelTablero.repaint();
        }
        System.out.println("Configuración restaurada a valores por defecto.");
    }

    private void mostrarDialogoElegirLado() {
        var dialog = new ElegirLadoDialog(this, this.cfgJugarComoBlancas);
        dialog.setVisible(true);
        if (dialog.seDebeEmpezar()) {
            this.cfgJugarComoBlancas = dialog.getLadoSeleccionadoBlancas();
            System.out.println("Lado elegido: " + (cfgJugarComoBlancas ? "Blancas" : "Negras"));
            boolean debeRotarAhora = !cfgJugarComoBlancas
                    && (cfgModoJuego == ModoJuego.HUMANO_VS_CPU || cfgModoJuego == ModoJuego.CPU_VS_CPU);
            if (this.rotarTablero != debeRotarAhora) {
                this.rotarTablero = debeRotarAhora;
                System.out.println("Rotación del tablero: " + this.rotarTablero);
            }
            iniciarPartida();
        } else {
            System.out.println("Selección de lado cancelada.");
        }
    }

    private void mostrarDialogoConfirmarFinalizar() {
        if (!partidaEnCurso || motor.getEstadoActual().esFinDePartida()) {
            return;
        }
        var dialog = new ConfirmarFinalizarDialog(this);
        dialog.setVisible(true);
        if (dialog.isConfirmado()) {
            System.out.println("Confirmado finalizar partida (vía ESC).");
            finalizarPartida();
        } else {
            System.out.println("Finalización cancelada (vía ESC).");
            if (esTurnoDelHumano() && !isPaused) {
                campoMovimiento.requestFocusInWindow();
            }
        }
    }

    private void mostrarDialogoConfirmarCierreApp() {
        // Crear y mostrar el nuevo diálogo personalizado
        var dialog = new ConfirmarCierreAppDialog(this);
        dialog.setVisible(true); // Esta llamada es bloqueante (espera a que se cierre)

        // Comprobar el resultado después de que el diálogo se cierre
        if (dialog.isConfirmado()) {
            System.out.println("Confirmado cierre de aplicación (vía ESC -> Dialog).");
            System.exit(0); // Cierra la JVM
        } else {
            System.out.println("Cierre de aplicación cancelado (vía ESC -> Dialog).");
        }
    }

    private void mostrarDialogoConfiguracion() {
        boolean estabaPausadoCvC = false;
        if (partidaEnCurso && cfgModoJuego == ModoJuego.CPU_VS_CPU && !isPaused) {
            stopAllTimers();
            isPaused = true;
            estabaPausadoCvC = true;
            botonPausaReanudar.setText("Reanudar");
            actualizarEstadoInfo(STATUS_PAUSED_TEXT);
            System.out.println("CPU vs CPU pausado para config.");
        }

        var dialog = new ConfiguracionDialog(this, true, cfgModoJuego, cfgVelocidadStr);
        dialog.setVisible(true);

        if (dialog.seAplicaronCambios()) {
            cfgModoJuego = dialog.getModoSeleccionado();
            cfgVelocidadStr = dialog.getVelocidadSeleccionada();
            cfgCpuDelay = switch (cfgVelocidadStr) {
                case "Lento" ->
                    SLOW_CPU_DELAY_MS;
                default ->
                    DEFAULT_CPU_DELAY_MS;
            };
            if (timerCPU != null) {
                timerCPU.setDelay(cfgCpuDelay);
            }

            boolean debeRotarAhora = !cfgJugarComoBlancas
                    && (cfgModoJuego == ModoJuego.HUMANO_VS_CPU || cfgModoJuego == ModoJuego.CPU_VS_CPU);
            if (this.rotarTablero != debeRotarAhora) {
                this.rotarTablero = debeRotarAhora;
                if (panelTablero != null) {
                    panelTablero.setRotado(this.rotarTablero);
                    panelTablero.repaint();
                }
                System.out.println("Rotación actualizada por config: " + this.rotarTablero);
            }
            System.out.println("Config actualizada: Modo=" + cfgModoJuego + ", Vel=" + cfgVelocidadStr);
            if (partidaEnCurso) {
                System.out.println("Partida finalizada por cambio de config.");
                finalizarPartida();
            } else {
                actualizarEstadoInfo(STATUS_LISTO_TEXT);
                actualizarControlesUI();
            }
            if (estabaPausadoCvC && !partidaEnCurso) {
                isPaused = true;
                botonPausaReanudar.setText("Reanudar");
                actualizarEstadoInfo(STATUS_PAUSED_TEXT);
                System.out.println("CPU vs CPU sigue marcado como pausado post-config.");
            }
        } else {
            System.out.println("Configuración no modificada.");
            if (estabaPausadoCvC && partidaEnCurso) {
                isPaused = false;
                botonPausaReanudar.setText("Pausar");
                if (esTurnoDeLaCPU()) {
                    actualizarEstadoInfo(motor.getEstadoActual());
                    System.out.println("CPU vs CPU reanudado tras cancelar config.");
                    iniciarMovimientoCPUConDelay();
                } else {
                    actualizarEstadoInfo(motor.getEstadoActual());
                }
            } else if (estabaPausadoCvC && !partidaEnCurso) {
                isPaused = false;
                actualizarEstadoInfo(STATUS_LISTO_TEXT);
                System.out.println("Estado pausa CVC limpiado tras cancelar config sin partida.");
            }
        }
        actualizarControlesUI();
    }

    private void iniciarPartida() {
        System.out.println("Iniciando partida...");
        stopAllTimers();
        partidaEnCurso = true;
        isPaused = false;
        this.esPrimerMovimientoCPUDePartida = true;
        botonPausaReanudar.setText("Pausar");
        motor.iniciarNuevaPartida();
        if (panelTablero != null) {
            panelTablero.setRotado(this.rotarTablero);
            panelTablero.setTablero(motor.getTablero());
            panelTablero.setEstadoJuego(motor.getEstadoActual());
            panelTablero.repaint();
        }
        actualizarHistorialDisplay();
        actualizarEstadoInfo(motor.getEstadoActual());
        campoMovimiento.setText("");
        actualizarControlesUI();
        if (esTurnoDeLaCPU() && !motor.getEstadoActual().esFinDePartida() && !isPaused) {
            System.out.println("CPU inicia.");
            iniciarMovimientoCPUConDelay();
        } else if (esTurnoDelHumano()) {
            System.out.println("Humano inicia.");
            campoMovimiento.requestFocusInWindow();
        }
    }

    private void finalizarPartida() {
        System.out.println("Finalizando partida...");
        stopAllTimers();
        partidaEnCurso = false;
        isPaused = false;
        esPrimerMovimientoCPUDePartida = false;
        if (panelTablero != null) {
            panelTablero.setEstadoJuego(EstadoJuego.EN_CURSO);
            panelTablero.repaint();
        }
        actualizarEstadoInfo(STATUS_LISTO_TEXT);
        campoMovimiento.setText("");
        actualizarControlesUI();
    }

    private void actualizarControlesUI() {
        boolean juegoTerminado = motor.getEstadoActual().esFinDePartida();
        boolean juegoActivoYNoPausado = partidaEnCurso && !juegoTerminado && !isPaused;
        boolean esModoCvC = cfgModoJuego == ModoJuego.CPU_VS_CPU;

        botonJugar.setEnabled(!partidaEnCurso || juegoTerminado);

        boolean mostrarPausaReanudar = esModoCvC && partidaEnCurso;
        botonPausaReanudar.setVisible(mostrarPausaReanudar);
        if (mostrarPausaReanudar) {
            botonPausaReanudar.setEnabled(!juegoTerminado);
            botonPausaReanudar.setText(isPaused ? "Reanudar" : "Pausar");
        }

        botonConfigurar.setVisible(true);
        botonConfigurar.setEnabled(!partidaEnCurso || juegoTerminado);

        campoMovimiento.setEnabled(juegoActivoYNoPausado && esTurnoDelHumano());
        if (campoMovimiento.isEnabled()) {
            campoMovimiento.requestFocusInWindow();
        }

        Container parent = botonJugar.getParent();
        if (parent != null) {
            parent.revalidate();
            parent.repaint();
        }
        if (panelDerecho != null) {
            panelDerecho.revalidate();
            panelDerecho.repaint();
        }
    }

    private void togglePausa() {
        if (!partidaEnCurso || motor.getEstadoActual().esFinDePartida() || cfgModoJuego != ModoJuego.CPU_VS_CPU) {
            return;
        }
        isPaused = !isPaused;
        if (isPaused) {
            botonPausaReanudar.setText("Reanudar");
            stopAllTimers();
            actualizarEstadoInfo(STATUS_PAUSED_TEXT);
            System.out.println("CPU vs CPU pausado.");
        } else {
            botonPausaReanudar.setText("Pausar");
            actualizarEstadoInfo(motor.getEstadoActual());
            System.out.println("CPU vs CPU reanudado.");
            if (esTurnoDeLaCPU()) {
                iniciarMovimientoCPUConDelay();
            }
        }
        actualizarControlesUI();
    }

    private boolean esTurnoDelHumano() {
        if (!partidaEnCurso || motor.getEstadoActual().esFinDePartida() || isPaused) {
            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) {
            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 iniciarMovimientoCPUConDelay() {
        if (!partidaEnCurso || !esTurnoDeLaCPU() || isPaused || motor.getEstadoActual().esFinDePartida() || timerDelayMovimientoCPU.isRunning()) {
            if (cfgModoJuego == ModoJuego.CPU_VS_CPU && timerCPU.isRunning() && !esTurnoDeLaCPU()) {
                timerCPU.stop();
            }
            return;
        }
        if (esPrimerMovimientoCPUDePartida) {
            System.out.println("Delay inicial CPU: " + PRIMER_MOV_CPU_DELAY_MS + "ms");
            timerDelayMovimientoCPU.setInitialDelay(PRIMER_MOV_CPU_DELAY_MS);
            this.esPrimerMovimientoCPUDePartida = false;
            timerDelayMovimientoCPU.start();
            return;
        }

        if ("Rápido".equals(cfgVelocidadStr)) {
            timerDelayMovimientoCPU.setInitialDelay(50);
            timerDelayMovimientoCPU.start();
            return;
        }

        int delayPrevio = (cfgModoJuego == ModoJuego.CPU_VS_CPU)
                ? MIN_CPU_MOVE_DELAY_MS
                : Math.max(MIN_CPU_MOVE_DELAY_MS, cfgCpuDelay);

        if (cfgModoJuego == ModoJuego.CPU_VS_CPU && timerCPU.isRunning()) {
            timerCPU.stop();
        }

        timerDelayMovimientoCPU.setInitialDelay(delayPrevio);
        timerDelayMovimientoCPU.start();
    }

    private void realizarMovimientoCPUConDelay() {
        if (!partidaEnCurso || !esTurnoDeLaCPU() || isPaused || motor.getEstadoActual().esFinDePartida()) {
            if (cfgModoJuego == ModoJuego.CPU_VS_CPU && timerCPU.isRunning()) {
                timerCPU.stop();
            }
            return;
        }
        EstadoJuego resultadoCPU = motor.realizarMovimientoAleatorioCPU();
        actualizarUIPostMovimiento(resultadoCPU);

        if (resultadoCPU.esFinDePartida()) {
            partidaEnCurso = false;
            actualizarControlesUI();
            if (timerCPU.isRunning()) {
                timerCPU.stop();
            }
        } else {
            if (cfgModoJuego == ModoJuego.CPU_VS_CPU) {
                if (!isPaused && partidaEnCurso) {
                    iniciarMovimientoCPUConDelay();
                }
            } else {
                actualizarControlesUI();
            }
        }
    }

    private void procesarEntradaUsuario() {
        if (!partidaEnCurso || !esTurnoDelHumano() || isPaused || motor.getEstadoActual().esFinDePartida()
                || motor.getEstadoActual() == EstadoJuego.PROMOCION_REQUIERIDA) {
            campoMovimiento.setText("");
            return;
        }
        String input = campoMovimiento.getText().trim().toUpperCase();
        campoMovimiento.setText("");
        if (input.length() != 4 || !Pattern.matches("^[A-H][1-8][A-H][1-8]$", input)) {
            mostrarErrorUsuario(ERROR_FORMAT_TEXT);
            actualizarControlesUI();
            return;
        }
        Posicion origen = Posicion.desdeNotacion(input.substring(0, 2));
        Posicion destino = Posicion.desdeNotacion(input.substring(2, 4));
        if (origen == null || destino == null) {
            mostrarErrorUsuario(ERROR_COORD_TEXT);
            actualizarControlesUI();
            return;
        }

        EstadoJuego resultado = motor.intentarMover(origen, destino);

        switch (resultado) {
            case MOVIMIENTO_INVALIDO -> {
                mostrarErrorUsuario(ERROR_MOVE_TEXT);
                actualizarControlesUI();
            }
            case PROMOCION_REQUIERIDA -> {
                actualizarEstadoInfo(resultado);
                char piezaPromocion = promocionarPeonGrafico();
                if (piezaPromocion == 0) {
                    System.out.println("Promoción cancelada por el usuario.");
                    actualizarEstadoInfo(EstadoJuego.PROMOCION_REQUIERIDA);
                    campoMovimiento.requestFocusInWindow();
                    return;
                }
                EstadoJuego resultadoPromo = motor.completarPromocion(piezaPromocion);
                actualizarUIPostMovimiento(resultadoPromo);
                if (resultadoPromo.esFinDePartida()) {
                    partidaEnCurso = false;
                    actualizarControlesUI();
                } else if (esTurnoDeLaCPU() && !isPaused) {
                    iniciarMovimientoCPUConDelay();
                } else {
                    actualizarControlesUI();
                }
            }
            default -> {
                actualizarUIPostMovimiento(resultado);
                if (resultado.esFinDePartida()) {
                    partidaEnCurso = false;
                    actualizarControlesUI();
                } else if (esTurnoDeLaCPU() && !isPaused) {
                    iniciarMovimientoCPUConDelay();
                } else {
                    actualizarControlesUI();
                }
            }
        }
    }

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

    private char promocionarPeonGrafico() {
        boolean promocionEsBlanca = !motor.esTurnoBlancas();
        System.out.println("Promoción requerida para peón " + (promocionEsBlanca ? "blanco." : "negro."));
        var dialog = new PromocionDialog(this, promocionEsBlanca);
        dialog.setVisible(true);

        if (dialog.isSeleccionRealizada()) {
            char piezaElegida = dialog.getPiezaSeleccionada();
            System.out.println("Pieza de promoción elegida: " + piezaElegida);
            return piezaElegida;
        } else {
            return 0;
        }
    }

    private void actualizarHistorialDisplay() {
        List<String> notacionMotor = motor.getHistorialNotacion();
        var sb = new StringBuilder();
        String header = String.format(HISTORIAL_HEADER_FORMAT, "id", "BLANCAS", "NEGRAS");
        String separator = String.format(HISTORIAL_SEPARATOR_FORMAT, "---", "--------", "--------");
        sb.append(header).append("\n").append(separator).append("\n");
        int numeroTurno = 1;
        for (int i = 0; i < notacionMotor.size(); i += 2) {
            String movBlanco = notacionMotor.get(i);
            String movNegro = (i + 1 < notacionMotor.size()) ? notacionMotor.get(i + 1) : "";
            String linea = String.format(HISTORIAL_MOVE_FORMAT_FULL, numeroTurno, movBlanco, movNegro);
            sb.append(linea).append("\n");
            numeroTurno++;
        }
        final String textoHistorial = sb.toString();
        SwingUtilities.invokeLater(() -> {
            areaHistorial.setText(textoHistorial);
            JScrollBar vertical = scrollHistorial.getVerticalScrollBar();
            if (vertical != null) {
                vertical.setValue(vertical.getMaximum());
            }
        });
    }

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

    private void restaurarEstadoInfoPostError() {
        if (!timerMensajeError.isRunning()) {
            if (!partidaEnCurso || motor.getEstadoActual().esFinDePartida()) {
                actualizarEstadoInfo(STATUS_LISTO_TEXT);
            } else if (isPaused) {
                actualizarEstadoInfo(STATUS_PAUSED_TEXT);
            } else {
                actualizarEstadoInfo(motor.getEstadoActual());
            }
        }
    }

    private void actualizarEstadoInfo(EstadoJuego estado) {
        if (estado == null) {
            actualizarEstadoInfo(STATUS_LISTO_TEXT);
            return;
        }
        if (isPaused) {
            actualizarEstadoInfo(STATUS_PAUSED_TEXT);
        } else if (!partidaEnCurso && !estado.esFinDePartida()) {
            actualizarEstadoInfo(STATUS_LISTO_TEXT);
        } else {
            actualizarEstadoInfo(estado.getMensaje());
        }
    }

    private void actualizarEstadoInfo(String texto) {
        if (texto == null) {
            texto = "";
        }
        if (texto.length() > MAX_ESTADO_LABEL_LENGTH) {
            texto = texto.substring(0, MAX_ESTADO_LABEL_LENGTH).trim() + "..";
        }
        final String textoFinal = texto.toUpperCase();
        SwingUtilities.invokeLater(() -> {
            labelEstadoInfo.setFont(FONT_COURIER_BOLD_20_ESTADO);
            labelEstadoInfo.setForeground(COLOR_ESTADO_NORMAL);
            labelEstadoInfo.setText(textoFinal);
        });
    }

    private static class UppercaseDocumentFilter extends DocumentFilter {

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

        @Override
        public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
            if (text != null) {
                fb.replace(offset, length, text.toUpperCase(), attrs);
            }
        }
    }

    private void configurarTeclaEscAbandono() {
        InputMap inputMap = getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = getRootPane().getActionMap();
        KeyStroke escapeKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
        String actionKey = "escapeAction";
        inputMap.put(escapeKeyStroke, actionKey);
        actionMap.put(actionKey, new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                boolean esPartidaActivaNoFinalizada = partidaEnCurso && !motor.getEstadoActual().esFinDePartida();

                if (esPartidaActivaNoFinalizada) {
                    System.out.println("ESC presionado durante partida activa.");
                    mostrarDialogoConfirmarFinalizar();
                } else {
                    System.out.println("ESC presionado fuera de partida activa.");
                    mostrarDialogoConfirmarCierreApp();
                }
            }
        });
    }

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatLightLaf());
            System.out.println("LookAndFeel establecido a: FlatLaf Light");
            UIManager.put("Button.paintBorders", true);
        } catch (UnsupportedLookAndFeelException ex) {
            System.err.println("Error al inicializar FlatLaf. Usando LookAndFeel por defecto.");
            try {
                UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
                System.out.println("Fallback LookAndFeel: Metal (CrossPlatform)");
            } catch (Exception e) {
                System.err.println("Error estableciendo incluso Metal LaF.");
            }
        }

        final int ANIMATION_DURATION_MS = 1300;
        final int EXTRA_DELAY_MS = 1000;
        final int SPLASH_DURATION_MS = ANIMATION_DURATION_MS + EXTRA_DELAY_MS;
        final var splash = new SplashScreen();
        splash.startProgress(ANIMATION_DURATION_MS);
        SwingUtilities.invokeLater(() -> splash.setVisible(true));

        var splashTimer = new Timer(SPLASH_DURATION_MS, e -> {
            splash.dispose();
            SwingUtilities.invokeLater(() -> {
                var frame = new AjedrezGrafico();
                frame.setVisible(true);
            });
        });
        splashTimer.setRepeats(false);
        splashTimer.start();
    }
}


Código Java 2 (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.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import javax.swing.JPanel;

/**
 * Componente JPanel que dibuja el tablero de ajedrez, las piezas y las
 * coordenadas. Puede dibujarse normal o rotado.
 */

public class PanelTablero extends JPanel {

    private Tablero tableroJuego; // Referencia al estado lógico
    private final int TAMANO_PANEL_PREFERIDO = 550;
    private final int CASILLAS = 8;
    private final int ESPACIO_COORD = 8; // Espacio borde-coordenadas

    // Fuentes
    private static final String RUTA_FUENTE_PIEZAS = "/ajedrez_gui/assets/CASEFONT.TTF";
    private static final float TAMANO_FUENTE_PIEZAS = 46f;
    private static final String NOMBRE_FUENTE_COORDENADAS = "Courier New";
    private static final int TAMANO_FUENTE_COORDENADAS = 22;
    private static final int ESTILO_FUENTE_COORDENADAS = Font.BOLD;
    private static final String FUENTE_FALLBACK_POR_DEFECTO = "Dialog";

    private final Font FONT_PIEZAS_CARGADA;
    private final Font FONT_COORDENADAS;

    // --- Constantes de Colores ---
    private final Color BACKGROUND_COLOR = new Color(190, 190, 190);
    private final Color SQUARE_COLOR_DARK = Color.GRAY;               // Casilla oscura
    private final Color COLOR_COORDENADAS = Color.BLACK;              // Color de A-H, 1-8
    private final Color COLOR_CONTORNO = Color.BLACK;                 // Borde del tablero
    private final Color COLOR_PIEZA_BLANCA = Color.WHITE;             // Color piezas blancas
    private final Color COLOR_PIEZA_NEGRA = Color.BLACK;              // Color piezas negras

    // Estado del panel
    private boolean rotado = false; // true -> negras abajo
    private EstadoJuego estadoActualDelJuego = EstadoJuego.EN_CURSO; // Para efecto rey mate

    public PanelTablero() {
        FONT_PIEZAS_CARGADA = GuiUtils.cargarFuenteDesdeRecurso(
                RUTA_FUENTE_PIEZAS, TAMANO_FUENTE_PIEZAS, FUENTE_FALLBACK_POR_DEFECTO);
        FONT_COORDENADAS = new Font(NOMBRE_FUENTE_COORDENADAS, ESTILO_FUENTE_COORDENADAS, TAMANO_FUENTE_COORDENADAS);

        this.setPreferredSize(new Dimension(TAMANO_PANEL_PREFERIDO, TAMANO_PANEL_PREFERIDO));
        this.setBackground(BACKGROUND_COLOR);
        this.tableroJuego = null;
    }

    // --- Setters ---
    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 nuevoEstado = (estado != null) ? estado : EstadoJuego.EN_CURSO;
        if (this.estadoActualDelJuego != nuevoEstado) {
            this.estadoActualDelJuego = nuevoEstado;
            this.repaint();
        }
    }

    // --- Dibujo principal ---
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        if (!(g instanceof Graphics2D g2)) {
            return;
        }
        setupAntialiasing(g2);

        // Cálculo de dimensiones y offsets
        FontMetrics fmCoord = g2.getFontMetrics(FONT_COORDENADAS);
        int paddingInferior = fmCoord.getHeight() + ESPACIO_COORD;
        int paddingIzquierdo = fmCoord.stringWidth("8") + ESPACIO_COORD;
        int paddingSuperior = fmCoord.getAscent() / 2 + ESPACIO_COORD / 2;
        int paddingDerecho = ESPACIO_COORD;

        int anchoPanel = getWidth();
        int altoPanel = getHeight();
        int anchoTableroEfectivo = anchoPanel - paddingIzquierdo - paddingDerecho;
        int altoTableroEfectivo = altoPanel - paddingSuperior - paddingInferior;
        int tamanoTableroDibujo = Math.min(anchoTableroEfectivo, altoTableroEfectivo);
        if (tamanoTableroDibujo < 0) {
            tamanoTableroDibujo = 0;
        }

        int radio = (tamanoTableroDibujo > 0) ? tamanoTableroDibujo / CASILLAS : 0;
        int tamanoTableroReal = radio * CASILLAS;

        int offsetXTablero = paddingIzquierdo + (anchoTableroEfectivo - tamanoTableroReal) / 2;
        int offsetYTablero = paddingSuperior + (altoTableroEfectivo - tamanoTableroReal) / 2;

        // Dibujar componentes
        if (tamanoTableroReal > 0 && radio > 0) {
            dibujarTablero(g2, offsetXTablero, offsetYTablero, radio);
            if (tableroJuego != null) {
                dibujarPiezas(g2, offsetXTablero, offsetYTablero, radio);
            }
            dibujarContorno(g2, offsetXTablero, offsetYTablero, tamanoTableroReal);
            dibujarCoordenadas(g2, offsetXTablero, offsetYTablero, radio, tamanoTableroReal, fmCoord);
        } else {
            g2.setColor(Color.RED);
            g2.drawString("Panel muy pequeño", 10, 20);
        }
    }

    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);
    }

    // Dibuja las casillas
    private void dibujarTablero(Graphics2D g2, int offsetX, int offsetY, int radio) {
        g2.setColor(SQUARE_COLOR_DARK); // Establecer color para casillas oscuras
        for (int j = 0; j < CASILLAS; j++) { // Filas (vertical)
            for (int i = 0; i < CASILLAS; i++) { // Columnas (horizontal)
                if ((i + j) % 2 != 0) {
                    g2.fillRect(offsetX + i * radio, offsetY + j * radio, radio, radio);
                }
            }
        }
    }    

    // Dibuja las piezas
    private void dibujarPiezas(Graphics2D g2, int offsetX, int offsetY, int radio) {
        if (FONT_PIEZAS_CARGADA == null || tableroJuego == null) {
            return;
        }

        for (int x = 0; x < Tablero.TAMANO_TABLERO; x++) { // Filas lógicas 0-7
            for (int y = 0; y < Tablero.TAMANO_TABLERO; y++) { // Columnas lógicas 0-7
                Posicion posLogica = new Posicion(x, y);
                Pieza pieza = tableroJuego.obtenerPieza(posLogica);

                if (pieza != null) {
                    String piezaChar = obtenerCaracterPiezaFuente(pieza);

                    // Calcular coords gráficas (fila/col en panel)
                    int filaGraf, colGraf;
                    if (rotado) { // Negras abajo
                        filaGraf = x;
                        colGraf = (CASILLAS - 1) - y;
                    } else { // Blancas abajo
                        filaGraf = (CASILLAS - 1) - x;
                        colGraf = y;
                    }

                    Rectangle rectCasilla = new Rectangle(
                            offsetX + colGraf * radio, offsetY + filaGraf * radio, radio, radio);

                    g2.setColor(pieza.esBlanca() ? COLOR_PIEZA_BLANCA : COLOR_PIEZA_NEGRA);

                    // Rotar rey si está en mate
                    boolean rotarEsteRey = false;
                    if (pieza instanceof Rey rey && estadoActualDelJuego.esJaqueMate()) {
                        boolean esReyPerdedor
                                = (estadoActualDelJuego == EstadoJuego.JAQUEMATE_GANA_NEGRAS && rey.esBlanca())
                                || (estadoActualDelJuego == EstadoJuego.JAQUEMATE_GANA_BLANCAS && !rey.esBlanca());
                        if (esReyPerdedor) {
                            rotarEsteRey = true;
                        }
                    }

                    // Dibujar pieza (rotada o normal)
                    if (rotarEsteRey) {
                        AffineTransform originalTransform = g2.getTransform();
                        g2.rotate(Math.PI / 2.0, rectCasilla.getCenterX(), rectCasilla.getCenterY());
                        centrarTexto(g2, piezaChar, rectCasilla, FONT_PIEZAS_CARGADA);
                        g2.setTransform(originalTransform);
                    } else {
                        centrarTexto(g2, piezaChar, rectCasilla, FONT_PIEZAS_CARGADA);
                    }
                }
            }
        }
    }

    // Dibuja el borde del tablero
    private void dibujarContorno(Graphics2D g2, int offsetX, int offsetY, int tamanoTableroReal) {
        Stroke bordeGrosor = new BasicStroke(2f);
        g2.setColor(COLOR_CONTORNO);
        g2.setStroke(bordeGrosor);
        g2.drawRect(offsetX, offsetY, tamanoTableroReal, tamanoTableroReal);
        g2.setStroke(new BasicStroke(1f));
    }

    // Dibuja coordenadas A-H, 1-8
    private void dibujarCoordenadas(Graphics2D g2, int offsetXTablero, int offsetYTablero, int radio, int tamanoTableroReal, FontMetrics fm) {
        if (FONT_COORDENADAS == null) {
            return;
        }
        g2.setFont(FONT_COORDENADAS);
        g2.setColor(COLOR_COORDENADAS);

        // Letras (A-H) debajo
        int yLetras = offsetYTablero + tamanoTableroReal + ESPACIO_COORD + fm.getAscent();
        for (int i = 0; i < CASILLAS; i++) {
            char letraChar = rotado ? (char) ('a' + (CASILLAS - 1 - i)) : (char) ('a' + i);
            String letra = String.valueOf(letraChar);
            int xLetra = offsetXTablero + i * radio + (radio - fm.stringWidth(letra)) / 2;
            g2.drawString(letra.toUpperCase(), xLetra, yLetras); // Mostrar en mayúsculas
        }

        // Números (1-8) a la izquierda
        int xNumeros = offsetXTablero - ESPACIO_COORD - fm.stringWidth("8");
        if (xNumeros < ESPACIO_COORD / 2) {
            xNumeros = ESPACIO_COORD / 2;
        }
        for (int j = 0; j < CASILLAS; j++) {
            int numeroInt = rotado ? (j + 1) : (CASILLAS - j);
            String numero = String.valueOf(numeroInt);
            int yNumero = offsetYTablero + j * radio + (radio - fm.getHeight()) / 2 + fm.getAscent();
            g2.drawString(numero, xNumeros, yNumero);
        }
    }

    // Centra texto en un rectángulo
    private void centrarTexto(Graphics g, String texto, Rectangle r, Font f) {
        if (f == null) {
            f = g.getFont();
        }
        FontMetrics metrics = g.getFontMetrics(f);
        int x = r.x + (r.width - metrics.stringWidth(texto)) / 2;
        int y = r.y + ((r.height - metrics.getHeight()) / 2) + metrics.getAscent();
        g.setFont(f);
        g.drawString(texto, x, y);
    }

    // Mapea Pieza a carácter en CASEFONT
    private String obtenerCaracterPiezaFuente(Pieza pieza) {
        // Usar pattern matching for instanceof
        return switch (pieza) {
            case Rey r ->
                "l"; // l minúscula
            case Dama q ->
                "w";
            case Torre t ->
                "t";
            case Alfil b ->
                "v";
            case Caballo k ->
                "m";
            case Peon p ->
                "o";
        };
    }
}


Código Java 3 (SplashScreen.java):

package ajedrez_gui;

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

/**
 * Pantalla de bienvenida (splash screen) con una imagen y una barra de
 * progreso.
 */

public class SplashScreen extends JWindow {

    // Dimensiones deseadas para la imagen escalada
    private static final int IMAGE_TARGET_WIDTH = 640;
    private static final int IMAGE_TARGET_HEIGHT = 320;

    // Estilo barra de progreso
    private static final int PROGRESS_BAR_HEIGHT = 25;
    private static final int BAR_MARGIN_HORIZONTAL = 60; // Margen izq/der
    private static final Color PROGRESS_BAR_COLOR = Color.GRAY;
    private static final int VERTICAL_OFFSET_FROM_CENTER = 120; // Desplazamiento Y desde el centro

    // Tamaño de la ventana (igual a la imagen)
    private static final int WINDOW_WIDTH = IMAGE_TARGET_WIDTH;
    private static final int WINDOW_HEIGHT = IMAGE_TARGET_HEIGHT;

    // Ruta a la imagen en el classpath
    private static final String IMAGE_PATH = "/ajedrez_gui/assets/chessmate.png";

    private JProgressBar progressBar;
    private Timer progressTimer;
    private double currentProgress = 0; // Usar double para incrementos pequeños

    public SplashScreen() {
        setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
        setLocationRelativeTo(null); // Centrar en pantalla

        // Usar JLayeredPane para poner la barra sobre la imagen
        var layeredPane = new JLayeredPane();
        layeredPane.setPreferredSize(new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));

        // Capa 0: Imagen de fondo
        var imageLabel = new JLabel();
        URL imageURL = getClass().getResource(IMAGE_PATH);
        if (imageURL != null) {
            var originalIcon = new ImageIcon(imageURL);
            // Escalar imagen suavemente
            Image scaledImage = originalIcon.getImage().getScaledInstance(
                    IMAGE_TARGET_WIDTH, IMAGE_TARGET_HEIGHT, Image.SCALE_SMOOTH);
            imageLabel.setIcon(new ImageIcon(scaledImage));
        } else {
            System.err.println("SplashScreen Error: No se pudo cargar imagen: " + IMAGE_PATH);
            // Mostrar error si no se carga la imagen
            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); // Capa inferior

        // Capa 1: Barra de progreso
        progressBar = new JProgressBar(0, 100);
        progressBar.setValue(0);
        progressBar.setStringPainted(false); // Sin porcentaje
        progressBar.setBorderPainted(false); // Sin borde
        progressBar.setForeground(PROGRESS_BAR_COLOR);
        progressBar.setOpaque(false); // Fondo transparente

        // Posicionar la barra
        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;
        // Asegurar que no se salga por abajo
        barY = Math.min(barY, WINDOW_HEIGHT - PROGRESS_BAR_HEIGHT - 5);

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

        setContentPane(layeredPane);
    }

    /**
     * Inicia la animación de la barra de progreso.
     *
     * @param animationDurationMillis Duración total deseada para la animación
     * (llenado de la barra).
     */

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

        if (animationDurationMillis <= 0) {
            animationDurationMillis = 100; // Mínimo
        }
        // Calcular intervalo y número de pasos para una animación fluida
        int updates = 50; // Número de actualizaciones deseadas
        int updateInterval = Math.max(10, animationDurationMillis / updates); // Intervalo entre updates (mín 10ms)
        double incrementPerStep = 100.0 / (double) (animationDurationMillis / updateInterval); // Incremento por update

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

            // Actualizar barra en el EDT
            SwingUtilities.invokeLater(() -> progressBar.setValue(progressToShow));

            if (progressToShow >= 100) {
                ((Timer) e.getSource()).stop(); // Detener timer al llegar a 100%
            }
        });

        progressTimer.setInitialDelay(0); // Empezar inmediatamente
        progressTimer.start();
    }

    // Sobrescribir dispose para detener el timer si aún está activo
    @Override
    public void dispose() {
        if (progressTimer != null && progressTimer.isRunning()) {
            progressTimer.stop();
        }
        super.dispose();
    }
}


Código Java 4 (GuiUtils.java):

package ajedrez_gui;

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

/**
 * Clase de utilidad para funciones relacionadas con la GUI. Contiene métodos
 * para cargar fuentes y aplicar estilos.
 */

public final class GuiUtils {

    // Constructor privado para evitar instanciación
    private GuiUtils() {
        throw new IllegalStateException("Clase de utilidad, no instanciable.");
    }

    /**
     * Carga una fuente TrueType (.ttf) desde un recurso del classpath.
     *
     * @param resourcePath Ruta al archivo .ttf dentro del classpath (e.g.,
     * "/ajedrez_gui/assets/font.ttf").
     * @param size Tamaño deseado de la fuente (como float).
     * @param fallbackFontName Nombre de la fuente de fallback (e.g., "Dialog").
     * @return La Font cargada o la de fallback.
     */

    public static Font cargarFuenteDesdeRecurso(String resourcePath, float size, 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); // Registrarla es buena práctica
                customFont = fontBase.deriveFont(Font.PLAIN, size); // Derivar con tamaño
                System.out.println("Fuente 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) { // Captura genérica
            System.err.println("Error inesperado cargando fuente: " + resourcePath);
            e.printStackTrace();
        }

        // Usar fallback si la carga falló
        if (customFont == null) {
            System.err.println("FALLBACK: Usando fuente '" + fallbackFontName + "' tamaño " + size);
            customFont = new Font(fallbackFontName, Font.PLAIN, (int) size);
        }
        return customFont;
    }

    /**
     * Aplica estilo (fuente, tamaño fijo, no focusable) a un JButton.
     *
     * @param button El JButton a estilizar.
     * @param font La Font a aplicar.
     * @param size La Dimension fija para el botón (pref, min, max).
     */

    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); // Evita el borde punteado de foco
    }
}


Resultado visual:




No hay comentarios:

Publicar un comentario

Con la tecnología de Blogger.