La segunda entrada
de este blog pretende explicar cómo se realiza el mini-juego llamado 15-Puzzle.
Este juego consiste de 15 piezas enumeradas del 1 al 15 que son dispuestas
aleatoriamente en un tablero de 16 espacios. Por lo tanto, siempre queda un
espacio libre por medio del cual se puede realizar el movimiento de las fichas
con el fin dejarlas organizadas numéricamente. Algunas variaciones de este
juego incluyen: un mayor o menor número de fichas o tiene como objetivo
organizar las fichas de tal forma que quede armada una imagen.
Inicio del juego
Fin del juego
Este pequeño
juego será desarrollado por medio de lenguajes que se ejecutan en el lado del
cliente: html y javascript. Para facilitar un poco las cosas se hará uso de la
biblioteca de JavaScript llamada jQuery.
Estructura
Es
importante que antes de comenzar a escribir código desenfrenadamente, se piense
un poco en la estructura del programa que se desea realizar. De esta forma se
puede ahorrar mucho tiempo y se pueden evitar algunos errores de lógica básicos
desde el principio. Naturalmente, con frecuencia no se puede pensar en
absolutamente todos los detalles; sin embargo, esto siempre puede brindar una
base sólida para comenzar.
En este
primer programa se intentará pensar una estructura basada en la programación
orientada a objetos. Así se puede pensar en las diferentes clases (que son los
“moldes” de nuestros objetos) que interactuarán en el juego y los métodos que
cada una de ellas debe poseer.Este juego
está formado básicamente por dos clases: la que representa el juego en general y la que representa
cada ficha del juego.
Clase Ficha
Atributos:
· Valor numérico de la ficha: este
atributo ayudará a identificar la ficha (¿es la 1, la 2 o qué número tiene la
ficha?).
· Posición correcta: este atributo
puede tomar el valor de falso o verdadero. Es el que indicará si la ficha se
encuentra en la posición correcta en el tablero.
· Posición actual en X: Este atributo
indica en qué fila se encuentra actualmente la ficha en el tablero de 4 x 4.
Puede tomar un valor de 1 a 4 para indicar cualquiera de las 4 filas.
· Posición actual en Y: Este atributo
indica en qué columna se encuentra actualmente la ficha en el tablero de 4 x 4.
Puede tomar un valor de 1 a 4 para indicar cualquiera de las 4 columnas.
· Posición correcta en X: Este atributo indica
en qué fila del tablero de 4 x 4 se debe encontrar la ficha para que su
posición sea la correcta. Puede tomar un valor de 1 a 4.
· Posición correcta en Y: Este atributo
indica en qué columna del tablero de 4 x 4 se debe encontrar la ficha para que
su posición sea la correcta. Puede tomar un valor de 1 a 4.
Métodos:
La clase Ficha no requiere de ningún método especial.
Constructor: El
constructor de la clase ficha, que es una función que nos permite inicializar
algunos de sus atributos, nos debe dar la posibilidad de inicializar el valor
de la ficha, su posición actual en X, su posición actual en Y, su posición
correcta en X y su posición correcta en Y. Además, debe garantizar que toda
ficha que se cree inicialmente tenga su atributo posición correcta en false.
Clase Juego
Atributos:
· Cuenta
de fichas en la posición incorrecta: Esta variable se usa con el fin de contar
el número de fichas que actualmente están en la posición incorrecta. Así, cada
vez que se mueve una ficha, se verifica dependiendo de su posición actual si se
le debe sumar o restar a la variable aquí descrita.
· Vector
de fichas: Es un vector de dieciséis posiciones donde cada posición consta de
un objeto ficha. En este juego el espacio en blanco se asumirá como una ficha
más. Es por eso que este vector consta de dieciséis posiciones y no de quince.
En la última posición siempre se colocará la “ficha” que simula el espacio.
Métodos:
A continuación se describen
los métodos de esta clase que son los que finalmente le dan toda la
funcionalidad al juego.
Inicializar juego: Este método permite realizar todas las
configuraciones iniciales del juego: distribuir aleatoriamente las posiciones
de cada ficha y verificar si esa distribución representa un juego solucionable.
Este método crea un vector de 15 posiciones con los números del 1 al 15
desorganizados de forma aleatoria por medio de una función que se explicará más
adelante (Random). Después, por medio
de otro método descrito más adelante, se comprueba si esta distribución
aleatoria se puede usar para crear un juego solucionable (este juego tiene la
particularidad de que solo el 50% de todas las distribuciones aleatorias que se
pueden generar forman un juego solucionable). Si la distribución representa un
juego solucionable, este vector se usa después para asignar las posiciones
actuales de cada ficha creada; si no, el vector es desorganizado nuevamente
hasta obtener una distribución que represente un juego solucionable. El
siguiente paso es recorrer el vector de
fichas y colocar una ficha nueva en cada una de sus posiciones. A cada
ficha se le asigna su valor, su posición actual (tanto en X como en Y) con
ayuda del vector de números desorganizados y su posición correcta (tanto en X
como en Y). Para realizar la asignación de posiciones (posiciones actuales y
posiciones correctas), se usan las operaciones de división (usando solo la
parte entera del resultado) y de módulo. Lo que obtenemos como resultado de
este método es un vector de fichas
con sus respectivas posiciones actuales (que representan el desorden del
juego), sus posiciones correctas (que representan el orden del juego) y los
valores de cada ficha. Además, en la posición número 16 del vector se crea una
ficha con valor numérico 16, posición correcta (4,4) y posición actual (4,4).
Ésta representa el espacio en blanco, lo cual quiere decir que sin importar
como hayan quedado ubicadas las demás fichas el espacio en blanco siempre
empezará en la posición de la esquina inferior derecha del tablero.
Es solucionable: Este método nos permite ingresar un
vector desorganizado con números del 1 al 15 y, usando la lógica de nuestro
juego, averiguar si el vector tiene una distribución que representa un juego
solucionable o no. Esta función se explicará línea a línea en los comentarios
del código. Su lógica general está explicada en el siguiente video: http://www.youtube.com/watch?v=TKXiHdgOHaU
Mover ficha: Este método recibe como parámetro un objeto Ficha. Permite verificar si es posible
mover una ficha (porque tiene el espacio blanco como vecino de alguno de sus
cuatro lados) o si no es posible. En caso de que sea posible, la función
realiza el movimiento de la ficha y devuelve como valor true; si no, sencillamente devuelve false. Para verificar si el movimiento de la ficha es posible, se
restan las posiciones en X de la ficha y el espacio en blanco y se almacena el
resultado. Después, se realiza la misma operación con las posiciones en Y. Si
al sumar los dos resultados el valor absoluto de la suma es igual a 1, el
movimiento de la ficha es posible; de lo contrario, no. Esto se debe a que la
ficha solo se puede mover cuando se encuentra en la misma fila del espacio en
blanco (entonces el resultado de la resta da 0), pero en la columna adyacente
(el resultado de la resta da 1 o -1); o viceversa. Si la ficha se puede mover,
se intercambian las posiciones actuales del espacio en blanco y de la ficha y
además, se llama al método verificar
posición para que este aumente o disminuya la cuenta de fichas en la posición incorrecta; si no se puede realizar
el movimiento; la función no hace nada y se retorna false.
Verificar posición: Este método recibe como parámetro un
objeto Ficha. Se llama cada vez que
una se realiza el movimiento de una ficha y tiene la función de modificar la cuenta de fichas en la posición incorrecta
según sea necesario. Si la ficha se mueve de una posición correcta a una
incorrecta, se le suma a la cuenta de
fichas en la posición incorrecta; si se mueve de una posición incorrecta a
una correcta, se le resta a la cuenta de
fichas en la posición incorrecta; pero si se mueve de una posición
incorrecta a otra posición incorrecta la cuenta
de fichas en la posición incorrecta queda intacta. Esta función usa las posiciones actuales XY y las posiciones correctas XY de la ficha para
saber si coinciden. Además usa el atributo de la ficha llamado posición correcta (que tiene valor de
falso o verdadero) para saber en qué estado estaba antes de que la ficha fuera
movida.
Verificar juego: Este es el método de la clase Juego que permite saber si el juego está
con todas sus fichas organizadas o si no. Este método solo tiene que revisar la
cuenta de fichas en la posición
incorrecta y si ésta está en cero, el juego está completamente organizado;
si no, el juego aún tiene fichas que se encuentran ubicadas incorrectamente.
Dibujar tablero: Los métodos
descritos anteriormente a este, son los que manejan el funcionamiento del juego
de una forma virtual; sin embargo, se necesita de una interfaz gráfica para
poder enviar las órdenes de movimientos y poder visualizar los resultados de
cada instrucción dada al juego. Este método es el que se encarga de dibujar el
tablero gráficamente y así poder por medio de la interfaz gráfica manejar todos
los objetos que representan nuestro juego. Para realizar este trabajo usamos
jQuery. Primero dibujamos el tablero del tamaño que se indica por medio del
parámetro que recibe este método. Este consiste en dos etiquetas
<section> anidadas para construir el borde grueso del tablero. Después se
recorre el vector de fichas y se
dibuja cada una de acuerdo a su posición
actual XY. Esto se hace usando posicionamiento absoluto en la hoja de
estilos y adaptando los atributos top y
left (se adaptan de acuerdo a la posición actual XY de cada ficha) de cada <div> que representa una
ficha. Al final, se obtiene el tablero dibujado con sus respectivas fichas
desorganizadas aleatoriamente.
Función Random
Esta función tiene la
capacidad de recibir un vector con ciertos valores y devolver un vector cuyos
valores aparecen desorganizados aleatoriamente. En el juego esta función fue
usada para desordenar el vector creado con los números del 1 al 15. Lo que hace
esta función es recorrer todas las posiciones del vector e intercambiar su
valor por el valor que tiene el vector en otra posición escogida
aleatoriamente. Así, al final se obtiene un vector con los mismos valores pero
con un orden diferente al original.
Programación
En esta sección se muestra
como se implementa en código JavaScript la lógica del juego que se definió
anteriormente. Para reforzar la explicación dada anteriormente el código
mostrado tiene comentarios que ayudan a entender el programa. Un profesor de la
universidad me dejó la extraña manía de querer programar con palabras en inglés
(o al menos intentarlo), espero que esto no sea inconveniente.
Clase Ficha
var Piece = function(value,rigthPosX,rigthPosY,posX,posY)
{
this.value = value; //Valor numérico de la ficha
this.isRigthPosition = false; //Variable que indica si la ficha se encuentra en la posición correcta
this.posX = posX; //Variable que indica la fila actual en la que se encuentra la ficha (tablero 4x4)
this.posY = posY; //Variable que indica la columna actual en la que se encuentra la ficha (tablero 4x4)
this.rigthPosX = rigthPosX; //Variable que indica la fila en la que debería esta la ficha para estar en la posición correcta
this.rigthPosY = rigthPosY; //Variable que indica la columna en la que debería esta la ficha para estar en la posición correcta
}
Clase Juego
var Game = function()
{
this.isWrongCount = 0; //Cuenta el número de fichas que se encuentran actualmente en la posición incorrecta
this.piecesArray = new Array(); //Vector que contiene las 15 fichas y el espacio (simulado como otra ficha)
//Este método permite hacer las configuraciones iniciales del juego. Ejemplo: repartir las fichas aleatoriamente.
this.initializeGame = function()
{
var randomArray = new Array(); //Variable local que ayuda a generar el orden aleatorio de la posición de las fichas
//Este for ayuda a llenar el vector con números del 1 al 15 (ordenadamente)
for(var i=0;i<15;i++)
randomArray[i] = i+1;
// Se usa este do-while para desorganizar el vector randomArray hasta que el nuevo "orden" represente una solución
// para el juego. El método (función) que nos ayuda a esto es la llamada isSolvable.
do
{
randomArray = Random(randomArray); //Se almacena en la misma variable el vector con un nuevo orden aleatorio
}
while(!this.isSolvable(randomArray)); //Se verifica si representa un juego solucionable
// Este ciclo se usa para recorrer el vector de fichas y crear una nueva ficha en las
// primeras 15 posiciones del vector
for(var i=0;i<15;i++)
{
// Se inicializa una ficha en cada posición del vector. Su valor con (i+1) para representar los números
// del 1 al 15, su posición correcta usando los valores de i (que está iterando en orden) y las operaciones
// de división y módulo; y su posición inicial en el juego usando los valores del vector randomArray y nuevamente
// las operaciones de división y módulo.
this.piecesArray[i] = new Piece(i+1,Math.floor(i/4+1),(i%4)+1,Math.floor((randomArray[i]-1)/4)+1,((randomArray[i]-1)%4)+1);
//Con este condicional se comprueba si la ficha en su posición incial concidencialmente quedó en la posición correcta
if((this.piecesArray[i].posX != this.piecesArray[i].rigthPosX) || (this.piecesArray[i].posY != this.piecesArray[i].rigthPosY))
this.isWrongCount++; //Se le suma 1 a la cuenta de fichas en la posición incorrecta
else
this.piecesArray[i].isRigthPosition = true; //Simplemente se coloca la variable isRigthPosition de esa ficha en true
}
//Finalmente se inicializa el espacio en blanco con valor de ficha 16 y posición inicial y correcta (4,4)
this.piecesArray[15] = new Piece(16,4,4,4,4);
}
//Esta función nos ayuda a verificar si la distribución del vector aleatorio representa un juego con solución
this.isSolvable = function(random_array)
{
var control_sum = 0; //Se inicializa la variable suma en 0
// El vector de control nos ayuda a almacenar la cantidad de números menores que se encuentran a la derecha del
// número que se esta examinando. Así, control_array[i] almacena la cantidad de números menores a la derecha de
// test_array[i].
var control_array = new Array();
// En este vector (test_array) quedan finalmente, organizados de izquierda a derecha y de arriba a abajo,
// los números del tablero del juego.
var test_array = new Array();
// En esta variable se almacena el tamaño del
// vector (que es el mismo para cualquiera de los otros vectores)
var length = random_array.length;
//Este primer ciclo sirve para ubicar los números en test_array de acuerdo a la distribución aleatoria
for(var i=0;i<length;i++)
test_array[random_array[i]-1] = i+1;
//Este ciclo inicializa todas las posiciones del vector de control en 0
for(var i=0;i<length;i++)
control_array[i] = 0;
//En este ciclo se cuenta la cantidad de números menores a la derecha de test_array[i] y se suman en control_array[i]
for(var i=0;i<length;i++)
{
for(var j=i+1;j<length;j++)
{
if(test_array[j]<test_array[i])
control_array[i]++;
}
control_sum += control_array[i]; //Se realiza una suma de todas las posiciones de control_array
}
//Si la suma es par, el juego tiene solución; si no, no tiene solución.
if(control_sum % 2 == 0)
return true;
else
return false;
}
//Esta función es la que dibuja el tablero de juego en html
this.drawBoard = function(pixel_size)
{
//Se le agrega a la etiqueta body un tablero de tamaño pixel_size que esta formado por dos divs anidados
//la proporción entre los divs esta especificada en el archivo .css usando los ids agregados aquí.
$("body").append('<section id="game_board" style="height:'+pixel_size+'px;width:'+pixel_size+'px;"><section id="piece_container"></section></section>');
// Por medio de este ciclo se adicionan las fichas ubicadas segun su posición inicial en filas y en columnas.
//Se usan porcentajes para ubicarlas a 0, 25, 50 y 75% del borde del tablero. Cada ficha mide el 25% del tablero (archivo .css)
for(var i=0;i<15;i++)
$("#piece_container").append('<div class="game_piece" style="top:'+((this.piecesArray[i].posX-1)*25)+'%;left:'+((this.piecesArray[i].posY-1)*25)+'%"><div class="number_container">'+this.piecesArray[i].value+'</div></div>');
}
// Este método verifica si una ficha se encuentra en su posición correcta y modifica la cuenta de fichas
// en la posición incorrecta
this.checkPosition = function(piece)
{
//Mira si la posición actual de la ficha coincide con la posición correcta de la misma
if((piece.posX == piece.rigthPosX) && (piece.posY == piece.rigthPosY))
{
piece.isRigthPosition = true; // Actualiza el valor de posición correcta de esa ficha
this.isWrongCount--; // Le resta uno a la cuenta de fichas en la posición incorrecta
}
else
{
if(piece.isRigthPosition) //Mira cuál era el estado de posición correcta anterior de la ficha.
{
//Si entra a este if quiere decir que la ficha se movió de una posición incorrecta a una correcta
piece.isRigthPosition = false;// Actualiza el valor de posición correcta de esa ficha
this.isWrongCount++;// Le suma a la cuenta de fichas en la posición incorrecta
}
}
}
//Este método representa el movimiento de una ficha al espacio en blanco
this.movePiece = function(piece_number)
{
//Se hace una resta entre la posición en fila de la ficha y la del espacio en blanco
var test_posX = this.piecesArray[piece_number-1].posX - this.piecesArray[15].posX;
//Se hace una resta entre la posición en columna de la ficha y la del espacio en blanco
var test_posY = this.piecesArray[piece_number-1].posY - this.piecesArray[15].posY;
var wasMoved = false; //Se inicializa la variable que se va a retornar
if(Math.abs(test_posX) + Math.abs(test_posY) == 1) //Se verifica que la suma de los valores absolutos sea igual a 1
{
var auxX = this.piecesArray[piece_number-1].posX; //Se almacena momentaneamente la fila de la ficha
var auxY = this.piecesArray[piece_number-1].posY; //Se almacena momentaneamente la columna de la ficha
//Se asigna el valor de fila de el espacio en blanco que siempre está en la última posición del vector de fichas
this.piecesArray[piece_number-1].posX = this.piecesArray[15].posX;
//Se asigna el valor de columna de el espacio en blanco que siempre está en la última posición del vector de fichas
this.piecesArray[piece_number-1].posY = this.piecesArray[15].posY;
this.piecesArray[15].posX = auxX; //Al espacio en blanco se le asigna el valor en fila que tenía antes la ficha
this.piecesArray[15].posY = auxY; //Al espacio en blanco se le asigna el valor en columna que tenía antes la ficha
this.checkPosition(this.piecesArray[piece_number-1]); //Se verifica la ´nueva posición de la ficha
wasMoved = true; //Se modifica a true el valor de la variable a retornar
}
return wasMoved; //Se retorna la variable que indica si se realizó o no movimiento
}
// Método que verifica si el juego ya se solucionó
this.checkGame = function()
{
if(this.isWrongCount == 0) //Se verifica el número de fichas en la posición incorrecta
return true; //Hay 0 fichas en la posición incorrecta
else
return false; //Hay alguna ficha en la posición incorrecta
}
}
Función Random
//Función hecha para desorganizar un vector de cualquier tamaño
function Random(array)
{
var length = array.length; //Se obtiene el tamaño del vector
var aux; //Variable auxiliar que permite el intercambio entre valores internos del vector
var randomPos; //variable que guarda el número aleatorio generado
// Se recorre todo el vector
for(var i=0;i<length;i++)
{
aux = array[i]; //Se almacena el valor del vector en la posición actual i
randomPos = Math.floor(Math.random() * length); //Se genera un número aleatorio entre 0 y el tamaño - 1
array[i] = array[randomPos]; //Se coloca el valor del vector en la posición random como valor del vector en la posición actual
array[randomPos] = aux; //Se coloca el valor del vector en la posición actual como valor del vector en la posición random
}
return array; //Se devuelve un vector con orden diferente al inicial
}
Programa Principal
Se le llamará programa
principal a las líneas de código que crean el juego después de que carga la
página y que finalmente declararán la forma en la cual interactúa el usuario con
el juego. Ya que se usó la librería de jQuery, estás líneas van entre el
manejador del evento que indica que el documento ya está listo.
Abajo se muestra el html que
conforma nuestro juego. El único elemento que se agregó al body es una pequeña
caja de texto que se usa para comunicarse con el jugador.
<!DOCTYPE html>
<html>
<head>
<title>Puzzle</title>
<link rel="stylesheet" type="text/css" href="estilos.css">
<!-- Esta línea de código se usa para incluir la biblioteca de jQuery -->
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<!-- Esta línea se usa para incluir nuestro programa -->
<script type="text/javascript" src="game.js"></script>
</head>
<body>
<h3 id="talking_box">!Cuando quieras!</h3>
</body>
</html>
El programa principal
arranca creando una nueva variable de tipo Juego. Después se usa el método inicializar juego de esta variable.
Finalmente hay una llamada al método dibujar
tablero para que este aparezca en el body
de nuestro html.
Para la parte de interacción
únicamente necesitamos declarar lo que ocurrirá cada vez que se haga clic sobre
una ficha ubicada en el tablero. Esto se hace con ayuda de jQuery. El primer
paso es obtener el número de la ficha cliqueada, luego usar el método mover ficha y verificar si el movimiento
se realizó o no. Si el movimiento fue exitoso dentro de nuestro programa,
usamos las propiedades de posición actual de la ficha para usarlas como
referencia para el movimiento de la ficha en la interfaz gráfica. Este
movimiento se realiza por medio de jQuery y su función de animación.
Finalmente, cada vez que se hace clic sobre una ficha, también se verifica si
el juego ha sido resuelto. En caso de que el juego sea resuelto, se cambia el
texto de la caja de diálogo por: “Has resuelto el juego”.
Este es nuestro programa principal
Este es nuestro programa principal
$(document).ready(function(){
var game = new Game();
game.initializeGame();
game.drawBoard(500);
$(".game_piece").click(function(){
var piece_number = $(this).children().text();
if(game.movePiece(piece_number))
$(this).animate({'top':((game.piecesArray[piece_number-1].posX-1)*25)+'%','left':((game.piecesArray[piece_number-1].posY-1)*25)+'%'},500);
if(game.checkGame())
$("#talking_box").text("Has resuelto el juego");
});
});
Estilos y CSS
Muchas de las propiedades gráficas
del juego como las proporciones, el tamaño de letra, los colores y otros se
encuentran el archivo de estilos. Aquí está el código con algunos comentarios.
*
{
margin: 0; /*Resetea los márgenes de todos los elementos en la página*/
}
body
{
background-color: #AA7639;
color:#884900;
font-size: 46px;
text-align: center;
-webkit-user-select: none; /*Para que no se pueda seleccionar el texto interior de las fichas*/
user-select: none;
}
#game_board
{
background-color: #884900;
border-radius: 5px;
margin: 20px auto;
position: relative;
}
#piece_container
{
background-color: #AA7639;
font-size: 0;
height: 96%; /*El alto del div interior que conforma el tablero para dar la sensación de un borde grueso*/
left: 2%;
position: absolute;
top:2%;
width: 96%; /*El ancho del div interior que conforma el tablero para dar la sensación de un borde grueso*/
}
#talking_box
{
background-color: #FFF3E6;
border: 5px solid #884900;
border-radius: 5px;
color: #FF8901;
height:auto;
margin: 10px auto;
padding-bottom: 5px;
width: 500px;
}
.game_piece
{
background-color: #FFF3E6;
border: 2px groove #737679;
box-sizing: border-box; /*Propiedad que hace que los bordes cuenten en el tamaño total de cada ficha*/
display: inline-block;
height: 25%; /*Ancho de la ficha: la cuarta parte del tablero */
position: absolute;
width: 25%;
}
.number_container
{
color: #FF8901;
cursor: default;
font-size: 50px;
height: 50%;
left: 50%;
margin: -25% 0 0-25%;
position: absolute;
text-align: center;
top: 50%;
width: 50%;
}
Anotaciones Finales
Espero que en algo haya
ayudado a alguien esta primera entrada a este blog. Después de haber hecho el
código anterior fue posible ver que se pueden realizar numerosas mejoras para
utilizar menos espacio en almacenamiento de variables y cosas así. Les dejo un
link donde se encuentran los archivos y otro donde pueden ver el juego
en acción.
Gracias por su tiempo y
espero poder hacer una próxima entrada agregándole un temporizador a nuestro
juego y un contador de movimientos.
Link para probar el juego funcionando y ver el código: Míralo aquí
Link para descargar los archivos: Descárgalo aquí
Link para probar el juego funcionando y ver el código: Míralo aquí
Link para descargar los archivos: Descárgalo aquí

