美文网首页
react学写棋类游戏

react学写棋类游戏

作者: 水落斜阳 | 来源:发表于2018-09-25 15:44 被阅读12次

    安装

    使用Create React App,最好的方式,但是必须node>6的版本
    全局安装

    npm install -g create-react-app
    
    create-react-app my-app
    cd my-app
    npm start
    
    1. 删除掉生成项目中 src/ 文件夹下的所有文件。

    2. src/ 文件夹下新建一个名为 index.css 的文件并拷贝 这里的 CSS 代码 到文件中。

    3. src/ 文件夹下新建一个名为 index.js 的文件并拷贝 这里的 JS 代码 到文件中, 并在此文件的最开头加上下面几行代码:

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    

    接下来通过命令行在你的项目目录下运行 npm start 命令并在浏览器中打开 http://localhost:3000 你就能够看到空的井字棋棋盘了

    开始编码

    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    
    
    class Square extends React.Component {
        render() {
            return (
                <button className="square">
                    {/* TODO */}
                </button>
            );
        }
    }
    
    class Board extends React.Component {
        renderSquare(i) {
            return <Square />;
        }
    
        render() {
            const status = 'Next player: X';
    
            return (
                <div>
                    <div className="status">{status}</div>
                    <div className="board-row">
                        {this.renderSquare(0)}
                        {this.renderSquare(1)}
                        {this.renderSquare(2)}
                    </div>
                    <div className="board-row">
                        {this.renderSquare(3)}
                        {this.renderSquare(4)}
                        {this.renderSquare(5)}
                    </div>
                    <div className="board-row">
                        {this.renderSquare(6)}
                        {this.renderSquare(7)}
                        {this.renderSquare(8)}
                    </div>
                </div>
            );
        }
    }
    
    class Game extends React.Component {
        render() {
            return (
                <div className="game">
                    <div className="game-board">
                        <Board />
                    </div>
                    <div className="game-info">
                        <div>{/* status */}</div>
                        <ol>{/* TODO */}</ol>
                    </div>
                </div>
            );
        }
    }
    
    // ========================================
    
    ReactDOM.render(
        <Game />,
        document.getElementById('root')
    );
    

    讲得更具体一点,我们现在有3个组件:

    Square
    Board
    Game
    Square 组件代表一个单独的 <button>,Board 组件包含了9个squares,也就是棋盘的9个格子。Game 组件则为我们即将要编写的代码预留了一些位置。现在这几个组件都是不具备任何的交互功能的

    通过 Props 传递数据

    我们先来试着从 Board 组件传递一些数据到 Square 组件。

    在 Board 组件的 renderSquare 方法中,我们将代码改写成下面这样,传递一个名为 value 的 prop 到 Square 当中

    class Board extends React.Component {
      renderSquare(i) {
        return <Square value={i} />;
      }
    
    class Square extends React.Component {
      render() {
        return (
          <button className="square">
            {this.props.value}
          </button>
        );
      }
    }
    
    image.png

    给组件添加交互功能

    接下来我们试着让棋盘的每一个格子在点击之后能落下一颗 “X” 作为棋子。我们试着把 render() 方法修改为如下内容:
    现在你试着点击一下某个格子,在浏览器里就会弹出一个警示框。
    在 React 组件的构造方法 constructor 当中,你可以通过 this.state 为该组件设置自身的状态数据。我们来试着把棋盘格子变化的数据储存在组件的 state 当中吧:

    首先,我们为组件添加构造函数并初始化 state:

    lass Square extends React.Component {
        constructor(){
            super()
            this.state = {
                value:null,
            }
        },
        render() {
            return (
                <button className="square" onClick={()=>alert('click')}>
                    {this.props.value}
                </button>
            );
        }
    }
    

    现在我们试着通过点击事件触发 state 的改变来更新棋盘格子显示的内容:

    将 <button> 当中的 this.props.value 替换为 this.state.value 。
    将 () => alert() 方法替换为 () => this.setState({value: 'X'}) 。
    现在我们的 <button> 标签就变成了下面这样:

    class Square extends React.Component {
        constructor(){
            super()
            this.state = {
                value:null,
            }
        }
        render() {
            return (
                <button className="square" onClick={()=>this.setState({value: 'X'})}>
                    {this.state.value}
                </button>
            );
        }
    }
    

    每当 this.setState 方法被触发时,组件都会开始准备更新,React 通过比较状态的变化来更新组件当中跟随数据改变了的内容。当组件重新渲染时,this.state.value 会变成 'X' ,所以你也就能够在格子里看到 X 的字样。

    现在你试着点击任何一个格子,都能够看到 X 出现在格子当中。

    开发工具

    ChromeFirefox 上安装 React 开发者工具可以让你在浏览器的开发控制台里看到 React 渲染出来的组件树。
    你同样可以在开发工具中观察到各个组件的 props 和 state.
    安装好开发工具之后,你可以在任意页面元素上面右键选择 “审查元素”,之后在弹出的控制台选项卡最右边会看到名为 React 的选项卡。

    状态提升

    我们现在已经编写好了井字棋游戏最基本的可以落子的棋盘。但是现在应用的状态是独立保存在棋盘上每个格子的 Square 组件当中的。想要编写出来一个真正能玩的游戏,我们还需要判断哪个玩家获胜,并在 X 或 O 两方之间交替落子。想要检查某个玩家是否获胜,需要获取所有9个格子上面的棋子分布的数据,现在这些数据分散在各个格子当中显然是很麻烦的。
    最好的解决方式是直接将所有的 state 状态数据存储在 Board 组件当中。之后 Board 组件可以将这些数据传递给各个 Square 组件
    当你遇到需要同时获取多个子组件数据,或者两个组件之间需要相互通讯的情况时,把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中的状态数据就能够更方便地交流共享了
    像这种提升组件状态的情形在重构 React 组件时经常会遇到。我们趁现在也就来实践一下,在 Board 组件的构造函数中初始化一个包含9个空值的数组作为状态数据,并将这个数组中的9个元素分别传递到对应的9个 Square 组件当中。

     constructor(){
            super()
            this.state = {
                squartes: Array(9).fill(null)
            }
        }
    

    现在传入的都是空数据,井字棋游戏进行会把数组填充成类似下面这样:

    [
      'O', null, 'X',
      'X', 'X', 'O',
      'O', null, null,
    ]
    

    我们在 value 属性中传递对应 state 数组元素的值。

        renderSquare(i) {
            return <Square value={this.state.squares[i]}/>;
        }
    

    现在我们需要修改当某个格子被点击时触发的事件处理函数。现在每个格子当中的数据是存储在整个棋盘当中的,所以我们就需要通过一些方法,让格子组件能够修改整个棋盘组件数据的内容。因为每个组件的 state 都是它私有的,所以我们不可以直接在格子组件当中进行修改。

    惯例的做法是,我们再通过 props 传递一个父组件当中的事件处理函数到子组件当中。也就是从 Board 组件里传递一个事件处理函数到 Square 当中,我们来把 renderSquare 方法改成下面这样

    
        renderSquare(i) {
            return <Square
                      value={this.state.squares[i]}
                      onClick={()=>this.handleClick(i)}
                    />;
        }
    

    注意到我们在写代码的时候,在各个属性直接换了行,这样可以改善我们代码的可读性。并且我们在 JSX 元素的最外层套上了一小括号,以防止 JavaScript 代码在解析时自动在换行处添加分号。

    现在我们从 Board 组件向 Square 组件中传递两个 props 参数:value 和 onClick. onClick 里传递的是一个之后在 Square 组件中能够触发的方法函数。我们动手来修改代码吧:

    将 Square 组件的 render 方法中的 this.state.value 替换为 this.props.value 。
    将 Square 组件的 render 方法中的 this.setState() 替换为 this.props.onClick() 。
    删掉 Square 组件中的 构造函数 constructor ,因为它现在已经不需要保存 state 了。
    进行如上修改之后,代码会变成下面这样:

    class Square extends React.Component {
      render() {
        return (
          <button className="square" onClick={() => this.props.onClick()}>
            {this.props.value}
          </button>
        );
      }
    }
    

    现在每次格子被点击时就会触发传入的 onClick 方法。我们来捋一下这其中发生了什么:

    1.添加 onClick 属性到内置的 DOM 元素 <button> 上让 React 开启了对点击事件的监听。
    2.当按钮,也就是棋盘格子被点击时, React 会调用 Square 组件的 render() 方法中的 onClick 事件处理函数。
    3.事件处理函数触发了传入其中的 this.props.onClick() 方法。这个方法是由 Board 传递给 Square 的。
    4.Board 传递了 onClick={() => this.handleClick(i)} 给 Square,所以当 Square 中的事件处理函数触发时,其实就是触发的 Board 当中的 this.handleClick(i) 方法。
    5.现在我们还没有编写 handleClick() 方法,所以代码还不能正常工作。
    注意到这里的 onClick 事件是 React 组件当中所特有的。不过 handleClick 这些方法则只是我们编写事件处理函数时候的命名习惯。

    现在我们来动手编写 handleClick 方法吧:

    handleClick(i) {
            //slice() 方法可从已有的数组中返回选定的元素 创建和state.squares一样的数组
           const squares = this.state.squares.slice()
            squares[i] = 'X'
            this.setState({squares: squares})
        }
    

    我们使用了 .slice() 方法来将之前的数组数据浅拷贝到了一个新的数组中,而不是修改已有的数组。你可以在 这个章节 来了解为什么不可变性在 React 当中的重要性。

    现在你点击棋盘上的格子应该就能够正常落子了。而且状态数据是统一保管在棋盘组件 Board 当中的。你应该注意到了,当事件处理函数触发棋盘父组件的状态数据改变时,格子子组件会自动重新渲染。

    现在格子组件 Square 不再拥有自身的状态数据了。它从棋盘父组件 Board 接受数据,并且当自己被点击时通知触发父组件改变状态数据,我们称这类的组件为 受控组件

    为什么不可变性在React当中非常重要

    在上一节内容当中,我们通过使用 .slice() 方法对已有的数组数据进行了浅拷贝,以此来防止对已有数据的改变。接下来我们稍微了解一下为什么这样的操作是一种非常重要的概念。

    改变应用数据的方式一般分为两种。第一种是直接修改已有的变量的值。第二种则是将已有的变量替换为一个新的变量。

    var player = {score: 1, name: 'Jeff'};
    player.score = 2;
    // Now player is {score: 2, name: 'Jeff'}
    

    替换修改数据

    var player = {score: 1, name: 'Jeff'};
    
    var newPlayer = Object.assign({}, player, {score: 2});
    // Now player is unchanged, but newPlayer is {score: 2, name: 'Jeff'}
    
    // 或者使用最新的对象分隔符语法,你可以这么写:
    // var newPlayer = {...player, score: 2};
    

    函数定义组件

    我们刚才已经去掉了 Square 的构造函数,事实上,更进一步的,React 专门为像 Square 组件这种只有 render 方法的组件提供了一种更简便的定义组件的方法: 函数定义组件 。只需要简单写一个以 props 为参数的 function 返回 JSX 元素就搞定了。

    下面我们以函数定义的方式重写 Square 组件:

    function Square(props) {
      return (
        <button className="square" onClick={props.onClick}>
          {props.value}
        </button>
      );
    }
    

    记得把所有的 this.props 替换成参数 props. 我们应用中的大部分简单组件都可以通过函数定义的方式来编写,并且 React 在将来还会对函数定义组件做出更多优化。

    另外一部分简化的内容则是事件处理函数的写法,这里我们把 onClick={() => props.onClick()} 直接修改为 onClick={props.onClick} , 注意不能写成 onClick={props.onClick()} 否则 props.onClick 方法会在 Square 组件渲染时被直接触发而不是等到 Board 组件渲染完成时通过点击触发,又因为此时 Board 组件正在渲染中(即 Board 组件的 render() 方法正在调用),又触发 handleClick(i) 方法调用 setState() 会再次调用 render() 方法导致死循环。

    轮流落子

    很明显现在我们点击棋盘只后落子的只有 X 。 下面我们要开发出 X 和 O 轮流落子的功能。

    我们将 X 默认设置为先手棋:

    class Board extends React.Component {
      constructor() {
        super();
        this.state = {
          squares: Array(9).fill(null),
          xIsNext: true,
        };
      }
    

    接下来,我们每走一步棋,都需要切换 xIsNext 的值以此来实现轮流落子的功能,接下来在 handleClick 方法中添加修改 xIsNext 的语句

      handleClick(i) {
        const squares = this.state.squares.slice();
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
          squares: squares,
          xIsNext: !this.state.xIsNext,
        });
      }
    

    到这里我们就实现了 X 和 O 轮流落子的效果了。我们再到 render 方法里添加一点内容来显示当前执子的一方

    render() {
        const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    
        return (
          // the rest has not changed
    

    判断赢家

    接下来我们来编写判断游戏获胜方的代码,首先在你的代码里添加下面这个判断获胜方的算法函数

     calculateWinner(squares) {
      const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
      ];
      for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
          return squares[a];
        }
      }
      return null;
    }
    

    然后你就可以在 Board 组件的 render 方法里调用它,来检查是否有人获胜并根据判断显示出 “Winner: [X/O]” 来表示获胜方。

    将 render 中的 status 替换为如下内容:

    render() {
        const winner = calculateWinner(this.state.squares);
        let status;
        if (winner) {
          status = 'Winner: ' + winner;
        } else {
          status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
    
        return (
          // the rest has not changed
    

    继续完善游戏规则,我们在 handleClick 里添加当前方格内已经落子/有一方获胜就就无法继续落子的判断逻辑

     handleClick(i) {
           const squares = this.state.squares.slice()
            if(this.calculateWinner(squares) || squares[i]) return
            squares[i] = this.state.xIsNext ? 'X' : 'O'
            this.setState({
                squares: squares,
                xIsNext: !this.state.xIsNext,
            })
        }
    

    保存历史记录

    接下来我们一起来实现保存棋局每一步的历史记录的功能。在现有的代码逻辑中,我们已经是在每走一步棋之后就返回一个新的 squares 数组了,所以想要保存历史记录也非常简单。

    我们计划通过一个数组对象来保存每一步的状态数据

    我们期望在顶层的 Game 组件中展示一个链接每一步历史记录的列表。所以就像我们之前将 state 从 Square 组件提升到 Board 中一样,现在我们把 Board 中的状态数据再提升到 Game 组件中来。

    首先在 Game 组件的构造函数中初始化我们需要的状态数据

    class Game extends React.Component {
        constructor(){
            super()
            this.state = {
                history: [{
                    squares: Array(9).fill(null),
                }],
                xIsNext: true,
            };
        }
    
        render() {
            return (
                <div className="game">
                    <div className="game-board">
                        <Board />
                    </div>
                    <div className="game-info">
                        <div>{/* status */}</div>
                        <ol>{/* TODO */}</ol>
                    </div>
                </div>
            );
        }
    }
    

    接下来,就好像我们之前对 Square 组件的操作一样。我们将 Board 中的状态数据全都移动到 Game 组件当中。Board 现在通过 props 获取从 Game 传递下来的数据和事件处理函数。

    1.删除 Board 的构造方法 constructor 。
    2.把 Board 的 renderSquare 方法中的 this.state.squares[i] 替换为 this.props.squares[i] 。
    3.把 Board 的 renderSquare 方法中的 this.handleClick(i) 替换为 this.props.onClick(i) 。

    将calculateWinner提到外面公用的

    Game 组件的 render 方法现在则要负责获取最近一步的历史记录(当前棋局状态),以及计算出游戏进行的状态(是否有人获胜)。

      render() {
        const history = this.state.history;
        const current = history[history.length - 1];
        const winner = calculateWinner(current.squares);
    
        let status;
        if (winner) {
          status = 'Winner: ' + winner;
        } else {
          status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={current.squares}
                onClick={(i) => this.handleClick(i)}
              />
    
            </div>
            <div className="game-info">
              <div>{status}</div>
              <ol>{/* TODO */}</ol>
            </div>
          </div>
        );
      }
    

    既然现在由 Game 组件负责渲染游戏状态,我们可以直接把 Board 组件的 render 方法里的 <div className="status">{status}</div> 删掉:

      render() {
        return (
          <div>
            <div className="board-row">
              {this.renderSquare(0)}
              {this.renderSquare(1)}
              {this.renderSquare(2)}
            </div>
            <div className="board-row">
              {this.renderSquare(3)}
              {this.renderSquare(4)}
              {this.renderSquare(5)}
            </div>
            <div className="board-row">
              {this.renderSquare(6)}
              {this.renderSquare(7)}
              {this.renderSquare(8)}
            </div>
          </div>
        );
      }
    

    之后,我们需要将 Board 组件里的 handleClick 移动到 Game 组件当中。你可以直接把它剪切粘贴过来。

    不过为了实现我们新的历史记录的功能,还需要稍微修改一下我们的代码,让 handleClick 在每次触发时,添加当前的棋局状态数据到 histroy 当中

     handleClick(i) {
        const history = this.state.history;
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
          return;
        }
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
          history: history.concat([{
            squares: squares
          }]),
          xIsNext: !this.state.xIsNext,
        });
      }
    

    代码编写到这一步,Board 组件当中现在应该只有 renderSquare 和 render 两个方法;应用状态 state 以及事件处理函数现在都定义在 Game 组件当中。
    完成代码

    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    
    
    function Square(props) {
        return (
            <button className="square" onClick={props.onClick}>
                {props.value}
            </button>
        )
    }
    
    function calculateWinner(squares) {
        const lines = [
            [0, 1, 2],
            [3, 4, 5],
            [6, 7, 8],
            [0, 3, 6],
            [1, 4, 7],
            [2, 5, 8],
            [0, 4, 8],
            [2, 4, 6],
        ];
        for (let i = 0; i < lines.length; i++) {
            const [a, b, c] = lines[i];
            if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
                return squares[a];
            }
        }
        return null;
    }
    
    class Board extends React.Component {
    
        renderSquare(i) {
            return <Square
                      value={this.props.squares[i]}
                      onClick={()=>this.props.onClick(i)}
                    />;
        }
    
        render() {
            return (
                <div>
                    <div className="board-row">
                        {this.renderSquare(0)}
                        {this.renderSquare(1)}
                        {this.renderSquare(2)}
                    </div>
                    <div className="board-row">
                        {this.renderSquare(3)}
                        {this.renderSquare(4)}
                        {this.renderSquare(5)}
                    </div>
                    <div className="board-row">
                        {this.renderSquare(6)}
                        {this.renderSquare(7)}
                        {this.renderSquare(8)}
                    </div>
                </div>
            );
        }
    }
    
    class Game extends React.Component {
        constructor(props){
            super(props)
            this.state = {
                history: [{
                    squares: Array(9).fill(null),
                }],
                xIsNext: true,
            };
        }
    
        handleClick(i) {
            const history = this.state.history
            const current = history[history.length - 1]
            const squares = current.squares.slice()
            if (calculateWinner(squares) || squares[i]) {
                return;
            }
            squares[i] = this.state.xIsNext ? 'X' : 'O';
            this.setState({
                history:history.concat([{
                    squares: squares
                }]),
                xIsNext: !this.state.xIsNext
            })
    
        }
    
    
        render() {
            const history = this.state.history
            const current = history[history.length - 1]
            const winner = calculateWinner(current.squares)
    
            let status
            if(winner) {
                status = 'Winner: ' + winner
            }else {
                status = 'Nest player: ' + (this.state.xIsNext ? 'X' : 'O')
            }
    
            return (
                <div className="game">
                    <div className="game-board">
                        <Board
                            squares={current.squares}
                            onClick={(i) => this.handleClick(i)}
                        />
                    </div>
                    <div className="game-info">
                        <div>{ status }</div>
                        <ol>{/* TODO */}</ol>
                    </div>
                </div>
            );
        }
    }
    
    ReactDOM.render(
        <Game />,
        document.getElementById('root')
    );
    

    展示每步历史记录链接

    现在我们来试着展示每一步棋的历史记录链接。在教程的开始我们提到过,React 元素事实上都是 JS 当中的对象,我们可以把元素当作参数或定义到变量中使用。在 React 当中渲染多个重复的项目时,我们一般都以数组的方式传递 React 元素。最基本的方法是使用数组的 map 方法,我们试着来修改 Game 组件的 render 方法吧:

      render() {
        const history = this.state.history;
        const current = history[history.length - 1];
        const winner = calculateWinner(current.squares);
    
        const moves = history.map((step, move) => {
          const desc = move ?
            'Move #' + move :
            'Game start';
          return (
            <li>
              <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
            </li>
          );
        });
    
        let status;
        if (winner) {
          status = 'Winner: ' + winner;
        } else {
          status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={current.squares}
                onClick={(i) => this.handleClick(i)}
              />
            </div>
            <div className="game-info">
              <div>{status}</div>
              <ol>{moves}</ol>
            </div>
          </div>
        );
      }
    

    对于每一步的历史记录,我们都创建了一个带 <a> 链接的 <li> 列表项。目前链接还没指向任何地方,别着急我们后面会继续实现切换至对应棋步的功能。有了我们现有的代码,已经能渲染出一个列表了,不过你留心的话,就会在控制台看到警告:

    Keys

    当你在 React 当中渲染列表项时,React 都会试着存储对应每个单独项的相关信息。如果你的组件包含 state 状态数据,那么这些状态数据必须被排序,不管你组件是怎么编写实现的。

    当你想要更新这些列表项时,React 必须能够知道是那一项改变了。这样你才能够在列表中增删改查项目。

    比方说下面这个例子,从前一个表单

    <li>Alexa: 7 tasks left</li>
    <li>Ben: 5 tasks left</li>
    

    变成下面这个表单

    <li>Ben: 9 tasks left</li>
    <li>Claudia: 8 tasks left</li>
    <li>Alexa: 5 tasks left</li>
    

    你用肉眼可以很轻易地分辨,Alexa 被移到了最后,多出来一个 Claudia。可是 React 只是电脑里运行地程序,它无从知晓这些改变。所以我们必须为列表中的每一项添加一个 key 作为唯一的标识符。标识符必须是唯一的,比方说刚才这个例子中的 alexa, ben, claudia 就可以用来做标识符。更普遍的一种情况,假如我们的数据是从数据库获取的话,表单每一项的 ID 就很适合当作它的 key :

    <li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
    

    强烈建议你在渲染列表项时添加 keys 值。 假如你没有现成可以作为唯一 key 值的数据使用的话,你可能需要考虑重新组织设计你的数据了。

    实现时间旅行

    在我们的棋步的列表中,已经有了现成的唯一 key 值,也就是每一次 move 的记录值。我们通过 <li key={move}> 来添加一下。

     const moves = history.map((step, move) => {
          const desc = move ?
            'Move #' + move :
            'Game start';
          return (
            <li key={move}>
              <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
            </li>
          );
        });
    

    在上面的代码中,我们同样为每一个 <a> 添加了一个 jumpTo 方法,用来将棋盘的状态切换至对应的棋步时的状态。接下来我们来着手实现这个方法:

    首先在 Game 组件的初始状态中多设置一项 stepNumber: 0

    class Game extends React.Component {
      constructor() {
        super();
        this.state = {
          history: [{
            squares: Array(9).fill(null),
          }],
          stepNumber: 0,
          xIsNext: true,
        };
      }
    

    接下来,我们正是编写 jumpTo 来切换 stepNumber 的值。根据游戏的逻辑,与此同时我们还需要修改 xIsNext 来保证对应棋步时,执子的一方是能对应上的。我们可以根据棋步计算出是谁在执子。

    我们把 jumpTo 编写在 Game 组件中:

      jumpTo(step) {
        this.setState({
          stepNumber: step,
          xIsNext: (step % 2) ? false : true,
        });
      }
    

    接下来,我们在 handleClick 方法中对 stepNumber 进行更新,添加 stepNumber: history.length 保证每走一步 stepNumber 会跟着改变:

     handleClick(i) {
        const history = this.state.history.slice(0, this.state.stepNumber + 1);
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
          return;
        }
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
          history: history.concat([{
            squares: squares
          }]),
          stepNumber: history.length,
          xIsNext: !this.state.xIsNext,
        });
      }
    

    现在你可以直接在 Game 组件的 render 方法里根据当前的棋步获取对应的棋局状态了:

    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);
    

    查看此步完整代码示例。

    现在你试着点击每一步棋记录的列表中的一项,棋盘会自动更新到对应项时的棋局状态

    总结,招帮官网文档来的,算是自我学习的一种方式

    相关文章

      网友评论

          本文标题:react学写棋类游戏

          本文链接:https://www.haomeiwen.com/subject/kygkoftx.html