En el anterior post, definimos la estructura básica del tablero. Ahora toca el turno de crear y estructurar el código Javascript. Para ello siempre seguiremos dos normas básicas a la hora de programar:

  • Por encima de todo primará que el resultado cumpla con lo especificado. En este caso, nuestro objetivo es que las reglas del juego se cumplan.

  • En un futuro, cuando vuelvas a leer el código o si se pone a trabajar otra persona con él, ha(s) de ser capaz de entenderlo sin un mayor esfuerzo.

Siguiendo estas dos reglas, que a priori son bastante obvias, estaremos seguros de que siempre haremos el mejor código según nuestros conocimientos.

Empecemos. Partimos del mismo punto donde terminamos en el anterior post:

HTML
JS
CSS
Previsualización
<html>
  <head>
    <meta charset="utf-8">
    <link href="./styles.css" rel="stylesheet" type="text/css">
  </head>

  <body>
    <div class="game" id="game"></div> 
    <script src="./script.js"></script>
  </body>
</html>

Crearemos una clase para encapsular nuestro código. Una máxima en informática es conseguir la máxima cohesion entre código que tiene que trabajar de forma dependiente. De la misma forma, hay que conseguir no tener acoplamiento entre diferentes piezas de código que debieran ser independientes.

class Board {
  #columns = 0
  #rows = 0
  #startIn = 0

  constructor (columns = 6, rows = 6, startIn = 3) {
    this.#columns = columns
    this.#rows = rows
    this.#startIn = startIn

    this.#createBoard()
  }

  #createBoard () {
    const game = document.getElementById('game')
    game.style.setProperty('--dynamic-columns', this.#columns);
    
    for (let i = 0; i < this.#columns * this.#rows; i++) {
      const button = document.createElement("button")
      button.classList.add('cell')
      button.textContent = this.#startIn
      game.appendChild(button)
      button.addEventListener('click', () => console.log('funciona'))
    }
  }

}

const board = new Board()

Simplemente hemos reestructurado el código que teníamos encapsulandolo en una clase. Hemos creado un método para crear el juego de 0 en caso de que lo queramos reiniciar. Ahora añadiremos la lógica al hacer click en un botón. Hasta ahora teniamos un console.log que tendremos que reemplazar:

class Board {
  #columns = 0
  #rows = 0
  #startIn = 0
  #values = []

  constructor (columns = 6, rows = 6, startIn = 3) {
    this.#columns = columns
    this.#rows = rows
    this.#startIn = startIn

    for (let i = 0; i < this.#columns * this.#rows; i++) {
      this.#values.push(startIn)
    }

    this.#createBoard()
  }

  #onCellClick (position) {
    this.#values[position] = this.#values[position] - 1 & this.#startIn

    this.#renderBoard()
  }

  #createBoard () {
    const game = document.getElementById('game')
    game.style.setProperty('--dynamic-columns', this.#columns);
    
    for (let i = 0; i < this.#columns * this.#rows; i++) {
      const button = document.createElement("button")
      button.classList.add('cell')
      button.textContent = this.#startIn
      game.appendChild(button)
      button.addEventListener('click', () => this.#onCellClick(i))
    }
  }

  #renderBoard () {
    const buttons = document.querySelectorAll("#game .cell")
    
    buttons.forEach((button, index) => {
      button.textContent = this.#values[index]
    })
  }
}

const board = new Board()

Hemos creado un array con los valores de cada botón. En el constructor lo rellenamos con todas las celdas que tenemos y su valor inicial.

Cada vez que clickamos en una celda, obtenemos el valor del array, le restamos 1 y obtenemos el resto entre el valor inicial para hacer obtener un anillo matemático.

this.#values[position] = this.#values[position] - 1 & this.#startIn

En matemáticas se conoce como la operación módulo. Así, cuando llegamos a -1 y obtenemos su resto volvemos a obtener el valor 3.

Otra parte que suele ser buena idea es separar la parte de lógica de la parte de representación. Cada vez que clickamos en un botón llamamos a la función para actualizar el tablero. No va a ser lo más óptimo pero nos ayudará a separar las diferentes partes del código.

Añadiremos ahora la lógica para comprobar que el juego ha terminado. Después de cada click comprobamos que todas las celdas son 0 y mostramos un modal:

HTML
JS
CSS
Previsualización
<html>
  <head>
    <meta charset="utf-8">
    <link >
  </head>

  <body>
    <div class="game" id="game" /> 
    <div id="end-modal" class="end-modal">
      Has ganado el juego
      ¿quieres jugar otra partida?

      <button class="button" onclick="board.resetGame()">Jugar de nuevo</button>
    </div>
  </body>
</html>

Cuando se cumple que todos los valores son 0, añadimos una clase al modal para poder mostrarlo. La clase tiene un método público para poder reiniciar el juego.

Para finalizar el juego, solo faltaría poder restar las celdas en cruz:

class Board {
  #columns = 0
  #rows = 0
  #startIn = 0
  #values = []

  constructor (columns = 6, rows = 6, startIn = 3) {
    this.#columns = columns
    this.#rows = rows
    this.#startIn = startIn

    for (let i = 0; i < this.#columns * this.#rows; i++) {
      this.#values.push(startIn)
    }

    this.#createBoard()
  }

  #manageCell(value) {
    return value - 1 & this.#startIn
  }

  #onCellClick (position) {
    this.#values[position] = this.#manageCell(this.#values[position])

    const leftCell = position - 1
    if ((leftCell >= 0) && (leftCell) % this.#columns < this.#columns - 1) {
      this.#values[leftCell] = this.#manageCell(this.#values[leftCell])
    }

    const rightCell = position + 1
    if ((rightCell <= this.#values.length) && rightCell % this.#columns > 0) {
      this.#values[rightCell] = this.#manageCell(this.#values[rightCell])
    }

    const belowCell = position + this.#columns
    if (belowCell <= this.#values.length) {
      this.#values[belowCell] = this.#manageCell(this.#values[belowCell])
    }

    const upCell = position - this.#columns
    if (upCell >= 0) {
      this.#values[upCell] = this.#manageCell(this.#values[upCell])
    }


    this.#renderBoard()

    this.#checkWinner()
  }

  #createBoard () {
    const game = document.getElementById('game')
    game.style.setProperty('--dynamic-columns', this.#columns);
    
    for (let i = 0; i < this.#columns * this.#rows; i++) {
      const button = document.createElement("button")
      button.classList.add('cell')
      button.textContent = this.#startIn
      game.appendChild(button)
      button.addEventListener('click', () => this.#onCellClick(i))
    }
  }

  #renderBoard () {
    const buttons = document.querySelectorAll("#game .cell")
    
    buttons.forEach((button, index) => {
      button.textContent = this.#values[index]
    })
  }

  #checkWinner () {
    if(!this.#values.some(Boolean)) {
      document.getElementById('end-modal').classList.add('show')
    }
  }

  resetGame () {
    this.#values = this.#values.map(() => this.#startIn)
    this.#renderBoard()
    document.getElementById('end-modal').classList.remove('show')
  }
}

const board = new Board()

La parte más complicada son las comprobaciones necesarias para que cuando se esta en un borde, no cambie la celda del otro lado del tablero.

Así habremos terminado nuestro primer juego web, pero podemos mejorarlo bastante. Te invito a que intentemos implementar lo siguiente:

  • Inputs para configurar el tamaño del tablero y el valor en el que empiezan las celdas.

  • Botones para poder deshacer y rehacer movimientos.

  • Listado con las mejores jugadas. Al ganar mostrar un input para guardar el nombre con el total de movimientos.