使用react编写组件_如何用React编写人生游戏

使用react编写组件

by Pablo Regen

通过帕勃罗·雷根(Pablo Regen)

The Game of Life involves a two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, alive or dead. At each step, every cell interacts with its eight adjacent neighbors by following a simple set of rules resulting in births and deaths.

生命游戏涉及正方形细胞的二维正交网格,每个网格都处于两种可能的存活或死亡状态之一。 在每个步骤中,每个单元都遵循一组简单的规则(导致出生和死亡)与其八个相邻邻居进行交互。

It’s a zero-player game. Its evolution is determined by its initial state, requiring no further input from players. One interacts with the game by creating an initial configuration and observing how it evolves, or, for advanced players, by creating patterns with particular properties.

这是一个零玩家游戏。 它的演变取决于其初始状态,不需要玩家的进一步投入。 通过创建初始配置并观察游戏的发展方式,或者对于高级玩家,通过创建具有特定属性的模式来与游戏进行交互。

规则 (Rules)
  1. Any live cell with fewer than two live neighbors dies, as if by underpopulation

    任何具有少于两个活邻居的活细胞都会死亡,就像是人口不足
  2. Any live cell with two or three live neighbors lives on to the next generation

    任何有两个或三个活邻居的活细胞都可以存活到下一代
  3. Any live cell with more than three live neighbors dies, as if by overpopulation

    任何具有三个以上活邻居的活细胞都会死亡,就好像是人口过剩
  4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction

    具有正好三个活邻居的任何死细胞都变成活细胞,就像通过繁殖

Although the game can be perfectly coded with vanilla JavaScript, I was happy to go through the challenge with React. So let’s start.

尽管可以使用原始JavaScript完美地编写游戏代码,但我还是很高兴通过React迎接挑战。 因此,让我们开始吧。

设置React (Setting up React)

There are several ways to set up React, but if you are new to it I recommend checking out the Create React App docs and github, as well as the detailed React overview by Tania Rascia.

有几种设置React的方法,但是,如果您不熟悉它,我建议您查看Create React App 文档github ,以及Tania Rascia的详细React概述。

设计游戏 (Designing the game)

The main image at the top is my implementation of the game. The board grid containing light (alive) and dark (dead) cells displays the game’s evolution. The controllers allow you to start/stop, go one step at a time, set up a new board or clear it to experiment with your own patterns by clicking on the individual cells. The slider controls the speed, and generation informs the number of completed iterations.

顶部的主要图像是我对游戏的实现。 包含亮(活动)和暗(死)单元的棋盘网格显示了游戏的发展。 控制器允许您启动/停止,一次只走一步,设置新板或清除它,以通过单击各个单元来试验自己的模式。 滑块控制速度,并且生成通知完成的迭代次数。

In addition to the main component holding the state, I’ll separately create a function to generate all board’s cell status from scratch, a component for the board grid and another one for the slider.

除了保持状态的主要组件之外,我还将单独创建一个函数以从头开始生成所有板的单元状态,一个用于板网格的组件,另一个用于滑块。

设置App.js (Setting up App.js)

First, let’s import React and React.Component from “react”. Then establish how many rows and columns the board grid has. I go with 40 by 60 but feel free to play with different numbers. Then come the separate function and function components (notice the capitalized first letter) described above as well as the class component holding the state and methods, including the render one. Finally let’s export the main component App.

首先,让我们从“ react”导入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;
生成新板的单元状态 (Generating a new board’s cell status)

Since we need to know the status of each cell and its 8 neighbors for each iteration, let’s create a function that returns an array of arrays each containing cells with boolean values. The number of arrays within the main array will match the number of rows, and the number of values within each of these arrays will match the number of columns. So each boolean value will represent the state of each cell, “alive” or “dead”. The function’s parameter defaults to less than 30% chance of being alive, but fell free to experiment with other numbers.

因为我们需要知道每个细胞及其邻居8每次迭代的状态,让我们创建一个返回数组的数组与布尔值各包含细胞的功能。 主数组中的数组数将与行数匹配,并且每个数组中的值数将与列数匹配。 因此,每个布尔值将代表每个单元格的状态,“活动”或“死亡”。 该函数的参数默认情况下存活率不到30%,但可以尝试使用其他数字。

const newBoardStatus = (cellStatus = () => Math.random() < 0.3) => {
    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, ...]
...
*/
生成板格 (Generating the board grid)

Let’s define a function component that creates the board grid and assigns it to a variable. The function receives the state of the whole board status and a method that allows users to toggle the status of individual cells as props. This method is defined on the main component where all the state of the application is held.

让我们定义一个功能组件,该组件创建电路板网格并将其分配给变量。 该功能接收整个面板状态的状态,以及一种允许用户将单个单元格的状态切换为道具的方法。 在保存应用程序所有状态的主要组件上定义此方法。

Each cell is represented by a table’s <td> and has a className attribute whose value depends on the boolean value of the corresponding board cell. The player clicking on a cell results in the method passed as props being called with the cell’s row and column location as argument.

每个单元格由表的<td>表示,并具有className属性,该属性的值取决于相应板单元格的布尔值。 玩家单击一个单元格会导致调用作为道具传递的方法,并以该单元格的行和列位置作为参数。

Check out Lifting State Up for additional info on passing methods as props, and don’t forget to add the keys.

请查看Lifting State Up,以获取有关通过方法作为道具的其他信息,并且不要忘记添加key

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(
                <td
                    key={`${r},${c}`}
                    className={boardStatus[r][c] ? 'alive' : 'dead'}
                    onClick={() => handleClick(r,c)}
                />
            );
        }
        tr.push(<tr key={r}>{td}</tr>);
    }
    return <table><tbody>{tr}</tbody></table>;
};
创建速度滑块 (Creating the speed slider)

This function component creates a slider to allow players change the speed of iterations. It receives the state of the current speed and a method to handle the speed change as props. You can try different minimum, maximum and step values. A speed change results in the method passed as props being called with the desired speed as argument.

该功能组件创建一个滑块,以允许玩家更改迭代速度。 它接收当前速度的状态以及作为道具处理速度变化的方法。 您可以尝试不同的最小值,最大值和步长值。 速度变化会导致以所需速度作为参数调用作为props传递的方法。

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

    return (
        <input
            type='range'
            min='50'
            max='1000'
            step='50'
            value={speed}
            onChange={handleChange}
        />
    );
};

主要成分 (Main component)

Since it contains the state of the application let’s make it a class component. Note that I’m not using Hooks, a new addition in React 16.8 that let you use state and other React features without writing a class. I prefer to use the experimental public class fields syntax, so I don’t bind the methods within the constructor.

由于它包含应用程序的状态,因此我们将其设为类组件。 请注意,我没有使用Hooks ,它是React 16.8中的新增功能,它使您无需编写类即可使用状态和其他React功能。 我更喜欢使用实验性的公共类字段语法 ,因此我不会在构造函数中绑定方法。

Let’s dissect it.

让我们对其进行剖析。

(State)

I define the state as an object with the properties for the board status, number of generation, game running or stopped and the speed of the iterations. When the game starts, the status of the board’s cells will be the one returned by the call to the function that generates a new board status. Generation starts at 0 and the game will only run after the user decides. The default speed is 500ms.

我将状态定义为对象,并具有棋盘状态,生成次数,游戏运行或停止以及迭代速度的属性。 游戏开始时,开发板单元的状态将是对生成新开发板状态的函数的调用返回的状态。 生成从0开始,只有在用户决定后游戏才会运行。 默认速度为500毫秒。

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

    // Other methods ...

}
运行/停止按钮 (Run/Stop button)

Function that returns a different button element depending on the state of the game: running or stopped.

根据游戏状态返回不同按钮元素的函数:运行还是停止。

class App extends Component {
    state = {...};

    runStopButton = () => {
        return this.state.isGameRunning ?
        <button type='button' onClick={this.handleStop}>Stop</button> :
        <button type='button' onClick={this.handleRun}>Start</button>;
    }
    
    // Other methods ...
}
清晰新板 (Clear and new board)

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.

处理玩家要求的方法是从一个新的随机棋盘的棋盘状态开始或者完全清除棋盘,这样他们就可以通过切换单个棋盘的状态进行试验。 它们之间的区别在于,一个清除木板的状态将所有单元的状态设置为false,而另一个不将任何参数传递给newBoardStatus方法,因此默认情况下每个单元的状态均变为随机布尔值。

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.

我们需要一种方法来处理玩家的请求,以切换各个单元的状态,这对于直接在板上试验自定义模式很有用。 每当玩家单击某个单元格时,BoardGrid组件就会调用它。 它通过调用一个函数并将其先前的状态作为参数传递来设置板状态的状态

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.

该功能会深度克隆前一块板的状态,以避免在下一行更新单个单元时通过引用对其进行修改。 (使用const clonedBoardStatus = […boardStatus]会修改原始状态,因为在复制数组时Spread语法有效地深入了一层,因此,它可能不适合复制多维数组 。请注意, JSON.parse(JSON.stringify(obj))在克隆的对象使用函数时不起作用)。 该功能最终返回更新的克隆板状态,有效地更新了板的状态。

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.

函数(“ nextStep”)定义了两个变量:板状态和深度克隆的板状态。 然后,无论何时调用某个函数,函数都会为单个单元格计算值为true的邻居(板内)的数量。 根据规则,每个单元格不必计算四个以上的真实邻居。 最后,根据规则,它更新克隆的板的单个单元状态并返回克隆的板状态,该状态在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 < totalBoardRows && y >= 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 < 2 || 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.

这3个方法仅设置speed和isGameRunning属性的状态值。

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.

然后,在componentDidUpdate Lifecycle方法中,让我们根据值的不同组合清除和/或设置计时器。 计时器以指定的速度间隔安排对handleStep方法的调用。

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.

App组件中的最后一个方法返回所需的结构和要显示的页面信息。 由于状态属于App组件,因此我们将状态和方法传递给需要它们作为道具的组件。

class App extends Component {
    // All previous methods ...

    render() {
        const { boardStatus, isGameRunning, generation, speed } = this.state;

        return (
            <div>
                <h1>Game of Life</h1>
                <BoardGrid boardStatus={boardStatus} onToggleCellStatus={this.handleToggleCellStatus} />
                <div className='flexRow upperControls'
                    <span>
                        {'+ '}
                        <Slider speed={speed} onSpeedChange={this.handleSpeedChange} />
                        {' -'}
                    </span>
                    {`Generation: ${generation}`}
                </div>
                <div className='flexRow lowerControls'>
                    {this.runStopButton()}
                    <button type='button' disabled={isGameRunning} onClick={this.handleStep}>Step</button>
                    <button type='button' onClick={this.handleClearBoard}>Clear Board</button>
                    <button type='button' onClick={this.handleNewBoard}>New Board</button>
                </div>
            </div>
        );
    }
}
导出默认应用 (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.

最后,让我们导出默认应用程序( export default App; ),该export default App;与样式由“ index.js”从“ index.scss”中导入,然后呈现到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.

github上查看完整代码 此处 玩游戏 。 请尝试以下这些模式,或者自己创建一个有趣的模式。

Thanks for reading.

谢谢阅读。

翻译自: https://www.freecodecamp.org/news/coding-the-game-of-life-with-react-7de2385b7356/

使用react编写组件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值