Compare commits

...

4 Commits

Author SHA1 Message Date
ecbcce12ea fix 2025-10-15 20:51:52 +02:00
f6c800fc63 gameboy save states 2025-10-15 20:46:48 +02:00
5e63875d35 bad sound in correct place 2025-10-13 22:02:55 +02:00
cc805abe80 snake app 2025-10-13 14:36:27 +02:00
7 changed files with 747 additions and 45 deletions

View File

@@ -4,6 +4,7 @@
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/snake_app.hpp"
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/esp_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
@@ -234,6 +235,7 @@ extern "C" void app_main() {
system.registerApp(apps::createMenuAppFactory());
system.registerApp(apps::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createSnakeAppFactory());
system.registerApp(apps::createTetrisAppFactory());
system.registerApp(apps::createGameboyAppFactory());

View File

@@ -16,4 +16,5 @@ add_subdirectory(menu)
add_subdirectory(clock)
add_subdirectory(settings)
add_subdirectory(gameboy)
add_subdirectory(snake)
add_subdirectory(tetris)

View File

@@ -299,6 +299,9 @@ public:
break;
}
if (mode != Mode::Running)
break;
GB_PERF_ONLY(perf.geometryUs = 0;)
@@ -306,11 +309,49 @@ public:
gb_run_frame(&gb);
GB_PERF_ONLY(perf.runUs = nowMicros() - runStartUs;)
{
uint32_t freqHz = 0;
uint8_t loud = 0;
if (apu.computeEffectiveTone(freqHz, loud)) {
// Basic smoothing: if freq didn't change much, keep it; otherwise snap quickly
const uint32_t prev = lastFreqHz;
if (prev != 0 && freqHz != 0) {
const uint32_t diff = (prev > freqHz) ? (prev - freqHz) : (freqHz - prev);
if (diff < 15) {
freqHz = prev; // minor jitter suppression
++stableFrames;
} else {
stableFrames = 0;
}
} else {
stableFrames = 0;
}
lastFreqHz = freqHz;
lastLoud = loud;
const uint32_t durMs = 16;
playTone(freqHz, durMs, 0);
} else {
lastFreqHz = 0;
lastLoud = 0;
// Don't enqueue anything; queue naturally drains and buzzer stops
}
}
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderGameFrame();
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
break;
}
case Mode::Prompt: {
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
handlePromptInput(input);
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderPrompt();
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
break;
}
}
prevInput = input;
@@ -777,8 +818,9 @@ public:
}
};
enum class Mode { Browse, Running };
enum class Mode { Browse, Running, Prompt };
enum class ScaleMode { Original, FullHeight, FullHeightWide };
enum class PromptKind { None, LoadState, SaveState };
struct PerfTracker {
enum class CallbackKind { RomRead, CartRamRead, CartRamWrite, LcdDraw, Error };
@@ -905,8 +947,19 @@ public:
void printStep(Mode stepMode, bool gbReady, bool frameDirty, uint32_t fps, const std::string& romName,
std::size_t romCount, std::size_t selectedIdx, bool browserDirty) const {
auto toMs = [](uint64_t us) { return static_cast<double>(us) / 1000.0; };
const char* modeStr = (stepMode == Mode::Running) ? "RUN" : "BROWSE";
auto toMs = [](uint64_t us) { return static_cast<double>(us) / 1000.0; };
const char* modeStr = "UNKNOWN";
switch (stepMode) {
case Mode::Running:
modeStr = "RUN";
break;
case Mode::Browse:
modeStr = "BROWSE";
break;
case Mode::Prompt:
modeStr = "PROMPT";
break;
}
const char* name = romName.empty() ? "-" : romName.c_str();
const std::size_t safeIdx = (romCount == 0) ? 0 : std::min(selectedIdx, romCount - 1);
std::printf(
@@ -1085,6 +1138,10 @@ public:
uint32_t fpsCurrent = 0;
std::string activeRomName;
std::string activeRomSavePath;
std::string activeRomStatePath;
PromptKind promptKind = PromptKind::None;
int promptSelection = 0; // 0 = Yes, 1 = No
bool promptDirty = false;
SimpleApu apu{};
// Smoothing state for buzzer tone
uint32_t lastFreqHz = 0;
@@ -1423,21 +1480,25 @@ public:
return false;
}
gb.direct.priv = this;
gb.direct.joypad = 0xFF;
gb_init_lcd(&gb, &GameboyApp::lcdDrawLine);
applyRuntimeBindings();
gb.direct.joypad = 0xFF;
gb.direct.interlace = false;
gb.direct.frame_skip = true;
const uint_fast32_t saveSize = gb_get_save_size(&gb);
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
std::string savePath;
std::string statePath;
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
if (fsReady)
savePath = buildSavePath(rom, romDirectory());
activeRomSavePath = savePath;
if (fsReady) {
const std::string romDir = romDirectory();
savePath = buildSavePath(rom, romDir);
statePath = buildStatePath(rom, romDir);
}
activeRomSavePath = savePath;
activeRomStatePath = statePath;
loadSaveFile();
resetFpsStats();
@@ -1452,6 +1513,10 @@ public:
if (!fsReady)
statusText.append(" (no save)");
setStatus(std::move(statusText));
if (stateFileExists())
enterPrompt(PromptKind::LoadState);
return true;
}
@@ -1464,6 +1529,10 @@ public:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
activeRomStatePath.clear();
promptKind = PromptKind::None;
promptSelection = 0;
promptDirty = false;
return;
}
@@ -1477,6 +1546,10 @@ public:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
activeRomStatePath.clear();
promptKind = PromptKind::None;
promptSelection = 0;
promptDirty = false;
std::memset(&gb, 0, sizeof(gb));
apu.reset();
mode = Mode::Browse;
@@ -1491,6 +1564,19 @@ public:
if (scaleToggleCombo)
toggleScaleMode();
const bool exitComboPressed = input.start && input.select;
const bool exitComboJustPressed = exitComboPressed && !(prevInput.start && prevInput.select);
if (exitComboJustPressed) {
if (!activeRomStatePath.empty()) {
enterPrompt(PromptKind::SaveState);
} else {
unloadRom();
setStatus("Save state unavailable");
}
gb.direct.joypad = 0xFF;
return;
}
uint8_t joypad = 0xFF;
if (input.a)
joypad &= ~JOYPAD_A;
@@ -1510,13 +1596,6 @@ public:
joypad &= ~JOYPAD_RIGHT;
gb.direct.joypad = joypad;
const bool exitComboPressed = input.start && input.select;
const bool exitComboJustPressed = exitComboPressed && !(prevInput.start && prevInput.select);
if (exitComboJustPressed) {
setStatus("Saved " + activeRomName);
unloadRom();
}
}
void renderGameFrame() {
@@ -1688,6 +1767,186 @@ public:
}
}
void applyRuntimeBindings() {
gb.gb_cart_ram_read = &GameboyApp::cartRamRead;
gb.gb_cart_ram_write = &GameboyApp::cartRamWrite;
gb.gb_error = &GameboyApp::errorCallback;
gb.display.lcd_draw_line = &GameboyApp::lcdDrawLine;
gb.direct.priv = this;
if (romDataView)
gb.direct.rom = romDataView;
}
bool saveStateToFile() {
if (activeRomStatePath.empty())
return false;
FILE* file = std::fopen(activeRomStatePath.c_str(), "wb");
if (!file)
return false;
const size_t written = std::fwrite(&gb, 1, sizeof(gb), file);
std::fclose(file);
return written == sizeof(gb);
}
bool loadStateFromFile() {
if (activeRomStatePath.empty())
return false;
FILE* file = std::fopen(activeRomStatePath.c_str(), "rb");
if (!file)
return false;
std::vector<uint8_t> backup(sizeof(gb));
std::memcpy(backup.data(), &gb, sizeof(gb));
const size_t read = std::fread(&gb, 1, sizeof(gb), file);
std::fclose(file);
if (read != sizeof(gb)) {
std::memcpy(&gb, backup.data(), sizeof(gb));
return false;
}
applyRuntimeBindings();
gb.direct.joypad = 0xFF;
frameDirty = true;
return true;
}
bool stateFileExists() const {
if (activeRomStatePath.empty())
return false;
struct stat st{};
return stat(activeRomStatePath.c_str(), &st) == 0 && S_ISREG(st.st_mode);
}
void enterPrompt(PromptKind kind) {
if (kind == PromptKind::None)
return;
promptKind = kind;
promptSelection = 0;
promptDirty = true;
mode = Mode::Prompt;
gb.direct.joypad = 0xFF;
scheduleNextTick(0);
}
void exitPrompt(Mode nextMode) {
promptKind = PromptKind::None;
promptSelection = 0;
promptDirty = true;
mode = nextMode;
}
void handlePromptInput(const InputState& input) {
if (promptKind == PromptKind::None)
return;
const bool leftOrUp = (input.left && !prevInput.left) || (input.up && !prevInput.up);
const bool rightOrDown = (input.right && !prevInput.right) || (input.down && !prevInput.down);
if (leftOrUp && promptSelection != 0) {
promptSelection = 0;
promptDirty = true;
} else if (rightOrDown && promptSelection != 1) {
promptSelection = 1;
promptDirty = true;
}
const bool confirm = (input.a && !prevInput.a) || (input.start && !prevInput.start);
const bool cancel = (input.b && !prevInput.b) || (input.select && !prevInput.select);
if (confirm) {
handlePromptDecision(promptSelection == 0);
} else if (cancel) {
handlePromptDecision(false);
}
}
void handlePromptDecision(bool yesSelected) {
const PromptKind kind = promptKind;
if (kind == PromptKind::None)
return;
if (kind == PromptKind::LoadState) {
exitPrompt(Mode::Running);
if (yesSelected) {
if (loadStateFromFile())
setStatus("Save state loaded");
else
setStatus("Load state failed");
}
frameDirty = true;
return;
}
if (kind == PromptKind::SaveState) {
const bool haveStatePath = !activeRomStatePath.empty();
bool saved = false;
if (yesSelected && haveStatePath)
saved = saveStateToFile();
exitPrompt(Mode::Running);
unloadRom();
if (yesSelected) {
if (!haveStatePath)
setStatus("Save state unavailable");
else if (saved)
setStatus("Save state written");
else
setStatus("Save state failed");
} else {
setStatus("Exited without state");
}
}
}
void renderPrompt() {
if (!promptDirty || promptKind == PromptKind::None)
return;
promptDirty = false;
framebuffer.frameReady();
framebuffer.clear(false);
auto drawCentered = [&](int y, std::string_view text, int scale = 1) {
const int width = font16x8::measureText(text, scale, 1);
const int x = (framebuffer.width() - width) / 2;
font16x8::drawText(framebuffer, x, y, text, scale, true, 1);
};
std::string_view headline;
std::string_view helper;
switch (promptKind) {
case PromptKind::LoadState:
headline = "LOAD SAVE STATE?";
helper = "A YES / B NO";
break;
case PromptKind::SaveState:
headline = "SAVE BEFORE EXIT?";
helper = "A YES / B NO";
break;
case PromptKind::None:
default:
headline = "";
helper = "";
break;
}
drawCentered(40, headline);
drawCentered(72, helper);
const std::string yesLabel = (promptSelection == 0) ? "> YES" : " YES";
const std::string noLabel = (promptSelection == 1) ? "> NO" : " NO";
drawCentered(104, yesLabel);
drawCentered(120, noLabel);
framebuffer.sendFrame();
}
void playTone(uint32_t freqHz, uint32_t durationMs, uint32_t gapMs) {
if (freqHz == 0 || durationMs == 0)
return;
@@ -1739,6 +1998,21 @@ public:
return result;
}
static std::string buildStatePath(const RomEntry& rom, std::string_view romDir) {
std::string slug = rom.saveSlug;
if (slug.empty())
slug = sanitizeSaveSlug(rom.name);
if (slug.empty())
slug = "rom";
std::string result(romDir);
if (!result.empty() && result.back() != '/')
result.push_back('/');
result.append(slug);
result.append(".state");
return result;
}
static GameboyApp* fromGb(struct gb_s* gb) {
CARDBOY_CHECK_CODE(if (!gb) return nullptr;);
return static_cast<GameboyApp*>(gb->direct.priv);
@@ -1926,35 +2200,6 @@ private:
drawLineOriginal(*self, pixels, static_cast<int>(line));
break;
}
// Simple per-scanline hook: at end of last line, decide tone for the frame.
if (line + 1 == LCD_HEIGHT) {
uint32_t freqHz = 0;
uint8_t loud = 0;
if (self->apu.computeEffectiveTone(freqHz, loud)) {
// Basic smoothing: if freq didn't change much, keep it; otherwise snap quickly
const uint32_t prev = self->lastFreqHz;
if (prev != 0 && freqHz != 0) {
const uint32_t diff = (prev > freqHz) ? (prev - freqHz) : (freqHz - prev);
if (diff < 15) {
freqHz = prev; // minor jitter suppression
++self->stableFrames;
} else {
self->stableFrames = 0;
}
} else {
self->stableFrames = 0;
}
self->lastFreqHz = freqHz;
self->lastLoud = loud;
const uint32_t durMs = 17;
self->playTone(freqHz, durMs, 0);
} else {
self->lastFreqHz = 0;
self->lastLoud = 0;
// Don't enqueue anything; queue naturally drains and buzzer stops
}
}
}
static const char* initErrorToString(enum gb_init_error_e err) {

View File

@@ -0,0 +1,9 @@
target_sources(cardboy_apps
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/snake_app.cpp
)
target_include_directories(cardboy_apps
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

View File

@@ -0,0 +1,11 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<cardboy::sdk::IAppFactory> createSnakeAppFactory();
} // namespace apps

View File

@@ -0,0 +1,432 @@
#include "cardboy/apps/snake_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/display_spec.hpp"
#include "cardboy/sdk/input_state.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <random>
#include <string>
#include <string_view>
#include <vector>
namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
constexpr char kSnakeAppName[] = "Snake";
constexpr int kBoardWidth = 32;
constexpr int kBoardHeight = 20;
constexpr int kCellSize = 10;
constexpr int kInitialSnakeLength = 5;
constexpr int kScorePerFood = 10;
constexpr int kMinMoveIntervalMs = 80;
constexpr int kBaseMoveIntervalMs = 220;
constexpr int kIntervalSpeedupPerSegment = 4;
struct Point {
int x = 0;
int y = 0;
bool operator==(const Point& other) const { return x == other.x && y == other.y; }
};
enum class Direction { Up, Down, Left, Right };
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
if (auto* rnd = ctx.random())
return rnd->nextUint32();
static std::random_device rd;
return rd();
}
class SnakeGame {
public:
explicit SnakeGame(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {
rng.seed(randomSeed(context));
loadHighScore();
reset();
}
void onStart() {
scheduleMoveTimer();
dirty = true;
renderIfNeeded();
}
void onStop() { cancelMoveTimer(); }
void handleEvent(const AppEvent& event) {
switch (event.type) {
case AppEventType::Button:
handleButtons(event.button);
break;
case AppEventType::Timer:
handleTimer(event.timer.handle);
break;
}
renderIfNeeded();
}
private:
AppContext& context;
typename AppContext::Framebuffer& framebuffer;
std::deque<Point> snake;
Point food{};
Direction direction = Direction::Right;
Direction queuedDirection = Direction::Right;
bool paused = false;
bool gameOver = false;
bool dirty = false;
int score = 0;
int highScore = 0;
AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer;
std::mt19937 rng;
void handleButtons(const AppButtonEvent& evt) {
const auto& cur = evt.current;
const auto& prev = evt.previous;
if (cur.b && !prev.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
if (cur.select && !prev.select) {
reset();
return;
}
if (cur.start && !prev.start) {
if (gameOver)
reset();
else {
paused = !paused;
dirty = true;
}
}
if (gameOver)
return;
if (cur.up && !prev.up)
queueDirection(Direction::Up);
else if (cur.down && !prev.down)
queueDirection(Direction::Down);
else if (cur.left && !prev.left)
queueDirection(Direction::Left);
else if (cur.right && !prev.right)
queueDirection(Direction::Right);
if (cur.a && !prev.a && !paused)
advance();
}
void handleTimer(AppTimerHandle handle) {
if (handle == moveTimer && !paused && !gameOver)
advance();
}
void reset() {
cancelMoveTimer();
snake.clear();
const int centerX = kBoardWidth / 2;
const int centerY = kBoardHeight / 2;
for (int i = 0; i < kInitialSnakeLength; ++i)
snake.push_back(Point{centerX - i, centerY});
direction = Direction::Right;
queuedDirection = Direction::Right;
paused = false;
gameOver = false;
score = 0;
dirty = true;
if (!spawnFood())
onGameOver();
scheduleMoveTimer();
}
void advance() {
direction = queuedDirection;
Point nextHead = snake.front();
switch (direction) {
case Direction::Up:
--nextHead.y;
break;
case Direction::Down:
++nextHead.y;
break;
case Direction::Left:
--nextHead.x;
break;
case Direction::Right:
++nextHead.x;
break;
}
if (isCollision(nextHead)) {
onGameOver();
return;
}
snake.push_front(nextHead);
if (nextHead == food) {
score += kScorePerFood;
updateHighScore();
if (!spawnFood()) {
onGameOver();
return;
}
scheduleMoveTimer();
if (auto* buzzer = context.buzzer())
buzzer->beepMove();
} else {
snake.pop_back();
}
dirty = true;
}
[[nodiscard]] bool isCollision(const Point& nextHead) const {
if (nextHead.x < 0 || nextHead.x >= kBoardWidth || nextHead.y < 0 || nextHead.y >= kBoardHeight)
return true;
return std::find(snake.begin(), snake.end(), nextHead) != snake.end();
}
void onGameOver() {
if (gameOver)
return;
gameOver = true;
cancelMoveTimer();
dirty = true;
if (auto* buzzer = context.buzzer())
buzzer->beepGameOver();
}
void queueDirection(Direction next) {
if (isOpposite(direction, next) || isOpposite(queuedDirection, next))
return;
queuedDirection = next;
}
[[nodiscard]] static bool isOpposite(Direction a, Direction b) {
if ((a == Direction::Up && b == Direction::Down) || (a == Direction::Down && b == Direction::Up))
return true;
if ((a == Direction::Left && b == Direction::Right) || (a == Direction::Right && b == Direction::Left))
return true;
return false;
}
bool spawnFood() {
std::vector<Point> freeCells;
freeCells.reserve(kBoardWidth * kBoardHeight - static_cast<int>(snake.size()));
for (int y = 0; y < kBoardHeight; ++y) {
for (int x = 0; x < kBoardWidth; ++x) {
Point p{x, y};
if (std::find(snake.begin(), snake.end(), p) == snake.end())
freeCells.push_back(p);
}
}
if (freeCells.empty())
return false;
std::uniform_int_distribution<std::size_t> dist(0, freeCells.size() - 1);
food = freeCells[dist(rng)];
return true;
}
void scheduleMoveTimer() {
cancelMoveTimer();
const std::uint32_t interval = currentInterval();
moveTimer = context.scheduleRepeatingTimer(interval);
}
void cancelMoveTimer() {
if (moveTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(moveTimer);
moveTimer = cardboy::sdk::kInvalidAppTimer;
}
}
[[nodiscard]] std::uint32_t currentInterval() const {
int interval = kBaseMoveIntervalMs - static_cast<int>(snake.size()) * kIntervalSpeedupPerSegment;
if (interval < kMinMoveIntervalMs)
interval = kMinMoveIntervalMs;
return static_cast<std::uint32_t>(interval);
}
void updateHighScore() {
if (score <= highScore)
return;
highScore = score;
if (auto* storage = context.storage())
storage->writeUint32("snake", "best", static_cast<std::uint32_t>(highScore));
}
void loadHighScore() {
if (auto* storage = context.storage()) {
std::uint32_t stored = 0;
if (storage->readUint32("snake", "best", stored))
highScore = static_cast<int>(stored);
}
}
void renderIfNeeded() {
if (!dirty)
return;
dirty = false;
framebuffer.frameReady();
framebuffer.clear(false);
drawBoard();
drawFood();
drawSnake();
drawHud();
framebuffer.sendFrame();
}
[[nodiscard]] int boardOriginX() const {
return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
}
[[nodiscard]] int boardOriginY() const {
const int centered = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
return std::max(24, centered);
}
void drawBoard() {
const int originX = boardOriginX();
const int originY = boardOriginY();
const int width = kBoardWidth * kCellSize;
const int height = kBoardHeight * kCellSize;
const int x0 = originX;
const int y0 = originY;
const int x1 = originX + width - 1;
const int y1 = originY + height - 1;
for (int x = x0; x <= x1; ++x) {
framebuffer.drawPixel(x, y0, true);
framebuffer.drawPixel(x, y1, true);
}
for (int y = y0; y <= y1; ++y) {
framebuffer.drawPixel(x0, y, true);
framebuffer.drawPixel(x1, y, true);
}
}
void drawSnake() {
if (snake.empty())
return;
std::size_t index = 0;
for (const auto& segment: snake) {
drawSnakeSegment(segment, index == 0);
++index;
}
}
void drawSnakeSegment(const Point& segment, bool head) {
const int originX = boardOriginX() + segment.x * kCellSize;
const int originY = boardOriginY() + segment.y * kCellSize;
for (int dy = 0; dy < kCellSize; ++dy) {
for (int dx = 0; dx < kCellSize; ++dx) {
const bool border = dx == 0 || dy == 0 || dx == kCellSize - 1 || dy == kCellSize - 1;
bool fill = ((dx + dy) & 0x1) == 0;
if (head)
fill = true;
const bool on = border || fill;
framebuffer.drawPixel(originX + dx, originY + dy, on);
}
}
}
void drawFood() {
const int cx = boardOriginX() + food.x * kCellSize + kCellSize / 2;
const int cy = boardOriginY() + food.y * kCellSize + kCellSize / 2;
const int r = std::max(2, kCellSize / 2 - 1);
for (int dy = -r; dy <= r; ++dy) {
for (int dx = -r; dx <= r; ++dx) {
if (std::abs(dx) + std::abs(dy) <= r)
framebuffer.drawPixel(cx + dx, cy + dy, true);
}
}
}
void drawHud() {
const int margin = 12;
const int textY = 8;
const std::string scoreStr = "SCORE " + std::to_string(score);
const std::string bestStr = "BEST " + std::to_string(highScore);
font16x8::drawText(framebuffer, margin, textY, scoreStr, 1, true, 1);
const int bestX = cardboy::sdk::kDisplayWidth - font16x8::measureText(bestStr, 1, 1) - margin;
font16x8::drawText(framebuffer, bestX, textY, bestStr, 1, true, 1);
const int footerY = cardboy::sdk::kDisplayHeight - 24;
const std::string menuStr = "B MENU";
const std::string selectStr = "SELECT RESET";
const std::string startStr = "START PAUSE";
const int selectX = (cardboy::sdk::kDisplayWidth - font16x8::measureText(selectStr, 1, 1)) / 2;
const int startX = cardboy::sdk::kDisplayWidth - font16x8::measureText(startStr, 1, 1) - margin;
font16x8::drawText(framebuffer, margin, footerY, menuStr, 1, true, 1);
font16x8::drawText(framebuffer, selectX, footerY, selectStr, 1, true, 1);
font16x8::drawText(framebuffer, startX, footerY, startStr, 1, true, 1);
if (paused && !gameOver)
drawBanner("PAUSED");
else if (gameOver)
drawBanner("GAME OVER");
}
void drawBanner(std::string_view text) {
const int w = font16x8::measureText(text, 2, 1);
const int h = font16x8::kGlyphHeight * 2;
const int x = (cardboy::sdk::kDisplayWidth - w) / 2;
const int y = boardOriginY() + kBoardHeight * kCellSize / 2 - h / 2;
for (int yy = -4; yy < h + 4; ++yy)
for (int xx = -6; xx < w + 6; ++xx)
framebuffer.drawPixel(x + xx, y + yy, yy == -4 || yy == h + 3 || xx == -6 || xx == w + 5);
font16x8::drawText(framebuffer, x, y, text, 2, true, 1);
}
};
class SnakeApp final : public cardboy::sdk::IApp {
public:
explicit SnakeApp(AppContext& ctx) : game(ctx) {}
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
private:
SnakeGame game;
};
class SnakeFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kSnakeAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
return std::make_unique<SnakeApp>(context);
}
};
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createSnakeAppFactory() { return std::make_unique<SnakeFactory>(); }
} // namespace apps

View File

@@ -2,6 +2,7 @@
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/snake_app.hpp"
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/desktop_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
@@ -29,6 +30,7 @@ int main() {
system.registerApp(apps::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createGameboyAppFactory());
system.registerApp(apps::createSnakeAppFactory());
system.registerApp(apps::createTetrisAppFactory());
system.run();