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 + React
-
-
setCount((count) => count + 1)}>
- count is {count}
-
+
+
- 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 to restart }
-
- Click on the Vite and React logos to learn more
-
>
)
}