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

View file

@ -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;
}

View file

@ -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 <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() {
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 (
<>
<div>
<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>
<Board state={state} onCellClick={onClick}/>
<div className="messages">
{winner ? <p>Winner is {winner}</p> : <p>Next player is <em>{nextPlayer}</em></p>}
{latestCells.length === 0 && (<p>Click any cell to start</p>)}
{winner && <p onClick={() => {
updateState((prevState) => prevState.map((row, r) => row.map((cell, c) => {
return {};
})));
updateLatestCells([]);
setNextPlayer('O');
updateWinner(undefined);
}}>Click to restart</p>}
</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>
</>
)
}