mirror of
https://github.com/DarkFlippers/unleashed-firmware
synced 2024-12-24 19:53:08 +00:00
72f250195c
All games now will increase flipper's level when you start them or win in some of them Games with endless play like tetris or flappy bird has no winning logic so they will increase level only when you start them
479 lines
17 KiB
C
479 lines
17 KiB
C
#include <furi.h>
|
|
#include <gui/gui.h>
|
|
#include <input/input.h>
|
|
#include <stdlib.h>
|
|
#include <gui/view.h>
|
|
#include <notification/notification.h>
|
|
#include <notification/notification_messages.h>
|
|
#include <dolphin/dolphin.h>
|
|
|
|
#define TAG "Arkanoid"
|
|
|
|
#define FLIPPER_LCD_WIDTH 128
|
|
#define FLIPPER_LCD_HEIGHT 64
|
|
#define MAX_SPEED 3
|
|
|
|
typedef enum { EventTypeTick, EventTypeKey } EventType;
|
|
|
|
typedef struct {
|
|
//Brick Bounds used in collision detection
|
|
int leftBrick;
|
|
int rightBrick;
|
|
int topBrick;
|
|
int bottomBrick;
|
|
bool isHit[4][13]; //Array of if bricks are hit or not
|
|
} BrickState;
|
|
|
|
typedef struct {
|
|
int dx; //Initial movement of ball
|
|
int dy; //Initial movement of ball
|
|
int xb; //Balls starting possition
|
|
int yb; //Balls starting possition
|
|
bool released; //If the ball has been released by the player
|
|
//Ball Bounds used in collision detection
|
|
int leftBall;
|
|
int rightBall;
|
|
int topBall;
|
|
int bottomBall;
|
|
} BallState;
|
|
|
|
typedef struct {
|
|
BallState ball_state;
|
|
BrickState brick_state;
|
|
NotificationApp* notify;
|
|
unsigned int COLUMNS; //Columns of bricks
|
|
unsigned int ROWS; //Rows of bricks
|
|
bool initialDraw; //If the inital draw has happened
|
|
int xPaddle; //X position of paddle
|
|
char text[16]; //General string buffer
|
|
bool bounced; //Used to fix double bounce glitch
|
|
int lives; //Amount of lives
|
|
int level; //Current level
|
|
unsigned int score; //Score for the game
|
|
unsigned int brickCount; //Amount of bricks hit
|
|
int tick; //Tick counter
|
|
bool gameStarted; // Did the game start?
|
|
int speed; // Ball speed
|
|
} ArkanoidState;
|
|
|
|
typedef struct {
|
|
EventType type;
|
|
InputEvent input;
|
|
} GameEvent;
|
|
|
|
static const NotificationSequence sequence_short_sound = {
|
|
&message_note_c5,
|
|
&message_delay_50,
|
|
&message_sound_off,
|
|
NULL,
|
|
};
|
|
|
|
// generate number in range [min,max)
|
|
int rand_range(int min, int max) {
|
|
return min + rand() % (max - min);
|
|
}
|
|
|
|
void move_ball(Canvas* canvas, ArkanoidState* st) {
|
|
st->tick++;
|
|
|
|
int current_speed = abs(st->speed - 1 - MAX_SPEED);
|
|
if(st->tick % current_speed != 0 && st->tick % (current_speed + 1) != 0) {
|
|
return;
|
|
}
|
|
|
|
if(st->ball_state.released) {
|
|
//Move ball
|
|
if(abs(st->ball_state.dx) == 2) {
|
|
st->ball_state.xb += st->ball_state.dx / 2;
|
|
// 2x speed is really 1.5 speed
|
|
if((st->tick / current_speed) % 2 == 0) st->ball_state.xb += st->ball_state.dx / 2;
|
|
} else {
|
|
st->ball_state.xb += st->ball_state.dx;
|
|
}
|
|
st->ball_state.yb = st->ball_state.yb + st->ball_state.dy;
|
|
|
|
//Set bounds
|
|
st->ball_state.leftBall = st->ball_state.xb;
|
|
st->ball_state.rightBall = st->ball_state.xb + 2;
|
|
st->ball_state.topBall = st->ball_state.yb;
|
|
st->ball_state.bottomBall = st->ball_state.yb + 2;
|
|
|
|
//Bounce off top edge
|
|
if(st->ball_state.yb <= 0) {
|
|
st->ball_state.yb = 2;
|
|
st->ball_state.dy = -st->ball_state.dy;
|
|
}
|
|
|
|
//Lose a life if bottom edge hit
|
|
if(st->ball_state.yb >= FLIPPER_LCD_HEIGHT) {
|
|
canvas_draw_frame(canvas, st->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
|
|
st->xPaddle = 54;
|
|
st->ball_state.yb = 60;
|
|
st->ball_state.released = false;
|
|
st->lives--;
|
|
st->gameStarted = false;
|
|
|
|
if(rand_range(0, 2) == 0) {
|
|
st->ball_state.dx = 1;
|
|
} else {
|
|
st->ball_state.dx = -1;
|
|
}
|
|
}
|
|
|
|
//Bounce off left side
|
|
if(st->ball_state.xb <= 0) {
|
|
st->ball_state.xb = 2;
|
|
st->ball_state.dx = -st->ball_state.dx;
|
|
}
|
|
|
|
//Bounce off right side
|
|
if(st->ball_state.xb >= FLIPPER_LCD_WIDTH - 2) {
|
|
st->ball_state.xb = FLIPPER_LCD_WIDTH - 4;
|
|
st->ball_state.dx = -st->ball_state.dx;
|
|
// arduboy.tunes.tone(523, 250);
|
|
}
|
|
|
|
//Bounce off paddle
|
|
if(st->ball_state.xb + 1 >= st->xPaddle && st->ball_state.xb <= st->xPaddle + 12 &&
|
|
st->ball_state.yb + 2 >= FLIPPER_LCD_HEIGHT - 1 &&
|
|
st->ball_state.yb <= FLIPPER_LCD_HEIGHT) {
|
|
st->ball_state.dy = -st->ball_state.dy;
|
|
st->ball_state.dx =
|
|
((st->ball_state.xb - (st->xPaddle + 6)) / 3); //Applies spin on the ball
|
|
// prevent straight bounce, but not prevent roguuemaster from stealing
|
|
if(st->ball_state.dx == 0) {
|
|
st->ball_state.dx = (rand_range(0, 2) == 1) ? 1 : -1;
|
|
}
|
|
}
|
|
|
|
//Bounce off Bricks
|
|
for(unsigned int row = 0; row < st->ROWS; row++) {
|
|
for(unsigned int column = 0; column < st->COLUMNS; column++) {
|
|
if(!st->brick_state.isHit[row][column]) {
|
|
//Sets Brick bounds
|
|
st->brick_state.leftBrick = 10 * column;
|
|
st->brick_state.rightBrick = 10 * column + 10;
|
|
st->brick_state.topBrick = 6 * row + 1;
|
|
st->brick_state.bottomBrick = 6 * row + 7;
|
|
|
|
//If A collison has occured
|
|
if(st->ball_state.topBall <= st->brick_state.bottomBrick &&
|
|
st->ball_state.bottomBall >= st->brick_state.topBrick &&
|
|
st->ball_state.leftBall <= st->brick_state.rightBrick &&
|
|
st->ball_state.rightBall >= st->brick_state.leftBrick) {
|
|
st->score += st->level;
|
|
// Blink led when we hit some brick
|
|
notification_message(st->notify, &sequence_short_sound);
|
|
//notification_message(st->notify, &sequence_blink_white_100);
|
|
|
|
st->brickCount++;
|
|
st->brick_state.isHit[row][column] = true;
|
|
canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4);
|
|
|
|
//Vertical collision
|
|
if(st->ball_state.bottomBall > st->brick_state.bottomBrick ||
|
|
st->ball_state.topBall < st->brick_state.topBrick) {
|
|
//Only bounce once each ball move
|
|
if(!st->bounced) {
|
|
st->ball_state.dy = -st->ball_state.dy;
|
|
st->ball_state.yb += st->ball_state.dy;
|
|
st->bounced = true;
|
|
}
|
|
}
|
|
|
|
//Hoizontal collision
|
|
if(st->ball_state.leftBall < st->brick_state.leftBrick ||
|
|
st->ball_state.rightBall > st->brick_state.rightBrick) {
|
|
//Only bounce once brick each ball move
|
|
if(!st->bounced) {
|
|
st->ball_state.dx = -st->ball_state.dx;
|
|
st->ball_state.xb += st->ball_state.dx;
|
|
st->bounced = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Reset Bounce
|
|
st->bounced = false;
|
|
} else {
|
|
//Ball follows paddle
|
|
st->ball_state.xb = st->xPaddle + 5;
|
|
}
|
|
}
|
|
|
|
void draw_lives(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
|
if(arkanoid_state->lives == 3) {
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7);
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8);
|
|
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 11);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 11);
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 12);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 12);
|
|
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 15);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 15);
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 16);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 16);
|
|
} else if(arkanoid_state->lives == 2) {
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7);
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8);
|
|
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 11);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 11);
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 12);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 12);
|
|
} else {
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7);
|
|
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8);
|
|
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8);
|
|
}
|
|
}
|
|
|
|
void draw_score(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
|
snprintf(arkanoid_state->text, sizeof(arkanoid_state->text), "%u", arkanoid_state->score);
|
|
canvas_draw_str_aligned(
|
|
canvas,
|
|
FLIPPER_LCD_WIDTH - 2,
|
|
FLIPPER_LCD_HEIGHT - 6,
|
|
AlignRight,
|
|
AlignBottom,
|
|
arkanoid_state->text);
|
|
}
|
|
|
|
void draw_ball(Canvas* canvas, ArkanoidState* ast) {
|
|
canvas_draw_dot(canvas, ast->ball_state.xb, ast->ball_state.yb);
|
|
canvas_draw_dot(canvas, ast->ball_state.xb + 1, ast->ball_state.yb);
|
|
canvas_draw_dot(canvas, ast->ball_state.xb, ast->ball_state.yb + 1);
|
|
canvas_draw_dot(canvas, ast->ball_state.xb + 1, ast->ball_state.yb + 1);
|
|
|
|
move_ball(canvas, ast);
|
|
}
|
|
|
|
void draw_paddle(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
|
canvas_draw_frame(canvas, arkanoid_state->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
|
|
}
|
|
|
|
void reset_level(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
|
//Undraw paddle
|
|
canvas_draw_frame(canvas, arkanoid_state->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
|
|
|
|
//Undraw ball
|
|
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb, arkanoid_state->ball_state.yb);
|
|
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb + 1, arkanoid_state->ball_state.yb);
|
|
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb, arkanoid_state->ball_state.yb + 1);
|
|
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb + 1, arkanoid_state->ball_state.yb + 1);
|
|
|
|
//Alter various variables to reset the game
|
|
arkanoid_state->xPaddle = 54;
|
|
arkanoid_state->ball_state.yb = 60;
|
|
arkanoid_state->brickCount = 0;
|
|
arkanoid_state->ball_state.released = false;
|
|
arkanoid_state->gameStarted = false;
|
|
|
|
// Reset all brick hit states
|
|
for(unsigned int row = 0; row < arkanoid_state->ROWS; row++) {
|
|
for(unsigned int column = 0; column < arkanoid_state->COLUMNS; column++) {
|
|
arkanoid_state->brick_state.isHit[row][column] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void arkanoid_state_init(ArkanoidState* arkanoid_state) {
|
|
// Init notification
|
|
arkanoid_state->notify = furi_record_open(RECORD_NOTIFICATION);
|
|
|
|
// Set the initial game state
|
|
arkanoid_state->COLUMNS = 13;
|
|
arkanoid_state->ROWS = 4;
|
|
arkanoid_state->ball_state.dx = -1;
|
|
arkanoid_state->ball_state.dy = -1;
|
|
arkanoid_state->speed = 2;
|
|
arkanoid_state->bounced = false;
|
|
arkanoid_state->lives = 3;
|
|
arkanoid_state->level = 1;
|
|
arkanoid_state->score = 0;
|
|
arkanoid_state->COLUMNS = 13;
|
|
arkanoid_state->COLUMNS = 13;
|
|
|
|
// Reset initial state
|
|
arkanoid_state->initialDraw = false;
|
|
arkanoid_state->gameStarted = false;
|
|
}
|
|
|
|
static void arkanoid_draw_callback(Canvas* const canvas, void* ctx) {
|
|
ArkanoidState* arkanoid_state = acquire_mutex((ValueMutex*)ctx, 25);
|
|
if(arkanoid_state == NULL) {
|
|
return;
|
|
}
|
|
|
|
//Initial level draw
|
|
if(!arkanoid_state->initialDraw) {
|
|
arkanoid_state->initialDraw = true;
|
|
|
|
// Set default font for text
|
|
canvas_set_font(canvas, FontSecondary);
|
|
|
|
//Draws the new level
|
|
reset_level(canvas, arkanoid_state);
|
|
}
|
|
|
|
//Draws new bricks and resets their values
|
|
for(unsigned int row = 0; row < arkanoid_state->ROWS; row++) {
|
|
for(unsigned int column = 0; column < arkanoid_state->COLUMNS; column++) {
|
|
if(!arkanoid_state->brick_state.isHit[row][column]) {
|
|
canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(arkanoid_state->lives > 0) {
|
|
draw_paddle(canvas, arkanoid_state);
|
|
draw_ball(canvas, arkanoid_state);
|
|
draw_score(canvas, arkanoid_state);
|
|
draw_lives(canvas, arkanoid_state);
|
|
|
|
if(arkanoid_state->brickCount == arkanoid_state->ROWS * arkanoid_state->COLUMNS) {
|
|
arkanoid_state->level++;
|
|
reset_level(canvas, arkanoid_state);
|
|
}
|
|
} else {
|
|
reset_level(canvas, arkanoid_state);
|
|
arkanoid_state->initialDraw = false;
|
|
arkanoid_state->lives = 3;
|
|
arkanoid_state->score = 0;
|
|
}
|
|
|
|
release_mutex((ValueMutex*)ctx, arkanoid_state);
|
|
}
|
|
|
|
static void arkanoid_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
|
|
furi_assert(event_queue);
|
|
|
|
GameEvent event = {.type = EventTypeKey, .input = *input_event};
|
|
furi_message_queue_put(event_queue, &event, FuriWaitForever);
|
|
}
|
|
|
|
static void arkanoid_update_timer_callback(FuriMessageQueue* event_queue) {
|
|
furi_assert(event_queue);
|
|
|
|
GameEvent event = {.type = EventTypeTick};
|
|
furi_message_queue_put(event_queue, &event, 0);
|
|
}
|
|
|
|
int32_t arkanoid_game_app(void* p) {
|
|
UNUSED(p);
|
|
int32_t return_code = 0;
|
|
|
|
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(GameEvent));
|
|
|
|
ArkanoidState* arkanoid_state = malloc(sizeof(ArkanoidState));
|
|
arkanoid_state_init(arkanoid_state);
|
|
|
|
ValueMutex state_mutex;
|
|
if(!init_mutex(&state_mutex, arkanoid_state, sizeof(ArkanoidState))) {
|
|
FURI_LOG_E(TAG, "Cannot create mutex\r\n");
|
|
return_code = 255;
|
|
goto free_and_exit;
|
|
}
|
|
|
|
// Set system callbacks
|
|
ViewPort* view_port = view_port_alloc();
|
|
view_port_draw_callback_set(view_port, arkanoid_draw_callback, &state_mutex);
|
|
view_port_input_callback_set(view_port, arkanoid_input_callback, event_queue);
|
|
|
|
FuriTimer* timer =
|
|
furi_timer_alloc(arkanoid_update_timer_callback, FuriTimerTypePeriodic, event_queue);
|
|
furi_timer_start(timer, furi_kernel_get_tick_frequency() / 22);
|
|
|
|
// Open GUI and register view_port
|
|
Gui* gui = furi_record_open(RECORD_GUI);
|
|
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
|
|
|
// Call dolphin deed on game start
|
|
DOLPHIN_DEED(DolphinDeedPluginGameStart);
|
|
|
|
GameEvent event;
|
|
for(bool processing = true; processing;) {
|
|
FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
|
|
ArkanoidState* arkanoid_state = (ArkanoidState*)acquire_mutex_block(&state_mutex);
|
|
|
|
if(event_status == FuriStatusOk) {
|
|
// Key events
|
|
if(event.type == EventTypeKey) {
|
|
if(event.input.type == InputTypePress || event.input.type == InputTypeLong ||
|
|
event.input.type == InputTypeRepeat) {
|
|
switch(event.input.key) {
|
|
case InputKeyBack:
|
|
processing = false;
|
|
break;
|
|
case InputKeyRight:
|
|
if(arkanoid_state->xPaddle < FLIPPER_LCD_WIDTH - 12) {
|
|
arkanoid_state->xPaddle += 8;
|
|
}
|
|
break;
|
|
case InputKeyLeft:
|
|
if(arkanoid_state->xPaddle > 0) {
|
|
arkanoid_state->xPaddle -= 8;
|
|
}
|
|
break;
|
|
case InputKeyUp:
|
|
if(arkanoid_state->speed < MAX_SPEED) {
|
|
arkanoid_state->speed++;
|
|
}
|
|
break;
|
|
case InputKeyDown:
|
|
if(arkanoid_state->speed > 1) {
|
|
arkanoid_state->speed--;
|
|
}
|
|
break;
|
|
case InputKeyOk:
|
|
if(arkanoid_state->gameStarted == false) {
|
|
//Release ball if FIRE pressed
|
|
arkanoid_state->ball_state.released = true;
|
|
|
|
//Apply random direction to ball on release
|
|
if(rand_range(0, 2) == 0) {
|
|
arkanoid_state->ball_state.dx = 1;
|
|
} else {
|
|
arkanoid_state->ball_state.dx = -1;
|
|
}
|
|
|
|
//Makes sure the ball heads upwards
|
|
arkanoid_state->ball_state.dy = -1;
|
|
//start the game flag
|
|
arkanoid_state->gameStarted = true;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
view_port_update(view_port);
|
|
release_mutex(&state_mutex, arkanoid_state);
|
|
}
|
|
furi_timer_free(timer);
|
|
view_port_enabled_set(view_port, false);
|
|
gui_remove_view_port(gui, view_port);
|
|
furi_record_close(RECORD_GUI);
|
|
furi_record_close(RECORD_NOTIFICATION);
|
|
view_port_free(view_port);
|
|
delete_mutex(&state_mutex);
|
|
|
|
free_and_exit:
|
|
free(arkanoid_state);
|
|
furi_message_queue_free(event_queue);
|
|
|
|
return return_code;
|
|
}
|