Design a 2048 Game
Company: Asana
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Onsite
Design the object model and core game logic for a **2048 game** on an `N x N` grid. This is an object-oriented design problem: focus on the classes, their responsibilities, the relationships between them, and the algorithm for a move — not on the UI or rendering.
For reference, 2048 is a sliding-tile puzzle. The player moves all tiles in one of four directions; tiles slide as far as they can, and two adjacent tiles of equal value merge into one tile of their sum. After every move that changes the board, a new tile spawns in a random empty cell.
### Constraints & Assumptions
- The grid is square, `N x N`, with `N = 4` by default but configurable.
- Tile values are powers of two; empty cells are represented by `0`.
- A new tile is a `2` with probability $0.9$ and a `4` with probability $0.1$ (configurable).
- The game starts with two random tiles already placed.
- The default win condition is reaching a `2048` tile; the game may continue after winning.
- The game logic must be deterministically testable — randomness has to be injectable so tests are reproducible.
- Assume a single local player; no networking or persistence is required (but the design should not preclude them).
### The Problem
Produce a class model and the core logic that satisfies the following requirements:
- **Initialization** — start a board with two random tiles.
- **Moves** — support `UP`, `DOWN`, `LEFT`, and `RIGHT`.
- **Slide & merge** — implement the standard rules: tiles slide toward the move direction, and each tile may merge **at most once per move**.
- **Scoring** — increase the score by the value of each merged tile.
- **Tile spawn** — spawn a new tile **only** when a move actually changes the board.
- **State detection** — detect the win state and the game-over state.
- **Separation of concerns** — keep UI/presentation strictly out of the game logic.
Also discuss how you would support these extensions without rewriting the merge logic: **undo**, custom board sizes, deterministic tests, and configurable spawn probabilities.
```hint Look for the repeated operation
Before writing four separate move handlers, ask whether `UP`/`DOWN`/`RIGHT` are genuinely different from `LEFT`, or whether they're the same operation viewed from a different angle. If you can reduce them to one case, what's the smallest unit of the board that operation acts on?
```
```hint The merge rule has a trap
The "each tile merges at most once per move" rule is the easiest thing to get wrong. Pick a concrete line that exposes the ambiguity and decide its result *before* coding: what should `2 2 2` become when slid? Your answer here dictates exactly how your merge loop must advance.
```
```hint Which pieces deserve to be pulled out?
Two parts of this system are the ones you'll most want to test in isolation and vary later: the rule that's easy to get wrong, and the part that's non-deterministic. Consider what the `Game` should depend on so that neither of those is hardwired into it.
```
```hint Game over isn't "board full"
Watch out: a completely full board can still have a legal move. Think about what actually has to be true for *every* direction to be blocked at once — that condition, not "no empty cells," is your game-over test.
```
### Clarifying Questions to Ask
- Is the board always square, and is `N` fixed at 4 or configurable?
- After reaching `2048`, does the game end immediately or can the player keep playing for a higher tile?
- What are the exact spawn probabilities for `2` vs `4`, and must they be configurable?
- Is undo a hard requirement, and if so, how many steps deep?
- Do we need persistence (save/resume) or networking, or is this a single local session?
- Should the score include only merge gains, or are there bonus/penalty rules?
### What a Strong Answer Covers
- A clean class breakdown with single-responsibility classes and clear ownership of state vs. rules vs. randomness vs. presentation.
- The "one line-merge function + grid transforms" insight that unifies the four directions.
- Correct handling of the at-most-one-merge-per-move rule and the move-must-change-board-before-spawn rule.
- Correct, non-trivial win **and** game-over detection (game-over ≠ board full).
- Dependency injection of the random source for deterministic testing.
- A concrete testing strategy (exhaustive line-merge cases, all four directions, seeded randomness, win/lose/undo integration).
- How the chosen abstractions make the extensions (undo, board size, spawn config) cheap, and the time/space complexity of a move ($O(N^2)$).
### Follow-up Questions
- How would you implement **undo** with minimal memory? Compare snapshotting the whole board vs. recording a reversible move delta, including how merges and the spawned tile complicate a pure inverse.
- How would you detect game-over *efficiently* if `N` were large — can you maintain it incrementally instead of scanning the whole board after every move?
- How would you adapt this design to a multiplayer or server-authoritative version where the server must validate moves and prevent cheating?
- How would you generalize from "merge equal values into their sum" to a configurable rule (e.g., a different target tile, or Fibonacci-style merges) without touching the slide logic?
Quick Answer: This question evaluates object-oriented design, state management, deterministic randomness handling, and algorithmic reasoning for implementing game rules on a configurable N x N grid.