mirror of
https://github.com/meshtastic/firmware.git
synced 2026-08-01 10:58:30 -04:00
* Snake! * Add spiLock to snake score saving * Check fixes * More careful locking * WIP: Big Display Node * Update src/graphics/HUB75Display.cpp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Add HUB75 Native * Add Tetris game module with core logic and UI integration - Implement TetrisGame class for game logic, including piece movement, rotation, and line clearing. - Create TetrisModule class to manage game state, input handling, and rendering on OLED display. - Introduce high score tracking with persistence and optional mesh broadcasting. - Define UI states for title, playing, paused, game over, and high scores. - Implement input handling for game controls and state transitions. - Add rendering functions for the game board, high scores, and title screen. * feat(snake): add snake graphics and update display logic in SnakeModule * Prompt for Initials for Tetris, too * refactor games module * Games refactor * hub75 native double buffer * Games tuning * Make joystick repeat events on held button * Add clouds and colors * Fix breakout and more color * difficulty tuning * trunk * refactor game announcements, etc * Scale chirpy gravity as game speed increases * Portduino, check for hub75 display before reading in hub75 options * Final(?) games tuning * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Properly ignore input when games screen not shown * Fail gracefully when HUB75 is selected but not supported. --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Ixitxachitl <kramerfm@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
181 lines
5.9 KiB
C++
181 lines
5.9 KiB
C++
#include "TestUtil.h"
|
|
#include "modules/games/Snake.h"
|
|
#include <unity.h>
|
|
|
|
// Pure-logic tests for SnakeGame: ring-buffer advance, reversal rejection, wall/self collision,
|
|
// growth on eat, and food-placement validity. No device globals or display stack required.
|
|
|
|
static const uint32_t kSeed = 0xC0FFEEu;
|
|
|
|
// Count how many board cells the snake currently occupies (cross-check for len()).
|
|
static uint16_t countOccupied(const SnakeGame &game)
|
|
{
|
|
uint16_t n = 0;
|
|
for (uint8_t y = 0; y < SnakeGame::GRID_H; y++)
|
|
for (uint8_t x = 0; x < SnakeGame::GRID_W; x++)
|
|
if (game.occupied(x, y))
|
|
n++;
|
|
return n;
|
|
}
|
|
|
|
static void test_reset_initialState()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed);
|
|
|
|
TEST_ASSERT_TRUE(game.isPlaying());
|
|
TEST_ASSERT_FALSE(game.isWon());
|
|
TEST_ASSERT_EQUAL_UINT16(SnakeGame::START_LEN, game.length());
|
|
TEST_ASSERT_EQUAL_UINT32(0u, game.score());
|
|
TEST_ASSERT_EQUAL_INT(SnakeGame::DIR_RIGHT, game.direction());
|
|
|
|
// Head spawns at board centre; the whole test file relies on this anchor.
|
|
SnakeGame::Cell head = game.head();
|
|
TEST_ASSERT_EQUAL_UINT8(SnakeGame::GRID_W / 2, head.x);
|
|
TEST_ASSERT_EQUAL_UINT8(SnakeGame::GRID_H / 2, head.y);
|
|
|
|
// Exactly START_LEN cells occupied, and the head is one of them.
|
|
TEST_ASSERT_EQUAL_UINT16(SnakeGame::START_LEN, countOccupied(game));
|
|
TEST_ASSERT_TRUE(game.occupied(head.x, head.y));
|
|
}
|
|
|
|
static void test_food_isValidAndOffBody()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed);
|
|
SnakeGame::Cell food = game.food();
|
|
TEST_ASSERT_TRUE(food.x < SnakeGame::GRID_W);
|
|
TEST_ASSERT_TRUE(food.y < SnakeGame::GRID_H);
|
|
TEST_ASSERT_FALSE(game.occupied(food.x, food.y)); // food never spawns on the snake
|
|
}
|
|
|
|
static void test_setDirection_rejectsReversal()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed); // heading right
|
|
|
|
TEST_ASSERT_FALSE(game.setDirection(SnakeGame::DIR_LEFT)); // 180 reversal -> rejected
|
|
TEST_ASSERT_TRUE(game.setDirection(SnakeGame::DIR_UP)); // perpendicular -> ok
|
|
TEST_ASSERT_TRUE(game.setDirection(SnakeGame::DIR_RIGHT)); // same as committed dir -> ok (no-op)
|
|
|
|
// A double-input within one tick can't chain into a reversal: after latching UP, LEFT is
|
|
// still checked against the committed RIGHT and rejected, so the neck stays safe.
|
|
game.setDirection(SnakeGame::DIR_UP);
|
|
TEST_ASSERT_FALSE(game.setDirection(SnakeGame::DIR_LEFT));
|
|
}
|
|
|
|
static void test_step_movesAndTailFollows()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed);
|
|
SnakeGame::Cell head = game.head();
|
|
game.placeFoodAt(0, 0); // corner, off the snake -> guaranteed non-eating step
|
|
|
|
TEST_ASSERT_TRUE(game.step());
|
|
SnakeGame::Cell newHead = game.head();
|
|
TEST_ASSERT_EQUAL_UINT8(head.x + 1, newHead.x); // moved one cell right
|
|
TEST_ASSERT_EQUAL_UINT8(head.y, newHead.y);
|
|
TEST_ASSERT_EQUAL_UINT16(SnakeGame::START_LEN, game.length()); // length unchanged when not eating
|
|
TEST_ASSERT_EQUAL_UINT16(SnakeGame::START_LEN, countOccupied(game));
|
|
TEST_ASSERT_EQUAL_UINT32(0u, game.score());
|
|
}
|
|
|
|
static void test_eat_growsAndScores()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed);
|
|
SnakeGame::Cell head = game.head();
|
|
game.placeFoodAt(head.x + 1, head.y); // food directly ahead
|
|
|
|
TEST_ASSERT_TRUE(game.step());
|
|
TEST_ASSERT_EQUAL_UINT16(SnakeGame::START_LEN + 1, game.length()); // grew by one
|
|
TEST_ASSERT_EQUAL_UINT32(1u, game.score());
|
|
TEST_ASSERT_EQUAL_UINT16(SnakeGame::START_LEN + 1, countOccupied(game));
|
|
|
|
// A fresh food was placed and is not on the snake.
|
|
SnakeGame::Cell food = game.food();
|
|
TEST_ASSERT_FALSE(game.occupied(food.x, food.y));
|
|
}
|
|
|
|
static void test_wallCollision_endsGame()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed);
|
|
game.placeFoodAt(0, 0);
|
|
game.setDirection(SnakeGame::DIR_UP); // head is at mid-height; drive straight up into the wall
|
|
|
|
bool alive = true;
|
|
int guard = 0;
|
|
while (alive && guard++ < SnakeGame::GRID_H + 4) {
|
|
game.placeFoodAt(0, 0); // keep food out of the way each tick
|
|
alive = game.step();
|
|
}
|
|
TEST_ASSERT_FALSE(alive);
|
|
TEST_ASSERT_FALSE(game.isPlaying());
|
|
}
|
|
|
|
static void test_selfCollision_endsGame()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed);
|
|
TEST_ASSERT_EQUAL_UINT8(16, game.head().x); // anchor the deterministic path below
|
|
TEST_ASSERT_EQUAL_UINT8(6, game.head().y);
|
|
|
|
// Grow to length 5 along a straight horizontal line (cells (14..18, 6)).
|
|
game.placeFoodAt(17, 6);
|
|
TEST_ASSERT_TRUE(game.step());
|
|
game.placeFoodAt(18, 6);
|
|
TEST_ASSERT_TRUE(game.step());
|
|
TEST_ASSERT_EQUAL_UINT16(5, game.length());
|
|
|
|
// Curl back on itself: DOWN, LEFT, then UP re-enters an occupied body cell.
|
|
game.setDirection(SnakeGame::DIR_DOWN);
|
|
game.placeFoodAt(0, 0);
|
|
TEST_ASSERT_TRUE(game.step());
|
|
game.setDirection(SnakeGame::DIR_LEFT);
|
|
game.placeFoodAt(0, 0);
|
|
TEST_ASSERT_TRUE(game.step());
|
|
game.setDirection(SnakeGame::DIR_UP);
|
|
game.placeFoodAt(0, 0);
|
|
TEST_ASSERT_FALSE(game.step()); // bites its own body
|
|
TEST_ASSERT_FALSE(game.isPlaying());
|
|
}
|
|
|
|
static void test_deadGame_stepIsNoOp()
|
|
{
|
|
SnakeGame game;
|
|
game.reset(kSeed);
|
|
game.setDirection(SnakeGame::DIR_UP);
|
|
for (int i = 0; i < SnakeGame::GRID_H + 4; i++) {
|
|
game.placeFoodAt(0, 0);
|
|
game.step();
|
|
}
|
|
TEST_ASSERT_FALSE(game.isPlaying());
|
|
uint32_t scoreBefore = game.score();
|
|
TEST_ASSERT_FALSE(game.step()); // stays dead, no state change
|
|
TEST_ASSERT_EQUAL_UINT32(scoreBefore, game.score());
|
|
}
|
|
|
|
void setUp(void) {}
|
|
|
|
void tearDown(void) {}
|
|
|
|
extern "C" {
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_reset_initialState);
|
|
RUN_TEST(test_food_isValidAndOffBody);
|
|
RUN_TEST(test_setDirection_rejectsReversal);
|
|
RUN_TEST(test_step_movesAndTailFollows);
|
|
RUN_TEST(test_eat_growsAndScores);
|
|
RUN_TEST(test_wallCollision_endsGame);
|
|
RUN_TEST(test_selfCollision_endsGame);
|
|
RUN_TEST(test_deadGame_stepIsNoOp);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|
|
}
|