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.
				<!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>
    &mdash; 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>

			
Descargar adjuntos
COMPARTE ESTE TUTORIAL

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP