<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cuatro en Raya</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
h1 { margin-bottom: .5rem; font-size: 1.6rem; letter-spacing: .05em; }
#estado {
font-size: 1.1rem;
min-height: 1.6rem;
margin-bottom: .75rem;
color: #adf;
}
canvas {
display: block;
cursor: pointer;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0,0,0,.5);
}
#reiniciar {
margin-top: 1rem;
padding: .45rem 1.6rem;
font-size: 1rem;
background: #0066cc;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}
#reiniciar:hover { background: #0052a3; }
#creditos {
margin-top: 1.25rem;
font-size: .8rem;
color: #667;
}
#creditos a { color: #89a; text-decoration: none; }
</style>
</head>
<body>
<h1>Cuatro en Raya</h1>
<div id="estado">Haz clic en una columna para empezar</div>
<canvas id="tablero"></canvas>
<button id="reiniciar">Nueva partida</button>
<div id="creditos">
Autor: <a href="https://carrero.es" target="_blank" rel="noopener">David Carrero</a>
— IA: Negamax con poda alfa-beta
</div>
<script>
/**
* Cuatro en Raya HTML5 + JavaScript puro
* Autor: David Carrero https://carrero.es
*
* IA: Negamax con poda alfa-beta, profundidad 6.
* Sin dependencias externas. Funciona en cualquier navegador moderno.
*/
// Constantes
const COLS = 7;
const FILAS = 6;
const CELDA = 80;
const RADIO = 30;
const BORDE = 10;
const PROFUNDIDAD = 6;
const VACIO = 0;
const JUGADOR = 1;
const COMP = 2;
// Estado global
let board; // board[fila][col], fila 0 = superior
let juegoTerminado;
let esperandoIA;
let colHover = -1;
// Canvas
const canvas = document.getElementById('tablero');
const ctx = canvas.getContext('2d');
const estado = document.getElementById('estado');
canvas.width = COLS * CELDA + 2 * BORDE;
canvas.height = (FILAS + 1) * CELDA + BORDE;
// Inicialización
function reiniciar() {
board = Array.from({ length: FILAS }, () => new Array(COLS).fill(VACIO));
juegoTerminado = false;
esperandoIA = false;
setMensaje('Haz clic en una columna para empezar');
dibujar();
}
// Lógica
function filaLibre(b, col) {
for (let f = FILAS - 1; f >= 0; f--) {
if (b[f][col] === VACIO) return f;
}
return -1;
}
function tableroLleno(b) {
return b[0].every(v => v !== VACIO);
}
function hayGanador(b, jugador) {
// Horizontal
for (let f = 0; f < FILAS; f++)
for (let c = 0; c <= COLS - 4; c++)
if (b[f][c] === jugador && b[f][c+1] === jugador &&
b[f][c+2] === jugador && b[f][c+3] === jugador) return true;
// Vertical
for (let c = 0; c < COLS; c++)
for (let f = 0; f <= FILAS - 4; f++)
if (b[f][c] === jugador && b[f+1][c] === jugador &&
b[f+2][c] === jugador && b[f+3][c] === jugador) return true;
// Diagonal
for (let f = 0; f <= FILAS - 4; f++)
for (let c = 0; c <= COLS - 4; c++)
if (b[f][c] === jugador && b[f+1][c+1] === jugador &&
b[f+2][c+2] === jugador && b[f+3][c+3] === jugador) return true;
// Diagonal /
for (let f = 3; f < FILAS; f++)
for (let c = 0; c <= COLS - 4; c++)
if (b[f][c] === jugador && b[f-1][c+1] === jugador &&
b[f-2][c+2] === jugador && b[f-3][c+3] === jugador) return true;
return false;
}
// Heurística
function puntuarVentana(ventana, jugador) {
const rival = jugador === COMP ? JUGADOR : COMP;
const fichas = ventana.filter(v => v === jugador).length;
const vacias = ventana.filter(v => v === VACIO).length;
const rFichas = ventana.filter(v => v === rival).length;
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;
}
function evaluar(b) {
let score = 0;
// Columna central vale más
for (let f = 0; f < FILAS; f++)
if (b[f][3] === COMP) score += 3;
// Horizontal
for (let f = 0; f < FILAS; f++)
for (let c = 0; c <= COLS - 4; c++)
score += puntuarVentana([b[f][c], b[f][c+1], b[f][c+2], b[f][c+3]], COMP);
// Vertical
for (let c = 0; c < COLS; c++)
for (let f = 0; f <= FILAS - 4; f++)
score += puntuarVentana([b[f][c], b[f+1][c], b[f+2][c], b[f+3][c]], COMP);
// Diagonal
for (let f = 0; f <= FILAS - 4; f++)
for (let c = 0; c <= COLS - 4; c++)
score += puntuarVentana([b[f][c], b[f+1][c+1], b[f+2][c+2], b[f+3][c+3]], COMP);
// Diagonal /
for (let f = 3; f < FILAS; f++)
for (let c = 0; c <= COLS - 4; c++)
score += puntuarVentana([b[f][c], b[f-1][c+1], b[f-2][c+2], b[f-3][c+3]], COMP);
return score;
}
// Negamax con poda alfa-beta
function negamax(b, prof, alfa, beta, 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);
if (maximizar) {
let mejor = -Infinity;
for (let c = 0; c < COLS; c++) {
const 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 {
let mejor = Infinity;
for (let c = 0; c < COLS; c++) {
const 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;
}
}
function mejorColumna() {
// Victoria inmediata
for (let c = 0; c < COLS; c++) {
const 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 al jugador
for (let c = 0; c < COLS; c++) {
const 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
let mejorCol = 3;
let mejorVal = -Infinity;
for (let c = 0; c < COLS; c++) {
const f = filaLibre(board, c);
if (f < 0) continue;
board[f][c] = COMP;
const val = negamax(board, PROFUNDIDAD, -Infinity, Infinity, false);
board[f][c] = VACIO;
if (val > mejorVal) { mejorVal = val; mejorCol = c; }
}
return mejorCol;
}
// Turno de la IA (asíncrono para no congelar el hilo)
function turnoIA() {
esperandoIA = true;
// setTimeout 0 libera el hilo para que se pinte "Pensando
"
setTimeout(() => {
const c = mejorColumna();
const f = filaLibre(board, c);
if (f >= 0) board[f][c] = COMP;
if (hayGanador(board, COMP)) {
juegoTerminado = true;
setMensaje('La máquina gana. Nueva partida para revancha.');
} else if (tableroLleno(board)) {
juegoTerminado = true;
setMensaje('Empate.');
} else {
setMensaje('Tu turno');
}
esperandoIA = false;
dibujar();
}, 10);
}
// Eventos
canvas.addEventListener('click', e => {
if (juegoTerminado || esperandoIA) return;
const col = Math.floor((e.offsetX - BORDE) / CELDA);
if (col < 0 || col >= COLS) return;
const f = filaLibre(board, col);
if (f < 0) return;
board[f][col] = JUGADOR;
if (hayGanador(board, JUGADOR)) {
juegoTerminado = true;
setMensaje('¡Has ganado! Nueva partida para revancha.');
dibujar();
return;
}
if (tableroLleno(board)) {
juegoTerminado = true;
setMensaje('Empate.');
dibujar();
return;
}
setMensaje('Pensando
');
dibujar();
turnoIA();
});
canvas.addEventListener('mousemove', e => {
const col = Math.floor((e.offsetX - BORDE) / CELDA);
const nuevo = (col >= 0 && col < COLS) ? col : -1;
if (nuevo !== colHover) {
colHover = nuevo;
dibujar();
}
});
canvas.addEventListener('mouseleave', () => {
colHover = -1;
dibujar();
});
document.getElementById('reiniciar').addEventListener('click', reiniciar);
// Dibujo
function dibujar() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Flecha de hover
if (!juegoTerminado && !esperandoIA && colHover >= 0) {
const cx = BORDE + colHover * CELDA + CELDA / 2;
ctx.fillStyle = '#cc2200';
ctx.beginPath();
ctx.moveTo(cx, BORDE + 22);
ctx.lineTo(cx - 12, BORDE + 2);
ctx.lineTo(cx + 12, BORDE + 2);
ctx.closePath();
ctx.fill();
}
// Tablero
ctx.fillStyle = '#0a3cb0';
roundRect(ctx, BORDE, CELDA, COLS * CELDA, FILAS * CELDA, 12);
ctx.fill();
// Fichas
for (let f = 0; f < FILAS; f++) {
for (let c = 0; c < COLS; c++) {
const x = BORDE + c * CELDA + CELDA / 2;
const y = CELDA + f * CELDA + CELDA / 2;
const v = board[f][c];
ctx.beginPath();
ctx.arc(x, y, RADIO, 0, Math.PI * 2);
if (v === JUGADOR) {
const grad = ctx.createRadialGradient(x - 6, y - 6, 4, x, y, RADIO);
grad.addColorStop(0, '#ff6655');
grad.addColorStop(1, '#cc1100');
ctx.fillStyle = grad;
} else if (v === COMP) {
const grad = ctx.createRadialGradient(x - 6, y - 6, 4, x, y, RADIO);
grad.addColorStop(0, '#ffee44');
grad.addColorStop(1, '#cc9900');
ctx.fillStyle = grad;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.15)';
}
ctx.fill();
ctx.strokeStyle = 'rgba(0,30,100,0.4)';
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function setMensaje(txt) {
estado.textContent = txt;
}
// Arranque
reiniciar();
</script>
</body>
</html>
Cuatro en Raya en JavaScript con IA Negamax
Cuatro en Raya jugable en el navegador, sin dependencias ni instalación. IA con Negamax y poda alfa-beta a profundidad 6: prioriza la columna central, bloquea amenazas del jugador y busca victoria inmediata antes de calcular. Renderizado en canvas HTML5 con gradientes y hover animado. Abre el HTML directamente en cualquier navegador moderno.
Descargar adjuntos
COMPARTE ESTE TUTORIAL
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP