Compare commits

..
10 Commits
8 changed files with 1034 additions and 25 deletions

No files matched your search

+64 -17
View File
@@ -95,6 +95,40 @@ Requirements:
--- ---
### Pong Game
Adds a classic Pong game against a CPU opponent, launched from the Plugins settings tab.
- Left/Right (or Up/Down, Back/Confirm) move your paddle in full paddle-width steps
- CPU controls the top paddle and tracks the ball
- First to 7 points wins; any button exits the win/lose screen
- Power always exits the game
---
### Dino Game
Adds the classic offline dinosaur runner as a full-screen game, launched from the Plugins settings tab.
- Endless runner: jump the cacti as the game speeds up over time
- Any button except Power jumps; Power exits the game
- On Game Over, any button restarts and Power exits
- A bird flies across the top of the screen for atmosphere - it's decorative only, no need to dodge it
- Score is shown top-right and resets each session
---
### Snake Game
Adds a classic Snake game sized to your device's screen, launched from the Plugins settings tab.
- The 4 front buttons are fixed to Left / Right / Up / Down for this game, with on-screen labels above each button showing the mapping
- Eat food to grow and score points; speed increases as your score climbs
- Hitting a wall or yourself ends the game; any front button restarts and Power exits
- Filling the entire board is a win
---
## Requirements ## Requirements
- Python 3.10+ - Python 3.10+
@@ -112,7 +146,7 @@ From the root of this repository, run:
python3 install.py python3 install.py
To auto-accept all plugin prompts, pass --yes (or -y): To auto-accept all plugin prompts and skip the restore/backup and baud rate prompts, pass --yes (or -y):
python3 install.py --yes python3 install.py --yes
@@ -121,22 +155,35 @@ By default this uses the default build environment. To use a different environme
python3 install.py --environment slim python3 install.py --environment slim
python3 install.py --environment gh_release python3 install.py --environment gh_release
Flags can be combined:
python3 install.py -y -e gh_release
Environment | Description Environment | Description
------------|------------- ------------|-------------
default | Debug logging enabled, version from current git branch (recommended) default | Debug logging enabled, version from current git branch (recommended)
gh_release | Info logging only, version hardcoded to release tag gh_release | Info logging only, version hardcoded to release tag
slim | No serial logging, smallest binary size slim | No serial logging, smallest binary size
Before flashing you'll be asked to pick an upload baud rate:
Rate | Description
-----|------------
115200 | Recommended - most reliable, avoids "chip stopped responding" errors
921600 | Faster - for those feeling froggy
To skip the prompt, pass --baud (or -b):
python3 install.py --baud 115200
Flags can be combined:
python3 install.py -y -e gh_release -b 115200
The installer will: The installer will:
1. Clone the CrossPoint Reader source repository 1. Offer to restore from a previous backup, if one exists in the current directory
2. Prompt you to select which plugins to install and apply them as patches 2. Offer to back up your current firmware before making any changes
3. Build the firmware with PlatformIO 3. Clone the CrossPoint Reader source repository
4. Auto-detect your device's serial port and flash the firmware 4. Prompt you to select which plugins to install and apply them as patches
5. Build the firmware with PlatformIO
6. Auto-detect your device's serial port, ask for an upload baud rate, and flash the firmware
Note: This script modifies and flashes custom firmware to your device. The author accepts no responsibility for any damage that may occur to your device as a result of using this installer. Note: This script modifies and flashes custom firmware to your device. The author accepts no responsibility for any damage that may occur to your device as a result of using this installer.
@@ -174,17 +221,17 @@ xteink-plugins/
│ ├── plugin.py │ ├── plugin.py
│ ├── GitHubSync.h/.cpp │ ├── GitHubSync.h/.cpp
│ └── GitHubSyncSettingsActivity.h/.cpp │ └── GitHubSyncSettingsActivity.h/.cpp
└── pong/ ├── pong/
│ ├── plugin.py
│ └── PongActivity.h/.cpp
├── dinogame/
│ ├── plugin.py
│ └── DinoActivity.h/.cpp
└── snakegame/
├── plugin.py ├── plugin.py
└── PongActivity.h/.cpp └── SnakeActivity.h/.cpp
``` ```
### Why plugin.py instead of patch.py
Previously every plugin's `patch.py` directly rewrote CrossPoint's shared source files with hand-written string edits, and each one had to hardcode knowledge of every *other* plugin's edits to land in the right place (e.g. darkmode's patch literally contained smallerfonts' settings entry). That's fine as long as you always install every plugin together in the same order, but install any subset, or add a new plugin later, and those hardcoded assumptions stop holding - settings referenced fields that were never declared, and the build failed.
`plugin.py` instead returns a declarative `PluginManifest` (see `framework/manifest.py`) describing *what* the plugin needs - a settings field, a row in the on-device "Plugins" tab, a new `SettingAction`, a boot-time hook, and so on. A single engine (`framework/engine.py`) collects the manifests of only the plugins you actually selected and writes each shared file exactly once, built fresh from that selection. Installing 1 plugin or all 7, in any combination, produces internally consistent output every time.
## Troubleshooting ## Troubleshooting
### Linux: Permission denied when flashing ### Linux: Permission denied when flashing
+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.")
+397
View File
@@ -0,0 +1,397 @@
#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;
constexpr int FRONT_INSET = 10;
constexpr int OBSTACLE_INSET_X = 10;
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 - FRONT_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 + OBSTACLE_INSET_X;
int ox1 = (int)ob.x + ow - OBSTACLE_INSET_X;
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)",
),
],
)
+292
View File
@@ -0,0 +1,292 @@
#include "SnakeActivity.h"
#include <GfxRenderer.h>
#include "MappedInputManager.h"
#include "fontIds.h"
void SnakeActivity::onEnter() {
Activity::onEnter();
screenW_ = renderer.getScreenWidth();
screenH_ = renderer.getScreenHeight();
randomSeed(millis());
resetGame();
renderPending_ = true;
requestUpdate();
}
void SnakeActivity::onExit() {
Activity::onExit();
}
void SnakeActivity::resetGame() {
cols_ = screenW_ / BLOCK;
rows_ = (screenH_ - HEADER_H - FOOTER_H) / BLOCK;
if (cols_ < 5) cols_ = 5;
if (rows_ < 5) rows_ = 5;
if ((long)cols_ * (long)rows_ > MAX_SNAKE_LEN) {
rows_ = MAX_SNAKE_LEN / cols_;
}
fieldLeft_ = (screenW_ - cols_ * BLOCK) / 2;
if (fieldLeft_ < 0) fieldLeft_ = 0;
int freeVertical = screenH_ - HEADER_H - FOOTER_H - rows_ * BLOCK;
fieldTop_ = HEADER_H + (freeVertical > 0 ? freeVertical / 2 : 0);
int cx = cols_ / 2;
int cy = rows_ / 2;
headIdx_ = 0;
length_ = 3;
snakeX_[0] = cx; snakeY_[0] = cy;
snakeX_[1] = cx - 1; snakeY_[1] = cy;
snakeX_[2] = cx - 2; snakeY_[2] = cy;
dir_ = Dir::Right;
pendingDir_ = Dir::Right;
score_ = 0;
tickInterval_ = TICK_START_MS;
gameOver_ = false;
won_ = false;
lastMove_ = millis();
placeFood();
}
bool SnakeActivity::isSnakeCell(int x, int y) const {
for (int i = 0; i < length_; i++) {
int idx = (headIdx_ + i) % MAX_SNAKE_LEN;
if (snakeX_[idx] == x && snakeY_[idx] == y) return true;
}
return false;
}
void SnakeActivity::placeFood() {
if (length_ >= cols_ * rows_) return;
int minX = (cols_ > 2) ? 1 : 0;
int maxX = (cols_ > 2) ? cols_ - 2 : cols_ - 1;
int minY = (rows_ > 2) ? 1 : 0;
int maxY = (rows_ > 2) ? rows_ - 2 : rows_ - 1;
int fx, fy;
do {
fx = random(minX, maxX + 1);
fy = random(minY, maxY + 1);
} while (isSnakeCell(fx, fy));
foodX_ = fx;
foodY_ = fy;
}
void SnakeActivity::handleDirectionInput() {
const int btn = mappedInput.getPressedFrontButton();
switch (btn) {
case HalGPIO::BTN_BACK:
if (dir_ != Dir::Right) pendingDir_ = Dir::Left;
break;
case HalGPIO::BTN_CONFIRM:
if (dir_ != Dir::Left) pendingDir_ = Dir::Right;
break;
case HalGPIO::BTN_LEFT:
if (dir_ != Dir::Down) pendingDir_ = Dir::Up;
break;
case HalGPIO::BTN_RIGHT:
if (dir_ != Dir::Up) pendingDir_ = Dir::Down;
break;
default:
break;
}
}
void SnakeActivity::updateGame() {
dir_ = pendingDir_;
int dx = 0, dy = 0;
switch (dir_) {
case Dir::Up: dy = -1; break;
case Dir::Down: dy = 1; break;
case Dir::Left: dx = -1; break;
case Dir::Right: dx = 1; break;
}
int newX = snakeX_[headIdx_] + dx;
int newY = snakeY_[headIdx_] + dy;
if (newX < 0 || newX >= cols_ || newY < 0 || newY >= rows_) {
gameOver_ = true;
gameOverAt_ = millis();
return;
}
bool willEat = (newX == foodX_ && newY == foodY_);
for (int i = 0; i < length_; i++) {
if (!willEat && i == length_ - 1) continue;
int idx = (headIdx_ + i) % MAX_SNAKE_LEN;
if (snakeX_[idx] == newX && snakeY_[idx] == newY) {
gameOver_ = true;
gameOverAt_ = millis();
return;
}
}
headIdx_ = (headIdx_ - 1 + MAX_SNAKE_LEN) % MAX_SNAKE_LEN;
snakeX_[headIdx_] = newX;
snakeY_[headIdx_] = newY;
if (willEat) {
if (length_ < MAX_SNAKE_LEN) length_++;
score_ += POINTS_PER_FOOD;
if (length_ >= cols_ * rows_) {
gameOver_ = true;
won_ = true;
gameOverAt_ = millis();
return;
}
if (score_ % SPEEDUP_EVERY_POINTS == 0 && tickInterval_ > TICK_MIN_MS) {
tickInterval_ -= TICK_STEP_MS;
if (tickInterval_ < TICK_MIN_MS) tickInterval_ = TICK_MIN_MS;
}
placeFood();
}
}
void SnakeActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Power)) {
while (mappedInput.isPressed(MappedInputManager::Button::Power)) {
mappedInput.update();
delay(10);
}
finish();
return;
}
if (gameOver_) {
if (millis() - gameOverAt_ >= GAME_OVER_LOCKOUT_MS &&
mappedInput.getPressedFrontButton() != -1) {
resetGame();
renderPending_ = true;
requestUpdate();
}
return;
}
handleDirectionInput();
unsigned long now = millis();
if (now - lastMove_ >= tickInterval_) {
lastMove_ = now;
updateGame();
renderPending_ = true;
}
if (renderPending_) {
requestUpdate();
}
}
void SnakeActivity::drawArrow(Dir dir, int cx, int cy, int size) const {
int half = size / 2;
int xs[3];
int ys[3];
switch (dir) {
case Dir::Left:
xs[0] = cx - half; ys[0] = cy;
xs[1] = cx + half; ys[1] = cy - half;
xs[2] = cx + half; ys[2] = cy + half;
break;
case Dir::Right:
xs[0] = cx + half; ys[0] = cy;
xs[1] = cx - half; ys[1] = cy - half;
xs[2] = cx - half; ys[2] = cy + half;
break;
case Dir::Up:
xs[0] = cx; ys[0] = cy - half;
xs[1] = cx - half; ys[1] = cy + half;
xs[2] = cx + half; ys[2] = cy + half;
break;
case Dir::Down:
xs[0] = cx; ys[0] = cy + half;
xs[1] = cx - half; ys[1] = cy - half;
xs[2] = cx + half; ys[2] = cy - half;
break;
}
renderer.fillPolygon(xs, ys, 3, true);
}
void SnakeActivity::drawFooterLabels() const {
static const Dir dirs[4] = {Dir::Left, Dir::Right, Dir::Up, Dir::Down};
int footerTop = screenH_ - FOOTER_H;
renderer.drawLine(0, footerTop, screenW_, footerTop);
int colW = screenW_ / 4;
int cy = screenH_ - ARROW_BOTTOM_PAD - ARROW_SIZE / 2;
for (int i = 0; i < 4; i++) {
int colCenterX = i * colW + colW / 2;
drawArrow(dirs[i], colCenterX, cy, ARROW_SIZE);
if (i > 0) {
renderer.drawLine(i * colW, footerTop, i * colW, screenH_);
}
}
}
void SnakeActivity::render(RenderLock&&) {
renderPending_ = false;
renderer.clearScreen();
char scoreStr[24];
snprintf(scoreStr, sizeof(scoreStr), "Score: %d", score_);
renderer.drawText(UI_10_FONT_ID, 12, 10, scoreStr, true);
int fieldW = cols_ * BLOCK;
int fieldH = rows_ * BLOCK;
renderer.drawLine(fieldLeft_, fieldTop_, fieldLeft_ + fieldW, fieldTop_);
renderer.drawLine(fieldLeft_, fieldTop_ + fieldH, fieldLeft_ + fieldW, fieldTop_ + fieldH);
renderer.drawLine(fieldLeft_, fieldTop_, fieldLeft_, fieldTop_ + fieldH);
renderer.drawLine(fieldLeft_ + fieldW, fieldTop_, fieldLeft_ + fieldW, fieldTop_ + fieldH);
for (int i = 0; i < length_; i++) {
int idx = (headIdx_ + i) % MAX_SNAKE_LEN;
int x = fieldLeft_ + snakeX_[idx] * BLOCK;
int y = fieldTop_ + snakeY_[idx] * BLOCK;
renderer.fillRect(x + 1, y + 1, BLOCK - 2, BLOCK - 2, true);
if (i == 0) {
int innerSize = BLOCK / 3;
renderer.fillRect(x + (BLOCK - innerSize) / 2, y + (BLOCK - innerSize) / 2,
innerSize, innerSize, false);
}
}
int fx = fieldLeft_ + foodX_ * BLOCK;
int fy = fieldTop_ + foodY_ * BLOCK;
renderer.fillRect(fx + 1, fy + 1, BLOCK - 2, BLOCK - 2, true);
renderer.fillRect(fx + 4, fy + 4, BLOCK - 8, BLOCK - 8, false);
drawFooterLabels();
if (gameOver_) {
int baseY = fieldTop_ + fieldH / 2 - 30;
renderer.drawCenteredText(UI_12_FONT_ID, baseY, won_ ? "YOU WIN!" : "GAME OVER",
true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, baseY + 30, "Any button to Restart");
renderer.drawCenteredText(UI_10_FONT_ID, baseY + 52, "Power button to Exit");
}
renderer.displayBuffer();
}
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include "activities/Activity.h"
class SnakeActivity final : public Activity {
public:
explicit SnakeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("Snake", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
bool skipLoopDelay() override { return true; }
void render(RenderLock&&) override;
private:
enum class Dir { Up, Down, Left, Right };
static constexpr int BLOCK = 28;
static constexpr int HEADER_H = 34;
static constexpr int FOOTER_H = 38;
static constexpr int ARROW_SIZE = 16;
static constexpr int ARROW_BOTTOM_PAD = 14;
static constexpr int MAX_SNAKE_LEN = 2048;
static constexpr unsigned long TICK_START_MS = 320;
static constexpr unsigned long TICK_MIN_MS = 200;
static constexpr unsigned long TICK_STEP_MS = 6;
static constexpr int POINTS_PER_FOOD = 10;
static constexpr int SPEEDUP_EVERY_POINTS = 50;
static constexpr unsigned long GAME_OVER_LOCKOUT_MS = 3000;
int screenW_ = 0;
int screenH_ = 0;
int cols_ = 0;
int rows_ = 0;
int fieldLeft_ = 0;
int fieldTop_ = 0;
int snakeX_[MAX_SNAKE_LEN];
int snakeY_[MAX_SNAKE_LEN];
int headIdx_ = 0;
int length_ = 0;
Dir dir_ = Dir::Right;
Dir pendingDir_ = Dir::Right;
int foodX_ = 0;
int foodY_ = 0;
int score_ = 0;
unsigned long tickInterval_ = TICK_START_MS;
bool gameOver_ = false;
bool won_ = false;
bool renderPending_ = false;
unsigned long lastMove_ = 0;
unsigned long gameOverAt_ = 0;
void resetGame();
void placeFood();
bool isSnakeCell(int x, int y) const;
void handleDirectionInput();
void updateGame();
void drawFooterLabels() const;
void drawArrow(Dir dir, int cx, int cy, int size) 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="snakegame",
pretty_name="Snake Game",
source_files=[
SourceFile("SnakeActivity.h", "src/activities"),
SourceFile("SnakeActivity.cpp", "src/activities"),
],
includes=[
Include("activities/SnakeActivity.h", "settings_activity_cpp"),
],
setting_actions=[
SettingActionEnumValue("SnakeGame"),
],
plugins_tab_entries=[
PluginsTabEntry(
label="Snake Game",
kind="action",
action_name="SnakeGame",
action_value_text="Launch",
activity_launch_expr="std::make_unique<SnakeActivity>(renderer, mappedInput)",
),
],
)