Compare commits

..

No commits in common. "develop" and "main" have entirely different histories.

3 changed files with 46 additions and 207 deletions

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tic Tac Toe Bolt</title> <title>Vite + React</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -5,44 +5,38 @@
text-align: center; text-align: center;
} }
.board { .logo {
display: flex; height: 6em;
flex-direction: column; padding: 1.5em;
gap: 10px; will-change: filter;
margin-top: 1em; transition: filter 300ms;
} }
.messages { .logo:hover {
line-height: 1em; filter: drop-shadow(0 0 2em #646cffaa);
} }
.board-row { .logo.react:hover {
display: flex; filter: drop-shadow(0 0 2em #61dafbaa);
flex-direction: row;
gap: 10px;
} }
.board-cell { @keyframes logo-spin {
width: 100px; from {
height: 100px; transform: rotate(0deg);
border: 1px solid black; }
display: flex; to {
justify-content: center; transform: rotate(360deg);
align-items: center; }
font-size: 24px;
cursor: pointer;
} }
.game-set .board-cell { @media (prefers-reduced-motion: no-preference) {
cursor: not-allowed; a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
} }
.game-set .board-cell:hover { .card {
background-color: unset; padding: 2em;
} }
.board-cell:hover { .read-the-docs {
background-color: #f0f0f0; color: #888;
}
.board-cell.fading {
color: red;
} }

View file

@ -1,188 +1,33 @@
import React, { useEffect, useState } from 'react' import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css' 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 <div className={`board-cell ${isFading ? 'fading' : ''}`} onClick={() => onClick && onClick(rowIndex, columnIndex)}>{symbol}</div>;
}
type RowProps = {
index: number,
state: RowState
onCellClick: CellClickHandler;
};
function Row({ state, index, onCellClick }: RowProps) {
return (
<div className="board-row">
{state.map((cellState: CellState, i) => <Cell
key={`cell-${index}-${i}`}
rowIndex={index}
columnIndex={i}
{...cellState}
onClick={onCellClick}/>)}
</div>
)
}
type BoardProps = {
state: BoardState;
hasWinner?: boolean;
onCellClick: CellClickHandler;
}
function Board({ state, hasWinner, onCellClick }: BoardProps) {
return (
<div className={`board ${hasWinner ? 'game-set' : ''}`}>
{state.map((row, i) => <Row key={`row-${i}`} index={i} state={row} onCellClick={onCellClick}/>)}
</div>
)
}
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<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() { function App() {
const [state, { playOn, reset: resetGame }] = useBoardState(5); const [count, setCount] = useState(0)
const nextPlayer = state.currentPlayer;
const latestCells = state.latestMoves;
const winner = state.winner;
return ( return (
<> <>
<Board state={state.board} hasWinner={!!winner } onCellClick={playOn}/> <div>
<div className="messages"> <a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p> <p>
<span style={{marginRight: '1em'}}>{winner ? `Winner is ${winner}` : (<>Next player is <em>{nextPlayer}</em></>)}</span> Edit <code>src/App.jsx</code> and save to test HMR
{latestCells.length === 0 && ("Click any cell to start")}
{winner && <button onClick={resetGame}>Click to restart</button>}
</p> </p>
</div> </div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</> </>
) )
} }