Hur man kodar Game of Life med React

The Game of Life involverar ett tvådimensionellt ortogonalt rutnät av kvadratiska celler, som var och en är i ett av två möjliga tillstånd, levande eller dött. Vid varje steg interagerar varje cell med sina åtta angränsande grannar genom att följa en enkel uppsättning regler som resulterar i födslar och dödsfall.

Det är ett spel med noll spelare. Dess utveckling bestäms av dess ursprungliga tillstånd och kräver ingen ytterligare input från spelare. Man interagerar med spelet genom att skapa en initial konfiguration och observera hur det utvecklas, eller, för avancerade spelare, genom att skapa mönster med speciella egenskaper.

Regler

  1. Alla levande celler med färre än två levande grannar dör, som om de är underbefolkade
  2. Alla levande celler med två eller tre levande grannar lever vidare till nästa generation
  3. Varje levande cell med mer än tre levande grannar dör, som av överbefolkning
  4. Varje död cell med exakt tre levande grannar blir en levande cell, som genom reproduktion

Även om spelet kan kodas perfekt med vanilj JavaScript, var jag glad att gå igenom utmaningen med React. Så låt oss börja.

Ställa in React

Det finns flera sätt att ställa in React, men om du är ny på det rekommenderar jag att du läser in Skapa React App- dokument och github, samt den detaljerade React-översikten av Tania Rascia.

Designa spelet

Huvudbilden högst upp är min implementering av spelet. Brädgallret som innehåller ljusa (levande) och mörka (döda) celler visar spelets utveckling. Kontrollerna låter dig starta / stoppa, gå ett steg i taget, ställa in en ny tavla eller rensa den för att experimentera med dina egna mönster genom att klicka på de enskilda cellerna. Skjutreglaget styr hastigheten och generationen informerar antalet slutförda iterationer.

Förutom huvudkomponenten som håller tillståndet skapar jag separat en funktion för att generera alla kortets cellstatus från grunden, en komponent för styrkortet och en annan för skjutreglaget.

Konfigurera App.js

Låt oss först importera React och React.Component från “react”. Bestäm sedan hur många rader och kolumner kortnätet har. Jag går med 40 med 60 men gärna spela med olika nummer. Sedan kommer de separata funktions- och funktionskomponenterna (se den stora bokstaven med stora bokstäver) som beskrivs ovan, liksom klasskomponenten som innehåller tillståndet och metoderna, inklusive renderingen. Låt oss slutligen exportera huvudkomponenten 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;

Skapa en ny styrelsecellstatus

Eftersom vi behöver veta statusen för varje cell och dess 8 grannar för varje iteration, låt oss skapa en funktion som returnerar en matris med matriser som alla innehåller celler med booleska värden. Antalet matriser i huvudmatrisen matchar antalet rader och antalet värden inom var och en av dessa matriser kommer att matcha antalet kolumner. Så varje booleskt värde representerar tillståndet för varje cell, "levande" eller "död". Funktionens parameter är som standard mindre än 30% chans att leva, men föll fritt att experimentera med andra siffror.

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

Generera brädanätet

Låt oss definiera en funktionskomponent som skapar tavlan och tilldelar den till en variabel. Funktionen får statusen för hela styrelsestatus och en metod som gör det möjligt för användare att växla status för enskilda celler som rekvisita. Denna metod definieras på huvudkomponenten där hela applikationens tillstånd hålls.

Varje cell representeras av en tabell och har ett attribut className vars värde beror på motsvarande kortcells booleska värde. Spelaren som klickar på en cell resulterar i att metoden skickas som rekvisita kallas med cellens rad- och kolumnplats som argument.

Kolla in Lifting State Up för ytterligare information om passeringsmetoder som rekvisita och glöm inte att lägga till nycklarna.

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

Skapa hastighetsreglaget

Denna funktionskomponent skapar en skjutreglage för att låta spelare ändra iterationshastigheten. Den tar emot tillståndet för den aktuella hastigheten och en metod för att hantera hastighetsförändringen som rekvisita. Du kan prova olika minimi-, max- och stegvärden. En hastighetsförändring resulterar i att metoden passeras som rekvisita kallas med önskad hastighet som argument.

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

Huvudkomponent

Eftersom det innehåller applikationens tillstånd, låt oss göra det till en klasskomponent. Observera att jag inte använder Hooks, ett nytt tillägg i React 16.8 som låter dig använda status och andra React-funktioner utan att skriva en klass. Jag föredrar att använda de experimentella offentliga klassfältens syntax, så jag binder inte metoderna inom konstruktören.

Låt oss dissekera det.

stat

Jag definierar tillståndet som ett objekt med egenskaperna för brädstatus, antal generationer, spel som körs eller stoppas och upprepningens hastighet. När spelet startar är statusen för brädans celler den som returneras av samtalet till den funktion som genererar en ny brädestatus. Generationen börjar vid 0 och spelet kommer bara att köras efter att användaren bestämmer sig. Standardhastigheten är 500 ms.

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

Kör / stopp-knappen

Funktion som returnerar ett annat knappelement beroende på spelets tillstånd: kör eller stoppas.

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

Tydlig och ny styrelse

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.

Thanks for reading.