348. Design Tic-Tac-Toe
mediumAsked at AtlassianDesign Tic-Tac-Toe is Atlassian's OOP-design problem. Implement a game where two players take turns marking cells; the move method returns the current player's win state. The grading focus is O(1) per move using per-row, per-column, and diagonal counters — not a full board scan.
By Alex Chen, Founder, InterviewChamp.AI · Last verified
Source citations
Public interview reports confirming this problem appears in Atlassian loops.
- Glassdoor (2026-Q1)— Atlassian SWE-II / Senior onsite reports cite Design Tic-Tac-Toe as a recurring OOP-design problem in the design round.
- Blind (2025-08)— Atlassian threads describe Design Tic-Tac-Toe as 'the clean-class question' that tests whether you can avoid O(n) per move.
Problem
Assume the following rules are for the tic-tac-toe game on an n x n board: 1) A move is guaranteed to be valid and is placed on an empty block. 2) Once a winning condition is reached, no more moves are allowed. 3) A player who succeeds in placing n of their marks in a horizontal, vertical, or diagonal row wins the game. Implement the TicTacToe class: TicTacToe(int n) initializes the n x n board. int move(int row, int col, int player) indicates that the player with id player plays at (row, col). Return the current winning condition (0 = no one wins, 1 = player 1 wins, 2 = player 2 wins). Each move method runs in O(1).
Constraints
2 <= n <= 100player is 1 or 2.1 <= row, col <= n(row, col) are unique for each different call to move.At most n^2 calls will be made to move.
Examples
Example 1
TicTacToe(3); move(0,0,1); move(0,2,2); move(2,2,1); move(1,1,2); move(2,0,1); move(1,0,2); move(2,1,1)[null,0,0,0,0,0,0,1]Approaches
1. Full board scan per move (brute, but explicit)
Store the board as a 2D array. After each move scan the affected row, column, and both diagonals (if applicable) for n-in-a-row.
- Time
- O(n) per move
- Space
- O(n^2)
class TicTacToeBoard {
constructor(n) {
this.n = n;
this.board = Array.from({ length: n }, () => new Array(n).fill(0));
}
move(row, col, player) {
this.board[row][col] = player;
const n = this.n;
let rowAll = true, colAll = true;
for (let i = 0; i < n; i++) {
if (this.board[row][i] !== player) rowAll = false;
if (this.board[i][col] !== player) colAll = false;
}
if (rowAll || colAll) return player;
if (row === col) {
let diag = true;
for (let i = 0; i < n; i++) if (this.board[i][i] !== player) { diag = false; break; }
if (diag) return player;
}
if (row + col === n - 1) {
let anti = true;
for (let i = 0; i < n; i++) if (this.board[i][n - 1 - i] !== player) { anti = false; break; }
if (anti) return player;
}
return 0;
}
}Tradeoff: O(n) per move violates the LeetCode spec — useful only to motivate the counter-based version. Show this and immediately point out 'I can drop the scan with per-row/col/diag counters'.
2. Per-row / per-col / per-diag counters (canonical O(1))
Track signed counts per row, per column, and both diagonals. Player 1 adds +1, player 2 adds -1. After any update, if |count| == n that player wins.
- Time
- O(1) per move
- Space
- O(n)
class TicTacToe {
constructor(n) {
this.n = n;
this.rows = new Array(n).fill(0);
this.cols = new Array(n).fill(0);
this.diag = 0;
this.anti = 0;
}
move(row, col, player) {
const delta = player === 1 ? 1 : -1;
this.rows[row] += delta;
this.cols[col] += delta;
if (row === col) this.diag += delta;
if (row + col === this.n - 1) this.anti += delta;
if (
Math.abs(this.rows[row]) === this.n ||
Math.abs(this.cols[col]) === this.n ||
Math.abs(this.diag) === this.n ||
Math.abs(this.anti) === this.n
) {
return player;
}
return 0;
}
}Tradeoff: The Atlassian-canonical answer. O(1) per move. The signed-delta trick (+1 for player 1, -1 for player 2) collapses two counters per axis into one — saves memory and eliminates a branch. Make this trick explicit when explaining your design.
Atlassian-specific tips
Atlassian uses this problem to grade your class-design instincts. BEFORE coding, narrate the class skeleton: 'fields = rows, cols, diag, anti; method = move(row, col, player)'. Sketch this on the whiteboard. The signed-delta is the load-bearing optimization Atlassian explicitly rewards — without it you'd need separate counters for each player on each axis. After you finish, expect 'now make it work for 4-in-a-row on a 100x100 board' (generalizes cleanly) or 'now handle simultaneous moves with locking' (concurrency tangent).
Common mistakes
- Storing the full 2D board AND counters — the counters alone are enough; the board wastes O(n^2) memory.
- Using two unsigned counters (one per player) per axis — works but doubles the bookkeeping. The signed-delta is cleaner.
- Forgetting Math.abs — the win condition fires when the counter REACHES n in either direction. Without abs, player 2's win never triggers.
Follow-up questions
An interviewer at Atlassian may pivot to one of these next:
- Generalize to n-in-a-row on a larger board (e.g., Connect Four 4-in-a-row on a 7x6 grid).
- Now multiple games are running concurrently — add a per-game lock.
- Detect a draw — track move count; on n*n moves with no winner, declare draw.
Solve it now
Free. No sign-up. Python and JavaScript run instantly in your browser.
FAQ
Why signed delta instead of two separate counters?
Because only one player can win on each axis at a time (n-in-a-row requires monopoly of that line). A signed counter compactly tracks 'how strongly is one player leading this line'. Hitting +n or -n means that line is locked in for one of the two.
Is there an even-faster lookup?
Not in asymptotic terms — O(1) is the floor. In wall-clock terms, indexing into 4 typed arrays beats indexing into 4 plain arrays by a constant. Mention this if asked but don't optimize prematurely.
Free learning resources
Curated free links for this problem.
Practice these live with InterviewChamp.AI
Drill Design Tic-Tac-Toe and other Atlassian interview questions under real-loop conditions with instant feedback on your reasoning, complexity claims, and code.
Practice these live with InterviewChamp.AI →