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..b7b6a48 100644 --- a/src/App.css +++ b/src/App.css @@ -5,38 +5,33 @@ text-align: center; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); +.board { + display: flex; + flex-direction: column; + gap: 10px; } -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +.board-row { + display: flex; + flex-direction: row; + gap: 10px; } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } +.board-cell { + width: 100px; + height: 100px; + border: 1px solid black; + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + cursor: pointer; } -.card { - padding: 2em; +.board-cell:hover { + background-color: #f0f0f0; } -.read-the-docs { - color: #888; -} +.board-cell.fading { + color: red; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index f67355a..838919a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,33 +1,159 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' +import { useEffect, useState } from 'react' import './App.css' +import React from 'react'; + +type Symbol = 'X' | 'O'; + +type CellState = { + symbol?: Symbol; + isFading?: boolean; +}; + +type RowState = CellState[]; +type BoardState = RowState[]; + +type CellClickHandler = (row: number, col: number) => boolean; +type CellProps = { + row: number; + col: number; + state: CellState; + onClick?: CellClickHandler; +} + + +function Cell({row, col, state: {symbol, isFading}, onClick}: CellProps) { + const onClick1 = (() => { + onClick && onClick(row, col) + }); + + return
{symbol}
; +} + +type RowProps = { + index: number, state: RowState + onCellClick: CellClickHandler; +}; + +function Row({state, index, onCellClick}: RowProps) { + return ( +
+ {state.map((cell: CellState, i) => )} +
+ ) +} + +type BoardProps = { + state: BoardState; + onCellClick: CellClickHandler; +} + +function Board({state, 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; + + 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 [count, setCount] = useState(0) + 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; + } + + 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'); + return false; + }; + + useEffect(() => { + const winner = findWinner(state); + if (winner) { + updateWinner(winner); + } + }, [state]); return ( <> -
- - Vite logo - - - React logo - + +
+ {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

}
-

Vite + React

-
- -

- Edit src/App.jsx and save to test HMR -

-
-

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

) }