Public Access
Games App: Pong, Snake and Tetris
A new App (issue #101) that shows its three Games as icons, as the Launcher shows Apps, and plays the one chosen. A Game left in the middle is found again paused; one being played keeps the screen lit. Tetris plays Korobeiniki on a speaker channel of its own, faster with each level: 0 switches it, 8 and 9 set its volume, both remembered. The drop is on 1, away from the arrows. The best of each Game is kept. Played on the device. The speeds are as tuned there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
@@ -186,6 +186,10 @@ Installing a new firmware image without a USB cable: pushed over Wi-Fi from the
|
||||
A tag `v*` on the project's Gitea with a signed **Update File**, a factory image for USB, the ELF to decode crashes and checksums, built and published by CI. The device reads them to look for updates.
|
||||
_Avoid_: flash, upgrade (alone)
|
||||
|
||||
**Game**:
|
||||
Something played inside the Games App: Pong, Snake or Tetris. 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**:
|
||||
One signed file (`.ota`) that carries a firmware version, its image and a signature. The same file works over Wi-Fi and from the SD card. Anything not signed with the project's key is refused.
|
||||
_Avoid_: binary, bin
|
||||
|
||||
@@ -16,6 +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 and Tetris.
|
||||
- **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.
|
||||
@@ -187,6 +188,12 @@ The SSH App (docs/milestones/N1.md, issue #2) is a terminal on another machine:
|
||||
|
||||
The terminal (`lib/term`, host-tested) understands what a shell, `less`, `top`, `nano` and `vim` send: the cursor, erasing, sixteen colours, scroll regions, the alternate screen; no mouse. Five text sizes with Ctrl and + or -, from 60 x 20 to 26 x 8, told to the far end; 100 lines of scrollback with Alt and up or down. The backtick key sends Esc. **Leaving the App doesn't end the session:** `SSH` shows in the Status Bar and the App finds it again. It costs 292 KB of flash (109 KB of it one table, for signing with the device's key) and about 50 KB of memory while a session is open, so it isn't started under 75 KB free and IRC doesn't connect while one is open.
|
||||
|
||||
## Games
|
||||
|
||||
The Games App (issue #101) holds three Games, shown as a row of icons like Apps in the Launcher: 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.
|
||||
|
||||
## VPN
|
||||
|
||||
A WireGuard tunnel (docs/milestones/N1.md), over whatever Wi-Fi the device is on: one peer, IPv4. Copy a client's `.conf` to the card as `/vpn/wg0.conf` and import it in Settings > Network > VPN (or `vpn import`); the configuration, private key included, is then kept in the device and never shown, and Settings offers to delete the file. A switch brings the tunnel up until the next restart; "Start with Wi-Fi" does it every time. It waits for the clock, which WireGuard needs. `VPN` shows in the Status Bar, bright once the server has answered.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 124 B |
Binary file not shown.
|
After Width: | Height: | Size: 112 B |
Binary file not shown.
|
After Width: | Height: | Size: 115 B |
Binary file not shown.
|
After Width: | Height: | Size: 99 B |
@@ -50,6 +50,10 @@ class App {
|
||||
virtual bool retainsContent() const { return false; }
|
||||
virtual void contentLost() {}
|
||||
|
||||
// True while the App is in use with no key being pressed (a Game being played, issue #101):
|
||||
// it counts as activity, so the screen neither dims nor goes off.
|
||||
virtual bool keepsScreenOn() const { return false; }
|
||||
|
||||
// True while something goes on in the App that is worth a look, whether it is in front or
|
||||
// not (unread messages, a recording running): the Launcher marks its tile (issue #9).
|
||||
virtual bool badge() const { return false; }
|
||||
|
||||
@@ -329,6 +329,42 @@ inline constexpr KeyHelp kVpn[] = {
|
||||
{"; .", "up, down"},
|
||||
};
|
||||
|
||||
// games: Games, the three games
|
||||
inline constexpr KeyHelp kGames[] = {
|
||||
{"Enter", "this game"},
|
||||
{", /", "the game before, the next"},
|
||||
};
|
||||
|
||||
// games-pong: Games, Pong
|
||||
inline constexpr KeyHelp kGamesPong[] = {
|
||||
{"; .", "the paddle, while held"},
|
||||
{"Enter", "start, or start again"},
|
||||
{"p", "pause; any key carries on"},
|
||||
{"`", "the other games; this one waits"},
|
||||
};
|
||||
|
||||
// games-snake: Games, Snake
|
||||
inline constexpr KeyHelp kGamesSnake[] = {
|
||||
{"; . , /", "turn"},
|
||||
{"Enter", "start, or start again"},
|
||||
{"p", "pause; any key carries on"},
|
||||
{"`", "the other games; this one waits"},
|
||||
};
|
||||
|
||||
// games-tetris: Games, Tetris
|
||||
inline constexpr KeyHelp kGamesTetris[] = {
|
||||
{", /", "left, right; held, it repeats"},
|
||||
{";", "turn"},
|
||||
{"z", "turn the other way"},
|
||||
{".", "fall faster, while held"},
|
||||
{"1", "drop"},
|
||||
{"0", "the music, on or off"},
|
||||
{"8 9", "the music, quieter, louder"},
|
||||
{"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"},
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
+++
|
||||
title = "Games"
|
||||
description = "Pong, Snake and Tetris, for when the radio is quiet."
|
||||
weight = 17
|
||||
[extra]
|
||||
tag = "Games"
|
||||
+++
|
||||
|
||||
The Games App opens on its three 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:
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| <kbd>Enter</kbd> | Starts, or starts again when a game is over |
|
||||
| <kbd>p</kbd> | Pauses. Any key carries on |
|
||||
| <kbd>`</kbd> | Back to the three 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 a game 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: each game starts gently and gets faster.
|
||||
|
||||
## 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.**
|
||||
|
||||
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.
|
||||
|
||||
## Snake
|
||||
|
||||
The arrows (<kbd>;</kbd> <kbd>.</kbd> <kbd>,</kbd> <kbd>/</kbd>) turn the snake. It grows by one with each thing it eats, and goes a little faster every third. **The walls kill it**, and so does its own body.
|
||||
|
||||
Two turns pressed quickly one after the other are both taken, one a step: that is how to turn round in two rows.
|
||||
|
||||
## Tetris
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| <kbd>,</kbd> <kbd>/</kbd> | Left and right. Held, it repeats |
|
||||
| <kbd>;</kbd> | Turns the piece. <kbd>z</kbd> turns it the other way |
|
||||
| <kbd>.</kbd> | Falls faster, while held |
|
||||
| <kbd>1</kbd> | Drops it at once. It is far from the arrows on purpose: a drop cannot be taken back |
|
||||
| <kbd>0</kbd> | The music, on or off. The device remembers it |
|
||||
| <kbd>8</kbd> <kbd>9</kbd> | The music, quieter and louder, in eight steps. Remembered too |
|
||||
|
||||
The well is 10 wide and 20 high. The pieces come from a bag: each of the seven once, in a shuffled order, then seven again, so the long one is never more than twelve pieces away. The **next piece** is shown on the right. A piece turned against a wall shifts a step away from it.
|
||||
|
||||
It plays to **Korobeiniki**, the tune Tetris has always had, round and round, and **faster with each level**, as the fall is; it stops when the game is paused or over. With Sound & LED off in the Settings there is no music either.
|
||||
|
||||
A full row goes: 100 points for one, 300 for two, 500 for three, **800 for four at once**, times the level. Falling faster gives a point a row, dropping two. The key that brought a piece down does not send the next one down too: let it go and press again. Every ten rows is a level, and the fall quickens.
|
||||
|
||||
## 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"]) }}
|
||||
@@ -385,6 +385,50 @@ rows = [
|
||||
["; .", "up, down"],
|
||||
]
|
||||
|
||||
[[scope]]
|
||||
id = "games"
|
||||
title = "Games, the three games"
|
||||
rows = [
|
||||
["Enter", "this game"],
|
||||
[", /", "the game before, the next"],
|
||||
]
|
||||
|
||||
[[scope]]
|
||||
id = "games-pong"
|
||||
title = "Games, Pong"
|
||||
rows = [
|
||||
["; .", "the paddle, while held"],
|
||||
["Enter", "start, or start again"],
|
||||
["p", "pause; any key carries on"],
|
||||
["`", "the other games; this one waits"],
|
||||
]
|
||||
|
||||
[[scope]]
|
||||
id = "games-snake"
|
||||
title = "Games, Snake"
|
||||
rows = [
|
||||
["; . , /", "turn"],
|
||||
["Enter", "start, or start again"],
|
||||
["p", "pause; any key carries on"],
|
||||
["`", "the other games; this one waits"],
|
||||
]
|
||||
|
||||
[[scope]]
|
||||
id = "games-tetris"
|
||||
title = "Games, Tetris"
|
||||
rows = [
|
||||
[", /", "left, right; held, it repeats"],
|
||||
[";", "turn"],
|
||||
["z", "turn the other way"],
|
||||
[".", "fall faster, while held"],
|
||||
["1", "drop"],
|
||||
["0", "the music, on or off"],
|
||||
["8 9", "the music, quieter, louder"],
|
||||
["Enter", "start, or start again"],
|
||||
["p", "pause; any key carries on"],
|
||||
["`", "the other games; this one waits"],
|
||||
]
|
||||
|
||||
[[scope]]
|
||||
id = "ssh"
|
||||
title = "SSH, the hosts"
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
#include "apps/games_app.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <M5Cardputer.h>
|
||||
#include <esp_random.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
#include "app_keys.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/icons.h"
|
||||
#include "ui/theme.h"
|
||||
#include "ui/widgets.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
using namespace games;
|
||||
|
||||
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[3] = {"Pong", "Snake", "Tetris"};
|
||||
const char* const kBestKeys[3] = {"best_pong", "best_snake", "best_tetris"};
|
||||
|
||||
// The seven pieces in the theme's seven accents: they follow the theme chosen.
|
||||
uint16_t pieceColor(uint8_t kind) {
|
||||
switch (kind) {
|
||||
case 1: return theme::kAccent;
|
||||
case 2: return theme::kYellow;
|
||||
case 3: return theme::kViolet;
|
||||
case 4: return theme::kGood;
|
||||
case 5: return theme::kRed;
|
||||
case 6: return theme::kMessage;
|
||||
default: return theme::kWarning;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The speaker mixes its channels: the tune has one, the beeps another, so a line cleared does
|
||||
// not cut a note short.
|
||||
constexpr int kTuneChannel = 0, kBeepChannel = 1;
|
||||
constexpr uint32_t kBreathMs = 25; // of silence at the end of each note, so two alike are two
|
||||
|
||||
void GamesApp::beep(int hz, int ms) {
|
||||
if (settings_.getBool(Setting::Sound)) M5Cardputer.Speaker.tone(hz, ms, kBeepChannel, true);
|
||||
}
|
||||
|
||||
bool GamesApp::musicOn() {
|
||||
if (music_ < 0) {
|
||||
int32_t stored = 1;
|
||||
music_ = store_.getInt("tetris_music", stored) ? (stored != 0) : 1; // on, until switched off
|
||||
}
|
||||
return music_ == 1;
|
||||
}
|
||||
|
||||
// The tune's loudness, in eight steps. The speaker is set for short beeps, at a quarter of what
|
||||
// it can do: for the tune it is turned up all the way, and each channel's own volume does the
|
||||
// rest, the tune's by the step chosen, the others' a quarter so a beep stays as loud as it was.
|
||||
constexpr int kLoudest = 8, kUsualVolume = 4;
|
||||
// The top is four fifths of all the speaker can do: above that the small speaker distorts.
|
||||
constexpr uint8_t kStepVolume[kLoudest] = {5, 10, 16, 26, 45, 77, 128, 204}; // 64 is the speaker's usual
|
||||
|
||||
int GamesApp::volume() {
|
||||
if (volume_ < 0) {
|
||||
int32_t stored = kUsualVolume;
|
||||
volume_ = store_.getInt("tetris_volume", stored) && stored >= 1 && stored <= kLoudest ? stored : kUsualVolume;
|
||||
}
|
||||
return volume_;
|
||||
}
|
||||
|
||||
void GamesApp::setVolume(int level) {
|
||||
level = std::clamp(level, 1, kLoudest);
|
||||
if (level == volume()) return;
|
||||
volume_ = level;
|
||||
store_.putInt("tetris_volume", level);
|
||||
if (masterWas_ >= 0) M5Cardputer.Speaker.setChannelVolume(kTuneChannel, kStepVolume[level - 1]);
|
||||
}
|
||||
|
||||
void GamesApp::loud(bool on) {
|
||||
auto& speaker = M5Cardputer.Speaker;
|
||||
if (on == (masterWas_ >= 0)) return;
|
||||
if (on) {
|
||||
masterWas_ = speaker.getVolume();
|
||||
speaker.setAllChannelVolume(static_cast<uint8_t>(masterWas_)); // times a master of 255: as before
|
||||
speaker.setChannelVolume(kTuneChannel, kStepVolume[volume() - 1]);
|
||||
speaker.setVolume(255);
|
||||
} else {
|
||||
speaker.setVolume(static_cast<uint8_t>(masterWas_));
|
||||
speaker.setAllChannelVolume(255);
|
||||
masterWas_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void GamesApp::quiet() {
|
||||
M5Cardputer.Speaker.stop(kTuneChannel);
|
||||
loud(false);
|
||||
}
|
||||
|
||||
int GamesApp::best(Which w) {
|
||||
if (best_[w] < 0) {
|
||||
int32_t stored = 0;
|
||||
best_[w] = store_.getInt(kBestKeys[w], stored) && stored > 0 ? stored : 0;
|
||||
}
|
||||
return best_[w];
|
||||
}
|
||||
|
||||
void GamesApp::onEnter() {
|
||||
lastMs_ = millis();
|
||||
requestRedraw();
|
||||
}
|
||||
|
||||
void GamesApp::pause() {
|
||||
if (which_ != Menu && state_[which_] == State::Playing) state_[which_] = State::Paused;
|
||||
quiet();
|
||||
}
|
||||
|
||||
void GamesApp::onExit() { pause(); }
|
||||
|
||||
void GamesApp::begin() {
|
||||
uint32_t seed = esp_random();
|
||||
if (which_ == Pong) pong_.reset(new games::Pong(seed));
|
||||
else if (which_ == Snake) snake_.reset(new games::Snake(seed));
|
||||
else if (which_ == Tetris) tetris_.reset(new games::Tetris(seed));
|
||||
state_[which_] = State::Playing;
|
||||
tune_.restart();
|
||||
newBest_ = false;
|
||||
tapped_ = TetrisInput();
|
||||
tapMove_ = 0;
|
||||
lastMs_ = millis();
|
||||
}
|
||||
|
||||
void GamesApp::ended(int score) {
|
||||
state_[which_] = State::Over;
|
||||
quiet();
|
||||
newBest_ = score > best(which_);
|
||||
if (newBest_) {
|
||||
best_[which_] = score;
|
||||
store_.putInt(kBestKeys[which_], score);
|
||||
}
|
||||
beep(newBest_ ? 1320 : 220, 250);
|
||||
}
|
||||
|
||||
bool GamesApp::onKey(const KeyEvent& e) {
|
||||
requestRedraw();
|
||||
if (which_ == Menu) {
|
||||
switch (e.key) {
|
||||
case Key::Left: chosen_ = (chosen_ + 2) % 3; return true;
|
||||
case Key::Right: chosen_ = (chosen_ + 1) % 3; return true;
|
||||
case Key::Select:
|
||||
which_ = static_cast<Which>(chosen_);
|
||||
lastMs_ = millis();
|
||||
return true;
|
||||
default: return false; // Back leaves the App
|
||||
}
|
||||
}
|
||||
State& state = state_[which_];
|
||||
if (which_ == Tetris && e.key == Key::Char && e.ch == '0') { // the tune, on or off, whatever the game is doing
|
||||
music_ = musicOn() ? 0 : 1;
|
||||
store_.putInt("tetris_music", music_);
|
||||
if (!music_) quiet();
|
||||
return true;
|
||||
}
|
||||
if (which_ == Tetris && e.key == Key::Char && (e.ch == '8' || e.ch == '9')) { // quieter, louder
|
||||
setVolume(volume() + (e.ch == '9' ? 1 : -1));
|
||||
return true;
|
||||
}
|
||||
if (e.key == Key::Back) { // to the list; the Game waits, paused
|
||||
pause();
|
||||
which_ = Menu;
|
||||
return true;
|
||||
}
|
||||
if (state == State::Ready || state == State::Over) {
|
||||
if (e.key == Key::Select) begin();
|
||||
return true;
|
||||
}
|
||||
if (state == State::Paused) { // any key carries on
|
||||
state = State::Playing;
|
||||
lastMs_ = millis();
|
||||
return true;
|
||||
}
|
||||
if (e.key == Key::Char && (e.ch == 'p' || e.ch == 'P')) {
|
||||
pause();
|
||||
return true;
|
||||
}
|
||||
return which_ == Pong ? pongKey(e) : which_ == Snake ? snakeKey(e) : tetrisKey(e);
|
||||
}
|
||||
|
||||
bool GamesApp::pongKey(const KeyEvent& e) {
|
||||
if (e.key != Key::Up && e.key != Key::Down) return true;
|
||||
tapMove_ = e.key == Key::Up ? -1 : 1;
|
||||
tapUntilMs_ = millis() + kTapMs;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GamesApp::snakeKey(const KeyEvent& e) {
|
||||
switch (e.key) {
|
||||
case Key::Up: snake_->turn(Snake::Dir::Up); break;
|
||||
case Key::Down: snake_->turn(Snake::Dir::Down); break;
|
||||
case Key::Left: snake_->turn(Snake::Dir::Left); break;
|
||||
case Key::Right: snake_->turn(Snake::Dir::Right); break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GamesApp::tetrisKey(const KeyEvent& e) {
|
||||
switch (e.key) {
|
||||
case Key::Up: tapped_.rotate = true; break;
|
||||
case Key::Left: tapped_.left = true; break;
|
||||
case Key::Right: tapped_.right = true; break;
|
||||
case Key::Down: tapped_.soft = true; break;
|
||||
case Key::Char:
|
||||
if (e.ch == 'z' || e.ch == 'Z') tapped_.rotateBack = true;
|
||||
else if (e.ch == '1') tapped_.hardDrop = true; // far from the arrows: not hit by mistake
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GamesApp::update(uint32_t nowMs) {
|
||||
uint32_t ms = std::min<uint32_t>(nowMs - lastMs_, 100);
|
||||
if (which_ == Menu || state_[which_] != State::Playing) {
|
||||
lastMs_ = nowMs;
|
||||
return;
|
||||
}
|
||||
if (ms == 0) return;
|
||||
lastMs_ = nowMs;
|
||||
|
||||
if (which_ == Pong) {
|
||||
int move = keys_.held(Key::Up) ? -1 : keys_.held(Key::Down) ? 1 : 0;
|
||||
if (!move && static_cast<int32_t>(tapUntilMs_ - nowMs) > 0) move = tapMove_;
|
||||
uint8_t events = pong_->step(ms / 1000.0f, move);
|
||||
if (events & Pong::Over) ended(pong_->longestRally());
|
||||
else if (events & Pong::Point) beep(330, 120);
|
||||
else if (events & Pong::Hit) beep(880, 15);
|
||||
else if (events & Pong::Wall) beep(440, 10);
|
||||
} else if (which_ == Snake) {
|
||||
uint8_t events = snake_->step(ms);
|
||||
if (events & Snake::Died) ended(snake_->score());
|
||||
else if (events & Snake::Ate) beep(990, 25);
|
||||
} else {
|
||||
TetrisInput in = tapped_;
|
||||
tapped_ = TetrisInput();
|
||||
in.left |= keys_.held(Key::Left);
|
||||
in.right |= keys_.held(Key::Right);
|
||||
in.soft |= keys_.held(Key::Down);
|
||||
uint8_t events = tetris_->step(ms, in);
|
||||
if (events & Tetris::Over) ended(tetris_->score());
|
||||
else if (events & Tetris::LevelUp) beep(1320, 120);
|
||||
else if (events & Tetris::Lines) beep(tetris_->lastCleared() == 4 ? 1320 : 880, 60);
|
||||
else if (events & Tetris::Locked) beep(220, 12);
|
||||
Note note{};
|
||||
uint32_t length = 0;
|
||||
tune_.setEighthMs(korobeiniki::eighthMsAt(tetris_->level())); // it hurries as the game does
|
||||
if (state_[Tetris] == State::Playing && musicOn() && settings_.getBool(Setting::Sound) && tune_.step(ms, note, length) && note.hz) {
|
||||
loud(true);
|
||||
M5Cardputer.Speaker.tone(note.hz, length - kBreathMs, kTuneChannel, true);
|
||||
}
|
||||
}
|
||||
if (nowMs - lastDrawMs_ >= kFrameMs) {
|
||||
lastDrawMs_ = nowMs;
|
||||
requestRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
void GamesApp::help(std::vector<KeyHelp>& out) const {
|
||||
switch (which_) {
|
||||
case Pong: return keys::add(out, keys::kGamesPong);
|
||||
case Snake: return keys::add(out, keys::kGamesSnake);
|
||||
case Tetris: return keys::add(out, keys::kGamesTetris);
|
||||
default: return keys::add(out, keys::kGames);
|
||||
}
|
||||
}
|
||||
|
||||
const char* GamesApp::helpTitle() const {
|
||||
return which_ == Pong ? "Games: Pong" : which_ == Snake ? "Games: Snake" : which_ == Tetris ? "Games: Tetris" : nullptr;
|
||||
}
|
||||
|
||||
// A box in the middle of the field: where the Game stands when it is not running.
|
||||
void GamesApp::drawBanner(Canvas& c, const std::string& title, const std::string& detail) {
|
||||
const auto& area = theme::kContent;
|
||||
const int w = 150, h = detail.empty() ? 24 : 40, x = (area.w - w) / 2, y = area.y + (area.h - h) / 2;
|
||||
c.fillRect(x, y, w, h, theme::kBackground);
|
||||
c.drawRect(x, y, w, h, theme::kAccent);
|
||||
c.setTextDatum(top_center);
|
||||
c.setFont(&fonts::bold);
|
||||
c.setTextColor(theme::kText);
|
||||
c.drawString(title.c_str(), area.w / 2, y + 5);
|
||||
if (!detail.empty()) {
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString(detail.c_str(), area.w / 2, y + 21);
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
}
|
||||
|
||||
// The three Games as the Launcher shows Apps: an icon each, the chosen one on a cyan tile, its
|
||||
// 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[3] = {icons::pong, icons::snake, icons::tetris};
|
||||
const int tileW = 60, tileH = 36, x0 = (kContent.w - 3 * tileW) / 2, y = kContent.y + 24;
|
||||
for (int i = 0; i < 3; 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;
|
||||
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);
|
||||
}
|
||||
c.drawBitmap(ix, iy, kIcons[i], icons::kSize, icons::kSize, chosen ? kFaint : kAccent);
|
||||
if (state_[i] == State::Paused) {
|
||||
c.fillRect(ix + icons::kSize - 6, iy - 1, 10, 10, chosen ? kAccent : kBackground);
|
||||
c.fillRect(ix + icons::kSize - 4, iy + 1, 6, 6, kWarning);
|
||||
}
|
||||
}
|
||||
auto w = static_cast<Which>(chosen_);
|
||||
c.setTextDatum(top_center);
|
||||
c.setFont(&fonts::bold);
|
||||
c.setTextColor(kText);
|
||||
c.drawString(kNames[chosen_], kContent.w / 2, y + tileH + 8);
|
||||
int b = best(w);
|
||||
std::string under = state_[chosen_] == State::Paused ? "paused" : "";
|
||||
if (b > 0) under += (under.empty() ? "" : ", ") + std::string(w == Pong ? "longest rally " : "best ") + std::to_string(b);
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextColor(kMuted);
|
||||
c.drawString(under.c_str(), kContent.w / 2, y + tileH + 8 + kLineHeight + 2);
|
||||
c.setTextDatum(top_left);
|
||||
}
|
||||
|
||||
void GamesApp::drawPong(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
State state = state_[Pong];
|
||||
const int top = area.y;
|
||||
for (int y = top + 2; y < top + static_cast<int>(Pong::kHeight); y += 8) c.drawFastVLine(area.w / 2, y, 4, theme::kFaint);
|
||||
if (pong_) {
|
||||
char score[8];
|
||||
c.setFont(&fonts::large);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.setTextDatum(top_center);
|
||||
std::snprintf(score, sizeof score, "%d", pong_->playerScore());
|
||||
c.drawString(score, area.w / 2 - 24, top + 3);
|
||||
std::snprintf(score, sizeof score, "%d", pong_->deviceScore());
|
||||
c.drawString(score, area.w / 2 + 24, top + 3);
|
||||
c.setTextDatum(top_left);
|
||||
const int ph = static_cast<int>(Pong::kPaddleH), pw = static_cast<int>(Pong::kPaddleW), m = static_cast<int>(Pong::kMargin);
|
||||
c.fillRect(m, top + static_cast<int>(pong_->playerY()) - ph / 2, pw, ph, theme::kAccent);
|
||||
c.fillRect(area.w - m - pw, top + static_cast<int>(pong_->deviceY()) - ph / 2, pw, ph, theme::kWarning);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
else if (state == State::Paused) drawBanner(c, "Paused", "");
|
||||
else if (state == State::Over)
|
||||
drawBanner(c, pong_->playerWon() ? "You win" : "The device wins",
|
||||
newBest_ ? "a new longest rally: " + std::to_string(b) : "rally " + std::to_string(pong_->longestRally()) + ", best " + std::to_string(b));
|
||||
}
|
||||
|
||||
void GamesApp::drawSnake(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
State state = state_[Snake];
|
||||
const int w = Snake::kCols * kCell, h = Snake::kRows * kCell, x0 = (area.w - w) / 2, y0 = area.y + area.h - h - 1;
|
||||
c.drawRect(x0 - 1, y0 - 1, w + 2, h + 2, theme::kFaint);
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
char line[32];
|
||||
std::snprintf(line, sizeof line, "%d", snake_ ? snake_->score() : 0);
|
||||
c.drawString(line, x0, area.y);
|
||||
std::snprintf(line, sizeof line, "best %d", best(Snake));
|
||||
c.setTextDatum(top_right);
|
||||
c.drawString(line, x0 + w, area.y);
|
||||
c.setTextDatum(top_left);
|
||||
if (snake_) {
|
||||
auto f = snake_->food();
|
||||
c.fillRect(x0 + f.col * kCell + 1, y0 + f.row * kCell + 1, kCell - 2, kCell - 2, theme::kWarning);
|
||||
bool head = true;
|
||||
for (auto& cell : snake_->body()) {
|
||||
c.fillRect(x0 + cell.col * kCell, y0 + cell.row * kCell, kCell - 1, kCell - 1, head ? theme::kText : theme::kGood);
|
||||
head = false;
|
||||
}
|
||||
}
|
||||
if (state == State::Ready) drawBanner(c, "Snake", "the walls bite");
|
||||
else if (state == State::Paused) drawBanner(c, "Paused", "");
|
||||
else if (state == State::Over)
|
||||
drawBanner(c, "Game over", newBest_ ? "a new best: " + std::to_string(snake_->score()) : std::to_string(snake_->score()) + ", best " + std::to_string(best(Snake)));
|
||||
}
|
||||
|
||||
void GamesApp::drawTetris(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
State state = state_[Tetris];
|
||||
const int w = Tetris::kCols * kCell, h = Tetris::kRows * kCell, x0 = (area.w - w) / 2, y0 = area.y + 1;
|
||||
c.drawFastVLine(x0 - 1, y0, h + 1, theme::kMuted);
|
||||
c.drawFastVLine(x0 + w, y0, h + 1, theme::kMuted);
|
||||
c.drawFastHLine(x0 - 1, y0 + h, w + 2, theme::kMuted);
|
||||
auto block = [&](int x, int y, uint8_t kind) { c.fillRect(x, y, kCell - 1, kCell - 1, pieceColor(kind)); };
|
||||
|
||||
c.setFont(&fonts::body);
|
||||
const int right = x0 + w + 12, left = 6;
|
||||
auto readout = [&](int x, int y, const char* label, int value) {
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString(label, x, y);
|
||||
c.setTextColor(theme::kText);
|
||||
c.drawString(std::to_string(value).c_str(), x, y + theme::kLineHeight);
|
||||
};
|
||||
readout(left, area.y + 4, "Score", tetris_ ? tetris_->score() : 0);
|
||||
readout(left, area.y + 38, "Best", best(Tetris));
|
||||
readout(left, area.y + 72, "Lines", tetris_ ? tetris_->lines() : 0);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString("Next", right, area.y + 4);
|
||||
readout(right, area.y + 72, "Level", tetris_ ? tetris_->level() + 1 : 1);
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
char music[20];
|
||||
std::snprintf(music, sizeof music, "music %d/%d", volume(), kLoudest);
|
||||
c.drawString(!settings_.getBool(Setting::Sound) ? "sound off" : musicOn() ? music : "music off", right, area.y + area.h - 10);
|
||||
c.setFont(&fonts::body);
|
||||
|
||||
if (tetris_) {
|
||||
for (int row = 0; row < Tetris::kRows; row++)
|
||||
for (int col = 0; col < Tetris::kCols; col++)
|
||||
if (uint8_t kind = tetris_->cell(col, row)) block(x0 + col * kCell, y0 + row * kCell, kind);
|
||||
if (state != State::Over)
|
||||
for (auto& cell : Tetris::cells(tetris_->piece()))
|
||||
if (cell.row >= 0) block(x0 + cell.col * kCell, y0 + cell.row * kCell, tetris_->piece().kind);
|
||||
Tetris::Piece next{tetris_->next(), 0, 0, 0};
|
||||
for (auto& cell : Tetris::cells(next)) block(right + cell.col * kCell, area.y + 22 + cell.row * kCell, next.kind);
|
||||
}
|
||||
if (state == State::Ready) drawBanner(c, "Tetris", "");
|
||||
else if (state == State::Paused) drawBanner(c, "Paused", "");
|
||||
else if (state == State::Over)
|
||||
drawBanner(c, "Game over", newBest_ ? "a new best: " + std::to_string(tetris_->score()) : std::to_string(tetris_->lines()) + " lines");
|
||||
}
|
||||
|
||||
void GamesApp::draw(Canvas& c) {
|
||||
c.setTextDatum(top_left);
|
||||
lastDrawMs_ = millis();
|
||||
switch (which_) {
|
||||
case Pong: drawPong(c); break;
|
||||
case Snake: drawSnake(c); break;
|
||||
case Tetris: drawTetris(c); break;
|
||||
default: drawMenu(c); break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "app.h"
|
||||
#include "key_mapper.h"
|
||||
#include "key_value_store.h"
|
||||
#include "pong.h"
|
||||
#include "settings.h"
|
||||
#include "snake.h"
|
||||
#include "tetris.h"
|
||||
#include "tune.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// The Games App (issue #101): three Games as a row of icons, and the one being played. The rules are in
|
||||
// lib/games; this draws them, gives them the keys and the time, beeps, and keeps the best of each.
|
||||
// A Game left in the middle, for the list or for another App, is found again paused.
|
||||
class GamesApp : public App {
|
||||
public:
|
||||
GamesApp(const KeyMapper& keys, const Settings& settings, KeyValueStore& store) : keys_(keys), settings_(settings), store_(store) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void update(uint32_t nowMs) override;
|
||||
void draw(Canvas& c) override;
|
||||
void help(std::vector<KeyHelp>& out) const override;
|
||||
const char* helpTitle() const override;
|
||||
// A Game being played is the device in use, key or no key: the screen stays lit.
|
||||
bool keepsScreenOn() const override { return which_ != Menu && state_[which_] == State::Playing; }
|
||||
|
||||
private:
|
||||
enum Which { Pong, Snake, Tetris, Menu }; // the first three index the arrays
|
||||
enum class State : uint8_t { Ready, Playing, Paused, Over };
|
||||
|
||||
void begin(); // a new game of the one chosen
|
||||
void pause();
|
||||
void ended(int score);
|
||||
void beep(int hz, int ms);
|
||||
bool musicOn();
|
||||
int volume(); // of the tune, 1 to kLoudest
|
||||
void setVolume(int level);
|
||||
void quiet(); // the tune stops where it is
|
||||
void loud(bool on); // the speaker turned up for the tune, or back as it was
|
||||
int best(Which w);
|
||||
bool pongKey(const KeyEvent& e);
|
||||
bool snakeKey(const KeyEvent& e);
|
||||
bool tetrisKey(const KeyEvent& e);
|
||||
void drawMenu(Canvas& c);
|
||||
void drawPong(Canvas& c);
|
||||
void drawSnake(Canvas& c);
|
||||
void drawTetris(Canvas& c);
|
||||
void drawBanner(Canvas& c, const std::string& title, const std::string& detail);
|
||||
|
||||
const KeyMapper& keys_;
|
||||
const Settings& settings_;
|
||||
KeyValueStore& store_;
|
||||
int chosen_ = 0; // in the row of three
|
||||
Which which_ = Menu;
|
||||
State state_[3] = {State::Ready, State::Ready, State::Ready};
|
||||
int best_[3] = {-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_;
|
||||
uint32_t lastMs_ = 0, lastDrawMs_ = 0;
|
||||
// Keys that came as presses since the last step: a tap moves too, held or not.
|
||||
int tapMove_ = 0;
|
||||
uint32_t tapUntilMs_ = 0;
|
||||
games::TetrisInput tapped_;
|
||||
// Tetris's tune (Korobeiniki), switched with 0 and remembered.
|
||||
games::TunePlayer tune_{games::korobeiniki::at, games::korobeiniki::kCount, games::korobeiniki::kEighthMs};
|
||||
int music_ = -1, volume_ = -1; // -1: not read from the store yet
|
||||
int masterWas_ = -1; // the speaker's own volume, while the tune has it turned up
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "services/ssh_service.h"
|
||||
#include "services/vpn_service.h"
|
||||
#include "services/web_share.h"
|
||||
#include "apps/games_app.h"
|
||||
#include "apps/gemini_app.h"
|
||||
#include "apps/gnss_app.h"
|
||||
#include "auto_update.h"
|
||||
@@ -261,6 +262,7 @@ void setup() {
|
||||
netTools.ntpServer = []() { return settings.getString(Setting::Ntp1); };
|
||||
// A long note being rewritten (issue #47): the editor waits for the card, so it draws from there.
|
||||
NoteEditor::onProgress = [](const std::string& name, int percent) { screen.renderUpdate("Saving", name, percent); };
|
||||
apps->registerApp({"games", "Games", false, new GamesApp(keyMapper, settings, nvs), icons::games});
|
||||
apps->registerApp({"system", "System", false,
|
||||
new SystemApp(*wifi, *battery, *storageService, *radioService, *gnssService, nvs), icons::system});
|
||||
apps->registerApp({"settings", "Settings", false,
|
||||
@@ -1611,6 +1613,14 @@ static void loopPass() {
|
||||
bus.dispatch();
|
||||
notifier->update(now);
|
||||
apps->update(now);
|
||||
// An App in use with no key pressed (a Game being played): the screen stays lit, and it is
|
||||
// not the quiet moment an update waits for.
|
||||
static uint32_t keptOnMs = 0;
|
||||
if (apps->foreground().keepsScreenOn() && now - keptOnMs >= 1000) {
|
||||
keptOnMs = now;
|
||||
lastKeyMs = now;
|
||||
power->onKey(now);
|
||||
}
|
||||
autoUpdateStep(now);
|
||||
|
||||
if (now - lastLog > 10000) {
|
||||
|
||||
@@ -3,6 +3,41 @@
|
||||
|
||||
namespace roro::icons {
|
||||
|
||||
const uint8_t games[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x03, 0xff, 0xff, 0xc0, // ......####################......
|
||||
0x03, 0xff, 0xff, 0xc0, // ......####################......
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x0c, 0x00, 0x00, 0x30, // ....##....................##....
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0xc0, 0x03, 0x0c, // ..##....##............##....##..
|
||||
0x30, 0xc0, 0x03, 0x0c, // ..##....##............##....##..
|
||||
0x33, 0xf0, 0x0c, 0xcc, // ..##..######........##..##..##..
|
||||
0x33, 0xf0, 0x0c, 0xcc, // ..##..######........##..##..##..
|
||||
0x30, 0xc0, 0x03, 0x0c, // ..##....##............##....##..
|
||||
0x30, 0xc0, 0x03, 0x0c, // ..##....##............##....##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x00, 0x00, 0x0c, // ..##........................##..
|
||||
0x30, 0x0f, 0xf0, 0x0c, // ..##........########........##..
|
||||
0x30, 0x0f, 0xf0, 0x0c, // ..##........########........##..
|
||||
0x30, 0x30, 0x0c, 0x0c, // ..##......##........##......##..
|
||||
0x30, 0x30, 0x0c, 0x0c, // ..##......##........##......##..
|
||||
0x0f, 0xc0, 0x03, 0xf0, // ....######............######....
|
||||
0x0f, 0xc0, 0x03, 0xf0, // ....######............######....
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t gemini[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
@@ -178,6 +213,41 @@ const uint8_t notes[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t pong[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x03, 0x00, 0x00, // ..............##................
|
||||
0x00, 0x03, 0x00, 0x00, // ..............##................
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x3c, 0x03, 0x00, 0x00, // ..####........##................
|
||||
0x3c, 0x03, 0x00, 0x00, // ..####........##................
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x3c, 0x03, 0x0f, 0x00, // ..####........##....####........
|
||||
0x3c, 0x03, 0x0f, 0x00, // ..####........##....####........
|
||||
0x3c, 0x00, 0x0f, 0x0f, // ..####..............####....####
|
||||
0x3c, 0x00, 0x0f, 0x0f, // ..####..............####....####
|
||||
0x00, 0x03, 0x00, 0x0f, // ..............##............####
|
||||
0x00, 0x03, 0x00, 0x0f, // ..............##............####
|
||||
0x00, 0x00, 0x00, 0x0f, // ............................####
|
||||
0x00, 0x00, 0x00, 0x0f, // ............................####
|
||||
0x00, 0x03, 0x00, 0x0f, // ..............##............####
|
||||
0x00, 0x03, 0x00, 0x0f, // ..............##............####
|
||||
0x00, 0x00, 0x00, 0x0f, // ............................####
|
||||
0x00, 0x00, 0x00, 0x0f, // ............................####
|
||||
0x00, 0x03, 0x00, 0x00, // ..............##................
|
||||
0x00, 0x03, 0x00, 0x00, // ..............##................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t settings[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
@@ -248,6 +318,41 @@ const uint8_t shell[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t snake[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x0f, 0xf0, // ....................########....
|
||||
0x00, 0x00, 0x0f, 0xf0, // ....................########....
|
||||
0x00, 0x00, 0x0c, 0xf0, // ....................##..####....
|
||||
0x00, 0x00, 0x0c, 0xf0, // ....................##..####....
|
||||
0x00, 0x00, 0x0f, 0xf0, // ....................########....
|
||||
0x00, 0x00, 0x0f, 0xf0, // ....................########....
|
||||
0x00, 0x00, 0x03, 0xc0, // ......................####......
|
||||
0x00, 0x00, 0x03, 0xc0, // ......................####......
|
||||
0x03, 0xff, 0xff, 0x00, // ......##################........
|
||||
0x03, 0xff, 0xff, 0x00, // ......##################........
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x03, 0xc0, 0x00, 0x00, // ......####......................
|
||||
0x03, 0xff, 0xff, 0x00, // ......##################........
|
||||
0x03, 0xff, 0xff, 0x00, // ......##################........
|
||||
0x00, 0x00, 0x0f, 0x00, // ....................####........
|
||||
0x00, 0x00, 0x0f, 0x00, // ....................####........
|
||||
0x00, 0x00, 0x0f, 0x00, // ....................####........
|
||||
0x00, 0x00, 0x0f, 0x00, // ....................####........
|
||||
0x3f, 0xff, 0xfc, 0x00, // ..####################..........
|
||||
0x3f, 0xff, 0xfc, 0x00, // ..####################..........
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x3c, 0x00, 0x00, 0x00, // ..####..........................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t ssh[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
@@ -353,6 +458,41 @@ const uint8_t system[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t tetris[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x0f, 0xcf, 0xcf, 0xc0, // ....######..######..######......
|
||||
0x0f, 0xcf, 0xcf, 0xc0, // ....######..######..######......
|
||||
0x0f, 0xcf, 0xcf, 0xc0, // ....######..######..######......
|
||||
0x0f, 0xcf, 0xcf, 0xc0, // ....######..######..######......
|
||||
0x0f, 0xcf, 0xcf, 0xc0, // ....######..######..######......
|
||||
0x0f, 0xcf, 0xcf, 0xc0, // ....######..######..######......
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x0f, 0xc0, 0x00, // ............######..............
|
||||
0x00, 0x0f, 0xc0, 0x00, // ............######..............
|
||||
0x00, 0x0f, 0xc0, 0x00, // ............######..............
|
||||
0x00, 0x0f, 0xc0, 0x00, // ............######..............
|
||||
0x00, 0x0f, 0xc0, 0x00, // ............######..............
|
||||
0x00, 0x0f, 0xc0, 0x00, // ............######..............
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x0f, 0xc0, 0x0f, 0xc0, // ....######..........######......
|
||||
0x0f, 0xc0, 0x0f, 0xc0, // ....######..........######......
|
||||
0x0f, 0xc0, 0x0f, 0xc0, // ....######..........######......
|
||||
0x0f, 0xc0, 0x0f, 0xc0, // ....######..........######......
|
||||
0x0f, 0xc0, 0x0f, 0xc0, // ....######..........######......
|
||||
0x0f, 0xc0, 0x0f, 0xc0, // ....######..........######......
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x3f, 0xff, 0xff, 0xf0, // ..##########################....
|
||||
0x3f, 0xff, 0xff, 0xf0, // ..##########################....
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
};
|
||||
|
||||
const uint8_t wifi_tools[128] = {
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
0x00, 0x00, 0x00, 0x00, // ................................
|
||||
|
||||
@@ -8,16 +8,20 @@ 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 games[128]; // games.png
|
||||
extern const uint8_t gemini[128]; // gemini.png
|
||||
extern const uint8_t gnss[128]; // gnss.png
|
||||
extern const uint8_t irc[128]; // irc.png
|
||||
extern const uint8_t lora[128]; // lora.png
|
||||
extern const uint8_t notes[128]; // notes.png
|
||||
extern const uint8_t pong[128]; // pong.png
|
||||
extern const uint8_t settings[128]; // settings.png
|
||||
extern const uint8_t shell[128]; // shell.png
|
||||
extern const uint8_t snake[128]; // snake.png
|
||||
extern const uint8_t ssh[128]; // ssh.png
|
||||
extern const uint8_t storage[128]; // storage.png
|
||||
extern const uint8_t system[128]; // system.png
|
||||
extern const uint8_t tetris[128]; // tetris.png
|
||||
extern const uint8_t wifi_tools[128]; // wifi-tools.png
|
||||
|
||||
} // namespace roro::icons
|
||||
|
||||
Reference in New Issue
Block a user