From c7c76e8875d4e6e055e6dcf968d048a5bd712dfa Mon Sep 17 00:00:00 2001 From: Sterling Archer Date: Thu, 16 Jul 2026 21:29:16 -0700 Subject: [PATCH] Add Dino Game plugin --- install.py | 76 +++++- plugins/dinogame/DinoActivity.cpp | 395 ++++++++++++++++++++++++++++++ plugins/dinogame/DinoActivity.h | 80 ++++++ plugins/dinogame/plugin.py | 33 +++ 4 files changed, 576 insertions(+), 8 deletions(-) create mode 100644 plugins/dinogame/DinoActivity.cpp create mode 100644 plugins/dinogame/DinoActivity.h create mode 100644 plugins/dinogame/plugin.py diff --git a/install.py b/install.py index afe44a6..fe9c829 100755 --- a/install.py +++ b/install.py @@ -22,6 +22,12 @@ ENVIRONMENTS = { "slim": "No serial logging, smallest binary size", } +BAUD_RATES = { + "1": ("115200", "Recommended - most reliable, avoids 'chip stopped responding'"), + "2": ("921600", "Faster - for those feeling froggy"), +} +DEFAULT_BAUD = "115200" + def run(cmd, cwd=None, check=True): print(f" $ {cmd}") @@ -70,7 +76,7 @@ def find_backup_files(): return sorted(glob.glob("backup.*.bin"), reverse=True) -def restore_from_backup(): +def restore_from_backup(yes_all: bool = False): """If any backup.*.bin files are sitting in the current directory, offer to write one back to the device instead of proceeding with a fresh install. This always runs before backup_device() and, if the user goes @@ -80,6 +86,10 @@ def restore_from_backup(): if not backups: return + if yes_all: + print(f" Found {len(backups)} existing backup(s), skipping restore (--yes).") + return + print(f" Found {len(backups)} existing backup{'s' if len(backups) != 1 else ''}:") for i, b in enumerate(backups, 1): tag = " ← most recent" if i == 1 else "" @@ -152,7 +162,11 @@ def restore_from_backup(): sys.exit("Aborted.") -def backup_device(): +def backup_device(yes_all: bool = False): + if yes_all: + print(" Skipping device backup (--yes).") + return + print(" Backup your current firmware before flashing? [Y/n]: ", end="", flush=True) answer = input().strip().lower() if answer not in ("", "y", "yes"): @@ -423,13 +437,47 @@ def prompt_for_upload_port(): print(" Please enter a valid port name.") -def build_and_flash(environment: str): +def prompt_for_baud_rate(override: str | None, yes_all: bool = False): + if override: + print(f"\n Upload baud rate: {override} (--baud)") + return override + + if yes_all: + print(f"\n Upload baud rate: {DEFAULT_BAUD} (--yes)") + return DEFAULT_BAUD + + print("\n Select upload baud rate:") + for key, (rate, desc) in BAUD_RATES.items(): + tag = " ← default" if rate == DEFAULT_BAUD else "" + print(f" {key}. {rate} — {desc}{tag}") + + choice = input( + f"\n Press ENTER for default ({DEFAULT_BAUD}), enter 1-{len(BAUD_RATES)}, " + "or type a custom rate: " + ).strip() + + if not choice: + return DEFAULT_BAUD + if choice in BAUD_RATES: + return BAUD_RATES[choice][0] + if choice.isdigit(): + return choice + + print(f" Invalid input, using default ({DEFAULT_BAUD}).") + return DEFAULT_BAUD + + +def build_and_flash(environment: str, baud_rate: str): print(f"\nBuilding firmware with PlatformIO (environment: {environment})...") run(f"pio run --environment {environment}", cwd=REPO_DIR) port = prompt_for_upload_port() - print(f"\n Flashing to {port}...") + local_ini = os.path.join(REPO_DIR, "platformio.local.ini") + with open(local_ini, "w") as f: + f.write(f"[env:{environment}]\nupload_speed = {baud_rate}\n") + + print(f"\n Flashing to {port} at {baud_rate} baud...") run( f"pio run --target upload --environment {environment} --upload-port {port}", cwd=REPO_DIR, @@ -452,7 +500,18 @@ def parse_args(): parser.add_argument( "-y", "--yes", action="store_true", - help="Auto-answer Y to all plugin install prompts", + help="Auto-answer Y to all plugin install prompts, and skip the " + "restore/backup prompts entirely (fast install, no backup)", + ) + parser.add_argument( + "-b", "--baud", + default=None, + metavar="RATE", + help=( + "Upload baud rate override, skips the interactive prompt " + f"(default: {DEFAULT_BAUD}). Lower it (e.g. 115200) if flashing " + "fails with 'chip stopped responding'." + ), ) parser.add_argument( "--no-reclone", @@ -483,13 +542,14 @@ def main(): print(" Aborted.") sys.exit(0) print() - restore_from_backup() - backup_device() + restore_from_backup(yes_all=args.yes) + backup_device(yes_all=args.yes) clone_repo(force=not args.no_reclone) clear_plugin_caches() chosen_plugins = select_plugins(yes_all=args.yes) apply_plugins(chosen_plugins, yes_all=args.yes) - build_and_flash(args.environment) + baud_rate = prompt_for_baud_rate(args.baud, yes_all=args.yes) + build_and_flash(args.environment, baud_rate) print("\nDone.") diff --git a/plugins/dinogame/DinoActivity.cpp b/plugins/dinogame/DinoActivity.cpp new file mode 100644 index 0000000..f6a9b42 --- /dev/null +++ b/plugins/dinogame/DinoActivity.cpp @@ -0,0 +1,395 @@ +#include "DinoActivity.h" + +#include +#include "MappedInputManager.h" +#include "fontIds.h" + +namespace { + +struct SpriteRow { + uint8_t row, c0, c1; +}; + +constexpr int DINO_GRID_W = 22; +constexpr int DINO_GRID_H = 20; +constexpr int DINO_COLLIDE_X_OFFSET = 6; +constexpr int CACTUS_SMALL_W = 15; +constexpr int CACTUS_SMALL_H = 20; +constexpr int CACTUS_BIG_W = 15; +constexpr int CACTUS_BIG_H = 26; +constexpr int BIRD_W = 18; +constexpr int BIRD_H = 7; + +constexpr SpriteRow DINO_HEAD_BODY[] = { + {0,14,19},{1,13,20},{2,13,20},{3,13,20},{4,13,21},{5,13,21},{6,10,19}, + {7,11,17},{8,9,18},{9,6,18},{10,0,15},{11,0,15},{12,0,15}, + {13,0,0},{13,4,15},{14,8,15}, +}; + +constexpr SpriteRow DINO_RUN1_LEGS[] = { + {15,6,9},{16,6,9},{17,6,9},{18,6,9},{19,5,9}, + {15,11,14},{16,11,14},{17,11,14}, +}; + +constexpr SpriteRow DINO_RUN2_LEGS[] = { + {15,6,9},{16,6,9},{17,6,9}, + {15,11,14},{16,11,14},{17,11,14},{18,11,14},{19,11,15}, +}; + +constexpr SpriteRow DINO_JUMP_LEGS[] = { + {15,7,14},{16,7,14},{17,7,14}, +}; + +constexpr SpriteRow CACTUS_SMALL[] = { + {0,7,7}, + {1,6,8}, + {2,3,3},{2,6,8},{2,11,11}, + {3,2,4},{3,6,8},{3,10,12}, + {4,2,4},{4,6,8},{4,10,12}, + {5,2,4},{5,6,8},{5,10,12}, + {6,2,4},{6,6,8},{6,10,12}, + {7,2,4},{7,6,8},{7,10,12}, + {8,2,4},{8,6,8},{8,10,12}, + {9,2,12}, + {10,2,12}, + {11,6,8},{12,6,8},{13,6,8},{14,6,8},{15,6,8}, + {16,6,8},{17,6,8},{18,6,8},{19,6,8}, +}; + +constexpr SpriteRow CACTUS_BIG[] = { + {0,7,7}, + {1,6,8}, + {2,3,3},{2,6,8},{2,11,11}, + {3,2,4},{3,6,8},{3,10,12}, + {4,2,4},{4,6,8},{4,10,12}, + {5,2,4},{5,6,8},{5,10,12}, + {6,2,4},{6,6,8},{6,10,12}, + {7,2,4},{7,6,8},{7,10,12}, + {8,2,4},{8,6,8},{8,10,12}, + {9,2,12}, + {10,2,12}, + {11,6,8},{12,6,8},{13,6,8},{14,6,8},{15,6,8}, + {16,6,8},{17,6,8},{18,6,8},{19,6,8},{20,6,8},{21,6,8}, + {22,6,8},{23,6,8},{24,6,8},{25,6,8}, +}; + +constexpr SpriteRow BIRD_UP[] = { + {0,2,3},{0,14,15}, + {1,4,5},{1,12,13}, + {2,6,7},{2,10,11}, + {3,6,11}, + {4,7,10}, + {5,8,9}, +}; + +constexpr SpriteRow BIRD_DOWN[] = { + {0,7,10}, + {1,6,11}, + {2,7,10}, + {3,8,9}, + {4,6,7},{4,10,11}, + {5,4,5},{5,12,13}, + {6,2,3},{6,14,15}, +}; + +template +constexpr size_t lengthOf(const SpriteRow (&)[N]) { return N; } + +void drawSprite(GfxRenderer& r, const SpriteRow* rows, size_t count, int x, int y, int block) { + for (size_t i = 0; i < count; i++) { + int w = (rows[i].c1 - rows[i].c0 + 1) * block; + r.fillRect(x + rows[i].c0 * block, y + rows[i].row * block, w, block, true); + } +} + +} // namespace + +void DinoActivity::onEnter() { + Activity::onEnter(); + + screenW_ = renderer.getScreenWidth(); + screenH_ = renderer.getScreenHeight(); + groundY_ = screenH_ - GROUND_MARGIN; + + randomSeed(millis()); + resetGame(); + + renderPending_ = true; + requestUpdate(); +} + +void DinoActivity::onExit() { + Activity::onExit(); +} + +void DinoActivity::resetGame() { + dinoY_ = 0; + velocityY_ = 0; + jumping_ = false; + + for (auto& ob : obstacles_) { + ob.active = false; + } + distanceSinceSpawn_ = 0; + nextSpawnDistance_ = START_SPEED * (float)random(MIN_GAP_TICKS, MAX_GAP_TICKS); + + birdX_ = screenW_ + random(0, 200); + birdY_ = random(BIRD_TOP_MIN, BIRD_TOP_MAX); + + gameSpeed_ = START_SPEED; + score_ = 0; + gameOver_ = false; + animFrame_ = false; + + lastUpdate_ = millis(); + lastAnim_ = lastUpdate_; + lastScoreTick_ = lastUpdate_; +} + +int DinoActivity::obstacleWidthPx(ObstacleType type) const { + switch (type) { + case ObstacleType::CactusSmall: return CACTUS_SMALL_W * BLOCK; + case ObstacleType::CactusBig: return CACTUS_BIG_W * BLOCK; + } + return 0; +} + +int DinoActivity::obstacleHeightPx(ObstacleType type) const { + switch (type) { + case ObstacleType::CactusSmall: return CACTUS_SMALL_H * BLOCK; + case ObstacleType::CactusBig: return CACTUS_BIG_H * BLOCK; + } + return 0; +} + +void DinoActivity::spawnObstacle() { + int slot = -1; + for (int i = 0; i < MAX_OBSTACLES; i++) { + if (!obstacles_[i].active) { + slot = i; + break; + } + } + if (slot < 0) return; + + ObstacleType type = (random(0, 100) < 55) ? ObstacleType::CactusSmall : ObstacleType::CactusBig; + + obstacles_[slot].active = true; + obstacles_[slot].type = type; + obstacles_[slot].x = screenW_; +} + +bool DinoActivity::anyNonPowerPressed() const { + return mappedInput.wasPressed(MappedInputManager::Button::Back) || + mappedInput.wasPressed(MappedInputManager::Button::Confirm) || + mappedInput.wasPressed(MappedInputManager::Button::Left) || + mappedInput.wasPressed(MappedInputManager::Button::Right) || + mappedInput.wasPressed(MappedInputManager::Button::Up) || + mappedInput.wasPressed(MappedInputManager::Button::Down); +} + +bool DinoActivity::checkCollision() const { + constexpr int INSET = 4; + + int dw = (DINO_GRID_W - DINO_COLLIDE_X_OFFSET) * BLOCK; + int dh = DINO_GRID_H * BLOCK; + int dx0 = DINO_X + DINO_COLLIDE_X_OFFSET * BLOCK + INSET; + int dx1 = DINO_X + DINO_COLLIDE_X_OFFSET * BLOCK + dw - INSET; + int dy0 = groundY_ - dh + (int)dinoY_ + INSET; + int dy1 = groundY_ + (int)dinoY_ - INSET; + + for (const auto& ob : obstacles_) { + if (!ob.active) continue; + + int ow = obstacleWidthPx(ob.type); + int oh = obstacleHeightPx(ob.type); + int oy0 = groundY_ - oh; + int oy1 = oy0 + oh; + int ox0 = (int)ob.x; + int ox1 = ox0 + ow; + + if (dx0 < ox1 && dx1 > ox0 && dy0 < oy1 && dy1 > oy0) { + return true; + } + } + return false; +} + +void DinoActivity::updateGame() { + for (auto& ob : obstacles_) { + if (!ob.active) continue; + ob.x -= gameSpeed_; + if (ob.x + obstacleWidthPx(ob.type) < 0) { + ob.active = false; + } + } + + distanceSinceSpawn_ += gameSpeed_; + if (distanceSinceSpawn_ >= nextSpawnDistance_) { + spawnObstacle(); + distanceSinceSpawn_ = 0; + nextSpawnDistance_ = gameSpeed_ * (float)random(MIN_GAP_TICKS, MAX_GAP_TICKS); + } + + birdX_ -= BIRD_SPEED; + if (birdX_ + BIRD_W * BLOCK < 0) { + birdX_ = screenW_ + random(0, 200); + birdY_ = random(BIRD_TOP_MIN, BIRD_TOP_MAX); + } + + if (jumping_) { + velocityY_ += GRAVITY; + dinoY_ += velocityY_; + if (dinoY_ >= 0) { + dinoY_ = 0; + velocityY_ = 0; + jumping_ = false; + } + } + + gameSpeed_ = START_SPEED + SPEED_STEP * (float)(score_ / SCORE_PER_SPEEDUP); + if (gameSpeed_ > MAX_SPEED) gameSpeed_ = MAX_SPEED; + + if (checkCollision()) { + gameOver_ = true; + } +} + +void DinoActivity::loop() { + if (gameOver_) { + if (mappedInput.wasPressed(MappedInputManager::Button::Power)) { + while (mappedInput.isPressed(MappedInputManager::Button::Power)) { + mappedInput.update(); + delay(10); + } + finish(); + return; + } + if (anyNonPowerPressed()) { + resetGame(); + renderPending_ = true; + requestUpdate(); + } + return; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Power)) { + while (mappedInput.isPressed(MappedInputManager::Button::Power)) { + mappedInput.update(); + delay(10); + } + finish(); + return; + } + + if (anyNonPowerPressed() && !jumping_) { + jumping_ = true; + velocityY_ = JUMP_VELOCITY; + } + + unsigned long now = millis(); + + if (now - lastUpdate_ >= PHYSICS_INTERVAL_MS) { + lastUpdate_ = now; + updateGame(); + renderPending_ = true; + } + + if (now - lastAnim_ >= ANIM_INTERVAL_MS) { + lastAnim_ = now; + animFrame_ = !animFrame_; + renderPending_ = true; + } + + if (now - lastScoreTick_ >= SCORE_INTERVAL_MS) { + lastScoreTick_ = now; + score_++; + renderPending_ = true; + } + + if (renderPending_) { + requestUpdate(); + } +} + +void DinoActivity::drawDino(int x, int topY) const { + drawSprite(renderer, DINO_HEAD_BODY, lengthOf(DINO_HEAD_BODY), x, topY, BLOCK); + + const SpriteRow* legSpans; + size_t legCount; + if (jumping_) { + legSpans = DINO_JUMP_LEGS; + legCount = lengthOf(DINO_JUMP_LEGS); + } else { + legSpans = animFrame_ ? DINO_RUN2_LEGS : DINO_RUN1_LEGS; + legCount = animFrame_ ? lengthOf(DINO_RUN2_LEGS) : lengthOf(DINO_RUN1_LEGS); + } + drawSprite(renderer, legSpans, legCount, x, topY, BLOCK); + + constexpr int EYE_ROW = 2, EYE_C0 = 15, EYE_C1 = 16, EYE_ROWS = 2; + renderer.fillRect(x + EYE_C0 * BLOCK, topY + EYE_ROW * BLOCK, + (EYE_C1 - EYE_C0 + 1) * BLOCK, EYE_ROWS * BLOCK, false); +} + +void DinoActivity::drawObstacle(const Obstacle& ob) const { + int h = obstacleHeightPx(ob.type); + int x = (int)ob.x; + int y = groundY_ - h; + + switch (ob.type) { + case ObstacleType::CactusSmall: + drawSprite(renderer, CACTUS_SMALL, lengthOf(CACTUS_SMALL), x, y, BLOCK); + break; + case ObstacleType::CactusBig: + drawSprite(renderer, CACTUS_BIG, lengthOf(CACTUS_BIG), x, y, BLOCK); + break; + } +} + +void DinoActivity::drawBird() const { + if (animFrame_) { + drawSprite(renderer, BIRD_UP, lengthOf(BIRD_UP), (int)birdX_, birdY_, BLOCK); + } else { + drawSprite(renderer, BIRD_DOWN, lengthOf(BIRD_DOWN), (int)birdX_, birdY_, BLOCK); + } +} + +void DinoActivity::render(RenderLock&&) { + renderPending_ = false; + + renderer.clearScreen(); + + renderer.drawLine(0, groundY_, screenW_, groundY_); + + drawBird(); + + char scoreStr[16]; + snprintf(scoreStr, sizeof(scoreStr), "%05lu", score_); + int scoreW = renderer.getTextWidth(UI_10_FONT_ID, scoreStr); + renderer.drawText(UI_10_FONT_ID, screenW_ - scoreW - 24, 24, scoreStr, true); + + int dinoTopY = groundY_ - DINO_GRID_H * BLOCK + (int)dinoY_; + drawDino(DINO_X, dinoTopY); + + for (const auto& ob : obstacles_) { + if (ob.active) drawObstacle(ob); + } + + if (gameOver_) { + const char* line1 = "GAME OVER"; + const char* line2 = "Any button to Restart"; + const char* line3 = "Power button to Exit"; + + int w1 = renderer.getTextWidth(UI_12_FONT_ID, line1, EpdFontFamily::BOLD); + int w2 = renderer.getTextWidth(UI_10_FONT_ID, line2); + int w3 = renderer.getTextWidth(UI_10_FONT_ID, line3); + + int baseY = groundY_ / 2 - 20; + renderer.drawText(UI_12_FONT_ID, screenW_ / 2 - w1 / 2, baseY, + line1, true, EpdFontFamily::BOLD); + renderer.drawText(UI_10_FONT_ID, screenW_ / 2 - w2 / 2, baseY + 36, line2, true); + renderer.drawText(UI_10_FONT_ID, screenW_ / 2 - w3 / 2, baseY + 62, line3, true); + } + + renderer.displayBuffer(); +} diff --git a/plugins/dinogame/DinoActivity.h b/plugins/dinogame/DinoActivity.h new file mode 100644 index 0000000..abf437d --- /dev/null +++ b/plugins/dinogame/DinoActivity.h @@ -0,0 +1,80 @@ +#pragma once + +#include "activities/Activity.h" + +class DinoActivity final : public Activity { +public: + explicit DinoActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("Dino", renderer, mappedInput) {} + + void onEnter() override; + void onExit() override; + void loop() override; + bool skipLoopDelay() override { return true; } + void render(RenderLock&&) override; + +private: + enum class ObstacleType { CactusSmall, CactusBig }; + + struct Obstacle { + bool active = false; + ObstacleType type = ObstacleType::CactusSmall; + float x = 0; + }; + + static constexpr int BLOCK = 5; + static constexpr int GROUND_MARGIN = 60; + static constexpr int DINO_X = 36; + static constexpr float GRAVITY = 0.5f; + static constexpr float JUMP_VELOCITY = -16.0f; + static constexpr int MAX_OBSTACLES = 3; + static constexpr float START_SPEED = 4.2f; + static constexpr float MAX_SPEED = 11.0f; + static constexpr float SPEED_STEP = 0.35f; + static constexpr int SCORE_PER_SPEEDUP = 150; + static constexpr int MIN_GAP_TICKS = 100; + static constexpr int MAX_GAP_TICKS = 160; + static constexpr float BIRD_SPEED = 2.0f; + static constexpr int BIRD_TOP_MIN = 40; + static constexpr int BIRD_TOP_MAX = 140; + static constexpr unsigned long PHYSICS_INTERVAL_MS = 33; + static constexpr unsigned long ANIM_INTERVAL_MS = 130; + static constexpr unsigned long SCORE_INTERVAL_MS = 90; + + int screenW_ = 0; + int screenH_ = 0; + int groundY_ = 0; + + float dinoY_ = 0; + float velocityY_ = 0; + bool jumping_ = false; + + Obstacle obstacles_[MAX_OBSTACLES]; + float distanceSinceSpawn_ = 0; + float nextSpawnDistance_ = 0; + + float birdX_ = 0; + int birdY_ = 0; + + float gameSpeed_ = START_SPEED; + unsigned long score_ = 0; + + bool gameOver_ = false; + bool renderPending_ = false; + bool animFrame_ = false; + + unsigned long lastUpdate_ = 0; + unsigned long lastAnim_ = 0; + unsigned long lastScoreTick_ = 0; + + void resetGame(); + void updateGame(); + void spawnObstacle(); + bool checkCollision() const; + bool anyNonPowerPressed() const; + void drawDino(int x, int topY) const; + void drawObstacle(const Obstacle& ob) const; + void drawBird() const; + int obstacleWidthPx(ObstacleType type) const; + int obstacleHeightPx(ObstacleType type) const; +}; diff --git a/plugins/dinogame/plugin.py b/plugins/dinogame/plugin.py new file mode 100644 index 0000000..473958a --- /dev/null +++ b/plugins/dinogame/plugin.py @@ -0,0 +1,33 @@ +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +from framework.manifest import ( + PluginManifest, SourceFile, Include, SettingActionEnumValue, PluginsTabEntry, +) + + +def get_manifest(ctx): + return PluginManifest( + name="dinogame", + pretty_name="Dino Game", + source_files=[ + SourceFile("DinoActivity.h", "src/activities"), + SourceFile("DinoActivity.cpp", "src/activities"), + ], + includes=[ + Include("activities/DinoActivity.h", "settings_activity_cpp"), + ], + setting_actions=[ + SettingActionEnumValue("DinoGame"), + ], + plugins_tab_entries=[ + PluginsTabEntry( + label="Dino Game", + kind="action", + action_name="DinoGame", + action_value_text="Launch", + activity_launch_expr="std::make_unique(renderer, mappedInput)", + ), + ], + )