What to model, and what not to
Three things carry the design. A Mark is X or O. A Board is a grid of marks that knows its size, whether a square is on the board and whether it is free. It knows nothing about turns or winning, which is what lets the same board serve tic-tac-toe today and five in a row on a larger grid later.
A Game holds a board, the mark whose turn it is, a move count and a status of IN_PROGRESS, X_WON, O_WON or DRAW. It is a small state machine with one non-terminal state and three terminal ones. Every rule lives here: alternation, legality, winning, drawing.
What you deliberately leave out of the core matters as much. A Player that reads from the console, a renderer and a computer opponent all sit outside the game. They ask the game for its state and submit moves. The game never calls out to ask for input. That inversion is what makes the core testable with five lines of setup and no I/O, and it is worth saying aloud in a machine coding round because it is the decision interviewers most often see missed.
Putting the check in the move
enum Mark {
X, O;
Mark other() { return this == X ? O : X; }
}
final class Board {
private final Mark[][] cells;
Board(int size) { cells = new Mark[size][size]; }
int size() { return cells.length; }
boolean inBounds(int r, int c) { return r >= 0 && r < size() && c >= 0 && c < size(); }
Mark at(int r, int c) { return cells[r][c]; }
void place(int r, int c, Mark m) {
if (!inBounds(r, c)) throw new IllegalArgumentException("off the board");
if (cells[r][c] != null) throw new IllegalArgumentException("square taken");
cells[r][c] = m;
}
}
public final class Game {
public enum { IN_PROGRESS, X_WON, O_WON, DRAW }
Board board;
needed;
Mark.X;
Status.IN_PROGRESS;
;
{
(needed < || needed > size) ();
.board = (size);
.needed = needed;
}
Status {
(status != Status.IN_PROGRESS) ();
(who != toMove) ( + who + );
board.place(row, col, who);
movesMade++;
(completesLine(row, col, who)) {
status = (who == Mark.X) ? Status.X_WON : Status.O_WON;
} (movesMade == board.size() * board.size()) {
status = Status.DRAW;
} {
toMove = who.other();
}
status;
}
{
[][] directions = {{, }, {, }, {, }, {, -}};
([] d : directions) {
+ count(row, col, d[], d[], m) + count(row, col, -d[], -d[], m);
(run >= needed) ;
}
;
}
{
;
row + dr, c = col + dc;
(board.inBounds(r, c) && board.at(r, c) == m) {
n++;
r += dr;
c += dc;
}
n;
}
{
(, );
g.play(Mark.X, , );
g.play(Mark.O, , );
g.play(Mark.X, , );
g.play(Mark.O, , );
System.out.println(g.play(Mark.X, , ));
}
}
Saved as Game.java, this compiles and runs as one file. Three choices in it are worth narrating while you type.
play takes the mark of the player moving rather than inferring it. A client that sends O's move on X's turn gets an exception, not a silently reassigned mark. The board validates the square before the game touches its own state, so a rejected move leaves the move count, the turn and the status exactly as they were.
The win test walks outwards from the square just placed in four directions, adding the runs on each side. A move can only complete a line that passes through it, so there is no reason to rescan the grid. On a three-by-three board that saving is invisible. On a fifteen-by-fifteen board with five in a row it is the difference between walking four lines through one point and checking all 225 squares, and the same code handles both because needed is a parameter.
The status is set in the same method that placed the mark. There is no window in which the board shows a winning line while the game still reports IN_PROGRESS.
Why the win check is not something the caller remembers to call
The common weak design in a machine coding round gives the board a hasWinner() method that scans everything, and leaves a while loop in main to call it after each move. It works in the demo and fails in three ways that an interviewer will probe.
First, the rule now lives in the loop, not the model, so a second caller, such as a test, a network handler or an undo feature, has to repeat it or forget it. Second, nothing stops a move after the game has been won, because the board has no idea a game exists. Third, the board has acquired the win rule, so changing to five in a row means editing storage code.
The ordering detail inside play is the other thing a strong candidate volunteers. Tic-tac-toe can be won on the ninth move, which also fills the board. Test "board full" first and you report a draw for a game X just won. Win first, then draw, then hand over the turn.
If the interviewer pushes on speed, there is a well-known refinement for the full-line rule. Keep a counter per row, per column and for the two diagonals, add one for X and subtract one for O, and a counter reaching plus or minus the board size is a win in constant time. Say its limit too: it only works when a win means filling an entire line, so it does not carry over to five in a row on a larger board.
What to leave visibly unfinished
With the core in place, spend the remaining machine coding time on tests rather than features. The useful ones are a win on each of the four directions, a ninth-move win, a genuine draw, a move after the game ends and a move out of turn. Then name the next steps and the seam each one uses. Undo means keeping a move list in Game and replaying or reversing it. A computer opponent is another caller that reads the board through a read-only view. A networked version needs one place where moves for a game are serialised, which is play itself if the game object is the unit of locking.
Make the move and its consequences one transition owned by the game. A win check the caller must remember to run is a rule that will eventually be skipped.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).