You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于React+Redux+TypeScript的井字棋胜负判定实现求助

井字棋Redux状态管理下的胜负判定实现方案

问题背景

我是React和Redux的初学者,正在自学。我的井字棋项目使用Redux管理状态,希望在X或O连成一线时判定胜负。以下是我的项目代码:

store.ts

// store.ts

import { configureStore } from '@reduxjs/toolkit';
import tactactoeReducer from '../components/ticTacToeSlice';

export const store = configureStore({
  reducer: {
    tactactoeReducer
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

hooks.ts

// hooks.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

ticTacToeSlice.ts

// ticTacToeSlice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState } from "../app/store";


type Cell = 'X' | 'O' | '-'
export type Table = Array<Array<Cell>>
export type Turn = Exclude<Cell, '-'>

export interface State {
  table: Table
  turn: Turn
}

const initialState: State = {
  table: [
    ['-', '-', '-'],
    ['-', '-', '-'],
    ['-', '-', '-']
  ],
  turn: "X"
}

export interface TurnAction {
  i: number,
  j: number,
}

export const tactactoeSlice = createSlice({
  name: "tactactoe",
  initialState,
  reducers: {
    turn: (state, action: PayloadAction<TurnAction>) => {
        state.table[action.payload.i][action.payload.j] = state.turn
        state.turn = state.turn == 'O' ? 'X' : 'O'
    }
  },
});

export const { turn } = tactactoeSlice.actions;
export const turnSelector = (state: RootState) => state.tactactoeReducer;
export default tactactoeSlice.reducer;

ticTacToe.tsx

// ticTacToe.tsx

import { useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../app/hooks";
import { State, turn, turnSelector } from "./ticTacToeSlice";

function TicTacToe() {
  const [states, setStates] = useState<State>();
  const selectedTurn = useAppSelector(turnSelector);
  const dispatch = useAppDispatch();

  useEffect(() => {
    setStates(selectedTurn);
  }, [selectedTurn]);

  function handleTurn(i: number, j: number) {
    const constTurn = {
      i: i,
      j: j
    };
    dispatch(turn(constTurn));
  }

  return (
    <div className="container">
      <h3>current turn: {states?.turn}</h3>
      <table>
        {
          states?.table.map((row, index) => {
            return (
              <tr>

                {row[0] === "-" ? (
                  <td className="paper-btn" onClick={() => { handleTurn(index, 0) }}>{row[0]}</td>
                ) : (
                  <td className="paper-btn">{row[0]}</td>
                )}

                {row[1] === "-" ? (
                  <td className="paper-btn" onClick={() => { handleTurn(index, 1) }}>{row[1]}</td>
                ) : (
                  <td className="paper-btn">{row[1]}</td>
                )}

                {row[2] === "-" ? (
                  <td className="paper-btn" onClick={() => { handleTurn(index, 2) }}>{row[2]}</td>
                ) : (
                  <td className="paper-btn">{row[2]}</td>
                )}
                
              </tr>
            )
          })
        }

      </table>

    </div>
  );
}
export default TicTacToe;

我已理解胜负判定的索引逻辑:

[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]

但不知道如何结合Redux实现该判定功能。


实现方案

1. 更新Slice状态结构

首先在State接口中添加游戏状态字段,记录胜负状态:

export interface State {
  table: Table
  turn: Turn
  winner: Turn | null // 记录赢家,null表示未分出胜负
  gameOver: boolean // 标记游戏是否结束
}

// 更新初始状态
const initialState: State = {
  table: [
    ['-', '-', '-'],
    ['-', '-', '-'],
    ['-', '-', '-']
  ],
  turn: "X",
  winner: null,
  gameOver: false
}

2. 编写胜负判定函数

在slice中添加工具函数,检查当前棋盘是否有玩家连成一线:

// 定义获胜组合(二维坐标形式)
const winningCombos = [
  // 横向
  [[0,0], [0,1], [0,2]],
  [[1,0], [1,1], [1,2]],
  [[2,0], [2,1], [2,2]],
  // 纵向
  [[0,0], [1,0], [2,0]],
  [[0,1], [1,1], [2,1]],
  [[0,2], [1,2], [2,2]],
  // 斜线
  [[0,0], [1,1], [2,2]],
  [[0,2], [1,1], [2,0]]
];

const checkWinner = (table: Table): Turn | null => {
  for (const combo of winningCombos) {
    const [a, b, c] = combo;
    const cellA = table[a[0]][a[1]];
    const cellB = table[b[0]][b[1]];
    const cellC = table[c[0]][c[1]];
    
    if (cellA !== '-' && cellA === cellB && cellB === cellC) {
      return cellA as Turn;
    }
  }
  return null;
};

3. 修改turn reducer逻辑

落子后立即调用判定函数,更新游戏状态:

export const tactactoeSlice = createSlice({
  name: "tactactoe",
  initialState,
  reducers: {
    turn: (state, action: PayloadAction<TurnAction>) => {
      // 游戏已结束则拦截落子操作
      if (state.gameOver) return;
      
      const {i, j} = action.payload;
      // 确保点击空白格子
      if (state.table[i][j] === '-') {
        state.table[i][j] = state.turn;
        // 检查是否有赢家
        const winner = checkWinner(state.table);
        if (winner) {
          state.winner = winner;
          state.gameOver = true;
        } else {
          // 无赢家则切换回合
          state.turn = state.turn === 'O' ? 'X' : 'O';
        }
      }
    },
    // 添加重置游戏的reducer
    resetGame: () => initialState
  },
});

export const { turn, resetGame } = tactactoeSlice.actions;

4. 更新组件逻辑

展示胜负结果,游戏结束后禁用格子点击:

import { useAppDispatch, useAppSelector } from "../app/hooks";
import { turn, turnSelector, resetGame } from "./ticTacToeSlice";

function TicTacToe() {
  // 直接从Redux获取状态,无需额外useState同步
  const gameState = useAppSelector(turnSelector);
  const dispatch = useAppDispatch();

  function handleTurn(i: number, j: number) {
    dispatch(turn({i, j}));
  }

  return (
    <div className="container">
      {gameState.gameOver ? (
        <h3>赢家是: {gameState.winner}!</h3>
      ) : (
        <h3>当前回合: {gameState.turn}</h3>
      )}
      
      {gameState.gameOver && (
        <button onClick={() => dispatch(resetGame())}>重置游戏</button>
      )}

      <table>
        {gameState.table.map((row, rowIndex) => (
          <tr key={rowIndex}>
            {row.map((cell, colIndex) => (
              <td 
                key={colIndex}
                className="paper-btn"
                onClick={() => {
                  // 仅游戏未结束且格子为空时允许点击
                  if (!gameState.gameOver && cell === '-') {
                    handleTurn(rowIndex, colIndex);
                  }
                }}
              >
                {cell}
              </td>
            ))}
          </tr>
        ))}
      </table>
    </div>
  );
}
export default TicTacToe;

关键说明

  • 胜负判定逻辑放在Redux reducer中,确保状态更新的一致性
  • 游戏结束后直接在reducer中拦截后续落子操作,避免无效状态变更
  • 组件直接从Redux获取状态,简化状态同步逻辑

内容的提问来源于stack exchange,提问作者alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 00:07:01