Common Problems
Connect Four
Try This Problem Yourself
Practice with guided hints and real-time feedback
Watch Video Walkthrough
Watch the author walk through the problem step-by-step
Watch Video Walkthrough
Watch the author walk through the problem step-by-step
Understanding the Problem
Play as RED against a simple AI opponent. The AI will try to win or block you, but it's not perfect!
Requirements
"Build the object-oriented design for a two-player Connect Four game. Players take turns dropping discs into a 7-column, 6-row board. The first to align four of their own discs vertically, horizontally, or diagonally wins."
Clarifying Questions
You: "How do players interact with the game? Do they just specify a column number and the disc drops?"Interviewer: "Yes, players choose a column from 0 to 6, and the disc falls to the lowest available spot."
You: "What are all the ways a game can end? Is it just four in a row, or are there draws?"Interviewer: "Four in a row—vertical, horizontal, or diagonal—wins. If the board fills up with no winner, it's a draw."
You: "What should happen if someone tries to drop a disc in a column that's already full? Should I return an error, throw an exception, or just ignore it?"Interviewer: "Return false or raise an error. Don't let invalid moves break the game state."
You: "And what if a player tries to move out of turn?"Interviewer: "Same thing. Reject it clearly."
You: "Are we designing this to support one game at a time, or do we need to handle multiple concurrent games?"Interviewer: "Just one game. Keep it simple."
You: "Got it. And is this backend logic only, or do we need UI support as well?"Interviewer: "Backend only. Someone else will handle rendering."
You: "Do we need to track move history or support undo?"Interviewer: "No, don't overcomplicate it."
You: "What about board size—does it need to be configurable, or always 7x6?"Interviewer: "Always 7x6."
Final Requirements
Requirements:
1. Two players take turns dropping discs into a 7-column, 6-row board
2. A disc falls to the lowest available row in the chosen column
3. The game ends when:
- A player gets four discs in a row (vertical, horizontal, or diagonal). They win.
- The board is full. It's a draw.
4. Invalid moves should be rejected clearly:
- Dropping in a full column.
- Moving out of turn.
- Moving after the game is over.
Out of scope:
- UI support
- Concurrent games
- Move history
- Undo
- Board size configurationCore Entities and Relationships
| Entity | Responsibility |
|---|---|
| Game | The orchestrator. Holds the Board, tracks which Player's turn it is, manages game state (in progress, won, draw), and enforces turn-based rules. When a player makes a move, Game validates it, tells Board to place the disc, checks if that move won, and switches turns. |
| Board | The 7x6 grid where discs live. Owns the grid state and handles disc placement. Knows how to check if a column is full, where a disc should fall, and whether four discs are connected. Doesn't care about whose turn it is or who's winning. |
| Player | Represents a person in the game. Simple data holder with a name and disc color. No game logic here. |
Class Design
Game
| Requirement | What Game must track |
|---|---|
| "Two players take turns dropping discs into a 7-column, 6-row board" | The two players, whose turn it is, and the board |
| "The game ends when a player wins or the board is full" | The game state (in progress, won, draw) |
| "A player gets four discs in a row. They win." | Who won (if anyone) |
Approach
class Game:
- board: Board
- player1: Player
- player2: Player
- currentPlayer: Player
- isOver: boolean
- hasWinner: boolean
- isDraw: boolean
- winner: Player?makeMove(player, column)
if isOver
return false
// ... place disc ...
if board.checkWin(...)
isOver = true
hasWinner = true
isDraw = false
winner = player
else if board.isFull()
isOver = true
hasWinner = false
isDraw = true
winner = null
// ...Challenges
- isOver=false, hasWinner=true - someone won but the game isn't over?
- isOver=true, hasWinner=true, isDraw=true - both a win and a draw?
- isOver=false, isDraw=true - it's a draw but the game is still going?
Approach
enum GameState:
IN_PROGRESS
WON
DRAWclass Game:
- board: Board
- player1: Player
- player2: Player
- currentPlayer: Player
- state: GameState
- winner: Player?makeMove(player, column)
if state != IN_PROGRESS
return false
// ... place disc ...
if board.checkWin(...)
state = WON
winner = player
else if board.isFull()
state = DRAW
// ...Challenges
enum GameState:
IN_PROGRESS
WON(winner: Player)
DRAWclass Game:
- board: Board
- player1: Player
- player2: Player
- currentPlayer: Player
- state: GameState // IN_PROGRESS, WON, DRAW
- winner: Player? // null if no winner yet or draw| Need from requirements | Method on Game |
|---|---|
| "Players take turns dropping discs" | makeMove(player, column) - the core action |
| "Reject moves out of turn" | getCurrentPlayer() - caller needs to know whose turn it is |
| "The game ends when..." | getGameState() - caller needs to check if game is over |
| "A player gets four discs in a row" | getWinner() - caller needs to know who won |
class Game:
- board: Board
- player1: Player
- player2: Player
- currentPlayer: Player
- state: GameState // IN_PROGRESS, WON, DRAW
- winner: Player? // null if no winner yet or draw
+ Game(player1, player2)
+ makeMove(player, column) -> bool
+ getCurrentPlayer() -> Player
+ getGameState() -> GameState
+ getWinner() -> Player?
+ getBoard() -> BoardGame(player1, player2)
board = Board()
this.player1 = player1
this.player2 = player2
currentPlayer = player1 // player1 goes first
state = IN_PROGRESS
winner = nullBoard
| Requirement | What Board must track |
|---|---|
| "7-column, 6-row board" | Fixed dimensions: number of rows and columns |
| "A disc falls to the lowest available row in the chosen column" | The current occupancy of each column (the grid) |
| "The board is full. It's a draw." | Whether there is at least one empty cell left |
| "A player gets four discs in a row…" | Enough information in the grid to check contiguous discs for a given player |
class Board:
- rows: int // 6
- cols: int // 7
- grid: DiscColor?[rows][cols] // null if empty; otherwise the disc color| Need from requirements | Method on Board |
|---|---|
| "Check that column has space before placing" | canPlace(column) |
| "A disc falls to the lowest available row" | placeDisc(column, color) returns the row |
| "The board is full. It's a draw." | isFull() |
| "A player gets four discs in a row" | checkWin(row, column, color) |
| UI or external code needs to render the grid | getCell(row, column) |
class Board:
- rows: int = 6
- cols: int = 7
- grid: DiscColor?[rows][cols]
+ Board()
+ getRows() -> int
+ getCols() -> int
+ canPlace(column) -> bool
+ placeDisc(column, color) -> int // returns row where disc lands
+ isFull() -> bool
+ checkWin(row, column, color) -> bool
+ getCell(row, column) -> DiscColor?Player
| Requirement | What Player must track |
|---|---|
| "Two players take turns…" | A name or ID so the Game can compare players |
| "their own discs" | The disc color associated with that player |
class Player:
- name: string
- color: DiscColor // RED or YELLOW- name lets the Game determine whose turn it is and validate that the caller of makeMove matches the expected player. This could be a display name or some stable ID—we just need something Game can compare.
- color links placed discs to the correct owner inside the Board grid.
class Player:
- name: string
- color: DiscColor
+ Player(name, color)
+ getName() -> string
+ getColor() -> DiscColorFinal Class Design
class Game:
- board: Board
- player1: Player
- player2: Player
- currentPlayer: Player
- state: GameState // IN_PROGRESS, WON, DRAW
- winner: Player?
+ Game(player1, player2)
+ makeMove(player, column) -> bool
+ getCurrentPlayer() -> Player
+ getGameState() -> GameState
+ getWinner() -> Player?
+ getBoard() -> Board
class Board:
- rows: int = 6
- cols: int = 7
- grid: DiscColor?[rows][cols]
+ Board()
+ getRows() -> int
+ getCols() -> int
+ canPlace(column) -> bool
+ placeDisc(column, color) -> int
+ isFull() -> bool
+ checkWin(row, column, color) -> bool
+ getCell(row, column) -> DiscColor?
class Player:
- name: string
- color: DiscColor
+ Player(name, color)
+ getName() -> string
+ getColor() -> DiscColor
enum GameState:
IN_PROGRESS
WON
DRAW
enum DiscColor:
RED
YELLOWImplementation
- Define the core logic - The happy path that fulfills the requirement.
- Consider edge cases - What can go wrong? Invalid inputs, illegal states, boundary conditions.
- makeMove to show turn enforcement and game flow
- placeDisc to show how discs fall in a column
- checkWin to show directional scanning
Game
- Place the disc via board.placeDisc(column, player.getColor()) -> returns row
- Check for win via board.checkWin(row, column, player.getColor())
- If no win, check for draw via board.isFull()
- Switch turn if game is still in progress
- Return true
- Game is already over (state is WON or DRAW)
- Wrong player's turn
- Invalid column or column is full (delegated to Board)
makeMove(player, column)
if state != IN_PROGRESS
return false
if player != currentPlayer
return false
row = board.placeDisc(column, player.getColor())
if row == -1
return false
if board.checkWin(row, column, player.getColor())
state = WON
winner = player
else if board.isFull()
state = DRAW
else
currentPlayer = (player == player1) ? player2 : player1 // switch turn
return true-
Have makeMove throw exceptions instead of returning false on invalid moves. This can be fine in some languages, but in an interview, a simple boolean result often keeps things clearer. If unsure, ask your interviewer.
-
Have makeMove implicitly use currentPlayer without taking player as an argument. This is simpler if the only code calling makeMove is your own. Taking player explicitly can be useful if we imagine a networked setting where moves arrive tagged with a player. Either choice is reasonable as long as we explain it.
Board
- Find the lowest empty row in that column—start from row = rows - 1 and move upward until you find grid[row][column] == null
- Place the disc—set grid[row][column] = color
- Return the row where the disc landed
- Column index out of bounds -> return error or -1
- Column is full -> return error or -1
placeDisc(column, color)
if column < 0 || column >= cols
return -1
if !canPlace(column)
return -1
for row = rows - 1 down to 0
if grid[row][column] == null
grid[row][column] = color
return row
return -1- Define the four directions: horizontal (0, 1), vertical (1, 0), diagonal down-right (1, 1), diagonal up-right (-1, 1)
- For each direction, count contiguous discs in both directions from (row, column)
- If any direction reaches 4 or more, return true
- Row or column out of bounds → return false
- Cell at (row, column) doesn't match the given color → return false
Approach
interface WinChecker {
bool check(Board board, int row, int col, DiscColor color)
}
class HorizontalWinChecker implements WinChecker {
bool check(Board board, int row, int col, DiscColor color) {
int count = 1
// Count left
for (int c = col - 1; c >= 0 && board.getCell(row, c) == color; c--) {
count++
}
// Count right
for (int c = col + 1; c < board.getCols() && board.getCell(row, c) == color; c++) {
count++
}
return count >= 4
}
}
class VerticalWinChecker implements WinChecker {
bool check(Board board, int row, int col, DiscColor color) {
int count = 1
// Count up
for (int r = row - 1; r >= 0 && board.getCell(r, col) == color; r--) {
count++
}
// Count down
for (int r = row + 1; r < board.getRows() && board.getCell(r, col) == color; r++) {
count++
}
return count >= 4
}
}
class DiagonalDownRightWinChecker implements WinChecker {
bool check(Board board, int row, int col, DiscColor color) {
// Similar counting logic for diagonal...
}
}
class DiagonalUpRightWinChecker implements WinChecker {
bool check(Board board, int row, int col, DiscColor color) {
// Similar counting logic for other diagonal...
}
}
// Then in Board:
bool checkWin(int row, int col, DiscColor color) {
WinChecker[] checkers = {
new HorizontalWinChecker(),
new VerticalWinChecker(),
new DiagonalDownRightWinChecker(),
new DiagonalUpRightWinChecker()
}
for (WinChecker checker : checkers) {
if (checker.check(this, row, col, color)) {
return true
}
}
return false
}Challenges
Approach
checkWin(row, col, color)
directions = [[0,1], [1,0], [1,1], [-1,1]]
for dir in directions
count = 1
count += countInDirection(row, col, dir[0], dir[1], color)
count += countInDirection(row, col, -dir[0], -dir[1], color)
if count >= 4
return true
return false
countInDirection(row, col, dr, dc, color)
count = 0
r = row + dr
c = col + dc
while inBounds(r, c) && grid[r][c] == color
count++
r += dr
c += dc
return countcheckWin(row, col, color)
if row < 0 || row >= rows || col < 0 || col >= cols
return false
if grid[row][col] != color
return false
directions = [[0,1], [1,0], [1,1], [-1,1]]
for dr, dc in directions:
count = 1
count += countInDirection(row, col, dr, dc, color) # move in the direction
count += countInDirection(row, col, -dr, -dc, color) # move in the opposite direction
if count >= 4
return true
return false
countInDirection(row, col, dr, dc, color)
count = 0
r = row + dr
c = col + dc
while inBounds(r, c) && grid[r][c] == color
count++
r += dr
c += dc
return countcanPlace(column)
if column < 0 || column >= cols
return false
return grid[0][column] == null // top row empty means column has space
isFull()
for c = 0 to cols - 1
if canPlace(c)
return false
return true
inBounds(row, col)
return row >= 0 && row < rows && col >= 0 && col < colsPlayer
Complete Code Implementation
from enum import Enum
from typing import Optional
class GameState(Enum):
IN_PROGRESS = "IN_PROGRESS"
WON = "WON"
DRAW = "DRAW"
class Game:
def __init__(self, player1, player2):
self.board = Board()
self.player1 = player1
self.player2 = player2
self.current_player = player1
self.state = GameState.IN_PROGRESS
self.winner: Optional["Player"] = None
def make_move(self, player, column: int) -> bool:
if self.state is not GameState.IN_PROGRESS:
return False
if player is not self.current_player:
return False
row = self.board.place_disc(column, player.color)
if row == -1:
return False
if self.board.check_win(row, column, player.color):
self.state = GameState.WON
self.winner = player
elif self.board.is_full():
self.state = GameState.DRAW
else:
self.current_player = self.player2 if self.current_player is self.player1 else self.player1
return True
def get_current_player(self) -> Player:
return self.current_player
def get_game_state(self) -> GameState:
return self.state
def get_winner(self) -> Optional[Player]:
return self.winner
def get_board(self) -> Board:
return self.board
Verification
Initial state:
Row 5 (bottom): [RED, YELLOW, RED, _, _, _, _]
Row 4: [RED, YELLOW, _, _, _, _, _]
currentPlayer = player1, state = IN_PROGRESS
Move 1: player1 → column 0
placeDisc(0, RED) → row 3
checkWin(3, 0, RED)?
Check vertical: (4,0)=RED, (5,0)=RED → count = 3
No win yet
currentPlayer = player2
Move 2: player2 → column 1
placeDisc(1, YELLOW) → row 3
checkWin(3, 1, YELLOW)?
Check vertical: (4,1)=YELLOW, (5,1)=YELLOW → count = 3
No win yet
currentPlayer = player1
Move 3: player1 → column 2
placeDisc(2, RED) → row 4
checkWin(4, 2, RED)?
Check horizontal: (4,1)=YELLOW → no consecutive 4
No win yet
currentPlayer = player2
Move 4: player2 → column 3
placeDisc(3, YELLOW) → row 5
checkWin(5, 3, YELLOW)? No
currentPlayer = player1
Move 5: player1 → column 0
placeDisc(0, RED) → row 2
checkWin(2, 0, RED)?
Check vertical down: (3,0)=RED, (4,0)=RED, (5,0)=RED
count = 1 + 3 = 4 ✓
Returns true!
state = WON, winner = player1
Move 6: player2 tries column 1
state != IN_PROGRESS → returns false immediatelyExtensibility
1. "How would you support different board sizes?"
"Today I fix the board at 6 rows by 7 columns because that's the requirement. If we wanted configurable sizes, I'd make rows and cols constructor parameters on Board. All of the placement and win logic already works for arbitrary dimensions because it relies on rows, cols, and inBounds. Game doesn't need to change much: it just chooses what size board to construct."
2. "How would you add undo or move history?"
“Undo belongs in Game because Game controls the lifecycle, turn order, and when state changes. I’d keep a moveHistory stack. Each time a move succeeds, I push a small Move record containing the player, row, and column. Undo would pop the last move, clear that cell in the Board, revert currentPlayer, and recalculate game state if needed. The Board doesn’t need any new logic besides maybe an internal clearCell method.”
class Move:
- player: Player
- row: int
- col: int
+ Move(player, row, col)class Game:
- moveHistory: Stack<Move>makeMove(player, column)
...
row = board.placeDisc(column, player.getColor())
moveHistory.push(Move(player, row, column))
...class Board:
+ clearCell(row, col)
grid[row][col] = nullundoLastMove()
if moveHistory.isEmpty()
return false
last = moveHistory.pop()
// revert board state
board.clearCell(last.row, last.col)
// revert turn order
currentPlayer = last.player
// recompute state (simplest version)
state = IN_PROGRESS
winner = null
return true3. "How would you add a computer opponent?"
“I’d keep the game rules exactly where they are. Game and Board don’t need to change. I’d introduce a small bot component that looks at the current board and returns a column. From Game’s perspective, a bot move is just another call to makeMove(currentPlayer, column).”
class BotEngine:
+ chooseMove(game) -> intchooseMove(game)
board = game.getBoard()
for col = 0 to board.getCols() - 1
if board.canPlace(col)
return col
return -1 // no moves availablegame = Game(humanPlayer, botPlayer)
bot = BotEngine()
while game.getGameState() == IN_PROGRESS
current = game.getCurrentPlayer()
if current == humanPlayer
column = /* read from UI / input */
else
column = bot.chooseMove(game)
game.makeMove(current, column)- We don't change Board at all.
- We don't change makeMove or the game rules.
- We just add a thin decision-making layer that chooses a column on behalf of a Player.
What is Expected at Each Level?
Junior
Mid-level
Senior
Mark as read
Currently up to 20% off
Hello Interview Premium
On This Page
Understanding the Problem
Requirements
Clarifying Questions
Final Requirements
Core Entities and Relationships
Class Design
Game
Board
Player
Final Class Design
Implementation
Game
Board
Player
Complete Code Implementation
Verification
Extensibility
1. "How would you support different board sizes?"
2. "How would you add undo or move history?"
3. "How would you add a computer opponent?"
What is Expected at Each Level?
Junior
Mid-level
Senior
Schedule a mock interview
Meet with a FAANG senior+ engineer or manager and learn exactly what it takes to get the job.

Your account is free and you can post anonymously if you choose.