目录需求分析实现分析涉及的组件涉及的状态编码实现项目初始化定义各个组件的props/stateSquare组件propsBoard组件propsGame组件state各组件代码Squ
最近开始接触React
,我认为读官方文档是最快上手一门技术的途径了,恰好React
的官方文档中有这样一个井字棋游戏的demo
,学习完后能够快速上手React
,这是我学习该demo
的总结
首先看看这个游戏都有哪些需求吧
X
和O
,每次落棋后需要切换到下一个玩家首先声明一下,我不会像官方文档那样一步步从底层实现,然后逐步状态提升至父组件的方式讲解,而是直接从全局分析,分析涉及哪些状态,应当由哪个组件管理以及这样做的原因是什么
先来思考一下整个游戏会涉及什么组件:
Board
Square
UI
以及游戏的逻辑,所以要有一个Game
组件X
还是O
我们可以自顶向下分析,最顶层的状态肯定是历史记录,因为它里面保存着每一步的棋盘,而棋盘本应该作为Board
组件的状态的,但又由于有多个变动的棋盘(用户点击历史记录切换棋盘时),所以不适合作为state
放到Board
组件中,而应当作为props
,由父组件Game
去控制当前展示的棋盘
而棋盘中的格子又是在棋盘中的,所以也导致本应该由棋盘格子Square
组件管理的格子内容状态提升至Game
组件管理,存放在历史记录的每个棋盘对象中,所以Square
的棋盘内容也应当以props
的形式存在
下一步轮到哪个玩家是视棋盘的情况而定的,所以我认为应当放到历史记录的棋盘对象里和棋盘一起进行管理,官方那种放到Game
的state
中而不是放到历史记录的每个棋盘中的做法我觉得不太合适
有了以上的分析,我们就可以开始写我们的井字棋游戏了!
首先使用vite
创建一个react
项目
pnpm create vite react-tic-tac-toe --template react-ts
cd react-tic-tac-toe
pnpm i
code .
这里我使用vscode
进行开发,当然,你也可以使用别的ide
(如Neovim
、WEBStORM
)
由于使用的是ts
进行开发,所以我们可以在真正写代码前先明确一下每个组件的props
和state
,一方面能够让自己理清一下各个组件的关系,另一方面也可以为之后编写代码提供一个良好的类型提示
每个棋盘格中需要放棋子,这里我使用字符X
和O
充当棋子,当然,棋盘上也可以不放棋子,所以设置一个squareContent
属性
点击每个格子就是落棋操作,也就是要填充一个字符到格子中,根据前面的分析我们知道,填充的逻辑应当交由棋盘Board
组件处理,所以再添加一个onFillSquare
的prop
,它起到一个类似事件通知的作用,当调用这个函数的时候,会调用父组件传入的函数,起到一个通知的作用
所以Square
组件的props
接口定义如下:
interface Props {
squareContent: string | null;
fillSquare: () => void;
}
棋盘中要管理多个格子,所以肯定要有一个squares
状态,用于控制各个格子
棋盘填充棋子的逻辑也应当交给Game
组件去完成,因为要维护历史记录,而棋盘的状态都是保存在历史记录中的,所以填充棋子也要作为Board
组件的一个prop
还要在棋盘上显示下一个玩家以及在对局结束时显示赢家信息,所以要有一个statusMsg
的prop
显示对局信息,以及nextPlayer
记录下一个玩家
最终Board
组件的props
接口定义如下:
interface Props {
squares: Squares;
statusMsg: string;
nextPlayer: Player;
fillSquare: (squareIdx: number) => void;
}
要记录历史信息,以及通过历史记录下标获取到对应历史记录的棋盘,所以它的State
如下
interface State {
history: BoardPropsNeeded[];
historyIdx: number;
}
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>
);
}
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>
);
}
}
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 井字棋游戏内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!
--结束END--
本文标题: React中井字棋游戏的实现示例
本文链接: https://www.lsjlt.com/news/171875.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0