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