mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
Compare commits
4 Commits
sound
...
ecbcce12ea
| Author | SHA1 | Date | |
|---|---|---|---|
| ecbcce12ea | |||
| f6c800fc63 | |||
| 5e63875d35 | |||
| cc805abe80 |
@@ -20,7 +20,7 @@
|
||||
#define DISP_WIDTH cardboy::sdk::kDisplayWidth
|
||||
#define DISP_HEIGHT cardboy::sdk::kDisplayHeight
|
||||
|
||||
#define BUZZER_PIN GPIO_NUM_22
|
||||
#define BUZZER_PIN GPIO_NUM_25
|
||||
|
||||
#define PWR_INT GPIO_NUM_10
|
||||
#define PWR_KILL GPIO_NUM_12
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -16,4 +16,5 @@ add_subdirectory(menu)
|
||||
add_subdirectory(clock)
|
||||
add_subdirectory(settings)
|
||||
add_subdirectory(gameboy)
|
||||
add_subdirectory(snake)
|
||||
add_subdirectory(tetris)
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/gameboy_app.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/minigb_apu/minigb_apu.c
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/apps/peanut_gb.h
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_compile_definitions(cardboy_apps
|
||||
PRIVATE
|
||||
MINIGB_APU_AUDIO_FORMAT_S16SYS=1
|
||||
)
|
||||
@@ -1,542 +0,0 @@
|
||||
/**
|
||||
* Game Boy APU emulator.
|
||||
* Copyright (c) 2019 Mahyar Koshkouei <mk@deltabeard.com>
|
||||
* Copyright (c) 2017 Alex Baines <alex@abaines.me.uk>
|
||||
* minigb_apu is released under the terms of the MIT license.
|
||||
*
|
||||
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
|
||||
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "minigb_apu.h"
|
||||
|
||||
#define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ)
|
||||
#define AUDIO_NSAMPLES (AUDIO_SAMPLES_TOTAL)
|
||||
|
||||
#define MAX(a, b) ( a > b ? a : b )
|
||||
#define MIN(a, b) ( a <= b ? a : b )
|
||||
|
||||
/* Factor in which values are multiplied to compensate for fixed-point
|
||||
* arithmetic. Some hard-coded values in this project must be recreated. */
|
||||
#ifndef FREQ_INC_MULT
|
||||
# define FREQ_INC_MULT 105
|
||||
#endif
|
||||
/* Handles time keeping for sound generation.
|
||||
* FREQ_INC_REF must be equal to, or larger than AUDIO_SAMPLE_RATE in order
|
||||
* to avoid a division by zero error.
|
||||
* Using a square of 2 simplifies calculations. */
|
||||
#define FREQ_INC_REF (AUDIO_SAMPLE_RATE * FREQ_INC_MULT)
|
||||
|
||||
#define MAX_CHAN_VOLUME 15
|
||||
|
||||
static void set_note_freq(struct chan *c)
|
||||
{
|
||||
/* Lowest expected value of freq is 64. */
|
||||
uint32_t freq = (DMG_CLOCK_FREQ_U / 4) / (2048 - c->freq);
|
||||
c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
static void chan_enable(struct minigb_apu_ctx *ctx,
|
||||
const uint_fast8_t i, const bool enable)
|
||||
{
|
||||
uint8_t val;
|
||||
|
||||
ctx->chans[i].enabled = enable;
|
||||
val = (ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) |
|
||||
(ctx->chans[3].enabled << 3) | (ctx->chans[2].enabled << 2) |
|
||||
(ctx->chans[1].enabled << 1) | (ctx->chans[0].enabled << 0);
|
||||
|
||||
ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val;
|
||||
}
|
||||
|
||||
static void update_env(struct chan *c)
|
||||
{
|
||||
c->env.counter += c->env.inc;
|
||||
|
||||
while (c->env.counter > FREQ_INC_REF) {
|
||||
if (c->env.step) {
|
||||
c->volume += c->env.up ? 1 : -1;
|
||||
if (c->volume == 0 || c->volume == MAX_CHAN_VOLUME) {
|
||||
c->env.inc = 0;
|
||||
}
|
||||
c->volume = MAX(0, MIN(MAX_CHAN_VOLUME, c->volume));
|
||||
}
|
||||
c->env.counter -= FREQ_INC_REF;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_len(struct minigb_apu_ctx *ctx, struct chan *c)
|
||||
{
|
||||
if (!c->len.enabled)
|
||||
return;
|
||||
|
||||
c->len.counter += c->len.inc;
|
||||
if (c->len.counter > FREQ_INC_REF) {
|
||||
chan_enable(ctx, c - ctx->chans, 0);
|
||||
c->len.counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool update_freq(struct chan *c, uint32_t *pos)
|
||||
{
|
||||
uint32_t inc = c->freq_inc - *pos;
|
||||
c->freq_counter += inc;
|
||||
|
||||
if (c->freq_counter > FREQ_INC_REF) {
|
||||
*pos = c->freq_inc - (c->freq_counter - FREQ_INC_REF);
|
||||
c->freq_counter = 0;
|
||||
return true;
|
||||
} else {
|
||||
*pos = c->freq_inc;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_sweep(struct chan *c)
|
||||
{
|
||||
c->sweep.counter += c->sweep.inc;
|
||||
|
||||
while (c->sweep.counter > FREQ_INC_REF) {
|
||||
if (c->sweep.shift) {
|
||||
uint16_t inc = (c->sweep.freq >> c->sweep.shift);
|
||||
if (c->sweep.down)
|
||||
inc *= -1;
|
||||
|
||||
c->freq = c->sweep.freq + inc;
|
||||
if (c->freq > 2047) {
|
||||
c->enabled = 0;
|
||||
} else {
|
||||
set_note_freq(c);
|
||||
c->sweep.freq = c->freq;
|
||||
}
|
||||
} else if (c->sweep.rate) {
|
||||
c->enabled = 0;
|
||||
}
|
||||
c->sweep.counter -= FREQ_INC_REF;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_square(struct minigb_apu_ctx *ctx, audio_sample_t *samples,
|
||||
const bool ch2)
|
||||
{
|
||||
struct chan *c = &ctx->chans[ch2];
|
||||
|
||||
if (!c->powered || !c->enabled)
|
||||
return;
|
||||
|
||||
set_note_freq(c);
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(ctx, c);
|
||||
if (!c->enabled)
|
||||
return;
|
||||
|
||||
update_env(c);
|
||||
if (!c->volume)
|
||||
continue;
|
||||
|
||||
if (!ch2)
|
||||
update_sweep(c);
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
int32_t sample = 0;
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->square.duty_counter = (c->square.duty_counter + 1) & 7;
|
||||
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
|
||||
c->val = (c->square.duty & (1 << c->square.duty_counter)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
sample += c->val;
|
||||
sample *= c->volume;
|
||||
sample /= 4;
|
||||
|
||||
samples[i + 0] += sample * c->on_left * ctx->vol_l;
|
||||
samples[i + 1] += sample * c->on_right * ctx->vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t wave_sample(struct minigb_apu_ctx *ctx,
|
||||
const unsigned int pos, const unsigned int volume)
|
||||
{
|
||||
uint8_t sample;
|
||||
|
||||
sample = ctx->audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION];
|
||||
if (pos & 1) {
|
||||
sample &= 0xF;
|
||||
} else {
|
||||
sample >>= 4;
|
||||
}
|
||||
return volume ? (sample >> (volume - 1)) : 0;
|
||||
}
|
||||
|
||||
static void update_wave(struct minigb_apu_ctx *ctx, audio_sample_t *samples)
|
||||
{
|
||||
struct chan *c = &ctx->chans[2];
|
||||
|
||||
if (!c->powered || !c->enabled || !c->volume)
|
||||
return;
|
||||
|
||||
set_note_freq(c);
|
||||
c->freq_inc *= 2;
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(ctx, c);
|
||||
if (!c->enabled)
|
||||
return;
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
audio_sample_t sample = 0;
|
||||
|
||||
c->wave.sample = wave_sample(ctx, c->val, c->volume);
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->val = (c->val + 1) & 31;
|
||||
sample += ((pos - prev_pos) / c->freq_inc) *
|
||||
((audio_sample_t)c->wave.sample - 8) *
|
||||
(AUDIO_SAMPLE_MAX/64);
|
||||
c->wave.sample = wave_sample(ctx, c->val, c->volume);
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
sample += ((audio_sample_t)c->wave.sample - 8) *
|
||||
(audio_sample_t)(AUDIO_SAMPLE_MAX/64);
|
||||
{
|
||||
/* First element is unused. */
|
||||
audio_sample_t div[] = { AUDIO_SAMPLE_MAX, 1, 2, 4 };
|
||||
sample = sample / (div[c->volume]);
|
||||
}
|
||||
|
||||
sample /= 4;
|
||||
samples[i + 0] += sample * c->on_left * ctx->vol_l;
|
||||
samples[i + 1] += sample * c->on_right * ctx->vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
static void update_noise(struct minigb_apu_ctx *ctx, audio_sample_t *samples)
|
||||
{
|
||||
struct chan *c = &ctx->chans[3];
|
||||
|
||||
if (c->freq >= 14)
|
||||
c->enabled = 0;
|
||||
|
||||
if (!c->powered || !c->enabled)
|
||||
return;
|
||||
|
||||
{
|
||||
const uint32_t lfsr_div_lut[] = {
|
||||
8, 16, 32, 48, 64, 80, 96, 112
|
||||
};
|
||||
uint32_t freq;
|
||||
|
||||
freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq);
|
||||
c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
|
||||
update_len(ctx, c);
|
||||
if (!c->enabled)
|
||||
return;
|
||||
|
||||
update_env(c);
|
||||
if (!c->volume)
|
||||
continue;
|
||||
|
||||
uint32_t pos = 0;
|
||||
uint32_t prev_pos = 0;
|
||||
int32_t sample = 0;
|
||||
|
||||
while (update_freq(c, &pos)) {
|
||||
c->noise.lfsr_reg = (c->noise.lfsr_reg << 1) |
|
||||
(c->val >= VOL_INIT_MAX/MAX_CHAN_VOLUME);
|
||||
|
||||
if (c->noise.lfsr_wide) {
|
||||
c->val = !(((c->noise.lfsr_reg >> 14) & 1) ^
|
||||
((c->noise.lfsr_reg >> 13) & 1)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
} else {
|
||||
c->val = !(((c->noise.lfsr_reg >> 6) & 1) ^
|
||||
((c->noise.lfsr_reg >> 5) & 1)) ?
|
||||
VOL_INIT_MAX / MAX_CHAN_VOLUME :
|
||||
VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
}
|
||||
|
||||
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
|
||||
prev_pos = pos;
|
||||
}
|
||||
|
||||
sample += c->val;
|
||||
sample *= c->volume;
|
||||
sample /= 4;
|
||||
|
||||
samples[i + 0] += sample * c->on_left * ctx->vol_l;
|
||||
samples[i + 1] += sample * c->on_right * ctx->vol_r;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SDL2 style audio callback function.
|
||||
*/
|
||||
void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx,
|
||||
audio_sample_t *stream)
|
||||
{
|
||||
memset(stream, 0, AUDIO_SAMPLES_TOTAL * sizeof(audio_sample_t));
|
||||
update_square(ctx, stream, 0);
|
||||
update_square(ctx, stream, 1);
|
||||
update_wave(ctx, stream);
|
||||
update_noise(ctx, stream);
|
||||
}
|
||||
|
||||
static void chan_trigger(struct minigb_apu_ctx *ctx, uint_fast8_t i)
|
||||
{
|
||||
struct chan *c = &ctx->chans[i];
|
||||
|
||||
chan_enable(ctx, i, 1);
|
||||
c->volume = c->volume_init;
|
||||
|
||||
// volume envelope
|
||||
{
|
||||
/* LUT created in Julia with:
|
||||
* `(FREQ_INC_MULT * 64)./vcat(8, 1:7)`
|
||||
* Must be recreated when FREQ_INC_MULT modified.
|
||||
*/
|
||||
const uint32_t inc_lut[8] = {
|
||||
#if FREQ_INC_MULT == 16
|
||||
128, 1024, 512, 341,
|
||||
256, 205, 171, 146
|
||||
#elif FREQ_INC_MULT == 64
|
||||
512, 4096, 2048, 1365,
|
||||
1024, 819, 683, 585
|
||||
#elif FREQ_INC_MULT == 105
|
||||
/* Multiples of 105 provide integer values. */
|
||||
840, 6720, 3360, 2240,
|
||||
1680, 1344, 1120, 960
|
||||
#else
|
||||
#error "LUT not calculated for this value of FREQ_INC_MULT"
|
||||
#endif
|
||||
};
|
||||
uint8_t val;
|
||||
|
||||
val = ctx->audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION];
|
||||
|
||||
c->env.step = val & 0x7;
|
||||
c->env.up = val & 0x8;
|
||||
c->env.inc = inc_lut[c->env.step];
|
||||
c->env.counter = 0;
|
||||
}
|
||||
|
||||
// freq sweep
|
||||
if (i == 0) {
|
||||
uint8_t val = ctx->audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION];
|
||||
|
||||
c->sweep.freq = c->freq;
|
||||
c->sweep.rate = (val >> 4) & 0x07;
|
||||
c->sweep.down = (val & 0x08);
|
||||
c->sweep.shift = (val & 0x07);
|
||||
c->sweep.inc = c->sweep.rate ?
|
||||
((128u * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0;
|
||||
c->sweep.counter = FREQ_INC_REF;
|
||||
}
|
||||
|
||||
int len_max = 64;
|
||||
|
||||
if (i == 2) { // wave
|
||||
len_max = 256;
|
||||
c->val = 0;
|
||||
} else if (i == 3) { // noise
|
||||
c->noise.lfsr_reg = 0xFFFF;
|
||||
c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME;
|
||||
}
|
||||
|
||||
c->len.inc = (256u * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load));
|
||||
c->len.counter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read audio register.
|
||||
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
|
||||
* This is not checked in this function.
|
||||
* \return Byte at address.
|
||||
*/
|
||||
uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr)
|
||||
{
|
||||
static const uint8_t ortab[] = {
|
||||
0x80, 0x3f, 0x00, 0xff, 0xbf,
|
||||
0xff, 0x3f, 0x00, 0xff, 0xbf,
|
||||
0x7f, 0xff, 0x9f, 0xff, 0xbf,
|
||||
0xff, 0xff, 0x00, 0x00, 0xbf,
|
||||
0x00, 0x00, 0x70,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
return ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] |
|
||||
ortab[addr - AUDIO_ADDR_COMPENSATION];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write audio register.
|
||||
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
|
||||
* This is not checked in this function.
|
||||
* \param val Byte to write at address.
|
||||
*/
|
||||
void minigb_apu_audio_write(struct minigb_apu_ctx *ctx,
|
||||
const uint16_t addr, const uint8_t val)
|
||||
{
|
||||
/* Find sound channel corresponding to register address. */
|
||||
uint_fast8_t i;
|
||||
|
||||
if(addr == 0xFF26)
|
||||
{
|
||||
ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80;
|
||||
/* On APU power off, clear all registers apart from wave
|
||||
* RAM. */
|
||||
if((val & 0x80) == 0)
|
||||
{
|
||||
memset(ctx->audio_mem,
|
||||
0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION);
|
||||
ctx->chans[0].enabled = false;
|
||||
ctx->chans[1].enabled = false;
|
||||
ctx->chans[2].enabled = false;
|
||||
ctx->chans[3].enabled = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* Ignore register writes if APU powered off. */
|
||||
if(ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00)
|
||||
return;
|
||||
|
||||
ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val;
|
||||
i = (addr - AUDIO_ADDR_COMPENSATION) / 5;
|
||||
|
||||
switch (addr) {
|
||||
case 0xFF12:
|
||||
case 0xFF17:
|
||||
case 0xFF21: {
|
||||
ctx->chans[i].volume_init = val >> 4;
|
||||
ctx->chans[i].powered = (val >> 3) != 0;
|
||||
|
||||
// "zombie mode" stuff, needed for Prehistorik Man and probably
|
||||
// others
|
||||
if (ctx->chans[i].powered && ctx->chans[i].enabled) {
|
||||
if ((ctx->chans[i].env.step == 0 && ctx->chans[i].env.inc != 0)) {
|
||||
if (val & 0x08) {
|
||||
ctx->chans[i].volume++;
|
||||
} else {
|
||||
ctx->chans[i].volume += 2;
|
||||
}
|
||||
} else {
|
||||
ctx->chans[i].volume = 16 - ctx->chans[i].volume;
|
||||
}
|
||||
|
||||
ctx->chans[i].volume &= 0x0F;
|
||||
ctx->chans[i].env.step = val & 0x07;
|
||||
}
|
||||
} break;
|
||||
|
||||
case 0xFF1C:
|
||||
ctx->chans[i].volume = ctx->chans[i].volume_init = (val >> 5) & 0x03;
|
||||
break;
|
||||
|
||||
case 0xFF11:
|
||||
case 0xFF16:
|
||||
case 0xFF20: {
|
||||
const uint8_t duty_lookup[] = { 0x10, 0x30, 0x3C, 0xCF };
|
||||
ctx->chans[i].len.load = val & 0x3f;
|
||||
ctx->chans[i].square.duty = duty_lookup[val >> 6];
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xFF1B:
|
||||
ctx->chans[i].len.load = val;
|
||||
break;
|
||||
|
||||
case 0xFF13:
|
||||
case 0xFF18:
|
||||
case 0xFF1D:
|
||||
ctx->chans[i].freq &= 0xFF00;
|
||||
ctx->chans[i].freq |= val;
|
||||
break;
|
||||
|
||||
case 0xFF1A:
|
||||
ctx->chans[i].powered = (val & 0x80) != 0;
|
||||
chan_enable(ctx, i, val & 0x80);
|
||||
break;
|
||||
|
||||
case 0xFF14:
|
||||
case 0xFF19:
|
||||
case 0xFF1E:
|
||||
ctx->chans[i].freq &= 0x00FF;
|
||||
ctx->chans[i].freq |= ((val & 0x07) << 8);
|
||||
/* Intentional fall-through. */
|
||||
case 0xFF23:
|
||||
ctx->chans[i].len.enabled = val & 0x40;
|
||||
if (val & 0x80)
|
||||
chan_trigger(ctx, i);
|
||||
|
||||
break;
|
||||
|
||||
case 0xFF22:
|
||||
ctx->chans[3].freq = val >> 4;
|
||||
ctx->chans[3].noise.lfsr_wide = !(val & 0x08);
|
||||
ctx->chans[3].noise.lfsr_div = val & 0x07;
|
||||
break;
|
||||
|
||||
case 0xFF24:
|
||||
{
|
||||
ctx->vol_l = ((val >> 4) & 0x07);
|
||||
ctx->vol_r = (val & 0x07);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0xFF25:
|
||||
for (uint_fast8_t j = 0; j < 4; j++) {
|
||||
ctx->chans[j].on_left = (val >> (4 + j)) & 1;
|
||||
ctx->chans[j].on_right = (val >> j) & 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void minigb_apu_audio_init(struct minigb_apu_ctx *ctx)
|
||||
{
|
||||
/* Initialise channels and samples. */
|
||||
memset(ctx->chans, 0, sizeof(ctx->chans));
|
||||
ctx->chans[0].val = ctx->chans[1].val = -1;
|
||||
|
||||
/* Initialise IO registers. */
|
||||
{
|
||||
const uint8_t regs_init[] = { 0x80, 0xBF, 0xF3, 0xFF, 0x3F,
|
||||
0xFF, 0x3F, 0x00, 0xFF, 0x3F,
|
||||
0x7F, 0xFF, 0x9F, 0xFF, 0x3F,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x3F,
|
||||
0x77, 0xF3, 0xF1 };
|
||||
|
||||
for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i)
|
||||
minigb_apu_audio_write(ctx, 0xFF10 + i, regs_init[i]);
|
||||
}
|
||||
|
||||
/* Initialise Wave Pattern RAM. */
|
||||
{
|
||||
const uint8_t wave_init[] = { 0xac, 0xdd, 0xda, 0x48,
|
||||
0x36, 0x02, 0xcf, 0x16,
|
||||
0x2c, 0x04, 0xe5, 0x2c,
|
||||
0xac, 0xdd, 0xda, 0x48 };
|
||||
|
||||
for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i)
|
||||
minigb_apu_audio_write(ctx, 0xFF30 + i, wave_init[i]);
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* minigb_apu is released under the terms listed within the LICENSE file.
|
||||
*
|
||||
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
|
||||
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef AUDIO_SAMPLE_RATE
|
||||
# define AUDIO_SAMPLE_RATE 20000
|
||||
#endif
|
||||
|
||||
/* The audio output format is in platform native endian. */
|
||||
#if defined(MINIGB_APU_AUDIO_FORMAT_S16SYS)
|
||||
typedef int16_t audio_sample_t;
|
||||
# define AUDIO_SAMPLE_MAX INT16_MAX
|
||||
# define AUDIO_SAMPLE_MIN INT16_MIN
|
||||
# define VOL_INIT_MAX (AUDIO_SAMPLE_MAX/8)
|
||||
# define VOL_INIT_MIN (AUDIO_SAMPLE_MIN/8)
|
||||
#elif defined(MINIGB_APU_AUDIO_FORMAT_S32SYS)
|
||||
typedef int32_t audio_sample_t;
|
||||
# define AUDIO_SAMPLE_MAX INT32_MAX
|
||||
# define AUDIO_SAMPLE_MIN INT32_MIN
|
||||
# define VOL_INIT_MAX (INT32_MAX/8)
|
||||
# define VOL_INIT_MIN (INT32_MIN/8)
|
||||
#else
|
||||
#error MiniGB APU: Invalid or unsupported audio format selected
|
||||
#endif
|
||||
|
||||
#define DMG_CLOCK_FREQ 4194304.0
|
||||
#define SCREEN_REFRESH_CYCLES 70224.0
|
||||
#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES)
|
||||
|
||||
/* Number of audio samples in each channel. */
|
||||
#define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC))
|
||||
/* Number of audio channels. The audio output is in interleaved stereo format.*/
|
||||
#define AUDIO_CHANNELS 2
|
||||
/* Number of audio samples output in each audio_callback call. */
|
||||
#define AUDIO_SAMPLES_TOTAL (AUDIO_SAMPLES * 2)
|
||||
|
||||
#define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1)
|
||||
#define AUDIO_ADDR_COMPENSATION 0xFF10
|
||||
|
||||
struct chan_len_ctr {
|
||||
uint8_t load;
|
||||
uint8_t enabled;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan_vol_env {
|
||||
uint8_t step;
|
||||
uint8_t up;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan_freq_sweep {
|
||||
uint8_t rate;
|
||||
uint8_t shift;
|
||||
uint8_t down;
|
||||
uint16_t freq;
|
||||
uint32_t counter;
|
||||
uint32_t inc;
|
||||
};
|
||||
|
||||
struct chan {
|
||||
uint8_t enabled;
|
||||
uint8_t powered;
|
||||
uint8_t on_left;
|
||||
uint8_t on_right;
|
||||
|
||||
uint8_t volume;
|
||||
uint8_t volume_init;
|
||||
|
||||
uint16_t freq;
|
||||
uint32_t freq_counter;
|
||||
uint32_t freq_inc;
|
||||
|
||||
int32_t val;
|
||||
|
||||
struct chan_len_ctr len;
|
||||
struct chan_vol_env env;
|
||||
struct chan_freq_sweep sweep;
|
||||
|
||||
union {
|
||||
struct {
|
||||
uint8_t duty;
|
||||
uint8_t duty_counter;
|
||||
} square;
|
||||
struct {
|
||||
uint16_t lfsr_reg;
|
||||
uint8_t lfsr_wide;
|
||||
uint8_t lfsr_div;
|
||||
} noise;
|
||||
struct {
|
||||
uint8_t sample;
|
||||
} wave;
|
||||
};
|
||||
};
|
||||
|
||||
struct minigb_apu_ctx {
|
||||
struct chan chans[4];
|
||||
int32_t vol_l, vol_r;
|
||||
|
||||
/**
|
||||
* Memory holding audio registers between 0xFF10 and 0xFF3F inclusive.
|
||||
*/
|
||||
uint8_t audio_mem[AUDIO_MEM_SIZE];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fill allocated buffer "stream" with AUDIO_SAMPLES_TOTAL number of 16-bit
|
||||
* signed samples (native endian order) in stereo interleaved format.
|
||||
* Each call corresponds to the time taken for each VSYNC in the Game Boy.
|
||||
*
|
||||
* \param ctx Library context. Must be initialised with audio_init().
|
||||
* \param stream Allocated pointer to store audio samples. Must be at least
|
||||
* AUDIO_SAMPLES_TOTAL in size.
|
||||
*/
|
||||
void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx,
|
||||
audio_sample_t *stream);
|
||||
|
||||
/**
|
||||
* Read audio register at given address "addr".
|
||||
* \param ctx Library context. Must be initialised with audio_init().
|
||||
* \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F,
|
||||
* inclusive.
|
||||
*/
|
||||
uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr);
|
||||
|
||||
/**
|
||||
* Write "val" to audio register at given address "addr".
|
||||
* \param ctx Library context. Must be initialised with audio_init().
|
||||
* \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F,
|
||||
* inclusive.
|
||||
* \param val Value to write to address.
|
||||
*/
|
||||
void minigb_apu_audio_write(struct minigb_apu_ctx *ctx,
|
||||
const uint16_t addr, const uint8_t val);
|
||||
|
||||
/**
|
||||
* Initialise audio driver.
|
||||
* \param ctx Library context.
|
||||
*/
|
||||
void minigb_apu_audio_init(struct minigb_apu_ctx *ctx);
|
||||
File diff suppressed because it is too large
Load Diff
9
Firmware/sdk/apps/snake/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/snake/CMakeLists.txt
Normal 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
|
||||
)
|
||||
11
Firmware/sdk/apps/snake/include/cardboy/apps/snake_app.hpp
Normal file
11
Firmware/sdk/apps/snake/include/cardboy/apps/snake_app.hpp
Normal 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
|
||||
432
Firmware/sdk/apps/snake/src/snake_app.cpp
Normal file
432
Firmware/sdk/apps/snake/src/snake_app.cpp
Normal 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
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user