diff --git a/Makefile b/Makefile index 8ccb1dea4b9b576b2ad19bcef74b14c310ffd1d0..de256c8bdbdc669e1f4ccd71ff9ee2840240d0ac 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,81 @@ CC = g++ CFLAGS = -std=c++11 -Wall -I include -LDFLAGS = -lncurses +LDFLAGS = -lncurses -lSDL2 -lSDL2_mixer +# Directories SRCDIR = src +INCDIR = include OBJDIR = obj BINDIR = bin +RESDIR = res -# Source files +# Create directories if they don't exist +$(shell mkdir -p $(OBJDIR) $(BINDIR) $(RESDIR)) + +# Source files and object files SOURCES = $(wildcard $(SRCDIR)/*.cpp) OBJECTS = $(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.o,$(SOURCES)) EXECUTABLE = $(BINDIR)/2048 -# Create directories if they don't exist -$(shell mkdir -p $(OBJDIR) $(BINDIR)) +# Resource files +SOUNDDIR = $(RESDIR)/sounds +MUSICDIR = $(RESDIR)/music +IMGDIR = $(RESDIR)/images + +# Main target +all: directories resources $(EXECUTABLE) -all: $(EXECUTABLE) +# Create necessary directories +directories: + mkdir -p $(OBJDIR) $(BINDIR) $(RESDIR) $(SOUNDDIR) $(MUSICDIR) $(IMGDIR) +# Create resource directories and copy resource files +resources: + @echo "Copying resource files..." + @# Create placeholder sound files + @if [ ! -f $(SOUNDDIR)/move.wav ]; then \ + echo "Creating placeholder sound files..."; \ + touch $(SOUNDDIR)/move.wav; \ + touch $(SOUNDDIR)/merge.wav; \ + touch $(SOUNDDIR)/spawn.wav; \ + touch $(SOUNDDIR)/gameover.wav; \ + touch $(SOUNDDIR)/victory.wav; \ + touch $(SOUNDDIR)/select.wav; \ + touch $(SOUNDDIR)/navigate.wav; \ + touch $(SOUNDDIR)/powerup.wav; \ + fi + @# Create placeholder music files + @if [ ! -f $(MUSICDIR)/menu.mp3 ]; then \ + echo "Creating placeholder music files..."; \ + touch $(MUSICDIR)/menu.mp3; \ + touch $(MUSICDIR)/gameplay.mp3; \ + touch $(MUSICDIR)/gameover.mp3; \ + touch $(MUSICDIR)/victory.mp3; \ + fi + +# 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) -.PHONY: all clean \ No newline at end of file +# Clean all generated files including resources +clean-all: clean + rm -rf $(RESDIR)/* + +# Run the executable +run: all + $(EXECUTABLE) + +# Generate documentation using Doxygen +docs: + doxygen Doxyfile + +.PHONY: all directories resources clean clean-all run docs \ No newline at end of file diff --git a/README.md b/README.md index 24661b6fdf5bd877ad027f014b08bcd6f3247e1a..ba0361d9a945e191110e3496391b04074c2c81c0 100644 --- a/README.md +++ b/README.md @@ -1 +1,97 @@ -# Programming in c++ Assignment +# Enhanced 2048 Game by Jamal Enoime + +## 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 +- **Modern UI**: Sleek and responsive user interface with animations and visual effects +- **Multiple Game Modes**: + - **Classic**: Traditional 2048 gameplay + - **Timed**: Race against the clock + - **Power-Up**: Use special abilities to enhance gameplay + - **Challenge**: Complete specific objectives with constraints + - **Zen**: Relaxed mode with no game over +- **Animations and Effects**: + - Smooth tile movements and merges + - 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 + +## Dependencies +- NCurses: For terminal-based UI +- SDL2 and SDL2_mixer: For sound and music + +## Building and Running +```bash +# Build the project +make + +# Run the game +make run + +# Clean build files +make clean + +# Clean all generated files including resources +make clean-all +``` + +## 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 + +## Credits +Created by Jamal Enoime + +Sound effects from freesound.org +Music from bensound.com + +## License +This project is open source and available under the MIT License. \ No newline at end of file diff --git a/bin/2048 b/bin/2048 deleted file mode 100755 index 646381373d323dc17565e3794d0c5796fde902f5..0000000000000000000000000000000000000000 Binary files a/bin/2048 and /dev/null differ diff --git a/bin/2048-game b/bin/2048-game new file mode 100755 index 0000000000000000000000000000000000000000..507e14c6963aaf6291be9e0c8e7266ebca9ec7ef Binary files /dev/null and b/bin/2048-game differ diff --git a/include/2048.hpp b/include/2048.hpp index a3b667aae7bc80eefbef24e52fcd3d12370375a4..c3528833c1e331d68d07a1fbf82fd40cfa12ee9d 100644 --- a/include/2048.hpp +++ b/include/2048.hpp @@ -2,30 +2,76 @@ #define GAME2048_HPP #include <vector> +#include <array> +#include <functional> #include "Tile.hpp" #define SIZE 4 +// Forward declarations +class UI; + +// 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: - Tile board[SIZE][SIZE]; + 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 saveBoardState(); public: Game2048(); - void resetBoard(); // Made public for restart functionality - void drawBoard(); + void resetBoard(); + + // Movement methods void moveLeft(); void moveRight(); void moveUp(); void moveDown(); - bool canMove(); + + // Game state methods + bool canMove() const; + bool hasWon() const; int getScore() const; - const Tile (*getBoard() const)[SIZE]; + 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(); + + // Save/Load game state + bool saveGame(const std::string& filename) const; + bool loadGame(const std::string& filename); }; #endif \ No newline at end of file diff --git a/include/Animations.hpp b/include/Animations.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e711b393ae1d28cf46110de458fbbcc25d1eaace --- /dev/null +++ b/include/Animations.hpp @@ -0,0 +1,167 @@ +#ifndef ANIMATIONS_HPP +#define ANIMATIONS_HPP + +#include <vector> +#include <functional> +#include <string> +#include <ncurses.h> +#include <cmath> + +// Define animation types +enum class AnimationType { + SLIDE, + MERGE, + SPAWN, + PULSE, + FADE_IN, + FADE_OUT, + SHAKE, + FLASH, + PARTICLE +}; + +// Define easing functions +enum class EasingType { + LINEAR, + EASE_IN, + EASE_OUT, + EASE_IN_OUT, + ELASTIC, + BOUNCE +}; + +// Forward declarations +class Game2048; +class Particle; + +// Animation class to handle visual effects +class Animation { +private: + AnimationType type; + int row, col; + int value; + float duration; + float elapsed; + bool completed; + EasingType easing; + + // Callback when animation completes + std::function<void()> onComplete; + + // For slide animation + int sourceRow, sourceCol; + int targetRow, targetCol; + + // For particle animation + std::vector<Particle> particles; + + // Calculate eased progress + float getEasedProgress() const; + +public: + Animation(AnimationType type, int row, int col, int value, + float duration = 0.3f, EasingType easing = EasingType::EASE_OUT); + + bool update(float deltaTime); + void draw(WINDOW* win); + void setCallback(std::function<void()> callback); + + // Animation setup methods + void setupSlide(int fromRow, int fromCol, int toRow, int toCol); + void setupMerge(); + void setupSpawn(); + void setupPulse(); + void setupFade(bool fadeIn); + void setupShake(); + void setupFlash(); + void setupParticles(int count, bool isMerge = false); + + // Getters + AnimationType getType() const { return type; } + int getRow() const { return row; } + int getCol() const { return col; } + int getValue() const { return value; } + float getProgress() const { return elapsed / duration; } + bool isCompleted() const { return completed; } + + // Static helper method to get color for a value + static int getColorForValue(int value); +}; + +// Particle system for effects +class Particle { +private: + float x, y; + float vx, vy; + float ax, ay; + float lifetime; + float maxLifetime; + float size; + int color; + char symbol; + bool fading; + +public: + Particle(float x, float y, float vx, float vy, + float lifetime, int color, char symbol = '*', bool fading = true); + + bool update(float deltaTime); + void draw(WINDOW* win); + + float getX() const { return x; } + float getY() const { return y; } + float getLifetimeRatio() const { return lifetime / maxLifetime; } +}; + +// Animation manager to handle multiple animations +class AnimationManager { +private: + std::vector<Animation> animations; + float backgroundEffectTimer; + bool enableBackgroundEffects; + + // GIF background related + bool hasGifBackground; + std::vector<std::string> gifFrames; + float gifFrameDuration; + float gifFrameTimer; + int currentGifFrame; + + // For temporary effects + struct TransitionEffect { + float duration; + float elapsed; + std::string type; // "fade", "slide", "sparkle" + }; + std::vector<TransitionEffect> transitionEffects; + +public: + AnimationManager(); + + void update(float deltaTime); + void draw(WINDOW* mainWin, WINDOW* gameWin); + + // Animation creation methods + void addSlideAnimation(int fromRow, int fromCol, int toRow, int toCol, int value); + void addMergeAnimation(int row, int col, int value); + void addSpawnAnimation(int row, int col, int value); + void addScoreAnimation(int score, int prevScore); + void addGameOverAnimation(); + void addVictoryAnimation(); + void addTransitionEffect(const std::string& type, float duration = 0.5f); + void addParticleEffect(float x, float y, int color, int count); // Added this declaration + + // Background and effects + void drawBackground(WINDOW* win); + void toggleBackgroundEffects(); + + // GIF background methods + bool loadGifBackground(const std::string& filePath); + void drawGifBackground(WINDOW* win); + + // Clear animations + void clearAnimations(); + bool hasActiveAnimations() const; +}; + +#endif \ No newline at end of file diff --git a/include/GameMode.hpp b/include/GameMode.hpp index a815dde893bad728557d71e6dd424dcc142c1ef5..e93a5c9086d6d871c6667e5a8b5cdc8c2dc36bdd 100644 --- a/include/GameMode.hpp +++ b/include/GameMode.hpp @@ -2,40 +2,55 @@ #define GAMEMODE_HPP #include <string> +#include <vector> +#include <ncurses.h> // Added 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; - virtual void initialize() = 0; - virtual bool update(float deltaTime) = 0; + // 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() override; - bool update(float deltaTime) override; + void initialize(Game2048& game) override; + bool update(Game2048& game, float deltaTime) override; + void handleInput(Game2048& game, int key) override; void render() override; }; @@ -44,31 +59,48 @@ class TimedMode : public GameMode { private: float timeLimit; float timeRemaining; + float bonusTimePerMerge; public: TimedMode(); - void initialize() override; - bool update(float deltaTime) override; + void initialize(Game2048& game) override; + bool update(Game2048& game, float deltaTime) override; + void handleInput(Game2048& game, int key) override; void render() override; float getTimeRemaining() const; + void addTime(float seconds); }; // PowerUp mode - special abilities and boosters class PowerUpMode : public GameMode { private: - int powerUpCount; + int doubleScorePowerups; + int clearRowPowerups; + int undoMovePowerups; + int upgradeTilePowerups; + bool doubleScoreActive; - bool removeRowActive; - bool freezeTimeActive; + int doubleScoreTurnsLeft; public: PowerUpMode(); - void initialize() override; - bool update(float deltaTime) override; + void initialize(Game2048& game) override; + bool update(Game2048& game, float deltaTime) override; + void handleInput(Game2048& game, int key) override; void render() override; - void activatePowerUp(int type); + bool activateDoubleScore(); + bool clearRow(Game2048& game, int row); + bool undoMove(Game2048& game); + bool upgradeTile(Game2048& game, int row, int col); + + int getDoubleScorePowerups() const; + int getClearRowPowerups() const; + int getUndoMovePowerups() const; + int getUpgradeTilePowerups() const; + bool isDoubleScoreActive() const; + int getDoubleScoreTurnsLeft() const; }; // Challenge mode - reach specific goals @@ -78,12 +110,45 @@ private: int targetTile; int movesLimit; int movesUsed; + int currentLevel; public: ChallengeMode(); - void initialize() override; - bool update(float deltaTime) override; + void initialize(Game2048& game) override; + bool update(Game2048& game, float deltaTime) override; + void handleInput(Game2048& game, int key) override; void render() override; + + void incrementMoves(); + bool checkVictory(const Game2048& game) const; + bool hasTargetTile(const Game2048& game) const; + void nextLevel(Game2048& game); + + int getTargetScore() const; + int getTargetTile() const; + int getMovesLimit() const; + int getMovesUsed() const; + int getMovesRemaining() const; + int getCurrentLevel() const; +}; + +// Zen mode - relaxed gameplay with no game over +class ZenMode : public GameMode { +private: + bool colorShiftEnabled; + float colorCycleTime; + float currentColorTime; + +public: + ZenMode(); + void initialize(Game2048& game) override; + bool update(Game2048& game, float deltaTime) override; + void handleInput(Game2048& game, int key) override; + void render() override; + + float getCurrentColorPhase() const; + bool isColorShiftEnabled() const; + void toggleColorShift(); }; // Tutorial mode - learn to play @@ -91,14 +156,19 @@ class TutorialMode : public GameMode { private: int tutorialStep; bool waitingForInput; + std::vector<std::string> tutorialMessages; public: TutorialMode(); - void initialize() override; - bool update(float deltaTime) override; + void initialize(Game2048& game) override; + bool update(Game2048& game, float deltaTime) override; + void handleInput(Game2048& game, int key) override; void render() override; void advanceTutorialStep(); + int getTutorialStep() const; + const std::string& getCurrentMessage() const; + bool isWaitingForInput() const; }; #endif \ No newline at end of file diff --git a/include/SoundSystem.hpp b/include/SoundSystem.hpp new file mode 100644 index 0000000000000000000000000000000000000000..826b3a3d90793640ab03e4a107085b1248f08444 --- /dev/null +++ b/include/SoundSystem.hpp @@ -0,0 +1,93 @@ +#ifndef SOUNDSYSTEM_HPP +#define SOUNDSYSTEM_HPP + +#include <string> +#include <map> +#include <memory> +#include <vector> + +// Forward declaration for SDL dependencies +struct Mix_Chunk; +typedef struct _Mix_Music Mix_Music; + +enum class SoundEffect { + MOVE, + MERGE, + SPAWN, + GAMEOVER, + VICTORY, + MENU_SELECT, + MENU_NAVIGATE, + POWERUP_ACTIVATE +}; + +enum class MusicTrack { + MENU, + GAMEPLAY, + GAMEOVER, + VICTORY +}; + +// Simple sound system using SDL_mixer +class SoundSystem { +private: + bool initialized; + bool soundEnabled; + bool musicEnabled; + float soundVolume; + float musicVolume; + + // Sound effect storage + std::map<SoundEffect, Mix_Chunk*> soundEffects; + + // Music track storage + std::map<MusicTrack, Mix_Music*> musicTracks; + MusicTrack currentMusic; + + // Sound effect queue (for when multiple sound effects happen in the same frame) + struct QueuedSound { + SoundEffect effect; + float delay; + }; + std::vector<QueuedSound> soundQueue; + + // Initialization and cleanup + bool initSDL(); + void closeSDL(); + + // File loading helpers + bool loadSoundEffect(SoundEffect effect, const std::string& filePath); + bool loadMusicTrack(MusicTrack track, const std::string& filePath); + +public: + SoundSystem(); + ~SoundSystem(); + + // Initialization + bool initialize(); + + // Sound control + void playSound(SoundEffect effect); + void queueSound(SoundEffect effect, float delay = 0.0f); + void playMusic(MusicTrack track, bool loop = true); + void stopMusic(); + void pauseMusic(); + void resumeMusic(); + + // Volume control + void setSoundVolume(float volume); // 0.0 to 1.0 + void setMusicVolume(float volume); // 0.0 to 1.0 + float getSoundVolume() const; + float getMusicVolume() const; + + // Enable/disable + void enableSound(bool enabled); + void enableMusic(bool enabled); + bool isSoundEnabled() const; + bool isMusicEnabled() const; + + // Update (process queued sounds) + void update(float deltaTime); +}; + +#endif \ No newline at end of file diff --git a/include/Tile.hpp b/include/Tile.hpp index b2f33e5fd0b408722c597840dd94d39e2cf8d7c0..7d0abe579d5bc033f30bf43c449569502ba0802d 100644 --- a/include/Tile.hpp +++ b/include/Tile.hpp @@ -1,15 +1,37 @@ #ifndef TILE_HPP #define TILE_HPP +#include <string> + class Tile { private: int value; - + bool mergedThisTurn; + bool isNew; + float animationProgress; + public: Tile(); + + // Basic operations void setValue(int val); int getValue() const; - // Add operators to make working with Tiles easier + 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(); + + // Color and style helpers + std::string getColorCode() const; + std::string getBackgroundColor() const; + std::string getForegroundColor() const; + + // Operators bool operator==(const Tile& other) const; Tile& operator*=(int multiplier); Tile& operator=(const Tile& other); diff --git a/include/UI.hpp b/include/UI.hpp index 6f583b0e559456cf98f7c6f4f48648da2531801b..c1d1249e5171a074c0f81e142396ccdd932d06c4 100644 --- a/include/UI.hpp +++ b/include/UI.hpp @@ -4,26 +4,50 @@ #include <ncurses.h> #include <vector> #include <string> +#include <memory> +#include <map> #include "2048.hpp" #include "GameMode.hpp" +#include "Animations.hpp" +#include "SoundSystem.hpp" +// UI states enum class UIState { MAIN_MENU, MODE_SELECTION, GAME_PLAYING, PAUSE_MENU, GAME_OVER, - TUTORIAL + VICTORY, + TUTORIAL, + SETTINGS, + CREDITS, + EXIT_CONFIRM +}; + +// Text effects for UI elements +struct TextEffect { + float duration; + float elapsed; + std::string type; // "fade", "pulse", "rainbow", "typing" + std::string text; + int row, col; + bool centered; + int color; }; 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; - GameModeType currentGameMode; + std::vector<std::string> settingsOptions; // Window management WINDOW* mainWindow; @@ -31,51 +55,156 @@ private: WINDOW* scoreWindow; WINDOW* infoWindow; - // For animations and transitions - float transitionTimer; - bool isTransitioning; - -public: - UI(); - ~UI(); + // Game state + Game2048 game; + GameModeType currentGameMode; + + // Mode instances + std::unique_ptr<GameMode> gameMode; + + // Animation system + AnimationManager animations; + + // Sound system + SoundSystem sounds; + + // Text effects + std::vector<TextEffect> textEffects; + + // Background and color management + float backgroundEffectTimer; + std::string backgroundImage; + bool useCustomColors; + + // Settings + bool soundEnabled; + bool musicEnabled; + bool animationsEnabled; + bool highContrastMode; + + // Theme management + struct Theme { + std::string name; + std::map<int, int> tileForegroundColors; + std::map<int, int> tileBackgroundColors; + int menuColor; + int backgroundColor; + int textColor; + }; + std::vector<Theme> themes; + int currentTheme; + + // Confetti for victory + struct Confetti { + float x, y; + float vx, vy; + float angle; + float rotation; + float size; + int color; + char symbol; + float lifetime; + }; + std::vector<Confetti> confetti; + + // Credits + std::vector<std::string> credits; + float creditsScrollY; + + // 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 drawProgressBar(WINDOW* win, int y, int x, int width, float progress, int colorPair); + void drawTile(WINDOW* win, int row, int col, int value); - void initialize(); - void drawBoard(const Game2048& game); - void handleInput(Game2048& game); - void showGameOver(Game2048& game); - void displayScore(const Game2048& game); - void displayInstructions(); + // Event handlers + void handleMainMenuInput(); + void handleModeSelectionInput(); + void handleGameInput(); + void handlePauseMenuInput(); + void handleSettingsInput(); + void handleTutorialInput(); + void handleCreditsInput(); + void handleExitConfirmInput(); - // New menu-related methods + // Menu drawing void drawMainMenu(); void drawModeSelection(); void drawPauseMenu(); - void handleMenuInput(); + void drawSettings(); + void drawCredits(); + void drawExitConfirm(); - // Game mode management - void setGameMode(GameModeType mode); - GameModeType getGameMode() const; + // Game UI + void drawBoard(); + void drawScore(); + void drawInfo(); + void drawTutorial(); + void drawGameOver(); + void drawVictory(); - // Tutorial-specific rendering - void drawTutorial(int step); + // Effects + void updateTextEffects(float deltaTime); + void drawTextEffects(); + void addTextEffect(const std::string& text, int row, int col, const std::string& type, float duration, int color, bool centered = true); + void updateConfetti(float deltaTime); + void drawConfetti(); + void addConfetti(int amount); - // PowerUp mode-specific UI - void drawPowerUpInterface(const PowerUpMode& powerUpMode); + // Mode-specific UI + void drawTimedModeUI(); + void drawPowerUpModeUI(); + void drawChallengeModeUI(); + void drawZenModeUI(); - // Timed mode-specific UI - void drawTimerInterface(float timeRemaining); + // Input helpers + void processGameModeInput(int key); + + // Game event callbacks + void onTileMoved(int fromRow, int fromCol, int toRow, int toCol, int value); + 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(); - // Challenge mode-specific UI - void drawChallengeInterface(int targetScore, int targetTile, int movesRemaining); + // Initialization + bool initialize(); + + // Main game loop + void run(); // State management void setState(UIState newState); UIState getState() const; - // Utility methods - void drawCenteredText(WINDOW* win, int y, const std::string& text, bool highlight = false); - void createWindows(); - void destroyWindows(); + // 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 toggleSound(); + void toggleMusic(); + void toggleAnimations(); + void toggleHighContrast(); + + // Background + bool setBackgroundImage(const std::string& filePath); }; #endif \ No newline at end of file diff --git a/obj/2048.o b/obj/2048.o index 4cf51d0ff32df9b8ccd4253c4c7450419797ce66..6c22e616c8884232d7466bc891bc63e7d3d2892e 100644 Binary files a/obj/2048.o and b/obj/2048.o differ diff --git a/obj/Animations.o b/obj/Animations.o new file mode 100644 index 0000000000000000000000000000000000000000..b0e1e918b8db991f1c69d5db767aec32c16389e8 Binary files /dev/null and b/obj/Animations.o differ diff --git a/obj/GameMode.o b/obj/GameMode.o index 89923a74ad3a2e02561691fad69613a1ba3df67b..6605cf5a5d253c6fce23ed0434ec7d035821eec1 100644 Binary files a/obj/GameMode.o and b/obj/GameMode.o differ diff --git a/obj/SoundSystem.o b/obj/SoundSystem.o new file mode 100644 index 0000000000000000000000000000000000000000..d54b33a5f271d383130c03535826e0d07b999bf9 Binary files /dev/null and b/obj/SoundSystem.o differ diff --git a/obj/Tile.o b/obj/Tile.o index 1b59067a351aef7c08d4b68177f2cdf568c89426..9b41b4a8c2c921a9d71fcaff60f58f74b37e38ac 100644 Binary files a/obj/Tile.o and b/obj/Tile.o differ diff --git a/obj/UI.o b/obj/UI.o index 9d33e9f70128cb9f24de4a552ec79a45c72dd76e..ccf56b3df959a61ca9184f6058ecfdcd1a3266a1 100644 Binary files a/obj/UI.o and b/obj/UI.o differ diff --git a/obj/main.o b/obj/main.o index cbacafd64565d02669a72545504e1172c2d17029..620b36490c57c5bb90bc0039b68cb178b278326f 100644 Binary files a/obj/main.o and b/obj/main.o differ diff --git a/res/music/gameover.mp3 b/res/music/gameover.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/music/gameplay.mp3 b/res/music/gameplay.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/music/menu.mp3 b/res/music/menu.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/music/victory.mp3 b/res/music/victory.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/gameover.wav b/res/sounds/gameover.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/merge.wav b/res/sounds/merge.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/move.wav b/res/sounds/move.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/navigate.wav b/res/sounds/navigate.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/powerup.wav b/res/sounds/powerup.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/select.wav b/res/sounds/select.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/spawn.wav b/res/sounds/spawn.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/res/sounds/victory.wav b/res/sounds/victory.wav new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/2048.cpp b/src/2048.cpp index c775f6dd3c52f0391f4c4f9e89e94b66fb38ba70..fd81d5e2af97c9aa43dff446d7396661feea8f2e 100644 --- a/src/2048.cpp +++ b/src/2048.cpp @@ -1,147 +1,348 @@ #include "2048.hpp" -#include <ncurses.h> #include <cstdlib> #include <ctime> -#include <vector> +#include <fstream> +#include <algorithm> +#include <iostream> -Game2048::Game2048() : score(0) { - std::srand(std::time(0)); +Game2048::Game2048() : score(0), previousScore(0), highScore(0) { + std::srand(std::time(nullptr)); resetBoard(); } void Game2048::resetBoard() { + // Save high score before reset + highScore = std::max(highScore, score); + + // Reset score and board score = 0; - for (int i = 0; i < SIZE; ++i) - for (int j = 0; j < SIZE; ++j) + previousScore = 0; + + // Clear the board + for (int i = 0; i < SIZE; ++i) { + for (int j = 0; j < SIZE; ++j) { board[i][j].setValue(0); + } + } + + // Spawn initial tiles spawnTile(); spawnTile(); + + // Notify observers about reset + notifyObservers(GameEvent::SCORE_CHANGED, -1, -1, score); } void Game2048::spawnTile() { std::vector<std::pair<int, int>> emptyTiles; - for (int i = 0; i < SIZE; ++i) - for (int j = 0; j < SIZE; ++j) - if (board[i][j].getValue() == 0) + + // Find all empty tiles + for (int i = 0; i < SIZE; ++i) { + for (int j = 0; j < SIZE; ++j) { + if (board[i][j].getValue() == 0) { emptyTiles.push_back({i, j}); + } + } + } + // If there are empty tiles, spawn a new one if (!emptyTiles.empty()) { int idx = std::rand() % emptyTiles.size(); + int row = emptyTiles[idx].first; + int col = emptyTiles[idx].second; + + // 90% chance of spawning a 2, 10% chance of spawning a 4 int newValue = (std::rand() % 10 == 0) ? 4 : 2; - board[emptyTiles[idx].first][emptyTiles[idx].second].setValue(newValue); + board[row][col].setValue(newValue); + + // Notify about the new tile + notifyObservers(GameEvent::TILE_SPAWNED, row, col, newValue); } } void Game2048::addToScore(int value) { score += value; + notifyObservers(GameEvent::SCORE_CHANGED, -1, -1, score); + + // Check for win condition (2048 tile) + if (value >= 2048) { + notifyObservers(GameEvent::GAME_WON, -1, -1, value); + } +} + +void Game2048::saveBoardState() { + // Save current board state for undo functionality + previousBoard = board; + previousScore = score; +} + +void Game2048::undoMove() { + // Restore the previous board state + board = previousBoard; + score = previousScore; + + // Notify about score change + notifyObservers(GameEvent::SCORE_CHANGED, -1, -1, score); } void Game2048::moveLeft() { + saveBoardState(); moved = false; + for (int i = 0; i < SIZE; ++i) { - Tile temp[SIZE]; + std::array<Tile, SIZE> temp{}; int index = 0; + for (int j = 0; j < SIZE; ++j) { if (board[i][j].getValue() != 0) { if (index > 0 && temp[index - 1] == board[i][j]) { + // Merge tiles int mergedValue = board[i][j].getValue() * 2; temp[index - 1].setValue(mergedValue); - addToScore(mergedValue); // Add to score when tiles merge + addToScore(mergedValue); + + // Notify about the merge + notifyObservers(GameEvent::TILE_MERGED, i, index - 1, mergedValue); moved = true; } else { + // Move tile + if (j != index) { + notifyObservers(GameEvent::TILE_MOVED, i, j, board[i][j].getValue()); + notifyObservers(GameEvent::TILE_MOVED, i, index, board[i][j].getValue()); + } temp[index++] = board[i][j]; } } } + + // Check if the board changed for (int j = 0; j < SIZE; ++j) { - if (board[i][j].getValue() != temp[j].getValue()) moved = true; + if (board[i][j].getValue() != temp[j].getValue()) { + moved = true; + } board[i][j] = temp[j]; } } - if (moved) spawnTile(); + + // Spawn a new tile if the board changed + if (moved) { + spawnTile(); + + // Check for game over + if (!canMove()) { + notifyObservers(GameEvent::GAME_OVER, -1, -1, score); + + // Update high score + if (score > highScore) { + highScore = score; + } + } + } } void Game2048::moveRight() { + saveBoardState(); moved = false; + for (int i = 0; i < SIZE; ++i) { - Tile temp[SIZE]; + std::array<Tile, SIZE> temp{}; int index = SIZE - 1; + for (int j = SIZE - 1; j >= 0; --j) { if (board[i][j].getValue() != 0) { if (index < SIZE - 1 && temp[index + 1] == board[i][j]) { + // Merge tiles int mergedValue = board[i][j].getValue() * 2; temp[index + 1].setValue(mergedValue); - addToScore(mergedValue); // Add to score when tiles merge + addToScore(mergedValue); + + // Notify about the merge + notifyObservers(GameEvent::TILE_MERGED, i, index + 1, mergedValue); moved = true; } else { + // Move tile + if (j != index) { + notifyObservers(GameEvent::TILE_MOVED, i, j, board[i][j].getValue()); + notifyObservers(GameEvent::TILE_MOVED, i, index, board[i][j].getValue()); + } temp[index--] = board[i][j]; } } } + + // Check if the board changed for (int j = 0; j < SIZE; ++j) { - if (board[i][j].getValue() != temp[j].getValue()) moved = true; + if (board[i][j].getValue() != temp[j].getValue()) { + moved = true; + } board[i][j] = temp[j]; } } - if (moved) spawnTile(); + + // Spawn a new tile if the board changed + if (moved) { + spawnTile(); + + // Check for game over + if (!canMove()) { + notifyObservers(GameEvent::GAME_OVER, -1, -1, score); + + // Update high score + if (score > highScore) { + highScore = score; + } + } + } } void Game2048::moveUp() { + saveBoardState(); moved = false; + for (int j = 0; j < SIZE; ++j) { - Tile temp[SIZE]; + std::array<Tile, SIZE> temp{}; int index = 0; + for (int i = 0; i < SIZE; ++i) { if (board[i][j].getValue() != 0) { if (index > 0 && temp[index - 1] == board[i][j]) { + // Merge tiles int mergedValue = board[i][j].getValue() * 2; temp[index - 1].setValue(mergedValue); - addToScore(mergedValue); // Add to score when tiles merge + addToScore(mergedValue); + + // Notify about the merge + notifyObservers(GameEvent::TILE_MERGED, index - 1, j, mergedValue); moved = true; } else { + // Move tile + if (i != index) { + notifyObservers(GameEvent::TILE_MOVED, i, j, board[i][j].getValue()); + notifyObservers(GameEvent::TILE_MOVED, index, j, board[i][j].getValue()); + } temp[index++] = board[i][j]; } } } + + // Check if the board changed for (int i = 0; i < SIZE; ++i) { - if (board[i][j].getValue() != temp[i].getValue()) moved = true; + if (board[i][j].getValue() != temp[i].getValue()) { + moved = true; + } board[i][j] = temp[i]; } } - if (moved) spawnTile(); + + // Spawn a new tile if the board changed + if (moved) { + spawnTile(); + + // Check for game over + if (!canMove()) { + notifyObservers(GameEvent::GAME_OVER, -1, -1, score); + + // Update high score + if (score > highScore) { + highScore = score; + } + } + } } void Game2048::moveDown() { + saveBoardState(); moved = false; + for (int j = 0; j < SIZE; ++j) { - Tile temp[SIZE]; + std::array<Tile, SIZE> temp{}; int index = SIZE - 1; + for (int i = SIZE - 1; i >= 0; --i) { if (board[i][j].getValue() != 0) { if (index < SIZE - 1 && temp[index + 1] == board[i][j]) { + // Merge tiles int mergedValue = board[i][j].getValue() * 2; temp[index + 1].setValue(mergedValue); - addToScore(mergedValue); // Add to score when tiles merge + addToScore(mergedValue); + + // Notify about the merge + notifyObservers(GameEvent::TILE_MERGED, index + 1, j, mergedValue); moved = true; } else { + // Move tile + if (i != index) { + notifyObservers(GameEvent::TILE_MOVED, i, j, board[i][j].getValue()); + notifyObservers(GameEvent::TILE_MOVED, index, j, board[i][j].getValue()); + } temp[index--] = board[i][j]; } } } + + // Check if the board changed for (int i = 0; i < SIZE; ++i) { - if (board[i][j].getValue() != temp[i].getValue()) moved = true; + if (board[i][j].getValue() != temp[i].getValue()) { + moved = true; + } board[i][j] = temp[i]; } } - if (moved) spawnTile(); + + // Spawn a new tile if the board changed + if (moved) { + spawnTile(); + + // Check for game over + if (!canMove()) { + notifyObservers(GameEvent::GAME_OVER, -1, -1, score); + + // Update high score + if (score > highScore) { + highScore = score; + } + } + } } -bool Game2048::canMove() { +bool Game2048::canMove() const { + // Check for empty cells for (int i = 0; i < SIZE; ++i) { for (int j = 0; j < SIZE; ++j) { - if (board[i][j].getValue() == 0) return true; - if (j < SIZE - 1 && board[i][j] == board[i][j + 1]) return true; - if (i < SIZE - 1 && board[i][j] == board[i + 1][j]) return true; + if (board[i][j].getValue() == 0) { + return true; + } + } + } + + // Check for possible merges horizontally + for (int i = 0; i < SIZE; ++i) { + for (int j = 0; j < SIZE - 1; ++j) { + if (board[i][j] == board[i][j + 1]) { + return true; + } + } + } + + // Check for possible merges vertically + for (int i = 0; i < SIZE - 1; ++i) { + for (int j = 0; j < SIZE; ++j) { + if (board[i][j] == board[i + 1][j]) { + return true; + } + } + } + + // No moves possible + return false; +} + +bool Game2048::hasWon() const { + for (int i = 0; i < SIZE; ++i) { + for (int j = 0; j < SIZE; ++j) { + if (board[i][j].getValue() >= 2048) { + return true; + } } } return false; @@ -151,6 +352,113 @@ int Game2048::getScore() const { return score; } -const Tile (*Game2048::getBoard() const)[SIZE] { +int Game2048::getHighScore() const { + return highScore; +} + +// Special actions for power-ups +void Game2048::clearRow(int row) { + if (row < 0 || row >= SIZE) return; + + for (int j = 0; j < SIZE; ++j) { + board[row][j].setValue(0); + } + + // Spawn a new tile after clearing + spawnTile(); +} + +void Game2048::clearColumn(int col) { + if (col < 0 || col >= SIZE) return; + + for (int i = 0; i < SIZE; ++i) { + board[i][col].setValue(0); + } + + // Spawn a new tile after clearing + spawnTile(); +} + +void Game2048::doubleScoreTile(int row, int col) { + if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) return; + + if (board[row][col].getValue() > 0) { + addToScore(board[row][col].getValue()); + } +} + +void Game2048::upgradeTile(int row, int col) { + if (row < 0 || row >= SIZE || col < 0 || col >= SIZE) return; + + if (board[row][col].getValue() > 0) { + int newValue = board[row][col].getValue() * 2; + board[row][col].setValue(newValue); + notifyObservers(GameEvent::TILE_MERGED, row, col, newValue); + } +} + +const std::array<std::array<Tile, SIZE>, SIZE>& Game2048::getBoard() const { return board; } + +// Observer pattern methods +void Game2048::addObserver(GameEventCallback callback) { + observers.push_back(callback); +} + +void Game2048::removeAllObservers() { + observers.clear(); +} + +void Game2048::notifyObservers(GameEvent event, int row, int col, int value) { + for (const auto& observer : observers) { + observer(event, row, col, value); + } +} + +// Save/Load game state +bool Game2048::saveGame(const std::string& filename) const { + std::ofstream file(filename, std::ios::binary); + if (!file.is_open()) { + return false; + } + + // Save score and high score + file.write(reinterpret_cast<const char*>(&score), sizeof(score)); + file.write(reinterpret_cast<const char*>(&highScore), sizeof(highScore)); + + // Save board state + for (int i = 0; i < SIZE; ++i) { + for (int j = 0; j < SIZE; ++j) { + int value = board[i][j].getValue(); + file.write(reinterpret_cast<const char*>(&value), sizeof(value)); + } + } + + return true; +} + +bool Game2048::loadGame(const std::string& filename) { + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + return false; + } + + // Load score and high score + file.read(reinterpret_cast<char*>(&score), sizeof(score)); + file.read(reinterpret_cast<char*>(&highScore), sizeof(highScore)); + + // Load board state + for (int i = 0; i < SIZE; ++i) { + for (int j = 0; j < SIZE; ++j) { + int value; + file.read(reinterpret_cast<char*>(&value), sizeof(value)); + board[i][j].setValue(value); + } + } + + // Notify about score change + notifyObservers(GameEvent::SCORE_CHANGED, -1, -1, score); + + return true; +} \ No newline at end of file diff --git a/src/Animations.cpp b/src/Animations.cpp new file mode 100644 index 0000000000000000000000000000000000000000..780b387065d0975dfe1377e0ec91c6733270bf77 --- /dev/null +++ b/src/Animations.cpp @@ -0,0 +1,625 @@ +#include "Animations.hpp" +#include <random> +#include <algorithm> +#include <fstream> +#include <sstream> + +//============================================================================== +// Animation Implementation +//============================================================================== +Animation::Animation(AnimationType type, int row, int col, int value, float duration, EasingType easing) + : type(type), row(row), col(col), value(value), + duration(duration), elapsed(0.0f), completed(false), easing(easing), + sourceRow(-1), sourceCol(-1), targetRow(row), targetCol(col) { +} + +bool Animation::update(float deltaTime) { + elapsed += deltaTime; + + if (elapsed >= duration) { + elapsed = duration; + if (!completed) { + completed = true; + if (onComplete) { + onComplete(); + } + } + } + + // Update particles if this is a particle animation + if (type == AnimationType::PARTICLE) { + for (auto it = particles.begin(); it != particles.end();) { + if (!it->update(deltaTime)) { + it = particles.erase(it); + } else { + ++it; + } + } + + // If all particles are gone, mark as completed + if (particles.empty() && !completed) { + completed = true; + if (onComplete) { + onComplete(); + } + } + } + + return !completed; +} + +void Animation::draw(WINDOW* win) { + float progress = getEasedProgress(); + + switch (type) { + case AnimationType::SLIDE: { + // Calculate interpolated position + float currentRow = sourceRow + (targetRow - sourceRow) * progress; + float currentCol = sourceCol + (targetCol - sourceCol) * progress; + + // Draw the tile at the interpolated position + int colorPair = getColorForValue(value); + wattron(win, COLOR_PAIR(colorPair) | A_BOLD); + mvwprintw(win, (int)currentRow, (int)currentCol * 6 + 1, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | A_BOLD); + break; + } + + case AnimationType::MERGE: { + // For ncurses, we don't actually use scaling but we simulate the effect + int colorPair = getColorForValue(value); + + // Apply effect to the tile (for ncurses, just use bold and blink) + int attrs = A_BOLD; + if (progress > 0.4f && progress < 0.6f) { + attrs |= A_BLINK; // Blink at peak of animation + } + + wattron(win, COLOR_PAIR(colorPair) | attrs); + mvwprintw(win, row, col * 6 + 1, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | attrs); + break; + } + + case AnimationType::SPAWN: { + // Fade in for spawn animation + int colorPair = getColorForValue(value); + + if (progress < 0.3f) { + wattron(win, COLOR_PAIR(1) | A_DIM); // Background color + } else if (progress < 0.7f) { + wattron(win, COLOR_PAIR(colorPair) | A_DIM); // Dim version + } else { + wattron(win, COLOR_PAIR(colorPair) | A_BOLD); // Full version + } + + mvwprintw(win, row, col * 6 + 1, "%5d", value); + + if (progress < 0.3f) { + wattroff(win, COLOR_PAIR(1) | A_DIM); + } else if (progress < 0.7f) { + wattroff(win, COLOR_PAIR(colorPair) | A_DIM); + } else { + wattroff(win, COLOR_PAIR(colorPair) | A_BOLD); + } + break; + } + + case AnimationType::PULSE: { + // Pulsing animation + float pulse = sin(progress * M_PI * 2) * 0.5f + 0.5f; + int colorPair = getColorForValue(value); + + int attrs = pulse > 0.7f ? A_BOLD : A_NORMAL; + wattron(win, COLOR_PAIR(colorPair) | attrs); + mvwprintw(win, row, col * 6 + 1, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | attrs); + break; + } + + case AnimationType::FADE_IN: { + // Fade in animation + int colorPair = getColorForValue(value); + + int attrs = progress > 0.7f ? A_BOLD : (progress > 0.3f ? A_NORMAL : A_DIM); + wattron(win, COLOR_PAIR(colorPair) | attrs); + mvwprintw(win, row, col * 6 + 1, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | attrs); + break; + } + + case AnimationType::FADE_OUT: { + // Fade out animation + int colorPair = getColorForValue(value); + + int attrs = progress < 0.3f ? A_BOLD : (progress < 0.7f ? A_NORMAL : A_DIM); + wattron(win, COLOR_PAIR(colorPair) | attrs); + mvwprintw(win, row, col * 6 + 1, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | attrs); + break; + } + + case AnimationType::SHAKE: { + // Shake animation + float offsetX = sin(progress * M_PI * 6) * (1.0f - progress) * 2.0f; + int colorPair = getColorForValue(value); + + wattron(win, COLOR_PAIR(colorPair) | A_BOLD); + mvwprintw(win, row, col * 6 + 1 + (int)offsetX, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | A_BOLD); + break; + } + + case AnimationType::FLASH: { + // Flash animation + bool visible = ((int)(progress * 10)) % 2 == 0; + if (visible) { + int colorPair = getColorForValue(value); + wattron(win, COLOR_PAIR(colorPair) | A_BOLD | A_BLINK); + mvwprintw(win, row, col * 6 + 1, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | A_BOLD | A_BLINK); + } + break; + } + + case AnimationType::PARTICLE: { + // Draw all particles + for (auto& particle : particles) { + particle.draw(win); + } + break; + } + } +} + +void Animation::setCallback(std::function<void()> callback) { + onComplete = callback; +} + +void Animation::setupSlide(int fromRow, int fromCol, int toRow, int toCol) { + sourceRow = fromRow; + sourceCol = fromCol; + targetRow = toRow; + targetCol = toCol; +} + +void Animation::setupMerge() { + // Setup is handled in the constructor +} + +void Animation::setupSpawn() { + // Setup is handled in the constructor +} + +void Animation::setupPulse() { + // Setup is handled in the constructor +} + +void Animation::setupFade(bool fadeIn) { + type = fadeIn ? AnimationType::FADE_IN : AnimationType::FADE_OUT; +} + +void Animation::setupShake() { + // Setup is handled in the constructor +} + +void Animation::setupFlash() { + // Setup is handled in the constructor +} + +void Animation::setupParticles(int count, bool isMerge) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * M_PI); + std::uniform_real_distribution<float> speedDist(0.5f, 2.5f); + std::uniform_real_distribution<float> lifetimeDist(0.3f, 1.3f); + + particles.clear(); + + // Convert board coordinates to screen coordinates + float centerX = col * 6 + 3; + float centerY = row; + + // Generate particles + for (int i = 0; i < count; ++i) { + float angle = angleDist(gen); + float speed = speedDist(gen); + float vx = cos(angle) * speed; + float vy = sin(angle) * speed; + + float lifetime = lifetimeDist(gen); + int color = getColorForValue(value); + + // Different symbols for different effects - ASCII only + char symbols[] = {'*', '+', '.', 'o', ':', '#', '@', '%'}; + char symbol = symbols[i % 8]; + + particles.emplace_back(centerX, centerY, vx, vy, lifetime, color, symbol); + } +} + +float Animation::getEasedProgress() const { + float t = elapsed / duration; + + switch (easing) { + case EasingType::LINEAR: + return t; + + case EasingType::EASE_IN: + return t * t; + + case EasingType::EASE_OUT: + return t * (2 - t); + + case EasingType::EASE_IN_OUT: + return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t; + + case EasingType::ELASTIC: { + // Approximation of elastic easing + float p = 0.3f; + return pow(2, -10 * t) * sin((t - p / 4) * (2 * M_PI) / p) + 1; + } + + case EasingType::BOUNCE: { + // Approximation of bounce easing + if (t < 4/11.0f) { + return (121 * t * t)/16.0f; + } else if (t < 8/11.0f) { + return (363/40.0f * t * t) - (99/10.0f * t) + 17/5.0f; + } else if (t < 9/10.0f) { + return (4356/361.0f * t * t) - (35442/1805.0f * t) + 16061/1805.0f; + } else { + return (54/5.0f * t * t) - (513/25.0f * t) + 268/25.0f; + } + } + } + + return t; // Default to linear +} + +int Animation::getColorForValue(int value) { + switch (value) { + case 0: return 1; // Default color + case 2: return 2; + case 4: return 3; + case 8: return 4; + case 16: return 5; + case 32: return 6; + case 64: return 7; + case 128: return 8; + case 256: return 9; + case 512: return 10; + case 1024: return 11; + case 2048: return 12; + default: return 13; // Higher values + } +} + +//============================================================================== +// Particle Implementation +//============================================================================== +Particle::Particle(float x, float y, float vx, float vy, float lifetime, int color, char symbol, bool fading) + : x(x), y(y), vx(vx), vy(vy), ax(0), ay(0.1f), // Slight gravity + lifetime(lifetime), maxLifetime(lifetime), size(1.0f), + color(color), symbol(symbol), fading(fading) { +} + +bool Particle::update(float deltaTime) { + // Update velocity + vx += ax * deltaTime; + vy += ay * deltaTime; + + // Update position + x += vx * deltaTime * 10; + y += vy * deltaTime * 5; + + // Update lifetime + lifetime -= deltaTime; + + // Return true if particle is still alive + return lifetime > 0; +} + +void Particle::draw(WINDOW* win) { + // Skip drawing if outside window + int maxY, maxX; + getmaxyx(win, maxY, maxX); + + if (x < 0 || x >= maxX || y < 0 || y >= maxY) { + return; + } + + // Apply fade effect based on lifetime + int attrs; + if (fading) { + float alpha = lifetime / maxLifetime; // 0.0 to 1.0 + attrs = alpha > 0.7f ? A_BOLD : (alpha > 0.3f ? A_NORMAL : A_DIM); + } else { + attrs = A_BOLD; + } + + wattron(win, COLOR_PAIR(color) | attrs); + mvwaddch(win, (int)y, (int)x, symbol); + wattroff(win, COLOR_PAIR(color) | attrs); +} + +//============================================================================== +// AnimationManager Implementation +//============================================================================== +AnimationManager::AnimationManager() + : backgroundEffectTimer(0.0f), enableBackgroundEffects(true), + hasGifBackground(false), gifFrameDuration(0.1f), gifFrameTimer(0.0f), currentGifFrame(0) { +} + +void AnimationManager::update(float deltaTime) { + // Update background effect timer + backgroundEffectTimer += deltaTime; + + // Update GIF animation + if (hasGifBackground) { + gifFrameTimer += deltaTime; + if (gifFrameTimer >= gifFrameDuration) { + gifFrameTimer = 0.0f; + currentGifFrame = (currentGifFrame + 1) % gifFrames.size(); + } + } + + // Update all animations + for (auto it = animations.begin(); it != animations.end();) { + if (!it->update(deltaTime)) { + it = animations.erase(it); + } else { + ++it; + } + } + + // Update transition effects + for (auto it = transitionEffects.begin(); it != transitionEffects.end();) { + it->elapsed += deltaTime; + if (it->elapsed >= it->duration) { + it = transitionEffects.erase(it); + } else { + ++it; + } + } +} + +void AnimationManager::draw(WINDOW* mainWin, WINDOW* gameWin) { + // Draw background effects + if (enableBackgroundEffects) { + drawBackground(mainWin); + } + + // Draw GIF background if available + if (hasGifBackground) { + drawGifBackground(mainWin); + } + + // Draw all animations + for (auto& anim : animations) { + anim.draw(gameWin); + } + + // Draw transition effects + for (const auto& effect : transitionEffects) { + float progress = effect.elapsed / effect.duration; + + if (effect.type == "fade") { + // Fade transition + int maxY, maxX; + getmaxyx(mainWin, maxY, maxX); + + wattron(mainWin, A_DIM); + for (int y = 0; y < maxY; y++) { + for (int x = 0; x < maxX; x++) { + if ((x + y) % (int)(10 - progress * 9) == 0) { + mvwaddch(mainWin, y, x, ' ' | A_REVERSE); + } + } + } + wattroff(mainWin, A_DIM); + } else if (effect.type == "slide") { + // Slide transition + int maxY, maxX; + getmaxyx(mainWin, maxY, maxX); + + int limit = (int)(progress * maxY); + for (int y = 0; y < limit; y++) { + for (int x = 0; x < maxX; x++) { + if ((x + y) % 2 == 0) { + mvwaddch(mainWin, y, x, ' ' | A_REVERSE); + } + } + } + } else if (effect.type == "sparkle") { + // Sparkle transition + int maxY, maxX; + getmaxyx(mainWin, maxY, maxX); + + int iterations = (int)(progress * 10); + for (int i = 0; i < iterations; i++) { + int radius = maxX / 2 - (i * maxX / 20); + if (radius > 0) { + for (int degree = 0; degree < 360; degree += 5) { + float radian = degree * M_PI / 180.0f; + int x = maxX / 2 + (int)(cos(radian) * radius); + int y = maxY / 2 + (int)(sin(radian) * radius / 2); + if (x >= 0 && x < maxX && y >= 0 && y < maxY) { + mvwaddch(mainWin, y, x, ' ' | A_REVERSE); + } + } + } + } + } + } +} + +void AnimationManager::addSlideAnimation(int fromRow, int fromCol, int toRow, int toCol, int value) { + Animation anim(AnimationType::SLIDE, toRow, toCol, value, 0.2f, EasingType::EASE_OUT); + anim.setupSlide(fromRow, fromCol, toRow, toCol); + animations.push_back(anim); +} + +void AnimationManager::addMergeAnimation(int row, int col, int value) { + Animation anim(AnimationType::MERGE, row, col, value, 0.3f, EasingType::EASE_IN_OUT); + anim.setupMerge(); + + // Add particle effect for merges + Animation particles(AnimationType::PARTICLE, row, col, value, 0.6f, EasingType::LINEAR); + particles.setupParticles(10, true); + + animations.push_back(anim); + animations.push_back(particles); +} + +void AnimationManager::addSpawnAnimation(int row, int col, int value) { + Animation anim(AnimationType::SPAWN, row, col, value, 0.3f, EasingType::EASE_OUT); + anim.setupSpawn(); + animations.push_back(anim); +} + +void AnimationManager::addScoreAnimation(int score, int prevScore) { + // This would ideally animate a score change in the UI + // For now, just add some particle effects to celebrate score increase + if (score > prevScore) { + int scoreDiff = score - prevScore; + int particleCount = std::min(20, scoreDiff / 10); + + if (particleCount > 0) { + // Get window dimensions - assuming score area is in top right + Animation particles(AnimationType::PARTICLE, 1, 15, scoreDiff, 0.8f, EasingType::LINEAR); + particles.setupParticles(particleCount); + animations.push_back(particles); + } + } +} + +void AnimationManager::addGameOverAnimation() { + // Add big particle explosion in center of board + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + Animation particles(AnimationType::PARTICLE, i, j, 0, 1.0f, EasingType::LINEAR); + particles.setupParticles(5); + animations.push_back(particles); + } + } + + // Add transition effect + addTransitionEffect("fade", 0.7f); +} + +void AnimationManager::addVictoryAnimation() { + // Add fancy particle effects for victory + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + Animation particles(AnimationType::PARTICLE, i, j, 2048, 1.5f, EasingType::LINEAR); + particles.setupParticles(8); + animations.push_back(particles); + } + } + + // Add transition effect + addTransitionEffect("sparkle", 1.0f); +} + +void AnimationManager::addTransitionEffect(const std::string& type, float duration) { + TransitionEffect effect; + effect.type = type; + effect.duration = duration; + effect.elapsed = 0.0f; + transitionEffects.push_back(effect); +} + +void AnimationManager::addParticleEffect(float x, float y, int color, int count) { + // Create a particle animation + // Convert screen coordinates to approximate board coordinates + int boardRow = static_cast<int>(y); + int boardCol = static_cast<int>(x / 6); + + Animation particles(AnimationType::PARTICLE, boardRow, boardCol, 0, 0.6f, EasingType::LINEAR); + particles.setupParticles(count); + animations.push_back(particles); +} + +void AnimationManager::drawBackground(WINDOW* win) { + int maxY, maxX; + getmaxyx(win, maxY, maxX); + + // Subtle animated pattern for backgrounds + float time = backgroundEffectTimer; + + for (int y = 0; y < maxY; y += 4) { + for (int x = 0; x < maxX; x += 8) { + float wave = sin(time + x * 0.05f + y * 0.03f) * 0.5f + 0.5f; + if (wave > 0.7f) { + wattron(win, A_DIM); + mvwaddch(win, y, x, '.'); + wattroff(win, A_DIM); + } + } + } +} + +void AnimationManager::toggleBackgroundEffects() { + enableBackgroundEffects = !enableBackgroundEffects; +} + +bool AnimationManager::loadGifBackground(const std::string& filePath) { + // This is a simplified placeholder for GIF loading + // In a real implementation, you would need a GIF decoding library + // Here we'll just simulate by loading text frames from a file + + std::ifstream file(filePath); + if (!file.is_open()) { + return false; + } + + gifFrames.clear(); + std::string line; + std::string currentFrame; + + while (std::getline(file, line)) { + if (line == "FRAME") { + if (!currentFrame.empty()) { + gifFrames.push_back(currentFrame); + currentFrame.clear(); + } + } else { + currentFrame += line + "\n"; + } + } + + if (!currentFrame.empty()) { + gifFrames.push_back(currentFrame); + } + + hasGifBackground = !gifFrames.empty(); + currentGifFrame = 0; + gifFrameTimer = 0.0f; + + return hasGifBackground; +} + +void AnimationManager::drawGifBackground(WINDOW* win) { + if (!hasGifBackground || gifFrames.empty()) { + return; + } + + // Draw the current frame + std::istringstream iss(gifFrames[currentGifFrame]); + std::string line; + int y = 0; + + while (std::getline(iss, line)) { + mvwprintw(win, y++, 0, "%s", line.c_str()); + } +} + +void AnimationManager::clearAnimations() { + animations.clear(); + transitionEffects.clear(); +} + +bool AnimationManager::hasActiveAnimations() const { + return !animations.empty() || !transitionEffects.empty(); +} \ No newline at end of file diff --git a/src/GameMode.cpp b/src/GameMode.cpp index d5e605b899ad14246ae04cbbd264d71b1fc35f03..e9ffb707c8d3d2ec24eaa578b4068f5941fcd277 100644 --- a/src/GameMode.cpp +++ b/src/GameMode.cpp @@ -1,8 +1,10 @@ #include "GameMode.hpp" -#include <ncurses.h> +#include <cmath> +#include <algorithm> +// Base GameMode implementation GameMode::GameMode(GameModeType type, const std::string& name, const std::string& description) - : type(type), name(name), description(description) { + : type(type), name(name), description(description), gameOver(false), gameWon(false) { } GameModeType GameMode::getType() const { @@ -17,134 +19,614 @@ const std::string& GameMode::getDescription() const { return description; } -// Classic Mode Implementation +bool GameMode::isGameOver() const { + return gameOver; +} + +bool GameMode::isGameWon() const { + return gameWon; +} + +void GameMode::setGameOver(bool over) { + gameOver = over; +} + +void GameMode::setGameWon(bool won) { + gameWon = won; +} + +//============================================================================== +// ClassicMode Implementation +//============================================================================== ClassicMode::ClassicMode() : GameMode(GameModeType::CLASSIC, "Classic", "The original 2048 game. Combine tiles to reach 2048!") { } -void ClassicMode::initialize() { - // Nothing special needed for classic mode +void ClassicMode::initialize(Game2048& game) { + game.resetBoard(); + gameOver = false; + gameWon = false; } -bool ClassicMode::update(float deltaTime) { - // Classic mode has no time-based updates - return true; +bool ClassicMode::update(Game2048& game, float deltaTime) { + // Check win condition + if (game.hasWon() && !gameWon) { + gameWon = true; + return true; + } + + // Check game over condition + if (!game.canMove() && !gameOver) { + gameOver = true; + return true; + } + + return false; +} + +void ClassicMode::handleInput(Game2048& game, int key) { + // Standard movement controls + switch (key) { + case KEY_LEFT: + game.moveLeft(); + break; + case KEY_RIGHT: + game.moveRight(); + break; + case KEY_UP: + game.moveUp(); + break; + case KEY_DOWN: + game.moveDown(); + break; + } } void ClassicMode::render() { - // Rendering handled by the main UI class + // No special rendering required for Classic mode } -// Timed Mode Implementation +//============================================================================== +// TimedMode Implementation +//============================================================================== TimedMode::TimedMode() : GameMode(GameModeType::TIMED, "Timed", "Race against the clock! Score as high as possible before time runs out."), - timeLimit(180.0f), // 3 minutes - timeRemaining(180.0f) { + timeLimit(180.0f), timeRemaining(180.0f), bonusTimePerMerge(1.0f) { } -void TimedMode::initialize() { +void TimedMode::initialize(Game2048& game) { + game.resetBoard(); timeRemaining = timeLimit; + gameOver = false; + gameWon = false; } -bool TimedMode::update(float deltaTime) { +bool TimedMode::update(Game2048& game, float deltaTime) { + // Decrease time timeRemaining -= deltaTime; - return timeRemaining > 0.0f; + + // Check for time out + if (timeRemaining <= 0) { + timeRemaining = 0; + if (!gameOver) { + gameOver = true; + return true; + } + } + + // Check win condition + if (game.hasWon() && !gameWon) { + gameWon = true; + return true; + } + + return false; +} + +void TimedMode::handleInput(Game2048& game, int key) { + // Standard movement controls + switch (key) { + case KEY_LEFT: + game.moveLeft(); + break; + case KEY_RIGHT: + game.moveRight(); + break; + case KEY_UP: + game.moveUp(); + break; + case KEY_DOWN: + game.moveDown(); + break; + } } void TimedMode::render() { - // Rendering handled by the UI class + // Rendering is handled by the UI class } float TimedMode::getTimeRemaining() const { return timeRemaining; } -// PowerUp Mode Implementation +void TimedMode::addTime(float seconds) { + timeRemaining += seconds; + if (timeRemaining > timeLimit) { + timeRemaining = timeLimit; + } +} + +//============================================================================== +// PowerUpMode Implementation +//============================================================================== PowerUpMode::PowerUpMode() : GameMode(GameModeType::POWERUP, "Power-Up", "Use special powers to boost your score!"), - powerUpCount(3), - doubleScoreActive(false), - removeRowActive(false), - freezeTimeActive(false) { + doubleScorePowerups(3), clearRowPowerups(2), undoMovePowerups(1), upgradeTilePowerups(1), + doubleScoreActive(false), doubleScoreTurnsLeft(0) { } -void PowerUpMode::initialize() { - powerUpCount = 3; +void PowerUpMode::initialize(Game2048& game) { + game.resetBoard(); + doubleScorePowerups = 3; + clearRowPowerups = 2; + undoMovePowerups = 1; + upgradeTilePowerups = 1; doubleScoreActive = false; - removeRowActive = false; - freezeTimeActive = false; + doubleScoreTurnsLeft = 0; + gameOver = false; + gameWon = false; } -bool PowerUpMode::update(float deltaTime) { - // Power-up specific logic - return true; +bool PowerUpMode::update(Game2048& game, float deltaTime) { + // Update double score turn counter + if (doubleScoreActive) { + doubleScoreTurnsLeft--; + if (doubleScoreTurnsLeft <= 0) { + doubleScoreActive = false; + } + } + + // Check win condition + if (game.hasWon() && !gameWon) { + gameWon = true; + return true; + } + + // Check game over condition + if (!game.canMove() && !gameOver) { + gameOver = true; + return true; + } + + return false; +} + +void PowerUpMode::handleInput(Game2048& game, int key) { + // Standard movement controls + switch (key) { + case KEY_LEFT: + game.moveLeft(); + if (doubleScoreActive) { + // If double score is active, decrease turn count + doubleScoreTurnsLeft--; + if (doubleScoreTurnsLeft <= 0) { + doubleScoreActive = false; + } + } + break; + case KEY_RIGHT: + game.moveRight(); + if (doubleScoreActive) { + doubleScoreTurnsLeft--; + if (doubleScoreTurnsLeft <= 0) { + doubleScoreActive = false; + } + } + break; + case KEY_UP: + game.moveUp(); + if (doubleScoreActive) { + doubleScoreTurnsLeft--; + if (doubleScoreTurnsLeft <= 0) { + doubleScoreActive = false; + } + } + break; + case KEY_DOWN: + game.moveDown(); + if (doubleScoreActive) { + doubleScoreTurnsLeft--; + if (doubleScoreTurnsLeft <= 0) { + doubleScoreActive = false; + } + } + break; + + // PowerUp specific controls + case '1': // Double Score + activateDoubleScore(); + break; + case '2': // Clear Row (will need row selection from UI) + // This is handled in the UI class + break; + case '3': // Undo Move + undoMove(game); + break; + case '4': // Upgrade Tile (will need tile selection from UI) + // This is handled in the UI class + break; + } } void PowerUpMode::render() { - // Rendering handled by the UI class + // Rendering is handled by the UI class +} + +bool PowerUpMode::activateDoubleScore() { + if (doubleScorePowerups <= 0) return false; + + doubleScorePowerups--; + doubleScoreActive = true; + doubleScoreTurnsLeft = 3; // Active for 3 turns + return true; +} + +bool PowerUpMode::clearRow(Game2048& game, int row) { + if (clearRowPowerups <= 0 || row < 0 || row >= SIZE) return false; + + clearRowPowerups--; + game.clearRow(row); + return true; } -void PowerUpMode::activatePowerUp(int type) { - if (powerUpCount <= 0) return; +bool PowerUpMode::undoMove(Game2048& game) { + if (undoMovePowerups <= 0) return false; - powerUpCount--; + undoMovePowerups--; + game.undoMove(); + return true; +} + +bool PowerUpMode::upgradeTile(Game2048& game, int row, int col) { + if (upgradeTilePowerups <= 0 || row < 0 || row >= SIZE || col < 0 || col >= SIZE) return false; - switch (type) { - case 1: // Double score - doubleScoreActive = true; + upgradeTilePowerups--; + game.upgradeTile(row, col); + return true; +} + +int PowerUpMode::getDoubleScorePowerups() const { + return doubleScorePowerups; +} + +int PowerUpMode::getClearRowPowerups() const { + return clearRowPowerups; +} + +int PowerUpMode::getUndoMovePowerups() const { + return undoMovePowerups; +} + +int PowerUpMode::getUpgradeTilePowerups() const { + return upgradeTilePowerups; +} + +bool PowerUpMode::isDoubleScoreActive() const { + return doubleScoreActive; +} + +int PowerUpMode::getDoubleScoreTurnsLeft() const { + return doubleScoreTurnsLeft; +} + +//============================================================================== +// ChallengeMode Implementation +//============================================================================== +ChallengeMode::ChallengeMode() + : GameMode(GameModeType::CHALLENGE, "Challenge", "Complete specific objectives within constraints."), + targetScore(5000), targetTile(512), movesLimit(100), movesUsed(0), currentLevel(1) { +} + +void ChallengeMode::initialize(Game2048& game) { + game.resetBoard(); + + // Set challenge based on level + currentLevel = 1; + targetScore = 2000 * currentLevel; + targetTile = 128 * std::pow(2, currentLevel - 1); + movesLimit = 50 + (currentLevel * 10); + movesUsed = 0; + + gameOver = false; + gameWon = false; +} + +bool ChallengeMode::update(Game2048& game, float deltaTime) { + // Check victory condition + if (checkVictory(game) && !gameWon) { + gameWon = true; + return true; + } + + // Check game over condition + if ((movesUsed >= movesLimit || !game.canMove()) && !gameOver) { + gameOver = true; + return true; + } + + return false; +} + +void ChallengeMode::handleInput(Game2048& game, int key) { + // Standard movement controls with move counting + switch (key) { + case KEY_LEFT: + game.moveLeft(); + incrementMoves(); break; - case 2: // Remove row - removeRowActive = true; + case KEY_RIGHT: + game.moveRight(); + incrementMoves(); break; - case 3: // Freeze time - freezeTimeActive = true; + case KEY_UP: + game.moveUp(); + incrementMoves(); + break; + case KEY_DOWN: + game.moveDown(); + incrementMoves(); break; } } -// Challenge Mode Implementation -ChallengeMode::ChallengeMode() - : GameMode(GameModeType::CHALLENGE, "Challenge", "Complete specific objectives within constraints."), - targetScore(5000), - targetTile(512), - movesLimit(100), - movesUsed(0) { +void ChallengeMode::render() { + // Rendering is handled by the UI class +} + +void ChallengeMode::incrementMoves() { + movesUsed++; +} + +bool ChallengeMode::checkVictory(const Game2048& game) const { + // Victory if either score target or tile target is reached + return (game.getScore() >= targetScore) || hasTargetTile(game); } -void ChallengeMode::initialize() { +bool ChallengeMode::hasTargetTile(const Game2048& game) const { + const auto& board = game.getBoard(); + for (int i = 0; i < SIZE; ++i) { + for (int j = 0; j < SIZE; ++j) { + if (board[i][j].getValue() >= targetTile) { + return true; + } + } + } + return false; +} + +void ChallengeMode::nextLevel(Game2048& game) { + // Advance to next level + currentLevel++; + + // Update targets based on level + targetScore = 2000 * currentLevel; + targetTile = 128 * std::pow(2, currentLevel - 1); + movesLimit = 50 + (currentLevel * 10); movesUsed = 0; + + // Reset game state + game.resetBoard(); + gameOver = false; + gameWon = false; } -bool ChallengeMode::update(float deltaTime) { - return movesUsed < movesLimit; +int ChallengeMode::getTargetScore() const { + return targetScore; } -void ChallengeMode::render() { - // Rendering handled by the UI class +int ChallengeMode::getTargetTile() const { + return targetTile; +} + +int ChallengeMode::getMovesLimit() const { + return movesLimit; +} + +int ChallengeMode::getMovesUsed() const { + return movesUsed; +} + +int ChallengeMode::getMovesRemaining() const { + return movesLimit - movesUsed; +} + +int ChallengeMode::getCurrentLevel() const { + return currentLevel; +} + +//============================================================================== +// ZenMode Implementation +//============================================================================== +ZenMode::ZenMode() + : GameMode(GameModeType::ZEN, "Zen", "Relaxed gameplay with no game over. Enjoy at your own pace."), + colorShiftEnabled(true), colorCycleTime(10.0f), currentColorTime(0.0f) { +} + +void ZenMode::initialize(Game2048& game) { + game.resetBoard(); + colorShiftEnabled = true; + currentColorTime = 0.0f; + gameOver = false; + gameWon = false; +} + +bool ZenMode::update(Game2048& game, float deltaTime) { + // Update color cycle time + if (colorShiftEnabled) { + currentColorTime += deltaTime; + if (currentColorTime > colorCycleTime) { + currentColorTime -= colorCycleTime; + } + } + + // Check win condition but don't end game + if (game.hasWon() && !gameWon) { + gameWon = true; + return true; + } + + // No game over in Zen mode, just spawn a new tile if no moves possible + if (!game.canMove()) { + game.resetBoard(); + } + + return false; +} + +void ZenMode::handleInput(Game2048& game, int key) { + // Standard movement controls + switch (key) { + case KEY_LEFT: + game.moveLeft(); + break; + case KEY_RIGHT: + game.moveRight(); + break; + case KEY_UP: + game.moveUp(); + break; + case KEY_DOWN: + game.moveDown(); + break; + case 'c': // Toggle color shift + toggleColorShift(); + break; + } +} + +void ZenMode::render() { + // Rendering is handled by the UI class +} + +float ZenMode::getCurrentColorPhase() const { + return currentColorTime / colorCycleTime; +} + +bool ZenMode::isColorShiftEnabled() const { + return colorShiftEnabled; +} + +void ZenMode::toggleColorShift() { + colorShiftEnabled = !colorShiftEnabled; } -// Tutorial Mode Implementation +//============================================================================== +// TutorialMode Implementation +//============================================================================== TutorialMode::TutorialMode() : GameMode(GameModeType::TUTORIAL, "Tutorial", "Learn how to play 2048."), - tutorialStep(0), - waitingForInput(true) { + tutorialStep(0), waitingForInput(true) { + + // Initialize tutorial messages + tutorialMessages = { + "Welcome to 2048! In this game, you'll combine tiles with the same number to create larger numbers.", + "Use the arrow keys to move all tiles in that direction. When two tiles with the same number touch, they merge!", + "The goal is to create a tile with the value 2048. Let's start by making your first move. Press an arrow key.", + "Great! Notice how tiles slide in the direction you pressed, and a new tile appears.", + "Try to create your first merge by moving tiles so that two equal numbers touch.", + "Excellent! When you merge tiles, their values add together. Keep merging to reach higher numbers.", + "Strategy tip: Try to keep your highest value tile in a corner, and build from there.", + "Another tip: Think ahead about what will happen when you make a move.", + "You're doing great! Keep practicing. Press any key to return to the main menu when you're ready." + }; } -void TutorialMode::initialize() { +void TutorialMode::initialize(Game2048& game) { + game.resetBoard(); tutorialStep = 0; waitingForInput = true; + gameOver = false; + gameWon = false; } -bool TutorialMode::update(float deltaTime) { - // Tutorial is not time-based - return true; +bool TutorialMode::update(Game2048& game, float deltaTime) { + // Tutorial mode is mostly waiting for input, not much to update + return false; +} + +void TutorialMode::handleInput(Game2048& game, int key) { + // Progress through tutorial based on input + switch (tutorialStep) { + case 0: // Welcome + case 1: // Explaining controls + advanceTutorialStep(); + break; + case 2: // First move + if (key == KEY_LEFT || key == KEY_RIGHT || key == KEY_UP || key == KEY_DOWN) { + // Process the move + if (key == KEY_LEFT) game.moveLeft(); + else if (key == KEY_RIGHT) game.moveRight(); + else if (key == KEY_UP) game.moveUp(); + else game.moveDown(); + + advanceTutorialStep(); + } + break; + case 3: // Observed first move + case 4: // Try to merge + if (key == KEY_LEFT || key == KEY_RIGHT || key == KEY_UP || key == KEY_DOWN) { + // Process the move + if (key == KEY_LEFT) game.moveLeft(); + else if (key == KEY_RIGHT) game.moveRight(); + else if (key == KEY_UP) game.moveUp(); + else game.moveDown(); + + // Check if a merge happened + // This would require tracking of merges which we simulate here + if (tutorialStep == 4 && (rand() % 3 == 0)) { // Simulate detecting a merge + advanceTutorialStep(); + } else if (tutorialStep == 3) { + advanceTutorialStep(); + } + } + break; + case 5: // Merged successful + case 6: // Strategy tip 1 + case 7: // Strategy tip 2 + case 8: // Final message + advanceTutorialStep(); + // Fix for signed/unsigned comparison warning + if (tutorialStep >= static_cast<int>(tutorialMessages.size())) { + // End of tutorial + gameOver = true; + } + break; + } } void TutorialMode::render() { - // Rendering handled by the UI class + // Rendering is handled by the UI class } void TutorialMode::advanceTutorialStep() { tutorialStep++; waitingForInput = true; +} + +int TutorialMode::getTutorialStep() const { + return tutorialStep; +} + +const std::string& TutorialMode::getCurrentMessage() const { + static const std::string endMessage = "Tutorial complete! You are now ready to play."; + + // Fix for signed/unsigned comparison warning + if (tutorialStep < static_cast<int>(tutorialMessages.size())) { + return tutorialMessages[tutorialStep]; + } + return endMessage; +} + +bool TutorialMode::isWaitingForInput() const { + return waitingForInput; } \ No newline at end of file diff --git a/src/SoundSystem.cpp b/src/SoundSystem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..efaeb57686694931a2f7edf02b90a8321969ce62 --- /dev/null +++ b/src/SoundSystem.cpp @@ -0,0 +1,93 @@ +#include "SoundSystem.hpp" +#include <iostream> + +// Mock implementation that doesn't use SDL2 +SoundSystem::SoundSystem() + : initialized(false), soundEnabled(false), musicEnabled(false), + soundVolume(0.0f), musicVolume(0.0f), currentMusic(MusicTrack::MENU) { +} + +SoundSystem::~SoundSystem() { + // Nothing to do +} + +bool SoundSystem::initialize() { + std::cout << "Sound system disabled (SDL2 not available)\n"; + return false; +} + +bool SoundSystem::initSDL() { + return false; +} + +void SoundSystem::closeSDL() { + // Nothing to do +} + +bool SoundSystem::loadSoundEffect(SoundEffect effect, const std::string& filePath) { + return false; +} + +bool SoundSystem::loadMusicTrack(MusicTrack track, const std::string& filePath) { + return false; +} + +void SoundSystem::playSound(SoundEffect effect) { + // Nothing to do +} + +void SoundSystem::queueSound(SoundEffect effect, float delay) { + // Nothing to do +} + +void SoundSystem::playMusic(MusicTrack track, bool loop) { + // Nothing to do +} + +void SoundSystem::stopMusic() { + // Nothing to do +} + +void SoundSystem::pauseMusic() { + // Nothing to do +} + +void SoundSystem::resumeMusic() { + // Nothing to do +} + +void SoundSystem::setSoundVolume(float volume) { + soundVolume = volume; +} + +void SoundSystem::setMusicVolume(float volume) { + musicVolume = volume; +} + +float SoundSystem::getSoundVolume() const { + return soundVolume; +} + +float SoundSystem::getMusicVolume() const { + return musicVolume; +} + +void SoundSystem::enableSound(bool enabled) { + soundEnabled = enabled; +} + +void SoundSystem::enableMusic(bool enabled) { + musicEnabled = enabled; +} + +bool SoundSystem::isSoundEnabled() const { + return soundEnabled; +} + +bool SoundSystem::isMusicEnabled() const { + return musicEnabled; +} + +void SoundSystem::update(float deltaTime) { + // Nothing to do +} \ No newline at end of file diff --git a/src/Tile.cpp b/src/Tile.cpp index 5a169ff090452a6c5c765382448c987bfe250676..bb9ebe0781a20acb1ce18ba58ccda8c34884df60 100644 --- a/src/Tile.cpp +++ b/src/Tile.cpp @@ -1,17 +1,103 @@ #include "Tile.hpp" -Tile::Tile() : value(0) {} +Tile::Tile() : value(0), mergedThisTurn(false), isNew(false), animationProgress(0.0f) {} void Tile::setValue(int val) { value = val; + if (val > 0) { + isNew = true; + } } int Tile::getValue() const { return value; } +void Tile::setMerged(bool merged) { + mergedThisTurn = merged; +} + +bool Tile::getMerged() const { + return mergedThisTurn; +} + +void Tile::setNew(bool newTile) { + isNew = newTile; +} + +bool Tile::getNew() const { + return isNew; +} + +void Tile::setAnimationProgress(float progress) { + animationProgress = progress; + if (progress >= 1.0f) { + // Reset flags when animation completes + isNew = false; + mergedThisTurn = false; + } +} + +float Tile::getAnimationProgress() const { + return animationProgress; +} + +void Tile::resetAnimation() { + animationProgress = 0.0f; + isNew = false; + mergedThisTurn = false; +} + +std::string Tile::getBackgroundColor() const { + // Return appropriate background color based on tile value + switch (value) { + case 0: return "#CDC1B4"; // Empty tile color + case 2: return "#EEE4DA"; // 2 tile color + case 4: return "#EDE0C8"; // 4 tile color + case 8: return "#F2B179"; // 8 tile color + case 16: return "#F59563"; // 16 tile color + case 32: return "#F67C5F"; // 32 tile color + case 64: return "#F65E3B"; // 64 tile color + case 128: return "#EDCF72"; // 128 tile color + case 256: return "#EDCC61"; // 256 tile color + case 512: return "#EDC850"; // 512 tile color + case 1024: return "#EDC53F"; // 1024 tile color + case 2048: return "#EDC22E"; // 2048 tile color + default: return "#3C3A32"; // Higher value tiles + } +} + +std::string Tile::getForegroundColor() const { + // Return appropriate text color based on tile value + if (value <= 4) { + return "#776E65"; // Dark text for low value tiles + } else { + return "#F9F6F2"; // Light text for high value tiles + } +} + +std::string Tile::getColorCode() const { + // Return ncurses compatible color pair + if (value == 0) return "1"; // Default color + + switch (value) { + case 2: return "2"; + case 4: return "3"; + case 8: return "4"; + case 16: return "5"; + case 32: return "6"; + case 64: return "7"; + case 128: return "8"; + case 256: return "9"; + case 512: return "10"; + case 1024: return "11"; + case 2048: return "12"; + default: return "13"; // Higher values + } +} + bool Tile::operator==(const Tile& other) const { - return value == other.value; + return value == other.value && value != 0; } Tile& Tile::operator*=(int multiplier) { diff --git a/src/UI.cpp b/src/UI.cpp index 3486cb957467fe2b8726820ea6568291f560e777..702a4914f0cdd70c3714fc3f32e8de53405d0173 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -1,27 +1,138 @@ #include "UI.hpp" -#include "2048.hpp" -#include "Extra.hpp" -#include "GameMode.hpp" -#include <ncurses.h> -#include <cstdlib> +#include <cmath> +#include <algorithm> #include <chrono> +#include <thread> +#include <fstream> +#include <sstream> +#include <ctime> -UI::UI() : currentState(UIState::MAIN_MENU), selectedMenuOption(0), - mainWindow(nullptr), gameWindow(nullptr), scoreWindow(nullptr), infoWindow(nullptr), - transitionTimer(0.0f), isTransitioning(false), currentGameMode(GameModeType::CLASSIC) { +// Credit text with author name +const std::string CREDIT_TEXT = "2048 Game - Created by Jamal Enoime"; + +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), + soundEnabled(true), musicEnabled(true), animationsEnabled(true), highContrastMode(false), + currentTheme(0), creditsScrollY(0.0f) { // Initialize menu options - mainMenuOptions = {"Play Game", "Select Mode", "Tutorial", "Quit"}; - modeSelectionOptions = {"Classic", "Timed", "Power-Up", "Challenge", "Back"}; - pauseMenuOptions = {"Resume", "Restart", "Main Menu", "Quit"}; + mainMenuOptions = {"Play Game", "Select Mode", "Settings", "Tutorial", "Credits", "Quit"}; + modeSelectionOptions = {"Classic", "Timed", "Power-Up", "Challenge", "Zen", "Back"}; + pauseMenuOptions = {"Resume", "Restart", "Settings", "Main Menu", "Quit"}; + settingsOptions = {"Sound: ON", "Music: ON", "Animations: ON", "High Contrast: OFF", "Theme: Default", "Back"}; + + // Initialize credits + credits = { + "2048 Game", + "", + "Created by Jamal Enoime", + "", + "Game Design", + "Jamal Enoime", + "", + "Programming", + "Jamal Enoime", + "", + "Graphics & UI", + "Jamal Enoime", + "", + "Sound Effects", + "freesound.org", + "", + "Music", + "bensound.com", + "", + "Special Thanks", + "To all the players!", + "", + "© 2025 Jamal Enoime" + }; + + // Initialize themes + initializeThemes(); } UI::~UI() { destroyWindows(); } -void UI::initialize() { +bool UI::initialize() { + // Initialize ncurses + initscr(); + start_color(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + curs_set(0); // Hide cursor + timeout(16); // Non-blocking input with ~60fps refresh rate + + // 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 + init_pair(16, COLOR_MAGENTA, COLOR_BLACK); // Magenta text + init_pair(17, COLOR_RED, COLOR_BLACK); // Red text + init_pair(18, COLOR_GREEN, COLOR_BLACK); // Green text + init_pair(19, COLOR_BLUE, COLOR_BLACK); // Blue text + init_pair(20, COLOR_WHITE, COLOR_BLUE); // White on blue + + // Initialize game + game = Game2048(); + + // Initialize animations + animations = AnimationManager(); + + // Initialize sound system + sounds.initialize(); + if (musicEnabled) { + sounds.playMusic(MusicTrack::MENU, true); + } + + // Create windows createWindows(); + + // Set up game callbacks + game.addObserver([this](GameEvent event, int row, int col, int value) { + switch (event) { + case GameEvent::TILE_MOVED: + // This event needs start and end positions - handled separately + break; + 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; + } + }); + + // Set default game mode + setGameMode(GameModeType::CLASSIC); + + return true; } void UI::createWindows() { @@ -34,15 +145,12 @@ void UI::createWindows() { // Game window for the actual 2048 board gameWindow = newwin(SIZE * 2 + 2, SIZE * 6 + 2, 2, (maxX - (SIZE * 6 + 2)) / 2); - box(gameWindow, 0, 0); // Score window for displaying score and other game info scoreWindow = newwin(3, 20, 2, maxX - 25); - box(scoreWindow, 0, 0); // Info window for instructions and status messages infoWindow = newwin(5, maxX - 4, maxY - 6, 2); - box(infoWindow, 0, 0); } void UI::destroyWindows() { @@ -52,20 +160,324 @@ void UI::destroyWindows() { if (infoWindow) delwin(infoWindow); } +void UI::initializeThemes() { + // Default theme (original 2048 colors) + Theme defaultTheme; + defaultTheme.name = "Default"; + defaultTheme.menuColor = 14; // Gold text + defaultTheme.backgroundColor = 1; // White background + defaultTheme.textColor = 0; // Black text + + // Neon theme + Theme neonTheme; + neonTheme.name = "Neon"; + neonTheme.menuColor = 15; // Cyan text + neonTheme.backgroundColor = 0; // Black background + neonTheme.textColor = 15; // Cyan text + + // Dark theme + Theme darkTheme; + darkTheme.name = "Dark"; + darkTheme.menuColor = 18; // Green text + darkTheme.backgroundColor = 0; // Black background + darkTheme.textColor = 7; // White text + + // Pastel theme + Theme pastelTheme; + pastelTheme.name = "Pastel"; + pastelTheme.menuColor = 16; // Magenta text + pastelTheme.backgroundColor = 11; // Yellow background + pastelTheme.textColor = 4; // Blue text + + // Add themes to the list + themes.push_back(defaultTheme); + themes.push_back(neonTheme); + themes.push_back(darkTheme); + themes.push_back(pastelTheme); +} + +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[4] = "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 animations + if (animationsEnabled) { + animations.update(deltaTime); + updateTextEffects(deltaTime); + updateConfetti(deltaTime); + } + + // Update sound system + sounds.update(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::CREDITS: + drawCredits(); + handleCreditsInput(); + break; + + case UIState::EXIT_CONFIRM: + drawExitConfirm(); + handleExitConfirmInput(); + break; + } + + // Limit frame rate to reduce CPU usage + std::this_thread::sleep_for(std::chrono::milliseconds(16)); // ~60 FPS + } + + // Clean up and exit + endwin(); +} + void UI::setState(UIState newState) { + previousState = currentState; currentState = newState; selectedMenuOption = 0; - isTransitioning = true; - transitionTimer = 0.5f; + + // Add transition effect + if (animationsEnabled) { + if (newState == UIState::GAME_PLAYING) { + animations.addTransitionEffect("sparkle", 0.5f); + } else if (newState == UIState::GAME_OVER) { + animations.addTransitionEffect("fade", 0.7f); + } else if (newState == UIState::VICTORY) { + animations.addTransitionEffect("sparkle", 1.0f); + addConfetti(100); + } else { + animations.addTransitionEffect("slide", 0.3f); + } + } + + // Play sound effects + if (newState == UIState::GAME_PLAYING) { + sounds.playSound(SoundEffect::MENU_SELECT); + if (musicEnabled) { + sounds.playMusic(MusicTrack::GAMEPLAY); + } + } else if (newState == UIState::MAIN_MENU) { + sounds.playSound(SoundEffect::MENU_SELECT); + if (musicEnabled) { + sounds.playMusic(MusicTrack::MENU); + } + } else if (newState == UIState::GAME_OVER) { + sounds.playSound(SoundEffect::GAMEOVER); + if (musicEnabled) { + sounds.playMusic(MusicTrack::GAMEOVER); + } + } else if (newState == UIState::VICTORY) { + sounds.playSound(SoundEffect::VICTORY); + if (musicEnabled) { + sounds.playMusic(MusicTrack::VICTORY); + } + } } UIState UI::getState() const { return currentState; } +void UI::setGameMode(GameModeType mode) { + currentGameMode = mode; + + // Create appropriate game mode instance using C++11 compatible syntax + switch (mode) { + case GameModeType::CLASSIC: + gameMode.reset(new ClassicMode()); + break; + case GameModeType::TIMED: + gameMode.reset(new TimedMode()); + break; + case GameModeType::POWERUP: + gameMode.reset(new PowerUpMode()); + break; + case GameModeType::CHALLENGE: + gameMode.reset(new ChallengeMode()); + break; + case GameModeType::ZEN: + gameMode.reset(new ZenMode()); + break; + case GameModeType::TUTORIAL: + gameMode.reset(new TutorialMode()); + 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::toggleSound() { + soundEnabled = !soundEnabled; + sounds.enableSound(soundEnabled); + settingsOptions[0] = std::string("Sound: ") + (soundEnabled ? "ON" : "OFF"); +} + +void UI::toggleMusic() { + musicEnabled = !musicEnabled; + sounds.enableMusic(musicEnabled); + settingsOptions[1] = std::string("Music: ") + (musicEnabled ? "ON" : "OFF"); +} + +void UI::toggleAnimations() { + animationsEnabled = !animationsEnabled; + settingsOptions[2] = std::string("Animations: ") + (animationsEnabled ? "ON" : "OFF"); + + if (!animationsEnabled) { + animations.clearAnimations(); + textEffects.clear(); + confetti.clear(); + } +} + +void UI::toggleHighContrast() { + highContrastMode = !highContrastMode; + settingsOptions[3] = std::string("High Contrast: ") + (highContrastMode ? "ON" : "OFF"); + + // Apply high contrast mode settings + if (highContrastMode) { + // Use high contrast colors + init_pair(1, COLOR_WHITE, COLOR_BLACK); + init_pair(2, COLOR_BLACK, COLOR_WHITE); + init_pair(3, COLOR_WHITE, COLOR_BLACK); + init_pair(4, COLOR_BLACK, COLOR_WHITE); + init_pair(5, COLOR_WHITE, COLOR_BLACK); + init_pair(6, COLOR_BLACK, COLOR_WHITE); + init_pair(7, COLOR_WHITE, COLOR_BLACK); + init_pair(8, COLOR_BLACK, COLOR_WHITE); + init_pair(9, COLOR_WHITE, COLOR_BLACK); + init_pair(10, COLOR_BLACK, COLOR_WHITE); + init_pair(11, COLOR_WHITE, COLOR_BLACK); + init_pair(12, COLOR_BLACK, COLOR_WHITE); + init_pair(13, COLOR_WHITE, COLOR_BLACK); + } else { + // Restore original colors + init_pair(1, COLOR_BLACK, COLOR_WHITE); + init_pair(2, COLOR_BLACK, COLOR_GREEN); + init_pair(3, COLOR_BLACK, COLOR_CYAN); + init_pair(4, COLOR_BLACK, COLOR_BLUE); + init_pair(5, COLOR_BLACK, COLOR_MAGENTA); + init_pair(6, COLOR_BLACK, COLOR_RED); + init_pair(7, COLOR_WHITE, COLOR_BLUE); + init_pair(8, COLOR_WHITE, COLOR_MAGENTA); + init_pair(9, COLOR_WHITE, COLOR_RED); + init_pair(10, COLOR_WHITE, COLOR_YELLOW); + init_pair(11, COLOR_BLACK, COLOR_YELLOW); + init_pair(12, COLOR_WHITE, COLOR_GREEN); + init_pair(13, COLOR_WHITE, COLOR_RED); + } +} + +bool UI::setBackgroundImage(const std::string& filePath) { + backgroundImage = filePath; + + // In a real implementation, you would load the image here + // Since ncurses doesn't support images directly, this would require + // additional libraries or a custom solution + + return true; +} + +// Drawing helpers void UI::drawCenteredText(WINDOW* win, int y, const std::string& text, bool highlight) { - int maxX, maxY; + int maxY, maxX; getmaxyx(win, maxY, maxX); + (void)maxY; // Explicitly mark maxY as unused to avoid warning + int x = (maxX - text.length()) / 2; if (highlight) wattron(win, A_REVERSE); @@ -73,27 +485,194 @@ void UI::drawCenteredText(WINDOW* win, int y, const std::string& text, bool high if (highlight) wattroff(win, A_REVERSE); } +void UI::drawBorderedWindow(WINDOW* win, const std::string& title) { + int maxY, maxX; + getmaxyx(win, maxY, maxX); + (void)maxY; // Explicitly mark maxY as unused to avoid warning + + // Draw the border + wborder(win, ACS_VLINE, ACS_VLINE, ACS_HLINE, ACS_HLINE, + ACS_ULCORNER, ACS_URCORNER, ACS_LLCORNER, ACS_LRCORNER); + + // 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::drawProgressBar(WINDOW* win, int y, int x, int width, float progress, int colorPair) { + int filledWidth = static_cast<int>(width * std::min(1.0f, std::max(0.0f, progress))); + + wattron(win, COLOR_PAIR(colorPair) | A_BOLD); + for (int i = 0; i < filledWidth; i++) { + mvwaddch(win, y, x + i, ACS_BLOCK); + } + wattroff(win, COLOR_PAIR(colorPair) | A_BOLD); + + wattron(win, A_DIM); + for (int i = filledWidth; i < width; i++) { + mvwaddch(win, y, x + i, ACS_HLINE); + } + wattroff(win, A_DIM); +} + +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 + } + + // Calculate screen position + int yPos = row * 2 + 1; + int xPos = col * 6 + 1; + + // Draw empty cell background + wattron(win, COLOR_PAIR(1)); + mvwprintw(win, yPos, xPos, " "); + wattroff(win, COLOR_PAIR(1)); + + // Draw the tile with color if not empty + if (value != 0) { + // Add shadow effect for non-zero tiles + wattron(win, COLOR_PAIR(1) | A_DIM); + mvwprintw(win, yPos + 1, xPos + 1, " "); + wattroff(win, COLOR_PAIR(1) | A_DIM); + + wattron(win, COLOR_PAIR(colorPair) | A_BOLD); + mvwprintw(win, yPos, xPos, "%5d", value); + wattroff(win, COLOR_PAIR(colorPair) | A_BOLD); + + // Add highlight to tiles with high values + if (value >= 128) { + wattron(win, A_DIM); + mvwaddch(win, yPos-1, xPos-1, '*'); + mvwaddch(win, yPos-1, xPos+5, '*'); + wattroff(win, A_DIM); + } + } +} + +// Menu drawing functions void UI::drawMainMenu() { werase(mainWindow); int maxY, maxX; getmaxyx(mainWindow, maxY, maxX); - // Title with color - wattron(mainWindow, COLOR_PAIR(COLOR_PAIR_2048) | A_BOLD); - drawCenteredText(mainWindow, maxY / 4, "2048 GAME", false); - wattroff(mainWindow, COLOR_PAIR(COLOR_PAIR_2048) | A_BOLD); + // Draw animated background + if (animationsEnabled) { + animations.drawBackground(mainWindow); + } - // Menu options - for (size_t i = 0; i < mainMenuOptions.size(); i++) { - drawCenteredText(mainWindow, maxY / 3 + i * 2, mainMenuOptions[i], i == selectedMenuOption); + // Draw decorative frame around the title + int titleY = maxY / 4; + std::string title = "2048 GAME"; + int titleLen = title.length(); + int startX = (maxX - titleLen - 4) / 2; + + // Draw animated title frame with pulsing effect + float pulseEffect = sin(backgroundEffectTimer * 4) * 0.5f + 0.5f; // 0.0 to 1.0 pulsing + + for (int i = -2; i <= titleLen + 1; i++) { + if (pulseEffect > 0.7f || (i % 2 == 0 && pulseEffect > 0.3f)) { + mvwaddch(mainWindow, titleY - 1, startX + i, ACS_HLINE | A_BOLD); + mvwaddch(mainWindow, titleY + 1, startX + i, ACS_HLINE | A_BOLD); + } + } + + for (int i = -1; i <= 1; i++) { + if (pulseEffect > 0.7f || (i % 2 == 0 && pulseEffect > 0.3f)) { + mvwaddch(mainWindow, titleY + i, startX - 2, ACS_VLINE | A_BOLD); + mvwaddch(mainWindow, titleY + i, startX + titleLen + 1, ACS_VLINE | A_BOLD); + } + } + + // Draw corners with different characters based on animation + if (pulseEffect > 0.7f) { + mvwaddch(mainWindow, titleY - 1, startX - 2, ACS_ULCORNER | A_BOLD); + mvwaddch(mainWindow, titleY - 1, startX + titleLen + 1, ACS_URCORNER | A_BOLD); + mvwaddch(mainWindow, titleY + 1, startX - 2, ACS_LLCORNER | A_BOLD); + mvwaddch(mainWindow, titleY + 1, startX + titleLen + 1, ACS_LRCORNER | A_BOLD); } - // Footer + // Title with color and pulse effect + int colorPair = 14; // Gold color + if (pulseEffect > 0.8f) colorPair = 13; // Flash the title with red + + wattron(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); + drawCenteredText(mainWindow, titleY, title, false); + wattroff(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); + + // Subtitle with fade-in effect + std::string subtitle = "The Addictive Puzzle Game"; wattron(mainWindow, A_DIM); - drawCenteredText(mainWindow, maxY - 2, "Use UP/DOWN to navigate, ENTER to select", false); + drawCenteredText(mainWindow, titleY + 3, subtitle, false); wattroff(mainWindow, A_DIM); + // Author credit + wattron(mainWindow, COLOR_PAIR(15) | A_DIM); + drawCenteredText(mainWindow, titleY + 4, "by Jamal Enoime", false); + wattroff(mainWindow, COLOR_PAIR(15) | A_DIM); + + // Menu options with animation and highlights + for (size_t i = 0; i < mainMenuOptions.size(); i++) { + int y = maxY / 3 + i * 2 + 5; + + // Add hover effects to selected option + if (static_cast<int>(i) == selectedMenuOption) { + // Draw animated arrows next to selected option + std::string menuText = ">> " + mainMenuOptions[i] + " <<"; + + // Pulse effect for selected item + wattron(mainWindow, COLOR_PAIR(2) | A_BOLD); + drawCenteredText(mainWindow, y, menuText, true); + wattroff(mainWindow, COLOR_PAIR(2) | A_BOLD); + + // Add particle effects around selected menu item if animations enabled + if (animationsEnabled && rand() % 10 == 0) { + float centerX = maxX / 2; + animations.addParticleEffect(centerX, y, 2, 1); + } + } else { + // Normal style for non-selected items + wattron(mainWindow, A_NORMAL); + drawCenteredText(mainWindow, y, mainMenuOptions[i], false); + wattroff(mainWindow, A_NORMAL); + } + } + + // Footer with animation + float footerPulse = sin(backgroundEffectTimer * 2) * 0.5f + 0.5f; + int footerAttr = footerPulse > 0.7f ? A_BOLD : A_DIM; + + wattron(mainWindow, footerAttr); + drawCenteredText(mainWindow, maxY - 2, "Use UP/DOWN to navigate, ENTER to select", false); + wattroff(mainWindow, footerAttr); + + // Version info + mvwprintw(mainWindow, maxY - 1, 1, "v1.0"); + + // Draw text effects and particles if animations enabled + if (animationsEnabled) { + drawTextEffects(); + animations.draw(mainWindow, mainWindow); // Second window is ignored in main menu + } + wrefresh(mainWindow); } @@ -103,45 +682,124 @@ void UI::drawModeSelection() { int maxY, maxX; getmaxyx(mainWindow, maxY, maxX); - // Title - wattron(mainWindow, COLOR_PAIR(COLOR_PAIR_1024) | A_BOLD); + // Draw animated background + if (animationsEnabled) { + animations.drawBackground(mainWindow); + } + + // Animated title with glow effect + float pulseEffect = sin(backgroundEffectTimer * 3) * 0.5f + 0.5f; + int colorPair = pulseEffect > 0.7f ? 11 : 10; // Alternate between yellow shades + + wattron(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); drawCenteredText(mainWindow, maxY / 4, "SELECT GAME MODE", false); - wattroff(mainWindow, COLOR_PAIR(COLOR_PAIR_1024) | A_BOLD); + wattroff(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); - // Mode options + // Mode options with animation for (size_t i = 0; i < modeSelectionOptions.size(); i++) { - drawCenteredText(mainWindow, maxY / 3 + i * 2, modeSelectionOptions[i], i == selectedMenuOption); + int y = maxY / 3 + i * 2 + 2; + + if (static_cast<int>(i) == selectedMenuOption) { + // Selected option gets special treatment + std::string optionText = "> " + modeSelectionOptions[i] + " <"; + + // Determine appropriate color for the game mode + int modeColor; + switch (i) { + case 0: modeColor = 2; break; // Classic - Green + case 1: modeColor = 3; break; // Timed - Cyan + case 2: modeColor = 4; break; // Power-Up - Blue + case 3: modeColor = 5; break; // Challenge - Magenta + case 4: modeColor = 12; break; // Zen - White on Green + default: modeColor = 1; break; // Back - Default + } + + wattron(mainWindow, COLOR_PAIR(modeColor) | A_BOLD); + drawCenteredText(mainWindow, y, optionText, true); + wattroff(mainWindow, COLOR_PAIR(modeColor) | A_BOLD); + + // Add particle effects to selected option if animations enabled + if (animationsEnabled && rand() % 15 == 0) { + float centerX = maxX / 2; + animations.addParticleEffect(centerX, y, modeColor, 1); + } + } else { + // Non-selected options + wattron(mainWindow, A_NORMAL); + drawCenteredText(mainWindow, y, modeSelectionOptions[i], false); + wattroff(mainWindow, A_NORMAL); + } } - // Mode description (if one is selected) - if (selectedMenuOption < 4) { + // Mode description + if (selectedMenuOption < 5) { std::string description; + int descColor; + switch (selectedMenuOption) { - case 0: description = "Classic 2048 - Combine tiles to reach 2048!"; break; - case 1: description = "Timed Mode - Race against the clock!"; break; - case 2: description = "Power-Up Mode - Use special abilities!"; break; - case 3: description = "Challenge Mode - Complete specific objectives!"; break; + case 0: + description = "Classic 2048 - Combine tiles to reach 2048!"; + descColor = 2; + break; + case 1: + description = "Timed Mode - Race against the clock!"; + descColor = 3; + break; + case 2: + description = "Power-Up Mode - Use special abilities!"; + descColor = 4; + break; + case 3: + description = "Challenge Mode - Complete specific objectives!"; + descColor = 5; + break; + case 4: + description = "Zen Mode - Relaxed gameplay with no game over."; + descColor = 12; + break; } - wattron(mainWindow, A_DIM); - drawCenteredText(mainWindow, maxY / 3 + modeSelectionOptions.size() * 2 + 2, description, false); - wattroff(mainWindow, A_DIM); + // Animated reveal of description + static float descriptionReveal = 0.0f; + descriptionReveal += 0.05f; + if (descriptionReveal > 1.0f) descriptionReveal = 1.0f; + + int revealLength = static_cast<int>(description.length() * descriptionReveal); + + wattron(mainWindow, COLOR_PAIR(descColor) | A_DIM); + drawCenteredText(mainWindow, maxY / 3 + static_cast<int>(modeSelectionOptions.size()) * 2 + 4, + description.substr(0, revealLength), false); + wattroff(mainWindow, COLOR_PAIR(descColor) | A_DIM); } - // Navigation help - wattron(mainWindow, A_DIM); + // Navigation help with pulsing effect + float helpPulse = sin(backgroundEffectTimer * 2) * 0.5f + 0.5f; + int helpAttr = helpPulse > 0.7f ? A_NORMAL : A_DIM; + + wattron(mainWindow, helpAttr); drawCenteredText(mainWindow, maxY - 2, "Use UP/DOWN to navigate, ENTER to select", false); - wattroff(mainWindow, A_DIM); + wattroff(mainWindow, helpAttr); + + // Draw text effects and particles if animations enabled + if (animationsEnabled) { + drawTextEffects(); + animations.draw(mainWindow, mainWindow); + } wrefresh(mainWindow); } void UI::drawPauseMenu() { - // Dim the background + // Semi-transparent overlay for the background wattron(mainWindow, A_DIM); + + // Create a pattern based on time for (int y = 0; y < getmaxy(mainWindow); y++) { for (int x = 0; x < getmaxx(mainWindow); x++) { - mvwaddch(mainWindow, y, x, ' '); + float wave = sin(backgroundEffectTimer * 2 + x * 0.05f + y * 0.05f) * 0.5f + 0.5f; + if ((x + y) % 4 == static_cast<int>(wave * 4)) { + mvwaddch(mainWindow, y, x, ' ' | A_REVERSE); + } } } wattroff(mainWindow, A_DIM); @@ -150,407 +808,1287 @@ void UI::drawPauseMenu() { int maxY, maxX; getmaxyx(mainWindow, maxY, maxX); - int boxHeight = pauseMenuOptions.size() * 2 + 4; - int boxWidth = 20; + int boxHeight = static_cast<int>(pauseMenuOptions.size()) * 2 + 4; + int boxWidth = 25; int boxY = (maxY - boxHeight) / 2; int boxX = (maxX - boxWidth) / 2; - WINDOW* pauseWin = subwin(mainWindow, boxHeight, boxWidth, boxY, boxX); - box(pauseWin, 0, 0); + // Draw an animated border + wattron(mainWindow, A_BOLD); + 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) { + + // Make border pulse with time + float borderPulse = sin(backgroundEffectTimer * 4 + (x + y) * 0.2f) * 0.5f + 0.5f; + int borderChar = (borderPulse > 0.7f) ? '#' : + (borderPulse > 0.4f) ? ACS_BLOCK : + (x == boxX || x == boxX + boxWidth - 1) ? ACS_VLINE : ACS_HLINE; + + mvwaddch(mainWindow, y, x, borderChar); + } else { + // Fill the background of the menu + mvwaddch(mainWindow, y, x, ' '); + } + } + } + wattroff(mainWindow, A_BOLD); - // Title - wattron(pauseWin, A_BOLD); - drawCenteredText(pauseWin, 1, "PAUSED", false); - wattroff(pauseWin, A_BOLD); + // Title with animation + wattron(mainWindow, COLOR_PAIR(13) | A_BOLD); + float titlePulse = sin(backgroundEffectTimer * 3) * 0.5f + 0.5f; + if (titlePulse > 0.7f) { + wattron(mainWindow, A_BLINK); + } + mvwprintw(mainWindow, boxY + 1, boxX + (boxWidth - 6) / 2, "PAUSED"); + if (titlePulse > 0.7f) { + wattroff(mainWindow, A_BLINK); + } + wattroff(mainWindow, COLOR_PAIR(13) | A_BOLD); - // Menu options + // Menu options with animation effects for (size_t i = 0; i < pauseMenuOptions.size(); i++) { - drawCenteredText(pauseWin, 3 + i * 2, pauseMenuOptions[i], i == selectedMenuOption); + if (static_cast<int>(i) == selectedMenuOption) { + // Animated selected option + wattron(mainWindow, COLOR_PAIR(2) | A_BOLD); + std::string menuText = "> " + pauseMenuOptions[i] + " <"; + + // Make the selected option "breathe" + float selectedPulse = sin(backgroundEffectTimer * 4) * 0.5f + 0.5f; + if (selectedPulse > 0.7f) { + wattron(mainWindow, A_BLINK); + } + + mvwprintw(mainWindow, boxY + 3 + static_cast<int>(i) * 2, + boxX + (boxWidth - static_cast<int>(menuText.length())) / 2, "%s", menuText.c_str()); + + if (selectedPulse > 0.7f) { + wattroff(mainWindow, A_BLINK); + } + wattroff(mainWindow, COLOR_PAIR(2) | A_BOLD); + } else { + // Non-selected options + mvwprintw(mainWindow, boxY + 3 + static_cast<int>(i) * 2, + boxX + (boxWidth - static_cast<int>(pauseMenuOptions[i].length())) / 2, + "%s", pauseMenuOptions[i].c_str()); + } } - wrefresh(pauseWin); - delwin(pauseWin); + // Draw text effects and particles if animations enabled + if (animationsEnabled) { + drawTextEffects(); + animations.draw(mainWindow, mainWindow); + } + + wrefresh(mainWindow); } -void UI::drawBoard(const Game2048& game) { - if (currentState != UIState::GAME_PLAYING) return; +void UI::drawSettings() { + werase(mainWindow); - werase(gameWindow); - box(gameWindow, 0, 0); + int maxY, maxX; + getmaxyx(mainWindow, maxY, maxX); - // Display the board - for (int i = 0; i < SIZE; ++i) { - for (int j = 0; j < SIZE; ++j) { - int value = game.getBoard()[i][j].getValue(); - int colorPair = getTileColorPair(value); + // Draw animated background + if (animationsEnabled) { + animations.drawBackground(mainWindow); + } + + // 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) { + // Selected option + std::string optionText = "> " + settingsOptions[i] + " <"; - // Calculate position for the tile - int yPos = i * 2 + 1; - int xPos = j * 6 + 1; + wattron(mainWindow, COLOR_PAIR(3) | A_BOLD); + drawCenteredText(mainWindow, y, optionText, true); + wattroff(mainWindow, COLOR_PAIR(3) | A_BOLD); - // Draw the tile with color - wattron(gameWindow, COLOR_PAIR(colorPair) | A_BOLD); - if (value != 0) { - mvwprintw(gameWindow, yPos, xPos, "%5d", value); - } else { - mvwprintw(gameWindow, yPos, xPos, " "); // Empty tile + // Add particle effects if animations enabled + if (animationsEnabled && rand() % 15 == 0) { + float centerX = maxX / 2; + animations.addParticleEffect(centerX, y, 3, 1); } - wattroff(gameWindow, COLOR_PAIR(colorPair) | A_BOLD); + } else { + // Non-selected options + wattron(mainWindow, A_NORMAL); + drawCenteredText(mainWindow, y, settingsOptions[i], false); + wattroff(mainWindow, A_NORMAL); } } - wrefresh(gameWindow); - - // Update score window - werase(scoreWindow); - box(scoreWindow, 0, 0); - - wattron(scoreWindow, COLOR_PAIR(COLOR_PAIR_HIGHER) | A_BOLD); - mvwprintw(scoreWindow, 1, 2, "Score: %d", game.getScore()); - wattroff(scoreWindow, COLOR_PAIR(COLOR_PAIR_HIGHER) | A_BOLD); - - wrefresh(scoreWindow); - - // Update info window - werase(infoWindow); - box(infoWindow, 0, 0); - - mvwprintw(infoWindow, 1, 2, "Controls: Arrow keys to move"); - mvwprintw(infoWindow, 2, 2, "P: Pause, R: Restart, Q: Quit"); + // 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); - if (currentGameMode == GameModeType::TIMED) { - mvwprintw(infoWindow, 3, 2, "Time left: %.1f", 180.0f); // Placeholder value - } else if (currentGameMode == GameModeType::POWERUP) { - mvwprintw(infoWindow, 3, 2, "Power-ups: 3 (1-3 to use)"); - } else if (currentGameMode == GameModeType::CHALLENGE) { - mvwprintw(infoWindow, 3, 2, "Target: 512 Moves: 50/100"); + // Draw text effects and particles if animations enabled + if (animationsEnabled) { + drawTextEffects(); + animations.draw(mainWindow, mainWindow); } - wrefresh(infoWindow); -} - -void UI::drawTimerInterface(float timeRemaining) { - mvwprintw(infoWindow, 3, 2, "Time left: %.1f", timeRemaining); - wrefresh(infoWindow); -} - -void UI::drawPowerUpInterface(const PowerUpMode& powerUpMode) { - mvwprintw(infoWindow, 3, 2, "Power-ups available: %d", 3); // Placeholder - mvwprintw(infoWindow, 4, 2, "1: Double Score, 2: Clear Row, 3: Undo"); - wrefresh(infoWindow); -} - -void UI::drawChallengeInterface(int targetScore, int targetTile, int movesRemaining) { - mvwprintw(infoWindow, 3, 2, "Target: %d tile Score: %d", targetTile, targetScore); - mvwprintw(infoWindow, 4, 2, "Moves remaining: %d", movesRemaining); - wrefresh(infoWindow); + wrefresh(mainWindow); } -void UI::drawTutorial(int step) { +void UI::drawCredits() { werase(mainWindow); int maxY, maxX; getmaxyx(mainWindow, maxY, maxX); + // Draw animated background + if (animationsEnabled) { + animations.drawBackground(mainWindow); + } + // Title - wattron(mainWindow, COLOR_PAIR(COLOR_PAIR_16) | A_BOLD); - drawCenteredText(mainWindow, 2, "TUTORIAL", false); - wattroff(mainWindow, COLOR_PAIR(COLOR_PAIR_16) | A_BOLD); - - // Tutorial content - std::string content; - switch (step) { - case 0: - content = "Welcome to 2048!"; - mvwprintw(mainWindow, 5, 5, "In this game, you combine tiles with the same number"); - mvwprintw(mainWindow, 6, 5, "to create a tile with the sum of those numbers."); - mvwprintw(mainWindow, 8, 5, "The goal is to create a tile with the value 2048."); - break; - case 1: - content = "Controls"; - mvwprintw(mainWindow, 5, 5, "Use the arrow keys to move all tiles in that direction."); - mvwprintw(mainWindow, 6, 5, "When two tiles with the same number touch, they merge!"); - mvwprintw(mainWindow, 8, 5, "Press 'r' to restart and 'q' to quit."); - break; - case 2: - content = "Game Modes"; - mvwprintw(mainWindow, 5, 5, "Classic: Traditional 2048 gameplay"); - mvwprintw(mainWindow, 6, 5, "Timed: Race against the clock"); - mvwprintw(mainWindow, 7, 5, "Power-Up: Use special abilities"); - mvwprintw(mainWindow, 8, 5, "Challenge: Meet specific objectives"); - break; - case 3: - content = "Strategy"; - mvwprintw(mainWindow, 5, 5, "Try to keep your highest tile in a corner."); - mvwprintw(mainWindow, 6, 5, "Plan your moves ahead. Don't make hasty decisions."); - mvwprintw(mainWindow, 7, 5, "Sometimes it's better to not merge tiles immediately."); - mvwprintw(mainWindow, 8, 5, "Focus on building up one corner of the board."); - break; - case 4: - content = "Ready to Play!"; - mvwprintw(mainWindow, 5, 5, "You've completed the tutorial!"); - mvwprintw(mainWindow, 6, 5, "Now it's time to play the game and reach 2048!"); - mvwprintw(mainWindow, 8, 5, "Good luck!"); - break; - default: - content = "End of Tutorial"; - mvwprintw(mainWindow, 5, 5, "You're ready to play 2048!"); - break; + wattron(mainWindow, COLOR_PAIR(14) | A_BOLD); + drawCenteredText(mainWindow, 2, "CREDITS", false); + wattroff(mainWindow, COLOR_PAIR(14) | A_BOLD); + + // Scroll credits + creditsScrollY += 0.05f; + if (creditsScrollY > credits.size() + maxY) { + creditsScrollY = 0; } - wattron(mainWindow, A_BOLD); - drawCenteredText(mainWindow, 3, content, false); - wattroff(mainWindow, A_BOLD); + for (size_t i = 0; i < credits.size(); i++) { + int y = 5 + static_cast<int>(i) - static_cast<int>(creditsScrollY); + + if (y >= 4 && y < maxY - 2) { + // Determine text style based on content + if (credits[i].empty()) { + // Empty line + continue; + } else if (i > 0 && credits[i-1].empty() && i+1 < credits.size() && !credits[i+1].empty()) { + // Section header + wattron(mainWindow, COLOR_PAIR(15) | A_BOLD); + drawCenteredText(mainWindow, y, credits[i], false); + wattroff(mainWindow, COLOR_PAIR(15) | A_BOLD); + } else if (credits[i] == "Jamal Enoime" || credits[i] == "Created by Jamal Enoime") { + // Author name with special styling + wattron(mainWindow, COLOR_PAIR(14) | A_BOLD); + drawCenteredText(mainWindow, y, credits[i], false); + wattroff(mainWindow, COLOR_PAIR(14) | A_BOLD); + + // Add sparkle effects around the author name if animations enabled + if (animationsEnabled && rand() % 10 == 0) { + animations.addParticleEffect(maxX / 2, y, 14, 2); + } + } else { + // Regular text + drawCenteredText(mainWindow, y, credits[i], false); + } + } + } - // Navigation + // Navigation help wattron(mainWindow, A_DIM); - if (step < 4) { - drawCenteredText(mainWindow, maxY - 2, "Press SPACE to continue or ESC to exit", false); - } else { - drawCenteredText(mainWindow, maxY - 2, "Press ESC to return to main menu", false); - } + drawCenteredText(mainWindow, maxY - 2, "Press ESC to return to main menu", false); wattroff(mainWindow, A_DIM); + // Draw text effects and particles if animations enabled + if (animationsEnabled) { + drawTextEffects(); + animations.draw(mainWindow, mainWindow); + } + wrefresh(mainWindow); } -void UI::handleInput(Game2048& game) { - switch (currentState) { - case UIState::MAIN_MENU: - handleMenuInput(); - break; - - case UIState::MODE_SELECTION: - handleMenuInput(); - break; - - case UIState::GAME_PLAYING: { - int ch = wgetch(mainWindow); - switch (ch) { - case KEY_LEFT: game.moveLeft(); break; - case KEY_RIGHT: game.moveRight(); break; - case KEY_UP: game.moveUp(); break; - case KEY_DOWN: game.moveDown(); break; - case 'r': case 'R': game.resetBoard(); break; - case 'p': case 'P': setState(UIState::PAUSE_MENU); break; - case 'q': case 'Q': setState(UIState::MAIN_MENU); break; - - // PowerUp mode specific keys - case '1': - if (currentGameMode == GameModeType::POWERUP) { - // Activate power-up 1 (would need PowerUpMode instance) - } - break; - case '2': - if (currentGameMode == GameModeType::POWERUP) { - // Activate power-up 2 - } - break; - case '3': - if (currentGameMode == GameModeType::POWERUP) { - // Activate power-up 3 - } - break; - } - break; - } - - case UIState::PAUSE_MENU: - handleMenuInput(); +void UI::drawExitConfirm() { + // Semi-transparent overlay + wattron(mainWindow, A_DIM); + + int maxY, maxX; + getmaxyx(mainWindow, maxY, maxX); + + for (int y = 0; y < maxY; y++) { + for (int x = 0; x < maxX; x++) { + mvwaddch(mainWindow, y, x, ' ' | A_REVERSE); + } + } + wattroff(mainWindow, A_DIM); + + // Confirmation box + int boxHeight = 7; + int boxWidth = 40; + int boxY = (maxY - boxHeight) / 2; + int boxX = (maxX - boxWidth) / 2; + + // Draw box with border + wattron(mainWindow, A_BOLD); + 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, ' '); + } + } + } + wattroff(mainWindow, A_BOLD); + + // Question + wattron(mainWindow, COLOR_PAIR(17) | A_BOLD); + mvwprintw(mainWindow, boxY + 2, boxX + (boxWidth - 29) / 2, "Are you sure you want to quit?"); + wattroff(mainWindow, COLOR_PAIR(17) | A_BOLD); + + // Options + std::string yes = "Yes"; + std::string no = "No"; + + // Highlight the selected option + if (selectedMenuOption == 0) { + wattron(mainWindow, COLOR_PAIR(17) | A_BOLD | A_REVERSE); + mvwprintw(mainWindow, boxY + 4, boxX + boxWidth / 3 - 2, "%s", yes.c_str()); + wattroff(mainWindow, COLOR_PAIR(17) | A_BOLD | A_REVERSE); + + wattron(mainWindow, COLOR_PAIR(18) | A_BOLD); + mvwprintw(mainWindow, boxY + 4, boxX + 2 * boxWidth / 3 - 2, "%s", no.c_str()); + wattroff(mainWindow, COLOR_PAIR(18) | A_BOLD); + } else { + wattron(mainWindow, COLOR_PAIR(17) | A_BOLD); + mvwprintw(mainWindow, boxY + 4, boxX + boxWidth / 3 - 2, "%s", yes.c_str()); + wattroff(mainWindow, COLOR_PAIR(17) | A_BOLD); + + wattron(mainWindow, COLOR_PAIR(18) | A_BOLD | A_REVERSE); + mvwprintw(mainWindow, boxY + 4, boxX + 2 * boxWidth / 3 - 2, "%s", no.c_str()); + wattroff(mainWindow, COLOR_PAIR(18) | A_BOLD | A_REVERSE); + } + + // Draw text effects and particles if animations enabled + if (animationsEnabled) { + drawTextEffects(); + animations.draw(mainWindow, mainWindow); + } + + wrefresh(mainWindow); +} + +void UI::drawBoard() { + if (currentState != UIState::GAME_PLAYING && + currentState != UIState::PAUSE_MENU && + currentState != UIState::GAME_OVER && + currentState != UIState::VICTORY) { + return; + } + + werase(gameWindow); + + // Draw fancy border with double lines + drawBorderedWindow(gameWindow, "2048"); + + // Display the board (static tiles, animations are drawn separately) + 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); + } + } + + // Draw animations if enabled + if (animationsEnabled) { + animations.draw(mainWindow, gameWindow); + } + + // Draw confetti for victory state + if (currentState == UIState::VICTORY && animationsEnabled) { + drawConfetti(); + } + + wrefresh(gameWindow); +} + +void UI::drawScore() { + werase(scoreWindow); + + // Draw border + drawBorderedWindow(scoreWindow, "Score"); + + // Current score with animation + wattron(scoreWindow, COLOR_PAIR(13) | A_BOLD); + mvwprintw(scoreWindow, 1, 2, "%d", game.getScore()); + wattroff(scoreWindow, COLOR_PAIR(13) | A_BOLD); + + // High score + wattron(scoreWindow, COLOR_PAIR(14) | A_NORMAL); + mvwprintw(scoreWindow, 2, 2, "Best: %d", game.getHighScore()); + wattroff(scoreWindow, COLOR_PAIR(14) | A_NORMAL); + + wrefresh(scoreWindow); +} + +void UI::drawInfo() { + werase(infoWindow); + + // Draw border + drawBorderedWindow(infoWindow, "Info"); + + // Add a title to info window + wattron(infoWindow, A_BOLD); + mvwprintw(infoWindow, 1, 2, "Controls:"); + wattroff(infoWindow, A_BOLD); + mvwprintw(infoWindow, 1, 12, "↑↓←→ to move"); + + mvwprintw(infoWindow, 2, 2, "P: Pause R: Restart Q: Quit"); + + // Draw mode-specific info + if (gameMode) { + switch (currentGameMode) { + case GameModeType::TIMED: + drawTimedModeUI(); + break; + case GameModeType::POWERUP: + drawPowerUpModeUI(); + break; + case GameModeType::CHALLENGE: + drawChallengeModeUI(); + break; + case GameModeType::ZEN: + drawZenModeUI(); + break; + case GameModeType::CLASSIC: + default: + // Classic mode - show goal + wattron(infoWindow, COLOR_PAIR(12) | A_BOLD); + mvwprintw(infoWindow, 3, 2, "🏆 Goal: Reach 2048 tile!"); + wattroff(infoWindow, COLOR_PAIR(12) | A_BOLD); + break; + } + } + + wrefresh(infoWindow); +} + +void UI::drawTimedModeUI() { + TimedMode* timedMode = dynamic_cast<TimedMode*>(gameMode.get()); + if (!timedMode) return; + + // Get time remaining + float timeRemaining = timedMode->getTimeRemaining(); + + // Choose color based on time remaining + int colorPair; + if (timeRemaining > 60.0f) { + colorPair = 2; // Green - plenty of time + } else if (timeRemaining > 30.0f) { + colorPair = 11; // Yellow - getting low + } else if (timeRemaining > 10.0f) { + colorPair = 6; // Red - very low + } else { + // Flash red when time is critical (under 10 seconds) + colorPair = 13; + wattron(infoWindow, A_BLINK); + } + + // Draw time remaining + wattron(infoWindow, COLOR_PAIR(colorPair) | A_BOLD); + mvwprintw(infoWindow, 3, 2, "⏱ Time left: %.1f", timeRemaining); + wattroff(infoWindow, COLOR_PAIR(colorPair) | A_BOLD); + + if (timeRemaining <= 10.0f) { + wattroff(infoWindow, A_BLINK); + } + + // Draw progress bar + int barWidth = 30; + float progress = timeRemaining / 180.0f; // Assuming 180 seconds total + drawProgressBar(infoWindow, 4, 2, barWidth, progress, colorPair); +} + +void UI::drawPowerUpModeUI() { + PowerUpMode* powerUpMode = dynamic_cast<PowerUpMode*>(gameMode.get()); + if (!powerUpMode) return; + + // Get powerup counts + int doubleScorePowerups = powerUpMode->getDoubleScorePowerups(); + int clearRowPowerups = powerUpMode->getClearRowPowerups(); + int undoMovePowerups = powerUpMode->getUndoMovePowerups(); + int upgradeTilePowerups = powerUpMode->getUpgradeTilePowerups(); + + // Draw powerup information + wattron(infoWindow, COLOR_PAIR(4) | A_BOLD); + mvwprintw(infoWindow, 3, 2, "✨ PowerUps:"); + wattroff(infoWindow, COLOR_PAIR(4) | A_BOLD); + + // Double score powerup + wattron(infoWindow, doubleScorePowerups > 0 ? A_NORMAL : A_DIM); + mvwprintw(infoWindow, 4, 2, "1:2xScore(%d)", doubleScorePowerups); + wattroff(infoWindow, doubleScorePowerups > 0 ? A_NORMAL : A_DIM); + + // Clear row powerup + wattron(infoWindow, clearRowPowerups > 0 ? A_NORMAL : A_DIM); + mvwprintw(infoWindow, 4, 15, "2:Clear(%d)", clearRowPowerups); + wattroff(infoWindow, clearRowPowerups > 0 ? A_NORMAL : A_DIM); + + // Undo move powerup + wattron(infoWindow, undoMovePowerups > 0 ? A_NORMAL : A_DIM); + mvwprintw(infoWindow, 4, 28, "3:Undo(%d)", undoMovePowerups); + wattroff(infoWindow, undoMovePowerups > 0 ? A_NORMAL : A_DIM); + + // Upgrade tile powerup + wattron(infoWindow, upgradeTilePowerups > 0 ? A_NORMAL : A_DIM); + mvwprintw(infoWindow, 4, 40, "4:Upgrade(%d)", upgradeTilePowerups); + wattroff(infoWindow, upgradeTilePowerups > 0 ? A_NORMAL : A_DIM); + + // Display if double score is active + if (powerUpMode->isDoubleScoreActive()) { + wattron(infoWindow, COLOR_PAIR(11) | A_BOLD | A_BLINK); + mvwprintw(infoWindow, 3, 15, "2X SCORE ACTIVE! (%d turns)", + powerUpMode->getDoubleScoreTurnsLeft()); + wattroff(infoWindow, COLOR_PAIR(11) | A_BOLD | A_BLINK); + } +} + +void UI::drawChallengeModeUI() { + ChallengeMode* challengeMode = dynamic_cast<ChallengeMode*>(gameMode.get()); + if (!challengeMode) return; + + // Get challenge targets + int targetScore = challengeMode->getTargetScore(); + int targetTile = challengeMode->getTargetTile(); + int movesRemaining = challengeMode->getMovesRemaining(); + int currentLevel = challengeMode->getCurrentLevel(); + + // Draw challenge information + wattron(infoWindow, COLOR_PAIR(5) | A_BOLD); + mvwprintw(infoWindow, 3, 2, "🎯 Level %d Challenge:", currentLevel); + wattroff(infoWindow, COLOR_PAIR(5) | A_BOLD); + + // Draw target score + mvwprintw(infoWindow, 4, 2, "Score: %d/%d", game.getScore(), targetScore); + + // Draw progress bar for score + float scoreProgress = std::min(1.0f, static_cast<float>(game.getScore()) / targetScore); + drawProgressBar(infoWindow, 4, 20, 15, scoreProgress, 5); + + // Draw target tile + mvwprintw(infoWindow, 4, 37, "Tile: %d", targetTile); + + // Draw moves remaining + int moveColor = movesRemaining > 10 ? 2 : (movesRemaining > 5 ? 11 : 13); + wattron(infoWindow, COLOR_PAIR(moveColor) | A_BOLD); + mvwprintw(infoWindow, 3, 23, "Moves: %d", movesRemaining); + wattroff(infoWindow, COLOR_PAIR(moveColor) | A_BOLD); +} + +void UI::drawZenModeUI() { + ZenMode* zenMode = dynamic_cast<ZenMode*>(gameMode.get()); + if (!zenMode) return; + + // Draw zen mode information + wattron(infoWindow, COLOR_PAIR(12) | A_BOLD); + mvwprintw(infoWindow, 3, 2, "☯ Zen Mode - Relax and enjoy"); + wattroff(infoWindow, COLOR_PAIR(12) | A_BOLD); + + // Draw color shift info + mvwprintw(infoWindow, 4, 2, "Press C to %s color shift", + zenMode->isColorShiftEnabled() ? "disable" : "enable"); +} + +void UI::drawTutorial() { + werase(mainWindow); + + int maxY, maxX; + getmaxyx(mainWindow, maxY, maxX); + + // Draw animated background + if (animationsEnabled) { + animations.drawBackground(mainWindow); + } + + TutorialMode* tutorialMode = dynamic_cast<TutorialMode*>(gameMode.get()); + if (!tutorialMode) { + // Create new tutorial mode if needed (use C++11 style here too) + tutorialMode = new TutorialMode(); + tutorialMode->initialize(game); + } + + // Title with animated underline + wattron(mainWindow, COLOR_PAIR(5) | A_BOLD); + drawCenteredText(mainWindow, 2, "TUTORIAL", false); + wattroff(mainWindow, COLOR_PAIR(5) | A_BOLD); + + // Animated underline + int titleWidth = 8; // "TUTORIAL" length + int startX = (maxX - titleWidth) / 2; + float underlinePulse = sin(backgroundEffectTimer * 3) * 0.5f + 0.5f; + + wattron(mainWindow, COLOR_PAIR(5) | (underlinePulse > 0.7 ? A_BOLD : A_NORMAL)); + for (int x = 0; x < titleWidth; x++) { + if (underlinePulse > 0.3 || x % 2 == 0) { + mvwaddch(mainWindow, 3, startX + x, ACS_HLINE); + } + } + wattroff(mainWindow, COLOR_PAIR(5) | (underlinePulse > 0.7 ? A_BOLD : A_NORMAL)); + + // Tutorial progress + int step = tutorialMode->getTutorialStep(); + + // Step indicator + std::string stepText = "Step " + std::to_string(step + 1) + " of 9"; + wattron(mainWindow, A_DIM); + mvwprintw(mainWindow, 5, 5, "%s", stepText.c_str()); + wattroff(mainWindow, A_DIM); + + // Progress bar + int progressWidth = maxX - 10; + float progress = (step + 1) / 9.0f; + drawProgressBar(mainWindow, 6, 5, progressWidth, progress, 5); + + // Display the tutorial message + const std::string& message = tutorialMode->getCurrentMessage(); + + // Add a text box for the message + int msgBoxY = 8; + int msgBoxHeight = 6; + int msgBoxWidth = maxX - 10; + int msgBoxX = 5; + + // Draw box + for (int y = msgBoxY; y < msgBoxY + msgBoxHeight; y++) { + for (int x = msgBoxX; x < msgBoxX + msgBoxWidth; x++) { + if (y == msgBoxY || y == msgBoxY + msgBoxHeight - 1 || + x == msgBoxX || x == msgBoxX + msgBoxWidth - 1) { + mvwaddch(mainWindow, y, x, ACS_BLOCK); + } else { + mvwaddch(mainWindow, y, x, ' '); + } + } + } + + // Display message with word wrapping + std::istringstream iss(message); + std::string word; + std::string line; + int lineY = msgBoxY + 1; + int lineX = msgBoxX + 2; + int maxLineWidth = msgBoxWidth - 4; + + while (iss >> word) { + if (static_cast<int>(line.length() + word.length() + 1) > maxLineWidth) { + mvwprintw(mainWindow, lineY++, lineX, "%s", line.c_str()); + line = word; + } else { + if (!line.empty()) line += " "; + line += word; + } + } + + if (!line.empty()) { + mvwprintw(mainWindow, lineY, lineX, "%s", line.c_str()); + } + + // Display game board if needed for tutorial + if (step >= 2) { + drawBoard(); + } + + // Navigation instructions + wattron(mainWindow, A_DIM); + if (step < 8) { + drawCenteredText(mainWindow, maxY - 2, "Press SPACE to continue or ESC to exit", false); + } else { + float finalPulse = sin(backgroundEffectTimer * 3) * 0.5f + 0.5f; + if (finalPulse > 0.7) { + wattron(mainWindow, A_BOLD); + } + drawCenteredText(mainWindow, maxY - 2, "Press any key to return to main menu", false); + if (finalPulse > 0.7) { + wattroff + (mainWindow, A_BOLD); + } + } + wattroff(mainWindow, A_DIM); + + // Draw animations if enabled + if (animationsEnabled) { + animations.draw(mainWindow, gameWindow); + } + + wrefresh(mainWindow); + if (step >= 2) { + wrefresh(gameWindow); + } +} + +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 semi-transparent background + for (int y = 0; y < maxY; y++) { + for (int x = 0; x < maxX; x++) { + if ((y + x) % 2 == 0) { + wattron(mainWindow, A_DIM); + mvwaddch(mainWindow, y, x, ' ' | A_REVERSE); + wattroff(mainWindow, A_DIM); + } + } + } + + // Draw game over 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) { + wattron(mainWindow, COLOR_PAIR(13) | A_BOLD); + mvwaddch(mainWindow, y, x, ACS_BLOCK); + wattroff(mainWindow, COLOR_PAIR(13) | A_BOLD); + } 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, COLOR_PAIR(15) | 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, COLOR_PAIR(15) | A_BOLD); + + // Instruction to continue + wattron(mainWindow, A_DIM); + mvwprintw(mainWindow, boxY + 6, boxX + 2, "Press any key to continue"); + wattroff(mainWindow, A_DIM); +} + +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 animated background + for (int y = 0; y < maxY; y++) { + for (int x = 0; x < maxX; x++) { + float wave = sin(backgroundEffectTimer * 3 + (x + y) * 0.1f) * 0.5f + 0.5f; + if (wave > 0.7f) { + wattron(mainWindow, COLOR_PAIR(14) | A_DIM); + mvwaddch(mainWindow, y, x, '*'); + wattroff(mainWindow, COLOR_PAIR(14) | A_DIM); + } + } + } + + // Draw victory 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) { + wattron(mainWindow, COLOR_PAIR(12) | A_BOLD); + mvwaddch(mainWindow, y, x, ACS_BLOCK); + wattroff(mainWindow, COLOR_PAIR(12) | A_BOLD); + } else { + mvwaddch(mainWindow, y, x, ' '); + } + } + } + + // Victory message with pulsing animation + float pulse = sin(backgroundEffectTimer * 4) * 0.5f + 0.5f; + int colorPair = pulse > 0.7f ? 12 : 14; + + wattron(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); + mvwprintw(mainWindow, boxY + 2, boxX + (boxWidth - 18) / 2, "VICTORY! YOU WIN!"); + wattroff(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); + + // Congratulatory message + wattron(mainWindow, COLOR_PAIR(15) | A_NORMAL); + mvwprintw(mainWindow, boxY + 4, boxX + 2, "Congratulations! You've reached 2048!"); + wattroff(mainWindow, COLOR_PAIR(15) | A_NORMAL); + + // Draw final score + wattron(mainWindow, COLOR_PAIR(14) | 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, COLOR_PAIR(14) | A_BOLD); + + // Instruction to continue + wattron(mainWindow, A_DIM); + mvwprintw(mainWindow, boxY + 8, boxX + 2, "Press any key to continue"); + wattroff(mainWindow, A_DIM); +} + +// 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--; + sounds.playSound(SoundEffect::MENU_NAVIGATE); + } break; - case UIState::GAME_OVER: { - int ch = wgetch(mainWindow); - if (ch == 'r' || ch == 'R') { - game.resetBoard(); - setState(UIState::GAME_PLAYING); - } else if (ch == 'q' || ch == 'Q' || ch == 27) { // 27 is ESC - setState(UIState::MAIN_MENU); + case KEY_DOWN: + if (selectedMenuOption < static_cast<int>(mainMenuOptions.size()) - 1) { + selectedMenuOption++; + sounds.playSound(SoundEffect::MENU_NAVIGATE); } break; - } - case UIState::TUTORIAL: { - int ch = wgetch(mainWindow); - static int tutorialStep = 0; + case '\n': // Enter key + case ' ': // Space key + sounds.playSound(SoundEffect::MENU_SELECT); - if (ch == ' ') { - tutorialStep++; - if (tutorialStep > 4) { - setState(UIState::MAIN_MENU); - tutorialStep = 0; - } else { - drawTutorial(tutorialStep); - } - } else if (ch == 27) { // ESC - setState(UIState::MAIN_MENU); - tutorialStep = 0; + 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 + setGameMode(GameModeType::TUTORIAL); + setState(UIState::TUTORIAL); + break; + + case 4: // Credits + setState(UIState::CREDITS); + break; + + case 5: // Quit + setState(UIState::EXIT_CONFIRM); + break; } break; - } + + case 'q': + case 'Q': + setState(UIState::EXIT_CONFIRM); + break; } } -void UI::handleMenuInput() { - int ch = wgetch(mainWindow); +void UI::handleModeSelectionInput() { + int key = wgetch(mainWindow); + if (key == ERR) return; - switch (currentState) { - case UIState::MAIN_MENU: - if (ch == KEY_UP) { - selectedMenuOption = (selectedMenuOption - 1 + mainMenuOptions.size()) % mainMenuOptions.size(); - drawMainMenu(); - } else if (ch == KEY_DOWN) { - selectedMenuOption = (selectedMenuOption + 1) % mainMenuOptions.size(); - drawMainMenu(); - } else if (ch == 10 || ch == KEY_ENTER) { // Enter key + switch (key) { + case KEY_UP: + if (selectedMenuOption > 0) { + selectedMenuOption--; + sounds.playSound(SoundEffect::MENU_NAVIGATE); + } + break; + + case KEY_DOWN: + if (selectedMenuOption < static_cast<int>(modeSelectionOptions.size()) - 1) { + selectedMenuOption++; + sounds.playSound(SoundEffect::MENU_NAVIGATE); + } + break; + + case '\n': // Enter key + case ' ': // Space key + sounds.playSound(SoundEffect::MENU_SELECT); + + 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: // Play Game - setState(UIState::GAME_PLAYING); - break; - case 1: // Select Mode - setState(UIState::MODE_SELECTION); - break; - case 2: // Tutorial - setState(UIState::TUTORIAL); - drawTutorial(0); - break; - case 3: // Quit - endwin(); - exit(0); - break; + case 0: mode = GameModeType::CLASSIC; break; + case 1: mode = GameModeType::TIMED; break; + case 2: mode = GameModeType::POWERUP; break; + case 3: mode = GameModeType::CHALLENGE; break; + case 4: mode = GameModeType::ZEN; break; + default: mode = GameModeType::CLASSIC; break; } + + setGameMode(mode); + setState(UIState::GAME_PLAYING); } break; - case UIState::MODE_SELECTION: - if (ch == KEY_UP) { - selectedMenuOption = (selectedMenuOption - 1 + modeSelectionOptions.size()) % modeSelectionOptions.size(); - drawModeSelection(); - } else if (ch == KEY_DOWN) { - selectedMenuOption = (selectedMenuOption + 1) % modeSelectionOptions.size(); - drawModeSelection(); - } else if (ch == 10 || ch == KEY_ENTER) { // Enter key - switch (selectedMenuOption) { - case 0: // Classic - setGameMode(GameModeType::CLASSIC); - setState(UIState::GAME_PLAYING); - break; - case 1: // Timed - setGameMode(GameModeType::TIMED); - setState(UIState::GAME_PLAYING); - break; - case 2: // Power-Up - setGameMode(GameModeType::POWERUP); - setState(UIState::GAME_PLAYING); - break; - case 3: // Challenge - setGameMode(GameModeType::CHALLENGE); - setState(UIState::GAME_PLAYING); - break; - case 4: // Back - setState(UIState::MAIN_MENU); - 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); + sounds.playSound(SoundEffect::MENU_SELECT); + } + break; + + case 'q': + case 'Q': + case 27: // ESC key + setState(UIState::PAUSE_MENU); + break; + + // PowerUp mode specific keys + case '1': + case '2': + case '3': + case '4': + if (currentGameMode == GameModeType::POWERUP) { + // Pass to game mode for handling + if (gameMode) { + gameMode->handleInput(game, key); + sounds.playSound(SoundEffect::POWERUP_ACTIVATE); } - } else if (ch == 27) { // ESC - setState(UIState::MAIN_MENU); } break; - case UIState::PAUSE_MENU: - if (ch == KEY_UP) { - selectedMenuOption = (selectedMenuOption - 1 + pauseMenuOptions.size()) % pauseMenuOptions.size(); - drawPauseMenu(); - } else if (ch == KEY_DOWN) { - selectedMenuOption = (selectedMenuOption + 1) % pauseMenuOptions.size(); - drawPauseMenu(); - } else if (ch == 10 || ch == KEY_ENTER) { // Enter key - switch (selectedMenuOption) { - case 0: // Resume - setState(UIState::GAME_PLAYING); - break; - case 1: // Restart - // Need to reset the game - setState(UIState::GAME_PLAYING); - break; - case 2: // Main Menu - setState(UIState::MAIN_MENU); - break; - case 3: // Quit - endwin(); - exit(0); - break; + // Zen mode specific keys + case 'c': + case 'C': + if (currentGameMode == GameModeType::ZEN) { + // Pass to game mode for handling + if (gameMode) { + gameMode->handleInput(game, key); + sounds.playSound(SoundEffect::MENU_SELECT); } - } else if (ch == 27 || ch == 'p' || ch == 'P') { // ESC or P to resume - setState(UIState::GAME_PLAYING); } break; } } -void UI::showGameOver(Game2048& game) { - // Dim the background - wattron(mainWindow, A_DIM); - for (int y = 0; y < getmaxy(mainWindow); y++) { - for (int x = 0; x < getmaxx(mainWindow); x++) { - mvwaddch(mainWindow, y, x, ' '); +void UI::handlePauseMenuInput() { + int key = wgetch(mainWindow); + if (key == ERR) return; + + switch (key) { + case KEY_UP: + if (selectedMenuOption > 0) { + selectedMenuOption--; + sounds.playSound(SoundEffect::MENU_NAVIGATE); + } + break; + + case KEY_DOWN: + if (selectedMenuOption < static_cast<int>(pauseMenuOptions.size()) - 1) { + selectedMenuOption++; + sounds.playSound(SoundEffect::MENU_NAVIGATE); + } + break; + + case '\n': // Enter key + case ' ': // Space key + sounds.playSound(SoundEffect::MENU_SELECT); + + 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: // Settings + setState(UIState::SETTINGS); + break; + + case 3: // Main Menu + setState(UIState::MAIN_MENU); + break; + + case 4: // 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--; + sounds.playSound(SoundEffect::MENU_NAVIGATE); + } + break; + + case KEY_DOWN: + if (selectedMenuOption < static_cast<int>(settingsOptions.size()) - 1) { + selectedMenuOption++; + sounds.playSound(SoundEffect::MENU_NAVIGATE); + } + break; + + case '\n': // Enter key + case ' ': // Space key + sounds.playSound(SoundEffect::MENU_SELECT); + + switch (selectedMenuOption) { + case 0: // Sound toggle + toggleSound(); + break; + + case 1: // Music toggle + toggleMusic(); + break; + + case 2: // Animations toggle + toggleAnimations(); + break; + + case 3: // High Contrast toggle + toggleHighContrast(); + break; + + case 4: // Theme selection + // Cycle through themes + setTheme((currentTheme + 1) % static_cast<int>(themes.size())); + break; + + case 5: // Back + setState(previousState); + break; + } + break; + + case 27: // ESC key + setState(previousState); + break; + } +} + +void UI::handleTutorialInput() { + int key = wgetch(mainWindow); + if (key == ERR) return; + + TutorialMode* tutorialMode = dynamic_cast<TutorialMode*>(gameMode.get()); + if (tutorialMode) { + // Pass key to tutorial mode + tutorialMode->handleInput(game, key); + + // Check if tutorial is completed + if (tutorialMode->getTutorialStep() >= 9) { + setState(UIState::MAIN_MENU); } } - wattroff(mainWindow, A_DIM); - // Create a popup window - int maxY, maxX; - getmaxyx(mainWindow, maxY, maxX); + // ESC to exit tutorial + if (key == 27) { + setState(UIState::MAIN_MENU); + } +} + +void UI::handleCreditsInput() { + int key = wgetch(mainWindow); + if (key == ERR) return; - int boxHeight = 7; - int boxWidth = 30; - int boxY = (maxY - boxHeight) / 2; - int boxX = (maxX - boxWidth) / 2; + // Any key returns to main menu + setState(UIState::MAIN_MENU); +} + +void UI::handleExitConfirmInput() { + int key = wgetch(mainWindow); + if (key == ERR) return; - WINDOW* gameOverWin = subwin(mainWindow, boxHeight, boxWidth, boxY, boxX); - box(gameOverWin, 0, 0); + switch (key) { + case KEY_LEFT: + selectedMenuOption = 0; // Yes + sounds.playSound(SoundEffect::MENU_NAVIGATE); + break; + + case KEY_RIGHT: + selectedMenuOption = 1; // No + sounds.playSound(SoundEffect::MENU_NAVIGATE); + break; + + case '\n': // Enter key + case ' ': // Space key + sounds.playSound(SoundEffect::MENU_SELECT); + + if (selectedMenuOption == 0) { + // Yes - exit the game + // In real implementation, this would break the main loop + endwin(); + exit(0); + } else { + // No - return to previous state + setState(previousState); + } + break; + + case 27: // ESC key + setState(previousState); + break; + } +} + +// Game event callbacks +void UI::onTileMoved(int fromRow, int fromCol, int toRow, int toCol, int value) { + if (animationsEnabled) { + animations.addSlideAnimation(fromRow, fromCol, toRow, toCol, value); + } - // Title - wattron(gameOverWin, COLOR_PAIR(COLOR_PAIR_HIGHER) | A_BOLD); - drawCenteredText(gameOverWin, 1, "GAME OVER!", false); - wattroff(gameOverWin, COLOR_PAIR(COLOR_PAIR_HIGHER) | A_BOLD); + if (soundEnabled) { + sounds.playSound(SoundEffect::MOVE); + } +} + +void UI::onTileMerged(int row, int col, int value) { + if (animationsEnabled) { + animations.addMergeAnimation(row, col, value); + } - // Score - std::string scoreText = "Final Score: " + std::to_string(game.getScore()); - drawCenteredText(gameOverWin, 3, scoreText, false); + if (soundEnabled) { + sounds.playSound(SoundEffect::MERGE); + } +} + +void UI::onTileSpawned(int row, int col, int value) { + if (animationsEnabled) { + animations.addSpawnAnimation(row, col, value); + } - // Options - drawCenteredText(gameOverWin, 5, "R: Restart Q: Quit", false); + if (soundEnabled) { + sounds.playSound(SoundEffect::SPAWN); + } +} + +void UI::onScoreChanged(int newScore, int prevScore) { + if (animationsEnabled) { + animations.addScoreAnimation(newScore, prevScore); + } +} + +void UI::onGameOver() { + setState(UIState::GAME_OVER); - wrefresh(gameOverWin); + if (animationsEnabled) { + animations.addGameOverAnimation(); + } - // Wait for input - while (true) { - int ch = wgetch(gameOverWin); - if (ch == 'r' || ch == 'R') { - game.resetBoard(); - setState(UIState::GAME_PLAYING); - break; - } else if (ch == 'q' || ch == 'Q' || ch == 27) { // Q or ESC - setState(UIState::MAIN_MENU); - break; - } + if (soundEnabled) { + sounds.playSound(SoundEffect::GAMEOVER); + } +} + +void UI::onVictory() { + setState(UIState::VICTORY); + + if (animationsEnabled) { + animations.addVictoryAnimation(); + addConfetti(100); } - delwin(gameOverWin); + if (soundEnabled) { + sounds.playSound(SoundEffect::VICTORY); + } } -void UI::setGameMode(GameModeType mode) { - currentGameMode = mode; +// Effect methods +void UI::updateTextEffects(float deltaTime) { + for (auto it = textEffects.begin(); it != textEffects.end();) { + it->elapsed += deltaTime; + if (it->elapsed >= it->duration) { + it = textEffects.erase(it); + } else { + ++it; + } + } } -GameModeType UI::getGameMode() const { - return currentGameMode; +void UI::drawTextEffects() { + for (const auto& effect : textEffects) { + float progress = effect.elapsed / effect.duration; + int y = effect.row; + int x = effect.centered ? + (getmaxx(mainWindow) - effect.text.length()) / 2 : + effect.col; + + if (effect.type == "fade") { + // Fade in/out effect + int attr; + if (progress < 0.3f) attr = A_DIM; + else if (progress < 0.7f) attr = A_NORMAL; + else attr = A_BOLD; + + wattron(mainWindow, COLOR_PAIR(effect.color) | attr); + mvwprintw(mainWindow, y, x, "%s", effect.text.c_str()); + wattroff(mainWindow, COLOR_PAIR(effect.color) | attr); + } else if (effect.type == "pulse") { + // Pulsing effect + float pulse = sin(progress * M_PI * 3) * 0.5f + 0.5f; + int attr = pulse > 0.7f ? A_BOLD : (pulse > 0.3f ? A_NORMAL : A_DIM); + + wattron(mainWindow, COLOR_PAIR(effect.color) | attr); + mvwprintw(mainWindow, y, x, "%s", effect.text.c_str()); + wattroff(mainWindow, COLOR_PAIR(effect.color) | attr); + } else if (effect.type == "rainbow") { + // Rainbow effect - cycle through colors + int colorOffset = static_cast<int>(progress * 10) % 5; + for (size_t i = 0; i < effect.text.length(); i++) { + int colorPair = ((i + colorOffset) % 5) + 14; // Cycle through colors 14-18 + wattron(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); + mvwaddch(mainWindow, y, x + static_cast<int>(i), effect.text[i]); + wattroff(mainWindow, COLOR_PAIR(colorPair) | A_BOLD); + } + } else if (effect.type == "typing") { + // Typing effect + int chars = static_cast<int>(effect.text.length() * progress); + wattron(mainWindow, COLOR_PAIR(effect.color) | A_BOLD); + mvwprintw(mainWindow, y, x, "%s", effect.text.substr(0, chars).c_str()); + wattroff(mainWindow, COLOR_PAIR(effect.color) | A_BOLD); + } + } } -void UI::displayScore(const Game2048& game) { - // This is now handled in drawBoard +void UI::addTextEffect(const std::string& text, int row, int col, const std::string& type, + float duration, int color, bool centered) { + TextEffect effect; + effect.text = text; + effect.row = row; + effect.col = col; + effect.type = type; + effect.duration = duration; + effect.elapsed = 0.0f; + effect.color = color; + effect.centered = centered; + + textEffects.push_back(effect); } -void UI::displayInstructions() { - // This is now handled in drawBoard with the info window +void UI::updateConfetti(float deltaTime) { + for (auto it = confetti.begin(); it != confetti.end();) { + // Update position + it->y += it->vy * deltaTime * 5.0f; + it->x += it->vx * deltaTime * 5.0f; + + // Add gravity + it->vy += 2.0f * deltaTime; + + // Update rotation + it->angle += it->rotation * deltaTime; + + // Update lifetime + it->lifetime -= deltaTime; + + // Remove if lifetime expired or out of screen + int maxY, maxX; + getmaxyx(mainWindow, maxY, maxX); + + if (it->lifetime <= 0.0f || it->y >= maxY || it->x < 0 || it->x >= maxX) { + it = confetti.erase(it); + } else { + ++it; + } + } } + +void UI::drawConfetti() { + for (const auto& c : confetti) { + // Determine how to draw based on rotation angle + char symbol; + if (fmod(c.angle, 2.0f * M_PI) < M_PI / 2 || + fmod(c.angle, 2.0f * M_PI) > 3 * M_PI / 2) { + symbol = '-'; + } else { + symbol = '|'; + } + + // Adjust color based on lifetime + int attr = c.lifetime > 0.7f ? A_BOLD : (c.lifetime > 0.3f ? A_NORMAL : A_DIM); + + wattron(mainWindow, COLOR_PAIR(c.color) | attr); + mvwaddch(mainWindow, static_cast<int>(c.y), static_cast<int>(c.x), c.symbol ? c.symbol : symbol); + wattroff(mainWindow, COLOR_PAIR(c.color) | attr); + } +} + +void UI::addConfetti(int amount) { + int maxY, maxX; + getmaxyx(mainWindow, maxY, maxX); + (void)maxY; // Explicitly mark maxY as unused to avoid warning + + std::srand(std::time(nullptr)); + + for (int i = 0; i < amount; i++) { + Confetti c; + c.x = std::rand() % maxX; + c.y = std::rand() % 5; // Start near the top + c.vx = (std::rand() % 100 - 50) / 25.0f; // -2.0 to 2.0 + c.vy = (std::rand() % 50) / 25.0f; // 0 to 2.0 + c.angle = (std::rand() % 628) / 100.0f; // 0 to 2π + c.rotation = (std::rand() % 200 - 100) / 50.0f; // -2.0 to 2.0 + c.size = 1.0f; + c.symbol = "*+.ox"[std::rand() % 5]; // Random symbol + c.color = 14 + (std::rand() % 5); // Random color from palette + c.lifetime = 1.0f + (std::rand() % 100) / 100.0f; // 1.0 to 2.0 seconds + + confetti.push_back(c); + } +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 934ac4ed69c018f3dcb34f24f595f52e2b151c73..3d2dd9282d519033c4150d423307fe77eead8c94 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,10 @@ #include "2048.hpp" #include "UI.hpp" #include "Extra.hpp" -#include "GameMode.hpp" #include <ncurses.h> #include <chrono> #include <thread> +#include <iostream> // Helper function for tracking time float getDeltaTime() { @@ -16,78 +16,27 @@ float getDeltaTime() { } int main() { - // Initialize the game and colors - initializeGame(); - - Game2048 game; - UI ui; - ui.initialize(); - - // Create game mode instances - ClassicMode classicMode; - TimedMode timedMode; - PowerUpMode powerUpMode; - ChallengeMode challengeMode; - TutorialMode tutorialMode; - - // Show the main menu initially - ui.drawMainMenu(); - - float timeRemaining = 180.0f; // For timed mode - - while (true) { - float deltaTime = getDeltaTime(); + try { + // Initialize the game + initializeGame(); - // Handle timing for timed mode - if (ui.getState() == UIState::GAME_PLAYING && ui.getGameMode() == GameModeType::TIMED) { - timeRemaining -= deltaTime; - ui.drawTimerInterface(timeRemaining); - - if (timeRemaining <= 0) { - ui.showGameOver(game); - } + // Create and initialize UI + UI ui; + if (!ui.initialize()) { + cleanupGame(); + return 1; } - // Handle user input based on current state - ui.handleInput(game); - - // Update display based on current state - switch (ui.getState()) { - case UIState::MAIN_MENU: - ui.drawMainMenu(); - break; - - case UIState::MODE_SELECTION: - ui.drawModeSelection(); - break; - - case UIState::GAME_PLAYING: - ui.drawBoard(game); - - // Check for game over - if (!game.canMove()) { - ui.showGameOver(game); - ui.drawBoard(game); - } - break; - - case UIState::PAUSE_MENU: - ui.drawPauseMenu(); - break; - - case UIState::GAME_OVER: - // Handled in showGameOver - break; - - case UIState::TUTORIAL: - // Handled in handleInput and drawTutorial - break; - } + // The UI class now handles the game loop internally + ui.run(); - // Sleep a bit to limit CPU usage - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // Cleanup + 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; } - - cleanupGame(); - return 0; } \ No newline at end of file