Comment coder le jeu de la vie avec React

Le jeu de la vie implique une grille orthogonale bidimensionnelle de cellules carrées, chacune d'elles étant dans l'un des deux états possibles, vivante ou morte. À chaque étape, chaque cellule interagit avec ses huit voisins adjacents en suivant un simple ensemble de règles entraînant des naissances et des décès.

C'est un jeu à zéro joueur. Son évolution est déterminée par son état initial, ne nécessitant aucune intervention supplémentaire des joueurs. On interagit avec le jeu en créant une configuration initiale et en observant son évolution, ou, pour les joueurs avancés, en créant des motifs aux propriétés particulières.

Règles

  1. Toute cellule vivante avec moins de deux voisins vivants meurt, comme par sous-population
  2. Toute cellule vivante avec deux ou trois voisins vivants passe à la génération suivante
  3. Toute cellule vivante avec plus de trois voisins vivants meurt, comme par surpopulation
  4. Toute cellule morte avec exactement trois voisins vivants devient une cellule vivante, comme par reproduction

Bien que le jeu puisse être parfaitement codé avec du JavaScript vanille, j'étais heureux de relever le défi avec React. Alors commençons.

Configurer React

Il existe plusieurs façons de configurer React, mais si vous êtes nouveau dans ce domaine, je vous recommande de consulter la documentation et le github de l' application Create React , ainsi que l'aperçu détaillé de React par Tania Rascia.

Concevoir le jeu

L'image principale en haut est ma mise en œuvre du jeu. La grille du plateau contenant des cellules claires (vivantes) et sombres (mortes) affiche l'évolution du jeu. Les contrôleurs vous permettent de démarrer / arrêter, de faire un pas à la fois, de configurer une nouvelle carte ou de l'effacer pour expérimenter vos propres modèles en cliquant sur les cellules individuelles. Le curseur contrôle la vitesse et la génération informe le nombre d'itérations terminées.

En plus du composant principal contenant l'état, je vais créer séparément une fonction pour générer l'état de toutes les cellules de la carte à partir de zéro, un composant pour la grille de la carte et un autre pour le curseur.

Configurer App.js

Tout d'abord, importons React et React.Component depuis «react». Ensuite, établissez le nombre de lignes et de colonnes de la grille du tableau. Je vais avec 40 par 60 mais n'hésitez pas à jouer avec des nombres différents. Viennent ensuite les composants de fonction et de fonction séparés (notez la première lettre en majuscule) décrits ci-dessus ainsi que le composant de classe contenant l'état et les méthodes, y compris celui de rendu. Enfin, exportons le composant principal App.

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;

Générer le statut de cellule d'un nouveau tableau

Puisque nous avons besoin de connaître l'état de chaque cellule et de ses 8 voisins pour chaque itération, créons une fonction qui renvoie un tableau de tableaux contenant chacun des cellules avec des valeurs booléennes. Le nombre de tableaux dans le tableau principal correspondra au nombre de lignes et le nombre de valeurs dans chacun de ces tableaux correspondra au nombre de colonnes. Ainsi, chaque valeur booléenne représentera l'état de chaque cellule, «vivant» ou «mort». Le paramètre de la fonction a par défaut moins de 30% de chances d'être en vie, mais est tombé libre d'expérimenter avec d'autres nombres.

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, ...] ... */

Générer la grille du tableau

Définissons un composant de fonction qui crée la grille du tableau et l'assigne à une variable. La fonction reçoit l'état de l'ensemble du statut de la carte et une méthode qui permet aux utilisateurs de basculer le statut de cellules individuelles en tant qu'accessoires. Cette méthode est définie sur le composant principal où tout l'état de l'application est conservé.

Chaque cellule est représentée par une table et possède un attribut className dont la valeur dépend de la valeur booléenne de la cellule de tableau correspondante. Le joueur qui clique sur une cellule entraîne l'appel de la méthode transmise comme accessoire avec la ligne et l'emplacement de la colonne de la cellule comme argument.

Consultez Lifting State Up pour plus d'informations sur le passage des méthodes en tant qu'accessoires, et n'oubliez pas d'ajouter les clés.

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}
; };

Création du curseur de vitesse

Ce composant de fonction crée un curseur pour permettre aux joueurs de modifier la vitesse des itérations. Il reçoit l'état de la vitesse actuelle et une méthode pour gérer le changement de vitesse comme accessoires. Vous pouvez essayer différentes valeurs minimum, maximum et pas. Un changement de vitesse entraîne l'appel de la méthode passée comme accessoire avec la vitesse souhaitée comme argument.

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

Composant principal

Puisqu'il contient l'état de l'application, faisons-en un composant de classe. Notez que je n'utilise pas Hooks, un nouvel ajout dans React 16.8 qui vous permet d'utiliser l'état et d'autres fonctionnalités de React sans écrire de classe. Je préfère utiliser la syntaxe des champs de classe publique expérimentale, donc je ne lie pas les méthodes dans le constructeur.

Disséquons-le.

Etat

Je définis l'état comme un objet avec les propriétés de l'état de la carte, du nombre de génération, du jeu en cours ou arrêté et de la vitesse des itérations. Au démarrage du jeu, l'état des cellules du plateau sera celui renvoyé par l'appel à la fonction qui génère un nouveau statut du plateau. La génération commence à 0 et le jeu ne fonctionnera qu'après la décision de l'utilisateur. La vitesse par défaut est de 500 ms.

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

Bouton Run / Stop

Fonction qui renvoie un élément de bouton différent selon l'état du jeu: en cours d'exécution ou arrêté.

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

Tableau clair et neuf

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.