diff --git a/firmware/include/modes/SnakeMode.h b/firmware/include/modes/SnakeMode.h index 7d281886..9c418b86 100644 --- a/firmware/include/modes/SnakeMode.h +++ b/firmware/include/modes/SnakeMode.h @@ -5,43 +5,55 @@ #include "handlers/ClockHandler.h" // NOLINT(misc-include-cleaner) #include "modules/ModeModule.h" +#include #include -#include #include class SnakeMode final : public ModeModule { private: - struct Pixel + enum class Stage : uint8_t // NOLINT(performance-enum-size) { - uint8_t x{0U}; // NOLINT(misc-non-private-member-variables-in-classes) - uint8_t y{0U}; // NOLINT(misc-non-private-member-variables-in-classes) - bool operator==(const Pixel &pixel) const { return x == pixel.x && y == pixel.y; } - bool operator!=(const Pixel &pixel) const { return x != pixel.x || y != pixel.y; } - bool operator<(const Pixel &pixel) const { return y < pixel.y || (y == pixel.y && x < pixel.x); } + READY, + MOVE, + DEATH, + REMOVE, }; unsigned long lastMillis{0UL}; uint8_t blinkCount{0U}; - uint8_t stage{0U}; - Pixel target; + size_t head{0U}; + size_t length{0U}; + size_t target{0U}; - std::deque snake{}; + std::array snake{}; + std::array occupied{}; std::unique_ptr clock{}; - void idle(); - [[nodiscard]] std::optional next() const; - void move(); + Stage stage{Stage::READY}; + void blink(); void clean(); - + void idle(); + void move(); void setClock(bool _clock); + void setDead(); void setTarget(); + void snakeReset(size_t start); + void snakeClear(); void transmit(); + [[nodiscard]] bool snakePushBack(size_t pixel); + + [[nodiscard]] size_t snakeAt(size_t index) const; + [[nodiscard]] size_t snakePopFront(); + + [[nodiscard]] std::optional findStepAvailable() const; + [[nodiscard]] std::optional findStepPath() const; + public: static constexpr std::string_view name{"Snake"}; diff --git a/firmware/src/modes/SnakeMode.cpp b/firmware/src/modes/SnakeMode.cpp index d91de932..34f07024 100644 --- a/firmware/src/modes/SnakeMode.cpp +++ b/firmware/src/modes/SnakeMode.cpp @@ -9,9 +9,9 @@ #include "services/DeviceService.h" #include "services/DisplayService.h" -#include +#include +#include #include -#include static_assert(GRID_COLUMNS >= 16U, __STRING(MODE_SNAKE) " is not compatible with this device's display size."); static_assert(GRID_ROWS >= 7U, __STRING(MODE_SNAKE) " is not compatible with this device's display size."); @@ -50,7 +50,10 @@ void SnakeMode::begin() nvs_close(handle); } Display.fillFrame(0U); - stage = 0U; + snakeClear(); + target = static_cast(random(clock == nullptr ? 0L : static_cast(5U * GRID_COLUMNS), + static_cast(GRID_COLUMNS * GRID_ROWS))); + stage = Stage::READY; } /** @@ -64,17 +67,18 @@ void SnakeMode::handle() } switch (stage) { - case 1U: + case Stage::READY: + idle(); + break; + case Stage::MOVE: move(); break; - case 2U: + case Stage::DEATH: blink(); break; - case 3U: + case Stage::REMOVE: clean(); break; - default: - idle(); } } @@ -83,12 +87,11 @@ void SnakeMode::handle() */ void SnakeMode::idle() { - const uint8_t x{static_cast(random(GRID_COLUMNS))}; - const uint8_t y{static_cast(random(clock == nullptr ? 0 : 5, GRID_ROWS))}; - snake = {{x, y}}; - Display.setPixel(x, y, UINT8_MAX); + snakeReset(static_cast(random(clock == nullptr ? 0L : static_cast(5U * GRID_COLUMNS), + static_cast(GRID_COLUMNS * GRID_ROWS)))); + Display.setPixel(snake[head], static_cast(random(1L, static_cast(0b1U << 8U)))); setTarget(); - stage = 1U; + stage = Stage::MOVE; } /** @@ -96,83 +99,102 @@ void SnakeMode::idle() * * @return The next unoccupied position, or `std::nullopt` when no adjacent position is available. */ -std::optional SnakeMode::next() const +std::optional SnakeMode::findStepPath() const { - Pixel start{snake.back()}; - std::map from; - std::queue frontier; - frontier.push(start); - from[start] = start; - bool pathFound{false}; - while (!frontier.empty()) + const uint8_t yMin{static_cast(clock == nullptr ? 0U : 5U)}; + std::array from{}; + std::array frontier{}; + std::array visited{}; + size_t frontierHead{0U}; + size_t frontierTail{0U}; + frontier[frontierTail++] = snake[head]; + from[snake[head]] = snake[head]; + visited[snake[head]] = true; + while (frontierHead < frontierTail) { - Pixel current{frontier.front()}; - frontier.pop(); + const size_t current{frontier[frontierHead]}; + ++frontierHead; if (current == target) { - pathFound = true; - break; + size_t step{target}; + while (from[step] != snake[head]) + { + step = from[step]; + } + return std::optional{step}; } - std::vector neighbors; - if (current.x != 0U) + std::array neighbors{}; + size_t count{0U}; + if (current % GRID_COLUMNS != 0U) { - neighbors.push_back(Pixel{static_cast(current.x - 1U), current.y}); + neighbors[count++] = current - 1U; } - if (current.y > (clock == nullptr ? 0U : 5U)) + if (current % GRID_COLUMNS < GRID_COLUMNS - 1U) { - neighbors.push_back(Pixel{current.x, static_cast(current.y - 1U)}); + neighbors[count++] = current + 1U; } - if (current.x < GRID_COLUMNS - 1U) + if (current / GRID_COLUMNS > yMin) { - neighbors.push_back(Pixel{static_cast(current.x + 1U), current.y}); + neighbors[count++] = current - GRID_COLUMNS; } - if (current.y < GRID_ROWS - 1U) + if (current / GRID_COLUMNS < GRID_ROWS - 1U) { - neighbors.push_back(Pixel{current.x, static_cast(current.y + 1U)}); + neighbors[count++] = current + GRID_COLUMNS; } - for (const Pixel &neighbor : neighbors) + for (size_t idx{0U}; idx < count; ++idx) { - if (std::find(snake.begin(), snake.end(), neighbor) == snake.end() && from.find(neighbor) == from.end()) + if (visited[neighbors[idx]] || occupied[neighbors[idx]]) { - frontier.push(neighbor); - from[neighbor] = current; + continue; } + visited[neighbors[idx]] = true; + from[neighbors[idx]] = current; + frontier[frontierTail] = neighbors[idx]; + ++frontierTail; } } - if (pathFound) - { - Pixel step{target}; - while (from[step] != start) - { - step = from[step]; - } - return step; - } - std::vector fallback; - if (start.y > (clock == nullptr ? 0U : 5U)) + return findStepAvailable(); +} + +std::optional SnakeMode::findStepAvailable() const +{ + std::array available{}; + size_t count{0U}; + if (snake[head] % GRID_COLUMNS > 0U) { - fallback.push_back(Pixel{start.x, static_cast(start.y - 1U)}); + available[count++] = snake[head] - 1U; } - if (start.x < GRID_COLUMNS - 1U) + if (snake[head] % GRID_COLUMNS < GRID_COLUMNS - 1U) { - fallback.push_back(Pixel{static_cast(start.x + 1U), start.y}); + available[count++] = snake[head] + 1U; } - if (start.y < GRID_ROWS - 1U) + if (snake[head] / GRID_COLUMNS > (clock == nullptr ? 0U : 5U)) { - fallback.push_back(Pixel{start.x, static_cast(start.y + 1U)}); + available[count++] = snake[head] - GRID_COLUMNS; } - if (start.x != 0U) + if (snake[head] / GRID_COLUMNS < GRID_ROWS - 1U) { - fallback.push_back(Pixel{static_cast(start.x - 1U), start.y}); + available[count++] = snake[head] + GRID_COLUMNS; } - for (const Pixel &option : fallback) + std::optional best{}; + size_t bestDistance{SIZE_MAX}; + for (size_t idx{0U}; idx < count; ++idx) { - if (std::find(snake.begin(), snake.end(), option) == snake.end()) + if (occupied[available[idx]]) + { + continue; + } + const size_t distance{static_cast(std::abs(static_cast(available[idx] % GRID_COLUMNS) - + static_cast(target % GRID_COLUMNS))) + + static_cast(std::abs(static_cast(available[idx] / GRID_COLUMNS) - + static_cast(target / GRID_COLUMNS)))}; + if (distance < bestDistance) { - return option; + best = available[idx]; + bestDistance = distance; } } - return std::nullopt; + return best; } /** @@ -183,33 +205,33 @@ std::optional SnakeMode::next() const */ void SnakeMode::move() { - if (millis() - lastMillis > INT8_MAX + snake.size()) + if (millis() - lastMillis > length + INT8_MAX) { - std::optional step{next()}; + const std::optional step{findStepPath()}; if (step.has_value()) { - snake.push_back(step.value()); - if (snake.back() == target) + if (!snakePushBack(step.value())) { - Display.setPixel(target.x, target.y, UINT8_MAX); + setDead(); + } + else if (snake[head] == target) + { + Display.setPixel(target, UINT8_MAX); setTarget(); } else { - const uint8_t step{static_cast(UINT8_MAX / snake.size())}; - for (size_t idx{0U}; idx < snake.size(); ++idx) + const uint8_t step{static_cast(UINT8_MAX / length)}; + for (size_t idx{0U}; idx < length; ++idx) { - Display.setPixel(snake[idx].x, snake[idx].y, step * (idx + 1U)); + Display.setPixel(snakeAt(idx), step * (idx + 1U)); } - Display.setPixel(snake.front().x, snake.front().y, 0U); - snake.pop_front(); + Display.setPixel(snakePopFront(), 0U); } } else { - lastMillis = millis(); - blinkCount = 0U; - stage = 2U; + setDead(); } lastMillis = millis(); } @@ -223,13 +245,13 @@ void SnakeMode::blink() if (millis() - lastMillis > UINT8_MAX) { const uint8_t brightness{static_cast((blinkCount & 0b1U) == 0U ? 0U : UINT8_MAX)}; - for (const Pixel &pixel : snake) + for (size_t idx{0U}; idx < length; ++idx) { - Display.setPixel(pixel.x, pixel.y, brightness); + Display.setPixel(snakeAt(idx), brightness); } if (++blinkCount >= 6U) { - stage = 3U; + stage = Stage::REMOVE; } lastMillis = millis(); } @@ -237,19 +259,72 @@ void SnakeMode::blink() void SnakeMode::clean() { - if (millis() - lastMillis > INT8_MAX && !snake.empty()) + if (millis() - lastMillis > INT8_MAX && length > 0U) { - Display.setPixel(snake.front().x, snake.front().y, 0U); - snake.pop_front(); + Display.setPixel(snakePopFront(), 0U); lastMillis = millis(); } - else if (snake.empty()) + else if (length == 0U) { - Display.setPixel(target.x, target.y, 0U); - stage = 0U; + Display.setPixel(target, 0U); + stage = Stage::READY; } } +void SnakeMode::snakeReset(size_t start) +{ + snakeClear(); + snake[0U] = start; + occupied[start] = true; + head = 0U; + length = 1U; +} + +void SnakeMode::snakeClear() +{ + occupied.fill(false); + head = 0U; + length = 0U; +} + +bool SnakeMode::snakePushBack(size_t pixel) +{ + if (length >= GRID_COLUMNS * GRID_ROWS) + { + return false; + } + head = (head + 1U) % (GRID_COLUMNS * GRID_ROWS); + snake[head] = pixel; + occupied[pixel] = true; + ++length; + return true; +} + +size_t SnakeMode::snakePopFront() +{ + const size_t tail{((GRID_COLUMNS * GRID_ROWS) + head - (length - 1U)) % (GRID_COLUMNS * GRID_ROWS)}; + occupied[snake[tail]] = false; + --length; + if (length == 0U) + { + head = 0U; + } + return snake[tail]; +} + +size_t SnakeMode::snakeAt(size_t index) const +{ + const size_t tail{((GRID_COLUMNS * GRID_ROWS) + head - (length - 1U)) % (GRID_COLUMNS * GRID_ROWS)}; + return snake[(tail + index) % (GRID_COLUMNS * GRID_ROWS)]; +} + +void SnakeMode::setDead() +{ + blinkCount = 0U; + lastMillis = millis(); + stage = Stage::DEATH; +} + /** * @brief Selects an unoccupied display position as the snake's target. * @@ -258,13 +333,22 @@ void SnakeMode::clean() */ void SnakeMode::setTarget() { - const uint8_t yMin{static_cast(clock == nullptr ? 0U : 5U)}; - do // NOLINT(cppcoreguidelines-avoid-do-while) + std::array available{}; + size_t count{0U}; + for (size_t idx{static_cast(clock == nullptr ? 0U : 5U * GRID_COLUMNS)}; idx < available.size(); ++idx) { - target.x = static_cast(random(GRID_COLUMNS)); - target.y = static_cast(random(yMin, GRID_ROWS)); - } while (Display.getPixel(target.x, target.y) != 0U); - Display.setPixel(target.x, target.y, static_cast(random(1, 0b1U << 8U))); + if (!occupied[idx]) + { + available[count++] = idx; + } + } + if (count == 0U) + { + setDead(); + return; + } + target = available[static_cast(random(static_cast(count)))]; + Display.setPixel(target, static_cast(random(1L, static_cast(0b1U << 8U)))); } /** @@ -287,7 +371,7 @@ void SnakeMode::setClock(bool _clock) if (_clock) { clock = std::make_unique(); - Display.setPixel(target.x, target.y, 0U); + Display.setPixel(target, 0U); setTarget(); } else if (clock != nullptr)