From 10f79e5743aa7b7dd04516f38ba12d626808ebca Mon Sep 17 00:00:00 2001 From: Feksaaargh Date: Wed, 2 Oct 2024 00:31:15 -0500 Subject: [PATCH 01/23] Add dummy WatchFaceMaze --- src/CMakeLists.txt | 1 + src/displayapp/UserApps.h | 1 + src/displayapp/apps/Apps.h.in | 1 + src/displayapp/screens/WatchFaceMaze.cpp | 15 ++++++++++ src/displayapp/screens/WatchFaceMaze.h | 38 ++++++++++++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 src/displayapp/screens/WatchFaceMaze.cpp create mode 100644 src/displayapp/screens/WatchFaceMaze.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0a97a0158b..c9f28850e0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -426,6 +426,7 @@ list(APPEND SOURCE_FILES displayapp/screens/WatchFaceTerminal.cpp displayapp/screens/WatchFacePineTimeStyle.cpp displayapp/screens/WatchFaceCasioStyleG7710.cpp + displayapp/screens/WatchFaceMaze.cpp ## diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 67bbfa7d41..7cdfb620fc 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -14,6 +14,7 @@ #include "displayapp/screens/WatchFaceInfineat.h" #include "displayapp/screens/WatchFacePineTimeStyle.h" #include "displayapp/screens/WatchFaceTerminal.h" +#include "displayapp/screens/WatchFaceMaze.h" namespace Pinetime { namespace Applications { diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index 2104a267c0..61c4286d93 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -52,6 +52,7 @@ namespace Pinetime { Terminal, Infineat, CasioStyleG7710, + Maze, }; template diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp new file mode 100644 index 0000000000..8a91834f93 --- /dev/null +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -0,0 +1,15 @@ +#include "displayapp/screens/WatchFaceMaze.h" + +using namespace Pinetime::Applications::Screens; + +WatchFaceMaze::WatchFaceMaze(Controllers::DateTime& dateTimeController) { + dateTimeController.Day(); + lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(title, "My test watchface"); + lv_label_set_align(title, LV_LABEL_ALIGN_CENTER); + lv_obj_align(title, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); +} + +WatchFaceMaze::~WatchFaceMaze() { + lv_obj_clean(lv_scr_act()); +} \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h new file mode 100644 index 0000000000..512e44140e --- /dev/null +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -0,0 +1,38 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "components/datetime/DateTimeController.h" + +namespace Pinetime { + namespace Applications { + namespace Screens { + class WatchFaceMaze : public Screen { + public: + WatchFaceMaze(Controllers::DateTime&); + ~WatchFaceMaze() override; + }; + } + + template <> + struct WatchFaceTraits { + static constexpr WatchFace watchFace = WatchFace::Maze; + static constexpr const char* name = "Maze"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceMaze(controllers.dateTimeController/*, + controllers.batteryController, + controllers.bleController, + controllers.notificationManager, + controllers.settingsController, + controllers.heartRateController, + controllers.motionController*/); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +} \ No newline at end of file From 3456e0d467d9318e01214246e089401b9c455b57 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:17:58 -0500 Subject: [PATCH 02/23] Add basic maze printing functionality --- src/displayapp/screens/WatchFaceMaze.cpp | 102 +++++++++++++++++++++-- src/displayapp/screens/WatchFaceMaze.h | 47 +++++++++-- 2 files changed, 137 insertions(+), 12 deletions(-) mode change 100644 => 100755 src/displayapp/screens/WatchFaceMaze.cpp mode change 100644 => 100755 src/displayapp/screens/WatchFaceMaze.h diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp old mode 100644 new mode 100755 index 8a91834f93..022cb4ab5d --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -2,14 +2,104 @@ using namespace Pinetime::Applications::Screens; -WatchFaceMaze::WatchFaceMaze(Controllers::DateTime& dateTimeController) { - dateTimeController.Day(); - lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr); - lv_label_set_text_static(title, "My test watchface"); - lv_label_set_align(title, LV_LABEL_ALIGN_CENTER); - lv_obj_align(title, lv_scr_act(), LV_ALIGN_CENTER, 0, 0); + +Maze::Maze() { + std::fill_n(mazemap, FLATSIZE, 0); +} + +uint8_t Maze::get(int x, int y) { + if (x<0||x>WIDTH||y<0||y>HEIGHT) {return 0;} + return get((y * WIDTH) + x); +} +uint8_t Maze::get(int index) { + if (index < 0 || index/2 >= FLATSIZE) {return 0;} + // odd means right (low) nibble, even means left (high) nibble + if (index & 0b1) return mazemap[index/2] & 0b00001111; + else return mazemap[index/2] >> 4; +} + +void Maze::set(int x, int y, uint8_t value) { + if (x<0||x>WIDTH||y<0||y>HEIGHT) {return;} + set(y * WIDTH + x, value); +} +void Maze::set(int index, uint8_t value) { + if (index < 0 || index/2 >= FLATSIZE) {return;} + // odd means right (low) nibble, even means left (high) nibble + if (index & 0b1) mazemap[index/2] = (mazemap[index/2] & 0b11110000) | (value & 0b00001111); + else mazemap[index/2] = (mazemap[index/2] & 0b00001111) | (value << 4); +} + + + + +WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, + Controllers::DateTime& dateTimeController) + : dateTimeController {dateTimeController}, + lvgl {lvgl}, + maze {Maze()} { + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + //Refresh(); } WatchFaceMaze::~WatchFaceMaze() { lv_obj_clean(lv_scr_act()); + lv_task_del(taskRefresh); +} + +void WatchFaceMaze::GenerateMaze() { + for (int i = 0; i < Maze::FLATSIZE*2; i++) { + maze.set(i, rand() & 0b11); + //maze.set(i, 0b11); + } +} + +void WatchFaceMaze::Refresh() { + currentDateTime = dateTimeController.CurrentDateTime(); + if (currentDateTime.IsUpdated()) { + GenerateMaze(); + //ProcessCorners(); + DrawMaze(); + } +} + +void WatchFaceMaze::DrawMaze() { + lv_area_t area; + lv_color_t *curbuf = buf2; + // print horizontal lines + area.x1 = 0; + area.x2 = 239; + for (int y = 0; y < Maze::HEIGHT; y++) { + curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer + for (int x = 0; x < Maze::WIDTH; x++) { + if (maze.get(x, y) & 0b01) {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_WHITE);} + else {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK);} + } + std::copy_n(curbuf, 240, &curbuf[240]); + area.y1 = Maze::TILESIZE * y; + area.y2 = Maze::TILESIZE * y + 1; + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, curbuf); + } + // print vertical lines + area.y1 = 0; + area.y2 = 239; + for (int x = 0; x < Maze::WIDTH; x++) { + curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer + for (int y = 0; y < Maze::HEIGHT; y++) { + uint8_t curblock = maze.get(x,y); + // handle corners: if any of the touching lines are present, add corner. else leave it black + if (curblock & 0b11 || maze.get(x-1,y) & 0b01 | maze.get(x,y-1) & 0b10) + {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_WHITE);} + else {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_BLACK);} + + if (curblock & 0b010) {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_WHITE);} + else {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_BLACK);} + } + area.x1 = Maze::TILESIZE * x; + area.x2 = Maze::TILESIZE * x + 1; + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, curbuf); + + } } \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h old mode 100644 new mode 100755 index 512e44140e..bce14b8931 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -4,30 +4,65 @@ #include "displayapp/screens/Screen.h" #include "displayapp/Controllers.h" #include "components/datetime/DateTimeController.h" +#include "utility/DirtyValue.h" +#include "displayapp/LittleVgl.h" namespace Pinetime { namespace Applications { namespace Screens { + class Maze { + public: + Maze(); + // I guess I don't need an explicit destructor since everything goes out of scope nicely? + uint8_t get(int, int); + uint8_t get(int); + void set(int, int, uint8_t); + void set(int, uint8_t); + + // 10x10 tiles on the maze + static constexpr int WIDTH = 24; + static constexpr int HEIGHT = 24; + static constexpr int TILESIZE = 10; + static constexpr int FLATSIZE = WIDTH*HEIGHT/2; + private: + uint8_t mazemap[FLATSIZE]; + }; + + + class WatchFaceMaze : public Screen { public: - WatchFaceMaze(Controllers::DateTime&); + WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&); ~WatchFaceMaze() override; + void Refresh() override; + + private: + lv_task_t* taskRefresh; + void GenerateMaze(); + void DrawMaze(); + + Utility::DirtyValue> currentDateTime {}; + Controllers::DateTime& dateTimeController; + Pinetime::Components::LittleVgl& lvgl; + lv_color_t buf1[480]; + lv_color_t buf2[480]; + Maze maze; }; } + + template <> struct WatchFaceTraits { static constexpr WatchFace watchFace = WatchFace::Maze; static constexpr const char* name = "Maze"; static Screens::Screen* Create(AppControllers& controllers) { - return new Screens::WatchFaceMaze(controllers.dateTimeController/*, + return new Screens::WatchFaceMaze(controllers.lvgl, + controllers.dateTimeController/*, controllers.batteryController, controllers.bleController, - controllers.notificationManager, - controllers.settingsController, - controllers.heartRateController, - controllers.motionController*/); + controllers.settingsController*/); }; static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { From eea241e6e28b67f23e9f221c8fddc52f15068310 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:16:16 -0500 Subject: [PATCH 03/23] Add maze generation and numbers --- src/displayapp/screens/WatchFaceMaze.cpp | 360 ++++++++++++++++++++--- src/displayapp/screens/WatchFaceMaze.h | 111 ++++++- 2 files changed, 414 insertions(+), 57 deletions(-) mode change 100755 => 100644 src/displayapp/screens/WatchFaceMaze.cpp mode change 100755 => 100644 src/displayapp/screens/WatchFaceMaze.h diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp old mode 100755 new mode 100644 index 022cb4ab5d..12ecfd3a2d --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -1,42 +1,125 @@ +#include #include "displayapp/screens/WatchFaceMaze.h" -using namespace Pinetime::Applications::Screens; +#include "Tile.h" +using namespace Pinetime::Applications::Screens; +// Despite being called Maze, this really is only a relatively simple wrapper for the specialized +// (fake) 2d array on which the maze structure is built. It should only have manipulations for +// the structure, generating and printing should be handled elsewhere. Maze::Maze() { std::fill_n(mazemap, FLATSIZE, 0); } -uint8_t Maze::get(int x, int y) { - if (x<0||x>WIDTH||y<0||y>HEIGHT) {return 0;} +// only returns 4 bits (since that's all that's stored) +// returns 0 in case of out of bounds access +MazeTile Maze::get(int x, int y) { + if (x<0||x>WIDTH||y<0||y>HEIGHT) {return MazeTile(0b0011);} return get((y * WIDTH) + x); } -uint8_t Maze::get(int index) { - if (index < 0 || index/2 >= FLATSIZE) {return 0;} +MazeTile Maze::get(int index) { + if (index < 0 || index/2 >= FLATSIZE) {return MazeTile(0b0011);} // odd means right (low) nibble, even means left (high) nibble - if (index & 0b1) return mazemap[index/2] & 0b00001111; - else return mazemap[index/2] >> 4; + if (index & 0b1) return MazeTile(mazemap[index/2] & 0b00001111); + else return MazeTile(mazemap[index/2] >> 4); } -void Maze::set(int x, int y, uint8_t value) { +// only stores the low 4 bits of the value +// if out of bounds, does nothing +void Maze::set(int x, int y, MazeTile tile) { if (x<0||x>WIDTH||y<0||y>HEIGHT) {return;} - set(y * WIDTH + x, value); + set(y * WIDTH + x, tile); } -void Maze::set(int index, uint8_t value) { +void Maze::set(int index, MazeTile tile) { if (index < 0 || index/2 >= FLATSIZE) {return;} // odd means right (low) nibble, even means left (high) nibble - if (index & 0b1) mazemap[index/2] = (mazemap[index/2] & 0b11110000) | (value & 0b00001111); - else mazemap[index/2] = (mazemap[index/2] & 0b00001111) | (value << 4); + if (index & 0b1) mazemap[index/2] = (mazemap[index/2] & 0b11110000) | tile.map; + else mazemap[index/2] = (mazemap[index/2] & 0b00001111) | tile.map << 4; +} + +// only operates on the low 4 bits of the uint8_t. +// only sets the bits from the value that are also on in the mask, rest are left alone +// e.g. existing = 1010, value = 0001, mask = 0011, then result = 1001 +// (mask defaults to 0xFF which keeps all bits) +void Maze::fill(uint8_t value, uint8_t mask) { + value = value & 0b00001111; + value |= value << 4; + if (mask == 0xFF) { + // did not include a mask + std::fill_n(mazemap, FLATSIZE, value); + } else { + // included a mask + mask = mask & 0b00001111; + mask |= mask << 4; + value = value & mask; // preprocess mask for value + mask = ~mask; // this mask will be applied to the value + for (uint8_t& mapitem : mazemap) { + mapitem = (mapitem & mask) + value; + } + } +} +inline void Maze::fill(MazeTile tile, uint8_t mask) { + fill(tile.map, mask); +} + +// For quickly manipulating. Also allows better abstraction by allowing setting of down and right sides. +// Silently does nothing if given invalid values. +void Maze::setSide(int index, TileAttr attr, bool value) { + switch(attr) { + case up: set(index, get(index).setUp(value)); break; + case down: set(index+WIDTH, get(index+WIDTH).setUp(value)); break; + case left: set(index, get(index).setLeft(value)); break; + case right: set(index+1, get(index+1).setLeft(value)); break; + case flagempty: set(index, get(index).setFlagEmpty(value)); break; + case flaggen: set(index, get(index).setFlagGen(value)); break; + } +} +void Maze::setSide(int x, int y, TileAttr attr, bool value) { + setSide(y*WIDTH+x, attr, value); +} +bool Maze::getSide(int index, TileAttr attr) { + switch(attr) { + case up: return get(index).getUp(); + case down: return get(index+WIDTH).getUp(); + case left: return get(index).getLeft(); + case right: return get(index+1).getLeft(); + case flagempty: return get(index).getFlagEmpty(); + case flaggen: return get(index).getFlagGen(); + } + return false; +} +bool Maze::getSide(int x, int y, TileAttr attr) { + return getSide(y*WIDTH+x, attr); +} + +void Maze::transparentPaste(int x1, int y1, int x2, int y2, const uint8_t toPaste[]) { + int pastewidth = x2-x1+1; + for (int y = 0; y <= y2-y1; y++) { + for (int x = 0; x <= x2-x1; x++) { + const int pasteFlatEquiv = y*pastewidth+x; + const int mazeFlatEquiv = ((y+y1)*(WIDTH/2))+(x+x1); + // high nibble + if (!(toPaste[pasteFlatEquiv] & (MazeTile::FLAGEMPTYMASK << 4))) // only do anything if empty flag is not set + {mazemap[mazeFlatEquiv] = (mazemap[mazeFlatEquiv] & 0b00001111) | (toPaste[pasteFlatEquiv] & 0b11110000);} + // low nibble + if (!(toPaste[pasteFlatEquiv] & MazeTile::FLAGEMPTYMASK)) // only do anything if empty flag is not set + {mazemap[mazeFlatEquiv] = (mazemap[mazeFlatEquiv] & 0b11110000) | (toPaste[pasteFlatEquiv] & 0b00001111);} + } + } } WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, - Controllers::DateTime& dateTimeController) + Controllers::DateTime& dateTimeController, + Controllers::Settings& settingsController) : dateTimeController {dateTimeController}, + settingsController {settingsController}, lvgl {lvgl}, - maze {Maze()} { + maze {Maze()}, + prng {MazeRNG()} { taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); //Refresh(); @@ -47,59 +130,246 @@ WatchFaceMaze::~WatchFaceMaze() { lv_task_del(taskRefresh); } -void WatchFaceMaze::GenerateMaze() { - for (int i = 0; i < Maze::FLATSIZE*2; i++) { - maze.set(i, rand() & 0b11); - //maze.set(i, 0b11); +void WatchFaceMaze::Refresh() { + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + // refresh time if either a minute has passed, or maze is still WIP + if (pausedGeneration || currentDateTime.IsUpdated()) { + // if generation wasn't paused (i.e. doing a ground up maze gen), set everything up + if (!pausedGeneration) { + prng.seed(currentDateTime.Get().time_since_epoch().count()); + InitializeMaze(); + PutNumbers(); // also works as the seed to generate the maze onto + } + GenerateMaze(); + // only draw once maze is fully generated (not paused) + if (!pausedGeneration) { + DrawMaze(); + } } } -void WatchFaceMaze::Refresh() { - currentDateTime = dateTimeController.CurrentDateTime(); - if (currentDateTime.IsUpdated()) { - GenerateMaze(); - //ProcessCorners(); - DrawMaze(); + +void WatchFaceMaze::InitializeMaze() { + // prepare maze by filling it with all walls and empty tiles + maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); +} + +void WatchFaceMaze::PutNumbers() { + uint8_t hours = dateTimeController.Hours(); + uint8_t minutes = dateTimeController.Minutes(); + // TODO 12 hour format support + /* + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + // 12 hour format + // don't have handling, just make a tiny seed and gen a normal maze + maze.setSide(prng.rand(0,Maze::WIDTH), prng.rand(0,Maze::HEIGHT), TileAttr::flagempty, false); + } + */ + //maze.setSide(prng.rand(0,Maze::WIDTH), prng.rand(0,Maze::HEIGHT), TileAttr::flagempty, false); + + // 24 hour format only right now + // top left: hours major digit + maze.transparentPaste(2, 1, 4, 10, numbers[hours/10]); + // top right: hours minor digit + maze.transparentPaste(5, 1, 7, 10, numbers[hours%10]); + // bottom left: minutes major digit + maze.transparentPaste(2, 13, 4, 22, numbers[minutes/10]); + // bottom right: minutes minor digit + maze.transparentPaste(5, 13, 7, 22, numbers[minutes%10]); + // put horizontal line connecting the digits horizontally. working in actual tile coords here + for (int stripLoc : {prng.rand(1,10), prng.rand(13,22)} ) { + for (int y = stripLoc, x = 9; x > 4; x--) { + if (maze.getSide(x, y, TileAttr::flagempty) == false) { + do { + maze.setSide(x, y, TileAttr::flagempty, false); + maze.setSide(x, y, TileAttr::right, false); + x++; + } while (maze.getSide(x, y, TileAttr::flagempty) == true); + break; // get out of the for loop + } + } + } + // now make a vertical stripe in the same way + for (int x = prng.rand(4,15), y = 10; y > 0; y--) { + if (maze.getSide(x, y, TileAttr::flagempty) == false) { + do { + maze.setSide(x, y, TileAttr::flagempty, false); + maze.setSide(x, y, TileAttr::down, false); + y++; + } while (maze.getSide(x, y, TileAttr::flagempty) == true); + break; // get out of the for loop + } + } +} + +void WatchFaceMaze::GenerateMaze() { + int x, y, oldx, oldy; + // task should only run for 3/4 the time it takes for the task to refresh. + // Will go over; only checks once it's finished with current line. It won't go too far over though. + auto maxGenTarget = dateTimeController.CurrentDateTime() + std::chrono::milliseconds((taskRefresh->period*3)/4); + + while (true) { + // FIND POSITION TO START BRANCH FROM + for (uint8_t i = 0; i < 30; i++) { + x = prng.rand(0, Maze::WIDTH-1); + y = prng.rand(0, Maze::HEIGHT-1); + if (maze.getSide(x,y, TileAttr::flagempty)) {break;} // found solution tile + if (i == 29) { + // failed all 30 attempts (this is inside the for loop for 'organization') + // find solution tile slowly but guaranteed + int count = 0; + // count number of valid tiles + for (int j = 0; j < Maze::WIDTH*Maze::HEIGHT; j++) + {if (maze.getSide(j, TileAttr::flagempty)) {count++;}} + if (count == 0) { + // all tiles filled; maze gen done + pausedGeneration = false; + return; + } + // if maze gen not done, select random index from valid tiles to start from + //count = prng.rand(0, count); + //printf("bruteforce chosen count: %i", count); + for (int j = 0; j < Maze::WIDTH*Maze::HEIGHT; j++) { + if (maze.getSide(j, TileAttr::flagempty)) {count--;} + if (count == 0) { + y = j / Maze::WIDTH; + x = j % Maze::WIDTH; + break; + } + } + } + } + // function now has a valid position a maze line can start from in x and y + oldx = -1, oldy = -1; + // GENERATE A SINGLE PATH + uint8_t direction; // which direction the cursor moved in + while (true) { + maze.setSide(x, y, TileAttr::flagempty, false); // no longer empty + maze.setSide(x, y, TileAttr::flaggen, true); // in generation + oldx = x, oldy = y; + // move to next tile + // this is very scuffed, but it prevents backtracking. + while (true) { + // effectively turn tbe cursor so it will never go straight back + switch (direction = prng.rand(0,3)) { + case 0: // moved up + if (y <= 0 || !maze.getSide(x,y,TileAttr::up)) {continue;} + y -= 1; break; + case 1: // moved left + if (x <= 0 || !maze.getSide(x,y,TileAttr::left)) {continue;} + x -= 1; break; + case 2: // moved down + if (y >= Maze::HEIGHT-1 || !maze.getSide(x,y,TileAttr::down)) {continue;} + y += 1; break; + case 3: // moved right + if (x >= Maze::WIDTH-1 || !maze.getSide(x,y,TileAttr::right)) {continue;} + x += 1; break; + } + break; + } + // moved to next tile. check if looped in on self + if (!maze.getSide(x, y, TileAttr::flaggen)) { + // did NOT loop in on self, simply remove wall and move on + switch (direction) { + case 0: // moved up + maze.setSide(x,y,TileAttr::down, false); break; + case 1: // moved left + maze.setSide(x,y,TileAttr::right, false); break; + case 2: // moved down + maze.setSide(x,y,TileAttr::up, false); break; + case 3: // moved right + maze.setSide(x,y,TileAttr::left, false); break; + } + // if attached to main maze, path finished generating + if (!maze.getSide(x, y, TileAttr::flagempty)) {break;} + } else { + // DID loop in on self, track down and eliminate loop + // targets are the coordinates of where it needs to backtrack to + int targetx = x, targety = y; + x = oldx, y = oldy; + while (x != targetx || y != targety) { + if (y > 0 && (maze.getSide(x, y, TileAttr::up) == false)) { // backtrack up + maze.setSide(x, y, TileAttr::up, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + y -= 1; + } else if (x > 0 && (maze.getSide(x, y, TileAttr::left) == false)) { // backtrack left + maze.setSide(x, y, TileAttr::left, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + x -= 1; + } else if (y < Maze::HEIGHT-1 && (maze.getSide(x, y, TileAttr::down) == false)) { // backtrack down + maze.setSide(x, y, TileAttr::down, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + y += 1; + } else if (x < Maze::WIDTH && (maze.getSide(x, y, TileAttr::right) == false)) { // backtrack right + maze.setSide(x, y, TileAttr::right, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + x += 1; + } + } + } + } + // mark all tiles as finalized and not in generation by removing ALL flaggen's + maze.fill(0, MazeTile::FLAGGENMASK); + if (dateTimeController.CurrentDateTime() > maxGenTarget) { + pausedGeneration = true; + return; + } } + // execution never gets here! it returns earlier in the function. } void WatchFaceMaze::DrawMaze() { + // this used to be nice code, but it was retrofitted to print offset by 1 pixel for a fancy border. + // I'm not proud of the logic but it works. lv_area_t area; - lv_color_t *curbuf = buf2; + lv_color_t *curbuf = buf1; // print horizontal lines - area.x1 = 0; - area.x2 = 239; - for (int y = 0; y < Maze::HEIGHT; y++) { - curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer + area.x1 = 1; + area.x2 = 238; + for (int y = 1; y < Maze::HEIGHT; y++) { for (int x = 0; x < Maze::WIDTH; x++) { - if (maze.get(x, y) & 0b01) {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_WHITE);} - else {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK);} + if (maze.get(x, y).getUp()) {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_WHITE);} + else {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK);} } - std::copy_n(curbuf, 240, &curbuf[240]); - area.y1 = Maze::TILESIZE * y; - area.y2 = Maze::TILESIZE * y + 1; + std::copy_n(curbuf, 238, &curbuf[238]); + area.y1 = Maze::TILESIZE * y - 1; + area.y2 = Maze::TILESIZE * y; lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, curbuf); + curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer } // print vertical lines - area.y1 = 0; - area.y2 = 239; - for (int x = 0; x < Maze::WIDTH; x++) { - curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer + area.y1 = 1; + area.y2 = 238; + for (int x = 1; x < Maze::WIDTH; x++) { for (int y = 0; y < Maze::HEIGHT; y++) { - uint8_t curblock = maze.get(x,y); + MazeTile curblock = maze.get(x,y); // handle corners: if any of the touching lines are present, add corner. else leave it black - if (curblock & 0b11 || maze.get(x-1,y) & 0b01 | maze.get(x,y-1) & 0b10) + if (curblock.getUp() || curblock.getLeft() || maze.get(x-1,y).getUp() || maze.get(x,y-1).getLeft()) {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_WHITE);} else {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_BLACK);} - if (curblock & 0b010) {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_WHITE);} - else {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_BLACK);} + if (curblock.getLeft()) {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_WHITE);} + else {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_BLACK);} } - area.x1 = Maze::TILESIZE * x; - area.x2 = Maze::TILESIZE * x + 1; + area.x1 = Maze::TILESIZE * x - 1; + area.x2 = Maze::TILESIZE * x; + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, &curbuf[4]); + curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer + } + // print borders + std::fill_n(curbuf, 240, LV_COLOR_GRAY); + for (int i = 0; i < 4; i++) { + if (i==0) {area.x1=0; area.x2=239; area.y1=0; area.y2=0; } // top + else if (i==1) {area.x1=0; area.x2=239; area.y1=239; area.y2=239;} // bottom + else if (i==2) {area.x1=0; area.x2=0; area.y1=0; area.y2=239;} // left + else if (i==3) {area.x1=239; area.x2=239; area.y1=0; area.y2=239;} // right lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, curbuf); - } } \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h old mode 100755 new mode 100644 index bce14b8931..87a9a742c8 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -10,19 +10,89 @@ namespace Pinetime { namespace Applications { namespace Screens { + + + // really just an abstraction of a uint8_t but with functions to get the individual bits + // reading up on bitfields and their inconsistencies scared me away from them... + struct MazeTile { + static constexpr uint8_t UPMASK = 0b0001; + static constexpr uint8_t LEFTMASK = 0b0010; + static constexpr uint8_t FLAGEMPTYMASK = 0b0100; + static constexpr uint8_t FLAGGENMASK = 0b1000; + uint8_t map = 0; + // set flags on tiles in the maze. returns the object so they can be chained. + MazeTile setUp(bool value) {map = (map&~UPMASK) | (value * UPMASK); return *this;} + MazeTile setLeft(bool value) {map = (map&~LEFTMASK) | (value * LEFTMASK); return *this;} + MazeTile setFlagEmpty(bool value) {map = (map&~FLAGEMPTYMASK) | (value * FLAGEMPTYMASK); return *this;} + MazeTile setFlagGen(bool value) {map = (map&~FLAGGENMASK) | (value * FLAGGENMASK); return *this;} + // get flags on tiles in the maze + bool getUp() {return map&UPMASK;} + bool getLeft() {return map&LEFTMASK;} + bool getFlagEmpty() {return map& FLAGEMPTYMASK;} + bool getFlagGen() {return map& FLAGGENMASK;} + }; + + + + + // custom PRNG for the maze to allow it to be deterministic for any given minute + class MazeRNG { + public: + MazeRNG(uint64_t start_seed = 64) {seed(start_seed);} + // reseed the generator. handles any input well. if it's 0, acts as though it was seeded with 1 + void seed(uint64_t seed) {state = seed ? seed : 1; rand();} + // rng lifted straight from https://en.wikipedia.org/wiki/Xorshift#xorshift* (asterisk is part of the link) + uint32_t rand() { + state ^= state >> 12; + state ^= state << 25; + state ^= state >> 27; + // wikipedia had the multiplication in the return. idk what it's doing exactly so i won't change that. + return state * 0x2545F4914F6CDD1DULL >> 32; + } + // random in range, inclusive on both ends (don't make max=min); return rand()%(max-min+1)+min;} + private: + uint64_t state; + }; + + + + + // little bit of convenience + enum TileAttr {up, down, left, right, flagempty, flaggen}; + + // could also be called Field or something. Does not handle stuff like generation or printing, + // ONLY handles interacting with the board. class Maze { public: Maze(); // I guess I don't need an explicit destructor since everything goes out of scope nicely? - uint8_t get(int, int); - uint8_t get(int); - void set(int, int, uint8_t); - void set(int, uint8_t); + // get and set can work with either xy or indexes + MazeTile get(int x, int y); + MazeTile get(int index); + void set(int x, int y, MazeTile tile); + void set(int index, MazeTile tile); + // fill fills all tiles in the maze with a given value, optionally with a mask on what bits to mask + // (use the MazeTile::*MASK-s) + void fill(MazeTile tile, uint8_t mask = 0xFF); + void fill(uint8_t value, uint8_t mask = 0xFF); + // allows abstractly setting a given side on a tile. supports down and right for convenience. + void setSide(int x, int y, TileAttr attr, bool value); + void setSide(int index, TileAttr attr, bool value); + // same as setSide, just getting. + bool getSide(int x, int y, TileAttr attr); + bool getSide(int index, TileAttr attr); + // paste onto board with transparency: if empty flag is set, ignore the tile + // toPaste is a 1d array of uint8_t, reflecting internal maze structure (so packed values) + // always places values left to right, top to bottom. coords are inclusive + // IS A NAIVE PASTE, X COORDS MUST BE COMPRESSED 2X IN WIDTH AND CONTENTS MUST BE PADDED FOR OFFSET PASTING + void transparentPaste(int x1, int y1, int x2, int y2, const uint8_t toPaste[]); - // 10x10 tiles on the maze + // 10x10 px tiles on the maze = 24x24 (on 240px screen) static constexpr int WIDTH = 24; static constexpr int HEIGHT = 24; static constexpr int TILESIZE = 10; + // the actual size of the entire map. only store 4 bits per block, so 2 blocks per byte static constexpr int FLATSIZE = WIDTH*HEIGHT/2; private: uint8_t mazemap[FLATSIZE]; @@ -30,28 +100,45 @@ namespace Pinetime { + class WatchFaceMaze : public Screen { public: - WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&); + WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&, Controllers::Settings&); ~WatchFaceMaze() override; - void Refresh() override; + void Refresh() override; private: lv_task_t* taskRefresh; void GenerateMaze(); + void InitializeMaze(); + void PutNumbers(); void DrawMaze(); - Utility::DirtyValue> currentDateTime {}; + Utility::DirtyValue> currentDateTime {}; Controllers::DateTime& dateTimeController; + Controllers::Settings& settingsController; Pinetime::Components::LittleVgl& lvgl; lv_color_t buf1[480]; lv_color_t buf2[480]; Maze maze; + MazeRNG prng; + bool pausedGeneration = false; // if generation was taking too much time and had to pause it to let other tasks run + uint8_t numbers[10][30] = {{0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, + {0x73,0x11,0x77,0x30,0x00,0x77,0x20,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x31,0x00,0x11,0x20,0x00,0x00}, + {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x77,0x73,0x00,0x77,0x30,0x00,0x73,0x00,0x07,0x30,0x00,0x77,0x20,0x00,0x11,0x20,0x00,0x00}, + {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x77,0x77,0x20,0x77,0x31,0x00,0x77,0x20,0x00,0x77,0x77,0x20,0x31,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, + {0x31,0x77,0x31,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x20,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20}, + {0x31,0x11,0x11,0x20,0x00,0x00,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x11,0x17,0x20,0x00,0x01,0x77,0x77,0x20,0x77,0x77,0x20,0x31,0x11,0x00,0x20,0x00,0x07}, + {0x73,0x11,0x17,0x30,0x00,0x07,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x11,0x17,0x20,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, + {0x31,0x11,0x11,0x20,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x73,0x00,0x77,0x72,0x07,0x77,0x72,0x07,0x77,0x30,0x07,0x77,0x20,0x77,0x77,0x20,0x77}, + {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x73,0x00,0x20,0x10,0x07,0x72,0x00,0x01,0x30,0x07,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, + {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x73,0x11,0x00,0x72,0x00,0x07}}; }; } + template <> struct WatchFaceTraits { static constexpr WatchFace watchFace = WatchFace::Maze; @@ -59,10 +146,10 @@ namespace Pinetime { static Screens::Screen* Create(AppControllers& controllers) { return new Screens::WatchFaceMaze(controllers.lvgl, - controllers.dateTimeController/*, - controllers.batteryController, - controllers.bleController, - controllers.settingsController*/); + controllers.dateTimeController, + controllers.settingsController/*, + controllers.batteryController, + controllers.bleController*/); }; static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { From 7409bf9020dd18d6f0e68080634a5ee3a26c9f82 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:19:45 -0500 Subject: [PATCH 04/23] Show loss on long tap --- src/displayapp/screens/WatchFaceMaze.cpp | 35 ++++++++++++++++++++++-- src/displayapp/screens/WatchFaceMaze.h | 5 ++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 12ecfd3a2d..246d8b1093 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -131,9 +131,11 @@ WatchFaceMaze::~WatchFaceMaze() { } void WatchFaceMaze::Refresh() { - currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); - // refresh time if either a minute has passed, or maze is still WIP - if (pausedGeneration || currentDateTime.IsUpdated()) { + auto time = dateTimeController.CurrentDateTime(); + currentDateTime = std::chrono::time_point_cast(time); + // refresh time if either a minute has passed, or loss timer expired. + // if minute passes while lossrefresh is required, ignore it. loss refresh timer will handle it. + if (pausedGeneration || (!lossRefreshRequired && currentDateTime.IsUpdated()) || (lossRefreshRequired && time > lossTargetRefreshTime)) { // if generation wasn't paused (i.e. doing a ground up maze gen), set everything up if (!pausedGeneration) { prng.seed(currentDateTime.Get().time_since_epoch().count()); @@ -144,10 +146,33 @@ void WatchFaceMaze::Refresh() { // only draw once maze is fully generated (not paused) if (!pausedGeneration) { DrawMaze(); + lossRefreshRequired = false; } } } +bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { + switch (event) { + case Pinetime::Applications::TouchEvents::LongTap: + // if generation is paused, let it continue working on that. This should really never trigger. + if (pausedGeneration) {return false;} + // don't _reeeally_ need to reset prng seed... + InitializeMaze(); + PutLoss(); // also works as the seed to generate the maze onto + GenerateMaze(); + // if GenerateMaze() here somehow timed out, the main Refresh() function will handle finishing it. just don't draw it here + if (!pausedGeneration) { + DrawMaze(); + } + // display loss for 2 seconds then refresh screen again + lossTargetRefreshTime = dateTimeController.CurrentDateTime() + std::chrono::seconds(2); + lossRefreshRequired = true; + return true; + default: + return false; + } +} + void WatchFaceMaze::InitializeMaze() { // prepare maze by filling it with all walls and empty tiles @@ -202,6 +227,10 @@ void WatchFaceMaze::PutNumbers() { } } +void WatchFaceMaze::PutLoss() { + maze.transparentPaste(0, 0, 11, 23, loss); +} + void WatchFaceMaze::GenerateMaze() { int x, y, oldx, oldy; // task should only run for 3/4 the time it takes for the task to refresh. diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 87a9a742c8..7521a00638 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -106,12 +106,14 @@ namespace Pinetime { WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&, Controllers::Settings&); ~WatchFaceMaze() override; void Refresh() override; + bool OnTouchEvent(TouchEvents event) override; private: lv_task_t* taskRefresh; void GenerateMaze(); void InitializeMaze(); void PutNumbers(); + void PutLoss(); void DrawMaze(); Utility::DirtyValue> currentDateTime {}; @@ -122,6 +124,8 @@ namespace Pinetime { lv_color_t buf2[480]; Maze maze; MazeRNG prng; + std::chrono::time_point lossTargetRefreshTime {}; + bool lossRefreshRequired = false; bool pausedGeneration = false; // if generation was taking too much time and had to pause it to let other tasks run uint8_t numbers[10][30] = {{0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, {0x73,0x11,0x77,0x30,0x00,0x77,0x20,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x31,0x00,0x11,0x20,0x00,0x00}, @@ -133,6 +137,7 @@ namespace Pinetime { {0x31,0x11,0x11,0x20,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x73,0x00,0x77,0x72,0x07,0x77,0x72,0x07,0x77,0x30,0x07,0x77,0x20,0x77,0x77,0x20,0x77}, {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x73,0x00,0x20,0x10,0x07,0x72,0x00,0x01,0x30,0x07,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x73,0x11,0x00,0x72,0x00,0x07}}; + uint8_t loss[288] = {0x77,0x77,0x77,0x77,0x77,0x73,0x17,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x72,0x07,0x77,0x77,0x77,0x77,0x77,0x77,0x31,0x77,0x77,0x77,0x72,0x07,0x73,0x17,0x77,0x77,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x31,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x31,0x00,0x11,0x11,0x11,0x10,0x01,0x10,0x01,0x11,0x00,0x11,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x01,0x11,0x11,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x00,0x00,0x00,0x77,0x77,0x77,0x77,0x77,0x77,0x72,0x07,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x72,0x07,0x77,0x77,0x77,0x77,0x77}; }; } From 4f69fd6010640b7d96b477ae83c9c60eee4316ec Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:02:06 -0500 Subject: [PATCH 05/23] Add new secrets --- src/displayapp/screens/WatchFaceMaze.cpp | 287 ++++++++++++++++------- src/displayapp/screens/WatchFaceMaze.h | 73 ++++-- 2 files changed, 251 insertions(+), 109 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 246d8b1093..ea2c5aa0eb 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -93,18 +93,26 @@ bool Maze::getSide(int x, int y, TileAttr attr) { return getSide(y*WIDTH+x, attr); } -void Maze::transparentPaste(int x1, int y1, int x2, int y2, const uint8_t toPaste[]) { - int pastewidth = x2-x1+1; - for (int y = 0; y <= y2-y1; y++) { - for (int x = 0; x <= x2-x1; x++) { - const int pasteFlatEquiv = y*pastewidth+x; - const int mazeFlatEquiv = ((y+y1)*(WIDTH/2))+(x+x1); - // high nibble - if (!(toPaste[pasteFlatEquiv] & (MazeTile::FLAGEMPTYMASK << 4))) // only do anything if empty flag is not set - {mazemap[mazeFlatEquiv] = (mazemap[mazeFlatEquiv] & 0b00001111) | (toPaste[pasteFlatEquiv] & 0b11110000);} - // low nibble - if (!(toPaste[pasteFlatEquiv] & MazeTile::FLAGEMPTYMASK)) // only do anything if empty flag is not set - {mazemap[mazeFlatEquiv] = (mazemap[mazeFlatEquiv] & 0b11110000) | (toPaste[pasteFlatEquiv] & 0b00001111);} +void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]) { + // Assumes a maze with empty flags all true, and all walls present + uint16_t flatcoord = 0; // the position in the array (inside the byte, so index 1 would be mask 0b00110000 in the first byte) + for (int y = y1; y <= y2; y++) { + for (int x = x1; x <= x2; x++) { + // get target wall out of the paste buffer into the low two bits of a byte + uint8_t working = (toPaste[flatcoord/4] & (0b11 << ((3-(flatcoord%4))*2))) >> ((3-(flatcoord%4))*2); + // handle left wall + if (!(working & 0b10)) { + setSide(x, y, TileAttr::left, false); + setSide(x, y, TileAttr::flagempty, false); + if (x > 0) setSide(x-1, y, TileAttr::flagempty, false); + } + // handle up wall + if (!(working & 0b01)) { + setSide(x, y, TileAttr::up, false); + setSide(x, y, TileAttr::flagempty, false); + if (y > 0) setSide(x, y-1, TileAttr::flagempty, false); + } + flatcoord++; } } } @@ -114,9 +122,11 @@ void Maze::transparentPaste(int x1, int y1, int x2, int y2, const uint8_t toPast WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, Controllers::DateTime& dateTimeController, - Controllers::Settings& settingsController) + Controllers::Settings& settingsController, + Controllers::MotorController& motor) : dateTimeController {dateTimeController}, settingsController {settingsController}, + motor {motor}, lvgl {lvgl}, maze {Maze()}, prng {MazeRNG()} { @@ -131,104 +141,210 @@ WatchFaceMaze::~WatchFaceMaze() { } void WatchFaceMaze::Refresh() { - auto time = dateTimeController.CurrentDateTime(); - currentDateTime = std::chrono::time_point_cast(time); - // refresh time if either a minute has passed, or loss timer expired. - // if minute passes while lossrefresh is required, ignore it. loss refresh timer will handle it. - if (pausedGeneration || (!lossRefreshRequired && currentDateTime.IsUpdated()) || (lossRefreshRequired && time > lossTargetRefreshTime)) { + realTime = dateTimeController.CurrentDateTime(); + currentDateTime = std::chrono::time_point_cast(realTime); + // refresh time if either a minute has passed, or screen refresh timer expired. + // if minute rolls over while screenrefresh is required, ignore it. the refresh timer will handle it. + if (pausedGeneration || // if generation paused, need to complete it + (currentState == Displaying::watchface && !screenRefreshRequired && currentDateTime.IsUpdated()) || // already on watchface, not waiting for a screen refresh, and time updated + (screenRefreshRequired && realTime > screenRefreshTargetTime)) { // waiting on a refresh // if generation wasn't paused (i.e. doing a ground up maze gen), set everything up if (!pausedGeneration) { - prng.seed(currentDateTime.Get().time_since_epoch().count()); - InitializeMaze(); - PutNumbers(); // also works as the seed to generate the maze onto + // only reseed PRNG if got here by the minute rolling over + if (!screenRefreshRequired) prng.seed(currentDateTime.Get().time_since_epoch().count()); + // prepare maze by filling it with all walls and empty tiles + maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); + // seed the maze with whatever is required at the moment + if (currentState == Displaying::watchface) {PutNumbers();} + else if (currentState == Displaying::blank) { + // seed maze with 4 tiles + uint8_t seed[1] = {0xD5}; + int randx = prng.rand(0,20); + int randy = prng.rand(3,20); + maze.pasteMazeSeed(randx, randy, randx + 3, randy, seed); + } + else if (currentState == Displaying::loss) { + maze.pasteMazeSeed(2, 2, 22, 21, loss);} + else if (currentState == Displaying::amogus) { + maze.pasteMazeSeed(3, 0, 21, 23, amogus);} + else if (currentState == Displaying::autismcreature) { + maze.pasteMazeSeed(0, 2, 11, 22, autismcreature);} + else if (currentState == Displaying::foxgame) { + maze.pasteMazeSeed(0, 1, 23, 22, foxgame);} + else if (currentState == Displaying::reminder) { + maze.pasteMazeSeed(0, 3, 23, 19, reminder);} + else if (currentState == Displaying::pinetime) { + maze.pasteMazeSeed(2, 0, 21, 23, pinetime);} } GenerateMaze(); // only draw once maze is fully generated (not paused) if (!pausedGeneration) { + ForceValidMaze(); DrawMaze(); - lossRefreshRequired = false; + screenRefreshRequired = false; } } } +// allow pushing the button to go back to the main watchface +bool WatchFaceMaze::OnButtonPushed() { + if (currentState != Displaying::watchface) { + screenRefreshRequired = true; + currentState = Displaying::watchface; + return true; + } + return false; +} + bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { - switch (event) { - case Pinetime::Applications::TouchEvents::LongTap: - // if generation is paused, let it continue working on that. This should really never trigger. - if (pausedGeneration) {return false;} - // don't _reeeally_ need to reset prng seed... - InitializeMaze(); - PutLoss(); // also works as the seed to generate the maze onto - GenerateMaze(); - // if GenerateMaze() here somehow timed out, the main Refresh() function will handle finishing it. just don't draw it here - if (!pausedGeneration) { - DrawMaze(); + // if generation is paused, let it continue working on that. This should really never trigger. + if (pausedGeneration) {return false;} + auto time = realTime; + //std::chrono::time_point time {}; + if (event == Pinetime::Applications::TouchEvents::LongTap) { + // LONG TAPPED + if (currentState == Displaying::watchface) { + // on watchface; either refresh maze or go to blank state + if (lastInputTime + std::chrono::milliseconds(2500) > time) { + // long tapped twice in sequence; switch to blank maze + currentState = Displaying::blank; + screenRefreshRequired = true; + std::fill_n(currentCode, sizeof(currentCode), 255); // clear current code in preparation for code entry + } else { + // long tapped not in main watchface; go back to previous state + screenRefreshRequired = true; } - // display loss for 2 seconds then refresh screen again - lossTargetRefreshTime = dateTimeController.CurrentDateTime() + std::chrono::seconds(2); - lossRefreshRequired = true; + lastInputTime = time; + motor.RunForDuration(20); + return true; + } else { + screenRefreshRequired = true; + currentState = Displaying::watchface; + // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast + lastInputTime = std::chrono::time_point(); + motor.RunForDuration(20); return true; - default: - return false; + } + } + // handle swipes to input code on blank screen + if (currentState != Displaying::watchface) { + uint8_t swipeDir = 255; + switch(event) { + case Pinetime::Applications::TouchEvents::SwipeUp: swipeDir = 0; break; + case Pinetime::Applications::TouchEvents::SwipeRight: swipeDir = 1; break; + case Pinetime::Applications::TouchEvents::SwipeDown: swipeDir = 2; break; + case Pinetime::Applications::TouchEvents::SwipeLeft: swipeDir = 3; break; + default: return false; // only handle swipe events + } + for (int i = sizeof(currentCode)-1; i > 0; i--) {currentCode[i] = currentCode[i-1];} + currentCode[0] = swipeDir; + // check if valid code has been entered on non-main screen + // structure also has the effect that if code gets entered while on the destination page, it doesn't refresh. + Displaying newState = currentState; + if (std::memcmp(currentCode, lossCode, sizeof(lossCode)) == 0) {newState = Displaying::loss;} // loss + else if (std::memcmp(currentCode, amogusCode, sizeof(amogusCode)) == 0) {newState = Displaying::amogus;} // amogus + else if (std::memcmp(currentCode, autismCode, sizeof(autismCode)) == 0) {newState = Displaying::autismcreature;} // autismcreature/tbh + else if (std::memcmp(currentCode, foxCode, sizeof(foxCode)) == 0) {newState = Displaying::foxgame;} // foxxo game + else if (std::memcmp(currentCode, reminderCode, sizeof(reminderCode)) == 0) {newState = Displaying::reminder;} // reminder + else if (std::memcmp(currentCode, pinetimeCode, sizeof(pinetimeCode)) == 0) {newState = Displaying::pinetime;} // pinetime logo + if (newState != currentState) { + currentState = newState; + screenRefreshRequired = true; + motor.RunForDuration(10); + } + return true; } + return false; } -void WatchFaceMaze::InitializeMaze() { - // prepare maze by filling it with all walls and empty tiles - maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); -} - void WatchFaceMaze::PutNumbers() { uint8_t hours = dateTimeController.Hours(); uint8_t minutes = dateTimeController.Minutes(); - // TODO 12 hour format support - /* + // modify hours to account for 12 hour format if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { - // 12 hour format - // don't have handling, just make a tiny seed and gen a normal maze - maze.setSide(prng.rand(0,Maze::WIDTH), prng.rand(0,Maze::HEIGHT), TileAttr::flagempty, false); + if (hours == 0) hours = 12; + if (hours > 12) { + maze.pasteMazeSeed(18, 15, 23, 22, pm); + hours -= 12; + } else { + maze.pasteMazeSeed(18, 15, 23, 22, am); + } } - */ - //maze.setSide(prng.rand(0,Maze::WIDTH), prng.rand(0,Maze::HEIGHT), TileAttr::flagempty, false); - - // 24 hour format only right now + // put numbers on screen // top left: hours major digit - maze.transparentPaste(2, 1, 4, 10, numbers[hours/10]); + maze.pasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); // top right: hours minor digit - maze.transparentPaste(5, 1, 7, 10, numbers[hours%10]); + maze.pasteMazeSeed(10, 1, 15, 10, numbers[hours % 10]); // bottom left: minutes major digit - maze.transparentPaste(2, 13, 4, 22, numbers[minutes/10]); + maze.pasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); // bottom right: minutes minor digit - maze.transparentPaste(5, 13, 7, 22, numbers[minutes%10]); - // put horizontal line connecting the digits horizontally. working in actual tile coords here - for (int stripLoc : {prng.rand(1,10), prng.rand(13,22)} ) { - for (int y = stripLoc, x = 9; x > 4; x--) { - if (maze.getSide(x, y, TileAttr::flagempty) == false) { - do { - maze.setSide(x, y, TileAttr::flagempty, false); - maze.setSide(x, y, TileAttr::right, false); - x++; - } while (maze.getSide(x, y, TileAttr::flagempty) == true); - break; // get out of the for loop + maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); +} + +// goes through the maze, finds disconnected segments and connects them +void WatchFaceMaze::ForceValidMaze() { + // flood fill + // this function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in this program) + // initialize x and y to bottom right + int x = Maze::WIDTH-1, y = Maze::HEIGHT - 1; + while (true) { + ForceValidMazeLoop: + maze.setSide(x, y, TileAttr::flaggen, true); + // move cursor + if (y > 0 && !maze.getSide(x, y, TileAttr::up) && !maze.getSide(x, y-1, TileAttr::flaggen)) {y--;} + else if (x < Maze::WIDTH-1 && !maze.getSide(x, y, TileAttr::right) && !maze.getSide(x+1, y, TileAttr::flaggen)) {x++;} + else if (y < Maze::HEIGHT-1 && !maze.getSide(x, y, TileAttr::down) && !maze.getSide(x, y+1, TileAttr::flaggen)) {y++;} + else if (x > 0 && !maze.getSide(x, y, TileAttr::left) && !maze.getSide(x-1, y, TileAttr::flaggen)) {x--;} + else { + int pokeLocationCount = 0; + // couldn't find any position to move to, need to set cursor to a different usable location + for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { + for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { + bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + // if tile to the left is of a different traversal state (is traversed boundary) + if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { + // if found boundary AND can get to it, just continue working from here + if (maze.getSide(proposedx, proposedy, TileAttr::left) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} + pokeLocationCount++; + } + // if tile to up is of a different traversal state (is traversed boundary) + if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { + // if found boundary AND can get to it, just continue working from here + if (maze.getSide(proposedx, proposedy, TileAttr::up) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} + pokeLocationCount++; + } + } + } + // if no poke locations found, maze is finished + if (pokeLocationCount == 0) {return;} + // if execution gets here, then no valid position to start filling from was found. need to poke a hole. + // choose a random poke location to poke a hole through. pokeLocationCount is now used as an index + pokeLocationCount = prng.rand(1, pokeLocationCount); + for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { + for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { + bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { + pokeLocationCount--; + if (pokeLocationCount == 0) { + maze.setSide(proposedx, proposedy, TileAttr::left, false); + x = proposedx, y = proposedy; + continue; + } + } + // if tile to up is of a different traversal state (is traversed boundary) + if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { + pokeLocationCount--; + if (pokeLocationCount == 0) { + maze.setSide(proposedx, proposedy, TileAttr::up, false); + x = proposedx, y = proposedy; + continue; + } + } + } } } } - // now make a vertical stripe in the same way - for (int x = prng.rand(4,15), y = 10; y > 0; y--) { - if (maze.getSide(x, y, TileAttr::flagempty) == false) { - do { - maze.setSide(x, y, TileAttr::flagempty, false); - maze.setSide(x, y, TileAttr::down, false); - y++; - } while (maze.getSide(x, y, TileAttr::flagempty) == true); - break; // get out of the for loop - } - } -} - -void WatchFaceMaze::PutLoss() { - maze.transparentPaste(0, 0, 11, 23, loss); } void WatchFaceMaze::GenerateMaze() { @@ -236,7 +352,6 @@ void WatchFaceMaze::GenerateMaze() { // task should only run for 3/4 the time it takes for the task to refresh. // Will go over; only checks once it's finished with current line. It won't go too far over though. auto maxGenTarget = dateTimeController.CurrentDateTime() + std::chrono::milliseconds((taskRefresh->period*3)/4); - while (true) { // FIND POSITION TO START BRANCH FROM for (uint8_t i = 0; i < 30; i++) { @@ -256,8 +371,8 @@ void WatchFaceMaze::GenerateMaze() { return; } // if maze gen not done, select random index from valid tiles to start from - //count = prng.rand(0, count); - //printf("bruteforce chosen count: %i", count); + // 'count' is now used as an index + count = prng.rand(1, count); for (int j = 0; j < Maze::WIDTH*Maze::HEIGHT; j++) { if (maze.getSide(j, TileAttr::flagempty)) {count--;} if (count == 0) { @@ -279,7 +394,6 @@ void WatchFaceMaze::GenerateMaze() { // move to next tile // this is very scuffed, but it prevents backtracking. while (true) { - // effectively turn tbe cursor so it will never go straight back switch (direction = prng.rand(0,3)) { case 0: // moved up if (y <= 0 || !maze.getSide(x,y,TileAttr::up)) {continue;} @@ -337,6 +451,9 @@ void WatchFaceMaze::GenerateMaze() { maze.setSide(x, y, TileAttr::flaggen, false); maze.setSide(x, y, TileAttr::flagempty, true); x += 1; + } else { + // bad backtrack; die + std::abort(); } } } diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 7521a00638..3b2ee2fe90 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -82,11 +82,10 @@ namespace Pinetime { // same as setSide, just getting. bool getSide(int x, int y, TileAttr attr); bool getSide(int index, TileAttr attr); - // paste onto board with transparency: if empty flag is set, ignore the tile - // toPaste is a 1d array of uint8_t, reflecting internal maze structure (so packed values) - // always places values left to right, top to bottom. coords are inclusive - // IS A NAIVE PASTE, X COORDS MUST BE COMPRESSED 2X IN WIDTH AND CONTENTS MUST BE PADDED FOR OFFSET PASTING - void transparentPaste(int x1, int y1, int x2, int y2, const uint8_t toPaste[]); + // paste onto an empty board. Marks a tile as not empty if any neighboring walls are gone. + // toPaste is a 1d array of uint8_t, only containing the two wall bits (left then up). So that's 4 walls in a byte. + // always places values left to right, top to bottom. Coords are inclusive. + void pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]); // 10x10 px tiles on the maze = 24x24 (on 240px screen) static constexpr int WIDTH = 24; @@ -99,45 +98,70 @@ namespace Pinetime { }; + // what is currently being displayed + enum Displaying {watchface, blank, loss, amogus, autismcreature, foxgame, reminder, pinetime}; class WatchFaceMaze : public Screen { public: - WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&, Controllers::Settings&); + WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&, Controllers::Settings&, Controllers::MotorController&); ~WatchFaceMaze() override; - void Refresh() override; + void Refresh() override; bool OnTouchEvent(TouchEvents event) override; + bool OnButtonPushed() override; private: lv_task_t* taskRefresh; - void GenerateMaze(); - void InitializeMaze(); void PutNumbers(); - void PutLoss(); + void GenerateMaze(); + void ForceValidMaze(); void DrawMaze(); Utility::DirtyValue> currentDateTime {}; + std::chrono::time_point realTime {}; Controllers::DateTime& dateTimeController; Controllers::Settings& settingsController; + Controllers::MotorController& motor; Pinetime::Components::LittleVgl& lvgl; lv_color_t buf1[480]; lv_color_t buf2[480]; Maze maze; MazeRNG prng; - std::chrono::time_point lossTargetRefreshTime {}; - bool lossRefreshRequired = false; + std::chrono::time_point screenRefreshTargetTime {}; + bool screenRefreshRequired = false; bool pausedGeneration = false; // if generation was taking too much time and had to pause it to let other tasks run - uint8_t numbers[10][30] = {{0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, - {0x73,0x11,0x77,0x30,0x00,0x77,0x20,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x31,0x00,0x11,0x20,0x00,0x00}, - {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x77,0x73,0x00,0x77,0x30,0x00,0x73,0x00,0x07,0x30,0x00,0x77,0x20,0x00,0x11,0x20,0x00,0x00}, - {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x77,0x77,0x20,0x77,0x31,0x00,0x77,0x20,0x00,0x77,0x77,0x20,0x31,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, - {0x31,0x77,0x31,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x20,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x20}, - {0x31,0x11,0x11,0x20,0x00,0x00,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x11,0x17,0x20,0x00,0x01,0x77,0x77,0x20,0x77,0x77,0x20,0x31,0x11,0x00,0x20,0x00,0x07}, - {0x73,0x11,0x17,0x30,0x00,0x07,0x20,0x77,0x77,0x20,0x77,0x77,0x20,0x11,0x17,0x20,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, - {0x31,0x11,0x11,0x20,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x77,0x73,0x00,0x77,0x72,0x07,0x77,0x72,0x07,0x77,0x30,0x07,0x77,0x20,0x77,0x77,0x20,0x77}, - {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x73,0x00,0x20,0x10,0x07,0x72,0x00,0x01,0x30,0x07,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x07}, - {0x73,0x11,0x17,0x30,0x00,0x01,0x20,0x77,0x20,0x20,0x77,0x20,0x20,0x11,0x00,0x72,0x00,0x00,0x77,0x77,0x20,0x77,0x77,0x20,0x73,0x11,0x00,0x72,0x00,0x07}}; - uint8_t loss[288] = {0x77,0x77,0x77,0x77,0x77,0x73,0x17,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x72,0x07,0x77,0x77,0x77,0x77,0x77,0x77,0x31,0x77,0x77,0x77,0x72,0x07,0x73,0x17,0x77,0x77,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x31,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x77,0x20,0x77,0x77,0x77,0x72,0x07,0x72,0x07,0x77,0x20,0x77,0x31,0x00,0x11,0x11,0x11,0x10,0x01,0x10,0x01,0x11,0x00,0x11,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x07,0x77,0x77,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x01,0x11,0x11,0x77,0x77,0x20,0x77,0x72,0x07,0x72,0x07,0x72,0x00,0x00,0x00,0x77,0x77,0x77,0x77,0x77,0x77,0x72,0x07,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x72,0x07,0x77,0x77,0x77,0x77,0x77}; + // used for tap sequences for easter eggs + std::chrono::time_point lastInputTime {}; + Displaying currentState = Displaying::watchface; + constexpr static uint8_t numbers[10][15] /*6x10*/ = { + {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x8F,0x88,0xF8,0x8F,0x88,0xF8,0x85,0x0E,0x03}, + {0xF5,0xFC,0x0F,0x80,0xFF,0x8F,0xF8,0xFF,0x8F,0xF8,0xFF,0x8F,0xD0,0x58,0x00}, + {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0xFF,0x0F,0xC0,0xF0,0x3C,0x0F,0x80,0x58,0x00}, + {0xF5,0x7C,0x01,0x8F,0x8F,0xF8,0xFD,0x0F,0x80,0xFF,0x8D,0xF8,0x85,0x0E,0x03}, + {0xDF,0xD8,0xF8,0x8F,0x88,0xF8,0x85,0x08,0x00,0xFF,0x8F,0xF8,0xFF,0x8F,0xF8}, + {0xD5,0x58,0x00,0x8F,0xF8,0xFF,0x85,0x78,0x01,0xFF,0x8F,0xF8,0xD5,0x08,0x03}, + {0xF5,0x7C,0x03,0x8F,0xF8,0xFF,0x85,0x78,0x01,0x8F,0x88,0xF8,0x85,0x0E,0x03}, + {0xD5,0x58,0x00,0xFF,0x8F,0xF8,0xFF,0x0F,0xE3,0xFE,0x3F,0xC3,0xF8,0xFF,0x8F}, + {0xF5,0x7C,0x01,0x8F,0x88,0xF0,0x84,0x3E,0x01,0xC3,0x88,0xF8,0x85,0x0E,0x03}, + {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x0E,0x00,0xFF,0x8F,0xF8,0xF5,0x0E,0x03}}; + constexpr static uint8_t am[12] /*6x8*/ = {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x08,0x00,0x8F,0x88,0xF8}; + constexpr static uint8_t pm[12] /*6x8*/ = {0xD5,0x78,0x01,0x8F,0x88,0xF8,0x85,0x08,0x03,0x8F,0xF8,0xFF}; + // array sizes are to indicate size (x*y). remember x is effectively halved. + constexpr static uint8_t loss[105] /*21x20*/ = {0xFD,0xFF,0xFF,0xF7,0xFF,0xFE,0x3F,0xFF,0xF8,0xFF,0xFF,0x8F,0xFF,0xFE,0x3F,0xDF,0xE3,0xFF,0xFF,0x8F,0xE3,0xF8,0xFF,0xFF,0xE3,0xF8,0xFE,0x3F,0xFF,0xF8,0xFE,0x3F,0x8F,0xFF,0xFE,0x3F,0x8F,0xE3,0xFF,0xFF,0x8F,0xE3,0xF8,0xFF,0xFF,0xE3,0xF8,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F,0xDF,0xFF,0x7F,0xFF,0x8F,0xE3,0xFF,0x8F,0xFF,0xE3,0xF8,0xFF,0xE3,0xFF,0xF8,0xFE,0x3F,0xF8,0xFF,0xFE,0x3F,0x8F,0xFE,0x3F,0xFF,0x8F,0xE3,0xFF,0x8F,0xFF,0xE3,0xF8,0xFF,0xE3,0xFF,0xF8,0xFE,0x3F,0xF8,0xF5,0x56,0x3F,0x8F,0xFE,0x38,0x00}; + constexpr static uint8_t amogus[114] /*19x24*/ = {0xFF,0xFF,0x55,0x7F,0xFF,0xFF,0xD0,0x00,0x7F,0xFF,0xFE,0x0F,0xF8,0x7F,0xFF,0xF0,0xFF,0x40,0x7F,0xFF,0x83,0xF0,0x00,0x5F,0xFE,0x3F,0x0F,0xFE,0x1F,0x50,0xF8,0xFF,0xFE,0x30,0x03,0xE3,0xFF,0xF8,0x8F,0x8F,0x87,0xFF,0xC2,0x3E,0x3F,0x85,0x54,0x38,0xF8,0xFF,0xE0,0x03,0xE3,0xE3,0xFF,0xFF,0x8F,0x8F,0x8F,0xFF,0xFE,0x3E,0x3E,0x3F,0xFF,0xF8,0xF8,0xF8,0xFF,0xFF,0xE3,0xE3,0xE3,0xFF,0xFF,0x8F,0x8F,0x8F,0xFF,0xFE,0x3E,0x14,0x3F,0xD7,0xF8,0xFE,0x00,0xFC,0x07,0xE3,0xFF,0x83,0xE3,0x8F,0x8F,0xFF,0x8F,0x8E,0x3E,0x3F,0xFE,0x3E,0x38,0xF8,0xFF,0xF8,0x50,0xE1,0x43,0xFF,0xE0,0x03,0x80,0x3F}; + constexpr static uint8_t autismcreature[126] /*24x21*/ = {0xFD,0x55,0x55,0x7F,0xFF,0xFF,0xF0,0x00,0x00,0x17,0xFF,0xFF,0xC3,0xFF,0xFF,0x81,0xFF,0xFF,0x8F,0xFF,0xFF,0xF8,0x7F,0xFF,0xBF,0x5F,0xF5,0xFE,0x3F,0xFF,0xBF,0x87,0xF8,0x7E,0x3F,0xFF,0xB9,0x03,0x90,0x3E,0x3F,0xFF,0xB8,0x03,0x80,0x3E,0x3F,0xFF,0xBE,0x0F,0xE0,0xFE,0x15,0x57,0x9F,0xFF,0xFF,0xFC,0x00,0x01,0x87,0xFF,0xFF,0xF0,0x3F,0xF8,0xE1,0x5F,0xFF,0x43,0xFF,0xF8,0xF8,0x05,0x54,0x0F,0xFF,0xF8,0xFE,0x00,0x00,0xFF,0xFF,0xF8,0xFE,0x3F,0xFF,0xFF,0xFF,0xF8,0xFE,0x3F,0x7F,0xD7,0xFD,0xF8,0xFE,0x3E,0x3F,0x01,0xF8,0xF8,0xFE,0x3E,0x3E,0x00,0xF8,0xF8,0xFE,0x3E,0x3E,0x38,0xF8,0xF8,0xFE,0x14,0x14,0x38,0x50,0x50,0xFF,0x80,0x00,0xFE,0x00,0x03}; + constexpr static uint8_t foxgame[132] /*24x22*/ = {0xFF,0xD7,0xFF,0xFF,0xF5,0xFF,0xFD,0x01,0x7F,0xFF,0x40,0x5F,0xF0,0x38,0x1F,0xFC,0x0E,0x07,0xC3,0xFF,0x87,0xF0,0xFF,0xE1,0x8F,0xFF,0xE3,0xE3,0xFF,0xF8,0x8F,0xFF,0xFF,0xE1,0xFF,0xF0,0x8F,0xFF,0xFF,0xE0,0x5F,0x43,0x8F,0xFF,0xFF,0xE0,0x04,0x0F,0x8F,0xFF,0xFF,0xE3,0xE0,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x85,0x55,0x55,0x41,0x55,0x55,0x80,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xDF,0xFF,0xFF,0xF7,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x87,0xFF,0xFF,0xE1,0xFF,0xFF,0xE1,0x7F,0xFF,0xF8,0x5F,0xFF,0xF8,0x17,0xFF,0xFE,0x05,0xFF,0xFF,0x83,0xFF,0xFF,0xE0,0xFF}; + constexpr static uint8_t reminder[102] /*24x17*/ = {0xFF,0xD5,0xF7,0xDF,0x57,0xFF,0xFF,0x80,0xE3,0x8E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE1,0x0E,0x17,0xFF,0xFF,0xE3,0xE0,0x0E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE3,0x8E,0x17,0xFF,0xFF,0xE3,0xE3,0x8E,0x03,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xF5,0x7F,0x5F,0xF5,0x5F,0xD5,0xC0,0x3C,0x07,0xC0,0x07,0x80,0x8F,0xF8,0xE3,0x88,0xE3,0x8F,0x8F,0xF8,0xE3,0x88,0xE3,0x85,0x8F,0x78,0x43,0x88,0xE3,0x80,0x8E,0x38,0x03,0x8F,0xE3,0x8F,0x84,0x38,0xE3,0x8F,0xE3,0x85,0xE0,0xF8,0xE3,0x8F,0xE3,0x80}; + //constexpr static uint8_t foxface[144] /*24x24*/ = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFD,0x7F,0xFF,0xFF,0xFF,0xFF,0xF8,0x17,0xFF,0xFF,0xFF,0xFF,0xF8,0x01,0x7F,0xFF,0xF5,0x5F,0xF8,0xF8,0x1F,0xFF,0x40,0x07,0xF8,0xFF,0x8F,0xF4,0x0F,0xE3,0xF8,0x7F,0x85,0x40,0xFF,0xC3,0xF8,0x1F,0xE0,0x0F,0xFD,0x03,0xF8,0xE7,0xFF,0xFF,0xF3,0xE3,0xF0,0xFF,0xFF,0xFF,0xFF,0xE3,0xC0,0xFD,0x7F,0xFF,0xFF,0xCF,0x8F,0xFE,0x1F,0xFD,0x7F,0x8F,0xBF,0xE4,0x0F,0xFE,0x1F,0x87,0xFF,0xE0,0x0F,0xE4,0x0F,0xE1,0xFF,0xF8,0x3F,0xE0,0x0F,0xF8,0xDF,0xFF,0xFF,0xF8,0x3F,0xF8,0xE5,0x7F,0xF5,0xFF,0xFF,0xF8,0xFF,0x95,0x40,0x7F,0xFF,0xFE,0xFF,0xFF,0xE0,0x15,0x55,0x54,0xBF,0xFF,0xF8,0xFF,0xFF,0xFF,0x9F,0xFF,0xFF,0xFF,0xFF,0xFD,0x87,0xFF,0xFF,0xFF,0xFF,0xF0,0xE1,0x5F,0xFF,0xFF,0xFF,0x43,0xF8,0x05,0x5F,0xFF,0xD4,0x3F}; + constexpr static uint8_t pinetime[120] /*20x24*/ = {0xFF,0xFF,0xF7,0xFF,0xFF,0xFF,0xFF,0xC1,0xFF,0xFF,0xFF,0xFF,0x00,0x7F,0xFF,0xFF,0xF6,0x00,0x37,0xFF,0xFF,0xC1,0x63,0x41,0xFF,0xFF,0x80,0xD5,0x80,0xFF,0xFF,0xB5,0x00,0x56,0xFF,0xF7,0xC0,0x00,0x01,0xF7,0xE1,0x60,0x00,0x03,0x43,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x5D,0x00,0x03,0xE0,0x00,0xD5,0x80,0x03,0xE0,0x35,0x00,0x56,0x03,0xE3,0x40,0x00,0x01,0x63,0x96,0x00,0x00,0x00,0x34,0x81,0x60,0x00,0x03,0x40,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x77,0x00,0x03,0xF8,0x00,0xC1,0x80,0x0F,0xF8,0x0D,0x00,0x58,0x0F,0xFF,0xD0,0x00,0x05,0xFF,0xFF,0xE0,0x00,0x03,0xFF,0xFF,0xFE,0x00,0x3F,0xFF,0xFF,0xFF,0xE3,0xFF,0xFF};\ + // note that the codes are effectively backwards; the currentCode is a stack being pushed from the left. 0-3, clockwise from up. + constexpr static uint8_t lossCode[8] = {0,0,2,2,3,1,3,1}; // RLRLDDUU (konami code backwards) + constexpr static uint8_t amogusCode[8] = {1,3,1,3,2,2,0,0}; // UUDDLRLR (konami code) + constexpr static uint8_t autismCode[8] = {3,1,3,1,3,1,3,1}; // RLRLRLRL (pet pet pet pet) + constexpr static uint8_t foxCode[7] = {0,1,0,3,2,1,2}; // the first of a type of secret in a curious game :3 + constexpr static uint8_t reminderCode[4] = {3,2,1,0}; // URDL + constexpr static uint8_t pinetimeCode[8] = {1,2,3,0,1,2,3,0}; // ULDRULDR (two counterclockwise rotations) + uint8_t currentCode[8]; }; } @@ -152,7 +176,8 @@ namespace Pinetime { static Screens::Screen* Create(AppControllers& controllers) { return new Screens::WatchFaceMaze(controllers.lvgl, controllers.dateTimeController, - controllers.settingsController/*, + controllers.settingsController, + controllers.motorController/*, controllers.batteryController, controllers.bleController*/); }; From 0dc3e3c6b2f01c8c7cc68397a285c14d0782d0c6 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:57:15 -0500 Subject: [PATCH 06/23] Add whitespace and comments to WatchFaceMaze.h --- src/displayapp/screens/WatchFaceMaze.h | 153 +++++++++++++++++-------- 1 file changed, 106 insertions(+), 47 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 3b2ee2fe90..0d09fec2ed 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -13,44 +13,50 @@ namespace Pinetime { // really just an abstraction of a uint8_t but with functions to get the individual bits - // reading up on bitfields and their inconsistencies scared me away from them... + // reading up on bitfields and their potential inconsistencies scared me away from them... struct MazeTile { - static constexpr uint8_t UPMASK = 0b0001; - static constexpr uint8_t LEFTMASK = 0b0010; + static constexpr uint8_t UPMASK = 0b0001; + static constexpr uint8_t LEFTMASK = 0b0010; static constexpr uint8_t FLAGEMPTYMASK = 0b0100; static constexpr uint8_t FLAGGENMASK = 0b1000; uint8_t map = 0; - // set flags on tiles in the maze. returns the object so they can be chained. - MazeTile setUp(bool value) {map = (map&~UPMASK) | (value * UPMASK); return *this;} - MazeTile setLeft(bool value) {map = (map&~LEFTMASK) | (value * LEFTMASK); return *this;} + + // Set flags on given tile. Returns the object so they can be chained. + // Am I cool for aligning it like this? Or does it just make it hard to read... + MazeTile setUp(bool value) {map = (map&~UPMASK) | (value * UPMASK); return *this;} + MazeTile setLeft(bool value) {map = (map&~LEFTMASK) | (value * LEFTMASK); return *this;} MazeTile setFlagEmpty(bool value) {map = (map&~FLAGEMPTYMASK) | (value * FLAGEMPTYMASK); return *this;} - MazeTile setFlagGen(bool value) {map = (map&~FLAGGENMASK) | (value * FLAGGENMASK); return *this;} - // get flags on tiles in the maze - bool getUp() {return map&UPMASK;} - bool getLeft() {return map&LEFTMASK;} - bool getFlagEmpty() {return map& FLAGEMPTYMASK;} - bool getFlagGen() {return map& FLAGGENMASK;} + MazeTile setFlagGen(bool value) {map = (map&~FLAGGENMASK) | (value * FLAGGENMASK); return *this;} + + // Get flags on given tile + bool getUp() {return map & UPMASK;} + bool getLeft() {return map & LEFTMASK;} + bool getFlagEmpty() {return map & FLAGEMPTYMASK;} + bool getFlagGen() {return map & FLAGGENMASK;} }; - // custom PRNG for the maze to allow it to be deterministic for any given minute + // custom PRNG for the maze to easily allow it to be deterministic for any given minute class MazeRNG { public: MazeRNG(uint64_t start_seed = 64) {seed(start_seed);} - // reseed the generator. handles any input well. if it's 0, acts as though it was seeded with 1 + + // Reseed the generator. Handles any input well. If seed is 0, acts as though it was seeded with 1. void seed(uint64_t seed) {state = seed ? seed : 1; rand();} - // rng lifted straight from https://en.wikipedia.org/wiki/Xorshift#xorshift* (asterisk is part of the link) + + // RNG lifted straight from https://en.wikipedia.org/wiki/Xorshift#xorshift* (asterisk is part of the link) uint32_t rand() { state ^= state >> 12; state ^= state << 25; state ^= state >> 27; - // wikipedia had the multiplication in the return. idk what it's doing exactly so i won't change that. return state * 0x2545F4914F6CDD1DULL >> 32; } - // random in range, inclusive on both ends (don't make max=min); return rand()%(max-min+1)+min;} + private: uint64_t state; }; @@ -58,7 +64,7 @@ namespace Pinetime { - // little bit of convenience + // little bit of convenience for working with directions enum TileAttr {up, down, left, right, flagempty, flaggen}; // could also be called Field or something. Does not handle stuff like generation or printing, @@ -66,25 +72,32 @@ namespace Pinetime { class Maze { public: Maze(); - // I guess I don't need an explicit destructor since everything goes out of scope nicely? - // get and set can work with either xy or indexes + + // Get and set can work with either xy or indexes MazeTile get(int x, int y); MazeTile get(int index); + void set(int x, int y, MazeTile tile); void set(int index, MazeTile tile); - // fill fills all tiles in the maze with a given value, optionally with a mask on what bits to mask - // (use the MazeTile::*MASK-s) + + // fill() fills all tiles in the maze with a given value, optionally with a mask on what bits to change. + // Only cares about the low 4 bits in the value and mask. + // (use the MazeTile::*MASK values) void fill(MazeTile tile, uint8_t mask = 0xFF); void fill(uint8_t value, uint8_t mask = 0xFF); - // allows abstractly setting a given side on a tile. supports down and right for convenience. + + // Allows abstractly setting a given side on a tile. Supports down and right for convenience. void setSide(int x, int y, TileAttr attr, bool value); void setSide(int index, TileAttr attr, bool value); - // same as setSide, just getting. + + // Same as setSide, just getting. bool getSide(int x, int y, TileAttr attr); bool getSide(int index, TileAttr attr); - // paste onto an empty board. Marks a tile as not empty if any neighboring walls are gone. + + // Paste onto an empty board. Marks a tile as not empty if any neighboring walls are gone. // toPaste is a 1d array of uint8_t, only containing the two wall bits (left then up). So that's 4 walls in a byte. - // always places values left to right, top to bottom. Coords are inclusive. + // If you have a weird number of tiles (like 17), just pad it out to the nearest byte. The function ignores any extra data. + // Always places values left to right, top to bottom. Coords are inclusive. void pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]); // 10x10 px tiles on the maze = 24x24 (on 240px screen) @@ -93,46 +106,82 @@ namespace Pinetime { static constexpr int TILESIZE = 10; // the actual size of the entire map. only store 4 bits per block, so 2 blocks per byte static constexpr int FLATSIZE = WIDTH*HEIGHT/2; + private: + // the internal map. Doesn't actually store MazeTiles, but packs their contents to 2 tiles per byte. uint8_t mazemap[FLATSIZE]; }; - // what is currently being displayed - enum Displaying {watchface, blank, loss, amogus, autismcreature, foxgame, reminder, pinetime}; + // What is currently being displayed. + // Watchface is normal operation; anything else is an easter egg. Really only used to indicate what + // should be displayed when the screen refreshes. + enum Displaying {watchface, blank, loss, amogus, autismcreature, foxgame, reminder, pinetime}; + + // The watchface itself class WatchFaceMaze : public Screen { public: WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&, Controllers::Settings&, Controllers::MotorController&); ~WatchFaceMaze() override; + + // Functions required for app operation. void Refresh() override; bool OnTouchEvent(TouchEvents event) override; bool OnButtonPushed() override; private: - lv_task_t* taskRefresh; + // Put numbers onto the screen. Acts as a seed to generate from. void PutNumbers(); + + // Generate the maze around whatever the maze was seeded with. + // MAZE MUST BE SEEDED ELSE ALL YOU'LL GENERATE IS AN INFINITE LOOP! + // If seed has disconnected components, maze will not be perfect. void GenerateMaze(); + + // If the maze has any disconnected components (such as if seeded with multiple disconnected blocks), + // poke holes to force all components to be connected. void ForceValidMaze(); + + // Draws the maze to the screen. void DrawMaze(); - Utility::DirtyValue> currentDateTime {}; - std::chrono::time_point realTime {}; + + // Stuff necessary for interacting with the OS and whatnot + lv_task_t* taskRefresh; Controllers::DateTime& dateTimeController; Controllers::Settings& settingsController; Controllers::MotorController& motor; - Pinetime::Components::LittleVgl& lvgl; - lv_color_t buf1[480]; - lv_color_t buf2[480]; + Components::LittleVgl& lvgl; + + // Maze and internal RNG (so it doesn't mess with other things by reseeding the regular C RNG provider) Maze maze; MazeRNG prng; + + // Time stuff. + // currentDateTime is used for keeping track of minutes. It's what refreshes the screen every minute. + // realTime is present due to what seems to be a race condition if dateTimeController.CurrentDateTime() is called + // by two things at once. It gets updated every refresh, or roughly every 20ms. Do not use it for anything high precision. + // (I really don't understand, but doing this decreased infinisim crashes due to mutexes releasing without being held so whatever) + Utility::DirtyValue> currentDateTime {}; + std::chrono::time_point realTime {}; + + // Buffers for use during printing. There's two it flips between because if there was only one, + // it would start being overwritten before the DMA finishes, so it'd corrupt parts of the display. + lv_color_t buf1[480]; + lv_color_t buf2[480]; + + // All concerning the printing of the screen. If screenRefreshRequired is set and screenRefreshTargetTime is + // greater than the current time, it waits until that target time before refreshing. Otherwise, it refreshes + // immediately when it sees that screenRefreshRequired is set. + // pausedGeneration is used if the maze took too long to generate, so it lets other processes get cpu time. + // It really should never trigger with this small 24x24 maze. std::chrono::time_point screenRefreshTargetTime {}; bool screenRefreshRequired = false; - bool pausedGeneration = false; // if generation was taking too much time and had to pause it to let other tasks run - // used for tap sequences for easter eggs - std::chrono::time_point lastInputTime {}; - Displaying currentState = Displaying::watchface; + bool pausedGeneration = false; + + // Number data and AM/PM data for displaying time constexpr static uint8_t numbers[10][15] /*6x10*/ = { {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x8F,0x88,0xF8,0x8F,0x88,0xF8,0x85,0x0E,0x03}, {0xF5,0xFC,0x0F,0x80,0xFF,0x8F,0xF8,0xFF,0x8F,0xF8,0xFF,0x8F,0xD0,0x58,0x00}, @@ -146,7 +195,25 @@ namespace Pinetime { {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x0E,0x00,0xFF,0x8F,0xF8,0xF5,0x0E,0x03}}; constexpr static uint8_t am[12] /*6x8*/ = {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x08,0x00,0x8F,0x88,0xF8}; constexpr static uint8_t pm[12] /*6x8*/ = {0xD5,0x78,0x01,0x8F,0x88,0xF8,0x85,0x08,0x03,0x8F,0xF8,0xFF}; - // array sizes are to indicate size (x*y). remember x is effectively halved. + + // Used for swipe sequences for easter eggs. + // currentState is what is being displayed. It's generally categorized into "watchface" and "not watchface". + // lastInputTime is for long clicking on the main watchface. If you long click twice in 2 seconds, it goes to the secret input screen. + // currentCode is the current swipe sequence that's being worked on + Displaying currentState = Displaying::watchface; + std::chrono::time_point lastInputTime {}; + uint8_t currentCode[8]; + + // Input codes for secret swipe gestures + // Note that the codes are effectively backwards; the currentCode is a stack being pushed from the left. Values are 0-3, clockwise from up. + constexpr static uint8_t lossCode[8] = {0,0,2,2,3,1,3,1}; // RLRLDDUU (konami code backwards) + constexpr static uint8_t amogusCode[8] = {1,3,1,3,2,2,0,0}; // UUDDLRLR (konami code) + constexpr static uint8_t autismCode[8] = {3,1,3,1,3,1,3,1}; // RLRLRLRL (pet pet pet pet) + constexpr static uint8_t foxCode[7] = {0,1,0,3,2,1,2}; // the first of a type of secret in a curious game :3 + constexpr static uint8_t reminderCode[4] = {3,2,1,0}; // URDL + constexpr static uint8_t pinetimeCode[8] = {1,2,3,0,1,2,3,0}; // ULDRULDR (two counterclockwise rotations) + + // Maze data for secrets. These are pasted onto the screen when the corresponding code is entered. constexpr static uint8_t loss[105] /*21x20*/ = {0xFD,0xFF,0xFF,0xF7,0xFF,0xFE,0x3F,0xFF,0xF8,0xFF,0xFF,0x8F,0xFF,0xFE,0x3F,0xDF,0xE3,0xFF,0xFF,0x8F,0xE3,0xF8,0xFF,0xFF,0xE3,0xF8,0xFE,0x3F,0xFF,0xF8,0xFE,0x3F,0x8F,0xFF,0xFE,0x3F,0x8F,0xE3,0xFF,0xFF,0x8F,0xE3,0xF8,0xFF,0xFF,0xE3,0xF8,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F,0xDF,0xFF,0x7F,0xFF,0x8F,0xE3,0xFF,0x8F,0xFF,0xE3,0xF8,0xFF,0xE3,0xFF,0xF8,0xFE,0x3F,0xF8,0xFF,0xFE,0x3F,0x8F,0xFE,0x3F,0xFF,0x8F,0xE3,0xFF,0x8F,0xFF,0xE3,0xF8,0xFF,0xE3,0xFF,0xF8,0xFE,0x3F,0xF8,0xF5,0x56,0x3F,0x8F,0xFE,0x38,0x00}; constexpr static uint8_t amogus[114] /*19x24*/ = {0xFF,0xFF,0x55,0x7F,0xFF,0xFF,0xD0,0x00,0x7F,0xFF,0xFE,0x0F,0xF8,0x7F,0xFF,0xF0,0xFF,0x40,0x7F,0xFF,0x83,0xF0,0x00,0x5F,0xFE,0x3F,0x0F,0xFE,0x1F,0x50,0xF8,0xFF,0xFE,0x30,0x03,0xE3,0xFF,0xF8,0x8F,0x8F,0x87,0xFF,0xC2,0x3E,0x3F,0x85,0x54,0x38,0xF8,0xFF,0xE0,0x03,0xE3,0xE3,0xFF,0xFF,0x8F,0x8F,0x8F,0xFF,0xFE,0x3E,0x3E,0x3F,0xFF,0xF8,0xF8,0xF8,0xFF,0xFF,0xE3,0xE3,0xE3,0xFF,0xFF,0x8F,0x8F,0x8F,0xFF,0xFE,0x3E,0x14,0x3F,0xD7,0xF8,0xFE,0x00,0xFC,0x07,0xE3,0xFF,0x83,0xE3,0x8F,0x8F,0xFF,0x8F,0x8E,0x3E,0x3F,0xFE,0x3E,0x38,0xF8,0xFF,0xF8,0x50,0xE1,0x43,0xFF,0xE0,0x03,0x80,0x3F}; constexpr static uint8_t autismcreature[126] /*24x21*/ = {0xFD,0x55,0x55,0x7F,0xFF,0xFF,0xF0,0x00,0x00,0x17,0xFF,0xFF,0xC3,0xFF,0xFF,0x81,0xFF,0xFF,0x8F,0xFF,0xFF,0xF8,0x7F,0xFF,0xBF,0x5F,0xF5,0xFE,0x3F,0xFF,0xBF,0x87,0xF8,0x7E,0x3F,0xFF,0xB9,0x03,0x90,0x3E,0x3F,0xFF,0xB8,0x03,0x80,0x3E,0x3F,0xFF,0xBE,0x0F,0xE0,0xFE,0x15,0x57,0x9F,0xFF,0xFF,0xFC,0x00,0x01,0x87,0xFF,0xFF,0xF0,0x3F,0xF8,0xE1,0x5F,0xFF,0x43,0xFF,0xF8,0xF8,0x05,0x54,0x0F,0xFF,0xF8,0xFE,0x00,0x00,0xFF,0xFF,0xF8,0xFE,0x3F,0xFF,0xFF,0xFF,0xF8,0xFE,0x3F,0x7F,0xD7,0xFD,0xF8,0xFE,0x3E,0x3F,0x01,0xF8,0xF8,0xFE,0x3E,0x3E,0x00,0xF8,0xF8,0xFE,0x3E,0x3E,0x38,0xF8,0xF8,0xFE,0x14,0x14,0x38,0x50,0x50,0xFF,0x80,0x00,0xFE,0x00,0x03}; @@ -154,14 +221,6 @@ namespace Pinetime { constexpr static uint8_t reminder[102] /*24x17*/ = {0xFF,0xD5,0xF7,0xDF,0x57,0xFF,0xFF,0x80,0xE3,0x8E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE1,0x0E,0x17,0xFF,0xFF,0xE3,0xE0,0x0E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE3,0x8E,0x17,0xFF,0xFF,0xE3,0xE3,0x8E,0x03,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xF5,0x7F,0x5F,0xF5,0x5F,0xD5,0xC0,0x3C,0x07,0xC0,0x07,0x80,0x8F,0xF8,0xE3,0x88,0xE3,0x8F,0x8F,0xF8,0xE3,0x88,0xE3,0x85,0x8F,0x78,0x43,0x88,0xE3,0x80,0x8E,0x38,0x03,0x8F,0xE3,0x8F,0x84,0x38,0xE3,0x8F,0xE3,0x85,0xE0,0xF8,0xE3,0x8F,0xE3,0x80}; //constexpr static uint8_t foxface[144] /*24x24*/ = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFD,0x7F,0xFF,0xFF,0xFF,0xFF,0xF8,0x17,0xFF,0xFF,0xFF,0xFF,0xF8,0x01,0x7F,0xFF,0xF5,0x5F,0xF8,0xF8,0x1F,0xFF,0x40,0x07,0xF8,0xFF,0x8F,0xF4,0x0F,0xE3,0xF8,0x7F,0x85,0x40,0xFF,0xC3,0xF8,0x1F,0xE0,0x0F,0xFD,0x03,0xF8,0xE7,0xFF,0xFF,0xF3,0xE3,0xF0,0xFF,0xFF,0xFF,0xFF,0xE3,0xC0,0xFD,0x7F,0xFF,0xFF,0xCF,0x8F,0xFE,0x1F,0xFD,0x7F,0x8F,0xBF,0xE4,0x0F,0xFE,0x1F,0x87,0xFF,0xE0,0x0F,0xE4,0x0F,0xE1,0xFF,0xF8,0x3F,0xE0,0x0F,0xF8,0xDF,0xFF,0xFF,0xF8,0x3F,0xF8,0xE5,0x7F,0xF5,0xFF,0xFF,0xF8,0xFF,0x95,0x40,0x7F,0xFF,0xFE,0xFF,0xFF,0xE0,0x15,0x55,0x54,0xBF,0xFF,0xF8,0xFF,0xFF,0xFF,0x9F,0xFF,0xFF,0xFF,0xFF,0xFD,0x87,0xFF,0xFF,0xFF,0xFF,0xF0,0xE1,0x5F,0xFF,0xFF,0xFF,0x43,0xF8,0x05,0x5F,0xFF,0xD4,0x3F}; constexpr static uint8_t pinetime[120] /*20x24*/ = {0xFF,0xFF,0xF7,0xFF,0xFF,0xFF,0xFF,0xC1,0xFF,0xFF,0xFF,0xFF,0x00,0x7F,0xFF,0xFF,0xF6,0x00,0x37,0xFF,0xFF,0xC1,0x63,0x41,0xFF,0xFF,0x80,0xD5,0x80,0xFF,0xFF,0xB5,0x00,0x56,0xFF,0xF7,0xC0,0x00,0x01,0xF7,0xE1,0x60,0x00,0x03,0x43,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x5D,0x00,0x03,0xE0,0x00,0xD5,0x80,0x03,0xE0,0x35,0x00,0x56,0x03,0xE3,0x40,0x00,0x01,0x63,0x96,0x00,0x00,0x00,0x34,0x81,0x60,0x00,0x03,0x40,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x77,0x00,0x03,0xF8,0x00,0xC1,0x80,0x0F,0xF8,0x0D,0x00,0x58,0x0F,0xFF,0xD0,0x00,0x05,0xFF,0xFF,0xE0,0x00,0x03,0xFF,0xFF,0xFE,0x00,0x3F,0xFF,0xFF,0xFF,0xE3,0xFF,0xFF};\ - // note that the codes are effectively backwards; the currentCode is a stack being pushed from the left. 0-3, clockwise from up. - constexpr static uint8_t lossCode[8] = {0,0,2,2,3,1,3,1}; // RLRLDDUU (konami code backwards) - constexpr static uint8_t amogusCode[8] = {1,3,1,3,2,2,0,0}; // UUDDLRLR (konami code) - constexpr static uint8_t autismCode[8] = {3,1,3,1,3,1,3,1}; // RLRLRLRL (pet pet pet pet) - constexpr static uint8_t foxCode[7] = {0,1,0,3,2,1,2}; // the first of a type of secret in a curious game :3 - constexpr static uint8_t reminderCode[4] = {3,2,1,0}; // URDL - constexpr static uint8_t pinetimeCode[8] = {1,2,3,0,1,2,3,0}; // ULDRULDR (two counterclockwise rotations) - uint8_t currentCode[8]; }; } From 1bf7ca96691e878b80da85af346a015515b01efa Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sun, 20 Oct 2024 01:10:08 -0500 Subject: [PATCH 07/23] Refactor WatchFaceMaze.cpp --- src/displayapp/screens/WatchFaceMaze.cpp | 438 ++++++++++++++--------- src/displayapp/screens/WatchFaceMaze.h | 14 + 2 files changed, 279 insertions(+), 173 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index ea2c5aa0eb..93feeb73da 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -5,6 +5,7 @@ using namespace Pinetime::Applications::Screens; + // Despite being called Maze, this really is only a relatively simple wrapper for the specialized // (fake) 2d array on which the maze structure is built. It should only have manipulations for // the structure, generating and printing should be handled elsewhere. @@ -12,8 +13,9 @@ Maze::Maze() { std::fill_n(mazemap, FLATSIZE, 0); } + // only returns 4 bits (since that's all that's stored) -// returns 0 in case of out of bounds access +// returns walls but unset flags in case of out of bounds access MazeTile Maze::get(int x, int y) { if (x<0||x>WIDTH||y<0||y>HEIGHT) {return MazeTile(0b0011);} return get((y * WIDTH) + x); @@ -25,6 +27,7 @@ MazeTile Maze::get(int index) { else return MazeTile(mazemap[index/2] >> 4); } + // only stores the low 4 bits of the value // if out of bounds, does nothing void Maze::set(int x, int y, MazeTile tile) { @@ -38,6 +41,7 @@ void Maze::set(int index, MazeTile tile) { else mazemap[index/2] = (mazemap[index/2] & 0b00001111) | tile.map << 4; } + // only operates on the low 4 bits of the uint8_t. // only sets the bits from the value that are also on in the mask, rest are left alone // e.g. existing = 1010, value = 0001, mask = 0011, then result = 1001 @@ -45,9 +49,11 @@ void Maze::set(int index, MazeTile tile) { void Maze::fill(uint8_t value, uint8_t mask) { value = value & 0b00001111; value |= value << 4; + if (mask == 0xFF) { // did not include a mask std::fill_n(mazemap, FLATSIZE, value); + } else { // included a mask mask = mask & 0b00001111; @@ -59,9 +65,9 @@ void Maze::fill(uint8_t value, uint8_t mask) { } } } -inline void Maze::fill(MazeTile tile, uint8_t mask) { - fill(tile.map, mask); -} +inline void Maze::fill(MazeTile tile, uint8_t mask) + {fill(tile.map, mask);} + // For quickly manipulating. Also allows better abstraction by allowing setting of down and right sides. // Silently does nothing if given invalid values. @@ -93,25 +99,29 @@ bool Maze::getSide(int x, int y, TileAttr attr) { return getSide(y*WIDTH+x, attr); } +// Paste a set of tiles into the given coords. void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]) { // Assumes a maze with empty flags all true, and all walls present uint16_t flatcoord = 0; // the position in the array (inside the byte, so index 1 would be mask 0b00110000 in the first byte) for (int y = y1; y <= y2; y++) { for (int x = x1; x <= x2; x++) { - // get target wall out of the paste buffer into the low two bits of a byte + // working holds the target wall (bit 2 for left wall, bit 1 for up wall) uint8_t working = (toPaste[flatcoord/4] & (0b11 << ((3-(flatcoord%4))*2))) >> ((3-(flatcoord%4))*2); + // handle left wall if (!(working & 0b10)) { setSide(x, y, TileAttr::left, false); setSide(x, y, TileAttr::flagempty, false); if (x > 0) setSide(x-1, y, TileAttr::flagempty, false); } + // handle up wall if (!(working & 0b01)) { setSide(x, y, TileAttr::up, false); setSide(x, y, TileAttr::flagempty, false); if (y > 0) setSide(x, y-1, TileAttr::flagempty, false); } + flatcoord++; } } @@ -132,51 +142,39 @@ WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, prng {MazeRNG()} { taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + // refreshing here seems to cause issues in infinisim //Refresh(); } + WatchFaceMaze::~WatchFaceMaze() { lv_obj_clean(lv_scr_act()); lv_task_del(taskRefresh); } + void WatchFaceMaze::Refresh() { + // store time for other functions to use, and update time if needed realTime = dateTimeController.CurrentDateTime(); currentDateTime = std::chrono::time_point_cast(realTime); + // refresh time if either a minute has passed, or screen refresh timer expired. // if minute rolls over while screenrefresh is required, ignore it. the refresh timer will handle it. if (pausedGeneration || // if generation paused, need to complete it - (currentState == Displaying::watchface && !screenRefreshRequired && currentDateTime.IsUpdated()) || // already on watchface, not waiting for a screen refresh, and time updated - (screenRefreshRequired && realTime > screenRefreshTargetTime)) { // waiting on a refresh + (currentState == Displaying::watchface && !screenRefreshRequired && currentDateTime.IsUpdated()) || // already on watchface, not waiting for a screen refresh, and time updated + (screenRefreshRequired && realTime > screenRefreshTargetTime)) { // waiting on a refresh + // if generation wasn't paused (i.e. doing a ground up maze gen), set everything up if (!pausedGeneration) { // only reseed PRNG if got here by the minute rolling over if (!screenRefreshRequired) prng.seed(currentDateTime.Get().time_since_epoch().count()); - // prepare maze by filling it with all walls and empty tiles - maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); - // seed the maze with whatever is required at the moment - if (currentState == Displaying::watchface) {PutNumbers();} - else if (currentState == Displaying::blank) { - // seed maze with 4 tiles - uint8_t seed[1] = {0xD5}; - int randx = prng.rand(0,20); - int randy = prng.rand(3,20); - maze.pasteMazeSeed(randx, randy, randx + 3, randy, seed); - } - else if (currentState == Displaying::loss) { - maze.pasteMazeSeed(2, 2, 22, 21, loss);} - else if (currentState == Displaying::amogus) { - maze.pasteMazeSeed(3, 0, 21, 23, amogus);} - else if (currentState == Displaying::autismcreature) { - maze.pasteMazeSeed(0, 2, 11, 22, autismcreature);} - else if (currentState == Displaying::foxgame) { - maze.pasteMazeSeed(0, 1, 23, 22, foxgame);} - else if (currentState == Displaying::reminder) { - maze.pasteMazeSeed(0, 3, 23, 19, reminder);} - else if (currentState == Displaying::pinetime) { - maze.pasteMazeSeed(2, 0, 21, 23, pinetime);} + InitializeMaze(); + SeedMaze(); } + + // always need to run GenerateMaze(). GenerateMaze(); + // only draw once maze is fully generated (not paused) if (!pausedGeneration) { ForceValidMaze(); @@ -186,11 +184,13 @@ void WatchFaceMaze::Refresh() { } } -// allow pushing the button to go back to the main watchface +// allow pushing the button to go back to the watchface bool WatchFaceMaze::OnButtonPushed() { if (currentState != Displaying::watchface) { screenRefreshRequired = true; currentState = Displaying::watchface; + // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast + lastInputTime = std::chrono::time_point(); return true; } return false; @@ -199,68 +199,85 @@ bool WatchFaceMaze::OnButtonPushed() { bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { // if generation is paused, let it continue working on that. This should really never trigger. if (pausedGeneration) {return false;} - auto time = realTime; - //std::chrono::time_point time {}; - if (event == Pinetime::Applications::TouchEvents::LongTap) { - // LONG TAPPED - if (currentState == Displaying::watchface) { - // on watchface; either refresh maze or go to blank state - if (lastInputTime + std::chrono::milliseconds(2500) > time) { - // long tapped twice in sequence; switch to blank maze - currentState = Displaying::blank; - screenRefreshRequired = true; - std::fill_n(currentCode, sizeof(currentCode), 255); // clear current code in preparation for code entry - } else { - // long tapped not in main watchface; go back to previous state - screenRefreshRequired = true; - } - lastInputTime = time; - motor.RunForDuration(20); - return true; - } else { - screenRefreshRequired = true; - currentState = Displaying::watchface; - // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast - lastInputTime = std::chrono::time_point(); - motor.RunForDuration(20); - return true; - } + + switch (event) { + case Pinetime::Applications::TouchEvents::LongTap: return HandleLongTap(); + case Pinetime::Applications::TouchEvents::Tap: return HandleTap(); + case Pinetime::Applications::TouchEvents::SwipeUp: return HandleSwipe(0); + case Pinetime::Applications::TouchEvents::SwipeRight: return HandleSwipe(1); + case Pinetime::Applications::TouchEvents::SwipeDown: return HandleSwipe(2); + case Pinetime::Applications::TouchEvents::SwipeLeft: return HandleSwipe(3); + default: return false; // only handle swipe events } - // handle swipes to input code on blank screen - if (currentState != Displaying::watchface) { - uint8_t swipeDir = 255; - switch(event) { - case Pinetime::Applications::TouchEvents::SwipeUp: swipeDir = 0; break; - case Pinetime::Applications::TouchEvents::SwipeRight: swipeDir = 1; break; - case Pinetime::Applications::TouchEvents::SwipeDown: swipeDir = 2; break; - case Pinetime::Applications::TouchEvents::SwipeLeft: swipeDir = 3; break; - default: return false; // only handle swipe events - } - for (int i = sizeof(currentCode)-1; i > 0; i--) {currentCode[i] = currentCode[i-1];} - currentCode[0] = swipeDir; - // check if valid code has been entered on non-main screen - // structure also has the effect that if code gets entered while on the destination page, it doesn't refresh. - Displaying newState = currentState; - if (std::memcmp(currentCode, lossCode, sizeof(lossCode)) == 0) {newState = Displaying::loss;} // loss - else if (std::memcmp(currentCode, amogusCode, sizeof(amogusCode)) == 0) {newState = Displaying::amogus;} // amogus - else if (std::memcmp(currentCode, autismCode, sizeof(autismCode)) == 0) {newState = Displaying::autismcreature;} // autismcreature/tbh - else if (std::memcmp(currentCode, foxCode, sizeof(foxCode)) == 0) {newState = Displaying::foxgame;} // foxxo game - else if (std::memcmp(currentCode, reminderCode, sizeof(reminderCode)) == 0) {newState = Displaying::reminder;} // reminder - else if (std::memcmp(currentCode, pinetimeCode, sizeof(pinetimeCode)) == 0) {newState = Displaying::pinetime;} // pinetime logo - if (newState != currentState) { - currentState = newState; +} + + +bool WatchFaceMaze::HandleLongTap() { + if (currentState == Displaying::watchface) { + // On watchface; either refresh maze or go to blank state + if (lastInputTime + std::chrono::milliseconds(2500) > realTime) { + // long tapped twice in sequence; switch to blank maze + currentState = Displaying::blank; + screenRefreshRequired = true; + std::fill_n(currentCode, sizeof(currentCode), 255); // clear current code in preparation for code entry + } else { + // long tapped not in main watchface; go back to previous state screenRefreshRequired = true; - motor.RunForDuration(10); } + lastInputTime = realTime; + motor.RunForDuration(20); + return true; + + } else { + // Not on watchface; go back to main watchface + screenRefreshRequired = true; + currentState = Displaying::watchface; + // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast + lastInputTime = std::chrono::time_point(); + motor.RunForDuration(20); return true; } +} + + +bool WatchFaceMaze::HandleTap() { return false; } +bool WatchFaceMaze::HandleSwipe(uint8_t direction) { + // Don't handle any swipes on watchface + if (currentState == Displaying::watchface) return false; + + // Add the new direction to the swipe list, dropping the last item + for (int i = sizeof(currentCode)-1; i > 0; i--) {currentCode[i] = currentCode[i-1];} + currentCode[0] = direction; + + // check if valid code has been entered + // this structure also has the effect that if code gets entered while on the requested page, it doesn't refresh. + Displaying newState = currentState; + if (std::memcmp(currentCode, lossCode, sizeof(lossCode)) == 0) {newState = Displaying::loss;} // loss + else if (std::memcmp(currentCode, amogusCode, sizeof(amogusCode)) == 0) {newState = Displaying::amogus;} // amogus + else if (std::memcmp(currentCode, autismCode, sizeof(autismCode)) == 0) {newState = Displaying::autismcreature;} // autismcreature/tbh + else if (std::memcmp(currentCode, foxCode, sizeof(foxCode)) == 0) {newState = Displaying::foxgame;} // foxxo game + else if (std::memcmp(currentCode, reminderCode, sizeof(reminderCode)) == 0) {newState = Displaying::reminder;} // reminder + else if (std::memcmp(currentCode, pinetimeCode, sizeof(pinetimeCode)) == 0) {newState = Displaying::pinetime;} // pinetime logo + + // only request a screen refresh if state has been updated + if (newState != currentState) { + currentState = newState; + screenRefreshRequired = true; + motor.RunForDuration(10); + } + return true; +} + + +// Put the current time onto the maze. Acts as a seed. void WatchFaceMaze::PutNumbers() { uint8_t hours = dateTimeController.Hours(); uint8_t minutes = dateTimeController.Minutes(); + // modify hours to account for 12 hour format if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { if (hours == 0) hours = 12; @@ -271,6 +288,7 @@ void WatchFaceMaze::PutNumbers() { maze.pasteMazeSeed(18, 15, 23, 22, am); } } + // put numbers on screen // top left: hours major digit maze.pasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); @@ -282,11 +300,40 @@ void WatchFaceMaze::PutNumbers() { maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); } + +// seeds the maze with whatever the current state needs +void WatchFaceMaze::SeedMaze() { + switch (currentState) { + case Displaying::watchface: + PutNumbers(); break; + case Displaying::blank: { // seed maze with 4 tiles + const uint8_t seed[1] = {0xD5}; + const int randx = prng.rand(0, 20); + const int randy = prng.rand(3, 20); + maze.pasteMazeSeed(randx, randy, randx + 3, randy, seed); + break; + } + case Displaying::loss: + maze.pasteMazeSeed(2, 2, 22, 21, loss); break; + case Displaying::amogus: + maze.pasteMazeSeed(3, 0, 21, 23, amogus); break; + case Displaying::autismcreature: + maze.pasteMazeSeed(0, 2, 11, 22, autismcreature); break; + case Displaying::foxgame: + maze.pasteMazeSeed(0, 1, 23, 22, foxgame); break; + case Displaying::reminder: + maze.pasteMazeSeed(0, 3, 23, 19, reminder); break; + case Displaying::pinetime: + maze.pasteMazeSeed(2, 0, 21, 23, pinetime); break; + } +} + + // goes through the maze, finds disconnected segments and connects them void WatchFaceMaze::ForceValidMaze() { - // flood fill - // this function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in this program) - // initialize x and y to bottom right + // crude maze-optimized flood fill: follow a path until can't move any more, then find some other location to follow from. repeat. + // this function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in normal control flow) + // initialize cursor x and y to bottom right int x = Maze::WIDTH-1, y = Maze::HEIGHT - 1; while (true) { ForceValidMazeLoop: @@ -302,12 +349,14 @@ void WatchFaceMaze::ForceValidMaze() { for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + // if tile to the left is of a different traversal state (is traversed boundary) if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { // if found boundary AND can get to it, just continue working from here if (maze.getSide(proposedx, proposedy, TileAttr::left) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} pokeLocationCount++; } + // if tile to up is of a different traversal state (is traversed boundary) if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { // if found boundary AND can get to it, just continue working from here @@ -316,61 +365,82 @@ void WatchFaceMaze::ForceValidMaze() { } } } - // if no poke locations found, maze is finished + // finished scanning maze; there are no locations the cursor can be placed for it to continue scanning + + // if there are no walls that can be poked through to increase reachable area, maze is finished if (pokeLocationCount == 0) {return;} - // if execution gets here, then no valid position to start filling from was found. need to poke a hole. + + // if execution gets here, need to poke a hole. // choose a random poke location to poke a hole through. pokeLocationCount is now used as an index pokeLocationCount = prng.rand(1, pokeLocationCount); for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { + // pretty much a copy of the previous code which FINDS poke locations, but now with the goal of actually doing the poking bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { pokeLocationCount--; + // found the target poke location, poke and loop if (pokeLocationCount == 0) { maze.setSide(proposedx, proposedy, TileAttr::left, false); x = proposedx, y = proposedy; - continue; + goto ForceValidMazeLoop; // continue OUTSIDE loop } } + // if tile to up is of a different traversal state (is traversed boundary) if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { pokeLocationCount--; + // found the target poke location, poke and loop if (pokeLocationCount == 0) { maze.setSide(proposedx, proposedy, TileAttr::up, false); x = proposedx, y = proposedy; - continue; + goto ForceValidMazeLoop; // continue processing } } } } } + // done poking a hole in the maze to expand the reachable area } } + +// Clear maze +void WatchFaceMaze::InitializeMaze() { + maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); +} + + +// Generates the maze around whatever it was seeded with void WatchFaceMaze::GenerateMaze() { - int x, y, oldx, oldy; + int x, y; // task should only run for 3/4 the time it takes for the task to refresh. // Will go over; only checks once it's finished with current line. It won't go too far over though. auto maxGenTarget = dateTimeController.CurrentDateTime() + std::chrono::milliseconds((taskRefresh->period*3)/4); + while (true) { - // FIND POSITION TO START BRANCH FROM + // find position to start generating a path from for (uint8_t i = 0; i < 30; i++) { x = prng.rand(0, Maze::WIDTH-1); y = prng.rand(0, Maze::HEIGHT-1); if (maze.getSide(x,y, TileAttr::flagempty)) {break;} // found solution tile if (i == 29) { // failed all 30 attempts (this is inside the for loop for 'organization') - // find solution tile slowly but guaranteed + // find solution tile slowly but guaranteed (scan over entire field and choose random valid tile) int count = 0; + // count number of valid tiles for (int j = 0; j < Maze::WIDTH*Maze::HEIGHT; j++) {if (maze.getSide(j, TileAttr::flagempty)) {count++;}} + + // if no valid tiles are left, maze is done if (count == 0) { - // all tiles filled; maze gen done pausedGeneration = false; return; } - // if maze gen not done, select random index from valid tiles to start from + + // if execution gets here then maze gen is not done. select random index from valid tiles to start from // 'count' is now used as an index count = prng.rand(1, count); for (int j = 0; j < Maze::WIDTH*Maze::HEIGHT; j++) { @@ -384,96 +454,115 @@ void WatchFaceMaze::GenerateMaze() { } } // function now has a valid position a maze line can start from in x and y - oldx = -1, oldy = -1; - // GENERATE A SINGLE PATH - uint8_t direction; // which direction the cursor moved in + GeneratePath(x, y); + + // if generating paths took too long, suspend it + if (dateTimeController.CurrentDateTime() > maxGenTarget) { + pausedGeneration = true; + return; + } + } + // execution never gets here! it returns earlier in the function. +} + + +void WatchFaceMaze::GeneratePath(int x, int y) { + int oldx = -1, oldy = -1; + uint8_t direction = -1; // which direction the cursor moved in + while (true) { + // set current tile to reflect that it's been worked on + maze.setSide(x, y, TileAttr::flagempty, false); // no longer empty + maze.setSide(x, y, TileAttr::flaggen, true); // in generation + oldx = x, oldy = y; // used in backtracking + + // move to next tile + // the if statements are very scuffed, but they prevent backtracking. while (true) { - maze.setSide(x, y, TileAttr::flagempty, false); // no longer empty - maze.setSide(x, y, TileAttr::flaggen, true); // in generation - oldx = x, oldy = y; - // move to next tile - // this is very scuffed, but it prevents backtracking. - while (true) { - switch (direction = prng.rand(0,3)) { - case 0: // moved up - if (y <= 0 || !maze.getSide(x,y,TileAttr::up)) {continue;} - y -= 1; break; - case 1: // moved left - if (x <= 0 || !maze.getSide(x,y,TileAttr::left)) {continue;} - x -= 1; break; - case 2: // moved down - if (y >= Maze::HEIGHT-1 || !maze.getSide(x,y,TileAttr::down)) {continue;} - y += 1; break; - case 3: // moved right - if (x >= Maze::WIDTH-1 || !maze.getSide(x,y,TileAttr::right)) {continue;} - x += 1; break; - } + switch (direction = prng.rand(0, 3)) { + case 0: // moved up + if (y <= 0 || !maze.getSide(x, y, TileAttr::up)) {continue;} + y -= 1; + break; + case 1: // moved left + if (x <= 0 || !maze.getSide(x, y, TileAttr::left)) {continue;} + x -= 1; + break; + case 2: // moved down + if (y >= Maze::HEIGHT - 1 || !maze.getSide(x, y, TileAttr::down)) {continue;} + y += 1; + break; + case 3: // moved right + if (x >= Maze::WIDTH - 1 || !maze.getSide(x, y, TileAttr::right)) {continue;} + x += 1; + break; + default: // invalid + std::abort(); + } + break; + } + + // moved to next tile, check if looped in on self + if (!maze.getSide(x, y, TileAttr::flaggen)) { + // did NOT loop in on self, simply remove wall and move on + switch (direction) { + case 0: maze.setSide(x, y, TileAttr::down, false); break; // moved up + case 1: maze.setSide(x, y, TileAttr::right, false); break; // moved left + case 2: maze.setSide(x, y, TileAttr::up, false); break; // moved down + case 3: maze.setSide(x, y, TileAttr::left, false); break; // moved right + } + // if attached to main maze, path finished generating + if (!maze.getSide(x, y, TileAttr::flagempty)) { break; } - // moved to next tile. check if looped in on self - if (!maze.getSide(x, y, TileAttr::flaggen)) { - // did NOT loop in on self, simply remove wall and move on - switch (direction) { - case 0: // moved up - maze.setSide(x,y,TileAttr::down, false); break; - case 1: // moved left - maze.setSide(x,y,TileAttr::right, false); break; - case 2: // moved down - maze.setSide(x,y,TileAttr::up, false); break; - case 3: // moved right - maze.setSide(x,y,TileAttr::left, false); break; - } - // if attached to main maze, path finished generating - if (!maze.getSide(x, y, TileAttr::flagempty)) {break;} - } else { - // DID loop in on self, track down and eliminate loop - // targets are the coordinates of where it needs to backtrack to - int targetx = x, targety = y; - x = oldx, y = oldy; - while (x != targetx || y != targety) { - if (y > 0 && (maze.getSide(x, y, TileAttr::up) == false)) { // backtrack up - maze.setSide(x, y, TileAttr::up, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); - y -= 1; - } else if (x > 0 && (maze.getSide(x, y, TileAttr::left) == false)) { // backtrack left - maze.setSide(x, y, TileAttr::left, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); - x -= 1; - } else if (y < Maze::HEIGHT-1 && (maze.getSide(x, y, TileAttr::down) == false)) { // backtrack down - maze.setSide(x, y, TileAttr::down, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); - y += 1; - } else if (x < Maze::WIDTH && (maze.getSide(x, y, TileAttr::right) == false)) { // backtrack right - maze.setSide(x, y, TileAttr::right, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); - x += 1; - } else { - // bad backtrack; die - std::abort(); - } + + } else { + // DID loop in on self, track down and eliminate loop + // targets are the coordinates of where it needs to backtrack to + int targetx = x, targety = y; + x = oldx, y = oldy; + while (x != targetx || y != targety) { + if (y > 0 && (maze.getSide(x, y, TileAttr::up) == false)) { // backtrack up + maze.setSide(x, y, TileAttr::up, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + y -= 1; + } else if (x > 0 && (maze.getSide(x, y, TileAttr::left) == false)) { // backtrack left + maze.setSide(x, y, TileAttr::left, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + x -= 1; + } else if (y < Maze::HEIGHT - 1 && (maze.getSide(x, y, TileAttr::down) == false)) { // backtrack down + maze.setSide(x, y, TileAttr::down, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + y += 1; + } else if (x < Maze::WIDTH && (maze.getSide(x, y, TileAttr::right) == false)) { // backtrack right + maze.setSide(x, y, TileAttr::right, true); + maze.setSide(x, y, TileAttr::flaggen, false); + maze.setSide(x, y, TileAttr::flagempty, true); + x += 1; + } else { + // bad backtrack; die + std::abort(); } } } - // mark all tiles as finalized and not in generation by removing ALL flaggen's - maze.fill(0, MazeTile::FLAGGENMASK); - if (dateTimeController.CurrentDateTime() > maxGenTarget) { - pausedGeneration = true; - return; - } + // done processing one step, now do it again! } - // execution never gets here! it returns earlier in the function. + // finished generating the entire path + // mark all tiles as finalized and not in generation by removing ALL flaggen's + maze.fill(0, MazeTile::FLAGGENMASK); } + void WatchFaceMaze::DrawMaze() { // this used to be nice code, but it was retrofitted to print offset by 1 pixel for a fancy border. // I'm not proud of the logic but it works. lv_area_t area; - lv_color_t *curbuf = buf1; - // print horizontal lines + lv_color_t *curbuf = buf1; // currently active buffer. Flips between buf1 and buf2 to give time for DMA to finish. + + // Print horizontal lines + // This doesn't bother with corners, those just get overwritten by the vertical lines area.x1 = 1; area.x2 = 238; for (int y = 1; y < Maze::HEIGHT; y++) { @@ -488,7 +577,8 @@ void WatchFaceMaze::DrawMaze() { lvgl.FlushDisplay(&area, curbuf); curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer } - // print vertical lines + + // Print vertical lines area.y1 = 1; area.y2 = 238; for (int x = 1; x < Maze::WIDTH; x++) { @@ -498,7 +588,7 @@ void WatchFaceMaze::DrawMaze() { if (curblock.getUp() || curblock.getLeft() || maze.get(x-1,y).getUp() || maze.get(x,y-1).getLeft()) {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_WHITE);} else {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_BLACK);} - + // handle actual wall segments if (curblock.getLeft()) {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_WHITE);} else {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_BLACK);} } @@ -508,7 +598,9 @@ void WatchFaceMaze::DrawMaze() { lvgl.FlushDisplay(&area, &curbuf[4]); curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer } - // print borders + + // Print borders + // don't need to worry about switching buffers here since buffer contents aren't changing std::fill_n(curbuf, 240, LV_COLOR_GRAY); for (int i = 0; i < 4; i++) { if (i==0) {area.x1=0; area.x2=239; area.y1=0; area.y2=0; } // top diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 0d09fec2ed..ad70015610 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -132,14 +132,28 @@ namespace Pinetime { bool OnButtonPushed() override; private: + // Functions related to touching the screen, for better separation of processes + bool HandleLongTap(); + bool HandleTap(); + bool HandleSwipe(uint8_t direction); + + // Seed the maze with whatever the currentState dictates should be shown + void SeedMaze(); + // Put numbers onto the screen. Acts as a seed to generate from. void PutNumbers(); + // Very simple function which just resets the maze to what is considered blank + void InitializeMaze(); + // Generate the maze around whatever the maze was seeded with. // MAZE MUST BE SEEDED ELSE ALL YOU'LL GENERATE IS AN INFINITE LOOP! // If seed has disconnected components, maze will not be perfect. void GenerateMaze(); + // Generates a single path starting at the provided x,y coords + void GeneratePath(int x, int y); + // If the maze has any disconnected components (such as if seeded with multiple disconnected blocks), // poke holes to force all components to be connected. void ForceValidMaze(); From 64d1b84f41823996785f71aa54a0a8e611541a34 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sun, 20 Oct 2024 02:54:20 -0500 Subject: [PATCH 08/23] Rename PutNumbers and adjust AMPM display --- src/displayapp/screens/WatchFaceMaze.cpp | 25 +++++++++++------------- src/displayapp/screens/WatchFaceMaze.h | 8 ++++---- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 93feeb73da..86dccd3da4 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -273,8 +273,8 @@ bool WatchFaceMaze::HandleSwipe(uint8_t direction) { } -// Put the current time onto the maze. Acts as a seed. -void WatchFaceMaze::PutNumbers() { +// Put time and date info on the screen. +void WatchFaceMaze::PutTimeDate() { uint8_t hours = dateTimeController.Hours(); uint8_t minutes = dateTimeController.Minutes(); @@ -282,22 +282,19 @@ void WatchFaceMaze::PutNumbers() { if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { if (hours == 0) hours = 12; if (hours > 12) { - maze.pasteMazeSeed(18, 15, 23, 22, pm); + maze.pasteMazeSeed(18, 15, 22, 22, pm); hours -= 12; } else { - maze.pasteMazeSeed(18, 15, 23, 22, am); + maze.pasteMazeSeed(18, 15, 22, 22, am); } } - // put numbers on screen - // top left: hours major digit - maze.pasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); - // top right: hours minor digit - maze.pasteMazeSeed(10, 1, 15, 10, numbers[hours % 10]); - // bottom left: minutes major digit - maze.pasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); - // bottom right: minutes minor digit - maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); + // put time on screen + maze.pasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); // top left: hours major digit + maze.pasteMazeSeed(10, 1, 15, 10, numbers[hours % 10]); // top right: hours minor digit + maze.pasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); // bottom left: minutes major digit + maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); // bottom right: minutes minor digit + } @@ -305,7 +302,7 @@ void WatchFaceMaze::PutNumbers() { void WatchFaceMaze::SeedMaze() { switch (currentState) { case Displaying::watchface: - PutNumbers(); break; + PutTimeDate(); break; case Displaying::blank: { // seed maze with 4 tiles const uint8_t seed[1] = {0xD5}; const int randx = prng.rand(0, 20); diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index ad70015610..061624e652 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -140,8 +140,8 @@ namespace Pinetime { // Seed the maze with whatever the currentState dictates should be shown void SeedMaze(); - // Put numbers onto the screen. Acts as a seed to generate from. - void PutNumbers(); + // Put time and date onto the screen. Acts as a seed to generate from. + void PutTimeDate(); // Very simple function which just resets the maze to what is considered blank void InitializeMaze(); @@ -207,8 +207,8 @@ namespace Pinetime { {0xD5,0x58,0x00,0xFF,0x8F,0xF8,0xFF,0x0F,0xE3,0xFE,0x3F,0xC3,0xF8,0xFF,0x8F}, {0xF5,0x7C,0x01,0x8F,0x88,0xF0,0x84,0x3E,0x01,0xC3,0x88,0xF8,0x85,0x0E,0x03}, {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x0E,0x00,0xFF,0x8F,0xF8,0xF5,0x0E,0x03}}; - constexpr static uint8_t am[12] /*6x8*/ = {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x08,0x00,0x8F,0x88,0xF8}; - constexpr static uint8_t pm[12] /*6x8*/ = {0xD5,0x78,0x01,0x8F,0x88,0xF8,0x85,0x08,0x03,0x8F,0xF8,0xFF}; + constexpr static uint8_t am[10] /*5x8*/ = {0xF5,0xF0,0x18,0xE2,0x38,0x84,0x20,0x08,0xE2,0x38}; + constexpr static uint8_t pm[10] /*5x8*/ = {0xD5,0xE0,0x18,0xE2,0x38,0x84,0x20,0x38,0xFE,0x3F}; // Used for swipe sequences for easter eggs. // currentState is what is being displayed. It's generally categorized into "watchface" and "not watchface". From 2123db09d6d1ef57f9adc7388f0832b96c37a1bb Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sun, 20 Oct 2024 02:54:36 -0500 Subject: [PATCH 09/23] Fix autism creature display error --- src/displayapp/screens/WatchFaceMaze.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 86dccd3da4..540356d720 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -315,7 +315,7 @@ void WatchFaceMaze::SeedMaze() { case Displaying::amogus: maze.pasteMazeSeed(3, 0, 21, 23, amogus); break; case Displaying::autismcreature: - maze.pasteMazeSeed(0, 2, 11, 22, autismcreature); break; + maze.pasteMazeSeed(0, 2, 23, 22, autismcreature); break; case Displaying::foxgame: maze.pasteMazeSeed(0, 1, 23, 22, foxgame); break; case Displaying::reminder: From 157939697d9254bc894e27a236841a332cad9149 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sun, 20 Oct 2024 04:59:53 -0500 Subject: [PATCH 10/23] Add battery and BLE indicators --- src/displayapp/screens/WatchFaceMaze.cpp | 104 +++++++++++++++++++---- src/displayapp/screens/WatchFaceMaze.h | 27 +++++- 2 files changed, 111 insertions(+), 20 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 540356d720..669d2519a6 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -133,10 +133,14 @@ void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[] WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, Controllers::DateTime& dateTimeController, Controllers::Settings& settingsController, - Controllers::MotorController& motor) + Controllers::MotorController& motor, + const Controllers::Battery& batteryController, + const Controllers::Ble& bleController) : dateTimeController {dateTimeController}, settingsController {settingsController}, motor {motor}, + batteryController {batteryController}, + bleController {bleController}, lvgl {lvgl}, maze {Maze()}, prng {MazeRNG()} { @@ -178,10 +182,22 @@ void WatchFaceMaze::Refresh() { // only draw once maze is fully generated (not paused) if (!pausedGeneration) { ForceValidMaze(); + if (currentState != Displaying::watchface) {ClearIndicators();} DrawMaze(); screenRefreshRequired = false; + // if switched to watchface, also add indicators for BLE and battery + if (currentState == Displaying::watchface) { + UpdateBatteryDisplay(true); + UpdateBleDisplay(true); + } } } + + // update battery and ble displays if on main watchface + if (currentState == Displaying::watchface) { + UpdateBatteryDisplay(false); + UpdateBleDisplay(false); + } } // allow pushing the button to go back to the watchface @@ -295,6 +311,8 @@ void WatchFaceMaze::PutTimeDate() { maze.pasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); // bottom left: minutes major digit maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); // bottom right: minutes minor digit + // reserve some space at the top right to put the battery and BLE indicators there + maze.pasteMazeSeed(21, 0, 23, 2, indicatorSpace); } @@ -304,10 +322,9 @@ void WatchFaceMaze::SeedMaze() { case Displaying::watchface: PutTimeDate(); break; case Displaying::blank: { // seed maze with 4 tiles - const uint8_t seed[1] = {0xD5}; const int randx = prng.rand(0, 20); const int randy = prng.rand(3, 20); - maze.pasteMazeSeed(randx, randy, randx + 3, randy, seed); + maze.pasteMazeSeed(randx, randy, randx + 3, randy, blankseed); break; } case Displaying::loss: @@ -556,7 +573,7 @@ void WatchFaceMaze::DrawMaze() { // this used to be nice code, but it was retrofitted to print offset by 1 pixel for a fancy border. // I'm not proud of the logic but it works. lv_area_t area; - lv_color_t *curbuf = buf1; // currently active buffer. Flips between buf1 and buf2 to give time for DMA to finish. + activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer, who knows if the buffer was used just before this // Print horizontal lines // This doesn't bother with corners, those just get overwritten by the vertical lines @@ -564,15 +581,15 @@ void WatchFaceMaze::DrawMaze() { area.x2 = 238; for (int y = 1; y < Maze::HEIGHT; y++) { for (int x = 0; x < Maze::WIDTH; x++) { - if (maze.get(x, y).getUp()) {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_WHITE);} - else {std::fill_n(&curbuf[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK);} + if (maze.get(x, y).getUp()) {std::fill_n(&activeBuffer[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_WHITE);} + else {std::fill_n(&activeBuffer[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK);} } - std::copy_n(curbuf, 238, &curbuf[238]); + std::copy_n(activeBuffer, 238, &activeBuffer[238]); area.y1 = Maze::TILESIZE * y - 1; area.y2 = Maze::TILESIZE * y; lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); - lvgl.FlushDisplay(&area, curbuf); - curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer + lvgl.FlushDisplay(&area, activeBuffer); + activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer } // Print vertical lines @@ -583,28 +600,81 @@ void WatchFaceMaze::DrawMaze() { MazeTile curblock = maze.get(x,y); // handle corners: if any of the touching lines are present, add corner. else leave it black if (curblock.getUp() || curblock.getLeft() || maze.get(x-1,y).getUp() || maze.get(x,y-1).getLeft()) - {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_WHITE);} - else {std::fill_n(&curbuf[y*Maze::TILESIZE*2], 4, LV_COLOR_BLACK);} + {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2], 4, LV_COLOR_WHITE);} + else {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2], 4, LV_COLOR_BLACK);} // handle actual wall segments - if (curblock.getLeft()) {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_WHITE);} - else {std::fill_n(&curbuf[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_BLACK);} + if (curblock.getLeft()) {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_WHITE);} + else {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_BLACK);} } area.x1 = Maze::TILESIZE * x - 1; area.x2 = Maze::TILESIZE * x; lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); - lvgl.FlushDisplay(&area, &curbuf[4]); - curbuf = (curbuf==buf1) ? buf2 : buf1; // switch buffer + lvgl.FlushDisplay(&area, &activeBuffer[4]); + activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer } // Print borders // don't need to worry about switching buffers here since buffer contents aren't changing - std::fill_n(curbuf, 240, LV_COLOR_GRAY); + std::fill_n(activeBuffer, 240, LV_COLOR_GRAY); for (int i = 0; i < 4; i++) { if (i==0) {area.x1=0; area.x2=239; area.y1=0; area.y2=0; } // top else if (i==1) {area.x1=0; area.x2=239; area.y1=239; area.y2=239;} // bottom else if (i==2) {area.x1=0; area.x2=0; area.y1=0; area.y2=239;} // left else if (i==3) {area.x1=239; area.x2=239; area.y1=0; area.y2=239;} // right lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); - lvgl.FlushDisplay(&area, curbuf); + lvgl.FlushDisplay(&area, activeBuffer); + } +} + + +void WatchFaceMaze::UpdateBatteryDisplay(bool forceRedraw) { + batteryPercent = batteryController.PercentRemaining(); + charging = batteryController.IsCharging(); + if (forceRedraw || batteryPercent.IsUpdated() || charging.IsUpdated()) { + // need to redraw battery stuff + activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer + + // number of pixels between top of indicator and fill line. rounds up, so 0% is 24px but 1% is 23px + lv_area_t area = {223,3,236,26}; + uint8_t fillLevel = 24 - ((uint16_t)(batteryPercent.Get()) * 24) / 100; + + // gray/green if not charging, blue-gray/aqua if charging + std::fill_n(activeBuffer, fillLevel*14, (charging.Get() ? LV_COLOR_MAKE(0x80,0x80,0xC0) : LV_COLOR_GRAY)); + std::fill_n((activeBuffer+fillLevel*14), (24-fillLevel)*14, (charging.Get() ? LV_COLOR_MAKE(0,0xC0,0x80) : LV_COLOR_GREEN)); + + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); + } +} + + +void WatchFaceMaze::UpdateBleDisplay(bool forceRedraw) { + bleConnected = bleController.IsConnected(); + if (forceRedraw || bleConnected.IsUpdated()) { + // need to redraw BLE indicator + activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer + + lv_area_t area = {213,3,216,26}; + std::fill_n(activeBuffer, 96, (bleConnected.Get() ? LV_COLOR_BLUE : LV_COLOR_GRAY)); + + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); } +} + + +void WatchFaceMaze::ClearIndicators() { + activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer + lv_area_t area; + std::fill_n(activeBuffer, 24*14, LV_COLOR_BLACK); + + // battery indicator + area.x1=223; area.y1=3; area.x2=236; area.y2=26; + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); + + // BLE indicator + area.x1=213; area.y1=3; area.x2=216; area.y2=26; + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); } \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 061624e652..79a353885e 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -123,7 +123,12 @@ namespace Pinetime { // The watchface itself class WatchFaceMaze : public Screen { public: - WatchFaceMaze(Pinetime::Components::LittleVgl&, Controllers::DateTime&, Controllers::Settings&, Controllers::MotorController&); + WatchFaceMaze(Pinetime::Components::LittleVgl&, + Controllers::DateTime&, + Controllers::Settings&, + Controllers::MotorController&, + const Controllers::Battery&, + const Controllers::Ble&); ~WatchFaceMaze() override; // Functions required for app operation. @@ -161,12 +166,19 @@ namespace Pinetime { // Draws the maze to the screen. void DrawMaze(); + // Draw the indicators at the top right. + void UpdateBatteryDisplay(bool forceRedraw); + void UpdateBleDisplay(bool forceRedraw); + void ClearIndicators(); + // Stuff necessary for interacting with the OS and whatnot lv_task_t* taskRefresh; Controllers::DateTime& dateTimeController; Controllers::Settings& settingsController; Controllers::MotorController& motor; + const Controllers::Battery& batteryController; + const Controllers::Ble& bleController; Components::LittleVgl& lvgl; // Maze and internal RNG (so it doesn't mess with other things by reseeding the regular C RNG provider) @@ -181,10 +193,17 @@ namespace Pinetime { Utility::DirtyValue> currentDateTime {}; std::chrono::time_point realTime {}; + // Indicator stuff + Utility::DirtyValue batteryPercent; + Utility::DirtyValue charging; + Utility::DirtyValue bleConnected; + // Buffers for use during printing. There's two it flips between because if there was only one, // it would start being overwritten before the DMA finishes, so it'd corrupt parts of the display. + // activeBuffer is, well, the currently active one. Flip with `activeBuffer = (activeBuffer==buf1) ? buf2 : buf1;` lv_color_t buf1[480]; lv_color_t buf2[480]; + lv_color_t *activeBuffer = buf1; // All concerning the printing of the screen. If screenRefreshRequired is set and screenRefreshTargetTime is // greater than the current time, it waits until that target time before refreshing. Otherwise, it refreshes @@ -209,6 +228,8 @@ namespace Pinetime { {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x0E,0x00,0xFF,0x8F,0xF8,0xF5,0x0E,0x03}}; constexpr static uint8_t am[10] /*5x8*/ = {0xF5,0xF0,0x18,0xE2,0x38,0x84,0x20,0x08,0xE2,0x38}; constexpr static uint8_t pm[10] /*5x8*/ = {0xD5,0xE0,0x18,0xE2,0x38,0x84,0x20,0x38,0xFE,0x3F}; + constexpr static uint8_t blankseed[1] /*4x1*/ = {0xD5}; + constexpr static uint8_t indicatorSpace[3] /*3x3*/ = {0xF6,0x8A,0x00}; // Used for swipe sequences for easter eggs. // currentState is what is being displayed. It's generally categorized into "watchface" and "not watchface". @@ -250,9 +271,9 @@ namespace Pinetime { return new Screens::WatchFaceMaze(controllers.lvgl, controllers.dateTimeController, controllers.settingsController, - controllers.motorController/*, + controllers.motorController, controllers.batteryController, - controllers.bleController*/); + controllers.bleController); }; static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { From 31ced38f138db5a468c3725613ffbeccb34fd8f3 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sun, 20 Oct 2024 13:58:50 -0500 Subject: [PATCH 11/23] Add low battery colors --- src/displayapp/screens/WatchFaceMaze.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 669d2519a6..670d899dbd 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -635,13 +635,25 @@ void WatchFaceMaze::UpdateBatteryDisplay(bool forceRedraw) { activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer // number of pixels between top of indicator and fill line. rounds up, so 0% is 24px but 1% is 23px - lv_area_t area = {223,3,236,26}; uint8_t fillLevel = 24 - ((uint16_t)(batteryPercent.Get()) * 24) / 100; + lv_area_t area = {223,3,236,26}; - // gray/green if not charging, blue-gray/aqua if charging - std::fill_n(activeBuffer, fillLevel*14, (charging.Get() ? LV_COLOR_MAKE(0x80,0x80,0xC0) : LV_COLOR_GRAY)); - std::fill_n((activeBuffer+fillLevel*14), (24-fillLevel)*14, (charging.Get() ? LV_COLOR_MAKE(0,0xC0,0x80) : LV_COLOR_GREEN)); - + // battery body color - green >25%, orange >10%, red <=10%. Charging always makes it yellow. + lv_color_t batteryBodyColor; + if (charging.Get()) {batteryBodyColor = LV_COLOR_YELLOW;} + else if (batteryPercent.Get() > 25) {batteryBodyColor = LV_COLOR_GREEN;} + else if (batteryPercent.Get() > 10) {batteryBodyColor = LV_COLOR_ORANGE;} + else {batteryBodyColor = LV_COLOR_RED;} + + // battery top color (upper gray section) - gray normally, light blue when charging, light red at <=10% charge + lv_color_t batteryTopColor; + if (charging.Get()) {batteryTopColor = LV_COLOR_MAKE(0x80,0x80,0xC0);} + else if (batteryPercent.Get() <= 10) {batteryTopColor = LV_COLOR_MAKE(0xC0,0x80,0x80);} + else {batteryTopColor = LV_COLOR_GRAY;} + + // actually fill the buffer with the chosen colors and print it + std::fill_n(activeBuffer, fillLevel*14, batteryTopColor); + std::fill_n((activeBuffer+fillLevel*14), (24-fillLevel)*14, batteryBodyColor); lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); } From 4efb4b993a6b1023c7feb14386eb7e4a7cfa4398 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Sun, 20 Oct 2024 14:20:09 -0500 Subject: [PATCH 12/23] Make Update*Display functions have default value --- src/displayapp/screens/WatchFaceMaze.cpp | 4 ++-- src/displayapp/screens/WatchFaceMaze.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 670d899dbd..3f29a9d951 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -195,8 +195,8 @@ void WatchFaceMaze::Refresh() { // update battery and ble displays if on main watchface if (currentState == Displaying::watchface) { - UpdateBatteryDisplay(false); - UpdateBleDisplay(false); + UpdateBatteryDisplay(); + UpdateBleDisplay(); } } diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 79a353885e..ee1d2b95bf 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -167,8 +167,8 @@ namespace Pinetime { void DrawMaze(); // Draw the indicators at the top right. - void UpdateBatteryDisplay(bool forceRedraw); - void UpdateBleDisplay(bool forceRedraw); + void UpdateBatteryDisplay(bool forceRedraw = false); + void UpdateBleDisplay(bool forceRedraw = false); void ClearIndicators(); From eba2baf7f4f86a7f2bf46af7a78b1e62e5774f72 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:40:55 -0500 Subject: [PATCH 13/23] Add confetti to autismcreature secret --- src/displayapp/screens/WatchFaceMaze.cpp | 171 ++++++++++++++++++++++- src/displayapp/screens/WatchFaceMaze.h | 50 ++++++- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 3f29a9d951..116f3afe14 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -130,6 +130,68 @@ void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[] +bool ConfettiParticle::step() { + // first apply gravity (if needed), then dampening, then apply velocity to position. + xvel *= DAMPING_FACTOR; + yvel += GRAVITY; + yvel *= DAMPING_FACTOR; + xpos += xvel; + ypos += yvel; + + updateMazeEquiv(); + + // return true if particle is finished (went OOB (ignore top; particle can still fall down)) + return (xpos < 0 || xpos > 240 || ypos > 240); +} + + +void ConfettiParticle::reset(MazeRNG &prng) { + // always start at bottom middle + xpos = 120; + ypos = 240; + + // produces float in range -5 to 5 with resolution of 0.01. very stupid but it works. + // technically 0.00 has 2x chance of being chosen as other values but idc + xvel = (((float)prng.rand(0,500))/100); + if (prng.rand(0,1)) {xvel = -xvel;} + // float -3 to -8.5 (remember up is -y); + yvel = -(((float)prng.rand(200,850))/100); + + updateMazeEquiv(); +} + + +// Probably not pixel perfect because of stuff like the maze border but it's close enough for this purpose +// actually it might be but I don't want to think about it more than necessary +void ConfettiParticle::updateMazeEquiv() { + // calculating tile is easy + tilex = xpos / 10; + tiley = ypos / 10; + + // calculating side is gross + if (tilex%10 > tiley%10) { + // top or right + if (tilex%10 > 10-(tiley%10)) {side = 1;} // right side + else {side = 0;} // top side + } else { + // bottom or left + if (tilex%10 > 10-(tiley%10)) {side = 2;} // bottom side + else {side = 3;} // left side + } + + // and now because I want ONLY unique sides, if it's bottom or right then just change it to the top/left of a neighboring tile + if (side == 1) { // right + tilex++; + side = 3; + } else if (side == 2) { // down + tiley++; + side = 0; + } +} + + + + WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, Controllers::DateTime& dateTimeController, Controllers::Settings& settingsController, @@ -198,6 +260,27 @@ void WatchFaceMaze::Refresh() { UpdateBatteryDisplay(); UpdateBleDisplay(); } + + // deal with confetti + // initialize confetti if tapped on autism creature + if (initConfetti) { + ClearConfetti(); + for (ConfettiParticle &particle : confettiArr) + {particle.reset(prng);} + confettiActive = true; + initConfetti = false; + } + // update confetti if needed + if (confettiActive) { + if (currentState != Displaying::autismcreature) { + // nuke confetti if went to a different display + ClearConfetti(); + confettiActive = false; + } else { + // still on autism creature display, step confetti + ProcessConfetti(); + } + } } // allow pushing the button to go back to the watchface @@ -257,7 +340,11 @@ bool WatchFaceMaze::HandleLongTap() { bool WatchFaceMaze::HandleTap() { - return false; + // confetti must only display on autismcreature + if (currentState != Displaying::autismcreature) {return false;} + // only need to set confettiActive, everything else is handled in functions called by refresh() + initConfetti = true; + return true; } @@ -627,6 +714,51 @@ void WatchFaceMaze::DrawMaze() { } +void WatchFaceMaze::DrawMazeSide(int16_t x, int16_t y, TileAttr side, lv_color_t wallcolor, lv_color_t bgcolor) { + // convert right and down sides to up and left, makes rest of the code easier + if (side == TileAttr::right) { + x++; + side = TileAttr::left; + } else if (side == TileAttr::down) { + y++; + side = TileAttr::up; + } + + // early exit if would print OOB + if ((x == 0 && side == TileAttr::left) || + (x < 0) || + (x >= Maze::WIDTH) || + (y == 0 && side == TileAttr::up) || + (y < 0) || + (y >= Maze::HEIGHT)) + {return;} + + // prepare buffer + activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; + std::fill_n(activeBuffer, 16, maze.getSide(x, y, side) ? wallcolor : bgcolor); + lv_area_t area; + + // figure where to print + if (side == TileAttr::up) { + // drawing top side + area.x1 = 10*x + 1; + area.x2 = 10*x + 8; + area.y1 = 10*y - 1; + area.y2 = 10*y; + } else if (side == TileAttr::left) { + // drawing left side + area.x1 = 10*x - 1; + area.x2 = 10*x; + area.y1 = 10*y + 1; + area.y2 = 10*y + 8; + } + + // print to screen + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); +} + + void WatchFaceMaze::UpdateBatteryDisplay(bool forceRedraw) { batteryPercent = batteryController.PercentRemaining(); charging = batteryController.IsCharging(); @@ -689,4 +821,41 @@ void WatchFaceMaze::ClearIndicators() { area.x1=213; area.y1=3; area.x2=216; area.y2=26; lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); +} + + +void WatchFaceMaze::ClearConfetti() { + // prevent superfluous calls + if (!confettiActive) {return;} + + // clear all particles and reset state + for (const ConfettiParticle &particle : confettiArr) { + DrawMazeSide(particle.tilex, particle.tiley, TileAttr(particle.side), LV_COLOR_WHITE, LV_COLOR_BLACK); + } + confettiActive = false; +} + + +void WatchFaceMaze::ProcessConfetti() { + // and draw all the confetti + // flag "done" stays true if all step() calls stated that the particle was done, otherwise it goes false + bool done = true; + for (ConfettiParticle &particle : confettiArr) { + int16_t oldx = particle.tilex; + int16_t oldy = particle.tiley; + uint8_t oldside = particle.side; + // if any step() calls return false (i.e. not finished), done gets set to false as well + done = particle.step() && done; + // need to redraw? + if (oldx != particle.tilex || oldy != particle.tiley || oldside != particle.side) { + DrawMazeSide(oldx, oldy, TileAttr(oldside), LV_COLOR_WHITE, LV_COLOR_BLACK); + DrawMazeSide(particle.tilex, particle.tiley, TileAttr(particle.side), LV_COLOR_RED, LV_COLOR_MAKE(0x80,0,0)); + } + } + + // handle done flag + // should only set confettiActive to false, since all confetti will have been cleared as it moved out of frame + if (done) { + confettiActive = false; + } } \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index ee1d2b95bf..abaea93205 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -55,7 +55,7 @@ namespace Pinetime { } // Random in range, inclusive on both ends (don't make max=min); return rand()%(max-min+1)+min;} + uint32_t rand(uint32_t min, uint32_t max) {assert(max>=min); return (rand()%(max-min+1))+min;} private: uint64_t state; @@ -114,6 +114,39 @@ namespace Pinetime { + class ConfettiParticle { + public: + // steps the confetti simulation. Importantly, updates the maze equivalent position. + // Returns true if the particle has finished processing, else false. + // Need to save the maze equiv pos elsewhere before stepping to be able to clear the old particle position (if redraw needed). + // I couldn't really figure a better way to do it without saving the old positions in the class itself... + bool step(); + + // reset position and generate new velocity using the passed prng + void reset(MazeRNG &prng); + + // holds the previous maze tile and which side of said tile the particle was drawn on + // used so it doesn't draw EVERY confetti particle every frame, but only when they update + int16_t tilex; + int16_t tiley; + uint8_t side; + + private: + // update the internal store of where the tile should be drawn on screen. Called automatically from step() and reset(). + void updateMazeEquiv(); + + // positions and velocities of particle, in pixels and pixels/step (~50 steps per second) + float xpos; + float ypos; + float xvel; + float yvel; + + // first apply gravity, then apply damping factor, then add velocity to position + static constexpr float GRAVITY = 0.1; // added to yvel every step (remember up is -y) + static constexpr float DAMPING_FACTOR = 0.99; // keep this much of the velocity every step (applied after gravity) + }; + + // What is currently being displayed. // Watchface is normal operation; anything else is an easter egg. Really only used to indicate what @@ -166,11 +199,21 @@ namespace Pinetime { // Draws the maze to the screen. void DrawMaze(); + // Draw a single side. Really only used for confetti, but is generic. + // CAVEAT: If drawing to a wall with no more walls around one of its endpoints (the wall is jutting out like a spike), the + // tip of this 'spike' will not get redrawn despite it maybe being more intuitive if it did. + // Given the use case in this code, I believe it is not worthwhile to fix. + void DrawMazeSide(int16_t x, int16_t y, TileAttr side, lv_color_t wallcolor, lv_color_t bgcolor); + // Draw the indicators at the top right. void UpdateBatteryDisplay(bool forceRedraw = false); void UpdateBleDisplay(bool forceRedraw = false); void ClearIndicators(); + // Draw and generally deal with confetti + void ProcessConfetti(); + void ClearConfetti(); + // Stuff necessary for interacting with the OS and whatnot lv_task_t* taskRefresh; @@ -198,6 +241,11 @@ namespace Pinetime { Utility::DirtyValue charging; Utility::DirtyValue bleConnected; + // Confetti for autism creature + ConfettiParticle confettiArr[20]; // can freely increase/decrease number of particles + bool initConfetti = false; // don't want to touch confettiArr in touch event handler, so use a flag and do it in refresh() + bool confettiActive = false; + // Buffers for use during printing. There's two it flips between because if there was only one, // it would start being overwritten before the DMA finishes, so it'd corrupt parts of the display. // activeBuffer is, well, the currently active one. Flip with `activeBuffer = (activeBuffer==buf1) ? buf2 : buf1;` From 1543ff44957f70d77b72f6cce038f2d08a849dca Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Tue, 22 Oct 2024 23:27:46 -0500 Subject: [PATCH 14/23] Make confetti multicolored --- src/displayapp/screens/WatchFaceMaze.cpp | 10 ++++++++-- src/displayapp/screens/WatchFaceMaze.h | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 116f3afe14..0e2561ca28 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -152,11 +152,17 @@ void ConfettiParticle::reset(MazeRNG &prng) { // produces float in range -5 to 5 with resolution of 0.01. very stupid but it works. // technically 0.00 has 2x chance of being chosen as other values but idc - xvel = (((float)prng.rand(0,500))/100); + xvel = (((float)prng.rand(0,300))/100); if (prng.rand(0,1)) {xvel = -xvel;} // float -3 to -8.5 (remember up is -y); yvel = -(((float)prng.rand(200,850))/100); + // Low 3 bits represent red, green, and blue. Also don't allow all three off or all three on at once. + // Effectively choose any max saturation color except black or white. + const uint8_t color = prng.rand(1,6); + wallcolor = LV_COLOR_MAKE((color&0b001) * 0xFF, ((color&0b010)>>1) * 0xFF, ((color&0b100)>>2) * 0xFF); + bgcolor = LV_COLOR_MAKE((color&0b001) * 0x80, ((color&0b010)>>1) * 0x80, ((color&0b100)>>2) * 0x80); + updateMazeEquiv(); } @@ -849,7 +855,7 @@ void WatchFaceMaze::ProcessConfetti() { // need to redraw? if (oldx != particle.tilex || oldy != particle.tiley || oldside != particle.side) { DrawMazeSide(oldx, oldy, TileAttr(oldside), LV_COLOR_WHITE, LV_COLOR_BLACK); - DrawMazeSide(particle.tilex, particle.tiley, TileAttr(particle.side), LV_COLOR_RED, LV_COLOR_MAKE(0x80,0,0)); + DrawMazeSide(particle.tilex, particle.tiley, TileAttr(particle.side), particle.wallcolor, particle.bgcolor); } } diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index abaea93205..201ae3c18d 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -131,6 +131,9 @@ namespace Pinetime { int16_t tiley; uint8_t side; + lv_color_t wallcolor; + lv_color_t bgcolor; + private: // update the internal store of where the tile should be drawn on screen. Called automatically from step() and reset(). void updateMazeEquiv(); @@ -242,7 +245,9 @@ namespace Pinetime { Utility::DirtyValue bleConnected; // Confetti for autism creature - ConfettiParticle confettiArr[20]; // can freely increase/decrease number of particles + // Infinisim warning: because each confetti moving causes 2 draw calls, this is really slow in Infinisim. Lower if using Infinisim. + constexpr static uint16_t CONFETTI_COUNT = 50; + ConfettiParticle confettiArr[CONFETTI_COUNT]; // can freely increase/decrease number of particles bool initConfetti = false; // don't want to touch confettiArr in touch event handler, so use a flag and do it in refresh() bool confettiActive = false; From 52928f4795d7079b45804d01198590bf21b874ea Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:58:19 -0500 Subject: [PATCH 15/23] Make confetti square rather than wall aligned (and adjust movement parameters) --- src/displayapp/screens/WatchFaceMaze.cpp | 104 ++++++----------------- src/displayapp/screens/WatchFaceMaze.h | 31 +++---- 2 files changed, 36 insertions(+), 99 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 0e2561ca28..820b052d8e 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -131,15 +131,14 @@ void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[] bool ConfettiParticle::step() { - // first apply gravity (if needed), then dampening, then apply velocity to position. + // first apply gravity (only to y), then dampening, then apply velocity to position. xvel *= DAMPING_FACTOR; + xpos += xvel; + yvel += GRAVITY; yvel *= DAMPING_FACTOR; - xpos += xvel; ypos += yvel; - updateMazeEquiv(); - // return true if particle is finished (went OOB (ignore top; particle can still fall down)) return (xpos < 0 || xpos > 240 || ypos > 240); } @@ -151,48 +150,17 @@ void ConfettiParticle::reset(MazeRNG &prng) { ypos = 240; // produces float in range -5 to 5 with resolution of 0.01. very stupid but it works. - // technically 0.00 has 2x chance of being chosen as other values but idc - xvel = (((float)prng.rand(0,300))/100); + // technically 0.00 has 2x chance of being chosen compared to other values but idc + xvel = (((float)prng.rand(0,500))/100); if (prng.rand(0,1)) {xvel = -xvel;} - // float -3 to -8.5 (remember up is -y); - yvel = -(((float)prng.rand(200,850))/100); + + // float -4 to -13 (remember up is -y); + yvel = -(((float)prng.rand(400,1300))/100); // Low 3 bits represent red, green, and blue. Also don't allow all three off or all three on at once. // Effectively choose any max saturation color except black or white. - const uint8_t color = prng.rand(1,6); - wallcolor = LV_COLOR_MAKE((color&0b001) * 0xFF, ((color&0b010)>>1) * 0xFF, ((color&0b100)>>2) * 0xFF); - bgcolor = LV_COLOR_MAKE((color&0b001) * 0x80, ((color&0b010)>>1) * 0x80, ((color&0b100)>>2) * 0x80); - - updateMazeEquiv(); -} - - -// Probably not pixel perfect because of stuff like the maze border but it's close enough for this purpose -// actually it might be but I don't want to think about it more than necessary -void ConfettiParticle::updateMazeEquiv() { - // calculating tile is easy - tilex = xpos / 10; - tiley = ypos / 10; - - // calculating side is gross - if (tilex%10 > tiley%10) { - // top or right - if (tilex%10 > 10-(tiley%10)) {side = 1;} // right side - else {side = 0;} // top side - } else { - // bottom or left - if (tilex%10 > 10-(tiley%10)) {side = 2;} // bottom side - else {side = 3;} // left side - } - - // and now because I want ONLY unique sides, if it's bottom or right then just change it to the top/left of a neighboring tile - if (side == 1) { // right - tilex++; - side = 3; - } else if (side == 2) { // down - tiley++; - side = 0; - } + const uint8_t colorBits = prng.rand(1,6); + color = LV_COLOR_MAKE((colorBits&0b001) * 0xFF, ((colorBits&0b010)>>1) * 0xFF, ((colorBits&0b100)>>2) * 0xFF); } @@ -720,44 +688,21 @@ void WatchFaceMaze::DrawMaze() { } -void WatchFaceMaze::DrawMazeSide(int16_t x, int16_t y, TileAttr side, lv_color_t wallcolor, lv_color_t bgcolor) { - // convert right and down sides to up and left, makes rest of the code easier - if (side == TileAttr::right) { - x++; - side = TileAttr::left; - } else if (side == TileAttr::down) { - y++; - side = TileAttr::up; - } - +void WatchFaceMaze::DrawTileInner(int16_t x, int16_t y, lv_color_t color) { // early exit if would print OOB - if ((x == 0 && side == TileAttr::left) || - (x < 0) || - (x >= Maze::WIDTH) || - (y == 0 && side == TileAttr::up) || - (y < 0) || - (y >= Maze::HEIGHT)) + if (x < 0 || y < 0 || x > Maze::WIDTH-1 || y > Maze::HEIGHT-1) {return;} // prepare buffer activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; - std::fill_n(activeBuffer, 16, maze.getSide(x, y, side) ? wallcolor : bgcolor); + std::fill_n(activeBuffer, 64, color); lv_area_t area; - // figure where to print - if (side == TileAttr::up) { - // drawing top side - area.x1 = 10*x + 1; - area.x2 = 10*x + 8; - area.y1 = 10*y - 1; - area.y2 = 10*y; - } else if (side == TileAttr::left) { - // drawing left side - area.x1 = 10*x - 1; - area.x2 = 10*x; - area.y1 = 10*y + 1; - area.y2 = 10*y + 8; - } + // define bounds + area.x1 = 10*x + 1; + area.x2 = 10*x + 8; + area.y1 = 10*y + 1; + area.y2 = 10*y + 8; // print to screen lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); @@ -836,7 +781,7 @@ void WatchFaceMaze::ClearConfetti() { // clear all particles and reset state for (const ConfettiParticle &particle : confettiArr) { - DrawMazeSide(particle.tilex, particle.tiley, TileAttr(particle.side), LV_COLOR_WHITE, LV_COLOR_BLACK); + DrawTileInner(particle.tileX(), particle.tileY(), LV_COLOR_BLACK); } confettiActive = false; } @@ -847,15 +792,14 @@ void WatchFaceMaze::ProcessConfetti() { // flag "done" stays true if all step() calls stated that the particle was done, otherwise it goes false bool done = true; for (ConfettiParticle &particle : confettiArr) { - int16_t oldx = particle.tilex; - int16_t oldy = particle.tiley; - uint8_t oldside = particle.side; + int16_t oldx = particle.tileX(); + int16_t oldy = particle.tileY(); // if any step() calls return false (i.e. not finished), done gets set to false as well done = particle.step() && done; // need to redraw? - if (oldx != particle.tilex || oldy != particle.tiley || oldside != particle.side) { - DrawMazeSide(oldx, oldy, TileAttr(oldside), LV_COLOR_WHITE, LV_COLOR_BLACK); - DrawMazeSide(particle.tilex, particle.tiley, TileAttr(particle.side), particle.wallcolor, particle.bgcolor); + if (oldx != particle.tileX() || oldy != particle.tileY()) { + DrawTileInner(oldx, oldy, LV_COLOR_BLACK); + DrawTileInner(particle.tileX(), particle.tileY(), particle.color); } } diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 201ae3c18d..050eb0729c 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -118,26 +118,21 @@ namespace Pinetime { public: // steps the confetti simulation. Importantly, updates the maze equivalent position. // Returns true if the particle has finished processing, else false. - // Need to save the maze equiv pos elsewhere before stepping to be able to clear the old particle position (if redraw needed). - // I couldn't really figure a better way to do it without saving the old positions in the class itself... + // Need to save the particle position elsewhere before stepping to be able to clear the old particle position (if redraw needed). + // I couldn't really figure a better way to do it other than saving the old positions in the class itself and that's just awful. bool step(); // reset position and generate new velocity using the passed prng void reset(MazeRNG &prng); - // holds the previous maze tile and which side of said tile the particle was drawn on - // used so it doesn't draw EVERY confetti particle every frame, but only when they update - int16_t tilex; - int16_t tiley; - uint8_t side; + // x and y positions of the particle. Positions are in pixels, so just divide by tile size to get the tile it's in. + int16_t tileX() const {return xpos > 0 ? xpos/((float)Maze::TILESIZE) : -1;} // need positive check else particles can get stuck to left wall + int16_t tileY() const {return ypos/((float)Maze::TILESIZE);} - lv_color_t wallcolor; - lv_color_t bgcolor; + // color of the particle + lv_color_t color; private: - // update the internal store of where the tile should be drawn on screen. Called automatically from step() and reset(). - void updateMazeEquiv(); - // positions and velocities of particle, in pixels and pixels/step (~50 steps per second) float xpos; float ypos; @@ -145,7 +140,7 @@ namespace Pinetime { float yvel; // first apply gravity, then apply damping factor, then add velocity to position - static constexpr float GRAVITY = 0.1; // added to yvel every step (remember up is -y) + static constexpr float GRAVITY = 0.3; // added to yvel every step (remember up is -y) static constexpr float DAMPING_FACTOR = 0.99; // keep this much of the velocity every step (applied after gravity) }; @@ -202,11 +197,9 @@ namespace Pinetime { // Draws the maze to the screen. void DrawMaze(); - // Draw a single side. Really only used for confetti, but is generic. - // CAVEAT: If drawing to a wall with no more walls around one of its endpoints (the wall is jutting out like a spike), the - // tip of this 'spike' will not get redrawn despite it maybe being more intuitive if it did. - // Given the use case in this code, I believe it is not worthwhile to fix. - void DrawMazeSide(int16_t x, int16_t y, TileAttr side, lv_color_t wallcolor, lv_color_t bgcolor); + // Fill in the inside of a maze square. Wall states don't affect this; it never draws in the area where walls go. + // Generic, but only actually used for confetti. + void DrawTileInner(int16_t x, int16_t y, lv_color_t color); // Draw the indicators at the top right. void UpdateBatteryDisplay(bool forceRedraw = false); @@ -246,7 +239,7 @@ namespace Pinetime { // Confetti for autism creature // Infinisim warning: because each confetti moving causes 2 draw calls, this is really slow in Infinisim. Lower if using Infinisim. - constexpr static uint16_t CONFETTI_COUNT = 50; + constexpr static uint16_t CONFETTI_COUNT = 1; ConfettiParticle confettiArr[CONFETTI_COUNT]; // can freely increase/decrease number of particles bool initConfetti = false; // don't want to touch confettiArr in touch event handler, so use a flag and do it in refresh() bool confettiActive = false; From 36a2877766a4acaabf1a7729fedc2522fee45e88 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Wed, 23 Oct 2024 03:09:39 -0500 Subject: [PATCH 16/23] Improve confetti spread by using angles --- src/displayapp/screens/WatchFaceMaze.cpp | 12 ++++++------ src/displayapp/screens/WatchFaceMaze.h | 6 +++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 820b052d8e..6585cea28b 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -149,13 +149,13 @@ void ConfettiParticle::reset(MazeRNG &prng) { xpos = 120; ypos = 240; - // produces float in range -5 to 5 with resolution of 0.01. very stupid but it works. - // technically 0.00 has 2x chance of being chosen compared to other values but idc - xvel = (((float)prng.rand(0,500))/100); - if (prng.rand(0,1)) {xvel = -xvel;} + // velocity in pixels/tick + float velocity = ((float)prng.rand(MIN_START_VELOCITY*100, MAX_START_VELOCITY*100))/100; + // angle, in radians, for going up at the chosen degree angle + float angle = ((float)prng.rand(0,MAX_START_ANGLE*2) - MAX_START_ANGLE + 90) * ((float)3.14159265 / 180); - // float -4 to -13 (remember up is -y); - yvel = -(((float)prng.rand(400,1300))/100); + xvel = std::cos(angle) * velocity * START_X_COMPRESS; + yvel = -std::sin(angle) * velocity; // Low 3 bits represent red, green, and blue. Also don't allow all three off or all three on at once. // Effectively choose any max saturation color except black or white. diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 050eb0729c..a7b17281c9 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -142,6 +142,10 @@ namespace Pinetime { // first apply gravity, then apply damping factor, then add velocity to position static constexpr float GRAVITY = 0.3; // added to yvel every step (remember up is -y) static constexpr float DAMPING_FACTOR = 0.99; // keep this much of the velocity every step (applied after gravity) + static constexpr uint8_t MAX_START_ANGLE = 45; // degrees off from straight vertical a particle can be going when spawned (<90) + static constexpr uint8_t MIN_START_VELOCITY = 5; // minimum velocity a particle can spawn with + static constexpr uint8_t MAX_START_VELOCITY = 14; // maximum velocity a particle can spawn with + static constexpr float START_X_COMPRESS = 1./2.; // multiply X velocity by this value. can give a more concentrated confetti blast. }; @@ -239,7 +243,7 @@ namespace Pinetime { // Confetti for autism creature // Infinisim warning: because each confetti moving causes 2 draw calls, this is really slow in Infinisim. Lower if using Infinisim. - constexpr static uint16_t CONFETTI_COUNT = 1; + constexpr static uint16_t CONFETTI_COUNT = 50; ConfettiParticle confettiArr[CONFETTI_COUNT]; // can freely increase/decrease number of particles bool initConfetti = false; // don't want to touch confettiArr in touch event handler, so use a flag and do it in refresh() bool confettiActive = false; From 169c8a14d2249f5891eb85b4a5fc27778de6e890 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Wed, 23 Oct 2024 03:38:33 -0500 Subject: [PATCH 17/23] Allow inputting a secret code while already on the secret page --- src/displayapp/screens/WatchFaceMaze.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 6585cea28b..3d829d0d27 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -331,8 +331,8 @@ bool WatchFaceMaze::HandleSwipe(uint8_t direction) { currentCode[0] = direction; // check if valid code has been entered - // this structure also has the effect that if code gets entered while on the requested page, it doesn't refresh. - Displaying newState = currentState; + // Displaying::watchface is used here simply as a dummy value, and it will never transition to that + Displaying newState = Displaying::watchface; if (std::memcmp(currentCode, lossCode, sizeof(lossCode)) == 0) {newState = Displaying::loss;} // loss else if (std::memcmp(currentCode, amogusCode, sizeof(amogusCode)) == 0) {newState = Displaying::amogus;} // amogus else if (std::memcmp(currentCode, autismCode, sizeof(autismCode)) == 0) {newState = Displaying::autismcreature;} // autismcreature/tbh @@ -341,10 +341,11 @@ bool WatchFaceMaze::HandleSwipe(uint8_t direction) { else if (std::memcmp(currentCode, pinetimeCode, sizeof(pinetimeCode)) == 0) {newState = Displaying::pinetime;} // pinetime logo // only request a screen refresh if state has been updated - if (newState != currentState) { + if (newState != Displaying::watchface) { currentState = newState; screenRefreshRequired = true; motor.RunForDuration(10); + std::fill_n(currentCode, sizeof(currentCode), 255); // clear code } return true; } From 484ac789c0c5d643a945f0c6034d80878e4aa444 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:05:02 -0500 Subject: [PATCH 18/23] Organize code and improve header comments --- src/displayapp/screens/WatchFaceMaze.cpp | 493 ++++++++++++----------- src/displayapp/screens/WatchFaceMaze.h | 110 ++--- 2 files changed, 321 insertions(+), 282 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 3d829d0d27..ae8876d39f 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -32,43 +32,16 @@ MazeTile Maze::get(int index) { // if out of bounds, does nothing void Maze::set(int x, int y, MazeTile tile) { if (x<0||x>WIDTH||y<0||y>HEIGHT) {return;} - set(y * WIDTH + x, tile); + set((y * WIDTH) + x, tile); } void Maze::set(int index, MazeTile tile) { if (index < 0 || index/2 >= FLATSIZE) {return;} // odd means right (low) nibble, even means left (high) nibble - if (index & 0b1) mazemap[index/2] = (mazemap[index/2] & 0b11110000) | tile.map; - else mazemap[index/2] = (mazemap[index/2] & 0b00001111) | tile.map << 4; + if (index & 0b1) {mazemap[index/2] = (mazemap[index/2] & 0b11110000) | tile.map;} + else {mazemap[index/2] = (mazemap[index/2] & 0b00001111) | tile.map << 4;} } -// only operates on the low 4 bits of the uint8_t. -// only sets the bits from the value that are also on in the mask, rest are left alone -// e.g. existing = 1010, value = 0001, mask = 0011, then result = 1001 -// (mask defaults to 0xFF which keeps all bits) -void Maze::fill(uint8_t value, uint8_t mask) { - value = value & 0b00001111; - value |= value << 4; - - if (mask == 0xFF) { - // did not include a mask - std::fill_n(mazemap, FLATSIZE, value); - - } else { - // included a mask - mask = mask & 0b00001111; - mask |= mask << 4; - value = value & mask; // preprocess mask for value - mask = ~mask; // this mask will be applied to the value - for (uint8_t& mapitem : mazemap) { - mapitem = (mapitem & mask) + value; - } - } -} -inline void Maze::fill(MazeTile tile, uint8_t mask) - {fill(tile.map, mask);} - - // For quickly manipulating. Also allows better abstraction by allowing setting of down and right sides. // Silently does nothing if given invalid values. void Maze::setSide(int index, TileAttr attr, bool value) { @@ -82,7 +55,7 @@ void Maze::setSide(int index, TileAttr attr, bool value) { } } void Maze::setSide(int x, int y, TileAttr attr, bool value) { - setSide(y*WIDTH+x, attr, value); + setSide((y*WIDTH)+x, attr, value); } bool Maze::getSide(int index, TileAttr attr) { switch(attr) { @@ -96,9 +69,37 @@ bool Maze::getSide(int index, TileAttr attr) { return false; } bool Maze::getSide(int x, int y, TileAttr attr) { - return getSide(y*WIDTH+x, attr); + return getSide((y*WIDTH)+x, attr); } + +// only operates on the low 4 bits of the uint8_t. +// only sets the bits from the value that are also on in the mask, rest are left alone +// e.g. existing = 1010, value = 0001, mask = 0011, then result = 1001 +// (mask defaults to 0xFF which keeps all bits) +void Maze::fill(uint8_t value, uint8_t mask) { + value = value & 0b00001111; + value |= value << 4; + + if (mask == 0xFF) { + // did not include a mask + std::fill_n(mazemap, FLATSIZE, value); + + } else { + // included a mask + mask = mask & 0b00001111; + mask |= mask << 4; + value = value & mask; // preprocess mask for value + mask = ~mask; // this mask will be applied to the value + for (uint8_t& mapitem : mazemap) { + mapitem = (mapitem & mask) + value; + } + } +} +inline void Maze::fill(MazeTile tile, uint8_t mask) + {fill(tile.map, mask);} + + // Paste a set of tiles into the given coords. void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]) { // Assumes a maze with empty flags all true, and all walls present @@ -106,7 +107,7 @@ void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[] for (int y = y1; y <= y2; y++) { for (int x = x1; x <= x2; x++) { // working holds the target wall (bit 2 for left wall, bit 1 for up wall) - uint8_t working = (toPaste[flatcoord/4] & (0b11 << ((3-(flatcoord%4))*2))) >> ((3-(flatcoord%4))*2); + const uint8_t working = (toPaste[flatcoord/4] & (0b11 << ((3-(flatcoord%4))*2))) >> ((3-(flatcoord%4))*2); // handle left wall if (!(working & 0b10)) { @@ -150,9 +151,9 @@ void ConfettiParticle::reset(MazeRNG &prng) { ypos = 240; // velocity in pixels/tick - float velocity = ((float)prng.rand(MIN_START_VELOCITY*100, MAX_START_VELOCITY*100))/100; + const float velocity = ((float)prng.rand(MIN_START_VELOCITY*100, MAX_START_VELOCITY*100))/100; // angle, in radians, for going up at the chosen degree angle - float angle = ((float)prng.rand(0,MAX_START_ANGLE*2) - MAX_START_ANGLE + 90) * ((float)3.14159265 / 180); + const float angle = ((float)prng.rand(0,MAX_START_ANGLE*2) - MAX_START_ANGLE + 90) * ((float)3.14159265 / 180); xvel = std::cos(angle) * velocity * START_X_COMPRESS; yvel = -std::sin(angle) * velocity; @@ -194,15 +195,28 @@ WatchFaceMaze::~WatchFaceMaze() { void WatchFaceMaze::Refresh() { - // store time for other functions to use, and update time if needed + // store time for other functions to use. functions called directly from Refresh() can just assume this is accurate. realTime = dateTimeController.CurrentDateTime(); - currentDateTime = std::chrono::time_point_cast(realTime); - // refresh time if either a minute has passed, or screen refresh timer expired. + // handle everything related to refreshing and printing stuff to the screen + HandleMazeRefresh(); + + // handle confetti printing + // yeah it's not very pretty how this is hanging out in the refresh() function but I don't want to modify anything related + // to printing in the touch interrupts + HandleConfetti(); +} + + +void WatchFaceMaze::HandleMazeRefresh() { + // convert time to minutes and update if needed + currentDateTime = std::chrono::time_point_cast(realTime); + + // refresh if generation isn't complete, a minute has passed on the watchface, or the screen refresh timer expired. // if minute rolls over while screenrefresh is required, ignore it. the refresh timer will handle it. if (pausedGeneration || // if generation paused, need to complete it - (currentState == Displaying::watchface && !screenRefreshRequired && currentDateTime.IsUpdated()) || // already on watchface, not waiting for a screen refresh, and time updated - (screenRefreshRequired && realTime > screenRefreshTargetTime)) { // waiting on a refresh + (currentState == Displaying::watchface && !screenRefreshRequired && currentDateTime.IsUpdated()) || // already on watchface, not waiting for a screen refresh, and time updated + (screenRefreshRequired && realTime > screenRefreshTargetTime)) { // waiting on a refresh // if generation wasn't paused (i.e. doing a ground up maze gen), set everything up if (!pausedGeneration) { @@ -212,7 +226,7 @@ void WatchFaceMaze::Refresh() { SeedMaze(); } - // always need to run GenerateMaze(). + // always need to run GenerateMaze() when refreshing. This is a maze watchface after all. GenerateMaze(); // only draw once maze is fully generated (not paused) @@ -228,19 +242,21 @@ void WatchFaceMaze::Refresh() { } } } - + // update battery and ble displays if on main watchface if (currentState == Displaying::watchface) { UpdateBatteryDisplay(); UpdateBleDisplay(); } +} + - // deal with confetti +void WatchFaceMaze::HandleConfetti() { // initialize confetti if tapped on autism creature if (initConfetti) { ClearConfetti(); for (ConfettiParticle &particle : confettiArr) - {particle.reset(prng);} + {particle.reset(prng);} confettiActive = true; initConfetti = false; } @@ -257,19 +273,74 @@ void WatchFaceMaze::Refresh() { } } -// allow pushing the button to go back to the watchface -bool WatchFaceMaze::OnButtonPushed() { - if (currentState != Displaying::watchface) { - screenRefreshRequired = true; - currentState = Displaying::watchface; - // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast - lastInputTime = std::chrono::time_point(); - return true; + +void WatchFaceMaze::UpdateBatteryDisplay(bool forceRedraw) { + batteryPercent = batteryController.PercentRemaining(); + charging = batteryController.IsCharging(); + if (forceRedraw || batteryPercent.IsUpdated() || charging.IsUpdated()) { + // need to redraw battery stuff + swapActiveBuffer(); + + // number of pixels between top of indicator and fill line. rounds up, so 0% is 24px but 1% is 23px + uint8_t fillLevel = 24 - ((uint16_t)(batteryPercent.Get()) * 24) / 100; + lv_area_t area = {223,3,236,26}; + + // battery body color - green >25%, orange >10%, red <=10%. Charging always makes it yellow. + lv_color_t batteryBodyColor; + if (charging.Get()) {batteryBodyColor = LV_COLOR_YELLOW;} + else if (batteryPercent.Get() > 25) {batteryBodyColor = LV_COLOR_GREEN;} + else if (batteryPercent.Get() > 10) {batteryBodyColor = LV_COLOR_ORANGE;} + else {batteryBodyColor = LV_COLOR_RED;} + + // battery top color (upper gray section) - gray normally, light blue when charging, light red at <=10% charge + lv_color_t batteryTopColor; + if (charging.Get()) {batteryTopColor = LV_COLOR_MAKE(0x80,0x80,0xC0);} + else if (batteryPercent.Get() <= 10) {batteryTopColor = LV_COLOR_MAKE(0xC0,0x80,0x80);} + else {batteryTopColor = LV_COLOR_GRAY;} + + // actually fill the buffer with the chosen colors and print it + std::fill_n(activeBuffer, fillLevel*14, batteryTopColor); + std::fill_n((activeBuffer+(fillLevel*14)), (24-fillLevel)*14, batteryBodyColor); + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); } - return false; } + +void WatchFaceMaze::UpdateBleDisplay(bool forceRedraw) { + bleConnected = bleController.IsConnected(); + if (forceRedraw || bleConnected.IsUpdated()) { + // need to redraw BLE indicator + swapActiveBuffer(); + + lv_area_t area = {213,3,216,26}; + std::fill_n(activeBuffer, 96, (bleConnected.Get() ? LV_COLOR_BLUE : LV_COLOR_GRAY)); + + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); + } +} + + +void WatchFaceMaze::ClearIndicators() { + swapActiveBuffer(); + lv_area_t area; + std::fill_n(activeBuffer, 24*14, LV_COLOR_BLACK); + + // battery indicator + area.x1=223; area.y1=3; area.x2=236; area.y2=26; + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); + + // BLE indicator + area.x1=213; area.y1=3; area.x2=216; area.y2=26; + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); +} + + bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { + // TODO ADD MUTEX OR SMTH // if generation is paused, let it continue working on that. This should really never trigger. if (pausedGeneration) {return false;} @@ -284,6 +355,18 @@ bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { } } +// allow pushing the button to go back to the watchface +bool WatchFaceMaze::OnButtonPushed() { + if (currentState != Displaying::watchface) { + screenRefreshRequired = true; + currentState = Displaying::watchface; + // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast + lastInputTime = std::chrono::time_point(); + return true; + } + return false; +} + bool WatchFaceMaze::HandleLongTap() { if (currentState == Displaying::watchface) { @@ -351,30 +434,9 @@ bool WatchFaceMaze::HandleSwipe(uint8_t direction) { } -// Put time and date info on the screen. -void WatchFaceMaze::PutTimeDate() { - uint8_t hours = dateTimeController.Hours(); - uint8_t minutes = dateTimeController.Minutes(); - - // modify hours to account for 12 hour format - if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { - if (hours == 0) hours = 12; - if (hours > 12) { - maze.pasteMazeSeed(18, 15, 22, 22, pm); - hours -= 12; - } else { - maze.pasteMazeSeed(18, 15, 22, 22, am); - } - } - - // put time on screen - maze.pasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); // top left: hours major digit - maze.pasteMazeSeed(10, 1, 15, 10, numbers[hours % 10]); // top right: hours minor digit - maze.pasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); // bottom left: minutes major digit - maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); // bottom right: minutes minor digit - - // reserve some space at the top right to put the battery and BLE indicators there - maze.pasteMazeSeed(21, 0, 23, 2, indicatorSpace); +// Clear maze +void WatchFaceMaze::InitializeMaze() { + maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); } @@ -405,86 +467,32 @@ void WatchFaceMaze::SeedMaze() { } -// goes through the maze, finds disconnected segments and connects them -void WatchFaceMaze::ForceValidMaze() { - // crude maze-optimized flood fill: follow a path until can't move any more, then find some other location to follow from. repeat. - // this function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in normal control flow) - // initialize cursor x and y to bottom right - int x = Maze::WIDTH-1, y = Maze::HEIGHT - 1; - while (true) { - ForceValidMazeLoop: - maze.setSide(x, y, TileAttr::flaggen, true); - // move cursor - if (y > 0 && !maze.getSide(x, y, TileAttr::up) && !maze.getSide(x, y-1, TileAttr::flaggen)) {y--;} - else if (x < Maze::WIDTH-1 && !maze.getSide(x, y, TileAttr::right) && !maze.getSide(x+1, y, TileAttr::flaggen)) {x++;} - else if (y < Maze::HEIGHT-1 && !maze.getSide(x, y, TileAttr::down) && !maze.getSide(x, y+1, TileAttr::flaggen)) {y++;} - else if (x > 0 && !maze.getSide(x, y, TileAttr::left) && !maze.getSide(x-1, y, TileAttr::flaggen)) {x--;} - else { - int pokeLocationCount = 0; - // couldn't find any position to move to, need to set cursor to a different usable location - for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { - for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { - bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); - - // if tile to the left is of a different traversal state (is traversed boundary) - if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { - // if found boundary AND can get to it, just continue working from here - if (maze.getSide(proposedx, proposedy, TileAttr::left) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} - pokeLocationCount++; - } - - // if tile to up is of a different traversal state (is traversed boundary) - if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { - // if found boundary AND can get to it, just continue working from here - if (maze.getSide(proposedx, proposedy, TileAttr::up) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} - pokeLocationCount++; - } - } - } - // finished scanning maze; there are no locations the cursor can be placed for it to continue scanning - - // if there are no walls that can be poked through to increase reachable area, maze is finished - if (pokeLocationCount == 0) {return;} - - // if execution gets here, need to poke a hole. - // choose a random poke location to poke a hole through. pokeLocationCount is now used as an index - pokeLocationCount = prng.rand(1, pokeLocationCount); - for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { - for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { - // pretty much a copy of the previous code which FINDS poke locations, but now with the goal of actually doing the poking - bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); - - if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { - pokeLocationCount--; - // found the target poke location, poke and loop - if (pokeLocationCount == 0) { - maze.setSide(proposedx, proposedy, TileAttr::left, false); - x = proposedx, y = proposedy; - goto ForceValidMazeLoop; // continue OUTSIDE loop - } - } +// Put time and date info on the screen. +void WatchFaceMaze::PutTimeDate() { + uint8_t hours = dateTimeController.Hours(); + uint8_t minutes = dateTimeController.Minutes(); - // if tile to up is of a different traversal state (is traversed boundary) - if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { - pokeLocationCount--; - // found the target poke location, poke and loop - if (pokeLocationCount == 0) { - maze.setSide(proposedx, proposedy, TileAttr::up, false); - x = proposedx, y = proposedy; - goto ForceValidMazeLoop; // continue processing - } - } - } - } + // modify hours to account for 12 hour format + if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { + // if 0am in 12 hour format, it's 12am + if (hours == 0) {hours = 12;} + // if after noon in 12 hour format, shift over by 12 hours + if (hours > 12) { + maze.pasteMazeSeed(18, 15, 22, 22, pm); + hours -= 12; + } else { + maze.pasteMazeSeed(18, 15, 22, 22, am); } - // done poking a hole in the maze to expand the reachable area } -} + // put time on screen + maze.pasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); // top left: hours major digit + maze.pasteMazeSeed(10, 1, 15, 10, numbers[hours % 10]); // top right: hours minor digit + maze.pasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); // bottom left: minutes major digit + maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); // bottom right: minutes minor digit -// Clear maze -void WatchFaceMaze::InitializeMaze() { - maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); + // reserve some space at the top right to put the battery and BLE indicators there + maze.pasteMazeSeed(21, 0, 23, 2, indicatorSpace); } @@ -498,8 +506,8 @@ void WatchFaceMaze::GenerateMaze() { while (true) { // find position to start generating a path from for (uint8_t i = 0; i < 30; i++) { - x = prng.rand(0, Maze::WIDTH-1); - y = prng.rand(0, Maze::HEIGHT-1); + x = (int)prng.rand(0, Maze::WIDTH-1); + y = (int)prng.rand(0, Maze::HEIGHT-1); if (maze.getSide(x,y, TileAttr::flagempty)) {break;} // found solution tile if (i == 29) { // failed all 30 attempts (this is inside the for loop for 'organization') @@ -543,8 +551,8 @@ void WatchFaceMaze::GenerateMaze() { void WatchFaceMaze::GeneratePath(int x, int y) { - int oldx = -1, oldy = -1; - uint8_t direction = -1; // which direction the cursor moved in + int oldx, oldy; // used in backtracking + uint8_t direction; // which direction the cursor moved in while (true) { // set current tile to reflect that it's been worked on maze.setSide(x, y, TileAttr::flagempty, false); // no longer empty @@ -594,7 +602,7 @@ void WatchFaceMaze::GeneratePath(int x, int y) { } else { // DID loop in on self, track down and eliminate loop // targets are the coordinates of where it needs to backtrack to - int targetx = x, targety = y; + const int targetx = x, targety = y; x = oldx, y = oldy; while (x != targetx || y != targety) { if (y > 0 && (maze.getSide(x, y, TileAttr::up) == false)) { // backtrack up @@ -631,11 +639,93 @@ void WatchFaceMaze::GeneratePath(int x, int y) { } +// goes through the maze, finds disconnected segments and connects them +void WatchFaceMaze::ForceValidMaze() { + // crude maze-optimized flood fill: follow a path until can't move any more, then find some other location to follow from. repeat. + // once it's traversed all reachable tiles, checks if there are any tiles that have not been traversed. if there are, then find a border + // between the traversed and non-traversed segments. poke a hole at one of these borders randomly. + // once the hole has been poked, more maze is reachable. continue this "fill-search then poke" scheme until the entire maze is accessible. + // this function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in normal control flow) + + // initialize cursor x and y to bottom right + int x = Maze::WIDTH-1, y = Maze::HEIGHT - 1; + while (true) { + // sorry for using goto but this needs to be really nested and the components are too integrated to split out into functions... + ForceValidMazeLoop: + maze.setSide(x, y, TileAttr::flaggen, true); + // move cursor + if (y > 0 && !maze.getSide(x, y, TileAttr::up) && !maze.getSide(x, y-1, TileAttr::flaggen)) {y--;} + else if (x < Maze::WIDTH-1 && !maze.getSide(x, y, TileAttr::right) && !maze.getSide(x+1, y, TileAttr::flaggen)) {x++;} + else if (y < Maze::HEIGHT-1 && !maze.getSide(x, y, TileAttr::down) && !maze.getSide(x, y+1, TileAttr::flaggen)) {y++;} + else if (x > 0 && !maze.getSide(x, y, TileAttr::left) && !maze.getSide(x-1, y, TileAttr::flaggen)) {x--;} + else { + int pokeLocationCount = 0; + // couldn't find any position to move to, need to set cursor to a different usable location + for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { + for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { + const bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + + // if tile to the left is of a different traversal state (is traversed boundary) + if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { + // if found boundary AND can get to it, just continue working from here + if (maze.getSide(proposedx, proposedy, TileAttr::left) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} + pokeLocationCount++; + } + + // if tile to up is of a different traversal state (is traversed boundary) + if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { + // if found boundary AND can get to it, just continue working from here + if (maze.getSide(proposedx, proposedy, TileAttr::up) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} + pokeLocationCount++; + } + } + } + // finished scanning maze; there are no locations the cursor can be placed for it to continue scanning + + // if there are no walls that can be poked through to increase reachable area, maze is finished + if (pokeLocationCount == 0) {return;} + + // if execution gets here, need to poke a hole. + // choose a random poke location to poke a hole through. pokeLocationCount is now used as an index + pokeLocationCount = prng.rand(1, pokeLocationCount); + for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { + for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { + // pretty much a copy of the previous code which FINDS poke locations, but now with the goal of actually doing the poking + const bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + + if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { + pokeLocationCount--; + // found the target poke location, poke and loop + if (pokeLocationCount == 0) { + maze.setSide(proposedx, proposedy, TileAttr::left, false); + x = proposedx, y = proposedy; + goto ForceValidMazeLoop; // continue OUTSIDE loop + } + } + + // if tile to up is of a different traversal state (is traversed boundary) + if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { + pokeLocationCount--; + // found the target poke location, poke and loop + if (pokeLocationCount == 0) { + maze.setSide(proposedx, proposedy, TileAttr::up, false); + x = proposedx, y = proposedy; + goto ForceValidMazeLoop; // continue processing + } + } + } + } + } + // done poking a hole in the maze to expand the reachable area + } +} + + void WatchFaceMaze::DrawMaze() { // this used to be nice code, but it was retrofitted to print offset by 1 pixel for a fancy border. // I'm not proud of the logic but it works. lv_area_t area; - activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer, who knows if the buffer was used just before this + swapActiveBuffer(); // who knows who used the buffer before this // Print horizontal lines // This doesn't bother with corners, those just get overwritten by the vertical lines @@ -647,11 +737,11 @@ void WatchFaceMaze::DrawMaze() { else {std::fill_n(&activeBuffer[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK);} } std::copy_n(activeBuffer, 238, &activeBuffer[238]); - area.y1 = Maze::TILESIZE * y - 1; - area.y2 = Maze::TILESIZE * y; + area.y1 = (Maze::TILESIZE * y) - 1; + area.y2 = (Maze::TILESIZE * y); lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); - activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer + swapActiveBuffer(); } // Print vertical lines @@ -665,14 +755,14 @@ void WatchFaceMaze::DrawMaze() { {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2], 4, LV_COLOR_WHITE);} else {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2], 4, LV_COLOR_BLACK);} // handle actual wall segments - if (curblock.getLeft()) {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_WHITE);} - else {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2+4], Maze::TILESIZE*2-4, LV_COLOR_BLACK);} + if (curblock.getLeft()) {std::fill_n(&activeBuffer[(y*Maze::TILESIZE*2)+4], (Maze::TILESIZE*2)-4, LV_COLOR_WHITE);} + else {std::fill_n(&activeBuffer[(y*Maze::TILESIZE*2)+4], (Maze::TILESIZE*2)-4, LV_COLOR_BLACK);} } area.x1 = Maze::TILESIZE * x - 1; area.x2 = Maze::TILESIZE * x; lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, &activeBuffer[4]); - activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer + swapActiveBuffer(); } // Print borders @@ -695,15 +785,15 @@ void WatchFaceMaze::DrawTileInner(int16_t x, int16_t y, lv_color_t color) { {return;} // prepare buffer - activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; + swapActiveBuffer(); std::fill_n(activeBuffer, 64, color); lv_area_t area; // define bounds - area.x1 = 10*x + 1; - area.x2 = 10*x + 8; - area.y1 = 10*y + 1; - area.y2 = 10*y + 8; + area.x1 = (Maze::TILESIZE * x) + 1; + area.x2 = (Maze::TILESIZE * x) + (Maze::TILESIZE - 2); + area.y1 = (Maze::TILESIZE * y) + 1; + area.y2 = (Maze::TILESIZE * y) + (Maze::TILESIZE - 2); // print to screen lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); @@ -711,71 +801,6 @@ void WatchFaceMaze::DrawTileInner(int16_t x, int16_t y, lv_color_t color) { } -void WatchFaceMaze::UpdateBatteryDisplay(bool forceRedraw) { - batteryPercent = batteryController.PercentRemaining(); - charging = batteryController.IsCharging(); - if (forceRedraw || batteryPercent.IsUpdated() || charging.IsUpdated()) { - // need to redraw battery stuff - activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer - - // number of pixels between top of indicator and fill line. rounds up, so 0% is 24px but 1% is 23px - uint8_t fillLevel = 24 - ((uint16_t)(batteryPercent.Get()) * 24) / 100; - lv_area_t area = {223,3,236,26}; - - // battery body color - green >25%, orange >10%, red <=10%. Charging always makes it yellow. - lv_color_t batteryBodyColor; - if (charging.Get()) {batteryBodyColor = LV_COLOR_YELLOW;} - else if (batteryPercent.Get() > 25) {batteryBodyColor = LV_COLOR_GREEN;} - else if (batteryPercent.Get() > 10) {batteryBodyColor = LV_COLOR_ORANGE;} - else {batteryBodyColor = LV_COLOR_RED;} - - // battery top color (upper gray section) - gray normally, light blue when charging, light red at <=10% charge - lv_color_t batteryTopColor; - if (charging.Get()) {batteryTopColor = LV_COLOR_MAKE(0x80,0x80,0xC0);} - else if (batteryPercent.Get() <= 10) {batteryTopColor = LV_COLOR_MAKE(0xC0,0x80,0x80);} - else {batteryTopColor = LV_COLOR_GRAY;} - - // actually fill the buffer with the chosen colors and print it - std::fill_n(activeBuffer, fillLevel*14, batteryTopColor); - std::fill_n((activeBuffer+fillLevel*14), (24-fillLevel)*14, batteryBodyColor); - lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); - lvgl.FlushDisplay(&area, activeBuffer); - } -} - - -void WatchFaceMaze::UpdateBleDisplay(bool forceRedraw) { - bleConnected = bleController.IsConnected(); - if (forceRedraw || bleConnected.IsUpdated()) { - // need to redraw BLE indicator - activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer - - lv_area_t area = {213,3,216,26}; - std::fill_n(activeBuffer, 96, (bleConnected.Get() ? LV_COLOR_BLUE : LV_COLOR_GRAY)); - - lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); - lvgl.FlushDisplay(&area, activeBuffer); - } -} - - -void WatchFaceMaze::ClearIndicators() { - activeBuffer = (activeBuffer==buf1) ? buf2 : buf1; // switch buffer - lv_area_t area; - std::fill_n(activeBuffer, 24*14, LV_COLOR_BLACK); - - // battery indicator - area.x1=223; area.y1=3; area.x2=236; area.y2=26; - lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); - lvgl.FlushDisplay(&area, activeBuffer); - - // BLE indicator - area.x1=213; area.y1=3; area.x2=216; area.y2=26; - lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); - lvgl.FlushDisplay(&area, activeBuffer); -} - - void WatchFaceMaze::ClearConfetti() { // prevent superfluous calls if (!confettiActive) {return;} diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index a7b17281c9..407354ae30 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -12,7 +12,7 @@ namespace Pinetime { namespace Screens { - // really just an abstraction of a uint8_t but with functions to get the individual bits + // Really just an abstraction of a uint8_t but with functions to get the individual bits. // reading up on bitfields and their potential inconsistencies scared me away from them... struct MazeTile { static constexpr uint8_t UPMASK = 0b0001; @@ -29,21 +29,22 @@ namespace Pinetime { MazeTile setFlagGen(bool value) {map = (map&~FLAGGENMASK) | (value * FLAGGENMASK); return *this;} // Get flags on given tile - bool getUp() {return map & UPMASK;} - bool getLeft() {return map & LEFTMASK;} - bool getFlagEmpty() {return map & FLAGEMPTYMASK;} - bool getFlagGen() {return map & FLAGGENMASK;} + bool getUp() const {return map & UPMASK;} + bool getLeft() const {return map & LEFTMASK;} + bool getFlagEmpty() const {return map & FLAGEMPTYMASK;} + bool getFlagGen() const {return map & FLAGGENMASK;} }; - // custom PRNG for the maze to easily allow it to be deterministic for any given minute + // Custom PRNG for the maze to easily allow it to be deterministic for any given minute + // Maybe faster too? Probably not by much if it is. class MazeRNG { public: MazeRNG(uint64_t start_seed = 64) {seed(start_seed);} - // Reseed the generator. Handles any input well. If seed is 0, acts as though it was seeded with 1. + // Reseed the generator. Handles any input well. If seed is 0, acts as though it was seeded with 1 (prevents breakage). void seed(uint64_t seed) {state = seed ? seed : 1; rand();} // RNG lifted straight from https://en.wikipedia.org/wiki/Xorshift#xorshift* (asterisk is part of the link) @@ -60,11 +61,11 @@ namespace Pinetime { private: uint64_t state; }; - - - - - // little bit of convenience for working with directions + + + + + // Little bit of convenience for working with tile flags enum TileAttr {up, down, left, right, flagempty, flaggen}; // could also be called Field or something. Does not handle stuff like generation or printing, @@ -80,12 +81,6 @@ namespace Pinetime { void set(int x, int y, MazeTile tile); void set(int index, MazeTile tile); - // fill() fills all tiles in the maze with a given value, optionally with a mask on what bits to change. - // Only cares about the low 4 bits in the value and mask. - // (use the MazeTile::*MASK values) - void fill(MazeTile tile, uint8_t mask = 0xFF); - void fill(uint8_t value, uint8_t mask = 0xFF); - // Allows abstractly setting a given side on a tile. Supports down and right for convenience. void setSide(int x, int y, TileAttr attr, bool value); void setSide(int index, TileAttr attr, bool value); @@ -93,6 +88,12 @@ namespace Pinetime { // Same as setSide, just getting. bool getSide(int x, int y, TileAttr attr); bool getSide(int index, TileAttr attr); + + // fill() fills all tiles in the maze with a given value, optionally with a mask on what bits to change. + // Only cares about the low 4 bits in the value and mask. + // (use the MazeTile::--MASK values for mask) + void fill(MazeTile tile, uint8_t mask = 0xFF); + void fill(uint8_t value, uint8_t mask = 0xFF); // Paste onto an empty board. Marks a tile as not empty if any neighboring walls are gone. // toPaste is a 1d array of uint8_t, only containing the two wall bits (left then up). So that's 4 walls in a byte. @@ -101,19 +102,23 @@ namespace Pinetime { void pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]); // 10x10 px tiles on the maze = 24x24 (on 240px screen) + // Warning: While these are respected in most functions, changing these could break other features like number displaying and + // indicators. static constexpr int WIDTH = 24; static constexpr int HEIGHT = 24; static constexpr int TILESIZE = 10; - // the actual size of the entire map. only store 4 bits per block, so 2 blocks per byte + // The actual size of the entire map. only store 4 bits per block, so 2 blocks per byte static constexpr int FLATSIZE = WIDTH*HEIGHT/2; private: - // the internal map. Doesn't actually store MazeTiles, but packs their contents to 2 tiles per byte. + // The internal map. Doesn't actually store MazeTiles, but packs their contents to 2 tiles per byte. uint8_t mazemap[FLATSIZE]; }; + + // Click on the autismcreature for a colorful surprise class ConfettiParticle { public: // steps the confetti simulation. Importantly, updates the maze equivalent position. @@ -142,14 +147,15 @@ namespace Pinetime { // first apply gravity, then apply damping factor, then add velocity to position static constexpr float GRAVITY = 0.3; // added to yvel every step (remember up is -y) static constexpr float DAMPING_FACTOR = 0.99; // keep this much of the velocity every step (applied after gravity) - static constexpr uint8_t MAX_START_ANGLE = 45; // degrees off from straight vertical a particle can be going when spawned (<90) - static constexpr uint8_t MIN_START_VELOCITY = 5; // minimum velocity a particle can spawn with - static constexpr uint8_t MAX_START_VELOCITY = 14; // maximum velocity a particle can spawn with + static constexpr int8_t MAX_START_ANGLE = 45; // degrees off from straight vertical a particle can be going when spawned (must be <90) + static constexpr int16_t MIN_START_VELOCITY = 5; // minimum velocity a particle can spawn with + static constexpr int16_t MAX_START_VELOCITY = 14; // maximum velocity a particle can spawn with static constexpr float START_X_COMPRESS = 1./2.; // multiply X velocity by this value. can give a more concentrated confetti blast. }; + // What is currently being displayed. // Watchface is normal operation; anything else is an easter egg. Really only used to indicate what // should be displayed when the screen refreshes. @@ -172,27 +178,34 @@ namespace Pinetime { bool OnButtonPushed() override; private: - // Functions related to touching the screen, for better separation of processes + // Functions related to refreshing, for better separation of processes + void HandleMazeRefresh(); + void HandleConfetti(); // okay it's not very well separated but at least the confetti part is separate + + // Functions related to drawing the indicators at the top right + void UpdateBatteryDisplay(bool forceRedraw = false); + void UpdateBleDisplay(bool forceRedraw = false); + void ClearIndicators(); + + // Functions related to touching the screen, also for better separation of processes bool HandleLongTap(); bool HandleTap(); bool HandleSwipe(uint8_t direction); - // Seed the maze with whatever the currentState dictates should be shown - void SeedMaze(); - - // Put time and date onto the screen. Acts as a seed to generate from. - void PutTimeDate(); - + // MAZE GENERATION + // Very simple function which just resets the maze to what is considered blank void InitializeMaze(); + // Seed the maze with whatever the currentState dictates should be shown + void SeedMaze(); + void PutTimeDate(); // Puts time onto the watchface. Part of SeedMaze, Do not call directly. + // Generate the maze around whatever the maze was seeded with. // MAZE MUST BE SEEDED ELSE ALL YOU'LL GENERATE IS AN INFINITE LOOP! // If seed has disconnected components, maze will not be perfect. void GenerateMaze(); - - // Generates a single path starting at the provided x,y coords - void GeneratePath(int x, int y); + void GeneratePath(int x, int y); // Generates a single path starting at the x,y coords. Part of GenerateMaze, do not call directly. // If the maze has any disconnected components (such as if seeded with multiple disconnected blocks), // poke holes to force all components to be connected. @@ -200,16 +213,13 @@ namespace Pinetime { // Draws the maze to the screen. void DrawMaze(); + + // OTHER FUNCTIONS // Fill in the inside of a maze square. Wall states don't affect this; it never draws in the area where walls go. // Generic, but only actually used for confetti. void DrawTileInner(int16_t x, int16_t y, lv_color_t color); - // Draw the indicators at the top right. - void UpdateBatteryDisplay(bool forceRedraw = false); - void UpdateBleDisplay(bool forceRedraw = false); - void ClearIndicators(); - // Draw and generally deal with confetti void ProcessConfetti(); void ClearConfetti(); @@ -224,7 +234,7 @@ namespace Pinetime { const Controllers::Ble& bleController; Components::LittleVgl& lvgl; - // Maze and internal RNG (so it doesn't mess with other things by reseeding the regular C RNG provider) + // Maze and internal RNG (so it doesn't mess with other things by reseeding the regular C++ RNG provider) Maze maze; MazeRNG prng; @@ -236,30 +246,32 @@ namespace Pinetime { Utility::DirtyValue> currentDateTime {}; std::chrono::time_point realTime {}; - // Indicator stuff + // Indicator values. Used for refreshing the respective indicators. Utility::DirtyValue batteryPercent; Utility::DirtyValue charging; Utility::DirtyValue bleConnected; // Confetti for autism creature - // Infinisim warning: because each confetti moving causes 2 draw calls, this is really slow in Infinisim. Lower if using Infinisim. + // Warning: because each confetti moving causes 2 draw calls, this is really slow in Infinisim. Lower if using Infinisim (to ~20). constexpr static uint16_t CONFETTI_COUNT = 50; - ConfettiParticle confettiArr[CONFETTI_COUNT]; // can freely increase/decrease number of particles - bool initConfetti = false; // don't want to touch confettiArr in touch event handler, so use a flag and do it in refresh() + ConfettiParticle confettiArr[CONFETTI_COUNT]; + bool initConfetti = false; // don't want to modify confettiArr in touch event handler, so use a flag and do it in refresh() bool confettiActive = false; - // Buffers for use during printing. There's two it flips between because if there was only one, - // it would start being overwritten before the DMA finishes, so it'd corrupt parts of the display. - // activeBuffer is, well, the currently active one. Flip with `activeBuffer = (activeBuffer==buf1) ? buf2 : buf1;` + // Buffers for use during printing. There's two it flips between because if there was only one, it would start + // being overwritten before the DMA finishes, and it'd corrupt parts of the display. + // activeBuffer is, well, the currently active one. Switch with swapActiveBuffer(); lv_color_t buf1[480]; lv_color_t buf2[480]; lv_color_t *activeBuffer = buf1; + void swapActiveBuffer() {activeBuffer = (activeBuffer==buf1) ? buf2 : buf1;} // All concerning the printing of the screen. If screenRefreshRequired is set and screenRefreshTargetTime is // greater than the current time, it waits until that target time before refreshing. Otherwise, it refreshes // immediately when it sees that screenRefreshRequired is set. // pausedGeneration is used if the maze took too long to generate, so it lets other processes get cpu time. // It really should never trigger with this small 24x24 maze. + // pausedGeneration does NOT protect against infinite loops from unseeded mazes! It only checks after each path has been generated! std::chrono::time_point screenRefreshTargetTime {}; bool screenRefreshRequired = false; bool pausedGeneration = false; @@ -291,10 +303,12 @@ namespace Pinetime { // Input codes for secret swipe gestures // Note that the codes are effectively backwards; the currentCode is a stack being pushed from the left. Values are 0-3, clockwise from up. + // After a code is inputted the code is cleared, so if making a code smaller than the max size take care that it doesn't overlap + // with any other code. constexpr static uint8_t lossCode[8] = {0,0,2,2,3,1,3,1}; // RLRLDDUU (konami code backwards) constexpr static uint8_t amogusCode[8] = {1,3,1,3,2,2,0,0}; // UUDDLRLR (konami code) constexpr static uint8_t autismCode[8] = {3,1,3,1,3,1,3,1}; // RLRLRLRL (pet pet pet pet) - constexpr static uint8_t foxCode[7] = {0,1,0,3,2,1,2}; // the first of a type of secret in a curious game :3 + constexpr static uint8_t foxCode[7] = {0,1,0,3,2,1,2}; // a healthy secret in a curious game :3 constexpr static uint8_t reminderCode[4] = {3,2,1,0}; // URDL constexpr static uint8_t pinetimeCode[8] = {1,2,3,0,1,2,3,0}; // ULDRULDR (two counterclockwise rotations) @@ -305,7 +319,7 @@ namespace Pinetime { constexpr static uint8_t foxgame[132] /*24x22*/ = {0xFF,0xD7,0xFF,0xFF,0xF5,0xFF,0xFD,0x01,0x7F,0xFF,0x40,0x5F,0xF0,0x38,0x1F,0xFC,0x0E,0x07,0xC3,0xFF,0x87,0xF0,0xFF,0xE1,0x8F,0xFF,0xE3,0xE3,0xFF,0xF8,0x8F,0xFF,0xFF,0xE1,0xFF,0xF0,0x8F,0xFF,0xFF,0xE0,0x5F,0x43,0x8F,0xFF,0xFF,0xE0,0x04,0x0F,0x8F,0xFF,0xFF,0xE3,0xE0,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x85,0x55,0x55,0x41,0x55,0x55,0x80,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xDF,0xFF,0xFF,0xF7,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x87,0xFF,0xFF,0xE1,0xFF,0xFF,0xE1,0x7F,0xFF,0xF8,0x5F,0xFF,0xF8,0x17,0xFF,0xFE,0x05,0xFF,0xFF,0x83,0xFF,0xFF,0xE0,0xFF}; constexpr static uint8_t reminder[102] /*24x17*/ = {0xFF,0xD5,0xF7,0xDF,0x57,0xFF,0xFF,0x80,0xE3,0x8E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE1,0x0E,0x17,0xFF,0xFF,0xE3,0xE0,0x0E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE3,0x8E,0x17,0xFF,0xFF,0xE3,0xE3,0x8E,0x03,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xF5,0x7F,0x5F,0xF5,0x5F,0xD5,0xC0,0x3C,0x07,0xC0,0x07,0x80,0x8F,0xF8,0xE3,0x88,0xE3,0x8F,0x8F,0xF8,0xE3,0x88,0xE3,0x85,0x8F,0x78,0x43,0x88,0xE3,0x80,0x8E,0x38,0x03,0x8F,0xE3,0x8F,0x84,0x38,0xE3,0x8F,0xE3,0x85,0xE0,0xF8,0xE3,0x8F,0xE3,0x80}; //constexpr static uint8_t foxface[144] /*24x24*/ = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFD,0x7F,0xFF,0xFF,0xFF,0xFF,0xF8,0x17,0xFF,0xFF,0xFF,0xFF,0xF8,0x01,0x7F,0xFF,0xF5,0x5F,0xF8,0xF8,0x1F,0xFF,0x40,0x07,0xF8,0xFF,0x8F,0xF4,0x0F,0xE3,0xF8,0x7F,0x85,0x40,0xFF,0xC3,0xF8,0x1F,0xE0,0x0F,0xFD,0x03,0xF8,0xE7,0xFF,0xFF,0xF3,0xE3,0xF0,0xFF,0xFF,0xFF,0xFF,0xE3,0xC0,0xFD,0x7F,0xFF,0xFF,0xCF,0x8F,0xFE,0x1F,0xFD,0x7F,0x8F,0xBF,0xE4,0x0F,0xFE,0x1F,0x87,0xFF,0xE0,0x0F,0xE4,0x0F,0xE1,0xFF,0xF8,0x3F,0xE0,0x0F,0xF8,0xDF,0xFF,0xFF,0xF8,0x3F,0xF8,0xE5,0x7F,0xF5,0xFF,0xFF,0xF8,0xFF,0x95,0x40,0x7F,0xFF,0xFE,0xFF,0xFF,0xE0,0x15,0x55,0x54,0xBF,0xFF,0xF8,0xFF,0xFF,0xFF,0x9F,0xFF,0xFF,0xFF,0xFF,0xFD,0x87,0xFF,0xFF,0xFF,0xFF,0xF0,0xE1,0x5F,0xFF,0xFF,0xFF,0x43,0xF8,0x05,0x5F,0xFF,0xD4,0x3F}; - constexpr static uint8_t pinetime[120] /*20x24*/ = {0xFF,0xFF,0xF7,0xFF,0xFF,0xFF,0xFF,0xC1,0xFF,0xFF,0xFF,0xFF,0x00,0x7F,0xFF,0xFF,0xF6,0x00,0x37,0xFF,0xFF,0xC1,0x63,0x41,0xFF,0xFF,0x80,0xD5,0x80,0xFF,0xFF,0xB5,0x00,0x56,0xFF,0xF7,0xC0,0x00,0x01,0xF7,0xE1,0x60,0x00,0x03,0x43,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x5D,0x00,0x03,0xE0,0x00,0xD5,0x80,0x03,0xE0,0x35,0x00,0x56,0x03,0xE3,0x40,0x00,0x01,0x63,0x96,0x00,0x00,0x00,0x34,0x81,0x60,0x00,0x03,0x40,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x77,0x00,0x03,0xF8,0x00,0xC1,0x80,0x0F,0xF8,0x0D,0x00,0x58,0x0F,0xFF,0xD0,0x00,0x05,0xFF,0xFF,0xE0,0x00,0x03,0xFF,0xFF,0xFE,0x00,0x3F,0xFF,0xFF,0xFF,0xE3,0xFF,0xFF};\ + constexpr static uint8_t pinetime[120] /*20x24*/ = {0xFF,0xFF,0xF7,0xFF,0xFF,0xFF,0xFF,0xC1,0xFF,0xFF,0xFF,0xFF,0x00,0x7F,0xFF,0xFF,0xF6,0x00,0x37,0xFF,0xFF,0xC1,0x63,0x41,0xFF,0xFF,0x80,0xD5,0x80,0xFF,0xFF,0xB5,0x00,0x56,0xFF,0xF7,0xC0,0x00,0x01,0xF7,0xE1,0x60,0x00,0x03,0x43,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x5D,0x00,0x03,0xE0,0x00,0xD5,0x80,0x03,0xE0,0x35,0x00,0x56,0x03,0xE3,0x40,0x00,0x01,0x63,0x96,0x00,0x00,0x00,0x34,0x81,0x60,0x00,0x03,0x40,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x77,0x00,0x03,0xF8,0x00,0xC1,0x80,0x0F,0xF8,0x0D,0x00,0x58,0x0F,0xFF,0xD0,0x00,0x05,0xFF,0xFF,0xE0,0x00,0x03,0xFF,0xFF,0xFE,0x00,0x3F,0xFF,0xFF,0xFF,0xE3,0xFF,0xFF}; }; } From 0c488ce5e5018b558eb2102c6252cc05851554dd Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:59:44 -0500 Subject: [PATCH 19/23] Remove add mutex todo; not needed --- src/displayapp/screens/WatchFaceMaze.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index ae8876d39f..5d2605c830 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -340,10 +340,12 @@ void WatchFaceMaze::ClearIndicators() { bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { - // TODO ADD MUTEX OR SMTH // if generation is paused, let it continue working on that. This should really never trigger. if (pausedGeneration) {return false;} - + + // Interrupts never seem to overlap (instead they're queued) so I don't have to worry about anything happening while one of + // these handlers are running + switch (event) { case Pinetime::Applications::TouchEvents::LongTap: return HandleLongTap(); case Pinetime::Applications::TouchEvents::Tap: return HandleTap(); @@ -351,7 +353,7 @@ bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { case Pinetime::Applications::TouchEvents::SwipeRight: return HandleSwipe(1); case Pinetime::Applications::TouchEvents::SwipeDown: return HandleSwipe(2); case Pinetime::Applications::TouchEvents::SwipeLeft: return HandleSwipe(3); - default: return false; // only handle swipe events + default: return false; } } From 33492a21a4823c952b27e7d2a089b9ca600c97da Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Wed, 23 Oct 2024 19:57:56 -0500 Subject: [PATCH 20/23] Add maze watchface to default watchfaces --- src/displayapp/apps/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index d78587609e..47e480ecf6 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -27,6 +27,7 @@ else() set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Terminal") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Infineat") set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::CasioStyleG7710") + set(DEFAULT_WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}, WatchFace::Maze") set(WATCHFACE_TYPES "${DEFAULT_WATCHFACE_TYPES}" CACHE STRING "List of watch faces to build into the firmware") endif() From abc980fd84fd8c0e6b3677497c6f6b9be063a4e0 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:53:57 -0500 Subject: [PATCH 21/23] Update WatchFaceMaze --- src/displayapp/screens/WatchFaceMaze.cpp | 872 +++++++++++++---------- src/displayapp/screens/WatchFaceMaze.h | 346 +++++---- 2 files changed, 713 insertions(+), 505 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index 5d2605c830..c99807bd38 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -1,227 +1,250 @@ -#include #include "displayapp/screens/WatchFaceMaze.h" -#include "Tile.h" - using namespace Pinetime::Applications::Screens; - // Despite being called Maze, this really is only a relatively simple wrapper for the specialized // (fake) 2d array on which the maze structure is built. It should only have manipulations for -// the structure, generating and printing should be handled elsewhere. +// the structure; generating and printing should be handled elsewhere. Maze::Maze() { - std::fill_n(mazemap, FLATSIZE, 0); + std::fill_n(mazeMap, FLATSIZE, 0); } - -// only returns 4 bits (since that's all that's stored) -// returns walls but unset flags in case of out of bounds access -MazeTile Maze::get(int x, int y) { - if (x<0||x>WIDTH||y<0||y>HEIGHT) {return MazeTile(0b0011);} - return get((y * WIDTH) + x); +// Only returns 4 bits (since that's all that's stored) +// Returns set walls but unset flags in case of out of bounds access +MazeTile Maze::Get(const coord_t x, const coord_t y) const { + if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) { + return MazeTile(MazeTile::UPMASK | MazeTile::LEFTMASK); + } + return Get((y * WIDTH) + x); } -MazeTile Maze::get(int index) { - if (index < 0 || index/2 >= FLATSIZE) {return MazeTile(0b0011);} + +MazeTile Maze::Get(const int32_t index) const { + if (index < 0 || index / 2 >= FLATSIZE) { + return MazeTile(MazeTile::UPMASK | MazeTile::LEFTMASK); + } // odd means right (low) nibble, even means left (high) nibble - if (index & 0b1) return MazeTile(mazemap[index/2] & 0b00001111); - else return MazeTile(mazemap[index/2] >> 4); + if (index % 2 == 1) { + return MazeTile(mazeMap[index / 2] & 0b00001111); + } + return MazeTile(mazeMap[index / 2] >> 4); } - -// only stores the low 4 bits of the value -// if out of bounds, does nothing -void Maze::set(int x, int y, MazeTile tile) { - if (x<0||x>WIDTH||y<0||y>HEIGHT) {return;} - set((y * WIDTH) + x, tile); +// Only stores the low 4 bits of the value +// If out of bounds, does nothing +void Maze::Set(const coord_t x, const coord_t y, const MazeTile tile) { + if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) { + return; + } + Set((y * WIDTH) + x, tile); } -void Maze::set(int index, MazeTile tile) { - if (index < 0 || index/2 >= FLATSIZE) {return;} + +void Maze::Set(const int32_t index, const MazeTile tile) { + if (index < 0 || index / 2 >= FLATSIZE) { + return; + } // odd means right (low) nibble, even means left (high) nibble - if (index & 0b1) {mazemap[index/2] = (mazemap[index/2] & 0b11110000) | tile.map;} - else {mazemap[index/2] = (mazemap[index/2] & 0b00001111) | tile.map << 4;} + if (index % 2 == 1) { + mazeMap[index / 2] = (mazeMap[index / 2] & 0b11110000) | tile.map; + } else { + mazeMap[index / 2] = (mazeMap[index / 2] & 0b00001111) | tile.map << 4; + } } - // For quickly manipulating. Also allows better abstraction by allowing setting of down and right sides. // Silently does nothing if given invalid values. -void Maze::setSide(int index, TileAttr attr, bool value) { - switch(attr) { - case up: set(index, get(index).setUp(value)); break; - case down: set(index+WIDTH, get(index+WIDTH).setUp(value)); break; - case left: set(index, get(index).setLeft(value)); break; - case right: set(index+1, get(index+1).setLeft(value)); break; - case flagempty: set(index, get(index).setFlagEmpty(value)); break; - case flaggen: set(index, get(index).setFlagGen(value)); break; - } -} -void Maze::setSide(int x, int y, TileAttr attr, bool value) { - setSide((y*WIDTH)+x, attr, value); -} -bool Maze::getSide(int index, TileAttr attr) { - switch(attr) { - case up: return get(index).getUp(); - case down: return get(index+WIDTH).getUp(); - case left: return get(index).getLeft(); - case right: return get(index+1).getLeft(); - case flagempty: return get(index).getFlagEmpty(); - case flaggen: return get(index).getFlagGen(); +void Maze::SetAttr(const int32_t index, const TileAttr attr, const bool value) { + switch (attr) { + case Up: + Set(index, Get(index).SetUp(value)); + break; + case Down: + Set(index + WIDTH, Get(index + WIDTH).SetUp(value)); + break; + case Left: + Set(index, Get(index).SetLeft(value)); + break; + case Right: + Set(index + 1, Get(index + 1).SetLeft(value)); + break; + case FlagEmpty: + Set(index, Get(index).SetFlagEmpty(value)); + break; + case FlagGen: + Set(index, Get(index).SetFlagGen(value)); + break; } - return false; } -bool Maze::getSide(int x, int y, TileAttr attr) { - return getSide((y*WIDTH)+x, attr); + +void Maze::SetAttr(const coord_t x, const coord_t y, const TileAttr attr, const bool value) { + SetAttr((y * WIDTH) + x, attr, value); +} + +bool Maze::GetTileAttr(const int32_t index, const TileAttr attr) const { + switch (attr) { + case Up: + return Get(index).GetUp(); + case Down: + return Get(index + WIDTH).GetUp(); + case Left: + return Get(index).GetLeft(); + case Right: + return Get(index + 1).GetLeft(); + case FlagEmpty: + return Get(index).GetFlagEmpty(); + case FlagGen: + return Get(index).GetFlagGen(); + } + return false; } +bool Maze::GetTileAttr(const coord_t x, const coord_t y, const TileAttr attr) const { + return GetTileAttr((y * WIDTH) + x, attr); +} -// only operates on the low 4 bits of the uint8_t. -// only sets the bits from the value that are also on in the mask, rest are left alone +// Only operates on the low 4 bits of the uint8_t. +// Only sets the bits from the value that are also on in the mask, rest are left alone // e.g. existing = 1010, value = 0001, mask = 0011, then result = 1001 // (mask defaults to 0xFF which keeps all bits) -void Maze::fill(uint8_t value, uint8_t mask) { +void Maze::Fill(uint8_t value, uint8_t mask) { value = value & 0b00001111; value |= value << 4; - if (mask == 0xFF) { - // did not include a mask - std::fill_n(mazemap, FLATSIZE, value); + if (mask >= 0x0F) { + // mask includes all bits, simply use fill_n + std::fill_n(mazeMap, FLATSIZE, value); } else { // included a mask mask = mask & 0b00001111; mask |= mask << 4; value = value & mask; // preprocess mask for value - mask = ~mask; // this mask will be applied to the value - for (uint8_t& mapitem : mazemap) { - mapitem = (mapitem & mask) + value; + mask = ~mask; // this inverted mask will be applied to the existing value in mazeMap + for (uint8_t& mapItem : mazeMap) { + mapItem = (mapItem & mask) + value; } } } -inline void Maze::fill(MazeTile tile, uint8_t mask) - {fill(tile.map, mask);} +inline void Maze::Fill(const MazeTile tile, const uint8_t mask) { + Fill(tile.map, mask); +} // Paste a set of tiles into the given coords. -void Maze::pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]) { +void Maze::PasteMazeSeed(const coord_t x1, const coord_t y1, const coord_t x2, const coord_t y2, const uint8_t toPaste[]) { // Assumes a maze with empty flags all true, and all walls present - uint16_t flatcoord = 0; // the position in the array (inside the byte, so index 1 would be mask 0b00110000 in the first byte) - for (int y = y1; y <= y2; y++) { - for (int x = x1; x <= x2; x++) { + int32_t flatCoord = 0; // the position in the array (inside the byte, so index 1 would be mask 0b00110000 in the first byte) + for (coord_t y = y1; y <= y2; y++) { + for (coord_t x = x1; x <= x2; x++) { // working holds the target wall (bit 2 for left wall, bit 1 for up wall) - const uint8_t working = (toPaste[flatcoord/4] & (0b11 << ((3-(flatcoord%4))*2))) >> ((3-(flatcoord%4))*2); + const uint8_t working = (toPaste[flatCoord / 4] & (0b11 << ((3 - (flatCoord % 4)) * 2))) >> ((3 - (flatCoord % 4)) * 2); // handle left wall - if (!(working & 0b10)) { - setSide(x, y, TileAttr::left, false); - setSide(x, y, TileAttr::flagempty, false); - if (x > 0) setSide(x-1, y, TileAttr::flagempty, false); + if (!(bool) (working & 0b10)) { + SetAttr(x, y, TileAttr::Left, false); + SetAttr(x, y, TileAttr::FlagEmpty, false); + if (x > 0) { + SetAttr(x - 1, y, TileAttr::FlagEmpty, false); + } } // handle up wall - if (!(working & 0b01)) { - setSide(x, y, TileAttr::up, false); - setSide(x, y, TileAttr::flagempty, false); - if (y > 0) setSide(x, y-1, TileAttr::flagempty, false); + if (!(bool) (working & 0b01)) { + SetAttr(x, y, TileAttr::Up, false); + SetAttr(x, y, TileAttr::FlagEmpty, false); + if (y > 0) { + SetAttr(x, y - 1, TileAttr::FlagEmpty, false); + } } - flatcoord++; + flatCoord++; } } } - - - -bool ConfettiParticle::step() { +bool ConfettiParticle::Step() { // first apply gravity (only to y), then dampening, then apply velocity to position. - xvel *= DAMPING_FACTOR; - xpos += xvel; + xVel *= DAMPING_FACTOR; + xPos += xVel; - yvel += GRAVITY; - yvel *= DAMPING_FACTOR; - ypos += yvel; + yVel += GRAVITY; + yVel *= DAMPING_FACTOR; + yPos += yVel; // return true if particle is finished (went OOB (ignore top; particle can still fall down)) - return (xpos < 0 || xpos > 240 || ypos > 240); + return xPos < 0 || xPos > 240 || yPos > 240; } - -void ConfettiParticle::reset(MazeRNG &prng) { +void ConfettiParticle::Reset(MazeRNG& prng) { // always start at bottom middle - xpos = 120; - ypos = 240; + xPos = 120; + yPos = 240; // velocity in pixels/tick - const float velocity = ((float)prng.rand(MIN_START_VELOCITY*100, MAX_START_VELOCITY*100))/100; + const float velocity = ((float) prng.Rand(MIN_START_VELOCITY * 100, MAX_START_VELOCITY * 100)) / 100; // angle, in radians, for going up at the chosen degree angle - const float angle = ((float)prng.rand(0,MAX_START_ANGLE*2) - MAX_START_ANGLE + 90) * ((float)3.14159265 / 180); + const float angle = ((float) prng.Rand(0, MAX_START_ANGLE * 2) - MAX_START_ANGLE + 90) * (std::numbers::pi_v / 180); - xvel = std::cos(angle) * velocity * START_X_COMPRESS; - yvel = -std::sin(angle) * velocity; + xVel = std::cos(angle) * velocity * START_X_COMPRESS; + yVel = -std::sin(angle) * velocity; // Low 3 bits represent red, green, and blue. Also don't allow all three off or all three on at once. // Effectively choose any max saturation color except black or white. - const uint8_t colorBits = prng.rand(1,6); - color = LV_COLOR_MAKE((colorBits&0b001) * 0xFF, ((colorBits&0b010)>>1) * 0xFF, ((colorBits&0b100)>>2) * 0xFF); + const uint8_t colorBits = prng.Rand(1, 6); + color = LV_COLOR_MAKE((colorBits & 0b001) * 0xFF, ((colorBits & 0b010) >> 1) * 0xFF, ((colorBits & 0b100) >> 2) * 0xFF); } - - - WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, Controllers::DateTime& dateTimeController, Controllers::Settings& settingsController, Controllers::MotorController& motor, const Controllers::Battery& batteryController, const Controllers::Ble& bleController) - : dateTimeController {dateTimeController}, - settingsController {settingsController}, - motor {motor}, - batteryController {batteryController}, - bleController {bleController}, - lvgl {lvgl}, - maze {Maze()}, - prng {MazeRNG()} { + : dateTimeController{dateTimeController}, + settingsController{settingsController}, + motor{motor}, + batteryController{batteryController}, + bleController{bleController}, + lvgl{lvgl}, + maze{Maze()}, + prng{MazeRNG()} { + + // set it to be in the past so it always takes two clicks to go to the secret, even if you're fast + lastLongClickTime = xTaskGetTickCount() - doubleDoubleClickDelay; taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); // refreshing here seems to cause issues in infinisim - //Refresh(); + // Refresh(); } - WatchFaceMaze::~WatchFaceMaze() { lv_obj_clean(lv_scr_act()); lv_task_del(taskRefresh); } - void WatchFaceMaze::Refresh() { - // store time for other functions to use. functions called directly from Refresh() can just assume this is accurate. - realTime = dateTimeController.CurrentDateTime(); // handle everything related to refreshing and printing stuff to the screen HandleMazeRefresh(); - + // handle confetti printing // yeah it's not very pretty how this is hanging out in the refresh() function but I don't want to modify anything related - // to printing in the touch interrupts - HandleConfetti(); + // to printing in the touch interrupts + HandleConfettiRefresh(); } - void WatchFaceMaze::HandleMazeRefresh() { // convert time to minutes and update if needed - currentDateTime = std::chrono::time_point_cast(realTime); - - // refresh if generation isn't complete, a minute has passed on the watchface, or the screen refresh timer expired. - // if minute rolls over while screenrefresh is required, ignore it. the refresh timer will handle it. - if (pausedGeneration || // if generation paused, need to complete it - (currentState == Displaying::watchface && !screenRefreshRequired && currentDateTime.IsUpdated()) || // already on watchface, not waiting for a screen refresh, and time updated - (screenRefreshRequired && realTime > screenRefreshTargetTime)) { // waiting on a refresh + currentDateTime = std::chrono::time_point_cast(dateTimeController.CurrentDateTime()); + + // Refresh if it's needed by some other component, a minute has passed on the watchface, or if generation has paused. + if (screenRefreshRequired || (currentState == Displaying::WatchFace && currentDateTime.IsUpdated()) || pausedGeneration) { // if generation wasn't paused (i.e. doing a ground up maze gen), set everything up if (!pausedGeneration) { // only reseed PRNG if got here by the minute rolling over - if (!screenRefreshRequired) prng.seed(currentDateTime.Get().time_since_epoch().count()); + if (!screenRefreshRequired) { + prng.Seed(currentDateTime.Get().time_since_epoch().count()); + } InitializeMaze(); SeedMaze(); } @@ -229,41 +252,43 @@ void WatchFaceMaze::HandleMazeRefresh() { // always need to run GenerateMaze() when refreshing. This is a maze watchface after all. GenerateMaze(); - // only draw once maze is fully generated (not paused) + // only finalize and draw once maze is fully generated (not paused) if (!pausedGeneration) { ForceValidMaze(); - if (currentState != Displaying::watchface) {ClearIndicators();} + if (currentState != Displaying::WatchFace) { + ClearIndicators(); + } DrawMaze(); screenRefreshRequired = false; - // if switched to watchface, also add indicators for BLE and battery - if (currentState == Displaying::watchface) { + // if on watchface, also add indicators for BLE and battery + if (currentState == Displaying::WatchFace) { UpdateBatteryDisplay(true); UpdateBleDisplay(true); } } } - + // update battery and ble displays if on main watchface - if (currentState == Displaying::watchface) { + if (currentState == Displaying::WatchFace) { UpdateBatteryDisplay(); UpdateBleDisplay(); } } - -void WatchFaceMaze::HandleConfetti() { +void WatchFaceMaze::HandleConfettiRefresh() { // initialize confetti if tapped on autism creature if (initConfetti) { ClearConfetti(); - for (ConfettiParticle &particle : confettiArr) - {particle.reset(prng);} + for (ConfettiParticle& particle : confettiArr) { + particle.Reset(prng); + } confettiActive = true; initConfetti = false; } // update confetti if needed if (confettiActive) { - if (currentState != Displaying::autismcreature) { - // nuke confetti if went to a different display + if (currentState != Displaying::AutismCreature) { + // remove confetti if went to a different display ClearConfetti(); confettiActive = false; } else { @@ -273,47 +298,54 @@ void WatchFaceMaze::HandleConfetti() { } } - -void WatchFaceMaze::UpdateBatteryDisplay(bool forceRedraw) { +void WatchFaceMaze::UpdateBatteryDisplay(const bool forceRedraw) { batteryPercent = batteryController.PercentRemaining(); charging = batteryController.IsCharging(); if (forceRedraw || batteryPercent.IsUpdated() || charging.IsUpdated()) { // need to redraw battery stuff - swapActiveBuffer(); + SwapActiveBuffer(); // number of pixels between top of indicator and fill line. rounds up, so 0% is 24px but 1% is 23px - uint8_t fillLevel = 24 - ((uint16_t)(batteryPercent.Get()) * 24) / 100; - lv_area_t area = {223,3,236,26}; + const uint8_t fillLevel = 24 - ((uint16_t) (batteryPercent.Get()) * 24) / 100; + constexpr lv_area_t area = {223, 3, 236, 26}; // battery body color - green >25%, orange >10%, red <=10%. Charging always makes it yellow. lv_color_t batteryBodyColor; - if (charging.Get()) {batteryBodyColor = LV_COLOR_YELLOW;} - else if (batteryPercent.Get() > 25) {batteryBodyColor = LV_COLOR_GREEN;} - else if (batteryPercent.Get() > 10) {batteryBodyColor = LV_COLOR_ORANGE;} - else {batteryBodyColor = LV_COLOR_RED;} + if (charging.Get()) { + batteryBodyColor = LV_COLOR_YELLOW; + } else if (batteryPercent.Get() > 25) { + batteryBodyColor = LV_COLOR_GREEN; + } else if (batteryPercent.Get() > 10) { + batteryBodyColor = LV_COLOR_ORANGE; + } else { + batteryBodyColor = LV_COLOR_RED; + } - // battery top color (upper gray section) - gray normally, light blue when charging, light red at <=10% charge + // battery top color (upper section) - gray normally, light blue when charging, light red at <=10% charge lv_color_t batteryTopColor; - if (charging.Get()) {batteryTopColor = LV_COLOR_MAKE(0x80,0x80,0xC0);} - else if (batteryPercent.Get() <= 10) {batteryTopColor = LV_COLOR_MAKE(0xC0,0x80,0x80);} - else {batteryTopColor = LV_COLOR_GRAY;} + if (charging.Get()) { + batteryTopColor = LV_COLOR_MAKE(0x80, 0x80, 0xC0); + } else if (batteryPercent.Get() <= 10) { + batteryTopColor = LV_COLOR_MAKE(0xC0, 0x80, 0x80); + } else { + batteryTopColor = LV_COLOR_GRAY; + } // actually fill the buffer with the chosen colors and print it - std::fill_n(activeBuffer, fillLevel*14, batteryTopColor); - std::fill_n((activeBuffer+(fillLevel*14)), (24-fillLevel)*14, batteryBodyColor); + std::fill_n(activeBuffer, fillLevel * 14, batteryTopColor); + std::fill_n(activeBuffer + (fillLevel * 14), (24 - fillLevel) * 14, batteryBodyColor); lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); } } - -void WatchFaceMaze::UpdateBleDisplay(bool forceRedraw) { +void WatchFaceMaze::UpdateBleDisplay(const bool forceRedraw) { bleConnected = bleController.IsConnected(); if (forceRedraw || bleConnected.IsUpdated()) { // need to redraw BLE indicator - swapActiveBuffer(); + SwapActiveBuffer(); - lv_area_t area = {213,3,216,26}; + constexpr lv_area_t area = {213, 3, 216, 26}; std::fill_n(activeBuffer, 96, (bleConnected.Get() ? LV_COLOR_BLUE : LV_COLOR_GRAY)); lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); @@ -321,204 +353,240 @@ void WatchFaceMaze::UpdateBleDisplay(bool forceRedraw) { } } - void WatchFaceMaze::ClearIndicators() { - swapActiveBuffer(); + SwapActiveBuffer(); lv_area_t area; - std::fill_n(activeBuffer, 24*14, LV_COLOR_BLACK); + std::fill_n(activeBuffer, 24 * 14, LV_COLOR_BLACK); // battery indicator - area.x1=223; area.y1=3; area.x2=236; area.y2=26; + area.x1 = 223; + area.y1 = 3; + area.x2 = 236; + area.y2 = 26; lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); // BLE indicator - area.x1=213; area.y1=3; area.x2=216; area.y2=26; + area.x1 = 213; + area.y1 = 3; + area.x2 = 216; + area.y2 = 26; lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); } - -bool WatchFaceMaze::OnTouchEvent(TouchEvents event) { +bool WatchFaceMaze::OnTouchEvent(const TouchEvents event) { // if generation is paused, let it continue working on that. This should really never trigger. - if (pausedGeneration) {return false;} - + if (pausedGeneration) { + return false; + } + // Interrupts never seem to overlap (instead they're queued) so I don't have to worry about anything happening while one of - // these handlers are running - + // these handlers are running + switch (event) { - case Pinetime::Applications::TouchEvents::LongTap: return HandleLongTap(); - case Pinetime::Applications::TouchEvents::Tap: return HandleTap(); - case Pinetime::Applications::TouchEvents::SwipeUp: return HandleSwipe(0); - case Pinetime::Applications::TouchEvents::SwipeRight: return HandleSwipe(1); - case Pinetime::Applications::TouchEvents::SwipeDown: return HandleSwipe(2); - case Pinetime::Applications::TouchEvents::SwipeLeft: return HandleSwipe(3); - default: return false; + case Pinetime::Applications::TouchEvents::LongTap: + return HandleLongTap(); + case Pinetime::Applications::TouchEvents::Tap: + return HandleTap(); + case Pinetime::Applications::TouchEvents::SwipeUp: + return HandleSwipe(0); + case Pinetime::Applications::TouchEvents::SwipeRight: + return HandleSwipe(1); + case Pinetime::Applications::TouchEvents::SwipeDown: + return HandleSwipe(2); + case Pinetime::Applications::TouchEvents::SwipeLeft: + return HandleSwipe(3); + default: + return false; } } // allow pushing the button to go back to the watchface bool WatchFaceMaze::OnButtonPushed() { - if (currentState != Displaying::watchface) { + if (currentState != Displaying::WatchFace) { screenRefreshRequired = true; - currentState = Displaying::watchface; - // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast - lastInputTime = std::chrono::time_point(); + currentState = Displaying::WatchFace; + // set lastLongClickTime to be in the past so it always needs two long taps to get back to blank, even if you're fast + lastLongClickTime = xTaskGetTickCount() - doubleDoubleClickDelay; return true; } return false; } - bool WatchFaceMaze::HandleLongTap() { - if (currentState == Displaying::watchface) { + if (currentState == Displaying::WatchFace) { // On watchface; either refresh maze or go to blank state - if (lastInputTime + std::chrono::milliseconds(2500) > realTime) { + if (xTaskGetTickCount() - lastLongClickTime < doubleDoubleClickDelay) { // long tapped twice in sequence; switch to blank maze - currentState = Displaying::blank; + currentState = Displaying::Blank; screenRefreshRequired = true; - std::fill_n(currentCode, sizeof(currentCode), 255); // clear current code in preparation for code entry + std::fill_n(currentCode, sizeof(currentCode), 255); // clear current code in preparation for code entry } else { // long tapped not in main watchface; go back to previous state screenRefreshRequired = true; } - lastInputTime = realTime; + lastLongClickTime = xTaskGetTickCount(); motor.RunForDuration(20); - return true; } else { // Not on watchface; go back to main watchface screenRefreshRequired = true; - currentState = Displaying::watchface; - // reset lastInputTime so it always needs two long taps to get back to blank, even if you're fast - lastInputTime = std::chrono::time_point(); + currentState = Displaying::WatchFace; + // set lastLongClickTime to be in the past so it always needs two long taps to get back to blank, even if you're fast + lastLongClickTime = xTaskGetTickCount() - doubleDoubleClickDelay; motor.RunForDuration(20); - return true; } -} + // no situation where long tap doesn't get handled + return true; +} bool WatchFaceMaze::HandleTap() { // confetti must only display on autismcreature - if (currentState != Displaying::autismcreature) {return false;} - // only need to set confettiActive, everything else is handled in functions called by refresh() + if (currentState != Displaying::AutismCreature) { + return false; + } + // only need to set initConfetti, everything else is handled in functions called by refresh() initConfetti = true; return true; } - -bool WatchFaceMaze::HandleSwipe(uint8_t direction) { +bool WatchFaceMaze::HandleSwipe(const uint8_t direction) { // Don't handle any swipes on watchface - if (currentState == Displaying::watchface) return false; + if (currentState == Displaying::WatchFace) { + return false; + } // Add the new direction to the swipe list, dropping the last item - for (int i = sizeof(currentCode)-1; i > 0; i--) {currentCode[i] = currentCode[i-1];} + for (unsigned int i = sizeof(currentCode) - 1; i > 0; i--) { + currentCode[i] = currentCode[i - 1]; + } currentCode[0] = direction; // check if valid code has been entered - // Displaying::watchface is used here simply as a dummy value, and it will never transition to that - Displaying newState = Displaying::watchface; - if (std::memcmp(currentCode, lossCode, sizeof(lossCode)) == 0) {newState = Displaying::loss;} // loss - else if (std::memcmp(currentCode, amogusCode, sizeof(amogusCode)) == 0) {newState = Displaying::amogus;} // amogus - else if (std::memcmp(currentCode, autismCode, sizeof(autismCode)) == 0) {newState = Displaying::autismcreature;} // autismcreature/tbh - else if (std::memcmp(currentCode, foxCode, sizeof(foxCode)) == 0) {newState = Displaying::foxgame;} // foxxo game - else if (std::memcmp(currentCode, reminderCode, sizeof(reminderCode)) == 0) {newState = Displaying::reminder;} // reminder - else if (std::memcmp(currentCode, pinetimeCode, sizeof(pinetimeCode)) == 0) {newState = Displaying::pinetime;} // pinetime logo + // Displaying::WatchFace is used here simply as a dummy value, and it will never transition to that + Displaying newState = Displaying::WatchFace; + if (std::memcmp(currentCode, lossCode, sizeof(lossCode)) == 0) { + newState = Displaying::Loss; + } else if (std::memcmp(currentCode, amogusCode, sizeof(amogusCode)) == 0) { + newState = Displaying::Amogus; + } else if (std::memcmp(currentCode, autismCode, sizeof(autismCode)) == 0) { + newState = Displaying::AutismCreature; + } else if (std::memcmp(currentCode, foxCode, sizeof(foxCode)) == 0) { + newState = Displaying::FoxGame; + } else if (std::memcmp(currentCode, reminderCode, sizeof(reminderCode)) == 0) { + newState = Displaying::GameReminder; + } else if (std::memcmp(currentCode, pinetimeCode, sizeof(pinetimeCode)) == 0) { + newState = Displaying::PineTime; + } // only request a screen refresh if state has been updated - if (newState != Displaying::watchface) { + if (newState != Displaying::WatchFace) { currentState = newState; screenRefreshRequired = true; motor.RunForDuration(10); - std::fill_n(currentCode, sizeof(currentCode), 255); // clear code + std::fill_n(currentCode, sizeof(currentCode), 0xFF); // clear code } return true; } - // Clear maze void WatchFaceMaze::InitializeMaze() { - maze.fill(MazeTile().setLeft(true).setUp(true).setFlagEmpty(true)); + maze.Fill(MazeTile().SetLeft(true).SetUp(true).SetFlagEmpty(true)); } - // seeds the maze with whatever the current state needs void WatchFaceMaze::SeedMaze() { switch (currentState) { - case Displaying::watchface: - PutTimeDate(); break; - case Displaying::blank: { // seed maze with 4 tiles - const int randx = prng.rand(0, 20); - const int randy = prng.rand(3, 20); - maze.pasteMazeSeed(randx, randy, randx + 3, randy, blankseed); + case Displaying::WatchFace: + PutTime(); + break; + case Displaying::Blank: { + // seed maze with 4 tiles + const coord_t randX = (coord_t) prng.Rand(0, 20); + const coord_t randY = (coord_t) prng.Rand(3, 20); + maze.PasteMazeSeed(randX, randY, randX + 3, randY, blankseed); break; } - case Displaying::loss: - maze.pasteMazeSeed(2, 2, 22, 21, loss); break; - case Displaying::amogus: - maze.pasteMazeSeed(3, 0, 21, 23, amogus); break; - case Displaying::autismcreature: - maze.pasteMazeSeed(0, 2, 23, 22, autismcreature); break; - case Displaying::foxgame: - maze.pasteMazeSeed(0, 1, 23, 22, foxgame); break; - case Displaying::reminder: - maze.pasteMazeSeed(0, 3, 23, 19, reminder); break; - case Displaying::pinetime: - maze.pasteMazeSeed(2, 0, 21, 23, pinetime); break; + case Displaying::Loss: + maze.PasteMazeSeed(2, 2, 22, 21, loss); + break; + case Displaying::Amogus: + maze.PasteMazeSeed(3, 0, 21, 23, amogus); + break; + case Displaying::AutismCreature: + maze.PasteMazeSeed(0, 2, 23, 22, autismCreature); + break; + case Displaying::FoxGame: + maze.PasteMazeSeed(0, 1, 23, 22, foxGame); + break; + case Displaying::GameReminder: + maze.PasteMazeSeed(0, 3, 23, 19, gameReminder); + break; + case Displaying::PineTime: + maze.PasteMazeSeed(2, 0, 21, 23, pinetime); + break; } } - // Put time and date info on the screen. -void WatchFaceMaze::PutTimeDate() { +void WatchFaceMaze::PutTime() { uint8_t hours = dateTimeController.Hours(); uint8_t minutes = dateTimeController.Minutes(); // modify hours to account for 12 hour format if (settingsController.GetClockType() == Controllers::Settings::ClockType::H12) { // if 0am in 12 hour format, it's 12am - if (hours == 0) {hours = 12;} + if (hours == 0) { + hours = 12; + } // if after noon in 12 hour format, shift over by 12 hours if (hours > 12) { - maze.pasteMazeSeed(18, 15, 22, 22, pm); + maze.PasteMazeSeed(18, 15, 22, 22, pm); hours -= 12; } else { - maze.pasteMazeSeed(18, 15, 22, 22, am); + maze.PasteMazeSeed(18, 15, 22, 22, am); } } // put time on screen - maze.pasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); // top left: hours major digit - maze.pasteMazeSeed(10, 1, 15, 10, numbers[hours % 10]); // top right: hours minor digit - maze.pasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); // bottom left: minutes major digit - maze.pasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); // bottom right: minutes minor digit + maze.PasteMazeSeed(3, 1, 8, 10, numbers[hours / 10]); // top left: hours major digit + maze.PasteMazeSeed(10, 1, 15, 10, numbers[hours % 10]); // top right: hours minor digit + maze.PasteMazeSeed(3, 13, 8, 22, numbers[minutes / 10]); // bottom left: minutes major digit + maze.PasteMazeSeed(10, 13, 15, 22, numbers[minutes % 10]); // bottom right: minutes minor digit // reserve some space at the top right to put the battery and BLE indicators there - maze.pasteMazeSeed(21, 0, 23, 2, indicatorSpace); + maze.PasteMazeSeed(21, 0, 23, 2, indicatorSpace); } - // Generates the maze around whatever it was seeded with void WatchFaceMaze::GenerateMaze() { - int x, y; + coord_t x = -1; + coord_t y = -1; // task should only run for 3/4 the time it takes for the task to refresh. // Will go over; only checks once it's finished with current line. It won't go too far over though. - auto maxGenTarget = dateTimeController.CurrentDateTime() + std::chrono::milliseconds((taskRefresh->period*3)/4); + uint32_t mazeGenStartTime = xTaskGetTickCount(); while (true) { // find position to start generating a path from for (uint8_t i = 0; i < 30; i++) { - x = (int)prng.rand(0, Maze::WIDTH-1); - y = (int)prng.rand(0, Maze::HEIGHT-1); - if (maze.getSide(x,y, TileAttr::flagempty)) {break;} // found solution tile + x = (coord_t) prng.Rand(0, Maze::WIDTH - 1); + y = (coord_t) prng.Rand(0, Maze::HEIGHT - 1); + if (maze.GetTileAttr(x, y, TileAttr::FlagEmpty)) { + break; + } // found solution tile if (i == 29) { // failed all 30 attempts (this is inside the for loop for 'organization') // find solution tile slowly but guaranteed (scan over entire field and choose random valid tile) int count = 0; // count number of valid tiles - for (int j = 0; j < Maze::WIDTH*Maze::HEIGHT; j++) - {if (maze.getSide(j, TileAttr::flagempty)) {count++;}} + for (int32_t j = 0; j < Maze::WIDTH * Maze::HEIGHT; j++) { + if (maze.GetTileAttr(j, TileAttr::FlagEmpty)) { + count++; + } + } // if no valid tiles are left, maze is done if (count == 0) { @@ -528,9 +596,11 @@ void WatchFaceMaze::GenerateMaze() { // if execution gets here then maze gen is not done. select random index from valid tiles to start from // 'count' is now used as an index - count = prng.rand(1, count); - for (int j = 0; j < Maze::WIDTH*Maze::HEIGHT; j++) { - if (maze.getSide(j, TileAttr::flagempty)) {count--;} + count = (coord_t) prng.Rand(1, count); + for (int32_t j = 0; j < Maze::WIDTH * Maze::HEIGHT; j++) { + if (maze.GetTileAttr(j, TileAttr::FlagEmpty)) { + count--; + } if (count == 0) { y = j / Maze::WIDTH; x = j % Maze::WIDTH; @@ -543,7 +613,7 @@ void WatchFaceMaze::GenerateMaze() { GeneratePath(x, y); // if generating paths took too long, suspend it - if (dateTimeController.CurrentDateTime() > maxGenTarget) { + if (xTaskGetTickCount() - mazeGenStartTime > taskRefresh->period * 3 / 4) { pausedGeneration = true; return; } @@ -551,81 +621,109 @@ void WatchFaceMaze::GenerateMaze() { // execution never gets here! it returns earlier in the function. } - -void WatchFaceMaze::GeneratePath(int x, int y) { - int oldx, oldy; // used in backtracking - uint8_t direction; // which direction the cursor moved in +void WatchFaceMaze::GeneratePath(coord_t x, coord_t y) { + // oldX, oldY are used in backtracking + coord_t oldX; + coord_t oldY; + // which direction the cursor moved in + uint8_t direction; while (true) { // set current tile to reflect that it's been worked on - maze.setSide(x, y, TileAttr::flagempty, false); // no longer empty - maze.setSide(x, y, TileAttr::flaggen, true); // in generation - oldx = x, oldy = y; // used in backtracking + maze.SetAttr(x, y, TileAttr::FlagEmpty, false); // no longer empty + maze.SetAttr(x, y, TileAttr::FlagGen, true); // in generation + oldX = x, oldY = y; // used in backtracking // move to next tile - // the if statements are very scuffed, but they prevent backtracking. + // the if statements are very scuffed, but they prevent turning around. while (true) { - switch (direction = prng.rand(0, 3)) { + switch (direction = prng.Rand(0, 3)) { case 0: // moved up - if (y <= 0 || !maze.getSide(x, y, TileAttr::up)) {continue;} + if (y <= 0 || !maze.GetTileAttr(x, y, TileAttr::Up)) { + continue; + } y -= 1; break; case 1: // moved left - if (x <= 0 || !maze.getSide(x, y, TileAttr::left)) {continue;} + if (x <= 0 || !maze.GetTileAttr(x, y, TileAttr::Left)) { + continue; + } x -= 1; break; case 2: // moved down - if (y >= Maze::HEIGHT - 1 || !maze.getSide(x, y, TileAttr::down)) {continue;} + if (y >= Maze::HEIGHT - 1 || !maze.GetTileAttr(x, y, TileAttr::Down)) { + continue; + } y += 1; break; case 3: // moved right - if (x >= Maze::WIDTH - 1 || !maze.getSide(x, y, TileAttr::right)) {continue;} + if (x >= Maze::WIDTH - 1 || !maze.GetTileAttr(x, y, TileAttr::Right)) { + continue; + } x += 1; break; - default: // invalid + default: // invalid, will never hit in normal operation std::abort(); } break; } // moved to next tile, check if looped in on self - if (!maze.getSide(x, y, TileAttr::flaggen)) { + if (!maze.GetTileAttr(x, y, TileAttr::FlagGen)) { + // did NOT loop in on self, simply remove wall and move on switch (direction) { - case 0: maze.setSide(x, y, TileAttr::down, false); break; // moved up - case 1: maze.setSide(x, y, TileAttr::right, false); break; // moved left - case 2: maze.setSide(x, y, TileAttr::up, false); break; // moved down - case 3: maze.setSide(x, y, TileAttr::left, false); break; // moved right + case 0: + maze.SetAttr(x, y, TileAttr::Down, false); + break; // moved up + case 1: + maze.SetAttr(x, y, TileAttr::Right, false); + break; // moved left + case 2: + maze.SetAttr(x, y, TileAttr::Up, false); + break; // moved down + case 3: + maze.SetAttr(x, y, TileAttr::Left, false); + break; // moved right + default: // invalid, will never hit in normal operation + std::abort(); } + // if attached to main maze, path finished generating - if (!maze.getSide(x, y, TileAttr::flagempty)) { + if (!maze.GetTileAttr(x, y, TileAttr::FlagEmpty)) { break; } } else { + // DID loop in on self, track down and eliminate loop // targets are the coordinates of where it needs to backtrack to - const int targetx = x, targety = y; - x = oldx, y = oldy; - while (x != targetx || y != targety) { - if (y > 0 && (maze.getSide(x, y, TileAttr::up) == false)) { // backtrack up - maze.setSide(x, y, TileAttr::up, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); + const coord_t targetX = x; + const coord_t targetY = y; + x = oldX, y = oldY; + while (x != targetX || y != targetY) { + if (y > 0 && (maze.GetTileAttr(x, y, TileAttr::Up) == false)) { + // backtrack up + maze.SetAttr(x, y, TileAttr::Up, true); + maze.SetAttr(x, y, TileAttr::FlagGen, false); + maze.SetAttr(x, y, TileAttr::FlagEmpty, true); y -= 1; - } else if (x > 0 && (maze.getSide(x, y, TileAttr::left) == false)) { // backtrack left - maze.setSide(x, y, TileAttr::left, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); + } else if (x > 0 && (maze.GetTileAttr(x, y, TileAttr::Left) == false)) { + // backtrack left + maze.SetAttr(x, y, TileAttr::Left, true); + maze.SetAttr(x, y, TileAttr::FlagGen, false); + maze.SetAttr(x, y, TileAttr::FlagEmpty, true); x -= 1; - } else if (y < Maze::HEIGHT - 1 && (maze.getSide(x, y, TileAttr::down) == false)) { // backtrack down - maze.setSide(x, y, TileAttr::down, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); + } else if (y < Maze::HEIGHT - 1 && (maze.GetTileAttr(x, y, TileAttr::Down) == false)) { + // backtrack down + maze.SetAttr(x, y, TileAttr::Down, true); + maze.SetAttr(x, y, TileAttr::FlagGen, false); + maze.SetAttr(x, y, TileAttr::FlagEmpty, true); y += 1; - } else if (x < Maze::WIDTH && (maze.getSide(x, y, TileAttr::right) == false)) { // backtrack right - maze.setSide(x, y, TileAttr::right, true); - maze.setSide(x, y, TileAttr::flaggen, false); - maze.setSide(x, y, TileAttr::flagempty, true); + } else if (x < Maze::WIDTH && (maze.GetTileAttr(x, y, TileAttr::Right) == false)) { + // backtrack right + maze.SetAttr(x, y, TileAttr::Right, true); + maze.SetAttr(x, y, TileAttr::FlagGen, false); + maze.SetAttr(x, y, TileAttr::FlagEmpty, true); x += 1; } else { // bad backtrack; die @@ -637,47 +735,57 @@ void WatchFaceMaze::GeneratePath(int x, int y) { } // finished generating the entire path // mark all tiles as finalized and not in generation by removing ALL flaggen's - maze.fill(0, MazeTile::FLAGGENMASK); + maze.Fill(0, MazeTile::FLAGGENMASK); } - // goes through the maze, finds disconnected segments and connects them void WatchFaceMaze::ForceValidMaze() { - // crude maze-optimized flood fill: follow a path until can't move any more, then find some other location to follow from. repeat. - // once it's traversed all reachable tiles, checks if there are any tiles that have not been traversed. if there are, then find a border - // between the traversed and non-traversed segments. poke a hole at one of these borders randomly. - // once the hole has been poked, more maze is reachable. continue this "fill-search then poke" scheme until the entire maze is accessible. - // this function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in normal control flow) - + // Crude maze-optimized flood fill: follow a path until can't move any more, then find some other location to follow from. repeat. + // Once it's traversed all reachable tiles, checks if there are any tiles that have not been traversed. if there are, then find a border + // between the traversed and non-traversed segments. poke a hole at one of these borders randomly. + // Once the hole has been poked, more maze is reachable. continue this "fill-search then poke" scheme until the entire maze is accessible. + // This function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in normal control flow) + // initialize cursor x and y to bottom right - int x = Maze::WIDTH-1, y = Maze::HEIGHT - 1; + coord_t x = Maze::WIDTH - 1; + coord_t y = Maze::HEIGHT - 1; while (true) { // sorry for using goto but this needs to be really nested and the components are too integrated to split out into functions... - ForceValidMazeLoop: - maze.setSide(x, y, TileAttr::flaggen, true); + ForceValidMazeLoop: + maze.SetAttr(x, y, TileAttr::FlagGen, true); // move cursor - if (y > 0 && !maze.getSide(x, y, TileAttr::up) && !maze.getSide(x, y-1, TileAttr::flaggen)) {y--;} - else if (x < Maze::WIDTH-1 && !maze.getSide(x, y, TileAttr::right) && !maze.getSide(x+1, y, TileAttr::flaggen)) {x++;} - else if (y < Maze::HEIGHT-1 && !maze.getSide(x, y, TileAttr::down) && !maze.getSide(x, y+1, TileAttr::flaggen)) {y++;} - else if (x > 0 && !maze.getSide(x, y, TileAttr::left) && !maze.getSide(x-1, y, TileAttr::flaggen)) {x--;} - else { - int pokeLocationCount = 0; + if (y > 0 && !maze.GetTileAttr(x, y, TileAttr::Up) && !maze.GetTileAttr(x, y - 1, TileAttr::FlagGen)) { + y--; + } else if (x < Maze::WIDTH - 1 && !maze.GetTileAttr(x, y, TileAttr::Right) && !maze.GetTileAttr(x + 1, y, TileAttr::FlagGen)) { + x++; + } else if (y < Maze::HEIGHT - 1 && !maze.GetTileAttr(x, y, TileAttr::Down) && !maze.GetTileAttr(x, y + 1, TileAttr::FlagGen)) { + y++; + } else if (x > 0 && !maze.GetTileAttr(x, y, TileAttr::Left) && !maze.GetTileAttr(x - 1, y, TileAttr::FlagGen)) { + x--; + } else { + unsigned int pokeLocationCount = 0; // couldn't find any position to move to, need to set cursor to a different usable location - for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { - for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { - const bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + for (coord_t proposedY = 0; proposedY < Maze::HEIGHT; proposedY++) { + for (coord_t proposedX = 0; proposedX < Maze::WIDTH; proposedX++) { + const bool ownState = maze.GetTileAttr(proposedX, proposedY, TileAttr::FlagGen); // if tile to the left is of a different traversal state (is traversed boundary) - if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { + if (proposedX > 0 && (maze.GetTileAttr(proposedX - 1, proposedY, TileAttr::FlagGen) != ownState)) { // if found boundary AND can get to it, just continue working from here - if (maze.getSide(proposedx, proposedy, TileAttr::left) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} + if (maze.GetTileAttr(proposedX, proposedY, TileAttr::Left) == false) { + x = proposedX, y = proposedY; + goto ForceValidMazeLoop; + } pokeLocationCount++; } - // if tile to up is of a different traversal state (is traversed boundary) - if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { + // if tile up is of a different traversal state (is traversed boundary) + if (proposedY > 0 && (maze.GetTileAttr(proposedX, proposedY - 1, TileAttr::FlagGen) != ownState)) { // if found boundary AND can get to it, just continue working from here - if (maze.getSide(proposedx, proposedy, TileAttr::up) == false) {x = proposedx, y = proposedy; goto ForceValidMazeLoop;} + if (maze.GetTileAttr(proposedX, proposedY, TileAttr::Up) == false) { + x = proposedX, y = proposedY; + goto ForceValidMazeLoop; + } pokeLocationCount++; } } @@ -685,34 +793,37 @@ void WatchFaceMaze::ForceValidMaze() { // finished scanning maze; there are no locations the cursor can be placed for it to continue scanning // if there are no walls that can be poked through to increase reachable area, maze is finished - if (pokeLocationCount == 0) {return;} + if (pokeLocationCount == 0) { + return; + } // if execution gets here, need to poke a hole. // choose a random poke location to poke a hole through. pokeLocationCount is now used as an index - pokeLocationCount = prng.rand(1, pokeLocationCount); - for (int proposedy = 0; proposedy < Maze::HEIGHT; proposedy++) { - for (int proposedx = 0; proposedx < Maze::WIDTH; proposedx++) { + pokeLocationCount = (int) prng.Rand(1, pokeLocationCount); + + for (coord_t proposedY = 0; proposedY < Maze::HEIGHT; proposedY++) { + for (coord_t proposedX = 0; proposedX < Maze::WIDTH; proposedX++) { // pretty much a copy of the previous code which FINDS poke locations, but now with the goal of actually doing the poking - const bool ownState = maze.getSide(proposedx, proposedy, TileAttr::flaggen); + const bool ownState = maze.GetTileAttr(proposedX, proposedY, TileAttr::FlagGen); - if (proposedx > 0 && (maze.getSide(proposedx-1, proposedy, TileAttr::flaggen) != ownState)) { + if (proposedX > 0 && (maze.GetTileAttr(proposedX - 1, proposedY, TileAttr::FlagGen) != ownState)) { pokeLocationCount--; // found the target poke location, poke and loop if (pokeLocationCount == 0) { - maze.setSide(proposedx, proposedy, TileAttr::left, false); - x = proposedx, y = proposedy; + maze.SetAttr(proposedX, proposedY, TileAttr::Left, false); + x = proposedX, y = proposedY; goto ForceValidMazeLoop; // continue OUTSIDE loop } } - // if tile to up is of a different traversal state (is traversed boundary) - if (proposedy > 0 && (maze.getSide(proposedx, proposedy-1, TileAttr::flaggen) != ownState)) { + // if tile up is of a different traversal state (is traversed boundary) + if (proposedY > 0 && (maze.GetTileAttr(proposedX, proposedY - 1, TileAttr::FlagGen) != ownState)) { pokeLocationCount--; // found the target poke location, poke and loop if (pokeLocationCount == 0) { - maze.setSide(proposedx, proposedy, TileAttr::up, false); - x = proposedx, y = proposedy; - goto ForceValidMazeLoop; // continue processing + maze.SetAttr(proposedX, proposedY, TileAttr::Up, false); + x = proposedX, y = proposedY; + goto ForceValidMazeLoop; // continue processing } } } @@ -722,72 +833,99 @@ void WatchFaceMaze::ForceValidMaze() { } } - void WatchFaceMaze::DrawMaze() { // this used to be nice code, but it was retrofitted to print offset by 1 pixel for a fancy border. // I'm not proud of the logic but it works. lv_area_t area; - swapActiveBuffer(); // who knows who used the buffer before this + SwapActiveBuffer(); // who knows who used the buffer before this // Print horizontal lines // This doesn't bother with corners, those just get overwritten by the vertical lines area.x1 = 1; area.x2 = 238; - for (int y = 1; y < Maze::HEIGHT; y++) { - for (int x = 0; x < Maze::WIDTH; x++) { - if (maze.get(x, y).getUp()) {std::fill_n(&activeBuffer[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_WHITE);} - else {std::fill_n(&activeBuffer[x*Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK);} + for (coord_t y = 1; y < Maze::HEIGHT; y++) { + for (coord_t x = 0; x < Maze::WIDTH; x++) { + if (maze.Get(x, y).GetUp()) { + std::fill_n(&activeBuffer[x * Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_WHITE); + } else { + std::fill_n(&activeBuffer[x * Maze::TILESIZE], Maze::TILESIZE, LV_COLOR_BLACK); + } } std::copy_n(activeBuffer, 238, &activeBuffer[238]); area.y1 = (Maze::TILESIZE * y) - 1; area.y2 = (Maze::TILESIZE * y); lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); - swapActiveBuffer(); + SwapActiveBuffer(); } // Print vertical lines area.y1 = 1; area.y2 = 238; - for (int x = 1; x < Maze::WIDTH; x++) { - for (int y = 0; y < Maze::HEIGHT; y++) { - MazeTile curblock = maze.get(x,y); + for (coord_t x = 1; x < Maze::WIDTH; x++) { + for (coord_t y = 0; y < Maze::HEIGHT; y++) { + MazeTile curblock = maze.Get(x, y); // handle corners: if any of the touching lines are present, add corner. else leave it black - if (curblock.getUp() || curblock.getLeft() || maze.get(x-1,y).getUp() || maze.get(x,y-1).getLeft()) - {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2], 4, LV_COLOR_WHITE);} - else {std::fill_n(&activeBuffer[y*Maze::TILESIZE*2], 4, LV_COLOR_BLACK);} + if (curblock.GetUp() || curblock.GetLeft() || maze.Get(x - 1, y).GetUp() || maze.Get(x, y - 1).GetLeft()) { + std::fill_n(&activeBuffer[y * Maze::TILESIZE * 2], 4, LV_COLOR_WHITE); + } else { + std::fill_n(&activeBuffer[y * Maze::TILESIZE * 2], 4, LV_COLOR_BLACK); + } // handle actual wall segments - if (curblock.getLeft()) {std::fill_n(&activeBuffer[(y*Maze::TILESIZE*2)+4], (Maze::TILESIZE*2)-4, LV_COLOR_WHITE);} - else {std::fill_n(&activeBuffer[(y*Maze::TILESIZE*2)+4], (Maze::TILESIZE*2)-4, LV_COLOR_BLACK);} + if (curblock.GetLeft()) { + std::fill_n(&activeBuffer[(y * Maze::TILESIZE * 2) + 4], (Maze::TILESIZE * 2) - 4, LV_COLOR_WHITE); + } else { + std::fill_n(&activeBuffer[(y * Maze::TILESIZE * 2) + 4], (Maze::TILESIZE * 2) - 4, LV_COLOR_BLACK); + } } - area.x1 = Maze::TILESIZE * x - 1; - area.x2 = Maze::TILESIZE * x; + area.x1 = (Maze::TILESIZE * x) - 1; + area.x2 = (Maze::TILESIZE * x); lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, &activeBuffer[4]); - swapActiveBuffer(); + SwapActiveBuffer(); } // Print borders // don't need to worry about switching buffers here since buffer contents aren't changing std::fill_n(activeBuffer, 240, LV_COLOR_GRAY); for (int i = 0; i < 4; i++) { - if (i==0) {area.x1=0; area.x2=239; area.y1=0; area.y2=0; } // top - else if (i==1) {area.x1=0; area.x2=239; area.y1=239; area.y2=239;} // bottom - else if (i==2) {area.x1=0; area.x2=0; area.y1=0; area.y2=239;} // left - else if (i==3) {area.x1=239; area.x2=239; area.y1=0; area.y2=239;} // right + if (i == 0) { + area.x1 = 0; + area.x2 = 239; + area.y1 = 0; + area.y2 = 0; + } // top + else if (i == 1) { + area.x1 = 0; + area.x2 = 239; + area.y1 = 239; + area.y2 = 239; + } // bottom + else if (i == 2) { + area.x1 = 0; + area.x2 = 0; + area.y1 = 0; + area.y2 = 239; + } // left + else if (i == 3) { + area.x1 = 239; + area.x2 = 239; + area.y1 = 0; + area.y2 = 239; + } // right lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); lvgl.FlushDisplay(&area, activeBuffer); } } - -void WatchFaceMaze::DrawTileInner(int16_t x, int16_t y, lv_color_t color) { +void WatchFaceMaze::DrawTileInner(const coord_t x, const coord_t y, const lv_color_t color) { // early exit if would print OOB - if (x < 0 || y < 0 || x > Maze::WIDTH-1 || y > Maze::HEIGHT-1) - {return;} + if (x < 0 || y < 0 || x > Maze::WIDTH - 1 || y > Maze::HEIGHT - 1) { + return; + } // prepare buffer - swapActiveBuffer(); + SwapActiveBuffer(); std::fill_n(activeBuffer, 64, color); lv_area_t area; @@ -802,38 +940,38 @@ void WatchFaceMaze::DrawTileInner(int16_t x, int16_t y, lv_color_t color) { lvgl.FlushDisplay(&area, activeBuffer); } - void WatchFaceMaze::ClearConfetti() { // prevent superfluous calls - if (!confettiActive) {return;} + if (!confettiActive) { + return; + } // clear all particles and reset state - for (const ConfettiParticle &particle : confettiArr) { - DrawTileInner(particle.tileX(), particle.tileY(), LV_COLOR_BLACK); + for (const ConfettiParticle& particle : confettiArr) { + DrawTileInner(particle.TileX(), particle.TileY(), LV_COLOR_BLACK); } confettiActive = false; } - void WatchFaceMaze::ProcessConfetti() { // and draw all the confetti // flag "done" stays true if all step() calls stated that the particle was done, otherwise it goes false - bool done = true; - for (ConfettiParticle &particle : confettiArr) { - int16_t oldx = particle.tileX(); - int16_t oldy = particle.tileY(); - // if any step() calls return false (i.e. not finished), done gets set to false as well - done = particle.step() && done; + bool isDone = true; + for (ConfettiParticle& particle : confettiArr) { + const coord_t oldX = particle.TileX(); + const coord_t oldY = particle.TileY(); + // if any step() calls return false (i.e. not finished), isDone gets set to false as well + isDone = particle.Step() && isDone; // need to redraw? - if (oldx != particle.tileX() || oldy != particle.tileY()) { - DrawTileInner(oldx, oldy, LV_COLOR_BLACK); - DrawTileInner(particle.tileX(), particle.tileY(), particle.color); + if (oldX != particle.TileX() || oldY != particle.TileY()) { + DrawTileInner(oldX, oldY, LV_COLOR_BLACK); + DrawTileInner(particle.TileX(), particle.TileY(), particle.color); } } // handle done flag // should only set confettiActive to false, since all confetti will have been cleared as it moved out of frame - if (done) { + if (isDone) { confettiActive = false; } } \ No newline at end of file diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 407354ae30..2c0da8abd6 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -1,8 +1,15 @@ #pragma once +#include +#include +#include "FreeRTOS.h" +#include "task.h" // configTICK_RATE_HZ +#include "sys/unistd.h" +#include "displayapp/LittleVgl.h" #include "displayapp/apps/Apps.h" #include "displayapp/screens/Screen.h" #include "displayapp/Controllers.h" +#include "displayapp/screens/Tile.h" #include "components/datetime/DateTimeController.h" #include "utility/DirtyValue.h" #include "displayapp/LittleVgl.h" @@ -11,9 +18,10 @@ namespace Pinetime { namespace Applications { namespace Screens { + using coord_t = int16_t; // Really just an abstraction of a uint8_t but with functions to get the individual bits. - // reading up on bitfields and their potential inconsistencies scared me away from them... + // Not using bitfields because they can't guarantee value positions, and I need everything in the low 4 bits. struct MazeTile { static constexpr uint8_t UPMASK = 0b0001; static constexpr uint8_t LEFTMASK = 0b0010; @@ -22,33 +30,61 @@ namespace Pinetime { uint8_t map = 0; // Set flags on given tile. Returns the object so they can be chained. - // Am I cool for aligning it like this? Or does it just make it hard to read... - MazeTile setUp(bool value) {map = (map&~UPMASK) | (value * UPMASK); return *this;} - MazeTile setLeft(bool value) {map = (map&~LEFTMASK) | (value * LEFTMASK); return *this;} - MazeTile setFlagEmpty(bool value) {map = (map&~FLAGEMPTYMASK) | (value * FLAGEMPTYMASK); return *this;} - MazeTile setFlagGen(bool value) {map = (map&~FLAGGENMASK) | (value * FLAGGENMASK); return *this;} + + MazeTile SetUp(const bool value) { + map = (map & ~UPMASK) | (value * UPMASK); + return *this; + } + + MazeTile SetLeft(const bool value) { + map = (map & ~LEFTMASK) | (value * LEFTMASK); + return *this; + } + + MazeTile SetFlagEmpty(const bool value) { + map = (map & ~FLAGEMPTYMASK) | (value * FLAGEMPTYMASK); + return *this; + } + + MazeTile SetFlagGen(const bool value) { + map = (map & ~FLAGGENMASK) | (value * FLAGGENMASK); + return *this; + } // Get flags on given tile - bool getUp() const {return map & UPMASK;} - bool getLeft() const {return map & LEFTMASK;} - bool getFlagEmpty() const {return map & FLAGEMPTYMASK;} - bool getFlagGen() const {return map & FLAGGENMASK;} - }; + bool GetUp() const { + return map & UPMASK; + } + bool GetLeft() const { + return map & LEFTMASK; + } + bool GetFlagEmpty() const { + return map & FLAGEMPTYMASK; + } + + bool GetFlagGen() const { + return map & FLAGGENMASK; + } + }; // Custom PRNG for the maze to easily allow it to be deterministic for any given minute - // Maybe faster too? Probably not by much if it is. class MazeRNG { public: - MazeRNG(uint64_t start_seed = 64) {seed(start_seed);} + MazeRNG(const uint64_t start_seed = 64) { + Seed(start_seed); + } // Reseed the generator. Handles any input well. If seed is 0, acts as though it was seeded with 1 (prevents breakage). - void seed(uint64_t seed) {state = seed ? seed : 1; rand();} + void Seed(const uint64_t seed) { + state = seed ? seed : 1; + Rand(); + } // RNG lifted straight from https://en.wikipedia.org/wiki/Xorshift#xorshift* (asterisk is part of the link) - uint32_t rand() { + uint32_t Rand() { state ^= state >> 12; state ^= state << 25; state ^= state >> 27; @@ -56,17 +92,17 @@ namespace Pinetime { } // Random in range, inclusive on both ends (don't make max=min); return (rand()%(max-min+1))+min;} + uint32_t Rand(const uint32_t min, const uint32_t max) { + assert(max >= min); + return (Rand() % (max - min + 1)) + min; + } private: uint64_t state; }; - - - - + // Little bit of convenience for working with tile flags - enum TileAttr {up, down, left, right, flagempty, flaggen}; + enum TileAttr { Up, Down, Left, Right, FlagEmpty, FlagGen }; // could also be called Field or something. Does not handle stuff like generation or printing, // ONLY handles interacting with the board. @@ -75,91 +111,89 @@ namespace Pinetime { Maze(); // Get and set can work with either xy or indexes - MazeTile get(int x, int y); - MazeTile get(int index); + MazeTile Get(coord_t x, coord_t y) const; + MazeTile Get(int32_t index) const; - void set(int x, int y, MazeTile tile); - void set(int index, MazeTile tile); + void Set(coord_t x, coord_t y, MazeTile tile); + void Set(int32_t index, MazeTile tile); // Allows abstractly setting a given side on a tile. Supports down and right for convenience. - void setSide(int x, int y, TileAttr attr, bool value); - void setSide(int index, TileAttr attr, bool value); + void SetAttr(coord_t x, coord_t y, TileAttr attr, bool value); + void SetAttr(int32_t index, TileAttr attr, bool value); + + // Same as SetAttr, just getting. + bool GetTileAttr(coord_t x, coord_t y, TileAttr attr) const; + bool GetTileAttr(int32_t index, TileAttr attr) const; - // Same as setSide, just getting. - bool getSide(int x, int y, TileAttr attr); - bool getSide(int index, TileAttr attr); - // fill() fills all tiles in the maze with a given value, optionally with a mask on what bits to change. // Only cares about the low 4 bits in the value and mask. // (use the MazeTile::--MASK values for mask) - void fill(MazeTile tile, uint8_t mask = 0xFF); - void fill(uint8_t value, uint8_t mask = 0xFF); + void Fill(MazeTile tile, uint8_t mask = 0xFF); + void Fill(uint8_t value, uint8_t mask = 0xFF); // Paste onto an empty board. Marks a tile as not empty if any neighboring walls are gone. // toPaste is a 1d array of uint8_t, only containing the two wall bits (left then up). So that's 4 walls in a byte. - // If you have a weird number of tiles (like 17), just pad it out to the nearest byte. The function ignores any extra data. + // If you have a weird number of tiles (like 17), just pad it out to the next byte. The function ignores any extra data. // Always places values left to right, top to bottom. Coords are inclusive. - void pasteMazeSeed(int x1, int y1, int x2, int y2, const uint8_t toPaste[]); + void PasteMazeSeed(coord_t x1, coord_t y1, coord_t x2, coord_t y2, const uint8_t toPaste[]); // 10x10 px tiles on the maze = 24x24 (on 240px screen) // Warning: While these are respected in most functions, changing these could break other features like number displaying and - // indicators. - static constexpr int WIDTH = 24; - static constexpr int HEIGHT = 24; + // indicators. + static constexpr coord_t WIDTH = 24; + static constexpr coord_t HEIGHT = 24; static constexpr int TILESIZE = 10; // The actual size of the entire map. only store 4 bits per block, so 2 blocks per byte - static constexpr int FLATSIZE = WIDTH*HEIGHT/2; + static constexpr int32_t FLATSIZE = WIDTH * HEIGHT / 2; private: // The internal map. Doesn't actually store MazeTiles, but packs their contents to 2 tiles per byte. - uint8_t mazemap[FLATSIZE]; + uint8_t mazeMap[FLATSIZE]; }; - - - // Click on the autismcreature for a colorful surprise class ConfettiParticle { public: - // steps the confetti simulation. Importantly, updates the maze equivalent position. + // Steps the confetti simulation. Importantly, updates the maze equivalent position. // Returns true if the particle has finished processing, else false. // Need to save the particle position elsewhere before stepping to be able to clear the old particle position (if redraw needed). - // I couldn't really figure a better way to do it other than saving the old positions in the class itself and that's just awful. - bool step(); + bool Step(); - // reset position and generate new velocity using the passed prng - void reset(MazeRNG &prng); + // Reset position and generate new direction + velocity using the passed rng object + void Reset(MazeRNG& prng); // x and y positions of the particle. Positions are in pixels, so just divide by tile size to get the tile it's in. - int16_t tileX() const {return xpos > 0 ? xpos/((float)Maze::TILESIZE) : -1;} // need positive check else particles can get stuck to left wall - int16_t tileY() const {return ypos/((float)Maze::TILESIZE);} + coord_t TileX() const { + return xPos > 0 ? (coord_t) ((float) xPos / (float) Maze::TILESIZE) : (coord_t) -1; + } // Need positive check else particles can get stuck to left wall + + coord_t TileY() const { + return (coord_t) ((float) yPos / (float) Maze::TILESIZE); + } - // color of the particle + // Color of the particle lv_color_t color; private: - // positions and velocities of particle, in pixels and pixels/step (~50 steps per second) - float xpos; - float ypos; - float xvel; - float yvel; + // Positions and velocities of particle, in pixels and pixels/step (~50 steps per second) + float xPos; + float yPos; + float xVel; + float yVel; // first apply gravity, then apply damping factor, then add velocity to position - static constexpr float GRAVITY = 0.3; // added to yvel every step (remember up is -y) - static constexpr float DAMPING_FACTOR = 0.99; // keep this much of the velocity every step (applied after gravity) - static constexpr int8_t MAX_START_ANGLE = 45; // degrees off from straight vertical a particle can be going when spawned (must be <90) - static constexpr int16_t MIN_START_VELOCITY = 5; // minimum velocity a particle can spawn with + static constexpr float GRAVITY = 0.3; // added to yvel every step (remember up is -y) + static constexpr float DAMPING_FACTOR = 0.99; // keep this much of the velocity every step (applied after gravity) + static constexpr int8_t MAX_START_ANGLE = 45; // degrees off from straight vertical a particle can be going when spawned (<90) + static constexpr int16_t MIN_START_VELOCITY = 5; // minimum velocity a particle can spawn with static constexpr int16_t MAX_START_VELOCITY = 14; // maximum velocity a particle can spawn with - static constexpr float START_X_COMPRESS = 1./2.; // multiply X velocity by this value. can give a more concentrated confetti blast. + static constexpr float START_X_COMPRESS = 1. / 2.; // multiply X velocity by this value. for a more concentrated confetti blast. }; - - - // What is currently being displayed. // Watchface is normal operation; anything else is an easter egg. Really only used to indicate what - // should be displayed when the screen refreshes. - enum Displaying {watchface, blank, loss, amogus, autismcreature, foxgame, reminder, pinetime}; + // should be displayed when the screen refreshes. + enum Displaying { WatchFace, Blank, Loss, Amogus, AutismCreature, FoxGame, GameReminder, PineTime }; // The watchface itself class WatchFaceMaze : public Screen { @@ -178,54 +212,53 @@ namespace Pinetime { bool OnButtonPushed() override; private: - // Functions related to refreshing, for better separation of processes + // Functions called from Refresh(), for slightly better separation of processes void HandleMazeRefresh(); - void HandleConfetti(); // okay it's not very well separated but at least the confetti part is separate - + void HandleConfettiRefresh(); + // Functions related to drawing the indicators at the top right void UpdateBatteryDisplay(bool forceRedraw = false); void UpdateBleDisplay(bool forceRedraw = false); void ClearIndicators(); - + // Functions related to touching the screen, also for better separation of processes bool HandleLongTap(); bool HandleTap(); bool HandleSwipe(uint8_t direction); // MAZE GENERATION - - // Very simple function which just resets the maze to what is considered blank + + // Resets the maze to what is considered blank void InitializeMaze(); // Seed the maze with whatever the currentState dictates should be shown void SeedMaze(); - void PutTimeDate(); // Puts time onto the watchface. Part of SeedMaze, Do not call directly. + void PutTime(); // Puts time onto the watchface. Part of SeedMaze, Do not call directly. // Generate the maze around whatever the maze was seeded with. // MAZE MUST BE SEEDED ELSE ALL YOU'LL GENERATE IS AN INFINITE LOOP! - // If seed has disconnected components, maze will not be perfect. + // If seed has disconnected components, maze will also have disconnected components. void GenerateMaze(); - void GeneratePath(int x, int y); // Generates a single path starting at the x,y coords. Part of GenerateMaze, do not call directly. + void GeneratePath(coord_t x, coord_t y); // Generates a single path starting at the x,y coords. Part of GenerateMaze, do not call directly. - // If the maze has any disconnected components (such as if seeded with multiple disconnected blocks), - // poke holes to force all components to be connected. + // If the maze has any disconnected components (such as if seed wasn't fully connected), poke holes to force all components to be + // connected. void ForceValidMaze(); - // Draws the maze to the screen. + // Draws the maze to the screen void DrawMaze(); - + // OTHER FUNCTIONS // Fill in the inside of a maze square. Wall states don't affect this; it never draws in the area where walls go. // Generic, but only actually used for confetti. - void DrawTileInner(int16_t x, int16_t y, lv_color_t color); + void DrawTileInner(coord_t x, coord_t y, lv_color_t color); - // Draw and generally deal with confetti + // Functions to update and generally deal with confetti void ProcessConfetti(); void ClearConfetti(); - - // Stuff necessary for interacting with the OS and whatnot + // Stuff necessary for interacting with the OS lv_task_t* taskRefresh; Controllers::DateTime& dateTimeController; Controllers::Settings& settingsController; @@ -238,13 +271,8 @@ namespace Pinetime { Maze maze; MazeRNG prng; - // Time stuff. - // currentDateTime is used for keeping track of minutes. It's what refreshes the screen every minute. - // realTime is present due to what seems to be a race condition if dateTimeController.CurrentDateTime() is called - // by two things at once. It gets updated every refresh, or roughly every 20ms. Do not use it for anything high precision. - // (I really don't understand, but doing this decreased infinisim crashes due to mutexes releasing without being held so whatever) + // Used for keeping track of minutes. It's what refreshes the screen every minute. Utility::DirtyValue> currentDateTime {}; - std::chrono::time_point realTime {}; // Indicator values. Used for refreshing the respective indicators. Utility::DirtyValue batteryPercent; @@ -255,77 +283,119 @@ namespace Pinetime { // Warning: because each confetti moving causes 2 draw calls, this is really slow in Infinisim. Lower if using Infinisim (to ~20). constexpr static uint16_t CONFETTI_COUNT = 50; ConfettiParticle confettiArr[CONFETTI_COUNT]; - bool initConfetti = false; // don't want to modify confettiArr in touch event handler, so use a flag and do it in refresh() + bool initConfetti = false; // don't want to modify confettiArr in touch event handler, so use a flag and do it in Refresh() bool confettiActive = false; - // Buffers for use during printing. There's two it flips between because if there was only one, it would start - // being overwritten before the DMA finishes, and it'd corrupt parts of the display. - // activeBuffer is, well, the currently active one. Switch with swapActiveBuffer(); + // Buffers for use during printing. There's two it flips between because if there was only one, it could start + // being overwritten before the DMA finishes, and it'd corrupt the write. + // activeBuffer is, well, the currently active one. Switch with SwapActiveBuffer(); lv_color_t buf1[480]; lv_color_t buf2[480]; - lv_color_t *activeBuffer = buf1; - void swapActiveBuffer() {activeBuffer = (activeBuffer==buf1) ? buf2 : buf1;} + lv_color_t* activeBuffer = buf1; - // All concerning the printing of the screen. If screenRefreshRequired is set and screenRefreshTargetTime is - // greater than the current time, it waits until that target time before refreshing. Otherwise, it refreshes - // immediately when it sees that screenRefreshRequired is set. + void SwapActiveBuffer() { + activeBuffer = (activeBuffer == buf1) ? buf2 : buf1; + } + + // All concerning the printing of the screen. + // screenRefreshRequired is just a flag that the screen needs redrawing. Used when switching between secrets. // pausedGeneration is used if the maze took too long to generate, so it lets other processes get cpu time. - // It really should never trigger with this small 24x24 maze. + // It really should never trigger with this small 24x24 maze. // pausedGeneration does NOT protect against infinite loops from unseeded mazes! It only checks after each path has been generated! - std::chrono::time_point screenRefreshTargetTime {}; bool screenRefreshRequired = false; bool pausedGeneration = false; // Number data and AM/PM data for displaying time constexpr static uint8_t numbers[10][15] /*6x10*/ = { - {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x8F,0x88,0xF8,0x8F,0x88,0xF8,0x85,0x0E,0x03}, - {0xF5,0xFC,0x0F,0x80,0xFF,0x8F,0xF8,0xFF,0x8F,0xF8,0xFF,0x8F,0xD0,0x58,0x00}, - {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0xFF,0x0F,0xC0,0xF0,0x3C,0x0F,0x80,0x58,0x00}, - {0xF5,0x7C,0x01,0x8F,0x8F,0xF8,0xFD,0x0F,0x80,0xFF,0x8D,0xF8,0x85,0x0E,0x03}, - {0xDF,0xD8,0xF8,0x8F,0x88,0xF8,0x85,0x08,0x00,0xFF,0x8F,0xF8,0xFF,0x8F,0xF8}, - {0xD5,0x58,0x00,0x8F,0xF8,0xFF,0x85,0x78,0x01,0xFF,0x8F,0xF8,0xD5,0x08,0x03}, - {0xF5,0x7C,0x03,0x8F,0xF8,0xFF,0x85,0x78,0x01,0x8F,0x88,0xF8,0x85,0x0E,0x03}, - {0xD5,0x58,0x00,0xFF,0x8F,0xF8,0xFF,0x0F,0xE3,0xFE,0x3F,0xC3,0xF8,0xFF,0x8F}, - {0xF5,0x7C,0x01,0x8F,0x88,0xF0,0x84,0x3E,0x01,0xC3,0x88,0xF8,0x85,0x0E,0x03}, - {0xF5,0x7C,0x01,0x8F,0x88,0xF8,0x85,0x0E,0x00,0xFF,0x8F,0xF8,0xF5,0x0E,0x03}}; - constexpr static uint8_t am[10] /*5x8*/ = {0xF5,0xF0,0x18,0xE2,0x38,0x84,0x20,0x08,0xE2,0x38}; - constexpr static uint8_t pm[10] /*5x8*/ = {0xD5,0xE0,0x18,0xE2,0x38,0x84,0x20,0x38,0xFE,0x3F}; + {0xF5, 0x7C, 0x01, 0x8F, 0x88, 0xF8, 0x8F, 0x88, 0xF8, 0x8F, 0x88, 0xF8, 0x85, 0x0E, 0x03}, + {0xF5, 0xFC, 0x0F, 0x80, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xD0, 0x58, 0x00}, + {0xF5, 0x7C, 0x01, 0x8F, 0x88, 0xF8, 0xFF, 0x0F, 0xC0, 0xF0, 0x3C, 0x0F, 0x80, 0x58, 0x00}, + {0xF5, 0x7C, 0x01, 0x8F, 0x8F, 0xF8, 0xFD, 0x0F, 0x80, 0xFF, 0x8D, 0xF8, 0x85, 0x0E, 0x03}, + {0xDF, 0xD8, 0xF8, 0x8F, 0x88, 0xF8, 0x85, 0x08, 0x00, 0xFF, 0x8F, 0xF8, 0xFF, 0x8F, 0xF8}, + {0xD5, 0x58, 0x00, 0x8F, 0xF8, 0xFF, 0x85, 0x78, 0x01, 0xFF, 0x8F, 0xF8, 0xD5, 0x08, 0x03}, + {0xF5, 0x7C, 0x03, 0x8F, 0xF8, 0xFF, 0x85, 0x78, 0x01, 0x8F, 0x88, 0xF8, 0x85, 0x0E, 0x03}, + {0xD5, 0x58, 0x00, 0xFF, 0x8F, 0xF8, 0xFF, 0x0F, 0xE3, 0xFE, 0x3F, 0xC3, 0xF8, 0xFF, 0x8F}, + {0xF5, 0x7C, 0x01, 0x8F, 0x88, 0xF0, 0x84, 0x3E, 0x01, 0xC3, 0x88, 0xF8, 0x85, 0x0E, 0x03}, + {0xF5, 0x7C, 0x01, 0x8F, 0x88, 0xF8, 0x85, 0x0E, 0x00, 0xFF, 0x8F, 0xF8, 0xF5, 0x0E, 0x03}}; + constexpr static uint8_t am[10] /*5x8*/ = {0xF5, 0xF0, 0x18, 0xE2, 0x38, 0x84, 0x20, 0x08, 0xE2, 0x38}; + constexpr static uint8_t pm[10] /*5x8*/ = {0xD5, 0xE0, 0x18, 0xE2, 0x38, 0x84, 0x20, 0x38, 0xFE, 0x3F}; constexpr static uint8_t blankseed[1] /*4x1*/ = {0xD5}; - constexpr static uint8_t indicatorSpace[3] /*3x3*/ = {0xF6,0x8A,0x00}; + constexpr static uint8_t indicatorSpace[3] /*3x3*/ = {0xF6, 0x8A, 0x00}; // Used for swipe sequences for easter eggs. - // currentState is what is being displayed. It's generally categorized into "watchface" and "not watchface". - // lastInputTime is for long clicking on the main watchface. If you long click twice in 2 seconds, it goes to the secret input screen. - // currentCode is the current swipe sequence that's being worked on - Displaying currentState = Displaying::watchface; - std::chrono::time_point lastInputTime {}; + // currentState is what is being displayed. It's generally categorized into "WatchFace" and "not WatchFace". + // lastInputTime is for long clicking on the main watchface. If you long click twice in a certain timespan, it goes to the secret input + // doubleDoubleClickDelay is the aforementioned 'certain timespan' to get to the secret input. In ticks. + // screen. currentCode is the current swipe sequence that's being inputted. + Displaying currentState = Displaying::WatchFace; + uint32_t lastLongClickTime; + constexpr static uint32_t doubleDoubleClickDelay = pdMS_TO_TICKS(2500); uint8_t currentCode[8]; // Input codes for secret swipe gestures - // Note that the codes are effectively backwards; the currentCode is a stack being pushed from the left. Values are 0-3, clockwise from up. - // After a code is inputted the code is cleared, so if making a code smaller than the max size take care that it doesn't overlap - // with any other code. - constexpr static uint8_t lossCode[8] = {0,0,2,2,3,1,3,1}; // RLRLDDUU (konami code backwards) - constexpr static uint8_t amogusCode[8] = {1,3,1,3,2,2,0,0}; // UUDDLRLR (konami code) - constexpr static uint8_t autismCode[8] = {3,1,3,1,3,1,3,1}; // RLRLRLRL (pet pet pet pet) - constexpr static uint8_t foxCode[7] = {0,1,0,3,2,1,2}; // a healthy secret in a curious game :3 - constexpr static uint8_t reminderCode[4] = {3,2,1,0}; // URDL - constexpr static uint8_t pinetimeCode[8] = {1,2,3,0,1,2,3,0}; // ULDRULDR (two counterclockwise rotations) + // Note that the codes are effectively backwards; the currentCode is like a stack being pushed from the left. Values are 0-3, + // clockwise from up. After a code is inputted the currentCode is cleared, so if making a code smaller than the max size take care + // that it doesn't overlap with any other code. + constexpr static uint8_t lossCode[8] = {0, 0, 2, 2, 3, 1, 3, 1}; // RLRLDDUU (konami code backwards) + constexpr static uint8_t amogusCode[8] = {1, 3, 1, 3, 2, 2, 0, 0}; // UUDDLRLR (konami code) + constexpr static uint8_t autismCode[8] = {3, 1, 3, 1, 3, 1, 3, 1}; // RLRLRLRL (pet pet pet pet) + constexpr static uint8_t foxCode[7] = {0, 1, 0, 3, 2, 1, 2}; // a healthy secret in Tunic :3 + constexpr static uint8_t reminderCode[4] = {3, 2, 1, 0}; // URDL (clockwise rotation) + constexpr static uint8_t pinetimeCode[8] = {1, 2, 3, 0, 1, 2, 3, 0}; // ULDRULDR (two counterclockwise rotations) // Maze data for secrets. These are pasted onto the screen when the corresponding code is entered. - constexpr static uint8_t loss[105] /*21x20*/ = {0xFD,0xFF,0xFF,0xF7,0xFF,0xFE,0x3F,0xFF,0xF8,0xFF,0xFF,0x8F,0xFF,0xFE,0x3F,0xDF,0xE3,0xFF,0xFF,0x8F,0xE3,0xF8,0xFF,0xFF,0xE3,0xF8,0xFE,0x3F,0xFF,0xF8,0xFE,0x3F,0x8F,0xFF,0xFE,0x3F,0x8F,0xE3,0xFF,0xFF,0x8F,0xE3,0xF8,0xFF,0xFF,0xE3,0xF8,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F,0xDF,0xFF,0x7F,0xFF,0x8F,0xE3,0xFF,0x8F,0xFF,0xE3,0xF8,0xFF,0xE3,0xFF,0xF8,0xFE,0x3F,0xF8,0xFF,0xFE,0x3F,0x8F,0xFE,0x3F,0xFF,0x8F,0xE3,0xFF,0x8F,0xFF,0xE3,0xF8,0xFF,0xE3,0xFF,0xF8,0xFE,0x3F,0xF8,0xF5,0x56,0x3F,0x8F,0xFE,0x38,0x00}; - constexpr static uint8_t amogus[114] /*19x24*/ = {0xFF,0xFF,0x55,0x7F,0xFF,0xFF,0xD0,0x00,0x7F,0xFF,0xFE,0x0F,0xF8,0x7F,0xFF,0xF0,0xFF,0x40,0x7F,0xFF,0x83,0xF0,0x00,0x5F,0xFE,0x3F,0x0F,0xFE,0x1F,0x50,0xF8,0xFF,0xFE,0x30,0x03,0xE3,0xFF,0xF8,0x8F,0x8F,0x87,0xFF,0xC2,0x3E,0x3F,0x85,0x54,0x38,0xF8,0xFF,0xE0,0x03,0xE3,0xE3,0xFF,0xFF,0x8F,0x8F,0x8F,0xFF,0xFE,0x3E,0x3E,0x3F,0xFF,0xF8,0xF8,0xF8,0xFF,0xFF,0xE3,0xE3,0xE3,0xFF,0xFF,0x8F,0x8F,0x8F,0xFF,0xFE,0x3E,0x14,0x3F,0xD7,0xF8,0xFE,0x00,0xFC,0x07,0xE3,0xFF,0x83,0xE3,0x8F,0x8F,0xFF,0x8F,0x8E,0x3E,0x3F,0xFE,0x3E,0x38,0xF8,0xFF,0xF8,0x50,0xE1,0x43,0xFF,0xE0,0x03,0x80,0x3F}; - constexpr static uint8_t autismcreature[126] /*24x21*/ = {0xFD,0x55,0x55,0x7F,0xFF,0xFF,0xF0,0x00,0x00,0x17,0xFF,0xFF,0xC3,0xFF,0xFF,0x81,0xFF,0xFF,0x8F,0xFF,0xFF,0xF8,0x7F,0xFF,0xBF,0x5F,0xF5,0xFE,0x3F,0xFF,0xBF,0x87,0xF8,0x7E,0x3F,0xFF,0xB9,0x03,0x90,0x3E,0x3F,0xFF,0xB8,0x03,0x80,0x3E,0x3F,0xFF,0xBE,0x0F,0xE0,0xFE,0x15,0x57,0x9F,0xFF,0xFF,0xFC,0x00,0x01,0x87,0xFF,0xFF,0xF0,0x3F,0xF8,0xE1,0x5F,0xFF,0x43,0xFF,0xF8,0xF8,0x05,0x54,0x0F,0xFF,0xF8,0xFE,0x00,0x00,0xFF,0xFF,0xF8,0xFE,0x3F,0xFF,0xFF,0xFF,0xF8,0xFE,0x3F,0x7F,0xD7,0xFD,0xF8,0xFE,0x3E,0x3F,0x01,0xF8,0xF8,0xFE,0x3E,0x3E,0x00,0xF8,0xF8,0xFE,0x3E,0x3E,0x38,0xF8,0xF8,0xFE,0x14,0x14,0x38,0x50,0x50,0xFF,0x80,0x00,0xFE,0x00,0x03}; - constexpr static uint8_t foxgame[132] /*24x22*/ = {0xFF,0xD7,0xFF,0xFF,0xF5,0xFF,0xFD,0x01,0x7F,0xFF,0x40,0x5F,0xF0,0x38,0x1F,0xFC,0x0E,0x07,0xC3,0xFF,0x87,0xF0,0xFF,0xE1,0x8F,0xFF,0xE3,0xE3,0xFF,0xF8,0x8F,0xFF,0xFF,0xE1,0xFF,0xF0,0x8F,0xFF,0xFF,0xE0,0x5F,0x43,0x8F,0xFF,0xFF,0xE0,0x04,0x0F,0x8F,0xFF,0xFF,0xE3,0xE0,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x85,0x55,0x55,0x41,0x55,0x55,0x80,0x00,0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xDF,0xFF,0xFF,0xF7,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x8F,0xFF,0xFF,0xE3,0xFF,0xFF,0x87,0xFF,0xFF,0xE1,0xFF,0xFF,0xE1,0x7F,0xFF,0xF8,0x5F,0xFF,0xF8,0x17,0xFF,0xFE,0x05,0xFF,0xFF,0x83,0xFF,0xFF,0xE0,0xFF}; - constexpr static uint8_t reminder[102] /*24x17*/ = {0xFF,0xD5,0xF7,0xDF,0x57,0xFF,0xFF,0x80,0xE3,0x8E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE1,0x0E,0x17,0xFF,0xFF,0xE3,0xE0,0x0E,0x03,0xFF,0xFF,0xE3,0xE3,0x8E,0x3F,0xFF,0xFF,0xE3,0xE3,0x8E,0x17,0xFF,0xFF,0xE3,0xE3,0x8E,0x03,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xF5,0x7F,0x5F,0xF5,0x5F,0xD5,0xC0,0x3C,0x07,0xC0,0x07,0x80,0x8F,0xF8,0xE3,0x88,0xE3,0x8F,0x8F,0xF8,0xE3,0x88,0xE3,0x85,0x8F,0x78,0x43,0x88,0xE3,0x80,0x8E,0x38,0x03,0x8F,0xE3,0x8F,0x84,0x38,0xE3,0x8F,0xE3,0x85,0xE0,0xF8,0xE3,0x8F,0xE3,0x80}; - //constexpr static uint8_t foxface[144] /*24x24*/ = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFD,0x7F,0xFF,0xFF,0xFF,0xFF,0xF8,0x17,0xFF,0xFF,0xFF,0xFF,0xF8,0x01,0x7F,0xFF,0xF5,0x5F,0xF8,0xF8,0x1F,0xFF,0x40,0x07,0xF8,0xFF,0x8F,0xF4,0x0F,0xE3,0xF8,0x7F,0x85,0x40,0xFF,0xC3,0xF8,0x1F,0xE0,0x0F,0xFD,0x03,0xF8,0xE7,0xFF,0xFF,0xF3,0xE3,0xF0,0xFF,0xFF,0xFF,0xFF,0xE3,0xC0,0xFD,0x7F,0xFF,0xFF,0xCF,0x8F,0xFE,0x1F,0xFD,0x7F,0x8F,0xBF,0xE4,0x0F,0xFE,0x1F,0x87,0xFF,0xE0,0x0F,0xE4,0x0F,0xE1,0xFF,0xF8,0x3F,0xE0,0x0F,0xF8,0xDF,0xFF,0xFF,0xF8,0x3F,0xF8,0xE5,0x7F,0xF5,0xFF,0xFF,0xF8,0xFF,0x95,0x40,0x7F,0xFF,0xFE,0xFF,0xFF,0xE0,0x15,0x55,0x54,0xBF,0xFF,0xF8,0xFF,0xFF,0xFF,0x9F,0xFF,0xFF,0xFF,0xFF,0xFD,0x87,0xFF,0xFF,0xFF,0xFF,0xF0,0xE1,0x5F,0xFF,0xFF,0xFF,0x43,0xF8,0x05,0x5F,0xFF,0xD4,0x3F}; - constexpr static uint8_t pinetime[120] /*20x24*/ = {0xFF,0xFF,0xF7,0xFF,0xFF,0xFF,0xFF,0xC1,0xFF,0xFF,0xFF,0xFF,0x00,0x7F,0xFF,0xFF,0xF6,0x00,0x37,0xFF,0xFF,0xC1,0x63,0x41,0xFF,0xFF,0x80,0xD5,0x80,0xFF,0xFF,0xB5,0x00,0x56,0xFF,0xF7,0xC0,0x00,0x01,0xF7,0xE1,0x60,0x00,0x03,0x43,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x5D,0x00,0x03,0xE0,0x00,0xD5,0x80,0x03,0xE0,0x35,0x00,0x56,0x03,0xE3,0x40,0x00,0x01,0x63,0x96,0x00,0x00,0x00,0x34,0x81,0x60,0x00,0x03,0x40,0xE0,0x15,0x80,0xD4,0x03,0xE0,0x00,0x77,0x00,0x03,0xF8,0x00,0xC1,0x80,0x0F,0xF8,0x0D,0x00,0x58,0x0F,0xFF,0xD0,0x00,0x05,0xFF,0xFF,0xE0,0x00,0x03,0xFF,0xFF,0xFE,0x00,0x3F,0xFF,0xFF,0xFF,0xE3,0xFF,0xFF}; + constexpr static uint8_t loss[105] /*21x20*/ = { + 0xFD, 0xFF, 0xFF, 0xF7, 0xFF, 0xFE, 0x3F, 0xFF, 0xF8, 0xFF, 0xFF, 0x8F, 0xFF, 0xFE, 0x3F, 0xDF, 0xE3, 0xFF, 0xFF, 0x8F, 0xE3, + 0xF8, 0xFF, 0xFF, 0xE3, 0xF8, 0xFE, 0x3F, 0xFF, 0xF8, 0xFE, 0x3F, 0x8F, 0xFF, 0xFE, 0x3F, 0x8F, 0xE3, 0xFF, 0xFF, 0x8F, 0xE3, + 0xF8, 0xFF, 0xFF, 0xE3, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xDF, 0xFF, 0x7F, 0xFF, + 0x8F, 0xE3, 0xFF, 0x8F, 0xFF, 0xE3, 0xF8, 0xFF, 0xE3, 0xFF, 0xF8, 0xFE, 0x3F, 0xF8, 0xFF, 0xFE, 0x3F, 0x8F, 0xFE, 0x3F, 0xFF, + 0x8F, 0xE3, 0xFF, 0x8F, 0xFF, 0xE3, 0xF8, 0xFF, 0xE3, 0xFF, 0xF8, 0xFE, 0x3F, 0xF8, 0xF5, 0x56, 0x3F, 0x8F, 0xFE, 0x38, 0x00}; + constexpr static uint8_t amogus[114] /*19x24*/ = { + 0xFF, 0xFF, 0x55, 0x7F, 0xFF, 0xFF, 0xD0, 0x00, 0x7F, 0xFF, 0xFE, 0x0F, 0xF8, 0x7F, 0xFF, 0xF0, 0xFF, 0x40, 0x7F, + 0xFF, 0x83, 0xF0, 0x00, 0x5F, 0xFE, 0x3F, 0x0F, 0xFE, 0x1F, 0x50, 0xF8, 0xFF, 0xFE, 0x30, 0x03, 0xE3, 0xFF, 0xF8, + 0x8F, 0x8F, 0x87, 0xFF, 0xC2, 0x3E, 0x3F, 0x85, 0x54, 0x38, 0xF8, 0xFF, 0xE0, 0x03, 0xE3, 0xE3, 0xFF, 0xFF, 0x8F, + 0x8F, 0x8F, 0xFF, 0xFE, 0x3E, 0x3E, 0x3F, 0xFF, 0xF8, 0xF8, 0xF8, 0xFF, 0xFF, 0xE3, 0xE3, 0xE3, 0xFF, 0xFF, 0x8F, + 0x8F, 0x8F, 0xFF, 0xFE, 0x3E, 0x14, 0x3F, 0xD7, 0xF8, 0xFE, 0x00, 0xFC, 0x07, 0xE3, 0xFF, 0x83, 0xE3, 0x8F, 0x8F, + 0xFF, 0x8F, 0x8E, 0x3E, 0x3F, 0xFE, 0x3E, 0x38, 0xF8, 0xFF, 0xF8, 0x50, 0xE1, 0x43, 0xFF, 0xE0, 0x03, 0x80, 0x3F}; + constexpr static uint8_t autismCreature[126] /*24x21*/ = { + 0xFD, 0x55, 0x55, 0x7F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x17, 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, + 0xF8, 0x7F, 0xFF, 0xBF, 0x5F, 0xF5, 0xFE, 0x3F, 0xFF, 0xBF, 0x87, 0xF8, 0x7E, 0x3F, 0xFF, 0xB9, 0x03, 0x90, 0x3E, 0x3F, 0xFF, + 0xB8, 0x03, 0x80, 0x3E, 0x3F, 0xFF, 0xBE, 0x0F, 0xE0, 0xFE, 0x15, 0x57, 0x9F, 0xFF, 0xFF, 0xFC, 0x00, 0x01, 0x87, 0xFF, 0xFF, + 0xF0, 0x3F, 0xF8, 0xE1, 0x5F, 0xFF, 0x43, 0xFF, 0xF8, 0xF8, 0x05, 0x54, 0x0F, 0xFF, 0xF8, 0xFE, 0x00, 0x00, 0xFF, 0xFF, 0xF8, + 0xFE, 0x3F, 0xFF, 0xFF, 0xFF, 0xF8, 0xFE, 0x3F, 0x7F, 0xD7, 0xFD, 0xF8, 0xFE, 0x3E, 0x3F, 0x01, 0xF8, 0xF8, 0xFE, 0x3E, 0x3E, + 0x00, 0xF8, 0xF8, 0xFE, 0x3E, 0x3E, 0x38, 0xF8, 0xF8, 0xFE, 0x14, 0x14, 0x38, 0x50, 0x50, 0xFF, 0x80, 0x00, 0xFE, 0x00, 0x03}; + constexpr static uint8_t foxGame[132] /*24x22*/ = { + 0xFF, 0xD7, 0xFF, 0xFF, 0xF5, 0xFF, 0xFD, 0x01, 0x7F, 0xFF, 0x40, 0x5F, 0xF0, 0x38, 0x1F, 0xFC, 0x0E, 0x07, 0xC3, + 0xFF, 0x87, 0xF0, 0xFF, 0xE1, 0x8F, 0xFF, 0xE3, 0xE3, 0xFF, 0xF8, 0x8F, 0xFF, 0xFF, 0xE1, 0xFF, 0xF0, 0x8F, 0xFF, + 0xFF, 0xE0, 0x5F, 0x43, 0x8F, 0xFF, 0xFF, 0xE0, 0x04, 0x0F, 0x8F, 0xFF, 0xFF, 0xE3, 0xE0, 0xFF, 0x8F, 0xFF, 0xFF, + 0xE3, 0xFF, 0xFF, 0x85, 0x55, 0x55, 0x41, 0x55, 0x55, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xE3, 0xFF, + 0xFF, 0x8F, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0xE1, 0xFF, 0xFF, + 0xE1, 0x7F, 0xFF, 0xF8, 0x5F, 0xFF, 0xF8, 0x17, 0xFF, 0xFE, 0x05, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xE0, 0xFF}; + constexpr static uint8_t gameReminder[102] /*24x17*/ = { + 0xFF, 0xD5, 0xF7, 0xDF, 0x57, 0xFF, 0xFF, 0x80, 0xE3, 0x8E, 0x03, 0xFF, 0xFF, 0xE3, 0xE3, 0x8E, 0x3F, 0xFF, 0xFF, 0xE3, 0xE1, + 0x0E, 0x17, 0xFF, 0xFF, 0xE3, 0xE0, 0x0E, 0x03, 0xFF, 0xFF, 0xE3, 0xE3, 0x8E, 0x3F, 0xFF, 0xFF, 0xE3, 0xE3, 0x8E, 0x17, 0xFF, + 0xFF, 0xE3, 0xE3, 0x8E, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF5, 0x7F, 0x5F, 0xF5, 0x5F, 0xD5, 0xC0, 0x3C, 0x07, + 0xC0, 0x07, 0x80, 0x8F, 0xF8, 0xE3, 0x88, 0xE3, 0x8F, 0x8F, 0xF8, 0xE3, 0x88, 0xE3, 0x85, 0x8F, 0x78, 0x43, 0x88, 0xE3, 0x80, + 0x8E, 0x38, 0x03, 0x8F, 0xE3, 0x8F, 0x84, 0x38, 0xE3, 0x8F, 0xE3, 0x85, 0xE0, 0xF8, 0xE3, 0x8F, 0xE3, 0x80}; + // constexpr static uint8_t foxFace[144] /*24x24*/ = { + // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x17, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x01, 0x7F, + // 0xFF, 0xF5, 0x5F, 0xF8, 0xF8, 0x1F, 0xFF, 0x40, 0x07, 0xF8, 0xFF, 0x8F, 0xF4, 0x0F, 0xE3, 0xF8, 0x7F, 0x85, 0x40, 0xFF, 0xC3, + // 0xF8, 0x1F, 0xE0, 0x0F, 0xFD, 0x03, 0xF8, 0xE7, 0xFF, 0xFF, 0xF3, 0xE3, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xC0, 0xFD, 0x7F, + // 0xFF, 0xFF, 0xCF, 0x8F, 0xFE, 0x1F, 0xFD, 0x7F, 0x8F, 0xBF, 0xE4, 0x0F, 0xFE, 0x1F, 0x87, 0xFF, 0xE0, 0x0F, 0xE4, 0x0F, 0xE1, + // 0xFF, 0xF8, 0x3F, 0xE0, 0x0F, 0xF8, 0xDF, 0xFF, 0xFF, 0xF8, 0x3F, 0xF8, 0xE5, 0x7F, 0xF5, 0xFF, 0xFF, 0xF8, 0xFF, 0x95, 0x40, + // 0x7F, 0xFF, 0xFE, 0xFF, 0xFF, 0xE0, 0x15, 0x55, 0x54, 0xBF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, + // 0x87, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xE1, 0x5F, 0xFF, 0xFF, 0xFF, 0x43, 0xF8, 0x05, 0x5F, 0xFF, 0xD4, 0x3F}; + constexpr static uint8_t pinetime[120] /*20x24*/ = { + 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x7F, 0xFF, 0xFF, 0xF6, 0x00, 0x37, 0xFF, + 0xFF, 0xC1, 0x63, 0x41, 0xFF, 0xFF, 0x80, 0xD5, 0x80, 0xFF, 0xFF, 0xB5, 0x00, 0x56, 0xFF, 0xF7, 0xC0, 0x00, 0x01, 0xF7, + 0xE1, 0x60, 0x00, 0x03, 0x43, 0xE0, 0x15, 0x80, 0xD4, 0x03, 0xE0, 0x00, 0x5D, 0x00, 0x03, 0xE0, 0x00, 0xD5, 0x80, 0x03, + 0xE0, 0x35, 0x00, 0x56, 0x03, 0xE3, 0x40, 0x00, 0x01, 0x63, 0x96, 0x00, 0x00, 0x00, 0x34, 0x81, 0x60, 0x00, 0x03, 0x40, + 0xE0, 0x15, 0x80, 0xD4, 0x03, 0xE0, 0x00, 0x77, 0x00, 0x03, 0xF8, 0x00, 0xC1, 0x80, 0x0F, 0xF8, 0x0D, 0x00, 0x58, 0x0F, + 0xFF, 0xD0, 0x00, 0x05, 0xFF, 0xFF, 0xE0, 0x00, 0x03, 0xFF, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF}; }; } - - - template <> struct WatchFaceTraits { static constexpr WatchFace watchFace = WatchFace::Maze; @@ -345,4 +415,4 @@ namespace Pinetime { } }; } -} \ No newline at end of file +} From 2d863e121e30a372a4c2338b05397fddf44647c4 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Wed, 30 Oct 2024 01:21:53 -0500 Subject: [PATCH 22/23] Change warning --- src/displayapp/screens/WatchFaceMaze.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index c99807bd38..ae5e382224 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -212,8 +212,8 @@ WatchFaceMaze::WatchFaceMaze(Pinetime::Components::LittleVgl& lvgl, lastLongClickTime = xTaskGetTickCount() - doubleDoubleClickDelay; taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); - // refreshing here seems to cause issues in infinisim - // Refresh(); + + // Calling Refresh() here causes all sorts of issues, rely on task to refresh instead } WatchFaceMaze::~WatchFaceMaze() { From 4dad28e9c0c15344dc651ea39c4f430e21728f23 Mon Sep 17 00:00:00 2001 From: Feksaaargh <158505890+Feksaaargh@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:07:15 -0600 Subject: [PATCH 23/23] Speed up generation --- src/displayapp/screens/WatchFaceMaze.cpp | 167 +++++++++-------------- src/displayapp/screens/WatchFaceMaze.h | 3 +- 2 files changed, 66 insertions(+), 104 deletions(-) diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp index ae5e382224..b3787239a8 100644 --- a/src/displayapp/screens/WatchFaceMaze.cpp +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -562,63 +562,31 @@ void WatchFaceMaze::PutTime() { // Generates the maze around whatever it was seeded with void WatchFaceMaze::GenerateMaze() { - coord_t x = -1; - coord_t y = -1; // task should only run for 3/4 the time it takes for the task to refresh. // Will go over; only checks once it's finished with current line. It won't go too far over though. - uint32_t mazeGenStartTime = xTaskGetTickCount(); + TickType_t mazeGenStartTime = xTaskGetTickCount(); - while (true) { - // find position to start generating a path from - for (uint8_t i = 0; i < 30; i++) { - x = (coord_t) prng.Rand(0, Maze::WIDTH - 1); - y = (coord_t) prng.Rand(0, Maze::HEIGHT - 1); + // generate a path for every open tile left to right, bottom to top. + // Wilson's algorithm explicitly allows this, and it still makes an unbiased maze. + for (coord_t x = 0; x < Maze::WIDTH; x++) { + for (coord_t y = 0; y < Maze::HEIGHT; y++) { if (maze.GetTileAttr(x, y, TileAttr::FlagEmpty)) { - break; - } // found solution tile - if (i == 29) { - // failed all 30 attempts (this is inside the for loop for 'organization') - // find solution tile slowly but guaranteed (scan over entire field and choose random valid tile) - int count = 0; - - // count number of valid tiles - for (int32_t j = 0; j < Maze::WIDTH * Maze::HEIGHT; j++) { - if (maze.GetTileAttr(j, TileAttr::FlagEmpty)) { - count++; - } - } - - // if no valid tiles are left, maze is done - if (count == 0) { - pausedGeneration = false; + // inspected tile is empty, generate a path from it + GeneratePath(x, y); + + // if generating paths took too long, suspend it + // generation should not be allowed to take more than 2x the refresh period + // Will go over; only checks once it's finished with current line. It won't go too far over though. + if (xTaskGetTickCount() - mazeGenStartTime > pdMS_TO_TICKS(taskRefresh->period * 2)) { + pausedGeneration = true; return; } - // if execution gets here then maze gen is not done. select random index from valid tiles to start from - // 'count' is now used as an index - count = (coord_t) prng.Rand(1, count); - for (int32_t j = 0; j < Maze::WIDTH * Maze::HEIGHT; j++) { - if (maze.GetTileAttr(j, TileAttr::FlagEmpty)) { - count--; - } - if (count == 0) { - y = j / Maze::WIDTH; - x = j % Maze::WIDTH; - break; - } - } } } - // function now has a valid position a maze line can start from in x and y - GeneratePath(x, y); - - // if generating paths took too long, suspend it - if (xTaskGetTickCount() - mazeGenStartTime > taskRefresh->period * 3 / 4) { - pausedGeneration = true; - return; - } } - // execution never gets here! it returns earlier in the function. + // generation finished; make it no longer paused + pausedGeneration = false; } void WatchFaceMaze::GeneratePath(coord_t x, coord_t y) { @@ -740,97 +708,90 @@ void WatchFaceMaze::GeneratePath(coord_t x, coord_t y) { // goes through the maze, finds disconnected segments and connects them void WatchFaceMaze::ForceValidMaze() { - // Crude maze-optimized flood fill: follow a path until can't move any more, then find some other location to follow from. repeat. - // Once it's traversed all reachable tiles, checks if there are any tiles that have not been traversed. if there are, then find a border - // between the traversed and non-traversed segments. poke a hole at one of these borders randomly. - // Once the hole has been poked, more maze is reachable. continue this "fill-search then poke" scheme until the entire maze is accessible. - // This function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in normal control flow) - - // initialize cursor x and y to bottom right - coord_t x = Maze::WIDTH - 1; - coord_t y = Maze::HEIGHT - 1; + // Weird depth-first search: has a cursor which keeps moving to the last seen place it can go to, marking where it has gone. + // When there are no more places to move to, scan the maze and find the boundary between the traversed area and the non-traversed area. + // Pick a random wall along this boundary between traversed and non traversed, and poke a hole there. + // Once the hole has been poked, more maze is reachable. Continue this "fill-search then poke" scheme until the entire maze is accessible. + // This function repurposes flaggen for traversed tiles, so it expects it to be false on all tiles (should be in normal control flow). + + coord_t x = 0; + coord_t y = 0; + + // bitfield only for use in the backtrack stack + struct coordXY { + coord_t x : 16; + coord_t y : 16; + }; + std::stack backtrackStack; + while (true) { - // sorry for using goto but this needs to be really nested and the components are too integrated to split out into functions... - ForceValidMazeLoop: maze.SetAttr(x, y, TileAttr::FlagGen, true); - // move cursor - if (y > 0 && !maze.GetTileAttr(x, y, TileAttr::Up) && !maze.GetTileAttr(x, y - 1, TileAttr::FlagGen)) { - y--; - } else if (x < Maze::WIDTH - 1 && !maze.GetTileAttr(x, y, TileAttr::Right) && !maze.GetTileAttr(x + 1, y, TileAttr::FlagGen)) { - x++; - } else if (y < Maze::HEIGHT - 1 && !maze.GetTileAttr(x, y, TileAttr::Down) && !maze.GetTileAttr(x, y + 1, TileAttr::FlagGen)) { - y++; - } else if (x > 0 && !maze.GetTileAttr(x, y, TileAttr::Left) && !maze.GetTileAttr(x - 1, y, TileAttr::FlagGen)) { - x--; + + // add all possible movement options to the stack + if (y > 0 && !maze.GetTileAttr(x, y, TileAttr::Up) && !maze.GetTileAttr(x, y - 1, TileAttr::FlagGen)) + backtrackStack.push(coordXY(x, y-1)); + if (x < Maze::WIDTH - 1 && !maze.GetTileAttr(x, y, TileAttr::Right) && !maze.GetTileAttr(x + 1, y, TileAttr::FlagGen)) + backtrackStack.push(coordXY(x+1, y)); + if (y < Maze::HEIGHT - 1 && !maze.GetTileAttr(x, y, TileAttr::Down) && !maze.GetTileAttr(x, y + 1, TileAttr::FlagGen)) + backtrackStack.push(coordXY(x, y+1)); + if (x > 0 && !maze.GetTileAttr(x, y, TileAttr::Left) && !maze.GetTileAttr(x - 1, y, TileAttr::FlagGen)) + backtrackStack.push(coordXY(x-1, y)); + + if (!backtrackStack.empty()) { + // stack not empty (still have traversal to do); pull a position from the stack and move from there + x = backtrackStack.top().x; + y = backtrackStack.top().y; + backtrackStack.pop(); + } else { - unsigned int pokeLocationCount = 0; - // couldn't find any position to move to, need to set cursor to a different usable location + // stack empty; find a location to poke a hole in and poke it + uint16_t pokeLocationCount = 0; + + // check entire maze for boundaries between traversed and non-traversed space for (coord_t proposedY = 0; proposedY < Maze::HEIGHT; proposedY++) { for (coord_t proposedX = 0; proposedX < Maze::WIDTH; proposedX++) { const bool ownState = maze.GetTileAttr(proposedX, proposedY, TileAttr::FlagGen); - // if tile to the left is of a different traversal state (is traversed boundary) - if (proposedX > 0 && (maze.GetTileAttr(proposedX - 1, proposedY, TileAttr::FlagGen) != ownState)) { - // if found boundary AND can get to it, just continue working from here - if (maze.GetTileAttr(proposedX, proposedY, TileAttr::Left) == false) { - x = proposedX, y = proposedY; - goto ForceValidMazeLoop; - } + if (proposedX > 0 && (maze.GetTileAttr(proposedX - 1, proposedY, TileAttr::FlagGen) != ownState)) pokeLocationCount++; - } - // if tile up is of a different traversal state (is traversed boundary) - if (proposedY > 0 && (maze.GetTileAttr(proposedX, proposedY - 1, TileAttr::FlagGen) != ownState)) { - // if found boundary AND can get to it, just continue working from here - if (maze.GetTileAttr(proposedX, proposedY, TileAttr::Up) == false) { - x = proposedX, y = proposedY; - goto ForceValidMazeLoop; - } + if (proposedY > 0 && (maze.GetTileAttr(proposedX, proposedY - 1, TileAttr::FlagGen) != ownState)) pokeLocationCount++; - } } } - // finished scanning maze; there are no locations the cursor can be placed for it to continue scanning - // if there are no walls that can be poked through to increase reachable area, maze is finished - if (pokeLocationCount == 0) { - return; - } + // if there are no boundaries, entire maze has been traversed and function can return + if (pokeLocationCount == 0) { return; } - // if execution gets here, need to poke a hole. - // choose a random poke location to poke a hole through. pokeLocationCount is now used as an index + // choose a random indexed boundary to poke, then go back through the boundary finding process to find the exact place to poke pokeLocationCount = (int) prng.Rand(1, pokeLocationCount); - - for (coord_t proposedY = 0; proposedY < Maze::HEIGHT; proposedY++) { - for (coord_t proposedX = 0; proposedX < Maze::WIDTH; proposedX++) { - // pretty much a copy of the previous code which FINDS poke locations, but now with the goal of actually doing the poking + for (coord_t proposedY = 0; proposedY < Maze::HEIGHT && pokeLocationCount > 0; proposedY++) { + for (coord_t proposedX = 0; proposedX < Maze::WIDTH && pokeLocationCount > 0; proposedX++) { const bool ownState = maze.GetTileAttr(proposedX, proposedY, TileAttr::FlagGen); - + // if tile to the left is of a different traversal state (is traversed boundary) if (proposedX > 0 && (maze.GetTileAttr(proposedX - 1, proposedY, TileAttr::FlagGen) != ownState)) { pokeLocationCount--; - // found the target poke location, poke and loop if (pokeLocationCount == 0) { maze.SetAttr(proposedX, proposedY, TileAttr::Left, false); x = proposedX, y = proposedY; - goto ForceValidMazeLoop; // continue OUTSIDE loop + break; } } - // if tile up is of a different traversal state (is traversed boundary) if (proposedY > 0 && (maze.GetTileAttr(proposedX, proposedY - 1, TileAttr::FlagGen) != ownState)) { pokeLocationCount--; - // found the target poke location, poke and loop if (pokeLocationCount == 0) { maze.SetAttr(proposedX, proposedY, TileAttr::Up, false); x = proposedX, y = proposedY; - goto ForceValidMazeLoop; // continue processing + break; } } } } + // end boundary poking } - // done poking a hole in the maze to expand the reachable area } + // end overall while loop } void WatchFaceMaze::DrawMaze() { diff --git a/src/displayapp/screens/WatchFaceMaze.h b/src/displayapp/screens/WatchFaceMaze.h index 2c0da8abd6..b060a212cc 100644 --- a/src/displayapp/screens/WatchFaceMaze.h +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -2,6 +2,7 @@ #include #include +#include #include "FreeRTOS.h" #include "task.h" // configTICK_RATE_HZ #include "sys/unistd.h" @@ -328,7 +329,7 @@ namespace Pinetime { // doubleDoubleClickDelay is the aforementioned 'certain timespan' to get to the secret input. In ticks. // screen. currentCode is the current swipe sequence that's being inputted. Displaying currentState = Displaying::WatchFace; - uint32_t lastLongClickTime; + TickType_t lastLongClickTime; constexpr static uint32_t doubleDoubleClickDelay = pdMS_TO_TICKS(2500); uint8_t currentCode[8];