import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.ExecutionException;
/**
* Cuatro en Raya Java Swing
*
* Autor: David Carrero https://carrero.es
* Original: Multivac (1998), applet JDK 1.0
*
* El applet original usaba java.awt.Applet, Event y métodos
* deprecated como mouseUp() y size(). Esta versión lo convierte
* en una aplicación Swing moderna con SwingWorker para que la IA
* no bloquee el hilo de la interfaz.
*
* Algoritmo: Negamax con poda alfa-beta, profundidad 5.
*
* Compilar: javac CuatroEnRaya.java
* Ejecutar: java CuatroEnRaya
*/
public class CuatroEnRaya extends JFrame {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
CuatroEnRaya ventana = new CuatroEnRaya();
ventana.setVisible(true);
});
}
public CuatroEnRaya() {
super("Cuatro en Raya");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setResizable(false);
add(new Tablero());
pack();
setLocationRelativeTo(null);
}
}
class Tablero extends JPanel {
private static final int COLS = 7;
private static final int FILAS = 6;
private static final int CELDA = 80;
private static final int BORDE = 10;
private static final int PROFUNDIDAD = 5;
static final int VACIO = 0;
static final int JUGADOR = 1;
static final int COMP = 2;
// board[fila][col] fila 0 es la fila superior del tablero
private int[][] board = new int[FILAS][COLS];
private boolean juegoTerminado = false;
private int colHover = -1;
private String mensaje = "Haz clic en una columna para empezar";
public Tablero() {
setPreferredSize(new Dimension(COLS * CELDA + 2 * BORDE, (FILAS + 2) * CELDA));
setBackground(Color.WHITE);
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
int c = xAColumna(e.getX());
if (c != colHover) {
colHover = c;
repaint();
}
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (juegoTerminado) {
reiniciar();
return;
}
int c = xAColumna(e.getX());
if (c >= 0 && c < COLS) {
jugarColumna(c);
}
}
});
}
// Utilidades del tablero
private int xAColumna(int x) {
return (x - BORDE) / CELDA;
}
/** Fila libre más baja de una columna, -1 si está llena. */
private int filaLibre(int[][] b, int col) {
for (int f = FILAS - 1; f >= 0; f--) {
if (b[f][col] == VACIO) return f;
}
return -1;
}
private boolean tableroLleno(int[][] b) {
for (int c = 0; c < COLS; c++) {
if (filaLibre(b, c) >= 0) return false;
}
return true;
}
private boolean hayGanador(int[][] b, int jugador) {
// Horizontal
for (int f = 0; f < FILAS; f++)
for (int c = 0; c <= COLS - 4; c++) {
boolean ok = true;
for (int i = 0; i < 4; i++) ok &= b[f][c + i] == jugador;
if (ok) return true;
}
// Vertical
for (int c = 0; c < COLS; c++)
for (int f = 0; f <= FILAS - 4; f++) {
boolean ok = true;
for (int i = 0; i < 4; i++) ok &= b[f + i][c] == jugador;
if (ok) return true;
}
// Diagonal descendente ()
for (int f = 0; f <= FILAS - 4; f++)
for (int c = 0; c <= COLS - 4; c++) {
boolean ok = true;
for (int i = 0; i < 4; i++) ok &= b[f + i][c + i] == jugador;
if (ok) return true;
}
// Diagonal ascendente (/)
for (int f = 3; f < FILAS; f++)
for (int c = 0; c <= COLS - 4; c++) {
boolean ok = true;
for (int i = 0; i < 4; i++) ok &= b[f - i][c + i] == jugador;
if (ok) return true;
}
return false;
}
// Evaluación heurística
private int puntuarVentana(int[] ventana, int jugador) {
int rival = (jugador == COMP) ? JUGADOR : COMP;
int fichas = 0, vacias = 0, rFichas = 0;
for (int v : ventana) {
if (v == jugador) fichas++;
else if (v == VACIO) vacias++;
else rFichas++;
}
if (fichas == 4) return 100;
if (fichas == 3 && vacias == 1) return 10;
if (fichas == 2 && vacias == 2) return 2;
if (rFichas == 3 && vacias == 1) return -80;
return 0;
}
private int evaluar(int[][] b, int jugador) {
int score = 0;
// Bonificar columna central
for (int f = 0; f < FILAS; f++)
if (b[f][3] == jugador) score += 3;
// Ventanas horizontales
for (int f = 0; f < FILAS; f++)
for (int c = 0; c <= COLS - 4; c++) {
int[] v = {b[f][c], b[f][c+1], b[f][c+2], b[f][c+3]};
score += puntuarVentana(v, jugador);
}
// Ventanas verticales
for (int c = 0; c < COLS; c++)
for (int f = 0; f <= FILAS - 4; f++) {
int[] v = {b[f][c], b[f+1][c], b[f+2][c], b[f+3][c]};
score += puntuarVentana(v, jugador);
}
// Diagonal
for (int f = 0; f <= FILAS - 4; f++)
for (int c = 0; c <= COLS - 4; c++) {
int[] v = {b[f][c], b[f+1][c+1], b[f+2][c+2], b[f+3][c+3]};
score += puntuarVentana(v, jugador);
}
// Diagonal /
for (int f = 3; f < FILAS; f++)
for (int c = 0; c <= COLS - 4; c++) {
int[] v = {b[f][c], b[f-1][c+1], b[f-2][c+2], b[f-3][c+3]};
score += puntuarVentana(v, jugador);
}
return score;
}
// Negamax con poda alfa-beta
private int negamax(int[][] b, int prof, int alfa, int beta, boolean maximizar) {
if (hayGanador(b, COMP)) return 1_000_000;
if (hayGanador(b, JUGADOR)) return -1_000_000;
if (tableroLleno(b) || prof == 0) return evaluar(b, COMP);
if (maximizar) {
int mejor = Integer.MIN_VALUE;
for (int c = 0; c < COLS; c++) {
int f = filaLibre(b, c);
if (f < 0) continue;
b[f][c] = COMP;
mejor = Math.max(mejor, negamax(b, prof - 1, alfa, beta, false));
b[f][c] = VACIO;
alfa = Math.max(alfa, mejor);
if (alfa >= beta) break;
}
return mejor;
} else {
int mejor = Integer.MAX_VALUE;
for (int c = 0; c < COLS; c++) {
int f = filaLibre(b, c);
if (f < 0) continue;
b[f][c] = JUGADOR;
mejor = Math.min(mejor, negamax(b, prof - 1, alfa, beta, true));
b[f][c] = VACIO;
beta = Math.min(beta, mejor);
if (alfa >= beta) break;
}
return mejor;
}
}
private int mejorColumna() {
// Victoria inmediata
for (int c = 0; c < COLS; c++) {
int f = filaLibre(board, c);
if (f < 0) continue;
board[f][c] = COMP;
if (hayGanador(board, COMP)) { board[f][c] = VACIO; return c; }
board[f][c] = VACIO;
}
// Bloquear victoria del jugador
for (int c = 0; c < COLS; c++) {
int f = filaLibre(board, c);
if (f < 0) continue;
board[f][c] = JUGADOR;
if (hayGanador(board, JUGADOR)) { board[f][c] = VACIO; return c; }
board[f][c] = VACIO;
}
// Negamax
int mejorCol = 3; // centro por defecto
int mejorVal = Integer.MIN_VALUE;
for (int c = 0; c < COLS; c++) {
int f = filaLibre(board, c);
if (f < 0) continue;
board[f][c] = COMP;
int val = negamax(board, PROFUNDIDAD, Integer.MIN_VALUE, Integer.MAX_VALUE, false);
board[f][c] = VACIO;
if (val > mejorVal) {
mejorVal = val;
mejorCol = c;
}
}
return mejorCol;
}
// Flujo de juego
private void jugarColumna(int col) {
int f = filaLibre(board, col);
if (f < 0) return;
board[f][col] = JUGADOR;
if (hayGanador(board, JUGADOR)) {
mensaje = "¡Has ganado! Clic para nueva partida.";
juegoTerminado = true;
repaint();
return;
}
if (tableroLleno(board)) {
mensaje = "Empate. Clic para nueva partida.";
juegoTerminado = true;
repaint();
return;
}
mensaje = "La máquina está pensando ";
repaint();
// Calcular movimiento de la IA fuera del EDT para no congelar la UI
new SwingWorker<Integer, Void>() {
@Override protected Integer doInBackground() { return mejorColumna(); }
@Override protected void done() {
try {
int c = get();
int fComp = filaLibre(board, c);
if (fComp >= 0) board[fComp][c] = COMP;
if (hayGanador(board, COMP)) {
mensaje = "La máquina gana. Clic para nueva partida.";
juegoTerminado = true;
} else if (tableroLleno(board)) {
mensaje = "Empate. Clic para nueva partida.";
juegoTerminado = true;
} else {
mensaje = "Tu turno";
}
} catch (InterruptedException | ExecutionException ignored) {}
repaint();
}
}.execute();
}
private void reiniciar() {
board = new int[FILAS][COLS];
juegoTerminado = false;
mensaje = "Haz clic en una columna para empezar";
repaint();
}
// Pintado
@Override
protected void paintComponent(Graphics g0) {
super.paintComponent(g0);
Graphics2D g = (Graphics2D) g0;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int ancho = getWidth();
// Flecha de hover
if (!juegoTerminado && colHover >= 0 && colHover < COLS) {
g.setColor(Color.RED.darker());
int cx = BORDE + colHover * CELDA + CELDA / 2;
g.fillPolygon(
new int[]{cx - 12, cx + 12, cx},
new int[]{BORDE + 2, BORDE + 2, BORDE + 22},
3
);
}
// Fondo del tablero
g.setColor(new Color(10, 60, 170));
g.fillRoundRect(BORDE, CELDA, COLS * CELDA, FILAS * CELDA, 14, 14);
// Fichas
for (int f = 0; f < FILAS; f++) {
for (int c = 0; c < COLS; c++) {
Color color = switch (board[f][c]) {
case JUGADOR -> new Color(220, 30, 30);
case COMP -> new Color(230, 200, 0);
default -> Color.WHITE;
};
int x = BORDE + c * CELDA + 8;
int y = CELDA + f * CELDA + 8;
g.setColor(color);
g.fillOval(x, y, CELDA - 16, CELDA - 16);
g.setColor(new Color(0, 40, 130));
g.drawOval(x, y, CELDA - 16, CELDA - 16);
}
}
// Barra de estado
g.setColor(new Color(235, 235, 235));
g.fillRect(0, (FILAS + 1) * CELDA, ancho, CELDA);
g.setColor(Color.DARK_GRAY);
g.setFont(new Font("SansSerif", Font.PLAIN, 15));
FontMetrics fm = g.getFontMetrics();
int msgY = (FILAS + 1) * CELDA + (CELDA + fm.getAscent() - fm.getDescent()) / 2;
g.drawString(mensaje, (ancho - fm.stringWidth(mensaje)) / 2, msgY);
}
}
Cuatro en Raya en Java Swing con IA Negamax
Cuatro en Raya para Java Swing. El original era un applet JDK 1.0 (los applets quedaron eliminados en Java 11); esta versión funciona como aplicación de escritorio. La IA usa Negamax con poda alfa-beta a profundidad 5 y SwingWorker para no congelar la interfaz mientras calcula. Compilar con: javac CuatroEnRaya.java Ejecutar con: java CuatroEnRaya
Descargar adjuntos
COMPARTE ESTE TUTORIAL
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP