From ba260f2895d6a60194c8ede3dedf734153753f55 Mon Sep 17 00:00:00 2001 From: WanCW Date: Mon, 28 Apr 2025 16:50:49 +0800 Subject: [PATCH] refactor: merge all game state into 1 state variable --- src/App.css | 13 ++- src/App.tsx | 247 +++++++++++++++++++++++++++++----------------------- 2 files changed, 150 insertions(+), 110 deletions(-) diff --git a/src/App.css b/src/App.css index b7b6a48..3bfd7ec 100644 --- a/src/App.css +++ b/src/App.css @@ -9,8 +9,11 @@ display: flex; flex-direction: column; gap: 10px; + margin-top: 1em; +} +.messages { + line-height: 1em; } - .board-row { display: flex; flex-direction: row; @@ -28,6 +31,14 @@ cursor: pointer; } +.game-set .board-cell { + cursor: not-allowed; +} + +.game-set .board-cell:hover { + background-color: unset; +} + .board-cell:hover { background-color: #f0f0f0; } diff --git a/src/App.tsx b/src/App.tsx index 838919a..ff66e0b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ -import { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import './App.css' -import React from 'react'; type Symbol = 'X' | 'O'; @@ -12,147 +11,177 @@ type CellState = { type RowState = CellState[]; type BoardState = RowState[]; -type CellClickHandler = (row: number, col: number) => boolean; -type CellProps = { - row: number; - col: number; - state: CellState; - onClick?: CellClickHandler; -} +type CellClickHandler = (row: number, col: number) => void; +type CellProps = CellState & { rowIndex: number, columnIndex: number, onClick?: CellClickHandler }; - -function Cell({row, col, state: {symbol, isFading}, onClick}: CellProps) { - const onClick1 = (() => { - onClick && onClick(row, col) - }); - - return
{symbol}
; +function Cell({ rowIndex, columnIndex, symbol, isFading, onClick }: CellProps) { + return
onClick && onClick(rowIndex, columnIndex)}>{symbol}
; } type RowProps = { - index: number, state: RowState + index: number, + state: RowState onCellClick: CellClickHandler; }; -function Row({state, index, onCellClick}: RowProps) { +function Row({ state, index, onCellClick }: RowProps) { return (
- {state.map((cell: CellState, i) => )} + {state.map((cellState: CellState, i) => )}
) } type BoardProps = { state: BoardState; + hasWinner?: boolean; onCellClick: CellClickHandler; } -function Board({state, onCellClick}: BoardProps) { +function Board({ state, hasWinner, onCellClick }: BoardProps) { return ( -
+
{state.map((row, i) => )}
) - } -function findWinner(state: BoardState): Symbol | undefined { - // check rows - if (state[0]?.[0]?.symbol === state[0]?.[1]?.symbol && state[0]?.[1]?.symbol === state[0]?.[2]?.symbol) - return state[0]?.[0]?.symbol; +function hasWin(board: BoardState, size: number, row: number, column: number): boolean { + const targetCell = board[row]?.[column]; + if (!targetCell) { return false; } - if (state[1]?.[0]?.symbol === state[1]?.[1]?.symbol && state[1]?.[1]?.symbol === state[1]?.[2]?.symbol) - return state[1]?.[0]?.symbol; - - if (state[2]?.[0]?.symbol === state[2]?.[1]?.symbol && state[2]?.[1]?.symbol === state[2]?.[2]?.symbol) - return state[2]?.[0]?.symbol; - - // check columns - if (state[0]?.[0]?.symbol === state[1]?.[0]?.symbol && state[1]?.[0]?.symbol === state[2]?.[0]?.symbol) - return state[0]?.[0]?.symbol; - - if (state[0]?.[1]?.symbol === state[1]?.[1]?.symbol && state[1]?.[1]?.symbol === state[2]?.[1]?.symbol) - return state[0]?.[1]?.symbol; - - if (state[0]?.[2]?.symbol === state[1]?.[2]?.symbol && state[1]?.[2]?.symbol === state[2]?.[2]?.symbol) - return state[0]?.[2]?.symbol; - - // check diagonals - if (state[0]?.[0]?.symbol === state[1]?.[1]?.symbol && state[1]?.[1]?.symbol === state[2]?.[2]?.symbol) - return state[0]?.[0]?.symbol; - - if (state[0]?.[2]?.symbol === state[1]?.[1]?.symbol && state[1]?.[1]?.symbol === state[2]?.[0]?.symbol) - return state[0]?.[2]?.symbol; - - return undefined; -} - -function App() { - const [nextPlayer, setNextPlayer] = useState('O') - - const [state, updateState] = useState([ - [{}, {}, {}], - [{}, {}, {}], - [{}, {}, {}], - ]) - - const [latestCells, updateLatestCells] = useState<{ row: number, col: number }[]>([]); - const [winner, updateWinner] = useState(undefined); - - const onClick = (rowNumber: number, colNumber: number): boolean => { - const targetCell = state[rowNumber]?.[colNumber]; - if (winner || !targetCell || targetCell.symbol) { - return false; + let win = true; + for (let r = 0; r < size; r++) { + if (board[r]?.[column]?.symbol !== targetCell.symbol) { + win = false; + break; } + } + if (win) { + return true; + } - updateState((prevState) => prevState.map((row, r) => row.map((cell, c) => { - if (r === rowNumber && c === colNumber) { - return {...cell, symbol: nextPlayer}; - } - if (latestCells.length >= 5 && latestCells[0] && latestCells[0].row === r && latestCells[0].col === c) { - return {...cell, isFading: true}; - } - return cell.isFading ? {symbol: undefined, isFading: false} : cell; - }))); - - updateLatestCells((prevState) => { - console.info('updating latestCells', {rowNumber, colNumber}); - const appended = [...prevState, {row: rowNumber, col: colNumber}]; - if (appended.length >= 6) { - return appended.slice(1); - } - return appended; - }) - setNextPlayer(nextPlayer === 'X' ? 'O' : 'X'); + win = true; + for (let c = 0; c < size; c++) { + if (board[row]?.[c]?.symbol !== targetCell.symbol) { + win = false; + break; + } + } + if (win) { + return true; + } + if (row !== column) { return false; - }; + } - useEffect(() => { - const winner = findWinner(state); - if (winner) { - updateWinner(winner); + + win = true; + for (let i = 0; i < size; i++) { + if (board[i]?.[i]?.symbol !== targetCell.symbol) { + win = false; + break; } - }, [state]); + } + + return win; +} + +type MoveRecord = { + row: number; + col: number; + player: Symbol; +}; + +type GameState = { + board: BoardState; + latestMoves:MoveRecord[]; + currentPlayer: Symbol; + winner?: Symbol; +} + +function useBoardState(size: number = 3): [GameState, { reset: ()=> void; playOn: (row: number, col: number) => boolean } ] { + const [state, updateState] = useState({ + board: Array(size).fill(Array(size).fill({})), + latestMoves: [], + currentPlayer: 'O', + }); + + return [ + state, + { + reset: () => updateState({ + board: Array(size).fill(Array(size).fill({})), + latestMoves: [], + currentPlayer: 'O', + }), + + playOn: (row: number, col: number) => { + if (state.winner) { + return false; + } + + const targetCell = state.board[row]?.[col]; + if (!targetCell || targetCell.symbol) { + return false; + } + + const cellToFading = state.latestMoves.length >= (2*size-1) ? state.latestMoves[0] : undefined; + const fadingRow = cellToFading?.row; + const fadingCol = cellToFading?.col; + + const nextState: GameState = { + board: state.board.map((oldRow, r) => oldRow.map((oldCell, c)=> { + return { + ...(oldCell.isFading ? {} : oldCell), + ...(r===row && c===col ? { symbol: state.currentPlayer } : undefined), + ...(r===fadingRow && c===fadingCol ? { isFading: true} : undefined), + }; + })), + + latestMoves: [ + ...state.latestMoves, + { + row, + col, + player: state.currentPlayer, + } + ].slice(-2*size+1), + + currentPlayer: state.currentPlayer === 'X' ? 'O' : 'X' as Symbol, + }; + + if (hasWin(nextState.board, size, row, col)) { + nextState.winner = state.currentPlayer; + } + updateState(nextState); + + return true; + }, + }, + ]; +} +function App() { + const [state, { playOn, reset: resetGame }] = useBoardState(5); + + const nextPlayer = state.currentPlayer; + const latestCells = state.latestMoves; + const winner = state.winner; return ( <> - +
- {winner ?

Winner is {winner}

:

Next player is {nextPlayer}

} - {latestCells.length === 0 && (

Click any cell to start

)} - {winner &&

{ - - updateState((prevState) => prevState.map((row, r) => row.map((cell, c) => { - return {}; - }))); - - updateLatestCells([]); - - setNextPlayer('O'); - updateWinner(undefined); - }}>Click to restart

} +

+ {winner ? `Winner is ${winner}` : (<>Next player is {nextPlayer})} + {latestCells.length === 0 && ("Click any cell to start")} + {winner && } +

)