React井字棋游戏-分析与代码实现
本文为个人原创,如需转载请标明出处。
目录
分析
游戏展示链接: https://react.docschina.org/tutorial/tutorial.html#completing-the-game
项目和官网代码不同,总体思路一致。
做这个项目前,分析根APP组件 <APP/>
需要哪些模块
左侧区域:
(1)棋盘 <Board/>
组件
(2)方块<Square/>
组件
右侧区域:
(1)棋手信息 <PlayerInfo/>
组件
(2)历史信息 <History/>
组件
1 井字棋界面显示
1.1 根APP组件共享状态
根APP组件需要这样几个变量:棋盘每个棋子的状态集合squares、历史信息中每走一步的步骤stepCount、以及当前每步的记录history
//App组件:
constructor(props){
super(props)
this.state={
squares:new Array(9).fill(null),
stepCount:0,
history:[]
此外还需要有获取棋手信息中的棋手方法,该方法可以通过共享状态stepCount来得到。该方法的返回值可以获取到棋手信息this.getCurPlayer()
//App组件:
//偶数为X选手 奇数为O选手
getCurPlayer(){
const {stepCount}=this.state
return (stepCount%2 == 0 ? "X":"O" )
}
根APP组件将棋手信息curPlayer、记录信息history和棋子的状态集合squares分别传给三个模块,如下:
//App组件:
render(){
const curPlayer=this.getCurPlayer()
const {history,squares}=this.state
return (
<div className="App">
<div className="LeftPanel">
<Board
squares={squares}
</div>
<div className="RightPanel">
<PlayerInfo
curPlayer={curPlayer}
<History
history={history} />
</div>
</div>
)
}
1.2 Board组件
在Board组件中获取根APP组件传来的 棋子状态集合squares 。每个方块(棋格)有一个位置信息pos,以及状态信息info(棋格中有三种类型:Null,X,O)。显然,pos和info都在Board组件定义。
//Board组件:
genSquare(pos){
const{squares}=this.props
return( <Square pos={pos} info={squares[pos]} />)
}
render(){
return(
<div className="board">
<div className="row">
{ this.genSquare(0)}
{ this.genSquare(1)}
{ this.genSquare(2)}
</div>
<div className="row">
{ this.genSquare(3)}
{ this.genSquare(4)}
{ this.genSquare(5)}
</div>
<div className="row">
{ this.genSquare(6)}
{ this.genSquare(7)}
{ this.genSquare(8)}
</div>
</div>
)
}
1.2.1 Square组件
在Square组件中获取到 上层(Board组件)传进来的pos 和 info 信息,然后用于显示
//Sqaure组件:
getSquareTitle(){
const {pos,info}=this.props
let title=pos
if(info){
title=info
}
return title
}
到此就可以显示出九宫格的数字。
1.3 PlayerInfo组件
同理。仿照1.2在PlayerInfo组件中获取到 上层(根APP组件)传进来的 curPlayer 信息,用于显示当前的棋手信息。
2 交互
2.1 Square组件点击事件的流动(从下往上)
此游戏所有的变化都是基于下棋进行,所以首先考虑棋盘上点击事件流的逻辑。首先,在Square组件上绑定onClick事件,和上一层(Board组件)传来的info,pos信息,一起封装给Board组件层,通知已经点击。
//Square组件:
handleClick=()=>{
const {info,pos}=this.props
if(this.props.onClick){
this.props.onClick(pos,info)
}
}
render(){
return(
<div className='square' onClick={this.handleClick}>
{this.getSquareTitle()}
</div>
)
}
思考:Board组件层有没有存储和管理对应的状态集合squares?
回答:没有,因为squares定义在最外层,所以此时返回到Board组件层还不够,应该返回到APP层实现点击事件的处理逻辑。
//Board组件:
//更新的代码
genSquare(pos){
const{squares}=this.props
return( <Square pos={pos} info={squares[pos]} onClick={this.props.onClickSquare}/>)
}
2.2 点击棋盘的各组件处理逻辑
APP层获取到有Square组件传来的 pos 和 info 后,即可以点击棋盘的操作。
在写代前,明确一下,点击事件时会触发哪几步
(1)<Board/>
的更新
(2)<History/>
的更新
(3)<PlayerInfo/>
的更新:已经在1.3中实现
接下来将逐一介绍。
2.2.1History组件更新
历史记录中应该保存 stepCount 、this.getCurPlayer()、pos
注意:此处的History更新的依据是stepCount
//APP组件:
handleClickSquare=(pos,info)=>{
//console.log(pos,info)
if(info==null){
const {history}=this.state
//console.log(history)
const historyNew=[
...history,
{
stepCount:this.state.stepCount,
player:this.getCurPlayer(),
pos:pos
}
]
//通过this.setState来更新history:使用prevState参数
//拿到原始的history对象,然后将更新后的history重置
//匿名函数外加圆括号,就可以不用写return
this.setState((prevState)=>({
history:historyNew,
stepCount:prevState.stepCount+1
}))
}
}
//更新的代码
<Board squares={squares}
onClickSquare={this.handleClickSquare}/>
此时每次点击方块的时候历史记录就可以更新。这个时候我们要在History组件中进行同步显示。
//History组件:
genHistoryItemUI(item,index){
return (
<li key={`history-iem -${index}`}>
<button>
#{item.stepCount}:棋手{item.player}下到位置{item.pos}
</button>
</li>
)
}
render(){
const {history}=this.props
if(history.length==0){
return (
<div className="history">
我是历史记录
</div>
)
}else{
//由于每次都要更新li,所以可以对<li></li>进行一个封装
return (
<div className="history">
<ul>
{history.map((item,index)=>
this.genHistoryItemUI(item,index))}
</ul>
</div>
)
}
}
}
2.2.2 Board更新
棋子状态squares更新的封装函数步骤为:
- 拿到当前状态中的 history 和 stepCount
- 重新创造一个新的squares
- 根据 history 和 stepCount对新的squares进行更新
//APP组件
updateCurPlayer(history,stepCount){
const squaresNew=new Array(9).fill(null);
for(let i=0;i<stepCount;i++){
//history[i]对应当前的记录
//curHistoryInfo.pos对应棋子在board中的索引位置
//curHistoryInfo.player对应棋子
let curHistoryInfo=history[i];
squaresNew[curHistoryInfo.pos]=curHistoryInfo.player;
}
return squaresNew
}
此封装函数显然放在APP组件的handleClickSquare()
函数中,每一次有变动即更新,但是注意,应该在history更新后放置该封装函数,显然现有history再有此封装函数。
以下为更新过后的handleClickSquare()
:
//APP组件
//更新的代码
handleClickSquare=(pos,info)=>{
...
this.setState((prevState)=>({
history:historyNew,
stepCount:prevState.stepCount+1,
squares:this.updateCurPlayer(historyNew,prevState.stepCount+1)
}))
}
}
2.3History组件的回退
此小节将介绍如何将History组件同Board组件进行联动,从而实现History历史信息的回退。
首先,History历史信息是以button形式呈现,所以我们先绑定点击事件。
同理,此处的点击事件流动还是从下往上(callback)。回退的参数是当前的history.item,用来记获取当前的一条history信息。
//History组件
handleClick(item){
const {onClickHistory}=this.props
if(onClickHistory){
onClickHistory(item)
}
}
//更新的代码
genHistoryItemUI(item,index){
return (
<li key={`history-iem -${index}`}>
{/* 此处绑定item可以让handleClick拿到item */}
<button onClick={this.handleClick.bind(this,item)}>
#{item.stepCount}:棋手{item.player}下到位置{item.pos}
</button>
</li>
)
}
注意:此处使用的是bind函数绑定this进行传参
在2.2.1和2.2.2中提到历史记录History的更新是通过stepCount的值来实现,棋盘Board中棋子状态squares的更新是通过。
那么我们只需要在根App组件回调点击函数handleClickHistory()
中更新stepCount和squares(用到 updateCurPlayer()
)即可完成历史信息的回退。
//APP组件:
handleClickHistory=(historyItem)=>{
console.log(historyItem)
const stepCountNew=historyItem.stepCount
this.setState((prevState)=>({
stepCount:stepCountNew+1,
squares:this.updateCurPlayer(prevState.history,stepCountNew+1)
}))
}
//更新的代码
render(){
...
<History
history={history}
onClickHistory={this.handleClickHistory}/>
}
到此步后,会发现回退然后重新下棋,之前的记录仍然存在。
所以我们在取历史记录history的时候应该取到当前位置的stepCount
//App组件
//更新的代码
if(info==null){
const {history}=this.state
console.log(history)
const historyNew=[
...history.slice(0,this.state.stepCount),
{
stepCount:this.state.stepCount,
player:this.getCurPlayer(),
pos:pos
}
]
回退的时候仍存在一个问题,就是回退到第一步时到不了初始状态:即棋盘为空的时候。
于是可以在最开始放一个空的历史信息。
//History组件
genHistoryStart(){
return (
<li key={`history-iem -start`}>
<button onClick={this.prop.onRestartGame}>
#重新开始
</button>
</li>
)
}
render(){
...
else{
return (
<div className="history">
<ul>
{this.genHistoryStart()}
{history.map((item,index)=>this.genHistoryItemUI(item,index))}
</ul>
</div>
)
}
}
同理,APP组件中的回调点击函数handleRestartGame()
同handleClickHistory()
一样,一样都是修改stepCount值
//APP组件
handleRestartGame=()=>{
const stepCountNew=0
this.setState((prevState)=>({
stepCount:stepCountNew,
squares:this.updateCurPlayer(prevState.history,stepCountNew)
}))
}
<History
history={history}
onRestartGame={this.handleRestartGame}
onClickHistory={this.handleClickHistory}/>
2.5游戏输赢判断
输赢通过检测棋盘的布局来判断输赢。棋面的布局可以通过squares布局进行计算,另一种是通过stepCount和history来进行计算。
本文采用的是前者。
//APP组件
isWin(){
let result=false;
let winPlayer=null;
const {squares}=this.state;
const winConditons=[
[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<winConditons.length;i++)
{
let curCondition=winConditons[i];
let first =squares[curCondition[0]]
let second =squares[curCondition[1]]
let third =squares[curCondition[2]]
//如果三个位置的花色相等
if(null!=first&& first===second && second===third){
result=true;
winPlayer=first
break;
}
}
return{
result:result,
winPlayer:winPlayer
}
}
render(){
...
<PlayerInfo curPlayer={curPlayer}
winResult={this.isWin()}/>
}
最后在PlayerInfo组件中进行显示更新即可。