ðŸĢHatchlingMachine CodingGame LogicState Management

Build Tic-Tac-Toe

Tic-tac-toe tests state modeling, win detection algorithms, turn management, and component decomposition. Interviewers often extend it with AI opponents or variable board sizes.

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

  1. Pure game logic — Win/draw detection separated from UI, testable functions
  2. Immutable state — Not mutating the board array directly
  3. Derived state — currentPlayer, status, gameOver computed from board, not stored separately
  4. Accessibility — role="grid", aria-label on cells, aria-live for status announcements
  5. Extensibility thinking — Mentions how to scale to NxN, add undo, or implement AI