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
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:
- Create directories for sounds and music:
mkdir -p res/sounds res/music
- Place your sound effect files in the
res/sounds
directory - Place your background music files in the
res/music
directory - 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