feat: make naive proof-of-concept
This commit is contained in:
parent
82665f5e58
commit
4d3786c495
3 changed files with 172 additions and 51 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>Vite + React</title>
|
<title>Tic Tac Toe Bolt</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
47
src/App.css
47
src/App.css
|
@ -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;
|
||||||
}
|
}
|
172
src/App.tsx
172
src/App.tsx
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue