Compare commits
No commits in common. "develop" and "main" have entirely different histories.
3 changed files with 46 additions and 207 deletions
|
@ -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>
|
||||||
|
|
54
src/App.css
54
src/App.css
|
@ -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;
|
|
||||||
}
|
}
|
197
src/App.tsx
197
src/App.tsx
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue