Как да кодирам Играта на живота с React

Играта на живота включва двуизмерна ортогонална мрежа от квадратни клетки, всяка от които е в едно от двете възможни състояния, живи или мъртви. На всяка стъпка всяка клетка взаимодейства със своите осем съседни съседи, като следва прост набор от правила, водещи до раждания и смърт.

Това е игра с нулев играч. Неговата еволюция се определя от първоначалното състояние, което не изисква допълнителни приноси от играчите. Човек взаимодейства с играта, като създава първоначална конфигурация и наблюдава как тя се развива, или, за напреднали играчи, чрез създаване на модели с определени свойства.

Правила

  1. Всяка жива клетка с по-малко от двама живи съседи умира, сякаш от недостатъчно население
  2. Всяка жива клетка с двама или трима живи съседи живее до следващото поколение
  3. Всяка жива клетка с повече от три живи съседи умира, сякаш от пренаселеност
  4. Всяка мъртва клетка с точно трима живи съседи се превръща в жива клетка, сякаш чрез размножаване

Въпреки че играта може да бъде перфектно кодирана с ванилов JavaScript, с удоволствие преминах през предизвикателството с React. Така че нека започнем.

Настройване на React

Има няколко начина за настройване на React, но ако не сте запознати с него, препоръчвам да разгледате документите и github на Create React App , както и подробния преглед на React от Tania Rascia.

Проектиране на играта

Основното изображение в горната част е моята реализация на играта. Решетката на дъската, съдържаща светли (живи) и тъмни (мъртви) клетки, показва развитието на играта. Контролерите ви позволяват да стартирате / спирате, да преминете една по една стъпка, да настроите нова дъска или да я изчистите, за да експериментирате със собствените си модели, като щракнете върху отделните клетки. Плъзгачът контролира скоростта и генерирането информира броя на завършените итерации.

В допълнение към основния компонент, държащ състоянието, ще създам отделно функция за генериране на състоянието на всички клетки на дъската от нулата, компонент за решетката на платката и друг за плъзгача.

Настройване на App.js

Първо, нека импортираме React и React.Component от „реагирайте“. След това установете колко редове и колони има решетката на дъската. Отивам с 40 на 60, но не се колебайте да играете с различни номера. След това идват описаните по-горе отделни функция и функционални компоненти (обърнете внимание на първата буква с главни букви), както и компонентът на класа, съдържащ състоянието и методите, включително изобразяването. Накрая нека експортираме основния компонент 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;

Генериране на състояние на клетката на нов съвет

Тъй като трябва да знаем състоянието на всяка клетка и нейните 8 съседи за всяка итерация, нека създадем функция, която връща масив от масиви, всеки от които съдържа клетки с булеви стойности. Броят на масивите в основния масив ще съответства на броя на редовете, а броят на стойностите във всеки от тези масиви ще съответства на броя на колоните. Така че всяка булева стойност ще представлява състоянието на всяка клетка, „жива“ или „мъртва“. Параметърът на функцията по подразбиране е по-малък от 30% шанс да остане жив, но е свободен да експериментира с други числа.

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

Генериране на решетката на дъската

Нека дефинираме функционален компонент, който създава мрежата на дъската и я присвоява на променлива. Функцията получава състоянието на цялото състояние на борда и метод, който позволява на потребителите да превключват състоянието на отделните клетки като подпори. Този метод е дефиниран за основния компонент, където се съхранява цялото състояние на приложението.

Всяка клетка е представена от таблица и има атрибут className, чиято стойност зависи от булевата стойност на съответната клетка на борда. Играчът, щраквайки върху клетка, води до получаване на метода, предаван като подпори, извиквани като аргумент на местоположението на реда и колоната на клетката.

Проверете Lifting State Up за допълнителна информация относно методите за предаване като подпори и не забравяйте да добавите ключовете.

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

Създаване на плъзгача за скорост

Този функционален компонент създава плъзгач, за да позволи на играчите да променят скоростта на повторения. Той получава състоянието на текущата скорост и метод за обработка на промяната на скоростта като подпори. Можете да опитате различни минимални, максимални и стъпкови стойности. Промяната на скоростта води до метода, предаден като подпори, извиквани с желаната скорост като аргумент.

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

Основен компонент

Тъй като съдържа състоянието на приложението, нека го направим компонент на класа. Имайте предвид, че не използвам Hooks, ново допълнение в React 16.8, което ви позволява да използвате състояние и други функции на React, без да пишете клас. Предпочитам да използвам експерименталния синтаксис на полета за публичен клас, така че не обвързвам методите в конструктора.

Нека го разчленим.

Щат

Определям състоянието като обект със свойствата за състоянието на дъската, броя на генерирането, играта, която се изпълнява или спира и скоростта на повторенията. Когато играта започне, състоянието на клетките на дъската ще бъде това, което се връща от повикването към функцията, генерираща ново състояние на дъската. Генерацията започва от 0 и играта ще се стартира само след като потребителят реши. Скоростта по подразбиране е 500ms.

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

Бутон Run / Stop

Функция, която връща различен бутонен елемент в зависимост от състоянието на играта: работеща или спряна.

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

Ясна и нова дъска

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.