Build Tic-Tac-Toe
Interview Question: "Build a playable tic-tac-toe game with win detection, turn indicator, and reset functionality."
Requirements
- 3x3 grid
- Alternating X and O turns
- Win detection (rows, columns, diagonals)
- Draw detection
- Game status display
- Reset/restart button
Game Logic (Pure Functions)
type Player = 'X' | 'O';
type Cell = Player | null;
type Board = Cell[];
const WINNING_LINES = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
[0, 4, 8], [2, 4, 6], // diagonals
];
function checkWinner(board: Board): { winner: Player; line: number[] } | null {
for (const line of WINNING_LINES) {
const [a, b, c] = line;
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return { winner: board[a]!, line };
}
}
return null;
}
function isDraw(board: Board): boolean {
return !checkWinner(board) && board.every(cell => cell !== null);
}
function getNextPlayer(board: Board): Player {
const xCount = board.filter(c => c === 'X').length;
const oCount = board.filter(c => c === 'O').length;
return xCount <= oCount ? 'X' : 'O';
}React Implementation
function TicTacToe() {
const [board, setBoard] = useState<Board>(Array(9).fill(null));
const result = checkWinner(board);
const draw = isDraw(board);
const currentPlayer = getNextPlayer(board);
const gameOver = !!result || draw;
const handleClick = (index: number) => {
if (board[index] || gameOver) return;
setBoard(prev => {
const next = [...prev];
next[index] = currentPlayer;
return next;
});
};
const reset = () => setBoard(Array(9).fill(null));
const status = result
? `${result.winner} wins!`
: draw
? "It's a draw!"
: `${currentPlayer}'s turn`;
return (
<div className="ttt-game">
<p className="status" aria-live="polite">{status}</p>
<div className="board" role="grid" aria-label="Tic-Tac-Toe board">
{[0, 1, 2].map(row => (
<div key={row} className="row" role="row">
{[0, 1, 2].map(col => {
const index = row * 3 + col;
const isWinning = result?.line.includes(index);
return (
<button
key={index}
className={`cell ${isWinning ? 'winning' : ''}`}
role="gridcell"
aria-label={`Row ${row + 1}, Column ${col + 1}${board[index] ? `, ${board[index]}` : ', empty'}`}
onClick={() => handleClick(index)}
disabled={!!board[index] || gameOver}
>
{board[index]}
</button>
);
})}
</div>
))}
</div>
<button onClick={reset} className="reset-btn">New Game</button>
</div>
);
}CSS
.board {
display: grid;
grid-template-rows: repeat(3, 1fr);
gap: 4px;
width: 300px;
}
.row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.cell {
aspect-ratio: 1;
font-size: 2.5rem;
font-weight: bold;
border: 2px solid #333;
background: white;
cursor: pointer;
transition: background-color 150ms;
}
.cell:hover:not(:disabled) { background: #f0f0f0; }
.cell:disabled { cursor: default; }
.cell.winning { background: #bbf7d0; }Extensions Interviewers May Ask
Variable Board Size (NxN)
function generateWinningLines(size: number): number[][] {
const lines: number[][] = [];
for (let i = 0; i < size; i++) {
lines.push(Array.from({ length: size }, (_, j) => i * size + j)); // rows
lines.push(Array.from({ length: size }, (_, j) => j * size + i)); // columns
}
lines.push(Array.from({ length: size }, (_, i) => i * size + i)); // diagonal
lines.push(Array.from({ length: size }, (_, i) => i * size + (size - 1 - i))); // anti-diagonal
return lines;
}Simple AI (Minimax)
function minimax(board: Board, isMaximizing: boolean): number {
const result = checkWinner(board);
if (result?.winner === 'O') return 10;
if (result?.winner === 'X') return -10;
if (isDraw(board)) return 0;
const player: Player = isMaximizing ? 'O' : 'X';
let best = isMaximizing ? -Infinity : Infinity;
for (let i = 0; i < board.length; i++) {
if (board[i]) continue;
board[i] = player;
const score = minimax(board, !isMaximizing);
board[i] = null;
best = isMaximizing ? Math.max(best, score) : Math.min(best, score);
}
return best;
}What Interviewers Look For
- Pure game logic â Win/draw detection separated from UI, testable functions
- Immutable state â Not mutating the board array directly
- Derived state â
currentPlayer,status,gameOvercomputed from board, not stored separately - Accessibility â
role="grid",aria-labelon cells,aria-livefor status announcements - Extensibility thinking â Mentions how to scale to NxN, add undo, or implement AI