Compare commits
10
Commits
f95ecfca17
...
4cddd1a34d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cddd1a34d | ||
|
|
a4daf0f299 | ||
|
|
03b7244fed | ||
|
|
4a7867f22e | ||
|
|
8ed0b4e28f | ||
|
|
df8aef978e | ||
|
|
c7c76e8875 | ||
|
|
59aebe7ff7 | ||
|
|
c2b7474697 | ||
|
|
60db39f9da |
No files matched your search
@@ -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
@@ -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.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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)",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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)",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
Reference in new issue
Block a user