Cómo codificar el juego de la vida con React

El juego de la vida implica una cuadrícula ortogonal bidimensional de celdas cuadradas, cada una de las cuales se encuentra en uno de dos estados posibles, vivo o muerto. En cada paso, cada celda interactúa con sus ocho vecinos adyacentes siguiendo un conjunto simple de reglas que resultan en nacimientos y muertes.

Es un juego de cero jugadores. Su evolución está determinada por su estado inicial, y no requiere más aportaciones de los jugadores. Uno interactúa con el juego creando una configuración inicial y observando cómo evoluciona o, para jugadores avanzados, creando patrones con propiedades particulares.

Reglas

  1. Cualquier célula viva con menos de dos vecinos vivos muere, como por falta de población
  2. Cualquier célula viva con dos o tres vecinos vivos vive para la próxima generación
  3. Cualquier célula viva con más de tres vecinos vivos muere, como por superpoblación
  4. Cualquier célula muerta con exactamente tres vecinos vivos se convierte en una célula viva, como por reproducción.

Aunque el juego se puede codificar perfectamente con JavaScript vanilla, estaba feliz de superar el desafío con React. Así que comencemos.

Configurar React

Hay varias formas de configurar React, pero si es nuevo en él, le recomiendo que consulte los documentos y github de la aplicación Create React , así como la descripción general detallada de React de Tania Rascia.

Diseñando el juego

La imagen principal en la parte superior es mi implementación del juego. La cuadrícula del tablero que contiene celdas claras (vivas) y oscuras (muertas) muestra la evolución del juego. Los controladores le permiten iniciar / detener, ir paso a paso, configurar un tablero nuevo o borrarlo para experimentar con sus propios patrones haciendo clic en las celdas individuales. El control deslizante controla la velocidad y la generación informa el número de iteraciones completadas.

Además del componente principal que contiene el estado, crearé por separado una función para generar el estado de todas las celdas de la placa desde cero, un componente para la cuadrícula de la placa y otro para el control deslizante.

Configuración de App.js

Primero, importemos React y React.Component de "react". Luego, establezca cuántas filas y columnas tiene la cuadrícula del tablero. Yo voy con 40 por 60 pero siéntete libre de jugar con diferentes números. Luego vienen los componentes de función y función separados (observe la primera letra en mayúscula) descritos anteriormente, así como el componente de clase que contiene el estado y los métodos, incluido el de renderizado. Finalmente, exportemos la aplicación del componente principal.

import React, { Component } from 'react'; const totalBoardRows = 40; const totalBoardColumns = 60; const newBoardStatus = () => {}; const BoardGrid = () => {}; const Slider = () => {}; class App extends Component { state = {}; // Methods ... render() { return ( ); } } export default App;

Generando el estado de celda de una nueva placa

Como necesitamos conocer el estado de cada celda y sus 8 vecinos para cada iteración, creemos una función que devuelva una matriz de matrices, cada una de las cuales contiene celdas con valores booleanos. La cantidad de matrices dentro de la matriz principal coincidirá con la cantidad de filas, y la cantidad de valores dentro de cada una de estas matrices coincidirá con la cantidad de columnas. Entonces, cada valor booleano representará el estado de cada celda, "vivo" o "muerto". El parámetro de la función tiene por defecto menos del 30% de probabilidad de estar vivo, pero puede experimentar con otros números.

const newBoardStatus = (cellStatus = () => Math.random()  { const grid = []; for (let r = 0; r < totalBoardRows; r++) { grid[r] = []; for (let c = 0; c < totalBoardColumns; c++) { grid[r][c] = cellStatus(); } } return grid; }; /* Returns an array of arrays, each containing booleans values (40) [Array(60), Array(60), ... ] 0: (60) [true, false, true, ... ] 1: (60) [false, false, false, ... ] 2: (60) [false, false, true, ...] ... */

Generando la cuadrícula del tablero

Definamos un componente de función que crea la cuadrícula de la placa y la asigna a una variable. La función recibe el estado de todo el estado de la placa y un método que permite a los usuarios cambiar el estado de celdas individuales como accesorios. Este método se define en el componente principal donde se mantiene todo el estado de la aplicación.

Cada celda está representada por una tabla y tiene un atributo className cuyo valor depende del valor booleano de la celda del tablero correspondiente. El jugador que hace clic en una celda da como resultado que el método pasado como accesorios se llame con la ubicación de la fila y columna de la celda como argumento.

Consulte Lifting State Up para obtener información adicional sobre cómo pasar métodos como accesorios, y no olvide agregar las claves.

const BoardGrid = ({ boardStatus, onToggleCellStatus }) => { const handleClick = (r,c) => onToggleCellStatus(r,c); const tr = []; for (let r = 0; r < totalBoardRows; r++) { const td = []; for (let c = 0; c < totalBoardColumns; c++) { td.push(  handleClick(r,c)} /> ); } tr.push({td}); } return {tr}
; };

Creando el control deslizante de velocidad

Este componente de función crea un control deslizante para permitir a los jugadores cambiar la velocidad de las iteraciones. Recibe el estado de la velocidad actual y un método para manejar el cambio de velocidad como accesorios. Puede probar diferentes valores mínimos, máximos y escalonados. Un cambio de velocidad da como resultado que el método pasado como props sea llamado con la velocidad deseada como argumento.

const Slider = ({ speed, onSpeedChange }) => { const handleChange = e => onSpeedChange(e.target.value); return (  ); };

Componente principal

Dado que contiene el estado de la aplicación, hagámoslo un componente de clase. Tenga en cuenta que no estoy usando Hooks, una nueva adición en React 16.8 que le permite usar el estado y otras características de React sin escribir una clase. Prefiero usar la sintaxis de campos de clase pública experimental, por lo que no enlazo los métodos dentro del constructor.

Vamos a diseccionarlo.

Estado

Defino el estado como un objeto con las propiedades del estado del tablero, el número de generación, el juego en ejecución o parado y la velocidad de las iteraciones. Cuando comience el juego, el estado de las celdas del tablero será el que devuelva la llamada a la función que genera un nuevo estado del tablero. La generación comienza en 0 y el juego solo se ejecutará después de que el usuario lo decida. La velocidad predeterminada es de 500 ms.

class App extends Component { state = { boardStatus: newBoardStatus(), generation: 0, isGameRunning: false, speed: 500 }; // Other methods ... }

Botón Ejecutar / Detener

Función que devuelve un elemento de botón diferente según el estado del juego: en ejecución o detenido.

class App extends Component { state = {...}; runStopButton = () => { return this.state.isGameRunning ? Stop : Start; } // Other methods ... }

Tablero claro y nuevo

Methods to handle players request to start with a new random board’s cell status or to clear the board completely so they can then experiment by toggling individual cell status. The difference between them is that the one that clears the board sets the state for all cells to false, while the other doesn’t pass any arguments to the newBoardStatus method so the status of each cell becomes by default a random boolean value.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => { this.setState({ boardStatus: newBoardStatus(() => false), generation: 0 }); } handleNewBoard = () => { this.setState({ boardStatus: newBoardStatus(), generation: 0 }); } // More methods ... }

Toggle cell status

We need a method to handle players’ requests to toggle individual cell status, which is useful to experiment with custom patterns directly on the board. The BoardGrid component calls it every time the player clicks on a cell. It sets the states of the board status by calling a function and passing it the previous state as argument.

The function deep clones the previous board’s status to avoid modifying it by reference when updating an individual cell on the next line. (Using const clonedBoardStatus = […boardStatus] would modify the original status because Spread syntax effectively goes one level deep while copying an array, therefore, it may be unsuitable for copying multidimensional arrays. Note that JSON.parse(JSON.stringify(obj)) doesn’t work if the cloned object uses functions). The function finally returns the updated cloned board status, effectively updating the status of the board.

For deep cloning check out here, here and here.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => {...} handleNewBoard = () => {...} handleToggleCellStatus = (r,c) => { const toggleBoardStatus = prevState => { const clonedBoardStatus = JSON.parse(JSON.stringify(prevState.boardStatus)); clonedBoardStatus[r][c] = !clonedBoardStatus[r][c]; return clonedBoardStatus; }; this.setState(prevState => ({ boardStatus: toggleBoardStatus(prevState) })); } // Other methods ... }

Generating the next step

Here is where the next game iteration is generated by setting the state of the board status to the returned value of a function. It also adds one to the generation’s state to inform the player how many iterations have been produced so far.

The function (“nextStep”) defines two variables: the board status and a deep cloned board status. Then a function calculates the amount of neighbors (within the board) with value true for an individual cell, whenever it is called. Due to the rules, there’s no need to count more than four true neighbors per cell. Lastly, and according to the rules, it updates the cloned board’s individual cell status and return the cloned board status, which is used in the setState.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => {...} handleNewBoard = () => {...} handleToggleCellStatus = () => {...} handleStep = () => { const nextStep = prevState => { const boardStatus = prevState.boardStatus; const clonedBoardStatus = JSON.parse(JSON.stringify(boardStatus)); const amountTrueNeighbors = (r,c) => { const neighbors = [[-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]]; return neighbors.reduce((trueNeighbors, neighbor) => { const x = r + neighbor[0]; const y = c + neighbor[1]; const isNeighborOnBoard = (x >= 0 && x = 0 && y < totalBoardColumns); /* No need to count more than 4 alive neighbors */ if (trueNeighbors < 4 && isNeighborOnBoard && boardStatus[x][y]) { return trueNeighbors + 1; } else { return trueNeighbors; } }, 0); }; for (let r = 0; r < totalBoardRows; r++) { for (let c = 0; c < totalBoardColumns; c++) { const totalTrueNeighbors = amountTrueNeighbors(r,c); if (!boardStatus[r][c]) { if (totalTrueNeighbors === 3) clonedBoardStatus[r][c] = true; } else { if (totalTrueNeighbors  3) clonedBoardStatus[r][c] = false; } } } return clonedBoardStatus; }; this.setState(prevState => ({ boardStatus: nextStep(prevState), generation: prevState.generation + 1 })); } // Other methods ... } 

Handling the speed change and the start/stop action

These 3 methods only set the state value for the speed and isGameRunning properties.

Then, within the componentDidUpdate Lifecycle method, let’s clear and/or set a timer depending on different combinations of values. The timer schedules a call to the handleStep method at the specified speed intervals.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => {...} handleNewBoard = () => {...} handleToggleCellStatus = () => {...} handleStep = () => {...} handleSpeedChange = newSpeed => { this.setState({ speed: newSpeed }); } handleRun = () => { this.setState({ isGameRunning: true }); } handleStop = () => { this.setState({ isGameRunning: false }); } componentDidUpdate(prevProps, prevState) { const { isGameRunning, speed } = this.state; const speedChanged = prevState.speed !== speed; const gameStarted = !prevState.isGameRunning && isGameRunning; const gameStopped = prevState.isGameRunning && !isGameRunning; if ((isGameRunning && speedChanged) || gameStopped) { clearInterval(this.timerID); } if ((isGameRunning && speedChanged) || gameStarted) { this.timerID = setInterval(() => { this.handleStep(); }, speed); } } // Render method ... }

The render method

The last method within the App component returns the desired structure and information of the page to be displayed. Since the state belongs to the App component, we pass the state and methods to the components that need them as props.

class App extends Component { // All previous methods ... render() { const { boardStatus, isGameRunning, generation, speed } = this.state; return ( 

Game of Life

Exporting the default App

Lastly, let’s export the default App (export default App;), which is imported along with the styles from “index.scss” by “index.js”, and then rendered to the DOM.

And that’s it! ?

Check out the full code on github and play the game here. Try these patterns below or create your own for fun.

Original text


Thanks for reading.