react井字棋---最全井字棋小游戏教程

上一期我们利用create-react-app搭建了好了一个react项目,这期我们通过跟随React官方教程--编写一个“井字棋"小游戏,来熟悉react的基本用法。

 

首先来看下“井字棋”的最终实现效果:

 

从演示中我们可以看到,这个游戏大致有以下功能:

1.  切换玩家;

2.  判定胜负;

3.  高亮获胜棋子;

4.  按步骤悔棋;

 

制作棋盘

 

首先我们需要制作一个棋盘。

在项目中新建一个Board.js文件:

 

 

 

import React from "react";
import "./style.css";

function Board() {
  return (
      <div className="board">
        <h1>井字棋游戏--React</h1>
        <button className="btn"></button>
        <button className="btn"></button>
        <button className="btn"></button>
        <button className="btn"></button>
        <button className="btn"></button>
        <button className="btn"></button>
        <button className="btn"></button>
        <button className="btn"></button>
        <button className="btn"></button>
      </div>
  );
}

export default Board;

Board.js

 

.btn {
  width: 100px;
  height: 100px;
  border: 1px solid;
  vertical-align: middle;
  font-size: 30px;
  font-weight: 600;
}
.board {
  width: 300px;
  margin-right: 20px;
}

css

 

import React from 'react';
import Board from './components/Board';

function App() {
  return (
    <div className="App">
        <Board />
    </div>
  );
} 

export default App;

app.js引入

 

 

不同于Vue的模板语法,react的这种写法叫做jsx语法,可以在js中直接编写dom标签。在渲染时,这种写法会被编译成 React.createElement('div',{className: 'board'})。

 

现在Board.js只有一个function Board,这种组件被称之为函数组件

 

这种写法明显冗余,我们希望通过循环来生成9个方格,在Vue中可以使用v-for,那么在react中应该怎么做呢?

 

import React from "react";
import "./style.css";

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        squares: Array(9).fill(""),
    };
  }
  render() {
    return (
      <div className="board">
        <h1>井字棋游戏--React</h1>
        {this.state.squares.map((el, index) => {
          return <button className="btn" key={index}></button>;
        })}
      </div>
    );
  }
}

export default Board;

 

首先我们将function Board 改成class Board 将其变为class组件。在constructor中定义一个state数据,其key为squares,是一个长度为9的值为空的数组。然后用map方法循环数组,输出dom节点。

 

接着我们将button标签拆分成单独的组件Square.js,以便学习父子组件的传值方式。

 

import React from "react";
import "./style.css";

function Square(props) {
  return (
    <button className="btn">
    </button>
  );
}
export default Square;

Square.js

 

import React from "react";
import Square from "./Square"
import "./style.css";

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        squares: Array(9).fill(""),
    };
  }
  render() {
    return (
      <div className="board">
        <h1>井字棋游戏--React</h1>
        {this.state.squares.map((el, index) => {
          return <Square key={index}></Square>;
        })}
      </div>
    );
  }
}

export default Board;

Board.js

 

组件传值与事件交互

 

首先需要将玩家信息“X”or“O”传递给子组件。父组件通过绑定一个字段,子组件通过props接收。

import React from "react";
import Square from "./Square"
import "./style.css";

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        squares: Array(9).fill(""),
        player: "X"
    };
  }
  render() {
    return (
      <div className="board">
        <h1>井字棋游戏--React</h1>
        {this.state.squares.map((el, index) => {
          return <Square key={index} player={this.state.player}></Square>;
        })}
      </div>
    );
  }
}

export default Board;

 

import React from "react";
import "./style.css";

function Square(props) {
    console.log(props);
  return (
    <button className="btn">
        {props.player}
    </button>
  );
}
export default Square;

 

 

子组件拿到数据了,但是我们需要的是,每次点击时显示棋子并切换玩家。

思路是这样的:子组件在初始化时,拿空数据,每次点击时,修改当前节点的数据,并切换下一个玩家。

 

定义一个切换玩家的函数:changePlayer

  changePlayer(index) {
      let player = this.state.player === "X" ? "O" : "X";
      let squares = [...this.state.squares];
      squares[index] = player;
      this.setState({
          player,
          squares
      })
  }

 

在react中是不允许直接修改state中的值的,必须通过setState来实现赋值,这个函数是异步的。

 

在react中,当数据改变时会触发render函数重新执行,生成虚拟dom,然后对比虚拟dom是否有改变,若有则重新渲染dom节点。

 

 

react组件生命周期

 

然后在子组件中通过属性传递事件函数

  render() {
    return (
      <div className="board">
        <h1>井字棋游戏--React</h1>
        {this.state.squares.map((el, index) => {
          return <Square key={index} player={el} changePlayer={() => {this.changePlayer(index)}}></Square>;
        })}
      </div>
    );
  }
}

 

向事件处理程序传递参数,需要注意this指向的问题,可以使用箭头函数或bind(this)来保持this的指向:

 

我个人更倾向于第一种。

 

接着在子组件中绑定click事件:

import React from "react";
import "./style.css";

function Square(props) {
  return (
    <button className="btn" onClick={props.changePlayer}>
        {props.player}
    </button>
  );
}
export default Square;

 

这样基本的切换玩家功能就实现了。

 

判定胜负

 

“井字棋”的规则很简单,就是横、竖、对角线三子成棋,即为胜利。

我们先添加一些提示语:Next player:X/O

在玩家胜利时将其切换为:Winner is :X/O

 

定义winner与winnerArr(成棋的棋子的index数组)

 
 constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(""),
      player: "X",
      winner: "",
      winnerArr: [],
    };
  }

 

通过条件判定,切换title

 render() {
    let { player, squares, winner } = this.state;
    let title = "";
    if (!winner) {
      title = <p>Next player:{player}</p>;
    } else {
      title = <p>Winner is:{winner}</p>;
    }
    return (
      <div className="board">
        <h1>井字棋游戏--React</h1>
        {title}
        {squares.map((el, index) => {
          return (
            <Square
              key={index}
              player={el}
              changePlayer={() => {
                this.changePlayer(index);
              }}
            ></Square>
          );
        })}
      </div>
    );
  }

 

判定获胜者函数:该函数将所有获胜可能的index值组合穷举出来,再从棋盘上取index组合中的三个值,三值相等即为胜者,游戏结束。

  // 判断获胜者
  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: squares[a],
          winnerArr: lines[i],
        };
      }
    }
    return null;
  }

 

该函数在每次下棋动作都应被调用:

changePlayer(index) {
    if (this.state.winner) {
      return;
    }
    let player = this.state.player === "X" ? "O" : "X";
    let squares = [...this.state.squares];
    squares[index] = player;
    this.setState({
      player,
      squares,
    });
    let winner = this.calculateWinner(squares);
    if (winner) {
      this.setState({
        winner: winner.squares,
        winnerArr: winner.winnerArr,
      });
    }
  }

 

至此,判定胜者功能完成。

 

高亮获胜棋子

 

这个功能涉及动态添加className。

 

首先定义一个动态返回className的函数:

  getClassName(index) {
    let { winner, winnerArr } = this.state;
    if (winner) {
      for (let i = 0; i < 3; i++) {
        if (winnerArr[i] === index) {
          return "winner-square";
        }
      }
      return "";
    } else {
      return "";
    }
  }

 

在组件中绑定:

 return (
      <div className="board">
        <h1>井字棋游戏--React</h1>
        {title}
        {squares.map((el, index) => {
          return (
            <Square
              key={index}
              player={el}
              changePlayer={() => {
                this.changePlayer(index);
              }}
              winnerClass={this.getClassName(index)}
            ></Square>
          );
        })}
      </div>
    );

 

子组件内:

function Square(props) {
  return (
    <button className={`btn ${props.winnerClass}`} onClick={props.changePlayer}>
        {props.player}
    </button>
  );
}

 

.winner-square {
  background-color: aquamarine;
}

 

注意动态绑定className的语法:{`btn ${props.winnerClass}`} 是es6的模板字符串语法。当然还有其他表示方式,但是这种方式较为简洁,个人更推荐。

 

 

按步骤悔棋

 

其实这个功能只要将每一步的数据保存在一个数组当中,点击悔棋列表,将数据切换回去即可。

 

定义history保存初始状态值:

constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(""),
      player: "X",
      winner: "",
      history: [
        {
          squares: Array(9).fill(""),
          player: "X",
        },
      ],
      step: 1,
      winnerArr: [],
    };
  }

 

在切换玩家函数中保存每一步的数据:

// 切换玩家
  changePlayer(i) {
    if (this.state.winner) {
      return;
    }
    let squares = [...this.state.squares];
    let history = this.state.history.slice(0, this.state.step);
    if (squares[i]) {
      return;
    }
    let player = this.state.player === "X" ? "O" : "X";
    squares[i] = this.state.player;
    history.push({
      squares,
      player,
    });
    // setState方法是异步执行的
    this.setState({
      player,
      squares,
      history,
      step: history.length,
    });
    let winner = this.calculateWinner(squares);
    if (winner) {
      this.setState({
        winner: winner.squares,
        winnerArr: winner.winnerArr
      });
    }
  }

 

返回到某一步:

 

 backTo(i) {
    this.setState((state) => {
      return {
        winner: "",
        squares: state.history[i].squares,
        player: state.history[i].player,
        step: i + 1,
      };
    });
  }

 

添加悔棋dom节点,并添加触发事件:

// 每次数据更新都会触发执行
  render() {
    let { player, squares, winner, history } = this.state;
    let title = "";
    if (!winner) {
      title = <p>Next player:{player}</p>;
    } else {
      title = <p>Winner is:{winner}</p>;
    }
    return (
      <Fragment>
        <h1>井字棋游戏--React</h1>
        {title}
        <div className="flex">
          <div className="board">
            {squares.map((el, index) => {
              return (
                <Square
                  changePlayer={() => this.changePlayer(index)}
                  key={index}
                  player={el}
                  index={index}
                  winnerClass={this.getClassName(index)}
                />
              );
            })}
          </div>
          <div className="back_step">
            <p>悔棋</p>
            {history.map((el, i) => {
              return (
                <button
                  key={i}
                  onClick={() => {
                    this.backTo(i);
                  }}
                >
                  {i === 0 ? "Back to game start" : "Back to No:" + i + " step"}
                </button>
              );
            })}
          </div>
        </div>
      </Fragment>
    );
  }

 

至此功能就全部实现了!

 

需要获取源码的小伙伴,请进入公众号回复“react” 即可获取GitHub地址。

 

 

小伙伴们加油!我们下期再见!

全栈攻城狮进阶

关注微信公众号,第一时间获取好文章!

 

 

所有人都祝你快乐,我只愿你遍历山河,觉得人间值得。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值