Skip to content
Snippets Groups Projects

Enhanced 2048 Game by Jamal Enoime

A terminal-based version of the popular 2048 puzzle game with multiple game modes and visual enhancements.

Project Structure

2048-game/
├── bin/                    # Compiled executable
├── include/                # Header files
│   ├── 2048.hpp           # Game logic header
│   ├── Tile.hpp           # Tile class header
│   ├── GameMode.hpp       # Game modes header
│   ├── UI.hpp             # User interface header
│   ├── Animations.hpp     # Animation system header
│   └── SoundSystem.hpp    # Sound system header
├── src/                    # Source files
│   ├── 2048.cpp           # Game logic implementation
│   ├── Tile.cpp           # Tile class implementation
│   ├── GameMode.cpp       # Game modes implementation
│   ├── UI.cpp             # User interface implementation
│   ├── Animations.cpp     # Animation system implementation
│   ├── SoundSystem.cpp    # Sound system implementation
│   └── main.cpp           # Entry point
├── obj/                    # Object files (generated during build)
├── res/                    # Resource files
│   ├── sounds/            # Sound effects
│   ├── music/             # Background music
│   └── images/            # Images and GIFs
├── Makefile               # Build configuration
└── README.md              # Project documentation

Features

  • Interactive 2048-terminal clone
  • Multiple Game Modes:
    • Classic: Traditional 2048 gameplay
    • Timed: Race against the clock
    • Power-Up: Use special abilities
    • Challenge: Complete specific objectives with constraints
    • Zen: Relaxed mode with no game over
  • Animations and Effects:
    • Particle effects for special events
    • Confetti celebration for victories
    • Text effects and transitions between screens
  • Sound System:
    • Sound effects for game actions
    • Background music for different game states
  • Customization:
    • Multiple color themes
    • High contrast mode for accessibility
    • Toggle for sounds, music, and animations
  • Tutorial: Interactive guide to teach new players
  • Game Statistics: Track high scores and performance

Screenshots

  • the directories
  • Building the game
  • game ui
  • user friendly tutorial
  • actual gameplay

Dependencies

  • NCurses: For terminal-based UI
  • SDL2 and SDL2_mixer: For sound and music

Installing Dependencies

For Ubuntu/Debian:

# Install NCurses
sudo apt-get install libncurses5-dev libncursesw5-dev

# Install SDL2 and SDL2_mixer
sudo apt-get install libsdl2-dev libsdl2-mixer-dev

For macOS (with Homebrew):

brew install ncurses sdl2 sdl2_mixer

For Windows:

Use WSL (Windows Subsystem for Linux) and follow the Ubuntu/Debian instructions.

Setting Up Sound Files

For the music and sound effects to work properly:

  1. Create directories for sounds and music:
mkdir -p res/sounds res/music
  1. Place your sound effect files in the res/sounds directory
  2. Place your background music files in the res/music directory
  3. Supported audio formats include WAV, MP3, and OGG (depending on your SDL2_mixer build)

How to Build and Run

# Build the project
make

# Run the game
./bin/2048

# Clean build files
make clean

Controls

  • Arrow keys: Move tiles / Navigate menus
  • Enter: Select menu options
  • P: Pause game
  • R: Restart game
  • Q/ESC: Quit/Return to menu

Special Mode Controls

  • Power-Up Mode:

    • 1: Activate double score
    • 2: Clear a row
    • 3: Undo move
    • 4: Upgrade a tile
  • Zen Mode:

    • C: Toggle color shift effect

Issues and Solutions

Known Issues

  • Gameplay Fragmentation: The gameplay sometimes fragments due to a logical error in the code.
  • Sound Issues: Sound files might not play correctly if not properly set up.

Solutions

To get a fully functional version of the game with no bugs but fewer features, replace the following files with the code provided below:

UI.cpp

#include "UI.hpp"
#include <cmath>
#include <algorithm>
#include <chrono>
#include <thread>

// Credit text with author name
const std::string CREDIT_TEXT = "2048 Game";

UI::UI()
    : currentState(UIState::MAIN_MENU), previousState(UIState::MAIN_MENU), selectedMenuOption(0),
      mainWindow(nullptr), gameWindow(nullptr), scoreWindow(nullptr), infoWindow(nullptr),
      currentGameMode(GameModeType::CLASSIC), backgroundEffectTimer(0.0f),
      animationsEnabled(true), currentTheme(0) {
    
    // Initialize menu options
    mainMenuOptions = {"Play Game", "Select Mode", "Settings", "Tutorial", "Quit"};
    modeSelectionOptions = {"Classic", "Back"};
    pauseMenuOptions = {"Resume", "Restart", "Main Menu", "Quit"};
    settingsOptions = {"Animations: ON", "Theme: Default", "Back"};
    
    // Initialize themes
    initializeThemes();
}

UI::~UI() {
    destroyWindows();
    endwin();
}

bool UI::initialize() {
    // Initialize ncurses
    initscr();
    start_color();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    curs_set(0);  // Hide cursor
    timeout(50);  // Non-blocking input with ~20fps refresh rate
    
    if (!has_colors()) {
        endwin();
        return false;
    }
    
    // Initialize color pairs
    init_pair(1, COLOR_BLACK, COLOR_WHITE);      // Default
    init_pair(2, COLOR_BLACK, COLOR_GREEN);      // 2
    init_pair(3, COLOR_BLACK, COLOR_CYAN);       // 4
    init_pair(4, COLOR_BLACK, COLOR_BLUE);       // 8
    init_pair(5, COLOR_BLACK, COLOR_MAGENTA);    // 16
    init_pair(6, COLOR_BLACK, COLOR_RED);        // 32
    init_pair(7, COLOR_WHITE, COLOR_BLUE);       // 64
    init_pair(8, COLOR_WHITE, COLOR_MAGENTA);    // 128
    init_pair(9, COLOR_WHITE, COLOR_RED);        // 256
    init_pair(10, COLOR_WHITE, COLOR_YELLOW);    // 512
    init_pair(11, COLOR_BLACK, COLOR_YELLOW);    // 1024
    init_pair(12, COLOR_WHITE, COLOR_GREEN);     // 2048
    init_pair(13, COLOR_WHITE, COLOR_RED);       // Higher
    init_pair(14, COLOR_YELLOW, COLOR_BLACK);    // Gold text
    init_pair(15, COLOR_CYAN, COLOR_BLACK);      // Cyan text
    
    // Initialize game
    game = Game2048();
    
    // Create windows
    createWindows();
    
    // Set up game callbacks
    game.addObserver([this](GameEvent event, int row, int col, int value) {
        switch (event) {
            case GameEvent::TILE_MERGED:
                onTileMerged(row, col, value);
                break;
            case GameEvent::TILE_SPAWNED:
                onTileSpawned(row, col, value);
                break;
            case GameEvent::SCORE_CHANGED:
                onScoreChanged(value, game.getScore() - value);
                break;
            case GameEvent::GAME_WON:
                onVictory();
                break;
            case GameEvent::GAME_OVER:
                onGameOver();
                break;
            default:
                break;
        }
    });
    
    // Set default game mode
    setGameMode(GameModeType::CLASSIC);
    
    return true;
}

void UI::createWindows() {
    int maxY, maxX;
    getmaxyx(stdscr, maxY, maxX);
    
    // Main window covers the entire screen
    mainWindow = newwin(maxY, maxX, 0, 0);
    keypad(mainWindow, TRUE);
    
    // Game window for the actual 2048 board
    gameWindow = newwin(SIZE * 2 + 2, SIZE * 6 + 2, 2, (maxX - (SIZE * 6 + 2)) / 2);
    
    // Score window for displaying score and other game info
    scoreWindow = newwin(3, 20, 2, maxX - 25);
    
    // Info window for instructions and status messages
    infoWindow = newwin(5, maxX - 4, maxY - 6, 2);
}

void UI::destroyWindows() {
    if (mainWindow) delwin(mainWindow);
    if (gameWindow) delwin(gameWindow);
    if (scoreWindow) delwin(scoreWindow);
    if (infoWindow) delwin(infoWindow);
}

void UI::initializeThemes() {
    // Default theme
    Theme defaultTheme;
    defaultTheme.name = "Default";
    
    // Dark theme
    Theme darkTheme;
    darkTheme.name = "Dark";
    
    // Add themes to the list
    themes.push_back(defaultTheme);
    themes.push_back(darkTheme);
}

void UI::applyTheme(int themeIndex) {
    if (themeIndex < 0 || themeIndex >= static_cast<int>(themes.size())) {
        return;
    }
    
    currentTheme = themeIndex;
    
    // Apply theme colors
    const Theme& theme = themes[currentTheme];
    
    // Update settings menu
    settingsOptions[1] = "Theme: " + theme.name;
}

void UI::run() {
    auto lastTime = std::chrono::high_resolution_clock::now();
    
    while (currentState != UIState::EXIT_CONFIRM) {
        // Calculate delta time
        auto currentTime = std::chrono::high_resolution_clock::now();
        std::chrono::duration<float> duration = currentTime - lastTime;
        float deltaTime = duration.count();
        lastTime = currentTime;
        
        // Update background effect timer
        backgroundEffectTimer += deltaTime;
        
        // Update game mode
        if (currentState == UIState::GAME_PLAYING && gameMode) {
            gameMode->update(game, deltaTime);
        }
        
        // Draw UI based on current state
        switch (currentState) {
            case UIState::MAIN_MENU:
                drawMainMenu();
                handleMainMenuInput();
                break;
                
            case UIState::MODE_SELECTION:
                drawModeSelection();
                handleModeSelectionInput();
                break;
                
            case UIState::GAME_PLAYING:
                drawBoard();
                drawScore();
                drawInfo();
                handleGameInput();
                break;
                
            case UIState::PAUSE_MENU:
                drawBoard(); // Draw board in background
                drawPauseMenu();
                handlePauseMenuInput();
                break;
                
            case UIState::GAME_OVER:
                drawBoard(); // Draw board in background
                drawGameOver();
                handlePauseMenuInput(); // Reusing pause menu input handler
                break;
                
            case UIState::VICTORY:
                drawBoard(); // Draw board in background
                drawVictory();
                handlePauseMenuInput(); // Reusing pause menu input handler
                break;
                
            case UIState::TUTORIAL:
                drawTutorial();
                handleTutorialInput();
                break;
                
            case UIState::SETTINGS:
                drawSettings();
                handleSettingsInput();
                break;
                
            case UIState::EXIT_CONFIRM:
                drawExitConfirm();
                handleExitConfirmInput();
                break;
        }
        
        // Limit frame rate to reduce CPU usage
        std::this_thread::sleep_for(std::chrono::milliseconds(50)); // ~20 FPS
    }
}

void UI::setState(UIState newState) {
    previousState = currentState;
    currentState = newState;
    selectedMenuOption = 0;
}

UIState UI::getState() const {
    return currentState;
}

void UI::setGameMode(GameModeType mode) {
    currentGameMode = mode;
    
    // Create appropriate game mode instance - C++11 compatible version
    switch (mode) {
        case GameModeType::CLASSIC:
            gameMode.reset(new ClassicMode());
            break;
        default:
            gameMode.reset(new ClassicMode());
            break;
    }
    
    // Initialize the game mode
    if (gameMode) {
        gameMode->initialize(game);
    }
}

GameModeType UI::getGameMode() const {
    return currentGameMode;
}

void UI::setTheme(int themeIndex) {
    applyTheme(themeIndex);
}

int UI::getThemeCount() const {
    return static_cast<int>(themes.size());
}

std::string UI::getThemeName(int index) const {
    if (index >= 0 && index < static_cast<int>(themes.size())) {
        return themes[index].name;
    }
    return "Unknown";
}

void UI::toggleAnimations() {
    animationsEnabled = !animationsEnabled;
    settingsOptions[0] = std::string("Animations: ") + (animationsEnabled ? "ON" : "OFF");
}

// Drawing helpers
void UI::drawCenteredText(WINDOW* win, int y, const std::string& text, bool highlight) {
    int maxX = getmaxx(win);
    
    int x = (maxX - text.length()) / 2;
    
    if (highlight) wattron(win, A_REVERSE);
    mvwprintw(win, y, x, "%s", text.c_str());
    if (highlight) wattroff(win, A_REVERSE);
}

void UI::drawBorderedWindow(WINDOW* win, const std::string& title) {
    int maxX = getmaxx(win);
    
    // Draw the border
    box(win, 0, 0);
    
    // Draw the title if provided
    if (!title.empty()) {
        int titleX = (maxX - title.length()) / 2;
        wattron(win, A_BOLD);
        mvwprintw(win, 0, titleX, " %s ", title.c_str());
        wattroff(win, A_BOLD);
    }
}

void UI::drawTile(WINDOW* win, int row, int col, int value) {
    int colorPair;
    switch (value) {
        case 0: colorPair = 1; break;   // Empty tile
        case 2: colorPair = 2; break;
        case 4: colorPair = 3; break;
        case 8: colorPair = 4; break;
        case 16: colorPair = 5; break;
        case 32: colorPair = 6; break;
        case 64: colorPair = 7; break;
        case 128: colorPair = 8; break;
        case 256: colorPair = 9; break;
        case 512: colorPair = 10; break;
        case 1024: colorPair = 11; break;
        case 2048: colorPair = 12; break;
        default: colorPair = 13; break; // Higher values
    }
    
    // Draw the tile with color if not empty
    wattron(win, COLOR_PAIR(colorPair) | A_BOLD);
    mvwprintw(win, row + 1, col * 6 + 1, "%5d", value == 0 ? 0 : value);
    wattroff(win, COLOR_PAIR(colorPair) | A_BOLD);
}

// Menu drawing functions
void UI::drawMainMenu() {
    werase(mainWindow);
    
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    // Draw title
    wattron(mainWindow, COLOR_PAIR(14) | A_BOLD);
    drawCenteredText(mainWindow, maxY / 4, "2048 GAME", false);
    wattroff(mainWindow, COLOR_PAIR(14) | A_BOLD);
    
    // Menu options
    for (size_t i = 0; i < mainMenuOptions.size(); i++) {
        int y = maxY / 3 + i * 2 + 5;
        
        if (static_cast<int>(i) == selectedMenuOption) {
            wattron(mainWindow, A_REVERSE | A_BOLD);
            drawCenteredText(mainWindow, y, mainMenuOptions[i], true);
            wattroff(mainWindow, A_REVERSE | A_BOLD);
        } else {
            drawCenteredText(mainWindow, y, mainMenuOptions[i], false);
        }
    }
    
    // Footer
    wattron(mainWindow, A_DIM);
    drawCenteredText(mainWindow, maxY - 2, "Use UP/DOWN to navigate, ENTER to select", false);
    wattroff(mainWindow, A_DIM);
    
    wrefresh(mainWindow);
}

void UI::drawModeSelection() {
    werase(mainWindow);
    
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    // Title
    wattron(mainWindow, COLOR_PAIR(14) | A_BOLD);
    drawCenteredText(mainWindow, maxY / 4, "SELECT GAME MODE", false);
    wattroff(mainWindow, COLOR_PAIR(14) | A_BOLD);
    
    // Mode options
    for (size_t i = 0; i < modeSelectionOptions.size(); i++) {
        int y = maxY / 3 + i * 2 + 2;
        
        if (static_cast<int>(i) == selectedMenuOption) {
            wattron(mainWindow, A_REVERSE | A_BOLD);
            drawCenteredText(mainWindow, y, modeSelectionOptions[i], true);
            wattroff(mainWindow, A_REVERSE | A_BOLD);
        } else {
            drawCenteredText(mainWindow, y, modeSelectionOptions[i], false);
        }
    }
    
    // Navigation help
    wattron(mainWindow, A_DIM);
    drawCenteredText(mainWindow, maxY - 2, "Use UP/DOWN to navigate, ENTER to select", false);
    wattroff(mainWindow, A_DIM);
    
    wrefresh(mainWindow);
}

void UI::drawPauseMenu() {
    // Draw a box in the center of the screen
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    int boxHeight = 6 + static_cast<int>(pauseMenuOptions.size()) * 2;
    int boxWidth = 30;
    int boxY = (maxY - boxHeight) / 2;
    int boxX = (maxX - boxWidth) / 2;
    
    // Draw the box
    for (int y = boxY; y < boxY + boxHeight; y++) {
        for (int x = boxX; x < boxX + boxWidth; x++) {
            if (y == boxY || y == boxY + boxHeight - 1 || x == boxX || x == boxX + boxWidth - 1) {
                mvwaddch(mainWindow, y, x, ACS_BLOCK);
            } else {
                mvwaddch(mainWindow, y, x, ' ');
            }
        }
    }
    
    // Title
    wattron(mainWindow, A_BOLD);
    mvwprintw(mainWindow, boxY + 2, boxX + (boxWidth - 6) / 2, "PAUSED");
    wattroff(mainWindow, A_BOLD);
    
    // Menu options
    for (size_t i = 0; i < pauseMenuOptions.size(); i++) {
        if (static_cast<int>(i) == selectedMenuOption) {
            wattron(mainWindow, A_REVERSE | A_BOLD);
            mvwprintw(mainWindow, boxY + 4 + static_cast<int>(i) * 2, 
                     boxX + (boxWidth - static_cast<int>(pauseMenuOptions[i].length())) / 2, 
                     "%s", pauseMenuOptions[i].c_str());
            wattroff(mainWindow, A_REVERSE | A_BOLD);
        } else {
            mvwprintw(mainWindow, boxY + 4 + static_cast<int>(i) * 2, 
                     boxX + (boxWidth - static_cast<int>(pauseMenuOptions[i].length())) / 2, 
                     "%s", pauseMenuOptions[i].c_str());
        }
    }
    
    wrefresh(mainWindow);
}

void UI::drawSettings() {
    werase(mainWindow);
    
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    // Title
    wattron(mainWindow, COLOR_PAIR(15) | A_BOLD);
    drawCenteredText(mainWindow, maxY / 4, "SETTINGS", false);
    wattroff(mainWindow, COLOR_PAIR(15) | A_BOLD);
    
    // Settings options
    for (size_t i = 0; i < settingsOptions.size(); i++) {
        int y = maxY / 3 + static_cast<int>(i) * 2 + 2;
        
        if (static_cast<int>(i) == selectedMenuOption) {
            wattron(mainWindow, A_REVERSE | A_BOLD);
            drawCenteredText(mainWindow, y, settingsOptions[i], true);
            wattroff(mainWindow, A_REVERSE | A_BOLD);
        } else {
            drawCenteredText(mainWindow, y, settingsOptions[i], false);
        }
    }
    
    // Navigation help
    wattron(mainWindow, A_DIM);
    drawCenteredText(mainWindow, maxY - 2, "Use UP/DOWN to navigate, ENTER to toggle, ESC to return", false);
    wattroff(mainWindow, A_DIM);
    
    wrefresh(mainWindow);
}

void UI::drawTutorial() {
    werase(mainWindow);
    
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    // Title
    wattron(mainWindow, A_BOLD);
    drawCenteredText(mainWindow, 2, "TUTORIAL", false);
    wattroff(mainWindow, A_BOLD);
    
    // Tutorial content
    std::vector<std::string> tutorial = {
        "Welcome to 2048!",
        "",
        "How to play:",
        "1. Use arrow keys to move all tiles in one direction",
        "2. When two tiles with the same number touch, they merge into one!",
        "3. After each move, a new tile appears (either 2 or 4)",
        "4. The goal is to create a tile with the value 2048",
        "",
        "Tips:",
        "- Keep your highest value tile in a corner",
        "- Try to maintain a clear path to that corner",
        "- Plan ahead and think about what will happen after each move"
    };
    
    for (size_t i = 0; i < tutorial.size(); i++) {
        if (tutorial[i].empty()) {
            continue;
        } else if (i > 0 && tutorial[i-1].empty()) {
            // Section header
            wattron(mainWindow, A_BOLD);
            mvwprintw(mainWindow, 5 + static_cast<int>(i), 5, "%s", tutorial[i].c_str());
            wattroff(mainWindow, A_BOLD);
        } else {
            mvwprintw(mainWindow, 5 + static_cast<int>(i), 5, "%s", tutorial[i].c_str());
        }
    }
    
    // Navigation help
    wattron(mainWindow, A_DIM);
    drawCenteredText(mainWindow, maxY - 2, "Press any key to return to main menu", false);
    wattroff(mainWindow, A_DIM);
    
    wrefresh(mainWindow);
}

void UI::drawExitConfirm() {
    // Draw a box in the center of the screen
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    int boxHeight = 7;
    int boxWidth = 40;
    int boxY = (maxY - boxHeight) / 2;
    int boxX = (maxX - boxWidth) / 2;
    
    // Draw the box
    for (int y = boxY; y < boxY + boxHeight; y++) {
        for (int x = boxX; x < boxX + boxWidth; x++) {
            if (y == boxY || y == boxY + boxHeight - 1 || x == boxX || x == boxX + boxWidth - 1) {
                mvwaddch(mainWindow, y, x, ACS_BLOCK);
            } else {
                mvwaddch(mainWindow, y, x, ' ');
            }
        }
    }
    
    // Question
    wattron(mainWindow, A_BOLD);
    mvwprintw(mainWindow, boxY + 2, boxX + (boxWidth - 29) / 2, "Are you sure you want to quit?");
    wattroff(mainWindow, A_BOLD);
    
    // Options
    std::string yes = "Yes";
    std::string no = "No";
    
    // Highlight selected option
    if (selectedMenuOption == 0) {
        wattron(mainWindow, A_REVERSE | A_BOLD);
        mvwprintw(mainWindow, boxY + 4, boxX + boxWidth / 3 - 2, "%s", yes.c_str());
        wattroff(mainWindow, A_REVERSE | A_BOLD);
        
        mvwprintw(mainWindow, boxY + 4, boxX + 2 * boxWidth / 3 - 2, "%s", no.c_str());
    } else {
        mvwprintw(mainWindow, boxY + 4, boxX + boxWidth / 3 - 2, "%s", yes.c_str());
        
        wattron(mainWindow, A_REVERSE | A_BOLD);
        mvwprintw(mainWindow, boxY + 4, boxX + 2 * boxWidth / 3 - 2, "%s", no.c_str());
        wattroff(mainWindow, A_REVERSE | A_BOLD);
    }
    
    wrefresh(mainWindow);
}

void UI::drawGameOver() {
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    // Display game over overlay
    int boxHeight = 7;
    int boxWidth = 30;
    int boxY = (maxY - boxHeight) / 2;
    int boxX = (maxX - boxWidth) / 2;
    
    // Draw box
    for (int y = boxY; y < boxY + boxHeight; y++) {
        for (int x = boxX; x < boxX + boxWidth; x++) {
            if (y == boxY || y == boxY + boxHeight - 1 || 
                x == boxX || x == boxX + boxWidth - 1) {
                mvwaddch(mainWindow, y, x, ACS_BLOCK);
            } else {
                mvwaddch(mainWindow, y, x, ' ');
            }
        }
    }
    
    // Draw game over message
    wattron(mainWindow, COLOR_PAIR(13) | A_BOLD);
    mvwprintw(mainWindow, boxY + 2, boxX + (boxWidth - 9) / 2, "GAME OVER");
    wattroff(mainWindow, COLOR_PAIR(13) | A_BOLD);
    
    // Draw final score
    wattron(mainWindow, A_BOLD);
    std::string scoreText = "Final Score: " + std::to_string(game.getScore());
    mvwprintw(mainWindow, boxY + 4, boxX + (boxWidth - static_cast<int>(scoreText.length())) / 2, 
             "%s", scoreText.c_str());
    wattroff(mainWindow, A_BOLD);
    
    // Instruction to continue
    wattron(mainWindow, A_DIM);
    mvwprintw(mainWindow, boxY + 6, boxX + 2, "Press any key to continue");
    wattroff(mainWindow, A_DIM);
    
    wrefresh(mainWindow);
}

void UI::drawVictory() {
    int maxY, maxX;
    getmaxyx(mainWindow, maxY, maxX);
    
    // Display victory overlay
    int boxHeight = 9;
    int boxWidth = 40;
    int boxY = (maxY - boxHeight) / 2;
    int boxX = (maxX - boxWidth) / 2;
    
    // Draw box
    for (int y = boxY; y < boxY + boxHeight; y++) {
        for (int x = boxX; x < boxX + boxWidth; x++) {
            if (y == boxY || y == boxY + boxHeight - 1 || 
                x == boxX || x == boxX + boxWidth - 1) {
                mvwaddch(mainWindow, y, x, ACS_BLOCK);
            } else {
                mvwaddch(mainWindow, y, x, ' ');
            }
        }
    }
    
    // Victory message
    wattron(mainWindow, COLOR_PAIR(12) | A_BOLD);
    mvwprintw(mainWindow, boxY + 2, boxX + (boxWidth - 18) / 2, "VICTORY! YOU WIN!");
    wattroff(mainWindow, COLOR_PAIR(12) | A_BOLD);
    
    // Congratulatory message
    wattron(mainWindow, A_NORMAL);
    mvwprintw(mainWindow, boxY + 4, boxX + 2, "Congratulations! You've reached 2048!");
    wattroff(mainWindow, A_NORMAL);
    
    // Draw final score
    wattron(mainWindow, A_BOLD);
    std::string scoreText = "Final Score: " + std::to_string(game.getScore());
    mvwprintw(mainWindow, boxY + 6, boxX + (boxWidth - static_cast<int>(scoreText.length())) / 2, 
             "%s", scoreText.c_str());
    wattroff(mainWindow, A_BOLD);
    
    // Instruction to continue
    wattron(mainWindow, A_DIM);
    mvwprintw(mainWindow, boxY + 8, boxX + 2, "Press any key to continue");
    wattroff(mainWindow, A_DIM);
    
    wrefresh(mainWindow);
}

void UI::drawBoard() {
    werase(gameWindow);
    
    // Draw border
    drawBorderedWindow(gameWindow, "2048");
    
    // Display the board
    const auto& board = game.getBoard();
    for (int i = 0; i < SIZE; ++i) {
        for (int j = 0; j < SIZE; ++j) {
            int value = board[i][j].getValue();
            drawTile(gameWindow, i, j, value);
        }
    }
    
    wrefresh(gameWindow);
}

void UI::drawScore() {
    werase(scoreWindow);
    
    // Draw border
    drawBorderedWindow(scoreWindow, "Score");
    
    // Current score
    wattron(scoreWindow, A_BOLD);
    mvwprintw(scoreWindow, 1, 2, "%d", game.getScore());
    wattroff(scoreWindow, A_BOLD);
    
    // High score
    wattron(scoreWindow, A_NORMAL);
    mvwprintw(scoreWindow, 2, 2, "Best: %d", game.getHighScore());
    wattroff(scoreWindow, A_NORMAL);
    
    wrefresh(scoreWindow);
}

void UI::drawInfo() {
    werase(infoWindow);
    
    // Draw border
    drawBorderedWindow(infoWindow, "Info");
    
    // Add controls info
    wattron(infoWindow, A_BOLD);
    mvwprintw(infoWindow, 1, 2, "Controls:");
    wattroff(infoWindow, A_BOLD);
    mvwprintw(infoWindow, 1, 12, "Arrow keys to move");
    
    mvwprintw(infoWindow, 2, 2, "P: Pause  R: Restart  Q: Quit");
    
    // Game goal
    wattron(infoWindow, A_BOLD);
    mvwprintw(infoWindow, 3, 2, "Goal: Reach the 2048 tile!");
    wattroff(infoWindow, A_BOLD);
    
    wrefresh(infoWindow);
}

// Event handlers for menu input
void UI::handleMainMenuInput() {
    int key = wgetch(mainWindow);
    if (key == ERR) return;
    
    switch (key) {
        case KEY_UP:
            if (selectedMenuOption > 0) {
                selectedMenuOption--;
            }
            break;
            
        case KEY_DOWN:
            if (selectedMenuOption < static_cast<int>(mainMenuOptions.size()) - 1) {
                selectedMenuOption++;
            }
            break;
            
        case '\n': // Enter key
        case ' ':  // Space key
            switch (selectedMenuOption) {
                case 0: // Play Game
                    setState(UIState::GAME_PLAYING);
                    break;
                    
                case 1: // Select Mode
                    setState(UIState::MODE_SELECTION);
                    break;
                    
                case 2: // Settings
                    setState(UIState::SETTINGS);
                    break;
                    
                case 3: // Tutorial
                    setState(UIState::TUTORIAL);
                    break;
                    
                case 4: // Quit
                    setState(UIState::EXIT_CONFIRM);
                    break;
            }
            break;
            
        case 'q':
        case 'Q':
            setState(UIState::EXIT_CONFIRM);
            break;
    }
}

void UI::handleModeSelectionInput() {
    int key = wgetch(mainWindow);
    if (key == ERR) return;
    
    switch (key) {
        case KEY_UP:
            if (selectedMenuOption > 0) {
                selectedMenuOption--;
            }
            break;
            
        case KEY_DOWN:
            if (selectedMenuOption < static_cast<int>(modeSelectionOptions.size()) - 1) {
                selectedMenuOption++;
            }
            break;
            
        case '\n': // Enter key
        case ' ':  // Space key
            if (selectedMenuOption == static_cast<int>(modeSelectionOptions.size()) - 1) {
                // Back option
                setState(UIState::MAIN_MENU);
            } else {
                // Set game mode and start game
                GameModeType mode;
                switch (selectedMenuOption) {
                    case 0: mode = GameModeType::CLASSIC; break;
                    default: mode = GameModeType::CLASSIC; break;
                }
                
                setGameMode(mode);
                setState(UIState::GAME_PLAYING);
            }
            break;
            
        case 27: // ESC key
            setState(UIState::MAIN_MENU);
            break;
    }
}

void UI::handleGameInput() {
    int key = wgetch(mainWindow);
    if (key == ERR) return;
    
    switch (key) {
        case KEY_LEFT:
        case KEY_RIGHT:
        case KEY_UP:
        case KEY_DOWN:
            // Pass to game mode for handling
            if (gameMode) {
                gameMode->handleInput(game, key);
            }
            break;
            
        case 'p':
        case 'P':
            setState(UIState::PAUSE_MENU);
            break;
            
        case 'r':
        case 'R':
            if (gameMode) {
                gameMode->initialize(game);
            }
            break;
            
        case 'q':
        case 'Q':
        case 27: // ESC key
            setState(UIState::PAUSE_MENU);
            break;
    }
}

void UI::handlePauseMenuInput() {
    int key = wgetch(mainWindow);
    if (key == ERR) return;
    
    switch (key) {
        case KEY_UP:
            if (selectedMenuOption > 0) {
                selectedMenuOption--;
            }
            break;
            
        case KEY_DOWN:
            if (selectedMenuOption < static_cast<int>(pauseMenuOptions.size()) - 1) {
                selectedMenuOption++;
            }
            break;
            
        case '\n': // Enter key
        case ' ':  // Space key
            switch (selectedMenuOption) {
                case 0: // Resume
                    setState(UIState::GAME_PLAYING);
                    break;
                    
                case 1: // Restart
                    if (gameMode) {
                        gameMode->initialize(game);
                    }
                    setState(UIState::GAME_PLAYING);
                    break;
                    
                case 2: // Main Menu
                    setState(UIState::MAIN_MENU);
                    break;
                    
                case 3: // Quit
                    setState(UIState::EXIT_CONFIRM);
                    break;
            }
            break;
            
        case 'p':
        case 'P':
        case 27: // ESC key
            // Resume game
            setState(UIState::GAME_PLAYING);
            break;
    }
}

void UI::handleSettingsInput() {
    int key = wgetch(mainWindow);
    if (key == ERR) return;
    
    switch (key) {
        case KEY_UP:
            if (selectedMenuOption > 0) {
                selectedMenuOption--;
            }
            break;
            
        case KEY_DOWN:
            if (selectedMenuOption < static_cast<int>(settingsOptions.size()) - 1) {
                selectedMenuOption++;
            }
            break;
            
        case '\n': // Enter key
        case ' ':  // Space key
            switch (selectedMenuOption) {
                case 0: // Animations toggle
                    toggleAnimations();
                    break;
                    
                case 1: // Theme selection
                    // Cycle through themes
                    setTheme((currentTheme + 1) % static_cast<int>(themes.size()));
                    break;
                    
                case 2: // Back
                    setState(previousState);
                    break;
            }
            break;
            
        case 27: // ESC key
            setState(previousState);
            break;
    }
}

void UI::handleTutorialInput() {
    int key = wgetch(mainWindow);
    if (key == ERR) return;
    
    // Any key returns to main menu
    setState(UIState::MAIN_MENU);
}

void UI::handleExitConfirmInput() {
    int key = wgetch(mainWindow);
    if (key == ERR) return;
    
    switch (key) {
        case KEY_LEFT:
            selectedMenuOption = 0; // Yes
            break;
            
        case KEY_RIGHT:
            selectedMenuOption = 1; // No
            break;
            
        case '\n': // Enter key
        case ' ':  // Space key
            if (selectedMenuOption == 0) {
                // Yes - exit the game
                endwin();
                exit(0);
            } else {
                // No - return to previous state
                setState(previousState);
            }
            break;
            
        case 27: // ESC key
            setState(previousState);
            break;
    }
}

// Game event callbacks
void UI::onTileMerged(int row, int col, int value) {
    // Simplified implementation - no animation
}

void UI::onTileSpawned(int row, int col, int value) {
    // Simplified implementation - no animation
}

void UI::onScoreChanged(int newScore, int prevScore) {
    // Simplified implementation - no animation
}

void UI::onGameOver() {
    setState(UIState::GAME_OVER);
}

void UI::onVictory() {
    setState(UIState::VICTORY);
}

main.cpp

#include "UI.hpp"
#include "Extra.hpp"
#include <iostream>

int main() {
    try {
        // Initialize the game
        initializeGame();
        
        // Create and initialize UI
        UI ui;
        if (!ui.initialize()) {
            cleanupGame();
            std::cerr << "Failed to initialize UI" << std::endl;
            return 1;
        }
        
        // Run the game
        ui.run();
        
        // Clean up
        cleanupGame();
        
        return 0;
    } catch (const std::exception& e) {
        // Clean up ncurses in case of exception
        endwin();
        std::cerr << "Error: " << e.what() << std::endl;
        return 1;
    }
}

Extra.hpp

#ifndef EXTRA_HPP
#define EXTRA_HPP

#include <ncurses.h>

// Initialize the game
void initializeGame();

// Clean up the game
void cleanupGame();

// Initialize colors
void initColors();

// Get color pair for a tile value
int getTileColorPair(int value);

#endif // EXTRA_HPP

Extra.cpp

#include "Extra.hpp"
#include <ncurses.h>

void initializeGame() {
    initscr();
    noecho();
    keypad(stdscr, TRUE);
    curs_set(0);
    
    // Initialize colors
    if(has_colors()) {
        start_color();
        initColors();
    }
}

void initColors() {
    // Initialize color pairs
    init_pair(1, COLOR_BLACK, COLOR_WHITE);      // Default
    init_pair(2, COLOR_BLACK, COLOR_GREEN);      // 2
    init_pair(3, COLOR_BLACK, COLOR_CYAN);       // 4
    init_pair(4, COLOR_BLACK, COLOR_BLUE);       // 8
    init_pair(5, COLOR_BLACK, COLOR_MAGENTA);    // 16
    init_pair(6, COLOR_BLACK, COLOR_RED);        // 32
    init_pair(7, COLOR_WHITE, COLOR_BLUE);       // 64
    init_pair(8, COLOR_WHITE, COLOR_MAGENTA);    // 128
    init_pair(9, COLOR_WHITE, COLOR_RED);        // 256
    init_pair(10, COLOR_WHITE, COLOR_YELLOW);    // 512
    init_pair(11, COLOR_BLACK, COLOR_YELLOW);    // 1024
    init_pair(12, COLOR_WHITE, COLOR_GREEN);     // 2048
    init_pair(13, COLOR_WHITE, COLOR_RED);       // Higher
    init_pair(14, COLOR_YELLOW, COLOR_BLACK);    // Gold text
    init_pair(15, COLOR_CYAN, COLOR_BLACK);      // Cyan text
}

int getTileColorPair(int value) {
    switch(value) {
        case 0: return 1;     // Default
        case 2: return 2;     // 2
        case 4: return 3;     // 4
        case 8: return 4;     // 8
        case 16: return 5;    // 16
        case 32: return 6;    // 32
        case 64: return 7;    // 64
        case 128: return 8;   // 128
        case 256: return 9;   // 256
        case 512: return 10;  // 512
        case 1024: return 11; // 1024
        case 2048: return 12; // 2048
        default: return 13;   // Higher values
    }
}

void cleanupGame() {
    endwin();
}

Makefile

CC = g++
CFLAGS = -std=c++11 -Wall -I include
LDFLAGS = -lncurses

# Directories
SRCDIR = src
INCDIR = include
OBJDIR = obj
BINDIR = bin

# Create directories if they don't exist
$(shell mkdir -p $(OBJDIR) $(BINDIR))

# Source files and object files
SOURCES = $(wildcard $(SRCDIR)/*.cpp)
OBJECTS = $(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES))
EXECUTABLE = $(BINDIR)/2048

# Main target
all: directories $(EXECUTABLE)

# Create necessary directories
directories:
	mkdir -p $(OBJDIR) $(BINDIR)

# Link the executable
$(EXECUTABLE): $(OBJECTS)
	$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
	@echo "Build successful! Run with: $(EXECUTABLE)"

# Compile object files
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
	$(CC) $(CFLAGS) -c $< -o $@

# Clean build files
clean:
	rm -rf $(OBJDIR)/*.o $(EXECUTABLE)

# Run the executable
run: all
	$(EXECUTABLE)

.PHONY: all directories clean run

this version can be run with make run in terminal

Credits

Created by Jamal Enoime