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.5: Representación de las Piezas.

Punto en el que nos encontramos dentro del proyecto:

 1 Primer esbozo del tablero de ajedrez (2018)
 2 Explicación normativa y reglas del juego (2024)
 3 Interfaz Gráfica Principal (2025)
   .AjedrezGrafico.java
   .PanelTablero.java
   .SplashScreen.java
   .GuiUtils.java
 4 Ventanas de Diálogo (2025)
   .ConfiguracionDialog.java
   .ElegirLadoDialog.java
   .PromocionDialog.java
   .ConfirmarFinalizarDialog.java
   .ConfirmarCierreAppDialog.java
 5 Lógica Central del Juego (2025)
   .MotorJuego.java
   .Tablero.java
   .Movimiento.java
   .EstadoJuego.java
   .Posicion.java

>6 Representación de las Piezas
   .Pieza.java
   .Peon.java
   .Caballo.java
   .Alfil.java
   .Torre.java
   .Dama.java
   .Rey.java



Con este último paso, en el que se han implementado las clases de las piezas, hemos alcanzado el punto en el que la aplicación está lista para ser compilada y utilizada para jugar.


Código Java 1 (Pieza.java):

package ajedrez_gui;

/**
 * Clase base abstracta y sellada para todas las piezas de ajedrez. Define
 * propiedades comunes y métodos abstractos/protegidos.
 */

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

    protected final boolean esBlanca; // Color de la pieza
    protected boolean haMovido;      // Flag para reglas especiales (enroque, mov peón)

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

    // Getters y Setters
    public boolean esBlanca() {
        return esBlanca;
    }

    public boolean haMovido() {
        return haMovido;
    }

    public void setMovida(boolean movida) {
        this.haMovido = movida;
    }

    /**
     * Verifica si un movimiento es válido según las reglas básicas de la pieza,
     * sin considerar jaques propios.
     *
     * @param origen Posición actual.
     * @param destino Posición deseada.
     * @param tablero Estado actual del tablero.
     * @return true si el movimiento básico es válido.
     */

    public abstract boolean esMovimientoBasicoValido(Posicion origen, Posicion destino, Tablero tablero);

    /**
     * Verifica si una casilla está vacía o contiene una pieza enemiga.
     */

    protected boolean esCasillaVaciaOEnemiga(Posicion pos, Tablero tablero) {
        if (!pos.esValida()) {
            return false;
        }
        Pieza piezaDestino = tablero.obtenerPieza(pos);
        return piezaDestino == null || piezaDestino.esBlanca() != this.esBlanca;
    }

    /**
     * Verifica si el camino recto (horizontal/vertical) está libre hasta la
     * casilla destino (exclusive).
     */

    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; // No es recto
        }
        if (dx == 0 && dy == 0) {
            return true;  // Mismo origen/destino (camino trivialmente libre)
        }
        int x = origen.x() + dx;
        int y = origen.y() + dy;
        while (x != destino.x() || y != destino.y()) {
            Posicion actual = new Posicion(x, y);
            // if (!actual.esValida()) return false; // No debería pasar si destino es válido
            if (tablero.obtenerPieza(actual) != null) {
                return false; // Bloqueado
            }
            x += dx;
            y += dy;
        }
        return true; // Camino libre
    }

    /**
     * Verifica si el camino diagonal está libre hasta la casilla destino
     * (exclusive).
     */

    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; // No es diagonal
        }
        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 (!actual.esValida()) return false;
            if (tablero.obtenerPieza(actual) != null) {
                return false; // Bloqueado
            }
            x += dx_step;
            y += dy_step;
        }
        return true; // Camino libre
    }

    /**
     * Crea una copia de la pieza con su estado actual.
     */

    public abstract Pieza copiar();

    /**
     * Devuelve la representación estándar de la pieza (mayúscula/minúscula).
     * P/p, N/n, B/b, R/r, Q/q, K/k.
     */

    @Override
    public abstract String toString();
}


Código Java 2 (Peon.java):

package ajedrez_gui;

/**
 * Representa la pieza Peón. Clase final que implementa la interfaz sellada
 * Pieza.
 */

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; // 1: blancas (arriba), -1: negras (abajo)
        int dx = destino.x() - origen.x(); // Cambio fila
        int dy = destino.y() - origen.y(); // Cambio columna
        Pieza piezaDestino = tablero.obtenerPieza(destino);

        // 1. Avance simple (1 casilla)
        if (dx == direccion && dy == 0 && piezaDestino == null) {
            return true;
        }

        // 2. Avance doble inicial (2 casillas)
        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;
        }

        // 3. Captura normal (diagonal)
        if (dx == direccion && Math.abs(dy) == 1 && piezaDestino != null && piezaDestino.esBlanca() != this.esBlanca()) {
            return true;
        }

        // 4. Captura En Passant
        Posicion enPassantTarget = tablero.getEnPassant();
        if (enPassantTarget != null && destino.equals(enPassantTarget)
                && dx == direccion && Math.abs(dy) == 1) {
            // Validar que realmente haya un peón enemigo para capturar EP
            // (Aunque Movimiento.determinarDetalles lo hace, es bueno tenerlo aquí también)

            Posicion peonCapturadoPos = new Posicion(origen.x(), destino.y());
            Pieza peonACapturar = tablero.obtenerPieza(peonCapturadoPos);
            if (peonACapturar instanceof Peon && peonACapturar.esBlanca() != this.esBlanca()) {
                return true; // Movimiento hacia casilla EP es válido
            }
        }

        return false; // Ninguna regla básica se cumplió
    }

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

    @Override
    public String toString() {
        // Notación estándar: P para blanco, p para negro
        return esBlanca() ? "P" : "p";
    }
}


Código Java 3 (Caballo.java):

package ajedrez_gui;

/**
 * Representa la pieza Caballo. Se mueve en "L". Clase final que implementa la
 * interfaz sellada Pieza.
 */

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;
        }
        // Movimiento en "L": 2 casillas en una dirección (x o y) y 1 en la perpendicular
        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)) {
            // Caballo salta, no necesita camino libre, solo verificar casilla destino
            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() {
        // Notación estándar: N para blanco, n para negro (Knight)
        return esBlanca() ? "N" : "n";
    }
}


Código Java 4 (Alfil.java):

package ajedrez_gui;

/**
 * Representa la pieza Alfil. Se mueve en diagonal. Clase final que implementa
 * la interfaz sellada Pieza.
 */

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;
        }
        // Movimiento diagonal: cambio absoluto en X debe ser igual al cambio absoluto en Y
        int dxAbs = Math.abs(destino.x() - origen.x());
        int dyAbs = Math.abs(destino.y() - origen.y());

        // Debe ser diagonal (dxAbs == dyAbs) y moverse al menos una casilla (dxAbs > 0)
        if (dxAbs == dyAbs) {
            // Verificar camino libre y si la casilla destino es válida (vacía o enemiga)
            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() {
        // Notación estándar: B para blanco, b para negro (Bishop)
        return esBlanca() ? "B" : "b";
    }
}


Código Java 5 (Torre.java):

package ajedrez_gui;

/**
 * Representa la pieza Torre. Se mueve horizontal o verticalmente. Clase final
 * que implementa la interfaz sellada Pieza.
 */

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;
        }
        // Movimiento debe ser puramente horizontal o vertical
        boolean esMovimientoRecto = (origen.x() == destino.x() && origen.y() != destino.y())
                || (origen.x() != destino.x() && origen.y() == destino.y());

        if (esMovimientoRecto) {
            // Verificar camino libre y casilla destino válida
            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() {
        // Notación estándar: R para blanco, r para negro (Rook)
        return esBlanca() ? "R" : "r";
    }
}


Código Java 6 (Dama.java):

package ajedrez_gui;

/**
 * Representa la pieza Dama (Reina). Se mueve como Torre y Alfil combinados.
 * Clase final que implementa la interfaz sellada Pieza.
 */

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

        // Movimiento recto (como Torre)
        boolean esMovimientoRecto = (dx == 0 && dy != 0) || (dx != 0 && dy == 0);
        // Movimiento diagonal (como Alfil)
        boolean esMovimientoDiagonal = dxAbs == dyAbs; // dxAbs > 0 está implícito si no es recto y origen!=destino

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

        return false; // No es ni recto ni diagonal
    }

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

    @Override
    public String toString() {
        // Notación estándar: Q para blanco, q para negro (Queen)
        return esBlanca() ? "Q" : "q";
    }
}


Código Java 7 (Rey.java):

package ajedrez_gui;

/**
 * Representa la pieza Rey. Se mueve una casilla en cualquier dirección. Tiene
 * el movimiento especial de Enroque. Clase final que implementa la interfaz
 * sellada Pieza.
 */

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

        // 1. Movimiento normal (1 casilla)
        if (dx_abs <= 1 && dy_abs <= 1 && (dx_abs != 0 || dy_abs != 0)) {
            return esCasillaVaciaOEnemiga(destino, tablero);
        }

        // 2. Movimiento de Enroque (2 casillas horizontalmente)
        // Solo posible si el rey no se ha movido

        if (!this.haMovido && dx_abs == 0 && dy_abs == 2) {
            // Determinar si es corto (derecha) o largo (izquierda)
            boolean ladoCorto = destino.y() > origen.y();
            // La validación completa del enroque (camino libre, no jaque, etc.)
            return validarEnroqueCompleto(origen, ladoCorto, tablero);
        }

        return false; // Ni movimiento normal ni enroque
    }

    /**
     * Valida todas las condiciones para un enroque. Llamado desde
     * esMovimientoBasicoValido.
     */

    private boolean validarEnroqueCompleto(Posicion posRey, boolean ladoCorto, Tablero tablero) {
        // 1. Verificar derechos de enroque (flags del tablero)
        if (!tablero.puedeEnrocar(this.esBlanca, ladoCorto)) {
            return false;
        }

        // 2. Rey no puede estar en jaque actualmente
        if (tablero.estaEnJaque(this.esBlanca)) {
            return false;
        }

        int fila = posRey.x();
        int colRey = posRey.y();
        int direccion = ladoCorto ? 1 : -1; // 1 para corto, -1 para largo
        int colTorre = ladoCorto ? 7 : 0; // Columna H o A

        // 3. Casillas entre rey y torre deben estar vacías
        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; // Camino bloqueado
            }
        }

        // 4. Casillas por las que pasa el rey no deben estar atacadas por el oponente
        Posicion casillaPaso1 = new Posicion(fila, colRey + direccion);
        Posicion casillaPaso2 = new Posicion(fila, colRey + 2 * direccion); // Destino final del rey
        if (tablero.estaAtacada(casillaPaso1, !this.esBlanca)
                || tablero.estaAtacada(casillaPaso2, !this.esBlanca)) {
            return false; // Rey pasa por o termina en jaque
        }

        // 5. Verificar si la torre está en su sitio y no se ha movido
        // (Esto es redundante si los flags de enroque se manejan correctamente, pero es una buena doble verificación)

        Posicion posTorre = new Posicion(fila, colTorre);
        Pieza piezaTorre = tablero.obtenerPieza(posTorre);
        if (!(piezaTorre instanceof Torre) || piezaTorre.esBlanca() != this.esBlanca || piezaTorre.haMovido()) {
            // System.err.println("Inconsistencia enroque: flag OK, pero torre movida/ausente.");
            return false; // No se puede enrocar si la torre no está o ya se movió
        }

        return true; // Todas las condiciones se cumplen
    }

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

    @Override
    public String toString() {
        // Notación estándar: K para blanco, k para negro (King)
        return esBlanca() ? "K" : "k";
    }
}


Resultado final:





Proyecto Ajedrez I.4. Lógica Central del Juego.

Antes de empezar como de costubre mostraremos el avance e indicaremos el punto en el que nos encontramos dentro del proyecto:

 1 Primer esbozo del tablero de ajedrez (2018)
 2 Explicación normativa y reglas del juego (2024)
 3 Interfaz Gráfica Principal (2025)
   .AjedrezGrafico.java
   .PanelTablero.java
   .SplashScreen.java
   .GuiUtils.java
 4 Ventanas de Diálogo (2025)
   .ConfiguracionDialog.java
   .ElegirLadoDialog.java
   .PromocionDialog.java
   .ConfirmarFinalizarDialog.java
   .ConfirmarCierreAppDialog.java

>5 Lógica Central del Juego
   .MotorJuego.java
   .Tablero.java
   .Movimiento.java
   .EstadoJuego.java
   .Posicion.java


En este post toca centrarnos en el punto 5 que representa la lógica central del juego.


Código Java 1 (MotorJuego.java):

package ajedrez_gui;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

/**
 * Controla la lógica principal del juego de ajedrez: gestiona el tablero,
 * turnos, movimientos, estados y reglas.
 */

public class MotorJuego {

    private Tablero tablero;
    private boolean turnoBlancas;
    private int contador50Movimientos; // Medios turnos para regla 50 mov
    private Map<String, Integer> historialPosiciones; // FEN -> count para triple rep
    private EstadoJuego estadoActual;
    private List<String> historialNotacion; // Notación para UI
    private Movimiento movimientoPendientePromocion; // Si se espera elección de promoción
    private final Random random = new Random(); // Para mov. CPU

    public MotorJuego() {
        iniciarNuevaPartida();
    }

    // Inicializa o resetea el juego
    public final void iniciarNuevaPartida() {
        tablero = new Tablero();
        turnoBlancas = true;
        contador50Movimientos = 0;
        historialPosiciones = new HashMap<>();
        historialNotacion = new ArrayList<>();
        movimientoPendientePromocion = null;
        registrarPosicionActual(); // Registrar posición inicial
        actualizarEstadoJuego(true); // Determinar estado inicial
        System.out.println("MotorJuego: Nueva partida iniciada. Estado: " + estadoActual);
    }

    // --- Getters ---
    public Tablero getTablero() {
        return tablero;
    }

    public boolean esTurnoBlancas() {
        return turnoBlancas;
    }

    public EstadoJuego getEstadoActual() {
        return estadoActual;
    }

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

    // Genera movimientos legales para el jugador actual
    public List<Movimiento> getMovimientosLegales() {
        if (estadoActual.esFinDePartida() || estadoActual == EstadoJuego.PROMOCION_REQUIERIDA) {
            return Collections.emptyList();
        }
        return tablero.generarMovimientosLegales(turnoBlancas);
    }

    // Intenta realizar un movimiento validándolo y ejecutándolo
    public EstadoJuego intentarMover(Posicion origen, Posicion destino) {
        if (estadoActual.esFinDePartida() || estadoActual == EstadoJuego.PROMOCION_REQUIERIDA) {
            System.err.println("MotorJuego: Intento de mover en estado inválido: " + estadoActual);
            return estadoActual;
        }
        if (origen == null || destino == null || !origen.esValida() || !destino.esValida()) {
            System.err.println("MotorJuego: Coordenadas inválidas.");
            return EstadoJuego.MOVIMIENTO_INVALIDO;
        }

        Pieza piezaEnOrigen = tablero.obtenerPieza(origen);
        if (piezaEnOrigen == null || piezaEnOrigen.esBlanca() != turnoBlancas) {
            return EstadoJuego.MOVIMIENTO_INVALIDO; // Pieza incorrecta o vacía
        }

        // Buscar el movimiento en la lista de legales
        List<Movimiento> movimientosLegales = tablero.generarMovimientosLegales(turnoBlancas);
        Movimiento movimientoSeleccionado = encontrarMovimientoEnLista(movimientosLegales, origen, destino);

        if (movimientoSeleccionado == null) {
            return EstadoJuego.MOVIMIENTO_INVALIDO; // No es un movimiento legal
        }

        // --- Movimiento legal encontrado ---
        Pieza piezaMovidaOriginal = movimientoSeleccionado.getPiezaMovida();
        Pieza piezaCapturada = movimientoSeleccionado.getPiezaCapturada();

        // Ejecutar en el tablero (mueve piezas, actualiza flags)
        movimientoSeleccionado.ejecutar();
        // Establecer posible casilla en passant para el siguiente turno
        tablero.setEnPassant(movimientoSeleccionado.obtenerNuevoEnPassant());

        // Verificar si resultó en promoción
        if (movimientoSeleccionado.esPromocion()) {
            this.movimientoPendientePromocion = movimientoSeleccionado;
            estadoActual = EstadoJuego.PROMOCION_REQUIERIDA;
            System.out.println("MotorJuego: Promoción requerida en " + destino);
            return estadoActual;
        } else {
            // Finalizar turno normalmente
            finalizarTurno(movimientoSeleccionado, piezaMovidaOriginal, piezaCapturada, null);
            return estadoActual;
        }
    }

    // Completa una promoción pendiente
    public EstadoJuego completarPromocion(char tipoPieza) {
        if (estadoActual != EstadoJuego.PROMOCION_REQUIERIDA || movimientoPendientePromocion == null) {
            System.err.println("MotorJuego: completarPromocion llamado incorrectamente.");
            return estadoActual == EstadoJuego.PROMOCION_REQUIERIDA ? EstadoJuego.ERROR_INTERNO : estadoActual;
        }

        boolean eraBlanca = movimientoPendientePromocion.getPiezaMovida().esBlanca();
        Posicion destinoPromocion = movimientoPendientePromocion.getDestino();
        char piezaCharMayus = Character.toUpperCase(tipoPieza);

        // Crear nueva pieza con switch expression y pattern matching (implícito)
        Pieza nuevaPieza = switch (piezaCharMayus) {
            case 'R' ->
                new Torre(eraBlanca);
            case 'B' ->
                new Alfil(eraBlanca);
            case 'N' ->
                new Caballo(eraBlanca);
            // case 'Q' -> new Dama(eraBlanca); // Cubierto por default
            default -> {
                piezaCharMayus = 'Q'; // Asegurar Q si es inválido
                yield new Dama(eraBlanca);
            }
        };

        tablero.setPieza(destinoPromocion, nuevaPieza); // Poner la pieza nueva
        System.out.println("MotorJuego: Peón promocionado a " + nuevaPieza.getClass().getSimpleName() + " (" + piezaCharMayus + ")");

        // Finalizar el turno pasando la información de promoción
        finalizarTurno(
                movimientoPendientePromocion,
                movimientoPendientePromocion.getPiezaMovida(), // El Peón original
                movimientoPendientePromocion.getPiezaCapturada(), // Si hubo captura
                piezaCharMayus // El tipo de pieza promocionada
        );

        movimientoPendientePromocion = null; // Limpiar
        return estadoActual;
    }

    // Lógica que se ejecuta al final de cada turno válido
    // Recibe el Movimiento para generar notación O-O / O-O-O

    private void finalizarTurno(Movimiento movimientoRealizado, Pieza piezaMovida, Pieza piezaCapturada, Character promocionChar) {

        // Evaluar estado del *oponente* después del movimiento
        boolean oponenteEnJaque = tablero.estaEnJaque(!turnoBlancas);
        List<Movimiento> movimientosOponente = tablero.generarMovimientosLegales(!turnoBlancas);
        boolean esJaqueMate = oponenteEnJaque && movimientosOponente.isEmpty();//
        String sufijoNotacion = "";
        if (esJaqueMate) {
            sufijoNotacion = "++"; // Mate
        } else if (oponenteEnJaque) {
            sufijoNotacion = "+"; // Jaque
        }
        // Actualizar contador 50 mov y historial posiciones (reset si peón o captura)
        if (piezaMovida instanceof Peon || piezaCapturada != null) {
            contador50Movimientos = 0;
            historialPosiciones.clear(); // Resetear historial FEN
        } else {
            contador50Movimientos++;
        }

        registrarPosicionActual(); // Registrar FEN *antes* de cambiar turno

        // --- Generar Notación ---
        String notacionFinal;
        if (movimientoRealizado.esEnroqueCorto()) {
            notacionFinal = "O-O" + sufijoNotacion;
        } else if (movimientoRealizado.esEnroqueLargo()) {
            notacionFinal = "O-O-O" + sufijoNotacion;
        } else {
            // Notación de coordenadas para otros movimientos
            Posicion origen = movimientoRealizado.getOrigen();
            Posicion destino = movimientoRealizado.getDestino();
            if (origen != null && destino != null) {
                String capturaStr = (piezaCapturada != null) ? "x" : "-";
                // Usar notación simple (pieza opcional, coordenadas)
                // String piezaStr = obtenerNotacionPiezaSimple(piezaMovida); // Opcional: SAN completo
                String piezaStr = (piezaMovida instanceof Peon) ? "" : piezaMovida.toString().toUpperCase(); // K, Q, R, B, N (o nada para peón)

                String promoStr = (promocionChar != null) ? "=" + promocionChar : "";
                // Formato: e2-e4, Nf3, Bx
                // Aquí usamos un formato más explícito: Pe2-e4, Ng1-f3, Bf1xc4, Pe7-e8=Q++

                notacionFinal = piezaMovida.toString().toUpperCase() // P, N, B, R, Q, K
                        + origen.toString()
                        + capturaStr
                        + destino.toString()
                        + promoStr
                        + sufijoNotacion;

                // Quitar P de peón si no captura
                if (piezaMovida instanceof Peon && piezaCapturada == null) {
                    notacionFinal = origen.toString() + capturaStr + destino.toString() + promoStr + sufijoNotacion;
                }
                // Quitar guion si no captura y no es peón
                if (piezaCapturada == null && !(piezaMovida instanceof Peon)) {
                    notacionFinal = notacionFinal.replace("-", "");
                }
                // Usar 'x' para captura de peón
                if (piezaMovida instanceof Peon && piezaCapturada != null) {
                    notacionFinal = origen.toString().charAt(0) + "x" + destino.toString() + promoStr + sufijoNotacion;
                }

            } else {
                System.err.println("MotorJuego WARN: finalizarTurno con origen/destino nulos.");
                notacionFinal = "???-???" + sufijoNotacion;
            }
        }

        historialNotacion.add(notacionFinal); // Añadir al historial
        System.out.println("MotorJuego: Movimiento registrado: " + notacionFinal);

        // Cambiar turno
        turnoBlancas = !turnoBlancas;

        // Actualizar estado para el NUEVO jugador
        actualizarEstadoJuego(false);
    }

    // Actualiza el estado del juego basado en las reglas
    private void actualizarEstadoJuego(boolean esInicioPartida) {
        List<Movimiento> movimientosLegales = tablero.generarMovimientosLegales(turnoBlancas);
        boolean reyActualEnJaque = tablero.estaEnJaque(turnoBlancas);

        // 1. Fin de partida por falta de movimientos
        if (movimientosLegales.isEmpty()) {
            estadoActual = reyActualEnJaque
                    ? (turnoBlancas ? EstadoJuego.JAQUEMATE_GANA_NEGRAS : EstadoJuego.JAQUEMATE_GANA_BLANCAS) // Mate
                    : EstadoJuego.AHOGADO; // Ahogado
        } // 2. Fin de partida por reglas de empate
        else if (contador50Movimientos >= 100) { // 50 mov = 100 medios turnos
            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;
        } // 3. Juego continúa: determinar si es jaque o turno normal
        else {
            estadoActual = reyActualEnJaque
                    ? (turnoBlancas ? EstadoJuego.JAQUE_BLANCAS : EstadoJuego.JAQUE_NEGRAS) // Jaque
                    : (turnoBlancas ? EstadoJuego.TURNO_BLANCAS : EstadoJuego.TURNO_NEGRAS); // Turno normal
        }
        // System.out.println("MotorJuego: Nuevo estado: " + estadoActual); // Debug
    }

    // Busca un movimiento en la lista por origen y destino
    private Movimiento encontrarMovimientoEnLista(List<Movimiento> movimientos, Posicion origen, Posicion destino) {
        if (movimientos == null || origen == null || destino == null) {
            return null;
        }
        for (var mov : movimientos) { // Usar var
            if (mov.getOrigen().equals(origen) && mov.getDestino().equals(destino)) {
                // Devuelve el primero encontrado. Si hay múltiples (promoción), se elige el primero.
                return mov;
            }
        }
        return null;
    }

    // Registra el FEN simplificado de la posición actual
    private void registrarPosicionActual() {
        String hash = tablero.getPosicionHash(turnoBlancas);
        historialPosiciones.put(hash, historialPosiciones.getOrDefault(hash, 0) + 1);
        // System.out.println("Posición registrada: " + hash + " Count: " + historialPosiciones.get(hash)); // Debug
    }

    // Realiza un movimiento aleatorio para la CPU
    public EstadoJuego realizarMovimientoAleatorioCPU() {
        if (estadoActual.esFinDePartida() || estadoActual == EstadoJuego.PROMOCION_REQUIERIDA) {
            return estadoActual;
        }
        List<Movimiento> movimientosLegales = getMovimientosLegales();
        if (movimientosLegales.isEmpty()) {
            actualizarEstadoJuego(false); // Forzar actualización si no hay movs
            System.err.println("MotorJuego WARN: CPU sin movimientos legales. Estado: " + estadoActual);
            return estadoActual;
        }

        Movimiento movCPU = movimientosLegales.get(random.nextInt(movimientosLegales.size()));
        System.out.println("MotorJuego: CPU elige mover: " + movCPU);

        EstadoJuego resultado = intentarMover(movCPU.getOrigen(), movCPU.getDestino());

        // Completar promoción si es necesario (CPU siempre elige Dama)
        if (resultado == EstadoJuego.PROMOCION_REQUIERIDA) {
            System.out.println("MotorJuego: CPU promociona a Dama (Q).");
            resultado = completarPromocion('Q');
        }
        return resultado;
    }
}


Código Java 2 (Tablero.java):

package ajedrez_gui;

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

/**
 * Representa el tablero de ajedrez 8x8. Contiene el estado de las piezas,
 * derechos de enroque y casilla en passant. Proporciona métodos para manipular
 * y consultar el estado del tablero.
 */

public final class Tablero {

    private final Pieza[][] casillas; // Array 2D para las piezas
    public static final int TAMANO_TABLERO = 8;
    // Constantes para filas clave (base 0)
    public static final int FILA_INICIAL_BLANCAS = 0; // Rango '1'
    public static final int FILA_PEONES_BLANCOS = 1; // Rango '2'
    public static final int FILA_PEONES_NEGROS = 6; // Rango '7'
    public static final int FILA_INICIAL_NEGRAS = 7; // Rango '8'

    // Estado de los derechos de enroque
    private boolean puedeEnrocarBlancoCorto;
    private boolean puedeEnrocarBlancoLargo;
    private boolean puedeEnrocarNegroCorto;
    private boolean puedeEnrocarNegroLargo;

    // Casilla objetivo para captura En Passant (si existe)
    private Posicion enPassantTarget;

    // Constructor principal: inicializa el tablero a la posición estándar
    public Tablero() {
        casillas = new Pieza[TAMANO_TABLERO][TAMANO_TABLERO];
        iniciarPosicionEstandar();
    }

    // Constructor para copia (usado internamente)
    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++) {
                // Copiar cada pieza (Pieza.copiar() devuelve una nueva instancia)
                this.casillas[i][j] = (casillasExistentes[i][j] != null) ? casillasExistentes[i][j].copiar() : null;
            }
        }
        // Copiar estado de enroque y en passant
        this.puedeEnrocarBlancoCorto = bCorto;
        this.puedeEnrocarBlancoLargo = bLargo;
        this.puedeEnrocarNegroCorto = nCorto;
        this.puedeEnrocarNegroLargo = nLargo;
        this.enPassantTarget = epTarget; // Posicion es record (inmutable), se puede copiar referencia
    }

    // --- Getters y Setters para estado ---
    public Posicion getEnPassant() {
        return enPassantTarget;
    }

    public void setEnPassant(Posicion nuevoEnPassant) {
        this.enPassantTarget = nuevoEnPassant;
    }

    // Coloca las piezas en la posición inicial estándar
    public void iniciarPosicionEstandar() {
        // Limpiar tablero
        for (int i = 0; i < TAMANO_TABLERO; i++) {
            for (int j = 0; j < TAMANO_TABLERO; j++) {
                casillas[i][j] = null;
            }
        }
        // Colocar piezas blancas
        casillas[FILA_INICIAL_BLANCAS][0] = new Torre(true);  // a1
        casillas[FILA_INICIAL_BLANCAS][1] = new Caballo(true);// b1
        casillas[FILA_INICIAL_BLANCAS][2] = new Alfil(true);  // c1
        casillas[FILA_INICIAL_BLANCAS][3] = new Dama(true);   // d1
        casillas[FILA_INICIAL_BLANCAS][4] = new Rey(true);    // e1
        casillas[FILA_INICIAL_BLANCAS][5] = new Alfil(true);  // f1
        casillas[FILA_INICIAL_BLANCAS][6] = new Caballo(true);// g1
        casillas[FILA_INICIAL_BLANCAS][7] = new Torre(true);  // h1
        for (int j = 0; j < TAMANO_TABLERO; j++) {
            casillas[FILA_PEONES_BLANCOS][j] = new Peon(true);// Fila 2
        }
        // Colocar piezas negras
        casillas[FILA_INICIAL_NEGRAS][0] = new Torre(false);  // a8
        casillas[FILA_INICIAL_NEGRAS][1] = new Caballo(false); // b8
        casillas[FILA_INICIAL_NEGRAS][2] = new Alfil(false);  // c8
        casillas[FILA_INICIAL_NEGRAS][3] = new Dama(false);   // d8
        casillas[FILA_INICIAL_NEGRAS][4] = new Rey(false);    // e8
        casillas[FILA_INICIAL_NEGRAS][5] = new Alfil(false);  // f8
        casillas[FILA_INICIAL_NEGRAS][6] = new Caballo(false); // g8
        casillas[FILA_INICIAL_NEGRAS][7] = new Torre(false);  // h8
        for (int j = 0; j < TAMANO_TABLERO; j++) {
            casillas[FILA_PEONES_NEGROS][j] = new Peon(false); // Fila 7
        }

        // Inicializar derechos de enroque y en passant
        inicializarEnroque();
        enPassantTarget = null;
    }

    // Obtiene la pieza en una posición dada
    public Pieza obtenerPieza(Posicion pos) {
        return (pos != null && pos.esValida()) ? casillas[pos.x()][pos.y()] : null;
    }

    // Coloca una pieza en una posición dada (sobrescribe si hay algo)
    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);
        }
    }

    // Encuentra la posición del rey de un color dado
    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];
                // Usar pattern matching for instanceof
                if (p instanceof Rey rey && rey.esBlanca() == esBlanco) {
                    return new Posicion(i, j);
                }
            }
        }
        // Esto no debería ocurrir en una partida normal
        System.err.println("Error Crítico: No se encontró el rey " + (esBlanco ? "blanco" : "negro") + "!");
        return null; // O lanzar una excepción
    }

    /**
     * Verifica si una casilla está siendo atacada por alguna pieza del color
     * especificado.
     *
     * @param pos La posición a verificar.
     * @param atacadoPorBlancas true si buscamos atacantes blancos, false si
     * negros.
     * @return true si la casilla está atacada, false en caso contrario.
     */

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

                // Si hay una pieza del color buscado
                if (piezaAtacante != null && piezaAtacante.esBlanca() == atacadoPorBlancas) {
                    // Verificar si su movimiento básico puede llegar a 'pos'
                    // Esto incluye camino libre para piezas deslizantes

                    if (piezaAtacante.esMovimientoBasicoValido(origenAtacante, pos, this)) {
                        // Caso especial Peón: esMovimientoBasicoValido puede ser true para avance,
                        // pero solo ataca en diagonal.

                        if (piezaAtacante instanceof Peon) {
                            // Ataca solo si el movimiento fue diagonal (cambio en y != 0)
                            if (pos.y() != origenAtacante.y()) {
                                return true; // Peón ataca diagonalmente
                            }
                            // Si dy == 0, era un avance, no un ataque. Continuar buscando.
                        } else {
                            // Para otras piezas, si esMovimientoBasicoValido es true, ataca.
                            return true;
                        }
                    }
                }
            }
        }
        return false; // Ninguna pieza de ese color ataca la posición
    }

    // Verifica si el rey de un color dado está en jaque
    public boolean estaEnJaque(boolean reyBlanco) {
        Posicion posRey = encontrarRey(reyBlanco);
        if (posRey == null) {
            // Si no se encuentra el rey, asumir jaque podría ser lo más seguro,
            // aunque indica un estado inválido del juego.

            System.err.println("Advertencia: Rey no encontrado al verificar jaque para " + (reyBlanco ? "blancas." : "negras."));
            return true;
        }
        // El rey está en jaque si su posición está atacada por el color opuesto
        return estaAtacada(posRey, !reyBlanco);
    }

    /**
     * Genera TODOS los movimientos pseudo-legales y luego los filtra para
     * asegurar que no dejan al propio rey en jaque.
     *
     * @param paraBlancas true si se generan movimientos para las blancas, false
     * para negras.
     * @return Una lista de objetos Movimiento legales.
     */

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

                // Si hay una pieza del color correcto
                if (pieza != null && pieza.esBlanca() == paraBlancas) {
                    // Iterar sobre todas las posibles casillas destino
                    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; // No mover a la misma casilla
                            }
                            // Crear un Movimiento potencial basado en el tablero ACTUAL
                            // (determina si es captura, enroque, etc.)

                            Movimiento movPotencial = new Movimiento(this, origen, destino);

                            // 1. Validar si el movimiento es básico válido según la pieza
                            //    (incluye validación de enroque en Rey.esMovimientoBasicoValido)

                            if (pieza.esMovimientoBasicoValido(origen, destino, this)) {

                                // 2. Simular el movimiento en una COPIA del tablero
                                Tablero copiaTablero = this.copiar();
                                // Obtener la pieza de la COPIA y ejecutar el movimiento simulado
                                Pieza piezaEnCopiaOrigen = copiaTablero.obtenerPieza(origen);
                                if (piezaEnCopiaOrigen != null) {
                                    // Crear Movimiento sobre la copia para simular
                                    Movimiento movSimulado = new Movimiento(copiaTablero, origen, destino);
                                    movSimulado.ejecutar(); // Ejecuta en la copia
                                    // Importante: Actualizar EP en la copia si el mov lo crea

                                    copiaTablero.setEnPassant(movSimulado.obtenerNuevoEnPassant());

                                    // 3. Verificar si el rey del jugador quedó en jaque en la copia
                                    if (!copiaTablero.estaEnJaque(paraBlancas)) {
                                        // Si no quedó en jaque, el movimiento es legal
                                        // Añadir el movimiento POTENCIAL (basado en tablero original)
                                        movimientosLegales.add(movPotencial);
                                    }
                                    // else { Movimiento ilegal porque deja al rey en jaque }
                                } else {
                                    System.err.println("Error simulación: Pieza desapareció en copia?");
                                }
                            }
                        }
                    }
                }
            }
        }
        return movimientosLegales;
    }

    // --- Gestión de Enroque ---
    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;
        }
    }

    // Invalida derechos de enroque si se mueve el rey o una torre desde su casilla inicial
    public void actualizarEnroquePorMovimiento(Posicion origen, Pieza piezaMovida) {
        if (piezaMovida == null || !origen.esValida()) {
            return;
        }

        // Usar pattern matching for instanceof
        if (piezaMovida instanceof Rey) {
            if (piezaMovida.esBlanca()) {
                invalidarEnroqueBlanco();
            } else {
                invalidarEnroqueNegro();
            }
        } else if (piezaMovida instanceof Torre torre) {
            // Posiciones iniciales de las torres
            Posicion TR_BLANCA_LARGA = new Posicion(FILA_INICIAL_BLANCAS, 0); // a1
            Posicion TR_BLANCA_CORTA = new Posicion(FILA_INICIAL_BLANCAS, 7); // h1
            Posicion TR_NEGRA_LARGA = new Posicion(FILA_INICIAL_NEGRAS, 0); // a8
            Posicion TR_NEGRA_CORTA = new Posicion(FILA_INICIAL_NEGRAS, 7); // h8

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

    // Invalida derechos de enroque si una torre es capturada en su casilla inicial
    public void actualizarEnroquePorCapturaTorre(Posicion posTorreCapturada) {
        if (posTorreCapturada == null || !posTorreCapturada.esValida()) {
            return;
        }

        // Posiciones iniciales de las torres (repetido para claridad)
        Posicion TR_BLANCA_LARGA = new Posicion(FILA_INICIAL_BLANCAS, 0); // a1
        Posicion TR_BLANCA_CORTA = new Posicion(FILA_INICIAL_BLANCAS, 7); // h1
        Posicion TR_NEGRA_LARGA = new Posicion(FILA_INICIAL_NEGRAS, 0); // a8
        Posicion TR_NEGRA_CORTA = new Posicion(FILA_INICIAL_NEGRAS, 7); // h8

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

    // --- Utilidades ---
    // Crea una copia profunda del tablero y su estado

    public Tablero copiar() {
        return new Tablero(this.casillas,
                this.puedeEnrocarBlancoCorto, this.puedeEnrocarBlancoLargo,
                this.puedeEnrocarNegroCorto, this.puedeEnrocarNegroLargo,
                this.enPassantTarget);
    }

    /**
     * Genera una cadena FEN simplificada (solo posición, turno, enroque, EP).
     * Útil para detectar repeticiones de posición.
     *
     * @param turnoActualBlancas Indica a quién le toca mover.
     * @return String representando la posición en formato FEN simplificado.
     */

    public String getPosicionHash(boolean turnoActualBlancas) {
        var sb = new StringBuilder();
        // 1. Posición de piezas (de fila 8 a 1)
        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;
                    // Usar el toString de la pieza (ya da mayús/minús)
                    // Necesitamos KQRBNP estándar FEN

                    char piezaChar = switch (p) {
                        case Rey r ->
                            'K';
                        case Dama q ->
                            'Q';
                        case Torre t ->
                            'R';
                        case Alfil b ->
                            'B';
                        case Caballo k ->
                            'N'; // N for Knight
                        case Peon pawn ->
                            'P';
                        // default -> '?'; // No debería pasar con sealed
                    };
                    sb.append(p.esBlanca() ? piezaChar : Character.toLowerCase(piezaChar));
                }
            }
            if (casillasVacias > 0) {
                sb.append(casillasVacias);
            }
            if (i > 0) {
                sb.append('/'); // Separador de filas
            }
        }

        // 2. Turno ('w' o 'b')
        sb.append(turnoActualBlancas ? " w" : " b");

        // 3. Derechos de enroque ('KQkq' o '-')
        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);

        // 4. Casilla En Passant ('e3', 'f6' o '-')
        sb.append(" ");
        sb.append(enPassantTarget != null ? enPassantTarget.toString() : "-");

        // 5 & 6. Contadores de 50 mov y número de jugada (omitidos para hash simple)
        // sb.append(" 0 1"); // Ejemplo

        return sb.toString();
    }

    /**
     * Verifica si hay material insuficiente en el tablero para que cualquier
     * bando pueda forzar un jaque mate. Simplificado: K vs K, K vs K+B, K vs
     * K+N, K+B vs K+B (mismo color)
     *
     * @return true si el material es insuficiente.
     */

    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; // Ignorar reyes y casillas vacías
                }
                // Usar pattern matching for instanceof
                if (p instanceof Peon || p instanceof Dama || p instanceof Torre) {
                    hayPiezasMayoresOPeones = true;
                    break; // Suficiente para saber que no es material insuficiente
                }

                boolean esCasillaClara = (i + j) % 2 != 0; // A1(0,0) es oscura, B1(0,1) es clara

                if (p.esBlanca()) {
                    if (p instanceof Caballo) {
                        caballosBlancos++;
                    } else if (p instanceof Alfil) {
                        if (esCasillaClara) {
                            alfilesBlancosCasillaClara++;
                        } else {
                            alfilesBlancosCasillaOscura++;
                        }
                    }
                } else { // Pieza negra
                    if (p instanceof Caballo) {
                        caballosNegros++;
                    } else if (p instanceof Alfil) {
                        if (esCasillaClara) {
                            alfilesNegrosCasillaClara++;
                        } else {
                            alfilesNegrosCasillaOscura++;
                        }
                    }
                }
            }
            if (hayPiezasMayoresOPeones) {
                break;
            }
        }

        // Si hay peones, damas o torres, no es insuficiente
        if (hayPiezasMayoresOPeones) {
            return false;
        }

        // Contar piezas menores totales
        int alfilesBlancos = alfilesBlancosCasillaClara + alfilesBlancosCasillaOscura;
        int alfilesNegros = alfilesNegrosCasillaClara + alfilesNegrosCasillaOscura;
        int totalPiezasMenoresBlancas = caballosBlancos + alfilesBlancos;
        int totalPiezasMenoresNegras = caballosNegros + alfilesNegros;

        // K vs K
        if (totalPiezasMenoresBlancas == 0 && totalPiezasMenoresNegras == 0) {
            return true;
        }

        // K vs K + (N o B) -> Insuficiente
        if ((totalPiezasMenoresBlancas == 1 && totalPiezasMenoresNegras == 0)
                || (totalPiezasMenoresBlancas == 0 && totalPiezasMenoresNegras == 1)) {
            return true;
        }

        // K + B vs K + B (mismo color de casilla) -> Insuficiente
        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;
            }
        }

        // Otros casos (K+N vs K, K+N vs K+N, K+2N vs K, etc.) se consideran suficientes
        // (aunque algunas sean tablas teóricas, las reglas FIDE no las marcan como empate inmediato).

        return false;
    }

    // Método simple para mostrar tablero en consola (para debug)
    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--) { // De fila 8 a 1
            System.out.print((i + 1) + " | ");
            for (int j = 0; j < TAMANO_TABLERO; j++) { // De columna a a h
                Pieza p = casillas[i][j];
                System.out.print((p == null ? "." : p.toString()) + " "); // Usa Pieza.toString()
            }
            System.out.println("| " + (i + 1));
        }
        System.out.println("  +-----------------+");
        System.out.println("   a b c d e f g h\n");
    }
}


Código Java 3 (Movimiento.java):

package ajedrez_gui;

/**
 * Representa un movimiento potencial o ejecutado en el tablero. Contiene
 * información sobre origen, destino, piezas involucradas y tipos especiales
 * (enroque, promo, EP).
 */

public class Movimiento {

    private final Tablero tablero; // Tablero de referencia
    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; // Casilla EP *antes* de este movimiento

    // Record interno para agrupar los detalles calculados

    private record DetallesMovimiento(
            Pieza piezaCapturada,
            boolean esEnroqueCorto,
            boolean esEnroqueLargo,
            boolean esPromocion,
            boolean esCapturaEnPassant
            ) {

    }

    // Constructor principal: Calcula detalles al crear
    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();
    }

    // Constructor privado (para copiar)
    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;
    }

    // Determina los detalles (captura, enroque, promo, EP) basado en el estado actual
    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);
        }

        // --- Lógica por tipo de pieza (usando pattern matching) ---
        if (piezaMovida instanceof Rey) {
            int dy = destino.y() - origen.y();
            int dx = destino.x() - origen.x();
            if (dx == 0 && Math.abs(dy) == 2) { // Movimiento de enroque
                enroqueC = dy == 2;
                enroqueL = dy == -2;
                capturada = null;
            } else { // Movimiento normal de rey
                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); // Puede ser promo con captura
            }

            int dx_peon = destino.x() - origen.x();
            int dy_peon_abs = Math.abs(destino.y() - origen.y());

            // Check En Passant
            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; // Captura el peón al lado
                    }
                }
            }

            // Check Captura Normal (si no fue EP)
            if (!capturaEP && dy_peon_abs == 1 && dx_peon == (peon.esBlanca() ? 1 : -1)) {
                Pieza piezaEnDestino = tableroActual.obtenerPieza(destino);
                // Si hay pieza y no es promoción (ya cubierta), es captura normal
                if (piezaEnDestino != null && !promocion) {
                    capturada = piezaEnDestino;
                }
            }
        } else { // Otras piezas (Torre, Alfil, Dama, Caballo)
            capturada = tableroActual.obtenerPieza(destino);
        }

        if (enroqueC || enroqueL) {
            capturada = null; // Asegurar que enroque no marque captura
        }
        return new DetallesMovimiento(capturada, enroqueC, enroqueL, promocion, capturaEP);
    }

    // Ejecuta el movimiento modificando el tablero
    public void ejecutar() {
        if (piezaMovida == null || !origen.esValida() || !destino.esValida()) {
            System.err.println("Movimiento.ejecutar(): Intento inválido.");
            return;
        }

        // 1. Actualizar derechos de enroque (por mover rey/torre o capturar torre)
        tablero.actualizarEnroquePorMovimiento(origen, piezaMovida);
        Pieza piezaEnDestinoAntes = tablero.obtenerPieza(destino);
        if (piezaEnDestinoAntes instanceof Torre) {
            tablero.actualizarEnroquePorCapturaTorre(destino);
        }

        // 2. Mover la pieza principal
        tablero.setPieza(destino, piezaMovida);
        tablero.setPieza(origen, null);

        // 3. Ejecutar acciones especiales (enroque, EP)
        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); // Marcar torre como movida
            }            // System.out.println("Movimiento.ejecutar(): Enroque ejecutado.");
        } else if (esCapturaEnPassant) {
            Posicion posPeonCapturado = new Posicion(origen.x(), destino.y());
            tablero.setPieza(posPeonCapturado, null); // Eliminar peón capturado EP
            // System.out.println("Movimiento.ejecutar(): Captura EP realizada en " + posPeonCapturado);

        }

        // 4. Marcar la pieza principal como movida
        piezaMovida.setMovida(true);

        // 5. El target En Passant se actualiza externamente (en MotorJuego)
    }

    // Determina si este movimiento crea una nueva casilla para En Passant
    public Posicion obtenerNuevoEnPassant() {
        // Solo si un peón avanzó dos casillas
        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; // No se crea casilla EP
    }

    // --- Getters ---
    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;
    }

    // Crea una copia superficial (mismas referencias a piezas/tablero)
    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);
    }

    // Representación textual para debug (incluye O-O/O-O-O)
    @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 4 (EstadoJuego.java):

package ajedrez_gui;

/**
 * Enumera los posibles estados del juego de ajedrez, incluyendo turnos, jaques,
 * finales y estados especiales.
 */

public enum EstadoJuego {
    // Estados de turno
    TURNO_BLANCAS("TURNO BLANCAS"),
    TURNO_NEGRAS("TURNO NEGRAS"),
    // Estados de jaque (no final)
    JAQUE_BLANCAS("JAQUE!"), // Rey blanco amenazado
    JAQUE_NEGRAS("JAQUE!"), // Rey negro amenazado

    // Estados de fin de partida: Jaque Mate
    JAQUEMATE_GANA_NEGRAS("JAQUEMATE!!"), // Blancas pierden
    JAQUEMATE_GANA_BLANCAS("JAQUEMATE!!"), // Negras pierden

    // Estados de fin de partida: Empate
    AHOGADO("EMPATE AHOGADO"),
    EMPATE_50_MOV("EMPATE 50 MOV"),
    EMPATE_TRIPLE_REP("EMPATE X3 REP"),
    EMPATE_MATERIAL_INSUF("EMPATE MATERIAL"),
    // Estados especiales/intermedios
    PROMOCION_REQUIERIDA("PROMOCION PEON"), // Esperando elección de pieza
    MOVIMIENTO_INVALIDO("MOV NO VALIDO"), // Intento de movimiento ilegal
    ERROR_INTERNO("ERROR INTERNO"), // Error inesperado
    EN_CURSO("En curso");               // Estado genérico inicial/visual

    private final String mensaje; // Mensaje corto para UI

    EstadoJuego(String mensaje) {
        // Validar longitud máxima
        if (mensaje.length() > 16) {
            System.err.println("Advertencia: Mensaje de estado > 16 chars: " + mensaje);
        }
        this.mensaje = mensaje;
    }

    // Devuelve el mensaje asociado
    public String getMensaje() {
        return mensaje;
    }

    // Indica si el estado representa el final de la partida
    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;
        };
    }

    // Indica si es específicamente Jaque Mate
    public boolean esJaqueMate() {
        return this == JAQUEMATE_GANA_BLANCAS || this == JAQUEMATE_GANA_NEGRAS;
    }

    // Indica si es específicamente un Empate
    public boolean esEmpate() {
        return switch (this) {
            case AHOGADO, EMPATE_50_MOV, EMPATE_TRIPLE_REP, EMPATE_MATERIAL_INSUF ->
                true;
            default ->
                false;
        };
    }

    // Indica si es un Jaque (pero no mate)
    public boolean esJaque() {
        return this == JAQUE_BLANCAS || this == JAQUE_NEGRAS;
    }
}


Código Java 5 (Posicion.java):

package ajedrez_gui;

/**
 * Representa una posición (casilla) en el tablero de ajedrez usando coordenadas
 * (x, y) base 0. Implementado como un `record` para inmutabilidad y métodos
 * automáticos. x = fila (0='1', 7='8'), y = columna (0='a', 7='h').
 */

public record Posicion(int x, int y) {

    // Constructor canónico y getters (x(), y()) generados automáticamente.
    /**
     * Verifica si la posición está dentro de los límites del tablero (8x8).
     *
     * @return true si 0 <= x < 8 y 0 <= y < 8.
     */

    public boolean esValida() {
        return x >= 0 && x < Tablero.TAMANO_TABLERO && y >= 0 && y < Tablero.TAMANO_TABLERO;
    }

    /**
     * Convierte la posición a notación algebraica estándar (e.g., "a1", "h8").
     *
     * @return La notación como String, o "??" si es inválida.
     */

    @Override
    public String toString() {
        if (!esValida()) {
            return "??";
        }
        char file = (char) ('a' + y); // Columna 0..7 -> a..h
        char rank = (char) ('1' + x); // Fila 0..7 -> 1..8
        return "" + file + rank;
    }

    /**
     * Crea una Posicion desde notación algebraica (e.g., "e4", "H8").
     * Case-insensitive.
     *
     * @param notacionAlgebraica La notación (String).
     * @return La Posicion correspondiente, o null si la notación es inválida.
     */

    public static Posicion desdeNotacion(String notacionAlgebraica) {
        if (notacionAlgebraica == null || !notacionAlgebraica.matches("^[a-hA-H][1-8]$")) {
            // System.err.println("Error: Formato de notación inválido: " + notacionAlgebraica);
            return null;
        }
        String lowerNotacion = notacionAlgebraica.toLowerCase();
        int y = lowerNotacion.charAt(0) - 'a'; // a=0, h=7
        int x = Character.getNumericValue(lowerNotacion.charAt(1)) - 1; // 1=0, 8=7

        // Validar rangos (redundante con esValida pero bueno tenerlo)
        if (x < 0 || x >= Tablero.TAMANO_TABLERO || y < 0 || y >= Tablero.TAMANO_TABLERO) {
            // System.err.println("Error: Coordenadas fuera de rango: " + notacionAlgebraica);
            return null;
        }
        return new Posicion(x, y);
    }

}


Con la tecnología de Blogger.