diff --git a/index.html b/index.html index 0c589ec..0068160 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + React + Tic Tac Toe Bolt
diff --git a/src/App.css b/src/App.css index b9d355d..3bfd7ec 100644 --- a/src/App.css +++ b/src/App.css @@ -5,38 +5,44 @@ text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; +.board { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 1em; } -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); +.messages { + line-height: 1em; } -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); +.board-row { + display: flex; + flex-direction: row; + gap: 10px; } -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +.board-cell { + width: 100px; + height: 100px; + border: 1px solid black; + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + cursor: pointer; } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } +.game-set .board-cell { + cursor: not-allowed; } -.card { - padding: 2em; +.game-set .board-cell:hover { + background-color: unset; } -.read-the-docs { - color: #888; +.board-cell:hover { + background-color: #f0f0f0; } + +.board-cell.fading { + color: red; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index f67355a..ff66e0b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,33 +1,188 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' +import React, { useEffect, useState } from 'react' import './App.css' +type Symbol = 'X' | 'O'; + +type CellState = { + symbol?: Symbol; + isFading?: boolean; +}; + +type RowState = CellState[]; +type BoardState = RowState[]; + +type CellClickHandler = (row: number, col: number) => void; +type CellProps = CellState & { rowIndex: number, columnIndex: number, onClick?: CellClickHandler }; + +function Cell({ rowIndex, columnIndex, symbol, isFading, onClick }: CellProps) { + return
onClick && onClick(rowIndex, columnIndex)}>{symbol}
; +} + +type RowProps = { + index: number, + state: RowState + onCellClick: CellClickHandler; +}; + +function Row({ state, index, onCellClick }: RowProps) { + return ( +
+ {state.map((cellState: CellState, i) => )} +
+ ) +} + +type BoardProps = { + state: BoardState; + hasWinner?: boolean; + onCellClick: CellClickHandler; +} + +function Board({ state, hasWinner, onCellClick }: BoardProps) { + return ( +
+ {state.map((row, i) => )} +
+ ) +} + +function hasWin(board: BoardState, size: number, row: number, column: number): boolean { + const targetCell = board[row]?.[column]; + if (!targetCell) { 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; + } + + 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; + } + + + win = true; + for (let i = 0; i < size; i++) { + if (board[i]?.[i]?.symbol !== targetCell.symbol) { + win = false; + break; + } + } + + 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 [count, setCount] = useState(0) + const [state, { playOn, reset: resetGame }] = useBoardState(5); + + const nextPlayer = state.currentPlayer; + const latestCells = state.latestMoves; + const winner = state.winner; return ( <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- + +

- Edit src/App.jsx and save to test HMR + {winner ? `Winner is ${winner}` : (<>Next player is {nextPlayer})} + {latestCells.length === 0 && ("Click any cell to start")} + {winner && }

-

- Click on the Vite and React logos to learn more -

) }