首頁 > 軟體

React中井字棋遊戲的實現範例

2022-08-05 14:00:21

最近開始接觸React,我認為讀官方檔案是最快上手一門技術的途徑了,恰好React的官方檔案中有這樣一個井字棋遊戲demo,學習完後能夠快速上手React,這是我學習該demo的總結

需求分析

首先看看這個遊戲都有哪些需求吧

  • 遊戲玩家:XO,每次落棋後需要切換到下一個玩家
  • 贏家判斷:什麼情況下會誕生贏家,如何進行判斷?
  • 禁止落棋的時機:遊戲已有贏家 or 棋盤上已有棋子時
  • 時間旅行:能夠展示遊戲下棋歷史,點選可跳轉回相應的棋局

實現分析

首先宣告一下,我不會像官方檔案那樣一步步從底層實現,然後逐步狀態提升至父元件的方式講解,而是直接從全域性分析,分析涉及哪些狀態,應當由哪個元件管理以及這樣做的原因是什麼

涉及的元件

先來思考一下整個遊戲會涉及什麼元件:

  • 首先最基本的,開啟遊戲最能吸引目光的,就是棋盤了,所以肯定得有一個棋盤元件Board
  • 棋盤有多個格子,因此還能將棋盤分割成多個格子元件Square
  • 還需要有一個遊戲介面去控制遊戲的UI以及遊戲的邏輯,所以要有一個Game元件

涉及的狀態

  • 棋盤中的每個格子的棋子是什麼,比如是X還是O
  • 下一步是哪個玩家
  • 棋盤的歷史記錄,每下一步棋都要儲存整個棋盤的狀態
  • 棋盤歷史記錄指標,控制當前的棋盤是歷史記錄中的哪個時候的棋盤

我們可以自頂向下分析,最頂層的狀態肯定是歷史記錄,因為它裡面儲存著每一步的棋盤,而棋盤本應該作為Board元件的狀態的,但又由於有多個變動的棋盤(使用者點選歷史記錄切換棋盤時),所以不適合作為state放到Board元件中,而應當作為props,由父元件Game去控制當前展示的棋盤

而棋盤中的格子又是在棋盤中的,所以也導致本應該由棋盤格子Square元件管理的格子內容狀態提升至Game元件管理,存放在歷史記錄的每個棋盤物件中,所以Square的棋盤內容也應當以props的形式存在

下一步輪到哪個玩家是視棋盤的情況而定的,所以我認為應當放到歷史記錄的棋盤物件裡和棋盤一起進行管理,官方那種放到Gamestate中而不是放到歷史記錄的每個棋盤中的做法我覺得不太合適

有了以上的分析,我們就可以開始寫我們的井字棋遊戲了!

編碼實現

專案初始化

首先使用vite建立一個react專案

pnpm create vite react-tic-tac-toe --template react-ts
cd react-tic-tac-toe
pnpm i
code .

這裡我使用vscode進行開發,當然,你也可以使用別的ide(如NeovimWebStorm

定義各個元件的props/state

由於使用的是ts進行開發,所以我們可以在真正寫程式碼前先明確一下每個元件的propsstate,一方面能夠讓自己理清一下各個元件的關係,另一方面也可以為之後編寫程式碼提供一個良好的型別提示

Square元件props

每個棋盤格中需要放棋子,這裡我使用字元XO充當棋子,當然,棋盤上也可以不放棋子,所以設定一個squareContent屬性

點選每個格子就是落棋操作,也就是要填充一個字元到格子中,根據前面的分析我們知道,填充的邏輯應當交由棋盤Board元件處理,所以再新增一個onFillSquareprop,它起到一個類似事件通知的作用,當呼叫這個函數的時候,會呼叫父元件傳入的函數,起到一個通知的作用

所以Square元件的props介面定義如下:

interface Props {
  squareContent: string | null;
  fillSquare: () => void;
}

Board元件props

棋盤中要管理多個格子,所以肯定要有一個squares狀態,用於控制各個格子

棋盤填充棋子的邏輯也應當交給Game元件去完成,因為要維護歷史記錄,而棋盤的狀態都是儲存在歷史記錄中的,所以填充棋子也要作為Board元件的一個prop

還要在棋盤上顯示下一個玩家以及在對局結束時顯示贏家資訊,所以要有一個statusMsgprop顯示對局資訊,以及nextPlayer記錄下一個玩家

最終Board元件的props介面定義如下:

interface Props {
  squares: Squares;
  statusMsg: string;
  nextPlayer: Player;
  fillSquare: (squareIdx: number) => void;
}

Game元件state

要記錄歷史資訊,以及通過歷史記錄下標獲取到對應歷史記錄的棋盤,所以它的State如下

interface State {
  history: BoardPropsNeeded[];
  historyIdx: number;
}

各元件程式碼

Square

export interface Props {
  squareContent: string | null;
  fillSquare: () => void;
}

export type Squares = Omit<Props, "fillSquare">[];

export default function Square(props: Props) {
  return (
    <div className="square" onClick={() => props.fillSquare()}>
      {props.squareContent}
    </div>
  );
}

Board

import React from "react";
import Square from "./Square";
import type { Squares } from "./Square";

export type Player = "X" | "O";

export interface Props {
  squares: Squares;
  statusMsg: string;
  nextPlayer: Player;
  fillSquare: (squareIdx: number) => void;
}

export default class Board extends React.Component<Props> {
  renderSquare(squareIdx: number) {
    const { squareContent } = this.props.squares[squareIdx];

    return (
      <Square
        squareContent={squareContent}
        fillSquare={() => this.props.fillSquare(squareIdx)}
      />
    );
  }

  render(): React.ReactNode {
    return (
      <div>
        <h1 className="board-status-msg">{this.props.statusMsg}</h1>
        <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>
    );
  }
}

Game

import React from "react";
import Board from "./Board";
import type { Props as BoardProps, Player } from "./Board";
import type { Squares } from "./Square";

type BoardPropsNeeded = Omit<BoardProps, "fillSquare">;

interface State {
  history: BoardPropsNeeded[];
  historyIdx: number;
}

export default class Game extends React.Component<any, State> {
  constructor(props: any) {
    super(props);

    this.state = {
      history: [
        {
          squares: new Array(9).fill({ squareContent: null }),
          nextPlayer: "X",
          statusMsg: "Next player: X",
        },
      ],
      historyIdx: 0,
    };
  }

  togglePlayer(): Player {
    const currentBoard = this.state.history[this.state.historyIdx];
    return currentBoard.nextPlayer === "X" ? "O" : "X";
  }

  fillSquare(squareIdx: number) {
    const history = this.state.history.slice(0, this.state.historyIdx + 1);
    const currentBoard = history[this.state.historyIdx];
    // 先判斷一下對局是否結束 結束的話就不能繼續落棋
    // 當前格子有棋子的話也不能落棋
    if (
      calcWinner(currentBoard.squares) ||
      currentBoard.squares[squareIdx].squareContent !== null
    )
      return;

    const squares = currentBoard.squares.slice();
    squares[squareIdx].squareContent = currentBoard.nextPlayer;
    this.setState({
      history: history.concat([
        {
          squares,
          statusMsg: currentBoard.statusMsg,
          nextPlayer: this.togglePlayer(),
        },
      ]),
      historyIdx: history.length,
    });
  }

  jumpTo(historyIdx: number) {
    this.setState({
      historyIdx,
    });
  }

  render(): React.ReactNode {
    const history = this.state.history;
    const currentBoard = history[this.state.historyIdx];
    const { nextPlayer } = currentBoard;
    const winner = calcWinner(currentBoard.squares);
    let boardStatusMsg: string;

    if (winner !== null) {
      boardStatusMsg = `Winner is ${winner}!`;
    } else {
      boardStatusMsg = `Next player: ${nextPlayer}`;
    }

    const historyItems = history.map((_, idx) => {
      const desc = idx ? `Go to #${idx}` : `Go to game start`;
      return (
        <li key={idx}>
          <button className="history-item" onClick={() => this.jumpTo(idx)}>
            {desc}
          </button>
        </li>
      );
    });

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={currentBoard.squares}
            statusMsg={boardStatusMsg}
            nextPlayer={nextPlayer}
            fillSquare={(squareIdx: number) => this.fillSquare(squareIdx)}
          />
        </div>
        <div className="divider"></div>
        <div className="game-info">
          <h1>History</h1>
          <ol>{historyItems}</ol>
        </div>
      </div>
    );
  }
}

const calcWinner = (squares: Squares): Player | null => {
  // 贏的時候的棋局情況
  const winnerCase = [
    [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 < winnerCase.length; i++) {
    const [a, b, c] = winnerCase[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a].squareContent as Player;
    }
  }

  return null;
};

 到此這篇關於React中井字棋遊戲的實現範例的文章就介紹到這了,更多相關React 井字棋遊戲內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com