* 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>
157 lines
5.2 KiB
C++
157 lines
5.2 KiB
C++
#pragma once
|
||
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
|
||
/**
|
||
* Pure, self-contained Tetris game logic.
|
||
*
|
||
* No Arduino/display dependencies - designed to be unit-tested natively and reused by the Tetris
|
||
* adapter below without pulling in the display stack.
|
||
*
|
||
* Board coordinate system: col=0 is leftmost, row=0 is top (gravity goes toward higher rows).
|
||
* board[row][col] holds 0 (empty) or 1..7 (locked piece colour index).
|
||
* No dynamic allocation: total struct size is ~260 bytes.
|
||
*/
|
||
class TetrisGame
|
||
{
|
||
public:
|
||
static constexpr uint8_t BOARD_COLS = 10;
|
||
static constexpr uint8_t BOARD_ROWS = 16; // 16×4 px = 64 px - fills a standard 64px OLED
|
||
static constexpr uint8_t PIECE_TYPES = 7; // I O T S Z J L
|
||
|
||
struct Piece {
|
||
int8_t col; // left column of the 4×4 bounding box (may be negative during spawn)
|
||
int8_t row; // top row of the 4×4 bounding box (may be negative during spawn)
|
||
uint8_t type; // 0..6
|
||
uint8_t rot; // 0..3
|
||
};
|
||
|
||
// board[row][col]: 0 = empty, 1..7 = locked piece colour
|
||
uint8_t board[BOARD_ROWS][BOARD_COLS];
|
||
|
||
/** Start (or restart) the game. seed drives the xorshift32 RNG. */
|
||
void reset(uint32_t seed);
|
||
|
||
/** Shift the current piece left one column. Returns true if it moved. */
|
||
bool moveLeft();
|
||
|
||
/** Shift the current piece right one column. Returns true if it moved. */
|
||
bool moveRight();
|
||
|
||
/**
|
||
* Rotate the current piece CW. Tries a basic wall-kick (±1, ±2 column) if the
|
||
* natural rotation overlaps a wall or locked cell. Returns true if it rotated.
|
||
*/
|
||
bool rotate();
|
||
|
||
/**
|
||
* Move the current piece down one row. If it cannot fall it is locked,
|
||
* lines are cleared, the next piece becomes current, and a new next is generated.
|
||
* Returns false after locking (game may or may not be over).
|
||
*/
|
||
bool softDrop();
|
||
|
||
/**
|
||
* Instantly drop the current piece to where it would land and lock it.
|
||
* Awards 2 pts per row dropped.
|
||
*/
|
||
void hardDrop();
|
||
|
||
/**
|
||
* Gravity tick: same as softDrop() - move down one row, lock if needed.
|
||
* Returns true while the game is alive, false after game-over.
|
||
*/
|
||
bool step();
|
||
|
||
bool isPlaying() const { return alive; }
|
||
uint32_t score() const { return pts; }
|
||
uint8_t level() const { return lvl; }
|
||
uint16_t linesCleared() const { return lines; }
|
||
|
||
const Piece ¤t() const { return cur; }
|
||
const Piece &next() const { return nxt; }
|
||
|
||
/**
|
||
* Returns the top row the current piece would occupy if instantly dropped.
|
||
* Used by the renderer to show a ghost/shadow piece.
|
||
*/
|
||
int8_t ghostRow() const;
|
||
|
||
/**
|
||
* Returns true if cell (pr, pc) within the 4×4 bounding box is filled for
|
||
* the given piece type and rotation. Safe for any (type, rot, pr, pc).
|
||
*/
|
||
static bool pieceCell(uint8_t type, uint8_t rot, uint8_t pr, uint8_t pc);
|
||
|
||
private:
|
||
Piece cur = {};
|
||
Piece nxt = {};
|
||
uint32_t pts = 0;
|
||
uint8_t lvl = 1;
|
||
uint16_t lines = 0;
|
||
uint32_t rng = 1; // xorshift32 state - must never be 0
|
||
bool alive = false;
|
||
|
||
uint32_t nextRandom();
|
||
Piece spawnPiece(uint8_t type) const;
|
||
void advanceNext(); // lock cur, clear lines, shift nxt→cur, spawn new nxt
|
||
bool canPlace(const Piece &p) const;
|
||
void lockPiece();
|
||
int clearLines(); // returns number of lines cleared (0..4)
|
||
|
||
// Shape table: SHAPES[type][rot][row] = 4-bit column bitmask.
|
||
// Bit 0 = leftmost column (col 0), bit 3 = rightmost (col 3) of the 4×4 box.
|
||
static const uint8_t SHAPES[PIECE_TYPES][4][4];
|
||
};
|
||
|
||
#include "configuration.h"
|
||
|
||
#if HAS_SCREEN && BASEUI_HAS_GAMES
|
||
|
||
#include "Game.h"
|
||
#include "HighScoreTable.h"
|
||
|
||
/**
|
||
* Tetris as a hosted Game. Wraps the pure TetrisGame logic above and supplies the attract art, the
|
||
* portrait playfield renderer, the rotate/move/drop input, the level-based speed curve, and its
|
||
* own high-score table. The new-high-score mesh announcement is shared by all games and lives in
|
||
* GamesModule.
|
||
*/
|
||
class Tetris : public Game
|
||
{
|
||
public:
|
||
Tetris();
|
||
|
||
const char *name() const override { return "Tetris"; }
|
||
|
||
void start(uint32_t seed) override { game.reset(seed); }
|
||
bool tick() override { return game.step(); }
|
||
bool isPlaying() const override { return game.isPlaying(); }
|
||
uint32_t score() const override { return game.score(); }
|
||
int32_t tickIntervalMs() const override;
|
||
|
||
void handleInput(input_broker_event ev) override;
|
||
|
||
void drawAttract(OLEDDisplay *display, int16_t x, int16_t y) override;
|
||
void drawPlaying(OLEDDisplay *display, int16_t x, int16_t y) override;
|
||
const char *gameOverHint() const override { return "SEL: scores BCK: exit"; }
|
||
|
||
HighScoreTableBase &scores() override { return scores_; }
|
||
|
||
private:
|
||
// On-disk high-score record; layout preserved from the original TetrisModule so tetris.dat
|
||
// keeps loading. Magic 'TETR', file version 1.
|
||
struct TetrisEntry {
|
||
uint32_t score;
|
||
char shortName[5]; // NUL-terminated 3-char display name
|
||
uint32_t nodeNum;
|
||
uint32_t epoch;
|
||
} __attribute__((packed));
|
||
|
||
TetrisGame game;
|
||
HighScoreTable<TetrisEntry> scores_{"/prefs/tetris.dat", 0x54455452u, 1, "Tetris"};
|
||
};
|
||
|
||
#endif // HAS_SCREEN && BASEUI_HAS_GAMES
|