diff --git a/README.md b/README.md index 1c091a9ae8ad693601365c4b38b010b01ad89aa8..63bdae936eddfb85f868628c2b7ea0cdc72f9db6 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,1453 @@ ### Issues - The gameplay seems to fragment possibly due to a logical error in my code (**will revisit**) +- (REVISIT) to get a fully function version of my game with no bugs replace the following +- 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 +-2048.hpp +// 2048.hpp +#ifndef GAME2048_HPP +#define GAME2048_HPP + +#include <vector> +#include <array> +#include <functional> +#include "Tile.hpp" + +#define SIZE 4 + +// Event types for observers +enum class GameEvent { + TILE_MOVED, + TILE_MERGED, + TILE_SPAWNED, + SCORE_CHANGED, + GAME_WON, + GAME_OVER +}; + +// Game state observer pattern +using GameEventCallback = std::function<void(GameEvent, int, int, int)>; + +class Game2048 { +private: + std::array<std::array<Tile, SIZE>, SIZE> board; + std::array<std::array<Tile, SIZE>, SIZE> previousBoard; + bool moved; + int score; + int previousScore; + int highScore; + std::vector<GameEventCallback> observers; + + void spawnTile(); + void addToScore(int value); + void notifyObservers(GameEvent event, int row = -1, int col = -1, int value = 0); + void notifyObservers(GameEvent event, int fromRow, int fromCol, int toRow, int toCol, int value); + void saveBoardState(); + +public: + Game2048(); + void resetBoard(); + + // Movement methods + void moveLeft(); + void moveRight(); + void moveUp(); + void moveDown(); + + // Game state methods + bool canMove() const; + bool hasWon() const; + int getScore() const; + int getHighScore() const; + void undoMove(); + + // Special actions for power-ups + void clearRow(int row); + void clearColumn(int col); + void doubleScoreTile(int row, int col); + void upgradeTile(int row, int col); + + // Board access + const std::array<std::array<Tile, SIZE>, SIZE>& getBoard() const; + + // Observer pattern methods + void addObserver(GameEventCallback callback); + void removeAllObservers(); +}; + +#endif // GAME2048_HPP + +// Tile.hpp +#ifndef TILE_HPP +#define TILE_HPP + +class Tile { +private: + int value; + bool mergedThisTurn; + bool isNew; + float animationProgress; + +public: + Tile(); + + // Basic operations + void setValue(int val); + int getValue() const; + void setMerged(bool merged); + bool getMerged() const; + void setNew(bool newTile); + bool getNew() const; + + // Animation management + void setAnimationProgress(float progress); + float getAnimationProgress() const; + void resetAnimation(); + + // Operators + bool operator==(const Tile& other) const; + Tile& operator*=(int multiplier); + Tile& operator=(const Tile& other); +}; + +#endif // TILE_HPP + +// GameMode.hpp +#ifndef GAMEMODE_HPP +#define GAMEMODE_HPP + +#include <string> +#include <ncurses.h> // For KEY_ constants +#include "2048.hpp" + +enum class GameModeType { + CLASSIC, + TIMED, + POWERUP, + CHALLENGE, + ZEN, + TUTORIAL +}; + +// Base abstract class for all game modes +class GameMode { +protected: + GameModeType type; + std::string name; + std::string description; + bool gameOver; + bool gameWon; + +public: + GameMode(GameModeType type, const std::string& name, const std::string& description); + virtual ~GameMode() = default; + + // Pure virtual methods that derived classes must implement + virtual void initialize(Game2048& game) = 0; + virtual bool update(Game2048& game, float deltaTime) = 0; + virtual void handleInput(Game2048& game, int key) = 0; + virtual void render() = 0; + + // Common methods for all modes + GameModeType getType() const; + const std::string& getName() const; + const std::string& getDescription() const; + bool isGameOver() const; + bool isGameWon() const; + virtual void setGameOver(bool over); + virtual void setGameWon(bool won); +}; + +// Classic mode - original 2048 gameplay +class ClassicMode : public GameMode { +public: + ClassicMode(); + void initialize(Game2048& game) override; + bool update(Game2048& game, float deltaTime) override; + void handleInput(Game2048& game, int key) override; + void render() override; +}; + +#endif // GAMEMODE_HPP + +// UI.hpp +#ifndef UI_HPP +#define UI_HPP + +#include <ncurses.h> +#include <vector> +#include <string> +#include <memory> +#include "2048.hpp" +#include "GameMode.hpp" + +// UI states +enum class UIState { + MAIN_MENU, + MODE_SELECTION, + GAME_PLAYING, + PAUSE_MENU, + GAME_OVER, + VICTORY, + TUTORIAL, + SETTINGS, + EXIT_CONFIRM +}; + +class UI { +private: + // State management + UIState currentState; + UIState previousState; + int selectedMenuOption; + + // Menu options + std::vector<std::string> mainMenuOptions; + std::vector<std::string> modeSelectionOptions; + std::vector<std::string> pauseMenuOptions; + std::vector<std::string> settingsOptions; + + // Window management + WINDOW* mainWindow; + WINDOW* gameWindow; + WINDOW* scoreWindow; + WINDOW* infoWindow; + + // Game state + Game2048 game; + GameModeType currentGameMode; + + // Mode instances + std::unique_ptr<GameMode> gameMode; + + // Background and color management + float backgroundEffectTimer; + bool animationsEnabled; + + // Theme management + struct Theme { + std::string name; + }; + std::vector<Theme> themes; + int currentTheme; + + // Helper methods + void createWindows(); + void destroyWindows(); + void initializeThemes(); + void applyTheme(int themeIndex); + + // Drawing helpers + void drawCenteredText(WINDOW* win, int y, const std::string& text, bool highlight = false); + void drawBorderedWindow(WINDOW* win, const std::string& title = ""); + void drawTile(WINDOW* win, int row, int col, int value); + + // Event handlers + void handleMainMenuInput(); + void handleModeSelectionInput(); + void handleGameInput(); + void handlePauseMenuInput(); + void handleSettingsInput(); + void handleTutorialInput(); + void handleExitConfirmInput(); + + // Menu drawing + void drawMainMenu(); + void drawModeSelection(); + void drawPauseMenu(); + void drawSettings(); + void drawTutorial(); + void drawExitConfirm(); + + // Game UI + void drawBoard(); + void drawScore(); + void drawInfo(); + void drawGameOver(); + void drawVictory(); + + // Game event callbacks + void onTileMerged(int row, int col, int value); + void onTileSpawned(int row, int col, int value); + void onScoreChanged(int newScore, int prevScore); + void onGameOver(); + void onVictory(); + +public: + UI(); + ~UI(); + + // Initialization + bool initialize(); + + // Main game loop + void run(); + + // State management + void setState(UIState newState); + UIState getState() const; + + // Game mode management + void setGameMode(GameModeType mode); + GameModeType getGameMode() const; + + // Theme management + void setTheme(int themeIndex); + int getThemeCount() const; + std::string getThemeName(int index) const; + + // Settings + void toggleAnimations(); +}; + +#endif // UI_HPP ## How to build and Run ```bash # Build the project +need to install the dependencies for ncurses with this code **sudo apt-get install libncurses5-dev libncursesw5-dev** then in terminal type make # Run the game