Combined cpp files, stuffed helper files in hpp, updates start and game over screens (#2583)

This commit is contained in:
RocketGod 2025-03-22 22:29:05 -07:00 committed by GitHub
parent a60169c7f1
commit c5b7326d4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 594 additions and 651 deletions

View File

@ -1,29 +0,0 @@
/*
* 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__*/

View File

@ -1,68 +0,0 @@
/*
* ------------------------------------------------------------
* | 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__*/

View File

@ -1,429 +0,0 @@
/*
* ------------------------------------------------------------
* | 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"
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
#define PADDLE_WIDTH 40
#define PADDLE_HEIGHT 10
#define BALL_SIZE 8
#define BRICK_WIDTH 20
#define BRICK_HEIGHT 10
#define BRICK_ROWS 5
#define BRICK_COLS 10
#define BRICK_GAP 2
#define GAME_AREA_TOP 50
#define GAME_AREA_BOTTOM 310
#define PADDLE_Y (GAME_AREA_BOTTOM - PADDLE_HEIGHT)
#define BALL_SPEED_INCREASE 0.1f
#define STATE_MENU 0
#define STATE_PLAYING 1
#define STATE_GAME_OVER 3
#define COLOR_BACKGROUND Black
#define COLOR_PADDLE Blue
#define COLOR_BALL White
#define COLOR_BORDER White
#define COLOR_BRICK_COLORS \
{ Red, Orange, Yellow, Green, Purple }
Ticker game_timer;
int paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) / 2;
float ball_x = SCREEN_WIDTH / 2;
float ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float ball_dx = 1.5f;
float ball_dy = -2.0f;
int score = 0;
int lives = 3;
int level = 1;
int game_state = STATE_MENU;
bool initialized = false;
bool ball_attached = true;
unsigned int brick_count = 0;
bool bricks[BRICK_ROWS][BRICK_COLS];
int brick_colors[BRICK_ROWS];
extern ui::Painter painter;
void init_game();
void init_level();
void draw_screen();
void draw_bricks();
void draw_paddle();
void draw_ball();
void draw_score();
void draw_lives();
void draw_level();
void draw_borders();
void move_paddle_left();
void move_paddle_right();
void launch_ball();
void update_game();
void check_collisions();
bool check_brick_collision(int row, int col);
void handle_game_over();
void show_menu();
void show_game_over();
bool check_level_complete();
void next_level();
void reset_game();
void game_timer_check() {
if (game_state == STATE_PLAYING) {
update_game();
}
}
void init_game() {
claim(stdout);
set_orientation(2);
set_font((unsigned char*)Arial12x12);
paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) / 2;
score = 0;
lives = 3;
level = 1;
brick_colors[0] = Red;
brick_colors[1] = Orange;
brick_colors[2] = Yellow;
brick_colors[3] = Green;
brick_colors[4] = Purple;
init_level();
game_state = STATE_MENU;
show_menu();
}
void init_level() {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float speed_multiplier = (level == 1) ? 1.0f : 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
ball_dx = (ball_dx > 0 ? 1.5f : -1.5f) * speed_multiplier;
ball_dy = -2.0f * speed_multiplier;
ball_attached = true;
brick_count = 0;
for (int row = 0; row < BRICK_ROWS; row++) {
for (int col = 0; col < BRICK_COLS; col++) {
bricks[row][col] = true;
brick_count++;
}
}
}
void draw_screen() {
cls();
background(COLOR_BACKGROUND);
draw_borders();
draw_bricks();
draw_paddle();
draw_ball();
draw_score();
draw_lives();
draw_level();
}
void draw_borders() {
rect(0, GAME_AREA_TOP - 1, SCREEN_WIDTH, GAME_AREA_TOP, COLOR_BORDER);
}
void draw_bricks() {
for (int row = 0; row < BRICK_ROWS; row++) {
for (int col = 0; col < BRICK_COLS; col++) {
if (bricks[row][col]) {
int x = col * (BRICK_WIDTH + BRICK_GAP);
int y = GAME_AREA_TOP + row * (BRICK_HEIGHT + BRICK_GAP) + 5;
fillrect(x, y, x + BRICK_WIDTH, y + BRICK_HEIGHT, brick_colors[row]);
rect(x, y, x + BRICK_WIDTH, y + BRICK_HEIGHT, Black);
}
}
}
}
void draw_paddle() {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_PADDLE);
}
void draw_ball() {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BALL);
}
void draw_score() {
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
}
void draw_lives() {
auto style = *ui::Theme::getInstance()->fg_red;
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
}
void draw_level() {
auto style = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({80, 30}, style, "Level: " + std::to_string(level));
}
void move_paddle_left() {
if (paddle_x > 0) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
if (ball_attached) {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
}
paddle_x -= 10;
if (paddle_x < 0) paddle_x = 0;
if (ball_attached) {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
}
draw_paddle();
if (ball_attached) {
draw_ball();
}
}
}
void move_paddle_right() {
if (paddle_x < SCREEN_WIDTH - PADDLE_WIDTH) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
if (ball_attached) {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
}
paddle_x += 10;
if (paddle_x > SCREEN_WIDTH - PADDLE_WIDTH) paddle_x = SCREEN_WIDTH - PADDLE_WIDTH;
if (ball_attached) {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
}
draw_paddle();
if (ball_attached) {
draw_ball();
}
}
}
void launch_ball() {
if (ball_attached) {
ball_attached = false;
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float speed_multiplier = (level == 1) ? 1.0f : 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
ball_dx = 1.5f * speed_multiplier;
ball_dy = -2.0f * speed_multiplier;
}
}
void update_game() {
if (ball_attached) {
return;
}
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
float next_ball_y = ball_y + ball_dy;
if (next_ball_y > GAME_AREA_BOTTOM) {
lives--;
draw_lives();
if (lives <= 0) {
handle_game_over();
} else {
ball_attached = true;
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
draw_ball();
}
return;
}
ball_x += ball_dx;
ball_y = next_ball_y;
if (ball_x < 0) {
ball_x = 0;
ball_dx = -ball_dx;
} else if (ball_x > SCREEN_WIDTH - BALL_SIZE) {
ball_x = SCREEN_WIDTH - BALL_SIZE;
ball_dx = -ball_dx;
}
if (ball_y < GAME_AREA_TOP) {
ball_y = GAME_AREA_TOP;
ball_dy = -ball_dy;
}
if (ball_y + BALL_SIZE >= PADDLE_Y && ball_y <= PADDLE_Y + PADDLE_HEIGHT) {
if (ball_x + BALL_SIZE >= paddle_x && ball_x <= paddle_x + PADDLE_WIDTH) {
ball_y = PADDLE_Y - BALL_SIZE;
float hit_position = (ball_x + (BALL_SIZE / 2)) - paddle_x;
float angle = (hit_position / PADDLE_WIDTH) - 0.5f;
ball_dx = angle * 4.0f;
if (ball_dx > -0.5f && ball_dx < 0.5f) {
ball_dx = (ball_dx > 0) ? 0.5f : -0.5f;
}
ball_dy = -ball_dy;
}
}
check_collisions();
draw_ball();
if (check_level_complete()) {
next_level();
}
}
void check_collisions() {
int grid_x = ball_x / (BRICK_WIDTH + BRICK_GAP);
int grid_y = (ball_y - GAME_AREA_TOP - 5) / (BRICK_HEIGHT + BRICK_GAP);
for (int row = grid_y - 1; row <= grid_y + 1; row++) {
for (int col = grid_x - 1; col <= grid_x + 1; col++) {
if (row >= 0 && row < BRICK_ROWS && col >= 0 && col < BRICK_COLS) {
if (bricks[row][col] && check_brick_collision(row, col)) {
return;
}
}
}
}
}
bool check_brick_collision(int row, int col) {
int brick_x = col * (BRICK_WIDTH + BRICK_GAP);
int brick_y = GAME_AREA_TOP + row * (BRICK_HEIGHT + BRICK_GAP) + 5;
if (ball_x + BALL_SIZE >= brick_x && ball_x <= brick_x + BRICK_WIDTH &&
ball_y + BALL_SIZE >= brick_y && ball_y <= brick_y + BRICK_HEIGHT) {
fillrect(brick_x, brick_y, brick_x + BRICK_WIDTH, brick_y + BRICK_HEIGHT, COLOR_BACKGROUND);
bricks[row][col] = false;
brick_count--;
score += (5 - row) * 10;
draw_score();
float center_x = brick_x + BRICK_WIDTH / 2;
float center_y = brick_y + BRICK_HEIGHT / 2;
float ball_center_x = ball_x + BALL_SIZE / 2;
float ball_center_y = ball_y + BALL_SIZE / 2;
float dx = std::abs(ball_center_x - center_x);
float dy = std::abs(ball_center_y - center_y);
if (dx * BRICK_HEIGHT > dy * BRICK_WIDTH) {
ball_dx = -ball_dx;
} else {
ball_dy = -ball_dy;
}
return true;
}
return false;
}
bool check_level_complete() {
return brick_count == 0;
}
void next_level() {
level++;
init_level();
draw_screen();
}
void handle_game_over() {
game_state = STATE_GAME_OVER;
show_game_over();
}
void show_menu() {
cls();
background(COLOR_BACKGROUND);
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_white = *ui::Theme::getInstance()->fg_light;
auto style_green = *ui::Theme::getInstance()->fg_green;
auto style_red = *ui::Theme::getInstance()->fg_red;
painter.draw_string({0, 40}, style_yellow, "* * * BREAKOUT * * *");
painter.draw_string({0, 70}, style_white, "========================");
painter.draw_string({0, 120}, style_green, "| ROTARY: MOVE PADDLE |");
painter.draw_string({0, 150}, style_green, "| SELECT: START/LAUNCH |");
painter.draw_string({0, 190}, style_white, "========================");
painter.draw_string({24, 230}, style_red, "* PRESS SELECT *");
}
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({72, 120}, style_red, "GAME OVER");
painter.draw_string({12, 160}, style_yellow, "FINAL SCORE: " + std::to_string(score));
painter.draw_string({0, 200}, style_green, "PRESS SELECT TO RESTART");
wait(1);
}
void reset_game() {
level = 1;
score = 0;
lives = 3;
game_state = STATE_PLAYING;
init_level();
draw_screen();
}
int main() {
if (!initialized) {
initialized = true;
game_timer.attach(&game_timer_check, 1.0 / 60.0);
init_game();
}
while (1) {
if (but_SELECT && game_state == STATE_MENU) {
game_state = STATE_PLAYING;
reset_game();
}
if (but_SELECT && game_state == STATE_GAME_OVER) {
reset_game();
}
if (but_SELECT && game_state == STATE_PLAYING && ball_attached) {
launch_ball();
}
if (but_LEFT && game_state == STATE_PLAYING) {
move_paddle_left();
}
if (but_RIGHT && game_state == STATE_PLAYING) {
move_paddle_right();
}
}
}

View File

@ -20,13 +20,13 @@ void initialize_app(ui::NavigationView& nav) {
extern "C" {
__attribute__((section(".external_app.app_breakout.application_information"), used)) application_information_t _application_information_breakout = {
/*.memory_location = */ (uint8_t*)0x00000000, // will be filled at compile time
/*.externalAppEntry = */ ui::external_app::breakout::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
(uint8_t*)0x00000000,
ui::external_app::breakout::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
/*.app_name = */ "Breakout",
/*.bitmap_data = */ {
"Breakout",
{
0x00,
0x00,
0x7F,
@ -60,11 +60,11 @@ __attribute__((section(".external_app.app_breakout.application_information"), us
0xF7,
0xF7,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
ui::Color::green().v,
app_location_t::GAMES,
-1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {0, 0, 0, 0},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
{0, 0, 0, 0},
0x00000000,
};
}
} // namespace ui::external_app::breakout

View File

@ -1,105 +0,0 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#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,
};
static bool but_RIGHT;
static bool but_LEFT;
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__*/

View File

@ -10,10 +10,477 @@
namespace ui::external_app::breakout {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Weffc++"
#include "breakout.cpp"
#pragma GCC diagnostic pop
Ticker game_timer;
int paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) / 2;
float ball_x = SCREEN_WIDTH / 2;
float ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float ball_dx = 1.5f;
float ball_dy = -2.0f;
int score = 0;
int lives = 3;
int level = 1;
int game_state = STATE_MENU;
bool initialized = false;
bool ball_attached = true;
unsigned int brick_count = 0;
bool menu_initialized = false;
bool blink_state = true;
uint32_t blink_counter = 0;
int16_t prompt_x = 0;
bool gameover_initialized = false;
bool gameover_blink_state = true;
uint32_t gameover_blink_counter = 0;
int16_t restart_x = 0;
bool bricks[BRICK_ROWS][BRICK_COLS];
int brick_colors[BRICK_ROWS];
const Color pp_colors[] = {
Color::white(),
Color::blue(),
Color::yellow(),
Color::purple(),
Color::green(),
Color::red(),
Color::magenta(),
Color::orange(),
Color::black(),
};
Painter painter;
bool but_RIGHT = false;
bool but_LEFT = false;
bool but_SELECT = false;
static Callback game_update_callback = nullptr;
static uint32_t game_update_timeout = 0;
static uint32_t game_update_counter = 0;
void cls() {
painter.fill_rectangle({0, 0, portapack::display.width(), portapack::display.height()}, Color::black());
}
void background(int color) {
(void)color;
}
void fillrect(int x1, int y1, int x2, int y2, int color) {
painter.fill_rectangle({x1, y1, x2 - x1, y2 - y1}, pp_colors[color]);
}
void rect(int x1, int y1, int x2, int y2, int color) {
painter.draw_rectangle({x1, y1, x2 - x1, y2 - y1}, pp_colors[color]);
}
void check_game_timer() {
if (game_update_callback) {
if (++game_update_counter >= game_update_timeout) {
game_update_counter = 0;
game_update_callback();
}
}
}
void Ticker::attach(Callback func, double delay_sec) {
game_update_callback = func;
game_update_timeout = delay_sec * 60;
}
void Ticker::detach() {
game_update_callback = nullptr;
}
void game_timer_check() {
if (game_state == STATE_PLAYING) {
update_game();
} else if (game_state == STATE_MENU) {
show_menu();
} else if (game_state == STATE_GAME_OVER) {
show_game_over();
}
}
void init_game() {
paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) / 2;
score = 0;
lives = 3;
level = 1;
brick_colors[0] = Red;
brick_colors[1] = Orange;
brick_colors[2] = Yellow;
brick_colors[3] = Green;
brick_colors[4] = Purple;
init_level();
game_state = STATE_MENU;
menu_initialized = false;
blink_state = true;
blink_counter = 0;
}
void init_level() {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float speed_multiplier = (level == 1) ? 1.0f : 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
ball_dx = (ball_dx > 0 ? 1.5f : -1.5f) * speed_multiplier;
ball_dy = -2.0f * speed_multiplier;
ball_attached = true;
brick_count = 0;
for (int row = 0; row < BRICK_ROWS; row++) {
for (int col = 0; col < BRICK_COLS; col++) {
bricks[row][col] = true;
brick_count++;
}
}
}
void draw_screen() {
cls();
background(COLOR_BACKGROUND);
draw_borders();
draw_bricks();
draw_paddle();
draw_ball();
draw_score();
draw_lives();
draw_level();
}
void draw_borders() {
rect(0, GAME_AREA_TOP - 1, SCREEN_WIDTH, GAME_AREA_TOP, COLOR_BORDER);
}
void draw_bricks() {
for (int row = 0; row < BRICK_ROWS; row++) {
for (int col = 0; col < BRICK_COLS; col++) {
if (bricks[row][col]) {
int x = col * (BRICK_WIDTH + BRICK_GAP);
int y = GAME_AREA_TOP + row * (BRICK_HEIGHT + BRICK_GAP) + 5;
fillrect(x, y, x + BRICK_WIDTH, y + BRICK_HEIGHT, brick_colors[row]);
rect(x, y, x + BRICK_WIDTH, y + BRICK_HEIGHT, Black);
}
}
}
}
void draw_paddle() {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_PADDLE);
}
void draw_ball() {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BALL);
}
void draw_score() {
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
}
void draw_lives() {
auto style = *ui::Theme::getInstance()->fg_red;
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
}
void draw_level() {
auto style = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({80, 30}, style, "Level: " + std::to_string(level));
}
void move_paddle_left() {
if (paddle_x > 0) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
if (ball_attached) {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
}
paddle_x -= 10;
if (paddle_x < 0) paddle_x = 0;
if (ball_attached) {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
}
draw_paddle();
if (ball_attached) {
draw_ball();
}
}
}
void move_paddle_right() {
if (paddle_x < SCREEN_WIDTH - PADDLE_WIDTH) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
if (ball_attached) {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
}
paddle_x += 10;
if (paddle_x > SCREEN_WIDTH - PADDLE_WIDTH) paddle_x = SCREEN_WIDTH - PADDLE_WIDTH;
if (ball_attached) {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
}
draw_paddle();
if (ball_attached) {
draw_ball();
}
}
}
void launch_ball() {
if (ball_attached) {
ball_attached = false;
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float speed_multiplier = (level == 1) ? 1.0f : 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
ball_dx = 1.5f * speed_multiplier;
ball_dy = -2.0f * speed_multiplier;
}
}
void update_game() {
if (ball_attached) {
return;
}
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
float next_ball_y = ball_y + ball_dy;
if (next_ball_y > GAME_AREA_BOTTOM) {
lives--;
draw_lives();
if (lives <= 0) {
handle_game_over();
} else {
ball_attached = true;
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
draw_ball();
}
return;
}
ball_x += ball_dx;
ball_y = next_ball_y;
if (ball_x < 0) {
ball_x = 0;
ball_dx = -ball_dx;
} else if (ball_x > SCREEN_WIDTH - BALL_SIZE) {
ball_x = SCREEN_WIDTH - BALL_SIZE;
ball_dx = -ball_dx;
}
if (ball_y < GAME_AREA_TOP) {
ball_y = GAME_AREA_TOP;
ball_dy = -ball_dy;
}
if (ball_y + BALL_SIZE >= PADDLE_Y && ball_y <= PADDLE_Y + PADDLE_HEIGHT) {
if (ball_x + BALL_SIZE >= paddle_x && ball_x <= paddle_x + PADDLE_WIDTH) {
ball_y = PADDLE_Y - BALL_SIZE;
float hit_position = (ball_x + (BALL_SIZE / 2)) - paddle_x;
float angle = (hit_position / PADDLE_WIDTH) - 0.5f;
ball_dx = angle * 4.0f;
if (ball_dx > -0.5f && ball_dx < 0.5f) {
ball_dx = (ball_dx > 0) ? 0.5f : -0.5f;
}
ball_dy = -ball_dy;
}
}
check_collisions();
draw_ball();
if (check_level_complete()) {
next_level();
}
}
void check_collisions() {
int grid_x = ball_x / (BRICK_WIDTH + BRICK_GAP);
int grid_y = (ball_y - GAME_AREA_TOP - 5) / (BRICK_HEIGHT + BRICK_GAP);
for (int row = grid_y - 1; row <= grid_y + 1; row++) {
for (int col = grid_x - 1; col <= grid_x + 1; col++) {
if (row >= 0 && row < BRICK_ROWS && col >= 0 && col < BRICK_COLS) {
if (bricks[row][col] && check_brick_collision(row, col)) {
return;
}
}
}
}
}
bool check_brick_collision(int row, int col) {
int brick_x = col * (BRICK_WIDTH + BRICK_GAP);
int brick_y = GAME_AREA_TOP + row * (BRICK_HEIGHT + BRICK_GAP) + 5;
if (ball_x + BALL_SIZE >= brick_x && ball_x <= brick_x + BRICK_WIDTH &&
ball_y + BALL_SIZE >= brick_y && ball_y <= brick_y + BRICK_HEIGHT) {
fillrect(brick_x, brick_y, brick_x + BRICK_WIDTH, brick_y + BRICK_HEIGHT, COLOR_BACKGROUND);
bricks[row][col] = false;
brick_count--;
score += (5 - row) * 10;
draw_score();
float center_x = brick_x + BRICK_WIDTH / 2;
float center_y = brick_y + BRICK_HEIGHT / 2;
float ball_center_x = ball_x + BALL_SIZE / 2;
float ball_center_y = ball_y + BALL_SIZE / 2;
float dx = std::abs(ball_center_x - center_x);
float dy = std::abs(ball_center_y - center_y);
if (dx * BRICK_HEIGHT > dy * BRICK_WIDTH) {
ball_dx = -ball_dx;
} else {
ball_dy = -ball_dy;
}
return true;
}
return false;
}
bool check_level_complete() {
return brick_count == 0;
}
void next_level() {
level++;
init_level();
draw_screen();
}
void handle_game_over() {
game_state = STATE_GAME_OVER;
gameover_initialized = false;
show_game_over();
}
void init_menu() {
cls();
background(COLOR_BACKGROUND);
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_blue = *ui::Theme::getInstance()->fg_blue;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
int16_t screen_width = 240;
int16_t title_x = (screen_width - 17 * 8) / 2;
int16_t divider_width = 24 * 8;
int16_t divider_x = (screen_width - divider_width) / 2;
int16_t instruction_width = 22 * 8;
int16_t instruction_x = (screen_width - instruction_width) / 2;
int16_t prompt_width = 16 * 8;
prompt_x = (screen_width - prompt_width) / 2;
painter.fill_rectangle({0, 30, screen_width, 30}, Color::black());
painter.draw_string({title_x + 2, 42}, style_yellow, "*** BREAKOUT ***");
painter.draw_string({divider_x, 70}, style_blue, "========================");
painter.fill_rectangle({instruction_x - 5, 110, instruction_width + 10, 70}, Color::black());
painter.draw_rectangle({instruction_x - 5, 110, instruction_width + 10, 70}, Color::white());
painter.draw_string({instruction_x, 120}, style_cyan, " ROTARY: MOVE PADDLE");
painter.draw_string({instruction_x, 150}, style_cyan, " SELECT: START/LAUNCH");
painter.draw_string({divider_x, 190}, style_blue, "========================");
menu_initialized = true;
}
void show_menu() {
if (!menu_initialized) {
init_menu();
}
auto style_red = *ui::Theme::getInstance()->fg_red;
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
painter.fill_rectangle({prompt_x - 2, 228, 16 * 8 + 4, 20}, Color::black());
if (blink_state) {
painter.draw_string({prompt_x, 230}, style_red, "* PRESS SELECT *");
}
}
}
void init_game_over() {
cls();
background(COLOR_BACKGROUND);
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
int16_t screen_width = 240;
int16_t title_width = 9 * 8;
int16_t title_x = (screen_width - title_width) / 2;
int16_t score_text_width = (16 + std::to_string(score).length()) * 8;
int16_t score_x = (screen_width - score_text_width) / 2;
painter.draw_rectangle({20, 80, screen_width - 40, 160}, Color::red());
painter.draw_rectangle({22, 82, screen_width - 44, 156}, Color::white());
painter.draw_string({title_x, 100}, style_red, "GAME OVER");
painter.fill_rectangle({40, 140, screen_width - 80, 30}, Color::black());
painter.draw_rectangle({40, 140, screen_width - 80, 30}, Color::yellow());
painter.draw_string({score_x, 150}, style_yellow, " FINAL SCORE: " + std::to_string(score));
int16_t restart_width = 12 * 8;
restart_x = (screen_width - restart_width) / 2;
gameover_initialized = true;
gameover_blink_state = true;
gameover_blink_counter = 0;
}
void show_game_over() {
if (!gameover_initialized) {
init_game_over();
}
auto style_green = *ui::Theme::getInstance()->fg_green;
if (++gameover_blink_counter >= 30) {
gameover_blink_counter = 0;
gameover_blink_state = !gameover_blink_state;
painter.fill_rectangle({restart_x - 2, 198, 12 * 8 + 4, 20}, Color::black());
if (gameover_blink_state) {
painter.draw_string({restart_x, 200}, style_green, "PRESS SELECT");
}
}
}
void reset_game() {
level = 1;
score = 0;
lives = 3;
game_state = STATE_PLAYING;
init_level();
draw_screen();
gameover_initialized = false;
gameover_blink_state = true;
gameover_blink_counter = 0;
}
BreakoutView::BreakoutView(NavigationView& nav)
: nav_{nav} {
@ -53,6 +520,10 @@ bool BreakoutView::on_encoder(const EncoderEvent delta) {
}
bool BreakoutView::on_key(const KeyEvent key) {
but_SELECT = (key == KeyEvent::Select);
but_LEFT = (key == KeyEvent::Left);
but_RIGHT = (key == KeyEvent::Right);
if (key == KeyEvent::Select) {
if (game_state == STATE_MENU) {
game_state = STATE_PLAYING;

View File

@ -9,25 +9,128 @@
#ifndef __UI_BREAKOUT_H__
#define __UI_BREAKOUT_H__
#include "ui.hpp"
#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::breakout {
enum {
White,
Blue,
Yellow,
Purple,
Green,
Red,
Maroon,
Orange,
Black,
};
extern const Color pp_colors[];
extern Painter painter;
extern bool but_RIGHT;
extern bool but_LEFT;
extern bool but_SELECT;
void cls();
void background(int color);
void fillrect(int x1, int y1, int x2, int y2, int color);
void rect(int x1, int y1, int x2, int y2, int color);
#define wait(x) chThdSleepMilliseconds(x * 1000)
using Callback = void (*)(void);
void check_game_timer();
class Ticker {
public:
Ticker() = default;
void attach(Callback func, double delay_sec);
void detach();
};
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
#define PADDLE_WIDTH 40
#define PADDLE_HEIGHT 10
#define BALL_SIZE 8
#define BRICK_WIDTH 20
#define BRICK_HEIGHT 10
#define BRICK_ROWS 5
#define BRICK_COLS 10
#define BRICK_GAP 2
#define GAME_AREA_TOP 50
#define GAME_AREA_BOTTOM 310
#define PADDLE_Y (GAME_AREA_BOTTOM - PADDLE_HEIGHT)
#define BALL_SPEED_INCREASE 0.1f
#define STATE_MENU 0
#define STATE_PLAYING 1
#define STATE_GAME_OVER 3
#define COLOR_BACKGROUND Black
#define COLOR_PADDLE Blue
#define COLOR_BALL White
#define COLOR_BORDER White
#define COLOR_BRICK_COLORS \
{ Red, Orange, Yellow, Green, Purple }
extern Ticker game_timer;
extern int paddle_x;
extern float ball_x;
extern float ball_y;
extern float ball_dx;
extern float ball_dy;
extern int score;
extern int lives;
extern int level;
extern int game_state;
extern bool initialized;
extern bool ball_attached;
extern unsigned int brick_count;
extern bool bricks[BRICK_ROWS][BRICK_COLS];
extern int brick_colors[BRICK_ROWS];
void game_timer_check();
void init_game();
void init_level();
void draw_screen();
void draw_bricks();
void draw_paddle();
void draw_ball();
void draw_score();
void draw_lives();
void draw_level();
void draw_borders();
void move_paddle_left();
void move_paddle_right();
void launch_ball();
void update_game();
void check_collisions();
bool check_brick_collision(int row, int col);
void handle_game_over();
void show_menu();
void show_game_over();
bool check_level_complete();
void next_level();
void reset_game();
class BreakoutView : public View {
public:
BreakoutView(NavigationView& nav);
void on_show() override;
std::string title() const override { return "Breakout"; };
std::string title() const override { return "Breakout"; }
void focus() override { dummy.focus(); };
void focus() override { dummy.focus(); }
void paint(Painter& painter) override;
void frame_sync();
bool on_encoder(const EncoderEvent event) override;
@ -50,4 +153,4 @@ class BreakoutView : public View {
} // namespace ui::external_app::breakout
#endif /*__UI_BREAKOUT_H__*/
#endif /* __UI_BREAKOUT_H__ */