React井字棋游戏-分析与代码实现

本文详细解析了如何使用React开发一款井字棋游戏,涉及组件设计、状态管理、事件流控制和游戏胜利条件判断。通过实例展示了如何在APP组件中共享状态,以及Board、Square、PlayerInfo和History组件间的协同工作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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组件)传进来的posinfo 信息,然后用于显示

//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组件)传来的infopos信息,一起封装给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组件更新

历史记录中应该保存 stepCountthis.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更新的封装函数步骤为:

  1. 拿到当前状态中的 historystepCount
  2. 重新创造一个新的squares
  3. 根据 historystepCount对新的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()中更新stepCountsquares(用到 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布局进行计算,另一种是通过stepCounthistory来进行计算。
本文采用的是前者。

//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组件中进行显示更新即可。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值