diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 9218e2cf5..d43492ad3 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -4,10 +4,14 @@ set(EXTCPPSRC external/tetris/main.cpp external/tetris/ui_tetris.cpp - #tetris + #breakout external/breakout/main.cpp external/breakout/ui_breakout.cpp + #snake + external/snake/main.cpp + external/snake/ui_snake.cpp + #afsk_rx external/afsk_rx/main.cpp external/afsk_rx/ui_afsk_rx.cpp @@ -192,6 +196,7 @@ set(EXTAPPLIST keyfob tetris breakout + snake extsensors foxhunt_rx audio_test diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index aac524cb9..a341f60b8 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -38,7 +38,6 @@ MEMORY ram_external_app_keyfob(rwx) : org = 0xADBD0000, len = 32k ram_external_app_tetris(rwx) : org = 0xADBE0000, len = 32k ram_external_app_extsensors(rwx) : org = 0xADBF0000, len = 32k - ram_external_app_breakout(rwx) : org = 0xADE00000, len = 32k ram_external_app_foxhunt_rx(rwx) : org = 0xADC00000, len = 32k ram_external_app_audio_test(rwx) : org = 0xADC10000, len = 32k ram_external_app_wardrivemap(rwx) : org = 0xADC20000, len = 32k @@ -65,6 +64,8 @@ MEMORY ram_external_app_view_wav(rwx) : org = 0xADD70000, len = 32k ram_external_app_sd_wipe(rwx) : org = 0xADD80000, len = 32k ram_external_app_playlist_editor(rwx) : org = 0xADD90000, len = 32k + ram_external_app_breakout(rwx) : org = 0xADDA0000, len = 32k + ram_external_app_snake(rwx) : org = 0xADDB0000, len = 32k } SECTIONS @@ -159,6 +160,12 @@ SECTIONS *(*ui*external_app*breakout*); } > ram_external_app_breakout + .external_app_snake : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_snake.application_information)); + *(*ui*external_app*snake*); + } > ram_external_app_snake + .external_app_extsensors : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_extsensors.application_information)); diff --git a/firmware/application/external/snake/Arial12x12.h b/firmware/application/external/snake/Arial12x12.h new file mode 100644 index 000000000..4dc451eeb --- /dev/null +++ b/firmware/application/external/snake/Arial12x12.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 Mark Thompson + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +// dummy include file to avoid changing original source + +#ifndef __UI_Arial12x12_H__ +#define __UI_Arial12x12_H__ + +#define Arial12x12 (0) + +#endif /*__UI_Arial12x12_H__*/ diff --git a/firmware/application/external/snake/SPI_TFT_ILI9341.h b/firmware/application/external/snake/SPI_TFT_ILI9341.h new file mode 100644 index 000000000..0f7db6feb --- /dev/null +++ b/firmware/application/external/snake/SPI_TFT_ILI9341.h @@ -0,0 +1,68 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#ifndef __UI_SPI_TFT_ILI9341_H__ +#define __UI_SPI_TFT_ILI9341_H__ + +ui::Painter painter; + +static int bg_color; + +enum { + White, + Blue, + Yellow, + Purple, + Green, + Red, + Maroon, + Orange, + Black, +}; + +static const Color pp_colors[] = { + Color::white(), + Color::blue(), + Color::yellow(), + Color::purple(), + Color::green(), + Color::red(), + Color::magenta(), + Color::orange(), + Color::black(), +}; + +static void claim(__FILE* x) { + (void)x; +}; + +static void cls() { + painter.fill_rectangle({0, 0, portapack::display.width(), portapack::display.height()}, Color::black()); +}; + +static void background(int color) { + bg_color = color; +}; + +static void set_orientation(int x) { + (void)x; +}; + +static void set_font(unsigned char* x) { + (void)x; +}; + +static void fillrect(int x1, int y1, int x2, int y2, int color) { + painter.fill_rectangle({x1, y1, x2 - x1, y2 - y1}, pp_colors[color]); +}; + +static void rect(int x1, int y1, int x2, int y2, int color) { + painter.draw_rectangle({x1, y1, x2 - x1, y2 - y1}, pp_colors[color]); +}; + +#endif /*__UI_SPI_TFT_ILI9341_H__*/ \ No newline at end of file diff --git a/firmware/application/external/snake/main.cpp b/firmware/application/external/snake/main.cpp new file mode 100644 index 000000000..106cffcea --- /dev/null +++ b/firmware/application/external/snake/main.cpp @@ -0,0 +1,67 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#include "ui.hpp" +#include "ui_snake.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::snake { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::snake + +extern "C" { + +__attribute__((section(".external_app.app_snake.application_information"), used)) application_information_t _application_information_snake = { + (uint8_t*)0x00000000, + ui::external_app::snake::initialize_app, + CURRENT_HEADER_VERSION, + VERSION_MD5, + "Snake", + { + 0x00, + 0x00, + 0x7E, + 0x42, + 0x42, + 0x42, + 0x7E, + 0x00, + 0x00, + 0x7E, + 0x42, + 0x42, + 0x42, + 0x7E, + 0x00, + 0x00, + 0x00, + 0x7E, + 0x42, + 0x42, + 0x42, + 0x7E, + 0x00, + 0x00, + 0x7E, + 0x42, + 0x42, + 0x42, + 0x7E, + 0x00, + 0x00, + }, + ui::Color::green().v, + app_location_t::GAMES, + -1, + {0, 0, 0, 0}, + 0x00000000, +}; +} \ No newline at end of file diff --git a/firmware/application/external/snake/mbed.h b/firmware/application/external/snake/mbed.h new file mode 100644 index 000000000..4d75f8c33 --- /dev/null +++ b/firmware/application/external/snake/mbed.h @@ -0,0 +1,98 @@ +#ifndef __UI_mbed_H__ +#define __UI_mbed_H__ + +using Callback = void (*)(void); + +#define wait_us(x) (void)0 +#define wait(x) chThdSleepMilliseconds(x * 1000) +#define PullUp 1 + +#include "ui_navigation.hpp" + +enum { + dp0, + dp1, + dp2, + dp3, + dp4, + dp5, + dp6, + dp7, + dp8, + dp9, + dp10, + dp11, + dp12, + dp13, + dp14, + dp15, + dp16, + dp17, + dp18, + dp19, + dp20, + dp21, + dp22, + dp23, + dp24, + dp25, +}; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +static bool but_RIGHT; +static bool but_LEFT; +#pragma GCC diagnostic pop +static bool but_SELECT; + +class Timer { + public: + Timer() { (void)0; }; + void reset() { (void)0; }; + void start() { (void)0; } + uint32_t read_ms() { return 1000; }; + + private: +}; + +static Callback game_update_callback; +static uint32_t game_update_timeout; +static uint32_t game_update_counter; + +static void check_game_timer() { + if (game_update_callback) { + if (++game_update_counter >= game_update_timeout) { + game_update_counter = 0; + game_update_callback(); + } + } +} + +class Ticker { + public: + Ticker() { (void)0; }; + void attach(Callback func, double delay_sec) { + game_update_callback = func; + game_update_timeout = delay_sec * 60; + } + void detach() { + game_update_callback = nullptr; + } + + private: +}; + +static Callback button_callback; + +class InterruptIn { + public: + InterruptIn(int reg) { + (void)reg; + }; + void fall(Callback func) { button_callback = func; }; + void mode(int v) { (void)v; }; + + private: +}; + +#endif /*__UI_mbed_H__*/ \ No newline at end of file diff --git a/firmware/application/external/snake/snake.cpp b/firmware/application/external/snake/snake.cpp new file mode 100644 index 000000000..d1f134969 --- /dev/null +++ b/firmware/application/external/snake/snake.cpp @@ -0,0 +1,234 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#include "mbed.h" +#include "SPI_TFT_ILI9341.h" +#include "Arial12x12.h" +#include "ui.hpp" +#include "random.hpp" + +extern int game_state; + +#define SCREEN_WIDTH 240 +#define SCREEN_HEIGHT 320 +#define SNAKE_SIZE 10 +#define INFO_BAR_HEIGHT 25 +#define GAME_AREA_TOP (INFO_BAR_HEIGHT + 1) +#define GAME_AREA_HEIGHT (SCREEN_HEIGHT - INFO_BAR_HEIGHT - 2) +#define GRID_WIDTH ((SCREEN_WIDTH - 2) / SNAKE_SIZE) +#define GRID_HEIGHT (GAME_AREA_HEIGHT / SNAKE_SIZE) +#define STATE_MENU 0 +#define STATE_PLAYING 1 +#define STATE_GAME_OVER 2 + +#define COLOR_BACKGROUND Black +#define COLOR_SNAKE Green +#define COLOR_FOOD Red +#define COLOR_BORDER White + +Ticker game_timer; + +int snake_x[GRID_WIDTH * GRID_HEIGHT]; +int snake_y[GRID_WIDTH * GRID_HEIGHT]; +int snake_length = 1; +int snake_dx = 1, snake_dy = 0; +int food_x, food_y; +int score = 0; +int game_state = STATE_MENU; +bool initialized = false; + +extern ui::Painter painter; + +void init_game(); +void update_game(); +void draw_screen(); +void draw_snake(); +void draw_full_snake(); +void erase_tail(int x, int y); +void draw_food(); +void erase_food(); +void draw_score(); +void draw_borders(); +void spawn_food(); +bool check_collision(); +void show_menu(); +void show_game_over(); + +void game_timer_check() { + if (game_state == STATE_PLAYING) { + update_game(); + } +} + +void init_game() { + claim(stdout); + set_orientation(2); + set_font((unsigned char*)Arial12x12); + snake_x[0] = GRID_WIDTH / 2; + snake_y[0] = GRID_HEIGHT / 2; + snake_length = 1; + snake_dx = 1; + snake_dy = 0; + score = 0; + spawn_food(); + if (game_state == STATE_MENU) { + show_menu(); + } else if (game_state == STATE_PLAYING) { + draw_screen(); + } +} + +void spawn_food() { + bool valid; + do { + food_x = rand() % GRID_WIDTH; + food_y = rand() % GRID_HEIGHT; + valid = true; + for (int i = 0; i < snake_length; i++) { + if (snake_x[i] == food_x && snake_y[i] == food_y) { + valid = false; + break; + } + } + } while (!valid); +} + +void update_game() { + int new_x = snake_x[0] + snake_dx; + int new_y = snake_y[0] + snake_dy; + bool ate_food = (new_x == food_x && new_y == food_y); + + int tail_x = snake_x[snake_length - 1]; + int tail_y = snake_y[snake_length - 1]; + + for (int i = snake_length - 1; i > 0; i--) { + snake_x[i] = snake_x[i - 1]; + snake_y[i] = snake_y[i - 1]; + } + + snake_x[0] = new_x; + snake_y[0] = new_y; + + if (ate_food) { + snake_x[snake_length] = tail_x; + snake_y[snake_length] = tail_y; + snake_length++; + score += 10; + spawn_food(); + draw_food(); + } else { + erase_tail(tail_x, tail_y); + } + + draw_snake(); + draw_score(); + + if (check_collision()) { + draw_borders(); + game_state = STATE_GAME_OVER; + show_game_over(); + return; + } +} + +bool check_collision() { + if (snake_x[0] < 0 || snake_x[0] >= GRID_WIDTH || snake_y[0] < 0 || snake_y[0] >= GRID_HEIGHT) { + return true; + } + for (int i = 1; i < snake_length; i++) { + if (snake_x[0] == snake_x[i] && snake_y[0] == snake_y[i]) { + return true; + } + } + return false; +} + +void draw_screen() { + cls(); + background(COLOR_BACKGROUND); + draw_borders(); + draw_full_snake(); + draw_food(); + draw_score(); +} + +void draw_snake() { + fillrect(1 + snake_x[0] * SNAKE_SIZE, GAME_AREA_TOP + snake_y[0] * SNAKE_SIZE, + 1 + snake_x[0] * SNAKE_SIZE + SNAKE_SIZE, GAME_AREA_TOP + snake_y[0] * SNAKE_SIZE + SNAKE_SIZE, COLOR_SNAKE); +} + +void draw_full_snake() { + for (int i = 0; i < snake_length; i++) { + fillrect(1 + snake_x[i] * SNAKE_SIZE, GAME_AREA_TOP + snake_y[i] * SNAKE_SIZE, + 1 + snake_x[i] * SNAKE_SIZE + SNAKE_SIZE, GAME_AREA_TOP + snake_y[i] * SNAKE_SIZE + SNAKE_SIZE, COLOR_SNAKE); + } +} + +void erase_tail(int x, int y) { + fillrect(1 + x * SNAKE_SIZE, GAME_AREA_TOP + y * SNAKE_SIZE, + 1 + x * SNAKE_SIZE + SNAKE_SIZE, GAME_AREA_TOP + y * SNAKE_SIZE + SNAKE_SIZE, COLOR_BACKGROUND); +} + +void draw_food() { + fillrect(1 + food_x * SNAKE_SIZE, GAME_AREA_TOP + food_y * SNAKE_SIZE, + 1 + food_x * SNAKE_SIZE + SNAKE_SIZE, GAME_AREA_TOP + food_y * SNAKE_SIZE + SNAKE_SIZE, COLOR_FOOD); +} + +void erase_food() { + fillrect(1 + food_x * SNAKE_SIZE, GAME_AREA_TOP + food_y * SNAKE_SIZE, + 1 + food_x * SNAKE_SIZE + SNAKE_SIZE, GAME_AREA_TOP + food_y * SNAKE_SIZE + SNAKE_SIZE, COLOR_BACKGROUND); +} + +void draw_score() { + auto style = *ui::Theme::getInstance()->fg_blue; + painter.draw_string({5, 5}, style, "Score: " + std::to_string(score)); +} + +void draw_borders() { + rect(0, GAME_AREA_TOP - 1, SCREEN_WIDTH, GAME_AREA_TOP, COLOR_BORDER); + rect(0, GAME_AREA_TOP, SCREEN_WIDTH, SCREEN_HEIGHT, COLOR_BORDER); +} + +void show_menu() { + cls(); + background(COLOR_BACKGROUND); + auto style_yellow = *ui::Theme::getInstance()->fg_yellow; + auto style_green = *ui::Theme::getInstance()->fg_green; + auto style_blue = *ui::Theme::getInstance()->fg_blue; + painter.draw_string({50, 40}, style_yellow, "* * * SNAKE * * *"); + painter.draw_string({0, 120}, style_blue, "USE THE D-PAD TO MOVE"); + painter.draw_string({0, 150}, style_blue, "EAT THE RED SQUARES TO GROW"); + painter.draw_string({0, 180}, style_blue, "DON'T HIT THE WALLS OR SELF"); + painter.draw_string({15, 240}, style_green, "** PRESS SELECT TO START **"); +} + +void show_game_over() { + cls(); + background(COLOR_BACKGROUND); + auto style_red = *ui::Theme::getInstance()->fg_red; + auto style_yellow = *ui::Theme::getInstance()->fg_yellow; + auto style_green = *ui::Theme::getInstance()->fg_green; + painter.draw_string({75, 90}, style_red, "GAME OVER"); + painter.draw_string({74, 150}, style_yellow, "SCORE: " + std::to_string(score)); + painter.draw_string({20, 220}, style_green, "PRESS SELECT TO RESTART"); + wait(1); +} + +int main() { + if (!initialized) { + initialized = true; + game_timer.attach(&game_timer_check, 1.0 / 5.0); + init_game(); + } + while (1) { + if (but_SELECT && (game_state == STATE_MENU || game_state == STATE_GAME_OVER)) { + game_state = STATE_PLAYING; + init_game(); + } + } +} \ No newline at end of file diff --git a/firmware/application/external/snake/ui_snake.cpp b/firmware/application/external/snake/ui_snake.cpp new file mode 100644 index 000000000..d445240c9 --- /dev/null +++ b/firmware/application/external/snake/ui_snake.cpp @@ -0,0 +1,71 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#include "ui_snake.hpp" + +namespace ui::external_app::snake { + +#include "snake.cpp" + +SnakeView::SnakeView(NavigationView& nav) + : nav_{nav} { + add_children({&dummy}); + game_timer.attach(&game_timer_check, 1.0 / 5.0); +} + +void SnakeView::on_show() { +} + +void SnakeView::paint(Painter& painter) { + (void)painter; + if (!initialized) { + initialized = true; + std::srand(LPC_RTC->CTIME0); + init_game(); + } +} + +void SnakeView::frame_sync() { + check_game_timer(); + set_dirty(); +} + +bool SnakeView::on_key(const KeyEvent key) { + if (key == KeyEvent::Select) { + if (game_state == STATE_MENU || game_state == STATE_GAME_OVER) { + game_state = STATE_PLAYING; + init_game(); + } + } else if (game_state == STATE_PLAYING) { + if (key == KeyEvent::Left) { + if (snake_dx == 0) { + snake_dx = -1; + snake_dy = 0; + } + } else if (key == KeyEvent::Right) { + if (snake_dx == 0) { + snake_dx = 1; + snake_dy = 0; + } + } else if (key == KeyEvent::Up) { + if (snake_dy == 0) { + snake_dx = 0; + snake_dy = -1; + } + } else if (key == KeyEvent::Down) { + if (snake_dy == 0) { + snake_dx = 0; + snake_dy = 1; + } + } + } + set_dirty(); + return true; +} + +} // namespace ui::external_app::snake \ No newline at end of file diff --git a/firmware/application/external/snake/ui_snake.hpp b/firmware/application/external/snake/ui_snake.hpp new file mode 100644 index 000000000..22fef7466 --- /dev/null +++ b/firmware/application/external/snake/ui_snake.hpp @@ -0,0 +1,48 @@ +/* + * ------------------------------------------------------------ + * | Made by RocketGod | + * | Find me at https://betaskynet.com | + * | Argh matey! | + * ------------------------------------------------------------ + */ + +#ifndef __UI_SNAKE_H__ +#define __UI_SNAKE_H__ + +#include "ui_navigation.hpp" +#include "event_m0.hpp" +#include "message.hpp" +#include "irq_controls.hpp" +#include "random.hpp" +#include "lpc43xx_cpp.hpp" +#include "limits.h" +#include "ui_widget.hpp" + +namespace ui::external_app::snake { + +class SnakeView : public View { + public: + SnakeView(NavigationView& nav); + void on_show() override; + std::string title() const override { return "Snake"; }; + void focus() override { dummy.focus(); }; + void paint(Painter& painter) override; + void frame_sync(); + bool on_key(KeyEvent key) override; + + private: + bool initialized = false; + NavigationView& nav_; + Button dummy{ + {240, 0, 0, 0}, + ""}; + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { + this->frame_sync(); + }}; +}; + +} // namespace ui::external_app::snake + +#endif /*__UI_SNAKE_H__*/ \ No newline at end of file