Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions src/components/Board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,12 @@ interface BoardProps {
onSquareClick: (index: number) => void;
}

export default function Board(props: BoardProps) {
export default function Board({ squares, onSquareClick }: BoardProps) {
return (
<div className="board">
<Square value={props.squares[0]} onClick={() => props.onSquareClick(0)} />
<Square value={props.squares[1]} onClick={() => props.onSquareClick(1)} />
<Square value={props.squares[2]} onClick={() => props.onSquareClick(2)} />
<Square value={props.squares[3]} onClick={() => props.onSquareClick(3)} />
<Square value={props.squares[4]} onClick={() => props.onSquareClick(4)} />
<Square value={props.squares[5]} onClick={() => props.onSquareClick(5)} />
<Square value={props.squares[6]} onClick={() => props.onSquareClick(6)} />
<Square value={props.squares[7]} onClick={() => props.onSquareClick(7)} />
<Square value={props.squares[8]} onClick={() => props.onSquareClick(8)} />
{squares.map((sqr, index) => (
<Square value={sqr} onClick={() => onSquareClick(index)} />
))}
</div>
);
}
113 changes: 97 additions & 16 deletions src/components/Game.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,105 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import Board from "./Board";

export default function Game() {
const [squares, setSquares] = useState<(string | null)[]>(
Array(9).fill(null)
);
const [isXNext, setIsXNext] = useState(true);

function handleSquareClick(index: number) {
// Temporary: no gameplay logic yet
console.log("Clicked square:", index);
const [squares, setSquares] = useState<(string | null)[]>(
Array(9).fill(null)
);

const timeLimit = 10;

Comment on lines +8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move constants like 'timeLimit' outside the component.
Defining it inside causes it to be re-initialized on every render,

const [isXNext, setIsXNext] = useState(true);
const [winner, setWinner] = useState<string | null>(null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need a separate state for 'winner' because it can be calculated directly from the 'squares' array during render. Storing it in state can lead to synchronization bugs.

const [time, setTime] = useState(timeLimit);
const [timeRunning, setTimeRunning] = useState(false);

useEffect(() => {
if (!timeRunning || winner) return;

const timerId = window.setInterval(() => {
setTime((currTime) => Math.max(currTime - 1, 0));
}, 1000);

return () => window.clearInterval(timerId);
}, [timeRunning, winner]);

useEffect(() => {
if (!timeRunning || winner) return;

if (time === 0) {
setIsXNext((currPlayer) => !currPlayer);
setTime(timeLimit);
}
}, [time, timeRunning, winner, timeLimit]);

Comment on lines +16 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have two separate UseEffects monitoring the timer. This can cause race conditions where the timer ticks to 0 but the logic to switch players doesn't trigger until the next render cycle.

function handleGameRestart() {
setSquares(Array(9).fill(null));
setIsXNext(true);
setWinner(null);
setTime(timeLimit);
setTimeRunning(false);
}

function handleSquareClick(index: number) {
if (squares[index] !== null || winner) return;

if (!timeRunning) setTimeRunning(true);

const nextSquares = [...squares];
nextSquares[index] = isXNext ? "X" : "O";
setSquares(nextSquares);

const winningPlayer = calculateWinner(nextSquares);
if (winningPlayer) return;

setIsXNext((p) => !p);
setTime(timeLimit);
}

function calculateWinner(boardArr: (string | null)[]) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
] as const;

for (const [a, b, c] of lines) {
if (
boardArr[a] &&
boardArr[a] === boardArr[b] &&
boardArr[a] === boardArr[c]
) {
setWinner(boardArr[a]);
setTimeRunning(false);
return boardArr[a];
}
}
return null;
}

return (
<div className="game-container">
<h1>Asaf's Basic React Tic Tac Toe</h1>

<p>Play timer: {time}</p>

return (
<div className="game-container">
<h1>Tic Tac Toe</h1>
<Board squares={squares} onSquareClick={handleSquareClick} />
<p>{winner == null ? null : "Winner: " + winner}</p>

<Board squares={squares} onSquareClick={handleSquareClick} />
<p>Next Player: {isXNext ? "X" : "O"}</p>
<p>
{winner == null
? isXNext
? "Next Player: X"
: "Next Player: O"
: null}
Comment on lines +95 to +99
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid nested ternary operators in the return block. Calculate a 'status' string before the return statement and just display {status} here.

</p>

</div>
);
<button onClick={handleGameRestart}>Restart</button>
</div>
);
}
20 changes: 11 additions & 9 deletions src/components/Square.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
interface SquareProps {
value: string | null;
onClick: () => void;
value: string | null;
onClickFunction: () => void;
}

export default function Square(props: SquareProps) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
export const Square = ({ value, onClickFunction }: SquareProps) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are using both a Named Export (export const) and a Default Export at the bottom. It's better to stick to one.

return (
<button className="square" onClick={onClickFunction}>
{value}
</button>
);
};

export default Square;