diff --git a/.vscode/settings.json b/.vscode/settings.json index a7b04eea3c..aa7768f142 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -65,6 +65,7 @@ "stdexcept": "cpp", "streambuf": "cpp", "cinttypes": "cpp", - "typeinfo": "cpp" + "typeinfo": "cpp", + "list": "cpp" } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2b69b8b02..759c05a2c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -384,6 +384,7 @@ list(APPEND SOURCE_FILES displayapp/screens/ApplicationList.cpp displayapp/screens/Notifications.cpp displayapp/screens/Twos.cpp + displayapp/screens/Adder.cpp displayapp/screens/HeartRate.cpp displayapp/screens/FlashLight.cpp displayapp/screens/List.cpp diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 6671ac9e51..5a15eca4f8 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -23,6 +23,7 @@ #include "displayapp/screens/SystemInfo.h" #include "displayapp/screens/Tile.h" #include "displayapp/screens/Twos.h" +#include "displayapp/screens/Adder.h" #include "displayapp/screens/FlashLight.h" #include "displayapp/screens/BatteryInfo.h" #include "displayapp/screens/Steps.h" diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 67bbfa7d41..2e158e8bf5 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -6,6 +6,7 @@ #include "displayapp/screens/Dice.h" #include "displayapp/screens/Timer.h" #include "displayapp/screens/Twos.h" +#include "displayapp/screens/Adder.h" #include "displayapp/screens/Tile.h" #include "displayapp/screens/ApplicationList.h" #include "displayapp/screens/WatchFaceDigital.h" diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 2104a267c0..03baf42c14 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -21,6 +21,7 @@ namespace Pinetime { Paint, Paddle, Twos, + Adder, HeartRate, Navigation, StopWatch, diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index d78587609e..70fe4a85cd 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -10,6 +10,7 @@ else () set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paint") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Paddle") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Twos") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Adder") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation") diff --git a/src/displayapp/screens/Adder.cpp b/src/displayapp/screens/Adder.cpp new file mode 100644 index 0000000000..9f633e587b --- /dev/null +++ b/src/displayapp/screens/Adder.cpp @@ -0,0 +1,340 @@ +#include "displayapp/DisplayApp.h" +#include "displayapp/screens/Adder.h" +#include <cstdlib> // For std::rand +#include <algorithm> // For std::max + +using namespace Pinetime::Applications::Screens; + +Adder::Adder(Pinetime::Components::LittleVgl& lvgl, Controllers::FS& fs) : lvgl(lvgl), filesystem(fs) { + InitializeGame(); +} + +Adder::~Adder() { + CleanUp(); +} + +void Adder::InitializeGame() { + LoadGame(); + + tileBuffer = new lv_color_t[TileSize * TileSize]; + std::fill(tileBuffer, tileBuffer + TileSize * TileSize, LV_COLOR_WHITE); + + displayHeight = LV_VER_RES; + displayWidth = LV_HOR_RES; + + fieldHeight = displayHeight / TileSize - 2; + fieldWidth = displayWidth / TileSize - 1; + fieldOffsetHorizontal = (displayWidth - fieldWidth * TileSize) / 2; + fieldOffsetVertical = (displayHeight - fieldHeight * TileSize) / 2 + (TileSize + 0.5) / 2; + + fieldSize = fieldWidth * fieldHeight; + field = new AdderField[fieldSize]; + + InitializeBody(); + CreateLevel(); + + refreshTask = lv_task_create( + [](lv_task_t* task) { + auto* adder = static_cast<Adder*>(task->user_data); + adder->Refresh(); + }, + AdderDelayInterval, + LV_TASK_PRIO_MID, + this); + + appReady = false; + vTaskDelay(20); +} + +void Adder::CleanUp() { + delete[] field; + delete[] tileBuffer; + if (refreshTask) { + lv_task_del(refreshTask); + } + lv_obj_clean(lv_scr_act()); +} + +void Adder::LoadGame() { + lfs_file file; + + if (filesystem.FileOpen(&file, GameSavePath, LFS_O_RDONLY) == LFS_ERR_OK) { + filesystem.FileRead(&file, reinterpret_cast<uint8_t*>(&data), sizeof(AdderSave)); + filesystem.FileClose(&file); + + if (data.Version != AdderVersion) { + data = AdderSave(); + } else { + highScore = std::max(data.HighScore, highScore); + } + } else { + data = AdderSave(); + filesystem.DirCreate("/games"); + filesystem.DirCreate("/games/adder"); + SaveGame(); + } +} + +void Adder::SaveGame() { + lfs_file file; + + if (filesystem.FileOpen(&file, GameSavePath, LFS_O_WRONLY | LFS_O_CREAT) == LFS_ERR_OK) { + filesystem.FileWrite(&file, reinterpret_cast<uint8_t*>(&data), sizeof(AdderSave)); + filesystem.FileClose(&file); + } +} + +void Adder::ResetGame() { + GameOver(); + appReady = false; + highScore = std::max(highScore, static_cast<unsigned int>(adderBody.size() - 2)); + data.HighScore = highScore; + SaveGame(); + + CreateLevel(); + InitializeBody(); + UpdateScore(0); + FullRedraw(); +} + +void Adder::InitializeBody() { + adderBody.clear(); + + unsigned int startPosition = (fieldHeight / 2) * fieldWidth + fieldWidth / 2 + 2; + adderBody = {startPosition, startPosition - 1}; + + currentDirection = 1; // Start moving to the right + prevDirection = currentDirection; +} + +void Adder::CreateLevel() { + for (unsigned int i = 0; i < fieldSize; ++i) { + unsigned int x = i % fieldWidth; + unsigned int y = i / fieldWidth; + if (y == 0 || y == fieldHeight - 1 || x == 0 || x == fieldWidth - 1) { + field[i] = AdderField::SOLID; + } else { + field[i] = AdderField::BLANK; + } + } +} + +void Adder::CreateFood() { + blanks.clear(); + for (unsigned int i = 0; i < fieldSize; ++i) { + if (field[i] == AdderField::BLANK) { + blanks.push_back(i); + } + } + + if (!blanks.empty()) { + unsigned int randomIndex = std::rand() % blanks.size(); + field[blanks[randomIndex]] = AdderField::FOOD; + UpdateSingleTile(blanks[randomIndex] % fieldWidth, blanks[randomIndex] / fieldWidth, LV_COLOR_GREEN); + } +} + +bool Adder::OnTouchEvent(Pinetime::Applications::TouchEvents event) { + switch (event) { + case TouchEvents::SwipeLeft: + currentDirection = -1; + break; + case TouchEvents::SwipeUp: + currentDirection = -fieldWidth; + break; + case TouchEvents::SwipeDown: + currentDirection = fieldWidth; + break; + case TouchEvents::SwipeRight: + currentDirection = 1; + break; + case TouchEvents::LongTap: + FullRedraw(); + default: + break; + } + + // Prevent the adder from directly reversing direction + if (prevDirection == -currentDirection) { + currentDirection = -currentDirection; + } + + // Update previous direction if it differs + if (currentDirection != prevDirection) { + prevDirection = currentDirection; + } + + return true; // Return true to indicate the touch event was handled +} + +void Adder::UpdatePosition() { + unsigned int newHead = adderBody.front() + currentDirection; + Adder::MoveConsequence result = CheckMove(); + + switch (result) { + case Adder::MoveConsequence::DEATH: + ResetGame(); + return; + + case Adder::MoveConsequence::EAT: + adderBody.push_front(newHead); + CreateFood(); + UpdateScore(adderBody.size() - 2); + break; + + case Adder::MoveConsequence::MOVE: + adderBody.pop_back(); + adderBody.push_front(newHead); + break; + } + + field[adderBody.front()] = AdderField::BODY; + field[adderBody.back()] = AdderField::BLANK; +} + +Adder::MoveConsequence Adder::CheckMove() const { + unsigned int newHead = adderBody.front() + currentDirection; + if (newHead >= fieldSize) { + return Adder::MoveConsequence::DEATH; + } + + switch (field[newHead]) { + case AdderField::BLANK: + return Adder::MoveConsequence::MOVE; + case AdderField::FOOD: + return Adder::MoveConsequence::EAT; + default: + return Adder::MoveConsequence::DEATH; + } +} + +void Adder::Refresh() { + if (!appReady) { + FullRedraw(); + CreateFood(); + vTaskDelay(1); // Required to let the OS draw the tile completely + UpdateScore(0); + vTaskDelay(1); // Required to let the OS draw the tile completely + appReady = true; + } else { + UpdatePosition(); + UpdateSingleTile(adderBody.front() % fieldWidth, adderBody.front() / fieldWidth, LV_COLOR_YELLOW); + vTaskDelay(1); // Required to let the OS draw the tile completely + UpdateSingleTile(adderBody.back() % fieldWidth, adderBody.back() / fieldWidth, LV_COLOR_BLACK); + vTaskDelay(1); // Required to let the OS draw the tile completely + } +} + +void Adder::FullRedraw() { + for (unsigned int x = 0; x < fieldWidth; ++x) { + for (unsigned int y = 0; y < fieldHeight; ++y) { + lv_color_t color; + switch (field[y * fieldWidth + x]) { + case AdderField::BODY: + color = LV_COLOR_YELLOW; + break; + case AdderField::SOLID: + color = LV_COLOR_WHITE; + break; + case AdderField::FOOD: + color = LV_COLOR_GREEN; + break; + default: + color = LV_COLOR_BLACK; + break; + } + UpdateSingleTile(x, y, color); + vTaskDelay(1); // Required to let the OS draw the tile completely + } + } +} + +void Adder::UpdateSingleTile(unsigned int x, unsigned int y, lv_color_t color) { + std::fill(tileBuffer, tileBuffer + TileSize * TileSize, color); + lv_area_t area {.x1 = static_cast<lv_coord_t>(x * TileSize + fieldOffsetHorizontal), + .y1 = static_cast<lv_coord_t>(y * TileSize + fieldOffsetVertical), + .x2 = static_cast<lv_coord_t>(x * TileSize + fieldOffsetHorizontal + TileSize - 1), + .y2 = static_cast<lv_coord_t>(y * TileSize + fieldOffsetVertical + TileSize - 1)}; + + lvgl.FlushDisplay(&area, tileBuffer); +} + +void Adder::GameOver() { + unsigned int digits[] = {7, 0, 5, 3}; // Digits forming the "GAME OVER" display + + // Determine offset based on field dimensions + unsigned int offset = fieldOffsetHorizontal > fieldOffsetVertical ? fieldOffsetHorizontal : fieldOffsetVertical; + + // Render "GAME OVER" animation + for (unsigned int r = 3 * offset; r < displayWidth - 4 * offset; r += 16) { + for (unsigned int i = 0; i < 4; i++) { + for (unsigned int j = 0; j < 64; j++) { + // Map font bits into the display buffer + digitBuffer[63 - j] = (DigitFont[digits[i]][j / 8] & 1 << j % 8) + ? LV_COLOR_WHITE + : LV_COLOR_BLACK; // Bitmagic to rotate the Digits to look like Letters + } + + lv_area_t area; + area.x1 = r + 8 * i; + area.y1 = r; + area.x2 = area.x1 + 7; + area.y2 = area.y1 + 7; + + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, digitBuffer); + vTaskDelay(1); // Required to let the OS draw the tile completely + } + } +} + +void Adder::UpdateScore(unsigned int score) { + // Extract individual digits of the score + unsigned int digits[] = {0, score % 10, (score % 100 - score % 10) / 10, (score - score % 100) / 100}; + + // Render the score + for (unsigned int i = 0; i < 4; i++) { + for (unsigned int j = 0; j < 64; j++) { + // Map font bits into the display buffer (using bit manipulation) + digitBuffer[j] = (DigitFont[digits[i]][j / 8] & (1 << (j % 8))) ? LV_COLOR_WHITE : LV_COLOR_BLACK; + } + + lv_area_t area; + area.x1 = displayWidth - 16 - 8 * i; // Adjust X to display digits + area.y1 = 4; // Y-offset for Score + area.x2 = area.x1 + 7; + area.y2 = area.y1 + 7; + + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, digitBuffer); + vTaskDelay(20); // Small delay to allow display refresh + } + + // Update the high score if necessary + unsigned int highScoreToWrite = (highScore > score) ? highScore : score; + unsigned int highScoreDigits[] = {0, + highScoreToWrite % 10, + (highScoreToWrite % 100 - highScoreToWrite % 10) / 10, + (highScoreToWrite - highScoreToWrite % 100) / 100}; + + // Render the high score + for (unsigned int i = 0; i < 4; i++) { + for (unsigned int j = 0; j < 64; j++) { + // Map font bits into the display buffer + digitBuffer[j] = (DigitFont[highScoreDigits[i]][j / 8] & (1 << (j % 8))) ? LV_COLOR_WHITE : LV_COLOR_BLACK; + } + + lv_area_t area; + area.x1 = 40 - 8 * i; // Adjust X to display digits + area.y1 = 4; // Y-offset for High Score + area.x2 = area.x1 + 7; + area.y2 = area.y1 + 7; + + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, digitBuffer); + vTaskDelay(20); // Small delay to allow display refresh + } + + // Save the high score if it has changed + highScore = highScoreToWrite; +} diff --git a/src/displayapp/screens/Adder.h b/src/displayapp/screens/Adder.h new file mode 100644 index 0000000000..d96c6953f7 --- /dev/null +++ b/src/displayapp/screens/Adder.h @@ -0,0 +1,118 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/Controllers.h" +#include "displayapp/screens/Screen.h" +#include <lvgl/lvgl.h> +#include "components/fs/FS.h" +#include <list> +#include <vector> + +namespace Pinetime { + namespace Applications { + namespace Screens { + + // Save file version + constexpr unsigned int AdderVersion = 1; + + struct AdderSave { + unsigned int Level {0}; + unsigned int HighScore {0}; + unsigned int Version {AdderVersion}; + }; + + enum class AdderField { UNDEFINED, BLANK, SOLID, BODY, FOOD }; + + class Adder : public Screen { + + public: + Adder(Pinetime::Components::LittleVgl& lvgl, Pinetime::Controllers::FS& fs); + ~Adder() override; + + enum class MoveConsequence { DEATH, EAT, MOVE }; + + // Overridden functions + void Refresh() override; + bool OnTouchEvent(Pinetime::Applications::TouchEvents event) override; + + private: + static constexpr const char* GameSavePath = "/games/adder/adder.sav"; + static constexpr unsigned int TileSize = 9; + static constexpr unsigned int AdderDelayInterval = 200; + + Pinetime::Components::LittleVgl& lvgl; + Controllers::FS& filesystem; + + AdderSave data; // Game save data + AdderField* field {nullptr}; + + lv_task_t* refreshTask {nullptr}; + lv_color_t* tileBuffer {nullptr}; + lv_color_t digitBuffer[64]; + + unsigned int displayHeight {0}; + unsigned int displayWidth {0}; + unsigned int fieldWidth {0}; + unsigned int fieldHeight {0}; + unsigned int fieldSize {0}; + unsigned int highScore {2}; + + unsigned int fieldOffsetHorizontal {0}; + unsigned int fieldOffsetVertical {0}; + + std::list<unsigned int> adderBody; + std::vector<unsigned int> blanks; + + int prevDirection {0}; + int currentDirection {1}; + + bool appReady {false}; + + // Methods + void InitializeGame(); + void LoadGame(); + void SaveGame(); + void ResetGame(); + + void InitializeBody(); + void CreateFood(); + void CreateLevel(); + + void UpdatePosition(); + void FullRedraw(); + void UpdateSingleTile(unsigned int fieldX, unsigned int fieldY, lv_color_t color); + void UpdateScore(unsigned int score); + void GameOver(); + void CleanUp(); + + MoveConsequence CheckMove() const; + + static constexpr const char DigitFont[10][8] = { + // Font for digits 0-9 + {0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // 0 + {0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // 1 + {0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // 2 + {0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // 3 + {0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // 4 + {0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // 5 + {0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // 6 + {0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // 7 + {0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // 8 + {0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00} // 9 + }; + }; + } // namespace Screens + + // Application Traits + template <> + struct AppTraits<Apps::Adder> { + static constexpr Apps app = Apps::Adder; + static constexpr const char* icon = "S"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Adder(controllers.lvgl, controllers.filesystem); + } + }; + + } // namespace Applications +} // namespace Pinetime