Hogyan kódoljuk az élet játékát a React-lel

Az Élet játéka négyzet alakú sejtek kétdimenziós, ortogonális rácsát foglalja magában, amelyek mindegyike két lehetséges állapot egyikében van, élve vagy holtan. Minden lépésben minden sejt kölcsönhatásba lép nyolc szomszédos szomszédjával, egyszerű szabályrendszert követve, ami születéseket és halálokat eredményez.

Ez egy nulla játékos. Fejlődését a kezdeti állapota határozza meg, és nem igényel további hozzájárulást a játékosoktól. Az ember kölcsönhatásba lép a játékkal egy kezdeti konfiguráció létrehozásával és annak fejlődésének megfigyelésével, vagy haladó játékosok számára meghatározott tulajdonságokkal rendelkező minták létrehozásával.

Szabályok

  1. Bármely élő sejt, amelynek kevesebb, mint két élő szomszédja van, meghal, mintha alulnépesedés következtében
  2. Bármely élő sejt két vagy három élő szomszéddal él a következő generáció számára
  3. Bármely élő sejt, amelynél háromnál több élő szomszéd van, mintha túlnépesedés következtében meghalna
  4. Bármely, pontosan három élő szomszéddal rendelkező elhalt sejt sejt lesz, mintha reprodukcióval lenne élő sejt

Bár a játék tökéletesen kódolható vanília JavaScript-lel, örömmel vettem át a kihívást a React-tel. Kezdjük hát.

A React beállítása

A React beállításának számos módja van, de ha még nem ismeri ezt, javasoljuk, hogy nézze meg a Create React App dokumentumait és a github-ot, valamint Tania Rascia részletes React áttekintését.

A játék megtervezése

A legfelső kép a játék megvalósítása. A tábla rács világos (élő) és sötét (elhalt) cellákat tartalmaz a játék fejlődéséről. A vezérlők lehetővé teszik az indítást / leállítást, egy-egy lépés végrehajtását, egy új tábla felállítását vagy törlését, hogy az egyes cellákra kattintva saját mintáival kísérletezzen. A csúszka vezérli a sebességet, a generálás pedig tájékoztatja a befejezett iterációk számát.

Az állapotot tartó fő összetevő mellett külön létrehozok egy funkciót, amely a tábla összes cellájának állapotát a semmiből generálja, egy komponenst a tábla rácsához és egy másikat a csúszkához.

Az App.js beállítása

Először importáljuk a React és a React.Component elemeket a „react” -ből. Ezután állapítsa meg, hogy a táblarács hány sort és oszlopot tartalmaz. 40-gyel megyek 60-mal, de nyugodtan játszhatok különböző számokkal. Ezután jöjjön a fent leírt külön függvény és függvénykomponensek (vegye figyelembe a nagybetűs első betűt), valamint az állapotot és módszereket tartó osztálykomponens, beleértve a renderelést is. Végül exportáljuk a fő összetevőt 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;

Új tábla cellájának állapotának generálása

Mivel tudnunk kell, hogy a status minden cella és a 8 szomszédja minden iterációban hozzunk létre egy függvényt, amely visszaadja egy tömb tömb egyes cellákat tartalmazó logikai értékeket. A fő tömb tömbjeinek száma meg fog egyezni a sorok számával, és az egyes tömbökön belüli értékek száma meg fog egyezni az oszlopok számával. Tehát minden logikai érték az egyes cellák állapotát fogja képviselni, „élve” vagy „halva”. A függvény paramétere alapértelmezés szerint kevesebb, mint 30% az életben maradás esélye, de szabadon kísérletezhetett más számokkal.

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

A táblarács létrehozása

Határozzunk meg egy olyan funkciókomponentust, amely létrehozza a táblarácsot és hozzárendeli egy változóhoz. A függvény megkapja a teljes tábla állapotának állapotát és egy olyan módszert, amely lehetővé teszi a felhasználók számára, hogy az egyes cellák állapotát kellékként váltsák. Ez a módszer a fő komponensen van meghatározva, ahol az alkalmazás összes állapota megmarad.

Minden cellát egy tábla képvisel, és rendelkezik egy className attribútummal, amelynek értéke függ a megfelelő tábla cellájának logikai értékétől. A cellára kattintó játékos azt eredményezi, hogy a metódus átadásra kerül, mivel a cellát meghívják argumentumként a cella sorának és oszlopának helyére.

Nézze meg a Lifting State Up című részt, ha további információt szeretne kapni a módszerekről, és ne felejtse el hozzáadni a kulcsokat.

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

A sebességcsúszka létrehozása

Ez a funkciókomponens létrehoz egy csúszkát, amely lehetővé teszi a játékosok számára az iterációk sebességének megváltoztatását. Megkapja az aktuális sebesség állapotát és egy módszert a sebességváltozás kezelésére támaszként. Kipróbálhat különböző minimum, maximum és step értékeket. A sebességváltozás azt eredményezi, hogy a kellékként átadott módszert argumentumként a kívánt sebességgel hívják meg.

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

Fő alkotóelem

Mivel az alkalmazás állapotát tartalmazza, tegyük osztálykomponenssé. Ne feledje, hogy nem a Hooks-ot használom, a React 16.8 új kiegészítését, amely lehetővé teszi az állapot és más React-funkciók használatát osztályírás nélkül. Inkább a kísérleti nyilvános osztálymezők szintaxisát használom, ezért nem kötöm le a metódusokat a konstruktoron belül.

Bontsuk fel.

Állapot

Az állapotot objektumként definiálom, megadva a tábla állapotának, a generálás számának, a futó vagy leállított játéknak és az iterációk sebességének tulajdonságait. Amikor a játék elkezdődik, a tábla celláinak állapota megegyezik azzal, amelyet a hívás visszaadott az új tábla állapotot generáló függvényre. A generálás 0-nál kezdődik, és a játék csak a felhasználó döntése után fog futni. Az alapértelmezett sebesség 500ms.

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

Run / Stop gomb

Funkció, amely a játék állapotától függően egy másik gombelemet ad vissza: fut vagy leáll.

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

Tiszta és új tábla

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.