Course contents

LLD problem — Tic-Tac-Toe

Sign in to run this lab and save your progress

Learn

Before you start

Tic-Tac-Toe is the smallest problem in this module. On purpose — this is where you practice the discipline of not over-designing.

The over-engineering trap

Every design-pattern student wants to use patterns everywhere. Tic-Tac-Toe is small enough that Factory / Strategy / State are all OVERKILL. Plain classes will do fine.

Your design.md's Patterns section can — and should — say:

"I didn't use any pattern from modules 1-3. Plain classes were the right choice for a game this small. Adding patterns here would add work without any payoff. If the requirements grew (multiple game modes, an AI opponent, replays), I'd revisit."

That's an interview-quality answer. Reaching for patterns just to look like you're using them is a common junior mistake.

Where the real design decisions live

Small problem, but real decisions:

  • Win detection. Compute all winning lines at __init__ (rows + columns + diagonals). Check after every move. OR: check only lines that include the just-placed mark. Which is simpler? Which is faster? design.md is where you make the call.
  • Current player. Store an explicit _current field OR compute it from the move count. The second is cheaper and can't get out of sync. Argue your choice.
  • Winner state. Store an explicit _winner field OR compute it from the board every time. Storing is simpler; computing avoids duplicating info but is slower. Which fits?

Every one of these is a small design decision. An interviewer will ask "why did you do it that way?" for at least one. Your design.md is your rehearsal.

The rules discipline

Even for a small problem, rules exist:

  • Never two marks in one cell
  • Current player alternates strictly (X then O then X...)
  • Game refuses moves after it ends

For each, say WHERE your code enforces it. If the answer is "my play() method checks", say so — the point is that ONE place enforces it, not that every caller has to check.

Your task

Classic 3x3 game. Two players. Simplest LLD problem in the module — good practice reading requirements and deciding what patterns (if any) fit.

The problem

Two players take turns placing their mark on a 3x3 grid:

  • X plays first.
  • Players alternate.
  • A player wins by getting 3 in a row: any row, column, or diagonal.
  • If all 9 squares fill without a win, it's a draw.

Operations:

  • Game(size=3) — construct. Default 3x3 (but the size is a parameter — see design goals).
  • .play(row, col) — current player places their mark. Returns:
    • "ok" — valid move, game continues
    • "X wins" / "O wins" — this move ended the game
    • "draw" — this move filled the board with no winner
  • .play raises ValueError for out-of-bounds, occupied cell, or moves after the game ended.
  • .current_player() — returns "X" or "O".
  • .board() — returns a 2D list of "X" / "O" / None.
  • .winner() — returns "X" / "O" / "draw" / None.

Requirement-gathering questions

  1. Fixed 3x3 or configurable size? — Configurable via the size parameter. Win condition is 3-in-a-row for any size (feel free to keep it that way OR change the win length to size, but pin your choice in design.md).
  2. Two players fixed? — Yes, X and O.
  3. Undo / redo? — Not for MVP.
  4. Multiple games in memory? — Yes, each Game is independent.
  5. Draw detection at every move or lazy? — Every move. Game state transitions on the move that ended it.

Design goals

  • Adding a bigger board should not require rewriting win detection.
  • Adding a new win rule (say, corners-and-center) should be a manageable change.
  • Invariants: never two marks in one cell. Current player is deterministic from move count. Game never accepts a move after ending.

Your task

Write design.md, then implement main.py. Same v3 template as lesson 1 — both are graded.

Public API:

from main import Game

g = Game(size=3)
g.current_player()   # "X"
g.play(0, 0)         # "ok", now O's turn
g.play(1, 1)         # "ok"
...
g.winner()           # "X" | "O" | "draw" | None

This is a small problem. The design.md discussion should be proportionate — don't invent patterns for the sake of it. If plain classes are the right choice, defend that.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
03-tic-tac-toe-starter