feat: make naive proof-of-concept

This commit is contained in:
WanCW 2025-04-25 19:52:34 +08:00
parent 82665f5e58
commit 4d3786c495
Signed by: wancw
GPG key ID: 1A22F8C8D1877952
3 changed files with 172 additions and 51 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>Vite + React</title> <title>Tic Tac Toe Bolt</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

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

View file

@ -1,33 +1,159 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css' 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 <div className={`board-cell ${isFading ? 'fading' : ''}`} id={`cell-${row}-${col}`}
onClick={onClick1}>{symbol}</div>;
}
type RowProps = {
index: number, state: RowState
onCellClick: CellClickHandler;
};
function Row({state, index, onCellClick}: RowProps) {
return (
<div className="board-row">
{state.map((cell: CellState, i) => <Cell key={`cell-${index}-${i}`} row={index} col={i} state={cell}
onClick={onCellClick}/>)}
</div>
)
}
type BoardProps = {
state: BoardState;
onCellClick: CellClickHandler;
}
function Board({state, onCellClick}: BoardProps) {
return (
<div className="board">
{state.map((row, i) => <Row key={`row-${i}`} index={i} state={row} onCellClick={onCellClick}/>)}
</div>
)
}
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() { function App() {
const [count, setCount] = useState(0) const [nextPlayer, setNextPlayer] = useState<Symbol>('O')
const [state, updateState] = useState<CellState[][]>([
[{}, {}, {}],
[{}, {}, {}],
[{}, {}, {}],
])
const [latestCells, updateLatestCells] = useState<{ row: number, col: number }[]>([]);
const [winner, updateWinner] = useState<Symbol | undefined>(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 ( return (
<> <>
<div> <Board state={state} onCellClick={onClick}/>
<a href="https://vite.dev" target="_blank"> <div className="messages">
<img src={viteLogo} className="logo" alt="Vite logo" /> {winner ? <p>Winner is {winner}</p> : <p>Next player is <em>{nextPlayer}</em></p>}
</a> {latestCells.length === 0 && (<p>Click any cell to start</p>)}
<a href="https://react.dev" target="_blank"> {winner && <p onClick={() => {
<img src={reactLogo} className="logo react" alt="React logo" />
</a> updateState((prevState) => prevState.map((row, r) => row.map((cell, c) => {
return {};
})));
updateLatestCells([]);
setNextPlayer('O');
updateWinner(undefined);
}}>Click to restart</p>}
</div> </div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</> </>
) )
} }