Comentarios sobre el diseño e implementación de un nuevo juego escrito en Javacript.
El objetivo del juego es eliminar todos los cuadros de colores de un tablero rectángular dispuestos de forma aleatoria. Para eliminar un cuadro hay que hacer click sobre el mismo, lo que provoca que tanto el cuadro pinchado como todos sus vecinos inmediatos (izquierda, derecha, arriba y abajo) que tengan el mismo color desaparezcan también de forma recursiva. Al eliminar cuadros, los que estuvieran por encima caen hacia abajo rellenando los huecos dejados por estos.
La forma en la que se disponen los cuadros de colores sobre el tablero es fija, pero su distribución es aleatoria, y no está garantizado que una determinada distribución tenga solución.
El juego no reviste mucha dificultad, por lo que el código debería ser fácil de seguir y entender. En este artículo me centraré en algunos aspectos concretos, como el proceso de generación aleatoria del tablero y el algoritmo de eliminación recursiva de los cuadros.
Código fuente
Al ser un programa escrito completamente en JavaScript, todo el código fuente se encuentra embebido dentro de la propia página HTML desde la que se ejecuta el juego, por lo que para acceder al mismo basta con seleccionar la opción «ver código fuente de la página» del navegador.
Clases
El juego se compone de las siguientes clases:
– Game: Es la clase que representa la aplicación. Se limita a instanciar el tablero y cederle el control.
– Board: Es la encargada de recibir y responder a los eventos de ratón, contiene toda la lógica de control y de fin de partida, y los algoritmos de eliminación de los cuadros.
– Grid: Es una clase que contiene la definición del aspecto del tablero en cuanto al número, tamaño y colores de los cuadros, y los métodos encargados de crear, acceder y destruir los mismos.
Generación del Tablero
El número de cuadros a dibujar, las dimensiones de los mismos, y cada uno de los tres colores que pueden asignárseles se encuentran definidos en variables miembros de la clase Grid.
El tablero en sí mismo consiste en una matriz rectángular de 40 filas de alto por 25 columnas de ancho, lo que da un total de 1000 cuadros de colores por partida. Por cada cuadro de color se crea dinámicamente un elemento HTML de tipo div en tiempo de ejecución que se añade a un div estático definido en la sección body de la página HTML.
A cada cuadro generado se le asigna un color de forma aleatoria. Lo que se hace es generar un número aleatorio entre 0 y 2, y utilizar dicho número como un índice sobre un array en el que se encuentran definidos los colores:
this.colors = new Array("#FFFF00", "#FF7F00", "#CF0000");
Cada índice generado se guarda en un array de arrays de tantas filas y columnas como tiene el tablero, de forma que posteriormente es sencillo localizar el color concreto de un cuadro.
En las primeras pruebas me limité a generar un color de forma aleatoria, asignar ese color a un cuadro y pasar al siguiente. Sin embargo este enfoque daba lugar a un tablero con una distribución de colores muy aleatoria, sin grandes superficies coloreadas de un mismo tono. Para conseguir un mejor resultado varíe un poco el procedimiento generando un color y asignando ese mismo color a una pequeña cantidad, también aleatoria, de cuadros consecutivos. El resultado final es una disposición visual bastante agradable que aparenta la existencia de columnas.
this.createCells = function() { var run = 1; var color = 0; for (var column = 0; column < this.columns; ++ column) { this.cells[column] = new Array(); for (var row = 0; row < this.rows; ++ row) { if (-- run == 0){ run = Math.floor( Math.random() * this.maxRunLength) + 1; color = Math.floor( Math.random() * this.colors.length); } this.createCell(column, row, color); } } }
Eliminación de Cuadros
Cuando se hace click con el ratón sobre un cuadro se procede a comprobar si tiene vecinos de su mismo color, y si es así se elimina del tablero la región coloreada que forman.
Debido al gran número de divs presentes en la página, y que estos están distribuidos dentro de una rejilla rectángular uniforme, en vez de asignar un gestor de eventos de ratón de forma individual a cada uno de ellos, lo que he hecho es añadir un único div estático dentro del tablero, con fondo transparente por encima de todos los demás, de forma que los clicks se centralizan en este único elemento HTML.
Para obtener la fila y columna correspondiente al cuadro pinchado, simplemente se divide la posición relativa dentro del tablero en la que se ha hecho el click, por el alto y ancho del mismo.
En este punto me gustaría comentar que tuve que probar con una serie de métodos distintos antes de conseguir la compatibilidad entre los navegadores soportados (Firefox e Internet Explorer). Y finalmente lo conseguí creando un par de funciones auxiliares que llaman al método adecuado en función de navegador. Concretamente, layerX y layerY para Firefox, y offsetX y offsetY para Internet Explorer:
function getMouseX(e) { return(e? e.layerX : window.event.offsetX); } function getMouseY(e) { return(e? e.layerY: window.event.offsetY); }
El algoritmo de eliminación de cuadros se encuentra en el método removeNeighbours de la clase Board, y es muy similar a los algoritmos recursivos de rellenado de superficies (fill) que se utilizan en los programas de dibujado.
La función se compone de tres bucles que borran las regiones por columnas. De forma que en el primer bucle se eliminan los cuadros del mismo color que se encuentran en la misma columna por encima del pinchado:
this.removeNeighbours = function(column, row, color) { var y0 = row; while( (y0 > 0) && (self.grid.getCellColor(column, y0 - 1) == color) ) self.grid.removeCell(column, -- y0);
En la variable y0 se va almacenando la fila por la que se va avanzado quedándose al final con la fila más alta eliminada dentro de la columna.
A continuación se elimina el propio cuadro pinchado en si mismo:
self.grid.removeCell(column, row);
El segundo bucle elimina los cuadros del mismo color que se encuentran en la misma columna por debajo del pinchado:
var y1 = row; while( (y1 < self.grid.rows - 1) && (self.grid.getCellColor(column, y1 + 1) == color) ) self.grid.removeCell(column, ++ y1);
En la variable y1 se va almacenando la fila por la que se va avanzado quedándose al final con la fila más baja eliminada dentro de la columna. De forma que en este punto se tiene en y0 la fila más alta eliminada y en y1 la más baja.
El último bucle recorre las columnas que se encuentran a la izquierda y derecha del cuadro pinchado utilizando las variables y0 e y1 como referencia de lo borrado hasta el momento. Si se encuentra un cuadro del mismo color que el pinchado a la izquierda o derecha se produce una llamada recursiva a la función utilizando el nuevo cuadro como «semilla»:
for (var y = y0; y <= y1; ++ y) { if ( (column > 0) && (self.grid.getCellColor(column - 1, y) == color) ) self.removeNeighbours(column - 1, y, color); if ( (column < self.grid.columns - 1) && (self.grid.getCellColor(column + 1, y) == color) ) self.removeNeighbours(column + 1, y, color); } }
La mayor parte de las comprobaciones que se realizan en las sentencias if sirven para evitar salirse de los laterales del tablero, o lo que es lo mismo, de los límites del array de arrays que lo representa.
Posibles mejoras
Las posibles mejoras que se me ocurren que podrían hacerse al juego son muy parecidas a las que se plantearon en el artículo anterior acerca del Mahjong. Cito a continuación algunas de ellas:
– Añadir un pequeño reloj contador de tiempo. De esta forma el jugador no sólo competiría contra el tablero, sino también contra el tiempo.
– Si se añade un contador de tiempo entonces también sería interesante salvar el mejor tiempo conseguido, dando así otro motivo para continuar jugando.
– Añadir distintos tableros con diversas formas y colores. Permitiendo elegir tablero al iniciar la partida, o avanzar por los distintos tableros a medida que se fueran resolviendo.
– Aumentar la dificultad del juego, o hacer que esta vaya variando a medida que se van resolviendo tableros.
– Llevar diversas cuentas de los cuadros que quedan en el tablero. Como pueden ser el total por eliminar, o los totales parciales por color.
– Avisar al jugador cuando el tablero no puede resolverse.