react井字棋游戏及其完善

井字棋完善内容

如果你之后还会有充裕的时间并且想练习你刚掌握的新技能的话,这里有一些可以完善的游戏功能实现供你参考,列表是由易到难排序的:

  1. 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
  2. 在历史记录列表中加粗显示当前选择的项目。
  3. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
  4. 添加一个可以升序或降序显示历史记录的按钮。
  5. 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
  6. 当无人获胜时,显示一个平局的消息。

图片示例

在这里插入图片描述

编码实战

Square 组件

import React from 'react';

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

export default Square;

Board 组件

import React, { Component } from 'react';
import Square from '../Square';


class Board extends Component {
  renderSquare(i) {
    const { lines } = this.props;
    const [a, b, c] = lines;
    let className = 'square'
    if (i === a || i === b || i === c) {
      className = 'squareWinner'
    }
    return (
      <Square
        key={i}
        className={className}
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {

    /**
     *@function 渲染3*3的棋盘
     *@return react元素
     * */
    const renderBoard = () => {
      let n = 0;
      let board = [];
      for (let i = 0; i < 3; i++) {
        const boardRow = [];
        for (let j = 0; j < 3; j++, n++) {
          boardRow.push(this.renderSquare(n));
        }
        board.push(<div className="board-row" key={i}>{boardRow}</div>);
      }
      return board;
    }

    return (
      <div>
        {renderBoard()}
      </div>
    );
  }
}
export default Board;

Game 组件

js

import React, { Component } from 'react';
import Board from '../Board';
import './index.css';

/**
 * @function 游戏结果判断
 * @param {array} squares 存储棋子的数组
 * @returns {object} 如果有胜利者,返回包含对应胜利者字符串和三连棋子数组的对象
 * @returns {null} 如果棋子已经占满棋盘但是没有胜利者,返回null
 * @returns {null} 如果棋子没有占满棋盘且没有胜利者,返回null
 * */
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]) {
      // alert('游戏结束胜利者为' + squares[a] + '!')
      const winnerObject = {
        winner: squares[a],
        lines: lines[i],
      }
      return winnerObject;
    }
  }
  if (squares.indexOf(null) === -1) {
    alert('Game Over ,无人生还');
    return null;
  }

  return null;
}

class Game extends Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
      descendingOrder: false, // 判断是否需要降序显示,默认为false升序显示
      lines: [], // 存储三连棋子的索引的数组
    };
  }

  /**
   * @function 读取游戏记录
   * */
  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  /**
   * @function 方格对应的点击事件
   * @description  游戏未结束,存档;游戏决出胜利者,三连高亮显示;游戏结束,弹出提示;
   * @param {number} i 对应方格在数组中的索引index
   * 
   * */

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[this.state.stepNumber];

    const squares = current.squares.slice();
    // 如果游戏结束或者该位置上已经有棋子跳出函数
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O'; // 在数组对应位置填充棋子
    // 如果游戏有胜利者,更新三联棋子索引数组
    if (calculateWinner(squares)) {
      this.setState({
        lines: calculateWinner(squares).lines,
      });
    }
    this.setState({
      history: history.concat([{
        squares: squares,
        squareIndex: i
      }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

  /**
   * @function 修改用于判断历史记录正序/逆序显示的参数descendingOrder
   * */
  reverseHistory = () => {
    const { descendingOrder } = this.state;
    this.setState({
      descendingOrder: !descendingOrder
    })
  }

  /**
   * @function 根据棋子索引分配坐标
   * @returns {array} 返回包含棋子索引的数组
   * */
  produceCoordinate = () => {
    const size = 3; // 指定棋盘大小,现棋盘大小为3*3
    const arrayLength = size * size;
    let coordinateArray = []; // 创建初始数组
    let yInit = size; // 初始化y坐标的值
    for (let i = 0; i < arrayLength; i++) {
      let indexNumber = i + 1;
      if (indexNumber % size > 0) {
        coordinateArray.push({ x: indexNumber % size, y: yInit })
      } else {
        coordinateArray.push({ x: size, y: yInit })
        yInit = yInit - 1;
      }
    }
    return coordinateArray;
  }


  /**
    * @function 根据棋子索引分配坐标-另一种方法
    * */
  produceCoordinate_another = () => {
    let coordinateArray = [];
    for (let j = 3; j > 0; j--) {
      for (let i = 1; i < 4; i++) {
        coordinateArray.push({ x: i, y: j })
      }
    };
    return coordinateArray;
  }

  render() {
    let { lines } = this.state;
    const { history, stepNumber, descendingOrder, } = this.state;
    const current = history[stepNumber];
    const winner = calculateWinner(current.squares) ? calculateWinner(current.squares).winner : null;
    const coordinateArray = this.produceCoordinate();
    const moves = history.map((item, index) => {
      let coordinate = "";

      if (item.squareIndex || item.squareIndex === 0) {
        const moveIndex = item.squareIndex;
        coordinate = `(${coordinateArray[moveIndex].x},${coordinateArray[moveIndex].y})`;
      }


      const desc = index ?
        `移动至第${index}步,棋子坐标为${coordinate}` :
        '重新开始游戏';
      return (
        <li key={index}>
          {/* 在历史记录列表中加粗显示当前选择的项目,动态加载类名 */}
          <button
            className={index === stepNumber ? 'currentButton' : 'button'}
            onClick={() => this.jumpTo(index)}
          >
            {desc}
          </button>
        </li>
      );
    });
    // 根据descendingOrder参数选择列表的升序/降序排序
    if (descendingOrder) {
      moves.reverse();
    }
    // 如果没有胜利者,清除保存的三连棋子索引数组
    if (!calculateWinner(current.squares)) {
      lines = []
    }
    let status;
    if (winner) {
      status = '胜利者: ' + winner;
    } else {
      status = '下一位玩家: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
            lines={lines}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          {/* 添加一个可以升序或降序显示历史记录的按钮 */}
          <button
            className='button'
            onClick={() => this.reverseHistory()}
          >
            {descendingOrder ? '升序排列' : '降序排列'}
          </button>
          <button
            className='button'
            onClick={() => this.produceCoordinate()}
          >
            根据棋子索引分配坐标
          </button>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

export default Game;

css

body {
  font: 14px 'Century Gothic', Futura, sans-serif;
  margin: 20px;
}

ol,
ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: '';
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.squareWinner {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.squareWinner:focus {
  outline: none;
}

.kbd-navigation .squareWinner:focus {
  background: #ddd;
}

.squareWinner {
  background: green;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.button {
  margin-top: 5px;
}
.currentButton {
  margin-top: 5px;
  font-weight: 900;
}

项目地址

天心天地生的GitHub

发布了188 篇原创文章 · 获赞 193 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/tianxintiandisheng/article/details/105398576