Add Dino Game plugin

This commit is contained in:
Sterling Archer
2026-07-16 21:29:16 -07:00
parent 59aebe7ff7
commit c7c76e8875
4 changed files with 576 additions and 8 deletions
+68 -8
View File
@@ -22,6 +22,12 @@ ENVIRONMENTS = {
"slim": "No serial logging, smallest binary size", "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): def run(cmd, cwd=None, check=True):
print(f" $ {cmd}") print(f" $ {cmd}")
@@ -70,7 +76,7 @@ def find_backup_files():
return sorted(glob.glob("backup.*.bin"), reverse=True) 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 """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 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 install. This always runs before backup_device() and, if the user goes
@@ -80,6 +86,10 @@ def restore_from_backup():
if not backups: if not backups:
return 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 ''}:") print(f" Found {len(backups)} existing backup{'s' if len(backups) != 1 else ''}:")
for i, b in enumerate(backups, 1): for i, b in enumerate(backups, 1):
tag = " ← most recent" if i == 1 else "" tag = " ← most recent" if i == 1 else ""
@@ -152,7 +162,11 @@ def restore_from_backup():
sys.exit("Aborted.") 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) print(" Backup your current firmware before flashing? [Y/n]: ", end="", flush=True)
answer = input().strip().lower() answer = input().strip().lower()
if answer not in ("", "y", "yes"): if answer not in ("", "y", "yes"):
@@ -423,13 +437,47 @@ def prompt_for_upload_port():
print(" Please enter a valid port name.") 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})...") print(f"\nBuilding firmware with PlatformIO (environment: {environment})...")
run(f"pio run --environment {environment}", cwd=REPO_DIR) run(f"pio run --environment {environment}", cwd=REPO_DIR)
port = prompt_for_upload_port() 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( run(
f"pio run --target upload --environment {environment} --upload-port {port}", f"pio run --target upload --environment {environment} --upload-port {port}",
cwd=REPO_DIR, cwd=REPO_DIR,
@@ -452,7 +500,18 @@ def parse_args():
parser.add_argument( parser.add_argument(
"-y", "--yes", "-y", "--yes",
action="store_true", 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( parser.add_argument(
"--no-reclone", "--no-reclone",
@@ -483,13 +542,14 @@ def main():
print(" Aborted.") print(" Aborted.")
sys.exit(0) sys.exit(0)
print() print()
restore_from_backup() restore_from_backup(yes_all=args.yes)
backup_device() backup_device(yes_all=args.yes)
clone_repo(force=not args.no_reclone) clone_repo(force=not args.no_reclone)
clear_plugin_caches() clear_plugin_caches()
chosen_plugins = select_plugins(yes_all=args.yes) chosen_plugins = select_plugins(yes_all=args.yes)
apply_plugins(chosen_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.") print("\nDone.")
+395
View File
@@ -0,0 +1,395 @@
#include "DinoActivity.h"
#include <GfxRenderer.h>
#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 <size_t N>
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();
}
+80
View File
@@ -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;
};
+33
View File
@@ -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<DinoActivity>(renderer, mappedInput)",
),
],
)