Design (LLD)  2048 Game - Machine Coding

Design (LLD) 2048 Game - Machine Coding

Here's a brief explanation of the game:

Game Rules:

  1. Game Board:

    • The game is played on a 4x4 grid.

    • Tiles with numbers (powers of 2) are randomly placed on the grid.

  2. Tile Movement:

    • Tiles can be moved in four directions: up, down, left, and right.

    • When a player makes a move, all tiles on the board slide as far as possible in the chosen direction.

  3. Tile Merging:

    • When two tiles with the same number collide while moving, they merge into a tile with the sum of their values.

    • For example, if two tiles with the number 2 collide, they merge into a single tile with the number 4.

  4. Scoring:

    • The player earns points for every merged tile.

    • The goal is to reach the highest possible tile value, ideally reaching the tile with the number 2048.

  5. Game Over:

    • The game ends when there are no more valid moves (i.e., the grid is full, and no tiles can be merged).

Features Required:

  1. Game Board:

    • A 4x4 grid representing the game board.

    • Tiles with values, initially two tiles with a value of 2 or 4.

  2. Game Logic:

    • Merging tiles with the same value when they collide.

    • Scoring based on the merged tiles.

  3. User Input:

    • Accepting user input for movement (left, right, up, down).
  4. Score Calculation:

    • Calculating and updating the score based on merged tiles.

Design Patterns Involved:

  1. Singleton Pattern:

    • GameManager as a singleton to manage the game state.
  2. Observer Pattern:

    • Subject (GameManager) and Observer (UI elements) for reacting to state changes.

    • Observers for changes in the game state.

    • Example: UI elements that update when the game state changes.

  3. Command Pattern:

    • Command interface and concrete command classes for different user inputs.

    • Example: MoveLeftCommand, MoveRightCommand, MoveUpCommand, MoveDownCommand.

  4. Factory Pattern:

    • CommandFactory for creating instances of commands.

    • Example: CommandFactory for creating move commands.

Relationship Diagram

Code:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

// Singleton Pattern
class GameManager {
    private static GameManager instance;
    private int[][] board;
    private List<GameObserver> observers;

    private GameManager() {
        // Private constructor to prevent instantiation
        initializeBoard();
        observers = new ArrayList<>();
    }

    public static GameManager getInstance() {
        if (instance == null) {
            instance = new GameManager();
        }
        return instance;
    }

    private void initializeBoard() {
        // Initialize the game board
        board = new int[4][4];
        addRandomTile();
        addRandomTile();
    }

    private void addRandomTile() {
        // Add a new tile (2 or 4) to a random empty cell
        Random random = new Random();
        int value = (random.nextInt(2) + 1) * 2; // Either 2 or 4
        int row, col;

        do {
            row = random.nextInt(4);
            col = random.nextInt(4);
        } while (board[row][col] != 0);

        board[row][col] = value;
    }

    public void move(Direction direction) {
        // Handle the move logic (left, right, up, down)
        // ...
        // After each move, add a new tile
        addRandomTile();
        notifyObservers(); // Notify observers about the board update
    }

    public int[][] getBoard() {
        return board;
    }

    // Observer Pattern
    public void addObserver(GameObserver observer) {
        observers.add(observer);
    }

    private void notifyObservers() {
        for (GameObserver observer : observers) {
            observer.update(board);
        }
    }
}
// Observer Pattern
interface GameObserver {
    void update(int[][] board);
}

class ScoreManager implements GameObserver {
    private int score;

    public void update(int[][] board) {
        // Update the score based on the current state of the board
        // For simplicity, let's assume the score is the sum of all tile values
        score = calculateScore(board);
        // Notify UI or other components about the score change
        System.out.println("Score Updated: " + score);
    }

    private int calculateScore(int[][] board) {
        int totalScore = 0;
        for (int[] row : board) {
            for (int cell : row) {
                totalScore += cell;
            }
        }
        return totalScore;
    }

    public int getScore() {
        return score;
    }
}

// PlayerData class
class PlayerData {
    private String playerName;
    private int score;

    public PlayerData(String playerName) {
        this.playerName = playerName;
        this.score = 0;
    }

    public int getScore() {
        return score;
    }

    public void updateScore(int points) {
        score += points;
    }
}
// Command Pattern
interface MoveCommand {
    void execute();
}

class MoveLeftCommand implements MoveCommand {
    public void execute() {
        GameManager.getInstance().move(Direction.LEFT);
    }
}

class MoveRightCommand implements MoveCommand {
    public void execute() {
        GameManager.getInstance().move(Direction.RIGHT);
    }
}

class MoveUpCommand implements MoveCommand {
    public void execute() {
        GameManager.getInstance().move(Direction.UP);
    }
}

class MoveDownCommand implements MoveCommand {
    public void execute() {
        GameManager.getInstance().move(Direction.DOWN);
    }
}
// Factory Pattern
class CommandFactory {
    public MoveCommand createCommand(Direction direction) {
        switch (direction) {
            case LEFT:
                return new MoveLeftCommand();
            case RIGHT:
                return new MoveRightCommand();
            case UP:
                return new MoveUpCommand();
            case DOWN:
                return new MoveDownCommand();
            default:
                throw new IllegalArgumentException("Invalid direction");
        }
    }
}
// Enum for directions
enum Direction {
    LEFT, RIGHT, UP, DOWN
}
// Main class
public class Game2048 {
    public static void main(String[] args) {
        GameManager gameManager = GameManager.getInstance();

        // Observer Pattern
        ScoreManager scoreManager = new ScoreManager();
        gameManager.addObserver(scoreManager);

        // Player Data
        PlayerData player = new PlayerData("Player1");

        // Command Pattern
        CommandFactory commandFactory = new CommandFactory();
        MoveCommand moveLeftCommand = commandFactory.createCommand(Direction.LEFT);
        MoveCommand moveRightCommand = commandFactory.createCommand(Direction.RIGHT);

        // Execute commands
        moveLeftCommand.execute();
        moveRightCommand.execute();

        // Get the current state of the board
        int[][] currentBoard = gameManager.getBoard();

        // Print the board
        for (int[] row : currentBoard) {
            for (int cell : row) {
                System.out.print(cell + "\t");
            }
            System.out.println();
        }

        // Observer Pattern: Update the score
        System.out.println("Current Score: " + scoreManager.getScore());

        // Update player data based on the score
        player.updateScore(scoreManager.getScore());
        System.out.println("Player Score: " + player.getScore());
    }
}

This example provides a basic structure for a 2048 game, incorporating the Singleton, Observer, Command, and Factory design patterns. The game manager is a singleton that manages the game state, and observers are notified of changes in the game state. Command classes encapsulate different user inputs, and a factory is used to create instances of these commands.

Did you find this article valuable?

Support Low Level Design (LLD) Coding by becoming a sponsor. Any amount is appreciated!