React中井字棋游戲的實(shí)現(xiàn)示例
最近開始接觸React
,我認(rèn)為讀官方文檔是最快上手一門技術(shù)的途徑了,恰好React
的官方文檔中有這樣一個(gè)井字棋游戲的demo
,學(xué)習(xí)完后能夠快速上手React
,這是我學(xué)習(xí)該demo
的總結(jié)
需求分析
首先看看這個(gè)游戲都有哪些需求吧
- 游戲玩家:
X
和O
,每次落棋后需要切換到下一個(gè)玩家 - 贏家判斷:什么情況下會(huì)誕生贏家,如何進(jìn)行判斷?
- 禁止落棋的時(shí)機(jī):游戲已有贏家 or 棋盤上已有棋子時(shí)
- 時(shí)間旅行:能夠展示游戲下棋歷史,點(diǎn)擊可跳轉(zhuǎn)回相應(yīng)的棋局
實(shí)現(xiàn)分析
首先聲明一下,我不會(huì)像官方文檔那樣一步步從底層實(shí)現(xiàn),然后逐步狀態(tài)提升至父組件的方式講解,而是直接從全局分析,分析涉及哪些狀態(tài),應(yīng)當(dāng)由哪個(gè)組件管理以及這樣做的原因是什么
涉及的組件
先來思考一下整個(gè)游戲會(huì)涉及什么組件:
- 首先最基本的,打開游戲最能吸引目光的,就是棋盤了,所以肯定得有一個(gè)棋盤組件
Board
- 棋盤有多個(gè)格子,因此還能將棋盤分割成多個(gè)格子組件
Square
- 還需要有一個(gè)游戲界面去控制游戲的
UI
以及游戲的邏輯,所以要有一個(gè)Game
組件
涉及的狀態(tài)
- 棋盤中的每個(gè)格子的棋子是什么,比如是
X
還是O
- 下一步是哪個(gè)玩家
- 棋盤的歷史記錄,每下一步棋都要保存整個(gè)棋盤的狀態(tài)
- 棋盤歷史記錄指針,控制當(dāng)前的棋盤是歷史記錄中的哪個(gè)時(shí)候的棋盤
我們可以自頂向下分析,最頂層的狀態(tài)肯定是歷史記錄,因?yàn)樗锩姹4嬷恳徊降钠灞P,而棋盤本應(yīng)該作為Board
組件的狀態(tài)的,但又由于有多個(gè)變動(dòng)的棋盤(用戶點(diǎn)擊歷史記錄切換棋盤時(shí)),所以不適合作為state
放到Board
組件中,而應(yīng)當(dāng)作為props
,由父組件Game
去控制當(dāng)前展示的棋盤
而棋盤中的格子又是在棋盤中的,所以也導(dǎo)致本應(yīng)該由棋盤格子Square
組件管理的格子內(nèi)容狀態(tài)提升至Game
組件管理,存放在歷史記錄的每個(gè)棋盤對象中,所以Square
的棋盤內(nèi)容也應(yīng)當(dāng)以props
的形式存在
下一步輪到哪個(gè)玩家是視棋盤的情況而定的,所以我認(rèn)為應(yīng)當(dāng)放到歷史記錄的棋盤對象里和棋盤一起進(jìn)行管理,官方那種放到Game
的state
中而不是放到歷史記錄的每個(gè)棋盤中的做法我覺得不太合適
有了以上的分析,我們就可以開始寫我們的井字棋游戲了!
編碼實(shí)現(xiàn)
項(xiàng)目初始化
首先使用vite
創(chuàng)建一個(gè)react
項(xiàng)目
pnpm create vite react-tic-tac-toe --template react-ts cd react-tic-tac-toe pnpm i code .
這里我使用vscode
進(jìn)行開發(fā),當(dāng)然,你也可以使用別的ide
(如Neovim
、WebStorm
)
定義各個(gè)組件的props/state
由于使用的是ts
進(jìn)行開發(fā),所以我們可以在真正寫代碼前先明確一下每個(gè)組件的props
和state
,一方面能夠讓自己理清一下各個(gè)組件的關(guān)系,另一方面也可以為之后編寫代碼提供一個(gè)良好的類型提示
Square組件props
每個(gè)棋盤格中需要放棋子,這里我使用字符X
和O
充當(dāng)棋子,當(dāng)然,棋盤上也可以不放棋子,所以設(shè)置一個(gè)squareContent
屬性
點(diǎn)擊每個(gè)格子就是落棋操作,也就是要填充一個(gè)字符到格子中,根據(jù)前面的分析我們知道,填充的邏輯應(yīng)當(dāng)交由棋盤Board
組件處理,所以再添加一個(gè)onFillSquare
的prop
,它起到一個(gè)類似事件通知的作用,當(dāng)調(diào)用這個(gè)函數(shù)的時(shí)候,會(huì)調(diào)用父組件傳入的函數(shù),起到一個(gè)通知的作用
所以Square
組件的props
接口定義如下:
interface Props { squareContent: string | null; fillSquare: () => void; }
Board組件props
棋盤中要管理多個(gè)格子,所以肯定要有一個(gè)squares
狀態(tài),用于控制各個(gè)格子
棋盤填充棋子的邏輯也應(yīng)當(dāng)交給Game
組件去完成,因?yàn)橐S護(hù)歷史記錄,而棋盤的狀態(tài)都是保存在歷史記錄中的,所以填充棋子也要作為Board
組件的一個(gè)prop
還要在棋盤上顯示下一個(gè)玩家以及在對局結(jié)束時(shí)顯示贏家信息,所以要有一個(gè)statusMsg
的prop
顯示對局信息,以及nextPlayer
記錄下一個(gè)玩家
最終Board
組件的props
接口定義如下:
interface Props { squares: Squares; statusMsg: string; nextPlayer: Player; fillSquare: (squareIdx: number) => void; }
Game組件state
要記錄歷史信息,以及通過歷史記錄下標(biāo)獲取到對應(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ù)落棋 // 當(dāng)前格子有棋子的話也不能落棋 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 => { // 贏的時(shí)候的棋局情況 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中井字棋游戲的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)React 井字棋游戲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react實(shí)現(xiàn)移動(dòng)端二級路由嵌套詳解
這篇文章主要介紹了react移動(dòng)端二級路由嵌套的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08詳解React中父子組件數(shù)據(jù)傳遞和修改的方式及原理
這篇文章主要為大家詳細(xì)介紹了React中父子組件數(shù)據(jù)傳遞和修改的方式及原理,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-04-04react中hooks使用useState的更新不觸發(fā)dom更新問題及解決
這篇文章主要介紹了react中hooks使用useState的更新不觸發(fā)dom更新問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01react-native 實(shí)現(xiàn)購物車滑動(dòng)刪除效果的示例代碼
這篇文章主要介紹了react-native 實(shí)現(xiàn)購物車滑動(dòng)刪除效果的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01