diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2b69b8b02..9ff6f207ca 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/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() diff --git a/src/displayapp/screens/WatchFaceMaze.cpp b/src/displayapp/screens/WatchFaceMaze.cpp new file mode 100644 index 0000000000..b3787239a8 --- /dev/null +++ b/src/displayapp/screens/WatchFaceMaze.cpp @@ -0,0 +1,938 @@ +#include "displayapp/screens/WatchFaceMaze.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); +} + +// 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(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 % 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(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(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 % 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::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; + } +} + +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 +// 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 >= 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 inverted mask will be applied to the existing value in mazeMap + for (uint8_t& mapItem : mazeMap) { + mapItem = (mapItem & mask) + value; + } + } +} + +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(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 + 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); + + // handle left wall + 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 (!(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++; + } + } +} + +bool ConfettiParticle::Step() { + // first apply gravity (only to y), then dampening, then apply velocity to position. + xVel *= DAMPING_FACTOR; + xPos += xVel; + + 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; +} + +void ConfettiParticle::Reset(MazeRNG& prng) { + // always start at bottom middle + xPos = 120; + yPos = 240; + + // velocity in pixels/tick + 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) * (std::numbers::pi_v / 180); + + 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); +} + +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()} { + + // 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); + + // Calling Refresh() here causes all sorts of issues, rely on task to refresh instead +} + +WatchFaceMaze::~WatchFaceMaze() { + lv_obj_clean(lv_scr_act()); + lv_task_del(taskRefresh); +} + +void WatchFaceMaze::Refresh() { + + // 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 + HandleConfettiRefresh(); +} + +void WatchFaceMaze::HandleMazeRefresh() { + // convert time to minutes and update if needed + 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()); + } + InitializeMaze(); + SeedMaze(); + } + + // always need to run GenerateMaze() when refreshing. This is a maze watchface after all. + GenerateMaze(); + + // only finalize and draw once maze is fully generated (not paused) + if (!pausedGeneration) { + ForceValidMaze(); + if (currentState != Displaying::WatchFace) { + ClearIndicators(); + } + DrawMaze(); + screenRefreshRequired = false; + // 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) { + UpdateBatteryDisplay(); + UpdateBleDisplay(); + } +} + +void WatchFaceMaze::HandleConfettiRefresh() { + // 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) { + // remove confetti if went to a different display + ClearConfetti(); + confettiActive = false; + } else { + // still on autism creature display, step confetti + ProcessConfetti(); + } + } +} + +void WatchFaceMaze::UpdateBatteryDisplay(const 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 + 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; + } + + // 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; + } + + // 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(const bool forceRedraw) { + bleConnected = bleController.IsConnected(); + if (forceRedraw || bleConnected.IsUpdated()) { + // need to redraw BLE indicator + SwapActiveBuffer(); + + 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); + 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(const TouchEvents event) { + // 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(); + 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) { + screenRefreshRequired = true; + 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) { + // On watchface; either refresh maze or go to blank state + if (xTaskGetTickCount() - lastLongClickTime < doubleDoubleClickDelay) { + // 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; + } + lastLongClickTime = xTaskGetTickCount(); + motor.RunForDuration(20); + + } else { + // Not on watchface; go back to main watchface + screenRefreshRequired = true; + 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); + } + + // 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 initConfetti, everything else is handled in functions called by refresh() + initConfetti = true; + return true; +} + +bool WatchFaceMaze::HandleSwipe(const 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 (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; + } 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) { + currentState = newState; + screenRefreshRequired = true; + motor.RunForDuration(10); + 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)); +} + +// seeds the maze with whatever the current state needs +void WatchFaceMaze::SeedMaze() { + switch (currentState) { + 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::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::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 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); + } + } + + // 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); +} + +// Generates the maze around whatever it was seeded with +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. + TickType_t mazeGenStartTime = xTaskGetTickCount(); + + // 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)) { + // 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; + } + + } + } + } + // generation finished; make it no longer paused + pausedGeneration = false; +} + +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.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 turning around. + while (true) { + switch (direction = prng.Rand(0, 3)) { + case 0: // moved up + if (y <= 0 || !maze.GetTileAttr(x, y, TileAttr::Up)) { + continue; + } + y -= 1; + break; + case 1: // moved left + if (x <= 0 || !maze.GetTileAttr(x, y, TileAttr::Left)) { + continue; + } + x -= 1; + break; + case 2: // moved down + 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.GetTileAttr(x, y, TileAttr::Right)) { + continue; + } + x += 1; + break; + default: // invalid, will never hit in normal operation + std::abort(); + } + break; + } + + // moved to next tile, check if looped in on self + if (!maze.GetTileAttr(x, y, TileAttr::FlagGen)) { + + // did NOT loop in on self, simply remove wall and move on + switch (direction) { + 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.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 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.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.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.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 + std::abort(); + } + } + } + // done processing one step, now do it again! + } + // finished generating the entire path + // mark all tiles as finalized and not in generation by removing ALL flaggen's + maze.Fill(0, MazeTile::FLAGGENMASK); +} + +// goes through the maze, finds disconnected segments and connects them +void WatchFaceMaze::ForceValidMaze() { + // 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) { + maze.SetAttr(x, y, TileAttr::FlagGen, true); + + // 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 { + // 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)) + pokeLocationCount++; + // if tile up is of a different traversal state (is traversed boundary) + if (proposedY > 0 && (maze.GetTileAttr(proposedX, proposedY - 1, TileAttr::FlagGen) != ownState)) + pokeLocationCount++; + } + } + + // if there are no boundaries, entire maze has been traversed and function can return + if (pokeLocationCount == 0) { return; } + + // 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 && 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--; + if (pokeLocationCount == 0) { + maze.SetAttr(proposedX, proposedY, TileAttr::Left, false); + x = proposedX, y = proposedY; + 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--; + if (pokeLocationCount == 0) { + maze.SetAttr(proposedX, proposedY, TileAttr::Up, false); + x = proposedX, y = proposedY; + break; + } + } + } + } + // end boundary poking + } + } + // end overall while loop +} + +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 + + // Print horizontal lines + // This doesn't bother with corners, those just get overwritten by the vertical lines + area.x1 = 1; + area.x2 = 238; + 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(); + } + + // Print vertical lines + area.y1 = 1; + area.y2 = 238; + 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); + } + // 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); + } + } + area.x1 = (Maze::TILESIZE * x) - 1; + area.x2 = (Maze::TILESIZE * x); + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, &activeBuffer[4]); + 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 + lvgl.SetFullRefresh(Components::LittleVgl::FullRefreshDirections::None); + lvgl.FlushDisplay(&area, activeBuffer); + } +} + +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; + } + + // prepare buffer + SwapActiveBuffer(); + std::fill_n(activeBuffer, 64, color); + lv_area_t area; + + // define bounds + 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); + lvgl.FlushDisplay(&area, activeBuffer); +} + +void WatchFaceMaze::ClearConfetti() { + // prevent superfluous calls + if (!confettiActive) { + return; + } + + // clear all particles and reset state + 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 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); + } + } + + // handle done flag + // should only set confettiActive to false, since all confetti will have been cleared as it moved out of frame + if (isDone) { + confettiActive = false; + } +} \ 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..b060a212cc --- /dev/null +++ b/src/displayapp/screens/WatchFaceMaze.h @@ -0,0 +1,419 @@ +#pragma once + +#include +#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" + +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. + // 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; + static constexpr uint8_t FLAGEMPTYMASK = 0b0100; + static constexpr uint8_t FLAGGENMASK = 0b1000; + uint8_t map = 0; + + // Set flags on given tile. Returns the object so they can be chained. + + 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; + } + }; + + // Custom PRNG for the maze to easily allow it to be deterministic for any given minute + class MazeRNG { + public: + 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(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() { + state ^= state >> 12; + state ^= state << 25; + state ^= state >> 27; + 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 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, + // ONLY handles interacting with the board. + class Maze { + public: + Maze(); + + // Get and set can work with either xy or indexes + MazeTile Get(coord_t x, coord_t y) const; + MazeTile Get(int32_t index) const; + + 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 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; + + // 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. + // 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(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 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 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]; + }; + + // Click on the autismcreature for a colorful surprise + 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 particle position elsewhere before stepping to be able to clear the old particle position (if redraw needed). + bool Step(); + + // 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. + 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 + 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; + + // 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 (<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. 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, GameReminder, PineTime }; + + // The watchface itself + class WatchFaceMaze : public Screen { + public: + WatchFaceMaze(Pinetime::Components::LittleVgl&, + Controllers::DateTime&, + Controllers::Settings&, + Controllers::MotorController&, + const Controllers::Battery&, + const Controllers::Ble&); + ~WatchFaceMaze() override; + + // Functions required for app operation. + void Refresh() override; + bool OnTouchEvent(TouchEvents event) override; + bool OnButtonPushed() override; + + private: + // Functions called from Refresh(), for slightly better separation of processes + void HandleMazeRefresh(); + 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 + + // Resets the maze to what is considered blank + void InitializeMaze(); + + // Seed the maze with whatever the currentState dictates should be shown + void SeedMaze(); + 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 also have disconnected components. + void GenerateMaze(); + 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 seed wasn't fully connected), poke holes to force all components to be + // connected. + void ForceValidMaze(); + + // 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(coord_t x, coord_t y, lv_color_t color); + + // Functions to update and generally deal with confetti + void ProcessConfetti(); + void ClearConfetti(); + + // Stuff necessary for interacting with the OS + 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) + Maze maze; + MazeRNG prng; + + // Used for keeping track of minutes. It's what refreshes the screen every minute. + Utility::DirtyValue> currentDateTime {}; + + // Indicator values. Used for refreshing the respective indicators. + Utility::DirtyValue batteryPercent; + Utility::DirtyValue charging; + Utility::DirtyValue bleConnected; + + // Confetti for autism creature + // 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 confettiActive = false; + + // 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; + } + + // 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. + // pausedGeneration does NOT protect against infinite loops from unseeded mazes! It only checks after each path has been generated! + 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}; + 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". + // 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; + TickType_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 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 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; + static constexpr const char* name = "Maze"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::WatchFaceMaze(controllers.lvgl, + controllers.dateTimeController, + controllers.settingsController, + controllers.motorController, + controllers.batteryController, + controllers.bleController); + }; + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + } + }; + } +}