Games App: Asteroids, and Pong by tilting
CI / build (pull_request) Successful in 3m2s
Site / build (pull_request) Successful in 12s

Asteroids is the sixth Game: lean the device to turn, ; for thrust, .
to fire, the arrows too for a device on a table. In Pong, 0 switches
between the keys and tilting and 9 turns the tilt round, both
remembered. Both read the accelerometer through the same smoothing, and
take however the device is held at the start as the middle.

Played on the device: the default way of Pong's tilt was swapped after
trying it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-15 12:06:29 +02:00
co-authored by Claude Opus 5.5
parent e1d1ec899e
commit 77260af157
10 changed files with 263 additions and 21 deletions
+1 -1
View File
@@ -187,7 +187,7 @@ A tag `v*` on the project's Gitea with a signed **Update File**, a factory image
_Avoid_: flash, upgrade (alone)
**Game**:
Something played inside the Games App: Pong, Snake, Tetris, Minesweeper or 2048. A Game is not an App: it has no tile in the Launcher, and is found again paused when the Games App is left in the middle of one.
Something played inside the Games App: Pong, Snake, Tetris, Minesweeper, 2048 or Asteroids. A Game is not an App: it has no tile in the Launcher, and is found again paused when the Games App is left in the middle of one.
_Avoid_: mini-app, toy
**Update File**:
+2 -2
View File
@@ -16,7 +16,7 @@ What it does today:
- **Shell:** the firmware's commands on the device itself, with completion, including `ping`, `nslookup`, `port`, `traceroute`, `tls` and `ifconfig`.
- **System:** load, tasks, memory, network, battery, live.
- **SSH:** a terminal on another machine, with a password or the device's own key.
- **Games:** Pong, Snake, Tetris, Minesweeper and 2048.
- **Games:** Pong, Snake, Tetris, Minesweeper, 2048 and Asteroids.
- **VPN:** a WireGuard tunnel.
- **Everywhere:** Fn+h lists the keys of the screen you are on; Fn+p takes a screenshot.
- **Updates:** signed, from the project's server, the card or a PC, with a rollback if the new firmware fails.
@@ -190,7 +190,7 @@ The terminal (`lib/term`, host-tested) understands what a shell, `less`, `top`,
## Games
The Games App (issue #101) holds five Games, shown as a row of icons like Apps in the Launcher. Two wait for a move: Minesweeper (20 x 9 with 28 mines, in cells of 12 pixels, laid at the first dig and never under or next to it; the fastest cleared field is kept) and 2048. Three run on a clock: Pong against the device (first to 7; the device's paddle has a top speed, so it can be beaten), Snake (38 x 19, the walls kill) and Tetris (10 x 20, the 7-bag, the next piece shown, a turn that shifts away from a wall, soft and hard drop). Each is a state machine in `lib/games`, host-tested: time and keys go in, the new state and what happened (a bounce, a line) come out, and a seed decides the dice, so a rule can be tested without playing.
The Games App (issue #101) holds six Games, shown as a row of icons like Apps in the Launcher. Two wait for a move: Minesweeper (20 x 9 with 28 mines, in cells of 12 pixels, laid at the first dig and never under or next to it; the fastest cleared field is kept) and 2048. Four run on a clock: Asteroids, steered by leaning the device (and Pong too, if `0` says so, tilted up and down) (the accelerometer, around the angle it was held at when the game began; `tiltTurn` in `lib/games` is host-tested), and Pong against the device (first to 7; the device's paddle has a top speed, so it can be beaten), Snake (38 x 19, the walls kill) and Tetris (10 x 20, the 7-bag, the next piece shown, a turn that shifts away from a wall, soft and hard drop). Each is a state machine in `lib/games`, host-tested: time and keys go in, the new state and what happened (a bounce, a line) come out, and a seed decides the dice, so a rule can be tested without playing.
Apps used to hear only key presses. `KeyMapper::held()` says which keys are down, for a paddle that moves while a key is held and for Tetris's repeat. An App that is in use with no key pressed says so (`App::keepsScreenOn`): the screen stays lit, and an update waiting for a quiet moment keeps waiting. A Game left in the middle is found again paused. The best of each Game (for Pong, the longest rally) is kept in internal flash. Tetris plays Korobeiniki, the folk tune it has always had, a note at a time on a speaker channel of its own (`lib/games/src/tune.h`, host-tested timing); `0` switches it, `8` and `9` set its loudness, both kept; it hurries with each level. The beeps and the tune obey Settings > This device > Sound & LED.
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

+13
View File
@@ -338,6 +338,8 @@ inline constexpr KeyHelp kGames[] = {
// games-pong: Games, Pong
inline constexpr KeyHelp kGamesPong[] = {
{"; .", "the paddle, while held"},
{"0", "steer with the keys, or by tilting"},
{"9", "tilting the other way up"},
{"Enter", "start, or start again"},
{"p", "pause; any key carries on"},
{"`", "the other games; this one waits"},
@@ -381,6 +383,17 @@ inline constexpr KeyHelp kGames2048[] = {
{"`", "the other games; this one waits"},
};
// games-asteroids: Games, Asteroids
inline constexpr KeyHelp kGamesAsteroids[] = {
{"lean", "turn: left or right"},
{", /", "turn, while held"},
{";", "thrust, while held"},
{".", "fire"},
{"Enter", "start, or start again"},
{"p", "pause; any key carries on"},
{"`", "the other games; this one waits"},
};
// ssh: SSH, the hosts
inline constexpr KeyHelp kSsh[] = {
{"Enter", "connect, open"},
+17 -4
View File
@@ -1,12 +1,12 @@
+++
title = "Games"
description = "Pong, Snake, Tetris, Minesweeper and 2048, for when the radio is quiet."
description = "Pong, Snake, Tetris, Minesweeper, 2048 and Asteroids, for when the radio is quiet."
weight = 17
[extra]
tag = "Games"
+++
The Games App opens on its five games, an icon each, as the Launcher shows Apps: <kbd>,</kbd> and <kbd>/</kbd> go from one to the next, with its name and your best so far under the row. <kbd>Enter</kbd> picks one, <kbd>Enter</kbd> again starts it.
The Games App opens on its six games, an icon each, as the Launcher shows Apps: <kbd>,</kbd> and <kbd>/</kbd> go from one to the next, with its name and your best so far under the row. <kbd>Enter</kbd> picks one, <kbd>Enter</kbd> again starts it.
In every game:
@@ -16,12 +16,14 @@ In every game:
| <kbd>p</kbd> | Pauses. Any key carries on |
| <kbd>`</kbd> | Back to the other games. This one waits, paused |
A game you leave in the middle, for another game or another App, is **found again paused**, and its icon has an orange dot. While Pong, Snake or Tetris is being played the screen does not dim. The games beep, and Tetris has its tune, unless **Settings → This device → Sound & LED** is off. There is no choice of difficulty: Pong, Snake and Tetris start gently and get faster, and the other two are always the same size.
A game you leave in the middle, for another game or another App, is **found again paused**, and its icon has an orange dot. While Pong, Snake, Tetris or Asteroids is being played the screen does not dim. The games beep, and Tetris has its tune, unless **Settings → This device → Sound & LED** is off. There is no choice of difficulty: the games on a clock start gently and get faster, and the other two are always the same size.
## Pong
You have the left paddle, the device the right one. Hold <kbd>;</kbd> to go up and <kbd>.</kbd> to go down. **First to 7 wins.**
Or steer by **tilting the device**: <kbd>0</kbd> switches between the keys and tilting, and the device remembers which. Tilted, the paddle rolls to the low side: lower the top edge and the paddle goes up, raise it and it goes down, about 15 degrees either way for the whole height. However you hold it when the game starts, or when you switch, is the middle. If that feels upside down, <kbd>9</kbd> turns it round (raise the top edge to go up), and that is remembered too. The keys still work while tilting. The top left corner says `keys`, `tilt` or `tilt, inverted`.
Where the ball meets your paddle decides how it leaves: off the middle it goes straight, off an end it goes steeply. Each return makes the ball a little faster. The device's paddle only moves so fast: a steep, fast ball gets past it.
What is kept is your **longest rally**: the most returns you made between two serves.
@@ -71,8 +73,19 @@ The arrows slide every tile on the 4 by 4 board as far as it goes. **Two tiles a
Reaching **2048** is said once, and the game goes on for as long as a move is left. There is nothing to pause: leave it and come back, and the board is as you left it.
## Asteroids
You steer the ship by **leaning the device**: lean it to the right and the ship turns clockwise, to the left and it turns the other way, faster the further you lean. However you hold it when the game starts counts as straight on, so there is no need to hold it flat. <kbd>,</kbd> and <kbd>/</kbd> held turn it too, for a device lying on a table.
| Key | Does |
|---|---|
| <kbd>;</kbd> | Thrust, while held. The ship keeps drifting when you let go, slowing a little |
| <kbd>.</kbd> | Fires. Four shots at most at a time, and they don't go far |
Everything wraps round: what leaves at one edge comes back at the other. A large rock breaks in two, each of those in two again, and the small ones are gone: 20, 50 and 100 points. Clear the field and the next wave has one rock more. You have three ships; a new one comes in the middle, blinking, and nothing can touch it for two seconds.
## The keys, as the device lists them
What <kbd>Fn</kbd> + <kbd>h</kbd> shows on these screens. These tables are generated from the firmware's own lists, so they are always the current ones.
{{ keys(scopes=["games", "games-pong", "games-snake", "games-tetris", "games-mines", "games-2048"]) }}
{{ keys(scopes=["games", "games-pong", "games-snake", "games-tetris", "games-mines", "games-2048", "games-asteroids"]) }}
+15
View File
@@ -398,6 +398,8 @@ id = "games-pong"
title = "Games, Pong"
rows = [
["; .", "the paddle, while held"],
["0", "steer with the keys, or by tilting"],
["9", "tilting the other way up"],
["Enter", "start, or start again"],
["p", "pause; any key carries on"],
["`", "the other games; this one waits"],
@@ -449,6 +451,19 @@ rows = [
["`", "the other games; this one waits"],
]
[[scope]]
id = "games-asteroids"
title = "Games, Asteroids"
rows = [
["lean", "turn: left or right"],
[", /", "turn, while held"],
[";", "thrust, while held"],
[".", "fire"],
["Enter", "start, or start again"],
["p", "pause; any key carries on"],
["`", "the other games; this one waits"],
]
[[scope]]
id = "ssh"
title = "SSH, the hosts"
+161 -9
View File
@@ -2,6 +2,7 @@
#include <Arduino.h>
#include <M5Cardputer.h>
#include <M5Unified.h>
#include <esp_random.h>
#include <algorithm>
@@ -21,8 +22,13 @@ namespace {
constexpr uint32_t kFrameMs = 33; // 30 pictures a second while a Game runs
constexpr uint32_t kTapMs = 70; // how long a tap moves Pong's paddle
constexpr int kCell = 6; // Snake's and Tetris's cells, in pixels
const char* const kNames[5] = {"Pong", "Snake", "Tetris", "Minesweeper", "2048"};
const char* const kBestKeys[5] = {"best_pong", "best_snake", "best_tetris", "best_mines", "best_2048"};
const char* const kNames[6] = {"Pong", "Snake", "Tetris", "Minesweeper", "2048", "Asteroids"};
const char* const kBestKeys[6] = {"best_pong", "best_snake", "best_tetris", "best_mines", "best_2048", "best_asteroids"};
// Which way a lean turns the ship. The sensor's x runs to the device's right (the Compass's level
// found that): leaning right brings it down, and that is a clockwise turn.
constexpr float kLeanSign = -1.0f;
constexpr float kLeanSeconds = 0.06f; // how fast the reading follows the hand
constexpr float kImuEvery = 0.01f; // seconds between two readings: the sensor gives 100 a second
constexpr int kMineCell = 12; // Minesweeper's cells, in pixels: a digit in each reads
constexpr uint32_t kRunAfterMs = 220, kRunEveryMs = 60; // a held arrow runs the cursor along
@@ -122,7 +128,10 @@ void GamesApp::onExit() { pause(); }
void GamesApp::begin() {
uint32_t seed = esp_random();
if (which_ == Pong) pong_.reset(new games::Pong(seed));
if (which_ == Pong) {
pong_.reset(new games::Pong(seed));
levelTilt();
}
else if (which_ == Snake) snake_.reset(new games::Snake(seed));
else if (which_ == Tetris) tetris_.reset(new games::Tetris(seed));
else if (which_ == Mines) {
@@ -131,6 +140,11 @@ void GamesApp::begin() {
cursorRow_ = games::Mines::kRows / 2;
runDir_ = -1;
} else if (which_ == Twenty48) twenty48_.reset(new games::Twenty48(seed));
else if (which_ == Asteroids) {
asteroids_.reset(new games::Asteroids(seed));
levelTilt();
fired_ = false;
}
state_[which_] = State::Playing;
tune_.restart();
newBest_ = false;
@@ -176,6 +190,17 @@ bool GamesApp::onKey(const KeyEvent& e) {
setVolume(volume() + (e.ch == '9' ? 1 : -1));
return true;
}
if (which_ == Pong && e.key == Key::Char && e.ch == '0') { // the keys, or tilting: at any moment
pongTilt_ = pongTilt() ? 0 : 1;
store_.putInt("pong_tilt", pongTilt_);
levelTilt(); // however it is held when switched is the middle
return true;
}
if (which_ == Pong && e.key == Key::Char && e.ch == '9') { // tilting the other way up
pongInverted_ = pongInverted() ? 0 : 1;
store_.putInt("pong_invert", pongInverted_);
return true;
}
if (e.key == Key::Back) { // to the other games; this one waits
pause();
which_ = Menu;
@@ -198,7 +223,7 @@ bool GamesApp::onKey(const KeyEvent& e) {
pause();
return true;
}
return which_ == Pong ? pongKey(e) : which_ == Snake ? snakeKey(e) : which_ == Tetris ? tetrisKey(e) : minesKey(e);
return which_ == Pong ? pongKey(e) : which_ == Snake ? snakeKey(e) : which_ == Tetris ? tetrisKey(e) : which_ == Asteroids ? asteroidsKey(e) : minesKey(e);
}
void GamesApp::moveCursor(int dc, int dr) {
@@ -226,6 +251,45 @@ bool GamesApp::minesKey(const KeyEvent& e) {
return true;
}
bool GamesApp::asteroidsKey(const KeyEvent& e) {
if (e.key == Key::Down) fired_ = true; // . fires; ; held is thrust, read in update()
return true;
}
void GamesApp::readTilt() {
if (!M5.Imu.update()) return;
auto data = M5.Imu.getImuData();
float g = std::sqrt(data.accel.x * data.accel.x + data.accel.y * data.accel.y + data.accel.z * data.accel.z);
if (g < 0.5f || g > 1.5f) return; // a shake, not a lean
float k = std::min(1.0f, kImuEvery / kLeanSeconds);
lean_ += k * (data.accel.x / g - lean_);
pitch_ += k * (data.accel.y / g - pitch_);
}
void GamesApp::levelTilt() {
for (int i = 0; i < 20 && !M5.Imu.update(); i++) delay(2); // a fresh reading
auto data = M5.Imu.getImuData();
float g = std::sqrt(data.accel.x * data.accel.x + data.accel.y * data.accel.y + data.accel.z * data.accel.z);
lean_ = level_ = g > 0.5f ? data.accel.x / g : 0;
pitch_ = pitchLevel_ = g > 0.5f ? data.accel.y / g : 0;
}
bool GamesApp::pongInverted() {
if (pongInverted_ < 0) {
int32_t stored = 0;
pongInverted_ = store_.getInt("pong_invert", stored) && stored == 1 ? 1 : 0;
}
return pongInverted_ == 1;
}
bool GamesApp::pongTilt() {
if (pongTilt_ < 0) {
int32_t stored = 0;
pongTilt_ = store_.getInt("pong_tilt", stored) && stored == 1 ? 1 : 0; // the keys, until switched
}
return pongTilt_ == 1;
}
bool GamesApp::twenty48Key(const KeyEvent& e) {
using Dir = games::Twenty48::Dir;
uint8_t events = 0;
@@ -285,8 +349,13 @@ void GamesApp::update(uint32_t nowMs) {
lastMs_ = nowMs;
if (which_ == Pong) {
int move = keys_.held(Key::Up) ? -1 : keys_.held(Key::Down) ? 1 : 0;
float move = keys_.held(Key::Up) ? -1 : keys_.held(Key::Down) ? 1 : 0;
if (!move && static_cast<int32_t>(tapUntilMs_ - nowMs) > 0) move = tapMove_;
if (pongTilt()) { // tilted: the paddle rolls to the low side, the keys still win
readTilt();
float pitch = pongInverted() ? 2 * pitchLevel_ - pitch_ : pitch_; // the other way up: mirrored round the middle
if (!move) move = games::moveToward(pong_->playerY(), games::tiltTarget(pitch, pitchLevel_));
}
uint8_t events = pong_->step(ms / 1000.0f, move);
if (events & Pong::Over) ended(pong_->longestRally());
else if (events & Pong::Point) beep(330, 120);
@@ -324,6 +393,21 @@ void GamesApp::update(uint32_t nowMs) {
return;
} else if (which_ == Twenty48) {
return; // it only moves when a key says so
} else if (which_ == Asteroids) {
games::AsteroidsInput in;
readTilt();
in.turn = kLeanSign * games::tiltTurn(lean_, level_);
if (keys_.held(Key::Left)) in.turn = -1; // the arrows too, for a device on a table
if (keys_.held(Key::Right)) in.turn = 1;
in.thrust = keys_.held(Key::Up);
in.fire = fired_;
fired_ = false;
uint8_t events = asteroids_->step(ms / 1000.0f, in);
if (events & games::Asteroids::Over) ended(asteroids_->score());
else if (events & games::Asteroids::Died) beep(150, 300);
else if (events & games::Asteroids::Wave) beep(990, 120);
else if (events & games::Asteroids::Hit) beep(300, 30);
else if (events & games::Asteroids::Shot) beep(1400, 8);
} else {
TetrisInput in = tapped_;
tapped_ = TetrisInput();
@@ -356,12 +440,13 @@ void GamesApp::help(std::vector<KeyHelp>& out) const {
case Tetris: return keys::add(out, keys::kGamesTetris);
case Mines: return keys::add(out, keys::kGamesMines);
case Twenty48: return keys::add(out, keys::kGames2048);
case Asteroids: return keys::add(out, keys::kGamesAsteroids);
default: return keys::add(out, keys::kGames);
}
}
const char* GamesApp::helpTitle() const {
static const char* const kTitles[kGames] = {"Games: Pong", "Games: Snake", "Games: Tetris", "Games: Minesweeper", "Games: 2048"};
static const char* const kTitles[kGames] = {"Games: Pong", "Games: Snake", "Games: Tetris", "Games: Minesweeper", "Games: 2048", "Games: Asteroids"};
return which_ == Menu ? nullptr : kTitles[which_];
}
@@ -387,13 +472,13 @@ void GamesApp::drawBanner(Canvas& c, const std::string& title, const std::string
// name under the row. A Game waiting paused has the Launcher's orange dot.
void GamesApp::drawMenu(Canvas& c) {
using namespace theme;
static const uint8_t* const kIcons[kGames] = {icons::pong, icons::snake, icons::tetris, icons::mines, icons::twenty48};
const int tileW = 46, tileH = 36, x0 = (kContent.w - kGames * tileW) / 2, y = kContent.y + 24; // five fit the width
static const uint8_t* const kIcons[kGames] = {icons::pong, icons::snake, icons::tetris, icons::mines, icons::twenty48, icons::asteroids};
const int tileW = 40, tileH = 36, x0 = (kContent.w - kGames * tileW) / 2, y = kContent.y + 24; // six fill the width
for (int i = 0; i < kGames; i++) {
int x = x0 + i * tileW, ix = x + (tileW - icons::kSize) / 2, iy = y + (tileH - icons::kSize) / 2;
bool chosen = i == chosen_;
if (chosen) {
int tx = ix - 6, tw = icons::kSize + 12;
int tw = std::min(icons::kSize + 12, tileW), tx = ix - (tw - icons::kSize) / 2;
c.fillRect(tx + 4, y, tw - 8, tileH, kAccent);
c.fillRect(tx + 2, y + 2, tw - 4, tileH - 4, kAccent);
c.fillRect(tx, y + 4, tw, tileH - 8, kAccent);
@@ -440,6 +525,9 @@ void GamesApp::drawPong(Canvas& c) {
const int b = static_cast<int>(Pong::kBall);
c.fillRect(static_cast<int>(pong_->ballX()) - b / 2, top + static_cast<int>(pong_->ballY()) - b / 2, b, b, theme::kText);
}
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
c.drawString(!pongTilt() ? "keys" : pongInverted() ? "tilt, inverted" : "tilt", 2, top + 2);
int b = best(Pong);
std::string bestLine = b > 0 ? "longest rally " + std::to_string(b) : "first to 7";
if (state == State::Ready) drawBanner(c, "Pong", bestLine);
@@ -630,6 +718,69 @@ void GamesApp::drawTwenty48(Canvas& c) {
if (state == State::Ready) drawBanner(c, "2048", best(GamesApp::Twenty48) > 0 ? "best " + std::to_string(best(GamesApp::Twenty48)) : "");
}
// Asteroids, in outlines: the rocks, the ship, the bullets, and a line of figures along the top.
void GamesApp::drawAsteroids(Canvas& c) {
using games::Asteroids;
const auto& area = theme::kContent;
State state = state_[GamesApp::Asteroids];
const int top = area.y;
if (asteroids_) {
for (auto& r : asteroids_->rocks()) {
float radius = r.radius();
// A rock across an edge is drawn on both sides of it, as the field wraps round.
for (float ox : {-Asteroids::kWidth, 0.0f, Asteroids::kWidth})
for (float oy : {-Asteroids::kHeight, 0.0f, Asteroids::kHeight}) {
float x = r.x + ox, y = r.y + oy;
if (x < -radius || x > Asteroids::kWidth + radius || y < -radius || y > Asteroids::kHeight + radius) continue;
int px[8], py[8];
for (int k = 0; k < 8; k++) {
float a = k * 0.785398f, d = radius * r.look[k] / 255.0f;
px[k] = static_cast<int>(std::lround(x + std::sin(a) * d));
py[k] = top + static_cast<int>(std::lround(y - std::cos(a) * d));
}
for (int k = 0; k < 8; k++) c.drawLine(px[k], py[k], px[(k + 1) % 8], py[(k + 1) % 8], theme::kText);
}
}
for (auto& b : asteroids_->bullets()) c.fillRect(static_cast<int>(b.x), top + static_cast<int>(b.y), 2, 2, theme::kYellow);
// The ship: a dart pointing where it goes. Shielded, it blinks.
if (asteroids_->shipShown() && (!asteroids_->shielded() || (millis() / 120) % 2)) {
float a = asteroids_->shipAngle(), s = std::sin(a), co = std::cos(a);
auto at = [&](float dx, float dy, int& x, int& y) {
x = static_cast<int>(std::lround(asteroids_->shipX() + dx * co - dy * s));
y = top + static_cast<int>(std::lround(asteroids_->shipY() + dx * s + dy * co));
};
int nx, ny, lx, ly, rx, ry, bx, by;
at(0, -6, nx, ny);
at(-4, 4, lx, ly);
at(4, 4, rx, ry);
at(0, 2, bx, by);
c.drawLine(nx, ny, lx, ly, theme::kAccent);
c.drawLine(nx, ny, rx, ry, theme::kAccent);
c.drawLine(lx, ly, bx, by, theme::kAccent);
c.drawLine(rx, ry, bx, by, theme::kAccent);
if (asteroids_->thrusting() && (millis() / 60) % 2) {
int fx, fy;
at(0, 7, fx, fy);
c.drawLine(bx, by, fx, fy, theme::kWarning);
}
}
}
c.setFont(&fonts::small);
c.setTextColor(theme::kMuted);
char line[32];
std::snprintf(line, sizeof line, "%d", asteroids_ ? asteroids_->score() : 0);
c.drawString(line, 2, top + 1);
c.setTextDatum(top_right);
std::snprintf(line, sizeof line, "wave %d ships %d", asteroids_ ? asteroids_->wave() : 1, asteroids_ ? asteroids_->lives() : Asteroids::kLives);
c.drawString(line, area.w - 2, top + 1);
c.setTextDatum(top_left);
int b = best(GamesApp::Asteroids);
if (state == State::Ready) drawBanner(c, "Asteroids", b > 0 ? "best " + std::to_string(b) : "lean to turn");
else if (state == State::Paused) drawBanner(c, "Paused", "");
else if (state == State::Over)
drawBanner(c, "Game over", newBest_ ? "a new best: " + std::to_string(asteroids_->score()) : std::to_string(asteroids_->score()) + ", best " + std::to_string(b));
}
void GamesApp::draw(Canvas& c) {
c.setTextDatum(top_left);
lastDrawMs_ = millis();
@@ -639,6 +790,7 @@ void GamesApp::draw(Canvas& c) {
case Tetris: drawTetris(c); break;
case Mines: drawMines(c); break;
case Twenty48: drawTwenty48(c); break;
case Asteroids: drawAsteroids(c); break;
default: drawMenu(c); break;
}
}
+18 -5
View File
@@ -5,6 +5,7 @@
#include "app.h"
#include "key_mapper.h"
#include "asteroids.h"
#include "key_value_store.h"
#include "mines.h"
#include "pong.h"
@@ -32,11 +33,11 @@ class GamesApp : public App {
const char* helpTitle() const override;
// A Game being played is the device in use, key or no key: the screen stays lit.
// Not for the two that wait for a move: there a key is pressed, or nobody is playing.
bool keepsScreenOn() const override { return which_ <= Tetris && state_[which_] == State::Playing; }
bool keepsScreenOn() const override { return which_ != Menu && which_ != Mines && which_ != Twenty48 && state_[which_] == State::Playing; }
private:
enum Which { Pong, Snake, Tetris, Mines, Twenty48, Menu }; // all but the last index the arrays
static constexpr int kGames = 5;
enum Which { Pong, Snake, Tetris, Mines, Twenty48, Asteroids, Menu }; // all but the last index the arrays
static constexpr int kGames = 6;
enum class State : uint8_t { Ready, Playing, Paused, Over };
void begin(); // a new game of the one chosen
@@ -54,6 +55,12 @@ class GamesApp : public App {
bool tetrisKey(const KeyEvent& e);
bool minesKey(const KeyEvent& e);
bool twenty48Key(const KeyEvent& e);
bool asteroidsKey(const KeyEvent& e);
void readTilt(); // the device's lean, both ways, from the accelerometer, smoothed
void levelTilt(); // however it is held now is "straight on"
bool pongTilt(); // Pong steered by tilting (0 switches, remembered)
bool pongInverted(); // and the other way up (9 switches, remembered)
void drawAsteroids(Canvas& c);
void moveCursor(int dc, int dr);
void drawMines(Canvas& c);
void drawTwenty48(Canvas& c);
@@ -68,14 +75,20 @@ class GamesApp : public App {
KeyValueStore& store_;
int chosen_ = 0; // in the row of games
Which which_ = Menu;
State state_[kGames] = {State::Ready, State::Ready, State::Ready, State::Ready, State::Ready};
int best_[kGames] = {-1, -1, -1, -1, -1}; // -1: not read from the store yet
State state_[kGames] = {State::Ready, State::Ready, State::Ready, State::Ready, State::Ready, State::Ready};
int best_[kGames] = {-1, -1, -1, -1, -1, -1}; // -1: not read from the store yet
bool newBest_ = false;
std::unique_ptr<games::Pong> pong_;
std::unique_ptr<games::Snake> snake_;
std::unique_ptr<games::Tetris> tetris_;
std::unique_ptr<games::Mines> mines_;
std::unique_ptr<games::Twenty48> twenty48_;
std::unique_ptr<games::Asteroids> asteroids_;
// Asteroids is steered by leaning the device left and right, Pong if asked by tilting it up and
// down: "level" is how it was held when the game began.
float lean_ = 0, level_ = 0, pitch_ = 0, pitchLevel_ = 0;
int pongTilt_ = -1, pongInverted_ = -1; // -1: not read from the store yet
bool fired_ = false;
// Minesweeper's cursor, and the arrow held to run it along.
int cursorCol_ = 0, cursorRow_ = 0, runDir_ = -1;
uint32_t runMs_ = 0, shownSeconds_ = 0;
+35
View File
@@ -3,6 +3,41 @@
namespace roro::icons {
const uint8_t asteroids[128] = {
0x00, 0x00, 0x00, 0x00, // ................................
0x00, 0x00, 0x00, 0x00, // ................................
0x00, 0x00, 0x3f, 0xc0, // ..................########......
0x00, 0x00, 0x3f, 0xc0, // ..................########......
0x00, 0x00, 0xc0, 0x30, // ................##........##....
0x00, 0x00, 0xc0, 0x30, // ................##........##....
0x00, 0x03, 0x00, 0x0c, // ..............##............##..
0x00, 0x03, 0x00, 0x0c, // ..............##............##..
0x00, 0x03, 0x00, 0x30, // ..............##..........##....
0x00, 0x03, 0x00, 0x30, // ..............##..........##....
0x00, 0x00, 0xc0, 0x0c, // ................##..........##..
0x00, 0x00, 0xc0, 0x0c, // ................##..........##..
0x00, 0x00, 0x3f, 0xf0, // ..................##########....
0x00, 0x00, 0x3f, 0xf0, // ..................##########....
0x00, 0x00, 0x00, 0x00, // ................................
0x00, 0x00, 0x00, 0x00, // ................................
0x03, 0x00, 0x00, 0x00, // ......##........................
0x03, 0x00, 0x00, 0x00, // ......##........................
0x03, 0x00, 0x03, 0x00, // ......##..............##........
0x03, 0x00, 0x03, 0x00, // ......##..............##........
0x0c, 0xc0, 0x00, 0x00, // ....##..##......................
0x0c, 0xc0, 0x00, 0x00, // ....##..##......................
0x0c, 0xc0, 0x0c, 0x00, // ....##..##..........##..........
0x0c, 0xc0, 0x0c, 0x00, // ....##..##..........##..........
0x30, 0x30, 0x00, 0x00, // ..##......##....................
0x30, 0x30, 0x00, 0x00, // ..##......##....................
0x33, 0x30, 0x00, 0x00, // ..##..##..##....................
0x33, 0x30, 0x00, 0x00, // ..##..##..##....................
0x3c, 0xf0, 0x00, 0x00, // ..####..####....................
0x3c, 0xf0, 0x00, 0x00, // ..####..####....................
0x00, 0x00, 0x00, 0x00, // ................................
0x00, 0x00, 0x00, 0x00, // ................................
};
const uint8_t games[128] = {
0x00, 0x00, 0x00, 0x00, // ................................
0x00, 0x00, 0x00, 0x00, // ................................
+1
View File
@@ -8,6 +8,7 @@ namespace roro::icons {
// One bit a pixel, the leftmost first, a row after another: what Canvas::drawBitmap takes.
constexpr int kSize = 32;
extern const uint8_t asteroids[128]; // asteroids.png
extern const uint8_t games[128]; // games.png
extern const uint8_t gemini[128]; // gemini.png
extern const uint8_t gnss[128]; // gnss.png