Desarrollo web para no iniciados – Parte 4 JavaScript (buscaminas)

Hemos visto una mínima parte de lo que es javascript, y ademas de una forma muy escueta, nuestra idea es jugar con estos conceptos para reforzarlos mientras construimos una aplicación completa. Hemos elegido hacer un buscaminas porque con ayuda del navegador web podemos construir uno usando muy poco código y así poder extendernos sobre el mismo para poder entenderlo completamente.

Lo primero que tenemos que tener claro es que queremos hacer, esto que a priori puede parecer lógico es fundamental tenerlo en cuenta. Antes de poder hacer un buscaminas tenemos que saber como funciona.

El buscaminas es un juego donde tenemos un tablero de NxM casillas en las que se encuentran repartidas una cierta cantidad de minas. Al principio todas las casillas están tapadas, y podremos ir eligiendo revelarlas seleccionándolas con el ratón. Si seleccionamos una mina, perdemos, en cambio si no hay una mina, nos mostrara el número de minas que se encuentran en sus alrededores. Un ejemplo sobre un tablero mínimo 3×3 con dos minas, *, todo revelado,:

 2*2
 2*2
 111

Si todabia no podemos hacernos una idea podeis mirar imágenes de muchas implementaciones.

Pongámonos en faena. Antes que nada pondremos todo dentro de una carpeta que crearemos, en el escritorio, por ejemplo, con el nombre buscaminas. Primero crearemos un archivo HTML que llamaremos index.html

<!DOCTYPE html>
<html lang="es">
  <head>
    <meta charset="utf-8">
    <title>Mi primer js, un buscaminas!</title>
    <link rel='stylesheet' href='estilos.css' type='text/css' media='all' />
    <script type="text/javascript" src="codigo.js"></script>
  </head>
  <body>
    <h1>Buscaminas</h1>
  </body>
</html>

Podemos ver que ademas tendremos dos archivos más, una hoja de estilos (estilos.css) y un archivo javascript (código.js), para mantener nuestras cosas bien ordenadas, procuraremos siempre organizar nuestros archivos, siguiendo un orden lógico.

Creamos el archivo javascript (codigo.js) con el principio de nuestro programa. Vamos a definir el tamaño del tablero y el número de minas.

var COLUMNAS = 10;
var FILAS = 10;
var MINAS = 5;

Con esto, si abrimos nuestro archivo index.html en chrome y nos vamos a las herramientas de desarrollo, al panel de “Network”
Consola de red chrome

Podemos observar que carga el index.html, no puede cargar el estilos.css, por que aun no lo hemos creado, y carga nuestro codigo.js. Como todavía no vamos a tocar los estilos, dejemos lo así hasta su momento.

Si nos dirijimos a nuestra consola y nos apetece saber cuantas casillas tendrá nuestro tablero podemos multiplicar las FILAS por las COLUMNAS

FILAS*COLUMNAS // 100

La consola es muy útil para ir bocetando el código que al final usaremos. Sigamos con nuestro buscaminas.

Sabemos cuantas filas tendrá y cuantas columnas, vamos a crear un array donde guardar la información del tablero, pero primero vamos a analizar la información que guardaremos.

Antes dijimos que en una casilla puede haber una mina o no y que además si no había una mina y en los alrededores si, mostrará el número de ellas. Podemos codificar esta información en nuestras casillas usando un numero tal que -1 sea una mina, 0 que no y que ademas no hay ninguna en los alrededores, y 1 hasta 8 para las que tienen en los alrededores. Primero crearemos un tablero vacío con un array de COLUMNASxFILAS con todo ceros. Creemos una función para esto:

var crear_tablero = function(columnas,filas) {
  var devolver = [];
  for(var x=0; x<columnas; x++) {
    devolver[x] = [];
    for(var y=0; y<filas; y++) {
      devolver[x][y] = 0;
    }
  }
  return devolver;
}

Esta funcion nos devuelve un array para nuestro tablero con las filas y columnas que le indiquemos con todos sus valores a 0. Podremos acceder a sus elementos por su posicion x,y usando [x][y]. Vamos a probarlo en el navegador. Lo añades a codigo.js, recargas nuestra página, y vas a la consola.

Parece que funciona, pero queremos verla funcionar, esto código apenas tarda unos milisegundos en ejecutarse, pero hace bastantes cosas, podemos usar las potentes herramientas del navegador para reproducirla, paso a paso.

Vamos al panel de Scripts, donde deberíamos ver nuestro, código y picando en el numero 6 de la numeración del codigo, pondremos un “breakpoint”.

Con esto conseguimos que nuestro navegador, se detenga en ese punto para poder inspeccionarlo a fondo. Probémoslo, vete a la consola y vuelve a llamar a la función:

crear_tablero(COLUMNAS,FILAS)

Al ejecutar el código nuestro navegador y encontrar el break point, lo detiene y nos vuelve al panel de Scripts, con nuestro código pausado he informandonos de las variables.

Podemos usar el botón que esta resaltado en la imagen anterior para ir ejecutando nuestro código, paso a paso, prueba lo y fíjate en como van cambiando las variables del panel derecho.

Puedes también avanzar paso a paso con F10 o terminar con F8, el botón a la izquierda del de paso a paso.

Como nota adicional, fijaros en que hemos creado un array vacío, [], y le añadimos los elementos indicando su posición, y que además para separalo por filas y columnas lo que tenemos son un array de arrays, para que se vea mejor, si fueras a hacer nuestro ejemplo a mano:

var hand_made = [
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0],
  [0,0,0,0,0,0,0,0,0,0]
];

hand_made[3][4] // 0

Tened presente que en un array podéis guardar cualquier cosa.

Sigamos, añadiendo unas minas al azar. Antes vamos a ver como conseguir algunos números aleatorios, fundamentales para crear variedad en nuestro código.

Javascript nos proporciona algunas funciones útiles a través de un, como se conoce en programación, API. El objeto Math nos proporciona, entre otras, dos funciones que nos servirán a este propósito, floor y random, la primera nos redondeará un número real a un entero y la segunda nos proporcionará un número aleatorio entre 0 y 1.

var numero_aleatorio = function(maximo) {
  return Math.floor(Math.random()*maximo);
}

Esta función nos devolverá un número entre 0 y máximo. Teniendo esto es fácil repartir unas minas en nuestro tablero.

var repartir_minas = function(tablero,minas,columnas,filas) {
  for(var i=0; i<minas; i++) {
    tablero[numero_aleatorio(columnas)][numero_aleatorio(filas)] = -1;
  }
}

Ahora, sólo nos falta calcular en las casillas que no tienen mina, sus adyacentes, que lo haremos contando las minas que están en las casillas alrededor de una en concreto, para ello, crearemos otra función que nos devuelve la información del tablero calculada:

var comprobar = function(tablero,x,y) {
  // Si es una mina devolvemos -1
  if(tablero[x][y]==-1) {
    return -1;
  } else { // En caso contrario calculamos las que 'toca'
    var total = 0;
    // Fila superior
    total += tablero[x-1]&&(tablero[x-1][y-1]==-1) ? 1 : 0;
    total += tablero[x]&&(tablero[x][y-1]==-1) ? 1 : 0;
    total += tablero[x+1]&&(tablero[x+1][y-1]==-1) ? 1 : 0;

    // Fila central, solo dos por que el centrono es una mina
    total += tablero[x-1]&&(tablero[x-1][y]==-1) ? 1 : 0;
    total += tablero[x+1]&&(tablero[x+1][y]==-1) ? 1 : 0;
    
    // Fila inferior
    total += tablero[x-1]&&(tablero[x-1][y+1]==-1) ? 1 : 0;
    total += tablero[x]&&(tablero[x][y+1]==-1) ? 1 : 0;
    total += tablero[x+1]&&(tablero[x+1][y+1]==-1) ? 1 : 0;
    return total;
  }
}

Bueno, aqui vamos a tener que detenernos a comprobar unas cuantas cosas nuevas.

Es muy común en programación ir acumulando valores, normalmente podriamos usar una expresión como

total = total + nuevo_valor

Como esto es tan comun existe una forma abreviada de escribirlo que tambien sirve para la resta multiplicación y división

total += valor // total = total + valor
total -= valor // total = total - valor
total *= valor // total = total * valor
total /= valor // total = total / valor

El operador ternario ?:. Los operadores que hemos visto hasta ahora tenian dos argumentos, en una suma por ejemplo, los dos números a sumar. Tenemos uno muy útil que trabaja con tres, una condición y dos posibles valores.

var total = 4>3 ? 4 : 3; // total == 4
total = 4<3 ? 4 : 3; // total == 3

Se comprueba la parte anterior al ? y si es cierta devuelve el primer valor si no el segundo. Podemos verlo como un mini if/else para asignar valores.

La ultima cosa a comentar antes de aplicar esto a la función recién vista, es referente a los array. Hemos visto como podemos usar sus valores usando unos corchetes para indicar la posicion, ¿pero que pasa cuando accedemos a un elemento que no existe tablero[-1]? javascript nos devolvera un valor vacío, undefined, que si lo comprobamos con un if por ejemplo sérá falso. Esto es importante por que si intentamos hacer algo con undefined nuestro navegador se quejará.

Ya estamos listos para desengranar nuestra nueva función.

var comprobar = function(tablero,x,y) {
...
}

A esta función le pasaremos el tablero, y los coordenadas que queremos comprobar y nos devolvera -1 si es una mina y si no el número de minas colindantes. En el mini ejemplo inicial para todo el tablero:

 2 -1 2
 2 -1 2
 1  1 1
  ...
  // Si es una mina devolvemos -1
  if(tablero[x][y]==-1) {
    return -1;
  } ...

Saber si es una mina es facil, solo tenemos que comprobar que su valor sea -1 y en ese caso devolver tambien -1.

    ...
  } else { // En caso contrario calculamos las que 'toca'
    var total = 0;
    ...

Tenemos que contar las ocho casillas que rodean a la actual lo haremos una a una y llevaremos la cuenta en total.

    ...
    // Fila superior
    total += tablero[x-1] && (tablero[x-1][y-1]==-1) ? 1 : 0;
    total += tablero[x] && (tablero[x][y-1]==-1) ? 1 : 0;
    total += tablero[x+1] && (tablero[x+1][y-1]==-1) ? 1 : 0;
    ...

Usamos la abreviatura += para ir contando la primera fila (y-1), para sus tres posiciones x-1, x, x+1. Fijaos en que primeros comprobamos que exista la columna, para evitar que se rompa cuando comprobamos la primera casilla [0][0]

Después comprobamos la fila central y la última.

Vamos ahora a crear nuestro interfaz. Para ello vamos a usar unos elementos span, que son elementos de linea, que no tienen por defecto carga semántica. Y los colocaremos en filas de parrafos, p, para ello necesitaremos poder crear nuevos nodos para nuestro HTML y poder añadirselo a otros nodos. Cuando ejecutas javascript en el navegador, este nos expone una variable document que representa nuestro documento HTML. Podeis comprobar en la consola como nos devuelve un objeto que podemos mirar de la misma forma que lo hacemos cuando inspeccionamos un elemento. Este objeto tiene una función, createElement, que podemos usar para crear un nuevo elemento pasandole el nombre del tag a crear.

Si vamos a la consola y escribimos:

document.createElement("p")

Vereis como nos devuelve un parrafo vacio. Tambien tiene una variable body que señala al cuerpo de nuestro HTML, probadlo en la consola:

document.body

Los nodos HTML tienen una función, appendChild, que sirve para añadirles otro elemento HTML dentro de este. Probad en la consola:

document.body.appendChild(document.createElement("p"))

Y comprobad despues en el panel Elements como nos ha añadido un nuevo nodo p vacío.

Nos falta ver que los nodos tambien tienen una variable innerHTML donde especificarles el contenido del mismo. Prueba esto en la consola.

var parrafo = document.createElement("p");
parrafo.innerHTML = "Texto de prueba";
document.body.appendChild(parrafo)

Ya podemos crear nuestro tablero html

var crear_tablero_html = function(columnas,filas) {
  var devolver = [];
  // Creamos todas las columnas
  for(var x=0; x<columnas; x++) {
    devolver[x] = [];
  }
  for(var y=0; y<filas; y++) {
    var p = document.createElement("p");
    document.body.appendChild(p);
    for(var x=0; x<columnas; x++) {
      var tag = document.createElement("span");
      tag.innerHTML = "&nbsp;"
      p.appendChild(tag);
      devolver[x][y] = tag;
    }
  }
  return devolver;
}

Que nos devuelve un array como el de crear_tablero, pero con los elementos HTML span, con un espacio en todos.

&nbsp; <!-- Espacio " " en HTML -->

Si nos dirigimos a la consola y ejecutamos esta función:

crear_tablero_html(COLUMNAS,FILAS)

No veremos nada, pero si vamos al panel Elements podremos comprobar como nos ha creado nuestros elementos.

Recordais que no teniamos hoja de estilos, ha llegado el momento de crearla, estilos.css. Como todas nuestras casillas son elementos span y ademas no tenemos otros, podemos aplicarle las reglas directamente al tag:

span {
  display:    inline-block;
  width:      1.5em;
  height:     1.5em;
  border:     1px solid black;
  text-align: center;
}

Veamos que hacen cada una de estas propiedades:

  display: inline-block;

Cuando vimos html y css, dijimos que habian elementos de bloque, aquellos que ocupan el espacio de su contenedor, y los elementos de línea, que solo usaban el espacio que requerían. Nuestro span son elementos de línea, y como tales no podemos especificarles un tamaño concreto, cosa que necesitamos para contruir nuestro tablero. Si los hicieramos elementos de bloque:

  display: block;

Podriamos darle el tamaño que quisieramos, pero seguiría ocupando el espacio, tendríamos que hacerlos float y tendriamos que hacer malabarismos para que queden, bien. Pero en los navegadores modernos tenemos un tipo más el “inline-block”, que nos auna lo mejor de los dos mundo, nos permite indicar sus dimensiones pero seguirán funcionado como si fueran de línea.

  width: 1.5em;
  height: 1.5em;

Indicamos las dimensiones de nuestros elementos, pero usamos la medida em que es relativa al alto de la fuente, 1.5em podemos verlo como, letra y media.

  border: 1px solid black;

Le ponemos un borde negro, sólido de 1 pixel, un punto de la pantalla.

 text-align: center;

Y por último hacemos que el texto se centre para que quede más aparente.

Si guardamos nuestra hoja de estilos, y volvemos a intentar crear nuestro tablero ahora veremos, como los parrafos tiene un margen que nos fastidia el invento. Añadimos la siguiente regla css.

p {
  margin:0;
  padding:0;
}

Ya podemos crear nuestro tablero, esto empieza a ponerse interesante, pero no es muy cómodo tener que estar diciendo exactamente lo que tiene que hacer en la consola. Vamos a dejar puesto lo que hemos conseguido para que se ejecute cuando se cargue la página. Al final de nuestro codigo.js ponemos:

var tablero = crear_tablero(COLUMNAS, FILAS);
repartir_minas(tablero, MINAS, COLUMNAS, FILAS);
var tablero_html = crear_tablero_html(COLUMNAS, FILAS);

Lo guardamos, volvemos a nuestra página y la recargamos. No sucede lo que esperamos, nuestra consola no muestra un feo error sobre appendChild y null, en chrome además nos muestra el archivo y la línea en que se produjo el error, si picamos en esta información, nos lleva a la vista de los scripts, donde podemos ver más claramente que lo que nos está diciendo es que document.body es null. Null es parecido a undefined y representa vacío. Pero si hemos estado probando en la consola y funciona, ahora, ¿porque no?, y ademas si voy a la consola y pruebo directamente, funciona, a la perfección. ¿Qué esta pasando?

El código que hemos añadido, se ejecuta cuando se carga el archivo, el problema es que cuando esto ocurre todabia el navegador no ha terminado de generar al arbol con los nodos HTML. El navegador nos permite engancharnos a un evento que se disparará cuando los nodos esten listos para usarse. Como este es un tema en que los navegadores no se ponen de acuerdo y hay muchas librerias que solucionan estos problemas y otros, que además deberían ser vuestro siguiente paso en el mundo de javascript, aqui para no complicar la cosa vamos a usar una solución un poco fea pero efectiva en nuestro caso, vamos a esperar un segundo.

El API de javascript nos proporciona una función setTimeout, que nos permite ejecutar una función en un determinado número de milisegundos, para un segundo 1000. Si rehacemos nuestra último añadido:

setTimeout( function() {
  var tablero = crear_tablero(COLUMNAS, FILAS);
  repartir_minas(tablero, MINAS, COLUMNAS, FILAS);
  var tablero_html = crear_tablero_html(COLUMNAS, FILAS);
}, 1000);

Si ahora recargamos nuestra página, ya podemos obtener el resultado esperado, tras un segundo de pausa. Ya estamos bastante cerca de conseguir nuestro objetivo. Nos falta que al picar en nuestras casillas si era una mina, perdamos, y si no, nos revele el numero de minas que toca. Vamos a crear una funcion que haga este trabajo

var comprobar_juego = function(tablero,tablero_html,x,y) {
  if(
    tablero_html[x]
    &&tablero_html[x][y]
    &&(tablero_html[x][y].innerHTML=="&nbsp;")
  ){
    var valor = comprobar(tablero,x,y);
    if(valor!=null){
      if(valor==-1){
        alert("BOOM!");
      } else if(valor==0) {
        tablero_html[x][y].innerHTML = ".";
      } else if(valor) {
        tablero_html[x][y].innerHTML = valor;
      }
    }
  }
}

Lo primero que hacemos es comprobar que la casilla existe y no se ha revelado aún, o sea, todabía tiene el símbolo del espacio.

if(
  tablero_html[x]
  &&tablero_html[x][y]
  &&(tablero_html[x][y].innerHTML=="&nbsp;")) 
{
 ...

Despues nos queda comprobar que tenemos en la casilla. Si es una mina, perdemos, en caso de que no toque ninguna mina, 0, ponemos un punto para distingirlos, y si toca algunas minas ponemos la cantidad.

    var valor = comprobar(tablero,x,y);
    if(valor!=null){
      if(valor==-1){
        alert("BOOM!");
      } else if(valor==0) {
        tablero_html[x][y].innerHTML = ".";
      } else if(valor) {
        tablero_html[x][y].innerHTML = valor;
      }
    }
  }
}

Nos falta poder unir esta parafernalia con un click del ratón. Los nodos HTML tienen funciones que nos permiten definirles funciones que se ejecutaran ante determinados eventos, como el click del ratón. El problema en este caso es que los navegadores no se ponen de acuerdo, tos, IE, con lo que crearemos una función que le asigne a un elemento nuestra llamada para el evento, para usarla cuando creamos los nodos html tal que.

      ...
      p.appendChild(tag);
      asignar_evento(tag,x,y,llamada);
      devolver[x][y] = tag;
      ...

Pasándole esta llamada en la creación:

var crear_tablero_html = function(columnas,filas,llamada) {
  ...

Al hacer la asignación a traves de una función, conseguimos dos cosas:
1. Poder encapsular en esta función la problematica comentada anteriormente sobre navegadores.
2. Marcar los valores del bucle, x, y, para usarlos cuando se resuelva el envento.

Veamos la función asignar evento:

var asignar_evento = function(nodo,x,y,llamada) {
  if (nodo.addEventListener) {
    nodo.addEventListener('click',function(){
      llamada(x,y);
    },false);
  } else if (nodo.attachEvent) { // IE
    nodo.attachEvent('onclick',function(){
      llamada(x,y);
    });
  }
}

Comprobamos la existencia de las funciones propias del navegador para saber como usarlas. Daros cuenta como estamos arrastrando una funcion como hacemos con otros datos. Y además que los datos que le pasaremos serán unicamente una coordenada x y otra y.

Nos falta el pegamento que junte todo esto. En nuestro “programa principal”, lo que tenemos actualmente trampeado con el setTimeout.

  var tablero_html;
  var llamada_comprobar_juego = function(x,y) {
    comprobar_juego(tablero,tablero_html,x,y);
  };
  tablero_html = crear_tablero_html(COLUMNAS,FILAS,llamada_comprobar_juego);

Fijaros en como retrasamos la creación del propio tablero_html, porque necesitamos, para nuestra llamada, poder acceder a esta variable.

Ya podemos probar la primera versión funcional de nuestro buscaminas.

Aplausos en la sala, hemos creado nuestro primer juego. Hay muchas cosas que podemos mejorar, como se dice, a programar solo se aprende programando, os lo voy a dejar como ejercicios.

1. Usar una estructura parecida a la que usamos para contar las minas que tocan, para al comprobar una casilla vacía, . , se comprueben tambien los alrededores. De forma que se despejen las zonas vacías.

2. Buscar una forma de ejecutar nuestro programa principal cuando este cargado el HTML, para no usar nuestro actual truco de esperar un segundo. Usar para ello un framework javascript, MooTools, jQuery, prototype, …

3. Jugar con los valores de COLUMNAS, FILAS, MINAS, y los estilos.

Os dejo el archivo completo codigo.js
codigo.js

Happy coding! Nos vemos en la última parte de esta introducción al desarrollo web.