#pragma once #include #include #include /** * Pure, self-contained Breakout game logic. * * No Arduino/display dependencies - designed to be unit-tested natively (see test/test_breakout) * and reused by the Breakout adapter below without pulling in the display stack. * * The board is a fixed BOARD_W x BOARD_H pixel field. A grid of bricks sits near the top, a paddle * slides along the bottom, and a ball bounces between them. Ball position/velocity are tracked in * fixed-point sub-pixels (SUBPX per pixel) so movement is smooth and deterministic; everything is * integer math and statically sized (no heap). */ class BreakoutGame { public: // Playfield, in pixels (a standard 128x64 OLED in landscape). static constexpr int16_t BOARD_W = 128; static constexpr int16_t BOARD_H = 64; // Brick grid. static constexpr uint8_t BRICK_COLS = 8; static constexpr uint8_t BRICK_ROWS = 5; static constexpr int16_t BRICK_W = BOARD_W / BRICK_COLS; // 16 static constexpr int16_t BRICK_H = 4; static constexpr int16_t BRICK_TOP = 12; // top of the first course; leaves room for the score row // Paddle. static constexpr int16_t PADDLE_W = 24; static constexpr int16_t PADDLE_H = 2; static constexpr int16_t PADDLE_Y = BOARD_H - 3; // top edge of the paddle static constexpr int16_t PADDLE_STEP = 6; // pixels moved per input event static constexpr uint8_t START_LIVES = 3; static constexpr uint8_t POINTS_PER_BRICK = 10; /** (Re)start a full game: rebuild bricks, reset lives/score, serve the ball. `seed` drives the * xorshift32 RNG used for the initial serve direction. */ void reset(uint32_t seed); /** Advance the simulation by one tick (move the ball, resolve collisions). Returns true if the * game is still in progress, false once the last life is lost. No-op returning false once dead. */ bool step(); /** Slide the paddle; the ball is unaffected. moveLeft/moveRight use the default per-press step; * movePaddle takes an explicit signed pixel delta (used when polling a held joystick). */ void movePaddle(int16_t dxPx); void moveLeft(); void moveRight(); bool isPlaying() const { return alive; } uint32_t score() const { return points; } uint8_t lives() const { return livesLeft; } uint8_t level() const { return levelNum; } uint16_t bricksRemaining() const { return bricksLeft; } // --- Rendering accessors (all in board pixels) --- int16_t paddleX() const { return paddleLeft; } int16_t ballX() const { return static_cast(ballPxX / SUBPX); } int16_t ballY() const { return static_cast(ballPxY / SUBPX); } bool brickAt(uint8_t row, uint8_t col) const { return row < BRICK_ROWS && col < BRICK_COLS && bricks[row][col]; } private: static constexpr int32_t SUBPX = 16; // fixed-point sub-pixels per pixel static constexpr int32_t BALL_VY = 40; // vertical ball speed (sub-pixels/step) static constexpr int16_t BALL_SIZE = 2; // ball is drawn BALL_SIZE x BALL_SIZE void buildBricks(); void serveBall(); void nextLevel(); uint32_t nextRandom(); uint8_t bricks[BRICK_ROWS][BRICK_COLS] = {0}; // 0 == cleared, 1 == present uint16_t bricksLeft = 0; int16_t paddleLeft = 0; // left edge of the paddle, in pixels int32_t ballPxX = 0, ballPxY = 0; // ball centre, in sub-pixels int32_t ballVx = 0, ballVy = 0; // ball velocity, in sub-pixels/step uint32_t points = 0; uint8_t livesLeft = 0; uint8_t levelNum = 1; uint32_t rng = 1; // xorshift32 state (never 0) bool alive = false; bool ballTick = false; // ball advances on every other step() (see step()) }; #include "configuration.h" #if HAS_SCREEN && BASEUI_HAS_GAMES #include "Game.h" #include "HighScoreTable.h" /** * Breakout as a hosted Game. Wraps the pure BreakoutGame logic above and supplies the attract art, * the playfield renderer, the paddle input, the level-based speed curve, and its own local * high-score table. No mesh protocol (scores are local-only). */ class Breakout : public Game { public: Breakout(); const char *name() const override { return "Breakout"; } void start(uint32_t seed) override { game.reset(seed); } bool tick() override; // polls a held joystick for the paddle, then advances the ball 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; HighScoreTableBase &scores() override { return scores_; } private: // On-disk high-score record. New file (magic 'BRKT', version 1); layout mirrors Snake's. struct BreakoutEntry { uint32_t score; uint32_t nodeNum; char shortName[5]; // three characters, NUL-terminated uint32_t epoch; // getValidTime(), 0 if no RTC } __attribute__((packed)); BreakoutGame game; HighScoreTable scores_{"/prefs/breakout.dat", 0x424B5254u, 1, "Breakout"}; }; #endif // HAS_SCREEN && BASEUI_HAS_GAMES