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 && }
+
>
)