logoCoderhouse.png
Ë
By Nicolas Alliaume • March 28, 2016

Creando un TA-TE-TI web en Javascript con un jugador artificial

Soy Nicolás Alliaume, profe de Coderhouse en Uruguay, y en este post les voy a mostrar paso a paso cómo programar el juego TA-TE-TI en Javascript utilizando jQuery, para jugar contra un jugador artificial.

Antes que nada, una aclaración para no meterme en problemas con los expertos en IA (Inteligencia Artificial): el jugador que programaremos es de tipo randómico, es decir, escogerá una jugada al azar. No utilizaremos técnicas de IA para generar un jugador que juege bien.

Dicho esto, manos a la obra. Primero, el HTML.

HTML

El HTML del TA-TE-TI es increíblemente sencillo. Por un lado tenemos el código estático, que tiene que ver con contenedores y mensajes (el que ves aquí abajo). Por otro lado, generaremos el HTML del tablero en forma dinámica desde Javascript (que veremos más adelante en este post), que nos facilita la tarea de reiniciar el juego al terminar.

<html>
<head>
<link rel="stylesheet" href="tateti.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="tateti.js"></script>
</head>
<body>
<ul></ul>
<span class="result"></span>
<button class="restart" onclick="restart();">Volver a jugar</button>
</body>
</html>

¿Notaste que en el botón tenemos onclick="restart();"? Al presionar este botón, el juego se reiniciará y podremos volver a jugar. Este mensaje se mostrará luego de que el juego termina. Pero no nos adelantemos. Por ahora, acompáñame a ver el CSS.

CSS

Personalmente, CSS me encanta. Por eso hice estas líneas que le dan un poco de color y estructura visual al juego.

* {
box-sizing: border-box;
}
body {
padding-top: 10%;
background-color: #ededed;
color: #333;
text-align: center;
margin: 0 auto;
font-family: sans-serif;
}
ul, li {
list-style: none;
padding: 0;
margin: 0;
font-size: 0px;
}
ul {
width: 202px;
margin: 0 auto;
}
li {
display: inline-block;
}
.cell {
border: 2px solid #777;
margin: 2px;
background: #fff;
height: 60px;
width: 60px;
font-size: 20px;
text-align: center;
display: inline-block;
vertical-align: top;
}
.cell.marked {
background-color: #DEDC2F;
}
.cell.marked.player-1 {
background-color: #F58C8C;
}
.result {
font-weight: bold;
font-size: 30px;
margin: 40px auto;
display: block;
}
.restart {
border: none;
font-size: 20px;
background-color: #222;
color: #fff;
padding: 10px 20px;
}

En vez de jugar con aros y cruces, jugaremos con colores (rojo y amarillo). ¡Mucho mas lindo! Si preferís aros y cruces, te reto a que lo cambies, sólo tocando el CSS ;)

Ahora, vamos a lo que hará la diferencia: el Javascript.

Javascript

Veamos el código completo, y luego te iré explicando paso a paso de qué se trata.

// 1
$(document).ready(restart);

// 2
var IA_PLAYER = 1,
USER_PLAYER = 2;

// 3
// Resetea y comienza el juego
function restart() {
$('.result').html('');
$('.restart').hide();

generateUIBoard();
userMoves(getInitialBoard());
}

// 4
// Retorna un tablero vacío
function getInitialBoard() {
return [
[null,null,null],
[null,null,null],
[null,null,null]];
}

// 5
// Genera el tablero en la página
function generateUIBoard() {
$('ul').empty();
for (var i = 0; i < 9; i++) {
$('ul').append('<li><div class="cell"></div></li>');
}
}

// 6
// Actualiza la página para reflejar el estado actual del
// tablero
function updateUI(board) {
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
var index = ((row * 3) + col) + 1;
if (board[row][col] != null) {
$('li:nth-child('+index+') .cell')
.addClass('marked')
.addClass('player-' + board[row][col]);
}
}
}
}

// 7
// Juega el jugador humano.
function userMoves(board) {
$('.cell:not(.marked)').click(function() {
$('.cell').unbind();

var $li = $(this).parent();
var row = Math.floor($li.index() / 3);
var col = $li.index() % 3;
board = checkCell(row, col, USER_PLAYER, board);
updateUI(board);

var w = winner(board);
if (w !== null) {
finish(w);
}
else {
setTimeout(function() { iaMoves(board); }, 1000);
}
});
}

// 8
// Juega la máquina. Devuelve el nuevo board.
function iaMoves(board) {
var options = getFreePositions(board);
var optionIndex = randomIntTo(options.length-1);
var coords = options[optionIndex];

// marcamos la casilla
board = checkCell(coords[0], coords[1], IA_PLAYER, board);
updateUI(board);

var w = winner(board);
if (w !== null) {
finish(w);
}
else {
userMoves(board);
}
}

// 9
function finish(winner) {
if (winner == IA_PLAYER) {
$('.result').html("Perdiste contra un jugador Random!");
} else if (winner == USER_PLAYER) {
$('.result').html("Ganaste!");
} else {
$('.result').html("Es un empate!");
}
$('.restart').show();
}

// Retorna un int random entre 0 y n inclusive.
function randomIntTo(n) {
return parseInt(Math.random() * (n+1));
}

// Marca la casilla y retorna el nuevo tablero
function checkCell(row, col, player, board) {
board[row][col] = player;
return board;
}

// 10
// Devuelve las coordenadas como [fila,columna] de las posiciones
// libres del tablero
function getFreePositions(board) {
var freePositions = [];
for (var row = 0; row < 3; row++) {
for (var col = 0; col < 3; col++) {
if (board[row][col] === null) {
freePositions.push([row, col]);
}
}
}
return freePositions;
}

// 11
// Retorna el ganador del tablero indicado, null si no hay ganador, 0
// si hay empate.
function winner(board) {
// verificar columnas
for (var c = 0; c <= 2; c++) {
if (tatetiCol(c, board)) return board[0][c];
}
// verificar filas
for (var r = 0; r <= 2; r++) {
if (tatetiRow(r, board)) return board[r][0];
}
// verificar diagonales
if (tatetiDiagonals(board)) return board[1][1];
// verificar empate
if (getFreePositions(board) == 0) return 0; // empate
return null;
}

// 12
// True si las celdas de la columna indicada son iguales y no nulas
function tatetiCol(col, board) {
return board[0][col] != null
&& (board[0][col] == board[1][col] && board[1][col] == board[2][col]);
}

// 13
// True si las celdas de la fila indicada son iguales y no nulas
function tatetiRow(row, board) {
return board[row][0] != null
&& (board[row][0] == board[row][1] && board[row][1] == board[row][2]);
}

// 14
// True si alguna de las diagonales tienen valores iguales y no nulos
function tatetiDiagonals(board) {
return (board[0][0] != null && (board[0][0] == board[1][1] && board[1][1] == board[2][2]))
|| (board[0][2] != null && (board[0][2] == board[1][1] && board[1][1] == board[2][0]));
}

Comenzamos en //1 haciendo un bind de jQuery para que, cuando se cargue la página, se ejecute la función restart. Esta función (que ya la mencionamos antes cuando vimos el HTML) está definida en //3, y genera el tablero de juego en su estado inicial (todas las casillas en blanco).

//2: Definimos 2 constantes (técnicamente son variables, pero las nombramos con todas las letras en mayúscula para indicar que su valor no cambiará; es una convención que utilizo).

//3: Limpiamos el HTML y generamos el tablero inicial. Comenzamos el juego llamando a userMoves. Veremos esta función en breve.

//4: Esta función retorna un tablero es su estado inicial. El tablero se representa como una matriz de 3x3. Cada celda de la matriz puede tomar uno de tres valores posibles: 1, 2 o null. Null significa "casilla libre", 1 significa "marcado por la IA", y 2 significa "marcado por el usuario". Para evitar usar unos y ceros, utilizamos las constantes definidas en //2.

//5: La función que genera el HTML del tablero inicial. Simplemente agrega al DOM una lista con elementos li, uno para casilla del tablero.

//6: Esta función recibe el estado del tablero (la matriz), y actualiza el HTML para reflejar el estado actual. Utilizando 2 for anidados, modifica las celdas (un elemento li de los generados en //5) que han sido marcadas por alguno de los dos jugadores, agregándoles la clase marker y player-X, donde X es 1 o 2 dependiendo de qué jugador la marcó. El algoritmo en español es algo como:

  • Para cada celda:
    • ¿Está marcada por el jugador 1?
      • Agregar clase marked y player-1 a la celda
    • Si no, ¿Está marcada por el jugador 2?
      • Agregar clase marked y player-2 a la celda
    • Si no, no hacer nada

Para obtener la celda desde el DOM utilizamos jQuery y el selector :nth-child. Este selector nos permite obtener un elemento por su índice en el DOM. El primer elemento tiene el índice 1.

DOM de celdas y selector nth-child

//7: Función que permite mover al jugador humano. Recibe el estado actual del tablero, que usaremos para hacer y validar la jugada. "Permitir mover al jugador humano" significa que agregamos el evento click a las celdas libres (que no están marcadas). Cuando el usuario selecciona una, se ejecuta la función definida inline para el evento, que marca la celda, altera el estado del tablero, actualiza la interfaz y verifica si el juego terminó con este movimiento.

Cuando el jugador selecciona una celda, quitamos los eventos de todas las celdas para que no pueda seleccionar más de una a la vez.

Al finalizar el movimiento, si el juego no termina, se le pasa el turno a la IA, esperando un segundo para darle el efecto de "pensar el movimiento".

//8: Esta función ejecuta un movimiento de la IA, seleccionando una casilla libre al azar, y actualizando el estado del tablero y la interfaz. Verifica si el juego termina con este movimiento y, si no, pasa el turno al jugador.

//9: Cuando el juego termina, esta función es llamada para actualizar la interfaz, que muestra un mensaje de "Ganaste", "Perdiste" o "Empataste", dependiendo del resultado, y la opción para volver a jugar.

//10: Esta función retorna un array con las posiciones libres del tablero. Utilizando dos for anidados recorremos la matriz para encontrar celdas en null. Es utilizada por la IA para encontrar una celda para marcar.

//11: Determina el ganador del juego para un estado del tablero dado. Primero, verifica si hay TA-TE-TI en alguna fila, y retorna el ganador si lo hay. Para eso usamos la función utilitaria definida en //12. Luego, hace lo mismo para las filas, utilizando la función //13. Y por último, verifica las diagonales (//14), y chequea por empate (si ninguna de las condiciones previas se cumplieron, y no hay celdas disponibles). En este último caso retorna null, y se entiende que el juego aún no termina.

Eso es todo :) ¿No fue tan dificil, verdad?

Siguientes pasos

Luego de jugar un par de veces notarás que el jugador artificial no es para nada habilidoso. Esto es porque no tiene lógica alguna a la hora de elegir una casilla para marcar.

¿Podrás mejorar las técnicas de juego de él? ¡Te invito a que lo intentes!