diff --git a/Makefile b/Makefile index 1c91df25..66380bbc 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,7 @@ ENABLE_BYP_RAW_DEMODULATORS ?= 0 ENABLE_BLMIN_TMP_OFF ?= 0 ENABLE_SCAN_RANGES ?= 1 ENABLE_FEAT_F4HWN ?= 1 +ENABLE_FEAT_F4HWN_GAME ?= 0 ENABLE_FEAT_F4HWN_SCREENSHOT ?= 0 ENABLE_FEAT_F4HWN_SPECTRUM ?= 1 ENABLE_FEAT_F4HWN_RX_TX_TIMER ?= 1 @@ -153,6 +154,9 @@ OBJS += app/menu.o ifeq ($(ENABLE_SPECTRUM), 1) OBJS += app/spectrum.o endif +ifeq ($(ENABLE_FEAT_F4HWN_GAME), 1) +OBJS += app/breakout.o +endif OBJS += app/scanner.o ifeq ($(ENABLE_UART),1) OBJS += app/uart.o @@ -430,6 +434,9 @@ ifeq ($(ENABLE_FEAT_F4HWN),1) else CFLAGS += -DSQL_TONE=550 endif +ifeq ($(ENABLE_FEAT_F4HWN_GAME),1) + CFLAGS += -DENABLE_FEAT_F4HWN_GAME +endif ifeq ($(ENABLE_FEAT_F4HWN_SCREENSHOT),1) CFLAGS += -DENABLE_FEAT_F4HWN_SCREENSHOT endif diff --git a/app/breakout.c b/app/breakout.c new file mode 100644 index 00000000..d42d0684 --- /dev/null +++ b/app/breakout.c @@ -0,0 +1,492 @@ +/* Copyright 2025 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "app/breakout.h" + +static uint32_t randSeed = 1; +static uint8_t blockAnim = 0; + +bool isInitialized = false; +bool isPaused = false; +bool isBeep = false; + +uint8_t levelCountBreackout = 1; + +uint16_t tone = 0; +uint16_t score = 0; + +int16_t ballCount = BALL_NUMBER; + +char str[12]; + +KeyboardState kbd = {KEY_INVALID, KEY_INVALID, 0}; + +Brick brick[BRICK_NUMBER]; +Racket racket; +Ball ball; + +const uint8_t BITMAP_block[4][15] = +{ + { + 0b00011110, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00011110, + }, + { + 0b00011110, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00011110, + }, + { + 0b00011110, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00011110, + }, + { + 0b00011110, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00110001, + 0b00101001, + 0b00100101, + 0b00100011, + 0b00011110, + } +}; + +const uint8_t BITMAP_blockOn[15] = +{ + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, + 0b00111111, +}; + +const uint8_t BITMAP_blockEmpty[15] = +{ + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, +}; + +// Initialise seed +void srand_custom(uint32_t seed) { + randSeed = seed; +} + +// Return pseudo-random from 0 to RAND_MAX (here 32767) +int rand_custom(void) { + randSeed = randSeed * 1103515245 + 12345; + return (randSeed >> 16) & 0x7FFF; // 15 bits +} + +// Return integer from min to max include +int randInt(int min, int max) { + return min + (rand_custom() % (max - min + 1)); +} + +// PlayBeep +void playBeep(uint16_t tone) +{ + //SYSTEM_DelayMs(10); // useless ? + BK4819_PlayTone(tone, true); // 500 Hz ON + AUDIO_AudioPathOn(); + BK4819_ExitTxMute(); + SYSTEM_DelayMs(100); + BK4819_EnterTxMute(); + //SYSTEM_DelayMs(10); // useless ? + AUDIO_AudioPathOff(); +} + +// Draw score +void drawScore() +{ + int16_t ballCurrent = ballCount; + + memset(gStatusLine, 0, sizeof(gStatusLine)); + sprintf(str, "Level %02u", levelCountBreackout); + GUI_DisplaySmallest(str, 0, 1, true, true); + + if(ballCurrent < 0) + { + ballCurrent = 0; + } + + sprintf(str, "Ball %02u", ballCurrent); + GUI_DisplaySmallest(str, 45, 1, true, true); + + sprintf(str, "Score %04u", score); + GUI_DisplaySmallest(str, 88, 1, true, true); +} + +// Init ball +void initBall() { + ball.x = 62; + ball.y = 30; + ball.w = 3; + ball.h = 3; + ball.dx = 0; + ball.dy = 1; + + UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h - 1, true); + UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, true); +} + +// Draw ball +void drawBall() { + UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h -1, false); + UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, false); + + ball.x += ball.dx; + ball.y += ball.dy; + + if (ball.y <= 0) // Up + { + ball.dx = map(randInt(0, 8), 0, 7, 3, -3); + ball.dy = 1; + } + else if (ball.x <= 2) // Left + { + ball.dx = abs(ball.dx); + } + else if (ball.x >= 124) // Right + { + ball.dx = -abs(ball.dx); + } + // And now Down... + if (ball.y == 47) { + if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + racket.w) { + ball.dx = map(racket.x + racket.w - ball.x, 0, racket.w, 3, -3); + ball.dy *= -1; + isBeep = true; + tone = 400; + } + } + else if(ball.y > 49) { + ballCount--; + UI_DisplayClear(); + drawScore(); + isBeep = true; + tone = 800; + if (ballCount < 0) { + ballCount = BALL_NUMBER; + levelCountBreackout = 1; + score = 0; + + initWall(); + drawWall(); + + isPaused = true; + UI_PrintStringSmallBold("GAME OVER", 0, 128, 4); + } + initRacket(); + initBall(); + } + + UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h - 1, true); + UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, true); +} + +// Init wall +void initWall() { + uint8_t offset = 6; + uint8_t i = 0; + uint8_t j = 0; + uint8_t k = 0; + + for (i = 0; i < BRICK_NUMBER; i++) { + if (i % 6 == 0) { + j = 0; + k++; + } + + brick[i].x = offset + (20 * j); + brick[i].y = -8 + 8 * k; + brick[i].w = 14; + brick[i].h = 5; + brick[i].s = randInt(0, 3); + brick[i].destroy = false; + + j++; + } +} + +// Draw wall +void drawWall() { + uint8_t i = 0; + + for (i = 0; i < BRICK_NUMBER; i++) { + if (brick[i].destroy == false) { + if ((ball.x + 1 >= brick[i].x && + ball.x - 1 <= brick[i].x + brick[i].w) && + ((ball.y + 1 >= brick[i].y && + ball.y - 1 <= brick[i].y + brick[i].h))) { + brick[i].destroy = true; + score++; + + ball.dx = map(brick[i].x + brick[i].w - ball.x, 0, brick[i].w, 2, -2); + ball.dy *= -1; + + memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_blockOn, sizeof(BITMAP_blockOn)); + ST7565_BlitLine(brick[i].y / 8); + playBeep(600); + memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_blockEmpty, sizeof(BITMAP_blockEmpty)); + ST7565_BlitLine(brick[i].y / 8); + + if (score % BRICK_NUMBER == 0) { + levelCountBreackout++; + ballCount++; + initWall(); + return; + } + } + + if (brick[i].destroy == false) { + memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_block[(brick[i].s + blockAnim) % 4], sizeof(BITMAP_block[(brick[i].s + blockAnim) % 4])); + } + } + } +} + +// Init racket +void initRacket() { + racket.w = 24; + racket.x = (64) - (racket.w / 2); + racket.y = 50; + racket.h = 2; + racket.p = racket.x; + + UI_DrawRectangleBuffer(gFrameBuffer, racket.x + 1, racket.y, racket.x + racket.w - 2, racket.y + racket.h, true); + UI_DrawLineBuffer(gFrameBuffer, racket.x, racket.y + 1, racket.x + racket.w - 1, racket.y + 1, true); +} + +// Draw racket +void drawRacket() { + if (racket.p != racket.x) { + UI_DrawRectangleBuffer(gFrameBuffer, racket.p + 1, racket.y, racket.p + racket.w - 2, racket.y + racket.h, false); + UI_DrawLineBuffer(gFrameBuffer, racket.p, racket.y + 1, racket.p + racket.w - 1, racket.y + 1, false); + racket.p = racket.x; + UI_DrawRectangleBuffer(gFrameBuffer, racket.x + 1, racket.y, racket.x + racket.w - 2, racket.y + racket.h, true); + UI_DrawLineBuffer(gFrameBuffer, racket.x, racket.y + 1, racket.x + racket.w - 1, racket.y + 1, true); + } +} + +// OnKeyDown +static void OnKeyDown(uint8_t key) +{ + bool wasPaused = isPaused; + + switch (key) + { + case KEY_4: + if(!isPaused && racket.x > 0) + racket.x -= 2; + isPaused = false; + break; + case KEY_0: + if(!isPaused && racket.x < 102) + racket.x += 2; + isPaused = false; + break; + case KEY_MENU: + isPaused = !isPaused; + kbd.counter = 0; + break; + case KEY_EXIT: + isPaused = false; + isInitialized = false; + break; + } + + if(isPaused) + { + UI_PrintStringSmallBold("PAUSE", 0, 128, 4); + } + else if(wasPaused) + { + // Clear the pause text + for(uint8_t i = 0; i < 8; i++) + { + UI_DrawLineBuffer(gFrameBuffer, 32, 32 + i, 96, 32 + i, false); + } + } +} + + +// Key +KEY_Code_t GetKey() +{ + KEY_Code_t btn = KEYBOARD_Poll(); + if (btn == KEY_INVALID && !GPIO_CheckBit(&GPIOC->DATA, GPIOC_PIN_PTT)) + { + btn = KEY_PTT; + } + return btn; +} + +// HandleUserInput +bool HandleUserInput() +{ + // Store previous key state + kbd.prev = kbd.current; + + // Get the current key + kbd.current = GetKey(); + + // Detect valid key press continuation (same key still pressed) + if (kbd.current != KEY_INVALID && kbd.current == kbd.prev) + { + kbd.counter = 1; + } + else + { + kbd.counter = 0; + } + + // Process the key if counter indicates it should be handled + if (kbd.counter == 1) + { + OnKeyDown(kbd.current); + + // Special handling for MENU key + if(kbd.current == KEY_MENU) + { + kbd.counter = 0; + SYSTEM_DelayMs(250); + } + } + + return true; +} + +// Tick +static void Tick() +{ + for(uint8_t i = 0; i < 2; i++) + { + HandleUserInput(); + } +} + +// APP_RunBreakout +void APP_RunBreakout(void) { + static uint8_t swap = 0; + + UI_DisplayClear(); + initWall(); + initRacket(); + initBall(); + memset(gStatusLine, 0, sizeof(gStatusLine)); + isInitialized = true; + + while(isInitialized) + { + Tick(); + if(!isPaused) + { + if(swap == 0) + { + blockAnim = (blockAnim + 1) % 4; + } + + swap = (swap + 1) % 4; + + drawScore(); + drawWall(); + drawRacket(); + drawBall(); + + if(isBeep) + { + playBeep(tone); + isBeep = false; + } + else + { + SYSTEM_DelayMs(40); + } + } + + ST7565_BlitStatusLine(); // blank status line + ST7565_BlitFullScreen(); + } +} \ No newline at end of file diff --git a/app/breakout.h b/app/breakout.h new file mode 100644 index 00000000..28a11935 --- /dev/null +++ b/app/breakout.h @@ -0,0 +1,80 @@ +/* Copyright 2025 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../bitmaps.h" +#include "../board.h" +#include "../bsp/dp32g030/gpio.h" +#include "../driver/bk4819-regs.h" +#include "../driver/bk4819.h" +#include "../driver/gpio.h" +#include "../driver/keyboard.h" +#include "../driver/st7565.h" +#include "../driver/system.h" +#include "../driver/systick.h" +#include "../external/printf/printf.h" +#include "../font.h" +#include "../helper/battery.h" +#include "../misc.h" +#include "../radio.h" +#include "../settings.h" +#include "../ui/helper.h" +#include "../audio.h" +#include +#include +#include +#include + +#define BRICK_NUMBER 18 +#define BALL_NUMBER 5 + +typedef struct { + uint8_t x; // x + uint8_t y; // y + uint8_t w; // width + uint8_t h; // height + uint8_t s; // style + bool destroy; // active, if true, check this button, else bypass +} Brick; + +typedef struct { + int8_t x; // x + uint8_t y; // y + uint8_t w; // width + uint8_t h; // height + uint8_t p; // previous x +} Racket; + +typedef struct { + int8_t x; // x + int8_t y; // y + uint8_t w; // width + uint8_t h; // height + int8_t dx; // move x + int8_t dy; // move y +} Ball; + +typedef struct KeyboardState +{ + KEY_Code_t current; + KEY_Code_t prev; + uint8_t counter; +} KeyboardState; + +void initWall(void); +void drawWall(void); +void initRacket(void); +void drawRacket(void); +void APP_RunBreakout(void); diff --git a/app/main.c b/app/main.c index 2de8caa6..feeff8ac 100644 --- a/app/main.c +++ b/app/main.c @@ -31,6 +31,10 @@ #include "app/spectrum.h" #endif +#ifdef ENABLE_FEAT_F4HWN_GAME +#include "app/breakout.h" +#endif + #include "audio.h" #include "board.h" #include "driver/bk4819.h" @@ -582,6 +586,13 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) ACTION_BackLight(); return; } + #ifdef ENABLE_FEAT_F4HWN_GAME + else if(Key == 7) + { + APP_RunBreakout(); + return; + } + #endif processFKeyFunction(Key, true); } diff --git a/compile-with-docker.sh b/compile-with-docker.sh index d9ce5a91..21ef5189 100755 --- a/compile-with-docker.sh +++ b/compile-with-docker.sh @@ -37,6 +37,7 @@ bandscope() { ENABLE_FMRADIO=0 \ ENABLE_VOX=0 \ ENABLE_AIRCOPY=1 \ + ENABLE_FEAT_F4HWN_GAME=0 \ ENABLE_FEAT_F4HWN_PMR=1 \ ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \ ENABLE_NOAA=0 \ @@ -53,6 +54,7 @@ broadcast() { ENABLE_FMRADIO=1 \ ENABLE_VOX=1 \ ENABLE_AIRCOPY=1 \ + ENABLE_FEAT_F4HWN_GAME=0 \ ENABLE_FEAT_F4HWN_PMR=1 \ ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \ ENABLE_NOAA=0 \ @@ -69,11 +71,12 @@ basic() { ENABLE_FMRADIO=1 \ ENABLE_VOX=0 \ ENABLE_AIRCOPY=0 \ - ENABLE_AUDIO_BAR=0 \ + ENABLE_FEAT_F4HWN_GAME=0 \ ENABLE_FEAT_F4HWN_SPECTRUM=0 \ ENABLE_FEAT_F4HWN_PMR=1 \ ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \ ENABLE_NOAA=0 \ + ENABLE_AUDIO_BAR=0 \ ENABLE_FEAT_F4HWN_RESUME_STATE=0 \ ENABLE_FEAT_F4HWN_CHARGING_C=0 \ ENABLE_FEAT_F4HWN_INV=1 \ @@ -92,6 +95,7 @@ rescueops() { ENABLE_FMRADIO=0 \ ENABLE_VOX=1 \ ENABLE_AIRCOPY=1 \ + ENABLE_FEAT_F4HWN_GAME=0 \ ENABLE_FEAT_F4HWN_PMR=1 \ ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \ ENABLE_NOAA=1 \ @@ -101,6 +105,23 @@ rescueops() { && cp f4hwn.rescueops* compiled-firmware/" } +game() { + echo "Game compilation..." + docker run --rm -v "${PWD}/compiled-firmware:/app/compiled-firmware" $IMAGE_NAME /bin/bash -c "cd /app && make -s \ + ENABLE_SPECTRUM=0 \ + ENABLE_FMRADIO=1 \ + ENABLE_VOX=1 \ + ENABLE_AIRCOPY=0 \ + ENABLE_FEAT_F4HWN_GAME=1 \ + ENABLE_FEAT_F4HWN_PMR=1 \ + ENABLE_FEAT_F4HWN_GMRS_FRS_MURS=1 \ + ENABLE_NOAA=0 \ + EDITION_STRING=Game \ + ENABLE_FEAT_F4HWN_RESCUE_OPS=0 \ + TARGET=f4hwn.game \ + && cp f4hwn.game* compiled-firmware/" +} + case "$1" in custom) custom @@ -120,14 +141,18 @@ case "$1" in rescueops) rescueops ;; + game) + game + ;; all) bandscope broadcast - rescueops basic + rescueops + game ;; *) - echo "Usage: $0 {custom|bandscope|broadcast|basic|standard|all}" + echo "Usage: $0 {custom|bandscope|broadcast|basic|rescueops|game|standard|all}" exit 1 ;; esac \ No newline at end of file diff --git a/ui/menu.c b/ui/menu.c index 0c5d0796..a4ad6d9d 100644 --- a/ui/menu.c +++ b/ui/menu.c @@ -1227,6 +1227,8 @@ void UI_DisplayMenu(void) #if defined(ENABLE_SPECTRUM) && defined(ENABLE_FMRADIO) UI_PrintStringSmallNormal("Basic", 54, 127, 6); + #elif defined(ENABLE_FEAT_F4HWN_GAME) + UI_PrintStringSmallNormal("Game", 54, 127, 6); #elif defined(ENABLE_SPECTRUM) UI_PrintStringSmallNormal("Bandscope", 54, 127, 6); #elif defined(ENABLE_FMRADIO)