Game 2048: Algorithmic Puzzle with Array Reversal Conjugation

Overview

Game 2048 is a Java implementation of the classic sliding-tile puzzle built on the Stanford ACM Graphics Library (acm.jar). The core algorithmic design reduces all four directional moves into a single three-phase 1D array consolidation pipeline (compact → merge → re-compact), with opposite directions unified via array reversal conjugation — reversing the array, applying the canonical left-ward combine, then reversing again. Game-over detection uses a multiplicative triple-product invariant: the product of all cell values × all horizontal differences × all vertical differences is nonzero if and only if no valid move exists. The rendering system maintains a dual-layer board representation — an int[][] model array as the authoritative game state and a parallel GCompound[][] view array of composite graphics objects — with logarithmic color cycling (log2(value) % 6) that automatically scales to arbitrarily high tile values.

Three-Phase Tile Movement Pipeline

Core 1D Consolidation

Every directional move (UP, DOWN, LEFT, RIGHT) is reduced to a 1D array consolidation problem. Rows or columns are extracted from the 4×4 grid, processed through a three-phase pipeline, and written back:

Phase 1 — Compact (moveZerosToEnd): Shifts all non-zero values to the front, pushing zeros to the end via reverse-iteration bubble shift:

private int[] moveZerosToEnd(int[] array) {
    for (int i = length - 1; i >= 0; i--) {
        if (array[i] == 0) {
            for (int j = i; j < length - 1; j++)
                array[j] = array[j+1];
            array[length-1] = 0;
        }
    }
    return array;
}

Phase 2 — Merge (addIdenticalNumbers): After compaction, adjacent equal values are merged by doubling the first and zeroing the second. The left-to-right single-pass correctly prevents chain-merging (e.g., [2,2,2,2][4,0,4,0], not [8,0,0,0]):

private int[] addIdenticalNumbers(int[] array) {
    for (int i = 0; i < length - 1; i++) {
        if (array[i] == array[i+1]) {
            array[i] *= 2;
            array[i+1] = 0;
        }
    }
    return array;
}

Phase 3 — Re-compact (moveZerosToEnd again): Zeros created by merging are pushed to the end.

The full pipeline is encapsulated as:

private int[] combineArraytoBeginning(int[] array) {
    return moveZerosToEnd(addIdenticalNumbers(moveZerosToEnd(array)));
}

Array Reversal Conjugation for Directional Symmetry

Rather than implementing four separate movement algorithms, opposite directions are unified via conjugation by reversal — a technique analogous to matrix conjugation in linear algebra:

private int[] combineArraytoEnd(int[] array) {
    return reverseArray(combineArraytoBeginning(reverseArray(array)));
}

Moving “toward the end” = reverse → combine toward beginning → reverse. This cuts the core logic by ~50%.

Direction Data Slice Combine Function
UP Column j (top→bottom) combineArraytoBeginning()
DOWN Column j (top→bottom) combineArraytoEnd() (conjugation)
LEFT Row i (left→right) combineArraytoBeginning()
RIGHT Row i (left→right) combineArraytoEnd() (conjugation)

For UP/DOWN, columns are extracted into temporary 1D arrays via updateColumn(); for LEFT/RIGHT, rows (gridNum[i]) are passed directly via updateRow().

Multiplicative Game-Over Detection

The gameOver() method uses a mathematically elegant triple-product invariant:

private boolean gameOver() {
    int product1 = 1;  // Product of all cell values
    int product2 = 1;  // Product of horizontal differences
    int product3 = 1;  // Product of vertical differences

    for (i, j): product1 *= gridNum[i][j];              // Zero if any cell empty
    for (i, j): product2 *= (gridNum[i][j] - gridNum[i][j+1]);  // Zero if horizontal match
    for (i, j): product3 *= (gridNum[i][j] - gridNum[i+1][j]);  // Zero if vertical match

    return (product1 * product2 * product3 != 0);
}

The game is over if and only if all three products are nonzero simultaneously:

  • product1 ≠ 0: No empty cells (all tiles filled)
  • product2 ≠ 0: No horizontally adjacent equal tiles (no horizontal merges possible)
  • product3 ≠ 0: No vertically adjacent equal tiles (no vertical merges possible)

If ANY condition is unsatisfied, the combined product is zero → game continues. This replaces verbose nested conditionals with a single algebraic expression.

Dual-Layer Board Representation

The game maintains two parallel 4×4 arrays forming a model-view separation:

Layer Type Role
Model int[][] gridNum Authoritative game state — 0 = empty, 2^k = tile value
View GCompound[][] gridSquare Renderable composite objects (colored GRect + centered GLabel)

Full-redraw strategy: After every move, all 16 tiles are removed from the canvas (removeAllSquares()) and redrawn from scratch (updateGraphics()) — a deliberate simplicity tradeoff where the O(n²) cost is negligible for n=4.

Logarithmic Color Cycling

Colors are assigned via log2(value) % 6, creating a cyclic 6-color palette that automatically scales to arbitrarily high tile values without a hardcoded color map:

log2(value) % 6 Color Example Tiles
1 YELLOW 2, 128, 8192
2 GREEN 4, 256, 16384
3 PINK 8, 512, 32768
4 CYAN 16, 1024, 65536
5 ORANGE 32, 2048, 131072
0 MAGENTA 64, 4096, 262144

Empty tiles (value 0) render as LIGHT_GRAY. The logarithmic mapping ensures visually distinct tiles at every power-of-two level while gracefully wrapping at higher values.

Stanford ACM Graphics Library

The application extends GraphicsProgram from Stanford’s acm.jar, which wraps Java AWT/Swing into a pedagogical graphics framework:

Component Specification
Window 600×800 pixels
Header zone “2048” logo (120×120 green square, red bold text) + max tile display (magenta)
Game grid 4×4 tiles, each ~137.5px ((600 - 5×10) / 4), 10px spacing
Controls 4 JButton instances (UP, DOWN, LEFT, RIGHT) in SOUTH region
Tile rendering GCompound = filled GRect (colored) + GLabel (SansSerif-40, centered)
Random generator RandomGenerator.getInstance() singleton PRNG

Configuration via Interface Constants

All layout parameters are externalized into Game2048Constants, which the main class implements:

public interface Game2048Constants {
    int APPLICATION_WIDTH = 600, APPLICATION_HEIGHT = 800;
    int DIMENSION = 4, WINNING_NUMBER = 2048;
    double SPACE = 10, LOGO_SIZE = 120;
    double SQUARE_SIZE = (APPLICATION_WIDTH - (DIMENSION + 1) * SPACE) / DIMENSION;
}

Using a Java interface for constants means values are implicitly public static final and directly accessible without qualification in implementing classes.

New Tile Spawning

After each move, a single tile with value 2 is placed at a random empty cell via rejection sampling — repeatedly generating random (i, j) coordinates until an empty cell (gridNum[i][j] == 0) is found:

private void addNewNumbers() {
    if (hasEmptySpace()) {
        while (true) {
            int i = rgen.nextInt(0, DIMENSION-1);
            int j = rgen.nextInt(0, DIMENSION-1);
            if (gridNum[i][j] == 0) { gridNum[i][j] = 2; break; }
        }
    }
}

The hasEmptySpace() check uses a multiplicative invariant: the product of all 16 cell values is zero if and only if at least one cell is empty, avoiding explicit branching/short-circuiting.

Event Loop and Win/Loss Detection

public void actionPerformed(ActionEvent e) {
    String cmd = e.getActionCommand();
    if (cmd.equals("UP")) moveUp();
    else if (cmd.equals("DOWN")) moveDown();
    else if (cmd.equals("LEFT")) moveLeft();
    else if (cmd.equals("RIGHT")) moveRight();
    addNewNumbers();
    updateGraphics();
}

Button clicks dispatch directional moves via ActionListener. The win condition checks max >= 2048 but does not terminate play — a congratulatory label appears while the game continues. The game-over condition triggers the triple-product invariant check. The “score” is the maximum tile value on the board, recalculated via full O(n²) scan after every move.

Demo

Tech Stack

Java (8+), Stanford ACM Graphics Library (acm.jar — GraphicsProgram, GCompound, GRect, GLabel, GObject), AWT/Swing (JButton, ActionListener)