React中井字棋游戲的實現(xiàn)示例
最近開始接觸React,我認為讀官方文檔是最快上手一門技術(shù)的途徑了,恰好React的官方文檔中有這樣一個井字棋游戲的demo,學習完后能夠快速上手React,這是我學習該demo的總結(jié)
需求分析
首先看看這個游戲都有哪些需求吧
- 游戲玩家:
X和O,每次落棋后需要切換到下一個玩家 - 贏家判斷:什么情況下會誕生贏家,如何進行判斷?
- 禁止落棋的時機:游戲已有贏家 or 棋盤上已有棋子時
- 時間旅行:能夠展示游戲下棋歷史,點擊可跳轉(zhuǎn)回相應(yīng)的棋局
實現(xiàn)分析
首先聲明一下,我不會像官方文檔那樣一步步從底層實現(xiàn),然后逐步狀態(tài)提升至父組件的方式講解,而是直接從全局分析,分析涉及哪些狀態(tài),應(yīng)當由哪個組件管理以及這樣做的原因是什么
涉及的組件
先來思考一下整個游戲會涉及什么組件:
- 首先最基本的,打開游戲最能吸引目光的,就是棋盤了,所以肯定得有一個棋盤組件
Board - 棋盤有多個格子,因此還能將棋盤分割成多個格子組件
Square - 還需要有一個游戲界面去控制游戲的
UI以及游戲的邏輯,所以要有一個Game組件
涉及的狀態(tài)
- 棋盤中的每個格子的棋子是什么,比如是
X還是O - 下一步是哪個玩家
- 棋盤的歷史記錄,每下一步棋都要保存整個棋盤的狀態(tài)
- 棋盤歷史記錄指針,控制當前的棋盤是歷史記錄中的哪個時候的棋盤
我們可以自頂向下分析,最頂層的狀態(tài)肯定是歷史記錄,因為它里面保存著每一步的棋盤,而棋盤本應(yīng)該作為Board組件的狀態(tài)的,但又由于有多個變動的棋盤(用戶點擊歷史記錄切換棋盤時),所以不適合作為state放到Board組件中,而應(yīng)當作為props,由父組件Game去控制當前展示的棋盤
而棋盤中的格子又是在棋盤中的,所以也導致本應(yīng)該由棋盤格子Square組件管理的格子內(nèi)容狀態(tài)提升至Game組件管理,存放在歷史記錄的每個棋盤對象中,所以Square的棋盤內(nèi)容也應(yīng)當以props的形式存在
下一步輪到哪個玩家是視棋盤的情況而定的,所以我認為應(yīng)當放到歷史記錄的棋盤對象里和棋盤一起進行管理,官方那種放到Game的state中而不是放到歷史記錄的每個棋盤中的做法我覺得不太合適
有了以上的分析,我們就可以開始寫我們的井字棋游戲了!
編碼實現(xiàn)
項目初始化
首先使用vite創(chuàng)建一個react項目
pnpm create vite react-tic-tac-toe --template react-ts cd react-tic-tac-toe pnpm i code .
這里我使用vscode進行開發(fā),當然,你也可以使用別的ide(如Neovim、WebStorm)
定義各個組件的props/state
由于使用的是ts進行開發(fā),所以我們可以在真正寫代碼前先明確一下每個組件的props和state,一方面能夠讓自己理清一下各個組件的關(guān)系,另一方面也可以為之后編寫代碼提供一個良好的類型提示
Square組件props
每個棋盤格中需要放棋子,這里我使用字符X和O充當棋子,當然,棋盤上也可以不放棋子,所以設(shè)置一個squareContent屬性
點擊每個格子就是落棋操作,也就是要填充一個字符到格子中,根據(jù)前面的分析我們知道,填充的邏輯應(yīng)當交由棋盤Board組件處理,所以再添加一個onFillSquare的prop,它起到一個類似事件通知的作用,當調(diào)用這個函數(shù)的時候,會調(diào)用父組件傳入的函數(shù),起到一個通知的作用
所以Square組件的props接口定義如下:
interface Props {
squareContent: string | null;
fillSquare: () => void;
}
Board組件props
棋盤中要管理多個格子,所以肯定要有一個squares狀態(tài),用于控制各個格子
棋盤填充棋子的邏輯也應(yīng)當交給Game組件去完成,因為要維護歷史記錄,而棋盤的狀態(tài)都是保存在歷史記錄中的,所以填充棋子也要作為Board組件的一個prop
還要在棋盤上顯示下一個玩家以及在對局結(jié)束時顯示贏家信息,所以要有一個statusMsg的prop顯示對局信息,以及nextPlayer記錄下一個玩家
最終Board組件的props接口定義如下:
interface Props {
squares: Squares;
statusMsg: string;
nextPlayer: Player;
fillSquare: (squareIdx: number) => void;
}
Game組件state
要記錄歷史信息,以及通過歷史記錄下標獲取到對應(yīng)歷史記錄的棋盤,所以它的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];
// 先判斷一下對局是否結(jié)束 結(jié)束的話就不能繼續(xù)落棋
// 當前格子有棋子的話也不能落棋
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;
};到此這篇關(guān)于React中井字棋游戲的實現(xiàn)示例的文章就介紹到這了,更多相關(guān)React 井字棋游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react實現(xiàn)pure render時bind(this)隱患需注意!
這篇文章主要為大家詳細介紹了值得你在react實現(xiàn)pure render的時候,需要注意的bind(this)隱患,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03
如何在React?Native開發(fā)中防止滑動過程中的誤觸
在使用React?Native開發(fā)的時,當我們快速滑動應(yīng)用的時候,可能會出現(xiàn)誤觸,導致我們會點擊到頁面中的某一些點擊事件,誤觸導致頁面元素響應(yīng)從而進行其他操作,表現(xiàn)出非常不好的用戶體驗。2023-05-05
react實現(xiàn)動態(tài)增減表單項的示例代碼
在做項目的時候,甲方給的信息有限,網(wǎng)頁的備案信息寫成固定的,之后驗收的時候,甲方要求把這個備案信息寫成動態(tài)的,可以自增減,下面通過實例代碼給大家介紹react實現(xiàn)動態(tài)增減表單項的示例,感興趣的朋友跟隨小編一起看看吧2024-05-05
React-Hook中使用useEffect清除定時器的實現(xiàn)方法
這篇文章主要介紹了React-Hook中useEffect詳解(使用useEffect清除定時器),主要介紹了useEffect的功能以及使用方法,還有如何使用他清除定時器,需要的朋友可以參考下2022-11-11

