refactor: merge all game state into 1 state variable
This commit is contained in:
parent
4d3786c495
commit
ba260f2895
2 changed files with 150 additions and 110 deletions
13
src/App.css
13
src/App.css
|
@ -9,8 +9,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-row {
|
.board-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -28,6 +31,14 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.game-set .board-cell {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-set .board-cell:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.board-cell:hover {
|
.board-cell:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
247
src/App.tsx
247
src/App.tsx
|
@ -1,6 +1,5 @@
|
||||||
import { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
type Symbol = 'X' | 'O';
|
type Symbol = 'X' | 'O';
|
||||||
|
|
||||||
|
@ -12,147 +11,177 @@ type CellState = {
|
||||||
type RowState = CellState[];
|
type RowState = CellState[];
|
||||||
type BoardState = RowState[];
|
type BoardState = RowState[];
|
||||||
|
|
||||||
type CellClickHandler = (row: number, col: number) => boolean;
|
type CellClickHandler = (row: number, col: number) => void;
|
||||||
type CellProps = {
|
type CellProps = CellState & { rowIndex: number, columnIndex: number, onClick?: CellClickHandler };
|
||||||
row: number;
|
|
||||||
col: number;
|
|
||||||
state: CellState;
|
|
||||||
onClick?: CellClickHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
function Cell({ rowIndex, columnIndex, symbol, isFading, onClick }: CellProps) {
|
||||||
function Cell({row, col, state: {symbol, isFading}, onClick}: CellProps) {
|
return <div className={`board-cell ${isFading ? 'fading' : ''}`} onClick={() => onClick && onClick(rowIndex, columnIndex)}>{symbol}</div>;
|
||||||
const onClick1 = (() => {
|
|
||||||
onClick && onClick(row, col)
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className={`board-cell ${isFading ? 'fading' : ''}`} id={`cell-${row}-${col}`}
|
|
||||||
onClick={onClick1}>{symbol}</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RowProps = {
|
type RowProps = {
|
||||||
index: number, state: RowState
|
index: number,
|
||||||
|
state: RowState
|
||||||
onCellClick: CellClickHandler;
|
onCellClick: CellClickHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Row({state, index, onCellClick}: RowProps) {
|
function Row({ state, index, onCellClick }: RowProps) {
|
||||||
return (
|
return (
|
||||||
<div className="board-row">
|
<div className="board-row">
|
||||||
{state.map((cell: CellState, i) => <Cell key={`cell-${index}-${i}`} row={index} col={i} state={cell}
|
{state.map((cellState: CellState, i) => <Cell
|
||||||
onClick={onCellClick}/>)}
|
key={`cell-${index}-${i}`}
|
||||||
|
rowIndex={index}
|
||||||
|
columnIndex={i}
|
||||||
|
{...cellState}
|
||||||
|
onClick={onCellClick}/>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoardProps = {
|
type BoardProps = {
|
||||||
state: BoardState;
|
state: BoardState;
|
||||||
|
hasWinner?: boolean;
|
||||||
onCellClick: CellClickHandler;
|
onCellClick: CellClickHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Board({state, onCellClick}: BoardProps) {
|
function Board({ state, hasWinner, onCellClick }: BoardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="board">
|
<div className={`board ${hasWinner ? 'game-set' : ''}`}>
|
||||||
{state.map((row, i) => <Row key={`row-${i}`} index={i} state={row} onCellClick={onCellClick}/>)}
|
{state.map((row, i) => <Row key={`row-${i}`} index={i} state={row} onCellClick={onCellClick}/>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findWinner(state: BoardState): Symbol | undefined {
|
function hasWin(board: BoardState, size: number, row: number, column: number): boolean {
|
||||||
// check rows
|
const targetCell = board[row]?.[column];
|
||||||
if (state[0]?.[0]?.symbol === state[0]?.[1]?.symbol && state[0]?.[1]?.symbol === state[0]?.[2]?.symbol)
|
if (!targetCell) { return false; }
|
||||||
return state[0]?.[0]?.symbol;
|
|
||||||
|
|
||||||
if (state[1]?.[0]?.symbol === state[1]?.[1]?.symbol && state[1]?.[1]?.symbol === state[1]?.[2]?.symbol)
|
let win = true;
|
||||||
return state[1]?.[0]?.symbol;
|
for (let r = 0; r < size; r++) {
|
||||||
|
if (board[r]?.[column]?.symbol !== targetCell.symbol) {
|
||||||
if (state[2]?.[0]?.symbol === state[2]?.[1]?.symbol && state[2]?.[1]?.symbol === state[2]?.[2]?.symbol)
|
win = false;
|
||||||
return state[2]?.[0]?.symbol;
|
break;
|
||||||
|
|
||||||
// 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<Symbol>('O')
|
|
||||||
|
|
||||||
const [state, updateState] = useState<CellState[][]>([
|
|
||||||
[{}, {}, {}],
|
|
||||||
[{}, {}, {}],
|
|
||||||
[{}, {}, {}],
|
|
||||||
])
|
|
||||||
|
|
||||||
const [latestCells, updateLatestCells] = useState<{ row: number, col: number }[]>([]);
|
|
||||||
const [winner, updateWinner] = useState<Symbol | undefined>(undefined);
|
|
||||||
|
|
||||||
const onClick = (rowNumber: number, colNumber: number): boolean => {
|
|
||||||
const targetCell = state[rowNumber]?.[colNumber];
|
|
||||||
if (winner || !targetCell || targetCell.symbol) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (win) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
updateState((prevState) => prevState.map((row, r) => row.map((cell, c) => {
|
win = true;
|
||||||
if (r === rowNumber && c === colNumber) {
|
for (let c = 0; c < size; c++) {
|
||||||
return {...cell, symbol: nextPlayer};
|
if (board[row]?.[c]?.symbol !== targetCell.symbol) {
|
||||||
}
|
win = false;
|
||||||
if (latestCells.length >= 5 && latestCells[0] && latestCells[0].row === r && latestCells[0].col === c) {
|
break;
|
||||||
return {...cell, isFading: true};
|
}
|
||||||
}
|
}
|
||||||
return cell.isFading ? {symbol: undefined, isFading: false} : cell;
|
if (win) {
|
||||||
})));
|
return true;
|
||||||
|
}
|
||||||
updateLatestCells((prevState) => {
|
if (row !== column) {
|
||||||
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');
|
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const winner = findWinner(state);
|
win = true;
|
||||||
if (winner) {
|
for (let i = 0; i < size; i++) {
|
||||||
updateWinner(winner);
|
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<GameState>({
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Board state={state} onCellClick={onClick}/>
|
<Board state={state.board} hasWinner={!!winner } onCellClick={playOn}/>
|
||||||
<div className="messages">
|
<div className="messages">
|
||||||
{winner ? <p>Winner is {winner}</p> : <p>Next player is <em>{nextPlayer}</em></p>}
|
<p>
|
||||||
{latestCells.length === 0 && (<p>Click any cell to start</p>)}
|
<span style={{marginRight: '1em'}}>{winner ? `Winner is ${winner}` : (<>Next player is <em>{nextPlayer}</em></>)}</span>
|
||||||
{winner && <p onClick={() => {
|
{latestCells.length === 0 && ("Click any cell to start")}
|
||||||
|
{winner && <button onClick={resetGame}>Click to restart</button>}
|
||||||
updateState((prevState) => prevState.map((row, r) => row.map((cell, c) => {
|
</p>
|
||||||
return {};
|
|
||||||
})));
|
|
||||||
|
|
||||||
updateLatestCells([]);
|
|
||||||
|
|
||||||
setNextPlayer('O');
|
|
||||||
updateWinner(undefined);
|
|
||||||
}}>Click to restart</p>}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue