What this prompt is really asking
Actions, errors, scope
Game · Board · Player
makeMove + vectors
Size · undo · bot
Clarifying questions → locked requirements
- You: Do players just pick a column 0–6 and the disc falls? They: Yes — lowest empty row.
- You: Win only, or draws too? They: Four in a row wins; full board = draw.
- You: Full column / out of turn / move after game over? They: Reject clearly; don’t corrupt state.
- You: One game or many concurrent? UI or backend only? They: One game, backend logic only.
Entities: Game, Board, Player
- Game — holds Board + two Players,
currentPlayer,state(IN_PROGRESS / WON / DRAW), optionalwinner. Entry point formakeMove. - Board — 6×7 grid of colours / empty.
placeDisc,checkWin,isFull,canPlace. No idea whose turn it is. - Player — name + colour (RED / YELLOW). Pure data.
Class design from the requirements
from enum import Enum, auto
from typing import Optional
class DiscColor(Enum):
RED = auto()
YELLOW = auto()
class GameState(Enum):
IN_PROGRESS = auto()
WON = auto()
DRAW = auto()
class Player:
def __init__(self, name: str, color: DiscColor):
self.name, self.color = name, color
class Board:
ROWS, COLS = 6, 7
def __init__(self):
self.grid: list[list[Optional[DiscColor]]] = [
[None] * self.COLS for _ in range(self.ROWS)
]
# can_place / place_disc / is_full / check_win — see implementation
class Game:
def __init__(self, player1: Player, player2: Player):
self.board = Board()
self.player1, self.player2 = player1, player2
self.current_player = player1
self.state = GameState.IN_PROGRESS
self.winner: Optional[Player] = None
def make_move(self, player: Player, column: int) -> bool: ...
def get_current_player(self) -> Player: ...
def get_game_state(self) -> GameState: ...
def get_winner(self) -> Optional[Player]: ...
import java.util.*;
enum DiscColor { RED, YELLOW }
enum GameState { IN_PROGRESS, WON, DRAW }
class Player {
String name;
DiscColor color;
Player(String name, DiscColor color) { this.name = name; this.color = color; }
}
class Board {
static final int ROWS = 6, COLS = 7;
DiscColor[][] grid = new DiscColor[ROWS][COLS]; // null = empty
// canPlace / placeDisc / isFull / checkWin — see implementation
}
class Game {
Board board = new Board();
Player player1, player2, currentPlayer;
GameState state = GameState.IN_PROGRESS;
Player winner; // nullable
Game(Player player1, Player player2) {
this.player1 = player1; this.player2 = player2;
this.currentPlayer = player1;
}
boolean makeMove(Player player, int column) { /* ... */ return false; }
Player getCurrentPlayer() { return currentPlayer; }
GameState getGameState() { return state; }
Player getWinner() { return winner; }
}
Implementation highlights
make_move, place_disc, and check_win / count-in-direction.def make_move(self, player: 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 < 0:
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 player is self.player1 else self.player1
)
return True
def place_disc(self, column: int, color: DiscColor) -> int:
if column < 0 or column >= self.COLS or not self.can_place(column):
return -1
for row in range(self.ROWS - 1, -1, -1):
if self.grid[row][column] is None:
self.grid[row][column] = color
return row
return -1
def check_win(self, row: int, col: int, color: DiscColor) -> bool:
if not (0 <= row < self.ROWS and 0 <= col < self.COLS):
return False
if self.grid[row][col] is not color:
return False
for dr, dc in ((0, 1), (1, 0), (1, 1), (1, -1)):
total = 1
total += self._count_in_direction(row, col, dr, dc, color)
total += self._count_in_direction(row, col, -dr, -dc, color)
if total >= 4:
return True
return False
def _count_in_direction(self, row, col, dr, dc, color) -> int:
n, r, c = 0, row + dr, col + dc
while 0 <= r < self.ROWS and 0 <= c < self.COLS and self.grid[r][c] is color:
n += 1
r += dr
c += dc
return n
boolean makeMove(Player player, int column) {
if (state != GameState.IN_PROGRESS) return false;
if (player != currentPlayer) return false;
int row = board.placeDisc(column, player.color);
if (row < 0) return false;
if (board.checkWin(row, column, player.color)) {
state = GameState.WON;
winner = player;
} else if (board.isFull()) {
state = GameState.DRAW;
} else {
currentPlayer = (player == player1) ? player2 : player1;
}
return true;
}
int placeDisc(int column, DiscColor color) {
if (column < 0 || column >= COLS || !canPlace(column)) return -1;
for (int row = ROWS - 1; row >= 0; row--) {
if (grid[row][column] == null) {
grid[row][column] = color;
return row;
}
}
return -1;
}
boolean checkWin(int row, int col, DiscColor color) {
if (!(0 <= row && row < ROWS && 0 <= col && col < COLS)) return false;
if (grid[row][col] != color) return false;
int[][] dirs = {{0, 1}, {1, 0}, {1, 1}, {1, -1}};
for (int[] d : dirs) {
int total = 1;
total += countInDirection(row, col, d[0], d[1], color);
total += countInDirection(row, col, -d[0], -d[1], color);
if (total >= 4) return true;
}
return false;
}
int countInDirection(int row, int col, int dr, int dc, DiscColor color) {
int n = 0, r = row + dr, c = col + dc;
while (0 <= r && r < ROWS && 0 <= c && c < COLS && grid[r][c] == color) {
n++; r += dr; c += dc;
}
return n;
}
Verification trace
- Bottom two cells of col 0 already RED; current = player1.
- P1 → col 0 lands row 3; vertical count = 3 — no win yet.
- P2 → col 1; no win. P1 → elsewhere; P2 → elsewhere.
- P1 → col 0 again → four RED stacked → state=WON, winner=P1.
- P2 tries any column → rejected because state ≠ IN_PROGRESS.
Extensibility follow-ups
- Configurable board size —
Board(rows, cols). Placement and win logic already use bounds helpers; Game just constructs the size you need. - Undo / history — choke point is
Game.make_move. Push(player, row, col)onto a stack; undo clears the cell, restores turn, recomputes state. - Bot opponent — do not shove AI into the Player interface. Keep Player as data; add a
BotEngine.choose_move(game) -> intthat reads the board and returns a column for the game loop to pass intomake_move.
Common interview pitfalls
These mistakes show up constantly on this prompt. Name the trap, then show the fix in your design — don’t wait for the interviewer to catch you.
- Forcing Strategy pattern for win checks when direction vectors on Board are clearer.
- Forgetting gravity — placing in a row/col like Tic Tac Toe.
- Win check only after move in one direction, missing diagonals.
- No draw detection when board is full.
- BotEngine that mutates board without going through make_move.
- Unlimited undo without command/history seam.
Interview script (say this)
Read this once out loud before a mock. It’s the spine of a strong answer — not a script to recite robotically.
- Connect Four: 7×6, gravity drops, alternate colors, four-in-a-row including diagonals.
- Entities: Game, Board, Player; Bot optional behind Player port.
- make_move(column) finds lowest empty row; then win/draw check.
- Win: from last disc, scan 4 directions (and opposites) counting runs.
- Trace: vertical win; full board draw; invalid column full.
- Keep win logic on Board — no premature Strategy.
Extra verification traces
Walk these three traces on the board. If you can narrate them cleanly, your implementation section usually follows.
def make_move(self, column: int) -> Outcome:
row = self.board.drop(column, self.current.color) # raises if full
if self.board.wins_from(row, column, self.current.color):
self.status = "WON"
return Outcome.WIN
if self.board.is_full():
self.status = "DRAW"
return Outcome.DRAW
self.swap_turn()
return Outcome.CONTINUE
enum Outcome { WIN, DRAW, CONTINUE }
Outcome makeMove(int column) {
int row = board.drop(column, current.color); // throws if full
if (board.winsFrom(row, column, current.color)) {
status = "WON";
return Outcome.WIN;
}
if (board.isFull()) {
status = "DRAW";
return Outcome.DRAW;
}
swapTurn();
return Outcome.CONTINUE;
}
Staff-level follow-ups
At staff+, they twist the prompt. Answer in one sentence that names the seam — don’t redesign the whole board.
- Popout / power-ups? — MoveCommand hierarchy; Board applies commands.
- NxM / connect-K? — Board parameterized; win length K.
- Online rematch? — GameFactory + immutable MatchConfig.
- AI difficulty? — BotEngine strategies MiniMax depth — Player interface stable.
Complete solution notes (Connect Four)
from threading import Lock
class GameService:
def __init__(self):
self.games: dict[str, object] = {}
self._locks: dict[str, Lock] = {}
self._map_lock = Lock()
def _lock_for(self, game_id: str) -> Lock:
with self._map_lock:
return self._locks.setdefault(game_id, Lock())
def make_move(self, game_id: str, *args):
with self._lock_for(game_id):
return self.games[game_id].make_move(*args)
# Different game ids → different locks → parallel matches.
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
class GameService {
Map<String, Object> games = new HashMap<>();
Map<String, ReentrantLock> locks = new HashMap<>();
private final ReentrantLock mapLock = new ReentrantLock();
ReentrantLock lockFor(String gameId) {
mapLock.lock();
try {
return locks.computeIfAbsent(gameId, k -> new ReentrantLock());
} finally {
mapLock.unlock();
}
}
Object makeMove(String gameId, Object... args) {
ReentrantLock lock = lockFor(gameId);
lock.lock();
try {
// return games.get(gameId).makeMove(...);
return null;
} finally {
lock.unlock();
}
}
}
// Different game ids → different locks → parallel matches.
Concurrency cases
- Two HTTP moves on one game — without per-game lock, turn checks race.
- Spectators reading board — copy board under lock or accept briefly stale reads.
- Matchmaking — separate lock from game rules lock.