From a17006580448e4cfbf031ad4335a968a90d1606e Mon Sep 17 00:00:00 2001 From: jbohack Date: Sun, 8 Jan 2023 04:19:36 -0500 Subject: [PATCH] added pong & asteroids https://github.com/antirez/flipper-asteroids https://github.com/nmrr/flipperzero-pong --- applications/plugins/asteroids/app.c | 689 ++++++++++++++++++ applications/plugins/asteroids/appicon.png | Bin 0 -> 145 bytes .../plugins/asteroids/application.fam | 12 + applications/plugins/pong/application.fam | 13 + applications/plugins/pong/flipper_pong.c | 298 ++++++++ applications/plugins/pong/pong.png | Bin 0 -> 6459 bytes 6 files changed, 1012 insertions(+) create mode 100644 applications/plugins/asteroids/app.c create mode 100644 applications/plugins/asteroids/appicon.png create mode 100644 applications/plugins/asteroids/application.fam create mode 100644 applications/plugins/pong/application.fam create mode 100644 applications/plugins/pong/flipper_pong.c create mode 100644 applications/plugins/pong/pong.png diff --git a/applications/plugins/asteroids/app.c b/applications/plugins/asteroids/app.c new file mode 100644 index 000000000..1a3945fd7 --- /dev/null +++ b/applications/plugins/asteroids/app.c @@ -0,0 +1,689 @@ +/* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved + * See the LICENSE file for information about the license. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PI +#define PI 3.14159265358979f +#endif + +#define TAG "Asteroids" // Used for logging +#define DEBUG_MSG 1 +#define SCREEN_XRES 128 +#define SCREEN_YRES 64 +#define GAME_START_LIVES 3 + +/* The game uses the OK button both to fire and to accelerate the ship. + * This makes it a lot more playable since the finger does not have to + * move between two keys. However it is important that the extra time the + * player needs to press the button to accelerate instead of just firing + * is precisely selected to provide a smooth experience. After a few + * attempts, it looks like 70 milliseconds is the right spot. */ +#define SHIP_ACCELERATION_KEYPRESS_TIME 70 + +/* ============================ Data structures ============================= */ + +typedef struct Ship { + float x, /* Ship x position. */ + y, /* Ship y position. */ + vx, /* x velocity. */ + vy, /* y velocity. */ + rot; /* Current rotation. 2*PI full rotation. */ +} Ship; + +typedef struct Bullet { + float x, y, vx, vy; /* Fields like in ship. */ + uint32_t ttl; /* Time to live, in ticks. */ +} Bullet; + +typedef struct Asteroid { + float x, y, vx, vy, rot, /* Fields like ship. */ + rot_speed, /* Angular velocity (rot speed and sense). */ + size; /* Asteroid size. */ + uint8_t shape_seed; /* Seed to give random shape. */ +} Asteroid; + +#define MAXBUL 10 /* Max bullets on the screen. */ +#define MAXAST 32 /* Max asteroids on the screen. */ +#define SHIP_HIT_ANIMATION_LEN 15 +typedef struct AsteroidsApp { + /* GUI */ + Gui *gui; + ViewPort *view_port; /* We just use a raw viewport and we render + everything into the low level canvas. */ + FuriMessageQueue *event_queue; /* Key press events go here. */ + + /* Game state. */ + int running; /* Once false exists the app. */ + bool gameover; /* Game over status. */ + uint32_t ticks; /* Game ticks. Increments at each refresh. */ + uint32_t score; /* Game score. */ + uint32_t lives; /* Number of lives in the current game. */ + uint32_t ship_hit; /* When non zero, the ship was hit by an asteroid + and we need to show an animation as long as + its value is non-zero (and decrease it's value + at each tick of animation). */ + + /* Ship state. */ + struct Ship ship; + + /* Bullets state. */ + struct Bullet bullets[MAXBUL]; /* Each bullet state. */ + int bullets_num; /* Active bullets. */ + uint32_t last_bullet_tick; /* Tick the last bullet was fired. */ + + /* Asteroids state. */ + Asteroid asteroids[MAXAST]; /* Each asteroid state. */ + int asteroids_num; /* Active asteroids. */ + + uint32_t pressed[InputKeyMAX]; /* pressed[id] is true if pressed. + Each array item contains the time + in milliseconds the key was pressed. */ + bool fire; /* Short press detected: fire a bullet. */ +} AsteroidsApp; + +/* ============================== Prototypes ================================ */ + +// Only functions called before their definition are here. + +void restart_game_after_gameover(AsteroidsApp *app); +uint32_t key_pressed_time(AsteroidsApp *app, InputKey key); + +/* ============================ 2D drawing ================================== */ + +/* This structure represents a polygon of at most POLY_MAX points. + * The function draw_poly() is able to render it on the screen, rotated + * by the amount specified. */ +#define POLY_MAX 8 +typedef struct Poly { + float x[POLY_MAX]; + float y[POLY_MAX]; + uint32_t points; /* Number of points actually populated. */ +} Poly; + +/* Define the polygons we use. */ +Poly ShipPoly = { + {-3, 0, 3}, + {-3, 6, -3}, + 3 +}; + +Poly ShipFirePoly = { + {-1.5, 0, 1.5}, + {-3, -6, -3}, + 3 +}; + +/* Rotate the point of the polygon 'poly' and store the new rotated + * polygon in 'rot'. The polygon is rotated by an angle 'a', with + * center at 0,0. */ +void rotate_poly(Poly *rot, Poly *poly, float a) { + /* We want to compute sin(a) and cos(a) only one time + * for every point to rotate. It's a slow operation. */ + float sin_a = (float)sin(a); + float cos_a = (float)cos(a); + for (uint32_t j = 0; j < poly->points; j++) { + rot->x[j] = poly->x[j]*cos_a - poly->y[j]*sin_a; + rot->y[j] = poly->y[j]*cos_a + poly->x[j]*sin_a; + } + rot->points = poly->points; +} + +/* This is an 8 bit LFSR we use to generate a predictable and fast + * pseudorandom sequence of numbers, to give a different shape to + * each asteroid. */ +void lfsr_next(unsigned char *prev) { + unsigned char lsb = *prev & 1; + *prev = *prev >> 1; + if (lsb == 1) *prev ^= 0b11000111; + *prev ^= *prev<<7; /* Mix things a bit more. */ +} + +/* Render the polygon 'poly' at x,y, rotated by the specified angle. */ +void draw_poly(Canvas *const canvas, Poly *poly, uint8_t x, uint8_t y, float a) +{ + Poly rot; + rotate_poly(&rot,poly,a); + canvas_set_color(canvas, ColorBlack); + for (uint32_t j = 0; j < rot.points; j++) { + uint32_t a = j; + uint32_t b = j+1; + if (b == rot.points) b = 0; + canvas_draw_line(canvas,x+rot.x[a],y+rot.y[a], + x+rot.x[b],y+rot.y[b]); + } +} + +/* A bullet is just a + pixels pattern. A single pixel is not + * visible enough. */ +void draw_bullet(Canvas *const canvas, Bullet *b) { + canvas_draw_dot(canvas,b->x-1,b->y); + canvas_draw_dot(canvas,b->x+1,b->y); + canvas_draw_dot(canvas,b->x,b->y); + canvas_draw_dot(canvas,b->x,b->y-1); + canvas_draw_dot(canvas,b->x,b->y+1); +} + +/* Draw an asteroid. The asteroid shapes is computed on the fly and + * is not stored in a permanent shape structure. In order to generate + * the shape, we use an initial fixed shape that we resize according + * to the asteroid size, perturbed according to the asteroid shape + * seed, and finally draw it rotated of the right amount. */ +void draw_asteroid(Canvas *const canvas, Asteroid *ast) { + Poly ap; + + /* Start with what is kinda of a circle. Note that this could be + * stored into a template and copied here, to avoid computing + * sin() / cos(). But the Flipper can handle it without problems. */ + uint8_t r = ast->shape_seed; + for (int j = 0; j < 8; j++) { + float a = (PI*2)/8*j; + + /* Before generating the point, to make the shape unique generate + * a random factor between .7 and 1.3 to scale the distance from + * the center. However this asteroid should have its unique shape + * that remains always the same, so we use a predictable PRNG + * implemented by an 8 bit shift register. */ + lfsr_next(&r); + float scaling = .7+((float)r/255*.6); + + ap.x[j] = (float)sin(a) * ast->size * scaling; + ap.y[j] = (float)cos(a) * ast->size * scaling; + } + ap.points = 8; + draw_poly(canvas,&ap,ast->x,ast->y,ast->rot); +} + +/* Draw small ships in the top-right part of the screen, one for + * each left live. */ +void draw_left_lives(Canvas *const canvas, AsteroidsApp *app) { + int lives = app->lives; + int x = SCREEN_XRES-5; + + Poly mini_ship = { + {-2, 0, 2}, + {-2, 4, -2}, + 3 + }; + while(lives--) { + draw_poly(canvas,&mini_ship,x,6,PI); + x -= 6; + } +} + +/* Given the current position, update it according to the velocity and + * wrap it back to the other side if the object went over the screen. */ +void update_pos_by_velocity(float *x, float *y, float vx, float vy) { + /* Return back from one side to the other of the screen. */ + *x += vx; + *y += vy; + if (*x >= SCREEN_XRES) *x = 0; + else if (*x < 0) *x = SCREEN_XRES-1; + if (*y >= SCREEN_YRES) *y = 0; + else if (*y < 0) *y = SCREEN_YRES-1; +} + +/* Render the current game screen. */ +void render_callback(Canvas *const canvas, void *ctx) { + AsteroidsApp *app = ctx; + + /* Clear screen. */ + canvas_set_color(canvas, ColorWhite); + canvas_draw_box(canvas, 0, 0, SCREEN_XRES-1, SCREEN_YRES-1); + + /* Draw score. */ + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontSecondary); + char score[32]; + snprintf(score,sizeof(score),"%lu",app->score); + canvas_draw_str(canvas, 0, 8, score); + + /* Draw left ships. */ + draw_left_lives(canvas,app); + + /* Draw ship, asteroids, bullets. */ + draw_poly(canvas,&ShipPoly,app->ship.x,app->ship.y,app->ship.rot); + if (key_pressed_time(app,InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME) + draw_poly(canvas,&ShipFirePoly,app->ship.x,app->ship.y,app->ship.rot); + + for (int j = 0; j < app->bullets_num; j++) + draw_bullet(canvas,&app->bullets[j]); + + for (int j = 0; j < app->asteroids_num; j++) + draw_asteroid(canvas,&app->asteroids[j]); + + /* Game over text. */ + if (app->gameover) { + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 28, 35, "GAME OVER"); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 25, 50, "Press OK to restart"); + } +} + +/* ============================ Game logic ================================== */ + +float distance(float x1, float y1, float x2, float y2) { + float dx = x1-x2; + float dy = y1-y2; + return sqrt(dx*dx+dy*dy); +} + +/* Detect a collision between the object at x1,y1 of radius r1 and + * the object at x2, y2 of radius r2. A factor < 1 will make the + * function detect the collision even if the objects are yet not + * relly touching, while a factor > 1 will make it detect the collision + * only after they are a bit overlapping. It basically is used to + * rescale the distance. + * + * Note that in this simplified 2D world, objects are all considered + * spheres (this is why this function only takes the radius). This + * is, after all, kinda accurate for asteroids, for bullets, and + * even for the ship "core" itself. */ +bool objects_are_colliding(float x1, float y1, float r1, + float x2, float y2, float r2, + float factor) +{ + /* The objects are colliding if the distance between object 1 and 2 + * is smaller than the sum of the two radiuses r1 and r2. + * So it would be like: sqrt((x1-x2)^2+(y1-y2)^2) < r1+r2. + * However we can avoid computing the sqrt (which is slow) by + * squaring the second term and removing the square root, making + * the comparison like this: + * + * (x1-x2)^2+(y1-y2)^2 < (r1+r2)^2. */ + float dx = (x1-x2)*factor; + float dy = (y1-y2)*factor; + float rsum = r1+r2; + return dx*dx+dy*dy < rsum*rsum; +} + +/* Create a new bullet headed in the same direction of the ship. */ +void ship_fire_bullet(AsteroidsApp *app) { + if (app->bullets_num == MAXBUL) return; + Bullet *b = &app->bullets[app->bullets_num]; + b->x = app->ship.x; + b->y = app->ship.y; + b->vx = -sin(app->ship.rot); + b->vy = cos(app->ship.rot); + + /* Ship should fire from its head, not in the middle. */ + b->x += b->vx*5; + b->y += b->vy*5; + + /* Give the bullet some velocity (for now the vector is just + * normalized to 1). */ + b->vx *= 3; + b->vy *= 3; + + /* It's more realistic if we add the velocity vector of the + * ship, too. Otherwise if the ship is going fast the bullets + * will be slower, which is not how the world works. */ + b->vx += app->ship.vx; + b->vy += app->ship.vy; + + b->ttl = 50; /* The bullet will disappear after N ticks. */ + app->bullets_num++; +} + +/* Remove the specified bullet by id (index in the array). */ +void remove_bullet(AsteroidsApp *app, int bid) { + /* Replace the top bullet with the empty space left + * by the removal of this bullet. This way we always take the + * array dense, which is an advantage when looping. */ + int n = --app->bullets_num; + if (n && bid != n) app->bullets[bid] = app->bullets[n]; +} + +/* Create a new asteroid, away from the ship. Return the + * pointer to the asteroid object, so that the caller can change + * certain things of the asteroid if needed. */ +Asteroid *add_asteroid(AsteroidsApp *app) { + if (app->asteroids_num == MAXAST) return NULL; + float size = 4+rand()%15; + float min_distance = 20; + float x,y; + do { + x = rand() % SCREEN_XRES; + y = rand() % SCREEN_YRES; + } while(distance(app->ship.x,app->ship.y,x,y) < min_distance+size); + Asteroid *a = &app->asteroids[app->asteroids_num++]; + a->x = x; + a->y = y; + a->vx = 2*(-.5 + ((float)rand()/RAND_MAX)); + a->vy = 2*(-.5 + ((float)rand()/RAND_MAX)); + a->size = size; + a->rot = 0; + a->rot_speed = ((float)rand()/RAND_MAX)/10; + if (app->ticks & 1) a->rot_speed = -(a->rot_speed); + a->shape_seed = rand() & 255; + return a; +} + +/* Remove the specified asteroid by id (index in the array). */ +void remove_asteroid(AsteroidsApp *app, int id) { + /* Replace the top asteroid with the empty space left + * by the removal of this one. This way we always take the + * array dense, which is an advantage when looping. */ + int n = --app->asteroids_num; + if (n && id != n) app->asteroids[id] = app->asteroids[n]; +} + +/* Called when an asteroid was reached by a bullet. The asteroid + * hit is the one with the specified 'id'. */ +void asteroid_was_hit(AsteroidsApp *app, int id) { + float sizelimit = 6; // Smaller than that, they disappear in one shot. + Asteroid *a = &app->asteroids[id]; + + /* Asteroid is large enough to break into fragments. */ + float size = a->size; + float x = a->x, y = a->y; + remove_asteroid(app,id); + if (size > sizelimit) { + int max_fragments = size / sizelimit; + int fragments = 2+rand()%max_fragments; + float newsize = size/fragments; + if (newsize < 2) newsize = 2; + for (int j = 0; j < fragments; j++) { + a = add_asteroid(app); + if (a == NULL) break; // Too many asteroids on screen. + a->x = x + -(size/2) + rand() % (int)newsize; + a->y = y + -(size/2) + rand() % (int)newsize; + a->size = newsize; + } + } else { + app->score++; + } +} + +/* Set game over state. When in game-over mode, the game displays a + * game over text with a background of many asteroids floating around. */ +void game_over(AsteroidsApp *app) { + restart_game_after_gameover(app); + app->gameover = true; + int asteroids = 8; + while(asteroids-- && add_asteroid(app) != NULL); +} + +/* Function called when a collision between the asteroid and the + * ship is detected. */ +void ship_was_hit(AsteroidsApp *app) { + app->ship_hit = SHIP_HIT_ANIMATION_LEN; + if (app->lives) { + app->lives--; + } else { + game_over(app); + } +} + +/* Restart game after the ship is hit. Will reset the ship position, bullets + * and asteroids to restart the game. */ +void restart_game(AsteroidsApp *app) { + app->ship.x = SCREEN_XRES / 2; + app->ship.y = SCREEN_YRES / 2; + app->ship.rot = PI; /* Start headed towards top. */ + app->ship.vx = 0; + app->ship.vy = 0; + app->bullets_num = 0; + app->last_bullet_tick = 0; + app->asteroids_num = 0; +} + +/* Called after game over to restart the game. This function + * also calls restart_game(). */ +void restart_game_after_gameover(AsteroidsApp *app) { + app->gameover = false; + app->ticks = 0; + app->score = 0; + app->ship_hit = 0; + app->lives = GAME_START_LIVES-1; /* -1 to account for current one. */ + restart_game(app); +} + +/* Move bullets. */ +void update_bullets_position(AsteroidsApp *app) { + for (int j = 0; j < app->bullets_num; j++) { + update_pos_by_velocity(&app->bullets[j].x,&app->bullets[j].y, + app->bullets[j].vx,app->bullets[j].vy); + if (--app->bullets[j].ttl == 0) { + remove_bullet(app,j); + j--; /* Process this bullet index again: the removal will + fill it with the top bullet to take the array dense. */ + } + } +} + +/* Move asteroids. */ +void update_asteroids_position(AsteroidsApp *app) { + for (int j = 0; j < app->asteroids_num; j++) { + update_pos_by_velocity(&app->asteroids[j].x,&app->asteroids[j].y, + app->asteroids[j].vx,app->asteroids[j].vy); + app->asteroids[j].rot += app->asteroids[j].rot_speed; + if (app->asteroids[j].rot < 0) app->asteroids[j].rot = 2*PI; + else if (app->asteroids[j].rot > 2*PI) app->asteroids[j].rot = 0; + } +} + +/* Collision detection and game state update based on collisions. */ +void detect_collisions(AsteroidsApp *app) { + /* Detect collision between bullet and asteroid. */ + for (int j = 0; j < app->bullets_num; j++) { + Bullet *b = &app->bullets[j]; + for (int i = 0; i < app->asteroids_num; i++) { + Asteroid *a = &app->asteroids[i]; + if (objects_are_colliding(a->x, a->y, a->size, + b->x, b->y, 1.5, 1)) + { + asteroid_was_hit(app,i); + remove_bullet(app,j); + /* The bullet no longer exist. Break the loop. + * However we want to start processing from the + * same bullet index, since now it is used by + * another bullet (see remove_bullet()). */ + j--; /* Scan this j value again. */ + break; + } + } + } + + /* Detect collision between ship and asteroid. */ + for (int j = 0; j < app->asteroids_num; j++) { + Asteroid *a = &app->asteroids[j]; + if (objects_are_colliding(a->x, a->y, a->size, + app->ship.x, app->ship.y, 4, 1)) + { + ship_was_hit(app); + break; + } + } +} + +/* This is the main game execution function, called 10 times for + * second (with the Flipper screen latency, an higher FPS does not + * make sense). In this function we update the position of objects based + * on velocity. Detect collisions. Update the score and so forth. + * + * Each time this function is called, app->tick is incremented. */ +void game_tick(void *ctx) { + AsteroidsApp *app = ctx; + + /* There are two special screens: + * + * 1. Ship was hit, we frozen the game as long as ship_hit isn't zero + * again, and show an animation of a rotating ship. */ + if (app->ship_hit) { + app->ship.rot += 0.5; + app->ship_hit--; + view_port_update(app->view_port); + if (app->ship_hit == 0) { + restart_game(app); + } + return; + } else if (app->gameover) { + /* 2. Game over. We need to update only background asteroids. In this + * state the game just displays a GAME OVER text with the floating + * asteroids in background. */ + if (key_pressed_time(app,InputKeyOk) > 100) { + restart_game_after_gameover(app); + } + update_asteroids_position(app); + view_port_update(app->view_port); + return; + } + + /* Handle key presses. */ + if (app->pressed[InputKeyLeft]) app->ship.rot -= .35; + if (app->pressed[InputKeyRight]) app->ship.rot += .35; + if (key_pressed_time(app,InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME) { + app->ship.vx -= 0.5*(float)sin(app->ship.rot); + app->ship.vy += 0.5*(float)cos(app->ship.rot); + } else if (app->pressed[InputKeyDown]) { + app->ship.vx *= 0.75; + app->ship.vy *= 0.75; + } + + /* Fire a bullet if needed. app->fire is set in + * asteroids_update_keypress_state() since depends on exact + * pressure timing. */ + if (app->fire) { + uint32_t bullet_min_period = 200; // In milliseconds + uint32_t now = furi_get_tick(); + if (now - app->last_bullet_tick >= bullet_min_period) { + ship_fire_bullet(app); + app->last_bullet_tick = now; + } + app->fire = false; + } + + /* Update positions and detect collisions. */ + update_pos_by_velocity(&app->ship.x,&app->ship.y,app->ship.vx,app->ship.vy); + update_bullets_position(app); + update_asteroids_position(app); + detect_collisions(app); + + /* From time to time, create a new asteroid. The more asteroids + * already on the screen, the smaller probability of creating + * a new one. */ + if (app->asteroids_num == 0 || + (random() % 5000) < (30/(1+app->asteroids_num))) + { + add_asteroid(app); + } + + app->ticks++; + view_port_update(app->view_port); +} + +/* ======================== Flipper specific code =========================== */ + +/* Here all we do is putting the events into the queue that will be handled + * in the while() loop of the app entry point function. */ +void input_callback(InputEvent* input_event, void* ctx) +{ + AsteroidsApp *app = ctx; + furi_message_queue_put(app->event_queue,input_event,FuriWaitForever); +} + +/* Allocate the application state and initialize a number of stuff. + * This is called in the entry point to create the application state. */ +AsteroidsApp* asteroids_app_alloc() { + AsteroidsApp *app = malloc(sizeof(AsteroidsApp)); + + app->gui = furi_record_open(RECORD_GUI); + app->view_port = view_port_alloc(); + view_port_draw_callback_set(app->view_port, render_callback, app); + view_port_input_callback_set(app->view_port, input_callback, app); + gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen); + app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + app->running = 1; /* Turns 0 when back is pressed. */ + restart_game_after_gameover(app); + memset(app->pressed,0,sizeof(app->pressed)); + return app; +} + +/* Free what the application allocated. It is not clear to me if the + * Flipper OS, once the application exits, will be able to reclaim space + * even if we forget to free something here. */ +void asteroids_app_free(AsteroidsApp *app) { + furi_assert(app); + + // View related. + view_port_enabled_set(app->view_port, false); + gui_remove_view_port(app->gui, app->view_port); + view_port_free(app->view_port); + furi_record_close(RECORD_GUI); + furi_message_queue_free(app->event_queue); + app->gui = NULL; + + free(app); +} + +/* Return the time in milliseconds the specified key is continuously + * pressed. Or 0 if it is not pressed. */ +uint32_t key_pressed_time(AsteroidsApp *app, InputKey key) { + return app->pressed[key] == 0 ? 0 : + furi_get_tick() - app->pressed[key]; +} + +/* Handle keys interaction. */ +void asteroids_update_keypress_state(AsteroidsApp *app, InputEvent input) { + if (input.type == InputTypePress) { + app->pressed[input.key] = furi_get_tick(); + } else if (input.type == InputTypeRelease) { + uint32_t dur = key_pressed_time(app,input.key); + app->pressed[input.key] = 0; + if (dur < 200 && input.key == InputKeyOk) app->fire = true; + } +} + +int32_t asteroids_app_entry(void* p) { + UNUSED(p); + AsteroidsApp *app = asteroids_app_alloc(); + + /* Create a timer. We do data analysis in the callback. */ + FuriTimer *timer = furi_timer_alloc(game_tick, FuriTimerTypePeriodic, app); + furi_timer_start(timer, furi_kernel_get_tick_frequency() / 10); + + /* This is the main event loop: here we get the events that are pushed + * in the queue by input_callback(), and process them one after the + * other. */ + InputEvent input; + while(app->running) { + FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100); + if (qstat == FuriStatusOk) { + if (DEBUG_MSG) FURI_LOG_E(TAG, "Main Loop - Input: type %d key %u", + input.type, input.key); + + /* Handle navigation here. Then handle view-specific inputs + * in the view specific handling function. */ + if (input.type == InputTypeShort && + input.key == InputKeyBack) + { + app->running = 0; + } else { + asteroids_update_keypress_state(app,input); + } + } else { + /* Useful to understand if the app is still alive when it + * does not respond because of bugs. */ + if (DEBUG_MSG) { + static int c = 0; c++; + if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout"); + } + } + } + + furi_timer_free(timer); + asteroids_app_free(app); + return 0; +} diff --git a/applications/plugins/asteroids/appicon.png b/applications/plugins/asteroids/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..45da095afff23ff1fc2bef96fbcf7f1aef080b1d GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>@%D6a4ABVg zoe;>!pupku_+R_7hiS&a;qkoj3XLk^#SAxGI9R++)Lbbs-(Vzj@Xa5Wa%;D}J$mI~ rn)zmy^2gWhwBNUA|LKqg|L?J#@$-JO=1CYk&{zgfS3j3^P6 +#include +#include +#include +#include +#include + +#define SCREEN_SIZE_X 128 +#define SCREEN_SIZE_Y 64 +#define FPS 20 + +#define PAD_SIZE_X 3 +#define PAD_SIZE_Y 8 +#define PLAYER1_PAD_SPEED 2 +#define PLAYER2_PAD_SPEED 2 +#define BALL_SIZE 4 + +typedef enum { + EventTypeInput, + ClockEventTypeTick, +} EventType; + +typedef struct { + EventType type; + InputEvent input; +} EventApp; + +typedef struct Players +{ + uint8_t player1_X,player1_Y,player2_X,player2_Y; + uint16_t player1_score,player2_score; + uint8_t ball_X,ball_Y,ball_X_speed,ball_Y_speed,ball_X_direction,ball_Y_direction; +} Players; + +static void draw_callback(Canvas* canvas, void* ctx) +{ + UNUSED(ctx); + Players* playersMutex = (Players*)acquire_mutex_block((ValueMutex*)ctx); + + canvas_draw_frame(canvas, 0, 0, 128, 64); + canvas_draw_box(canvas, playersMutex->player1_X, playersMutex->player1_Y, PAD_SIZE_X, PAD_SIZE_Y); + canvas_draw_box(canvas, playersMutex->player2_X, playersMutex->player2_Y, PAD_SIZE_X, PAD_SIZE_Y); + canvas_draw_box(canvas, playersMutex->ball_X, playersMutex->ball_Y, BALL_SIZE, BALL_SIZE); + + canvas_set_font(canvas, FontPrimary); + canvas_set_font_direction(canvas, CanvasDirectionBottomToTop); + char buffer[16]; + snprintf(buffer, sizeof(buffer), "%u - %u", playersMutex->player1_score, playersMutex->player2_score); + canvas_draw_str_aligned(canvas, SCREEN_SIZE_X/2+15, SCREEN_SIZE_Y/2+2, AlignCenter, AlignTop, buffer); + + release_mutex((ValueMutex*)ctx, playersMutex); +} + +static void input_callback(InputEvent* input_event, void* ctx) +{ + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + EventApp event = {.type = EventTypeInput, .input = *input_event}; + furi_message_queue_put(event_queue, &event, FuriWaitForever); +} + +static void clock_tick(void* ctx) { + furi_assert(ctx); + FuriMessageQueue* queue = ctx; + EventApp event = {.type = ClockEventTypeTick}; + furi_message_queue_put(queue, &event, 0); +} + +bool insidePad(uint8_t x, uint8_t y, uint8_t playerX, uint8_t playerY) +{ + if (x >= playerX && x <= playerX+PAD_SIZE_X && y >= playerY && y <= playerY+PAD_SIZE_Y) return true; + return false; +} + +uint8_t changeSpeed() +{ + uint8_t randomuint8[1]; + while(1) + { + furi_hal_random_fill_buf(randomuint8,1); + randomuint8[0] &= 0b00000011; + if (randomuint8[0] >= 1) break; + } + return randomuint8[0]; +} + +uint8_t changeDirection() +{ + uint8_t randomuint8[1]; + furi_hal_random_fill_buf(randomuint8,1); + randomuint8[0] &= 0b1; + return randomuint8[0]; +} + +int32_t flipper_pong_app() +{ + EventApp event; + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(EventApp)); + + Players players; + players.player1_X = SCREEN_SIZE_X-PAD_SIZE_X-1; + players.player1_Y = SCREEN_SIZE_Y/2 - PAD_SIZE_Y/2; + players.player1_score = 0; + + players.player2_X = 1; + players.player2_Y = SCREEN_SIZE_Y/2 - PAD_SIZE_Y/2; + players.player2_score = 0; + + players.ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2; + players.ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2; + players.ball_X_speed = 1; + players.ball_Y_speed = 1; + players.ball_X_direction = changeDirection(); + players.ball_Y_direction = changeDirection(); + + ValueMutex state_mutex; + init_mutex(&state_mutex, &players, sizeof(Players)); + + ViewPort* view_port = view_port_alloc(); + view_port_draw_callback_set(view_port, draw_callback, &state_mutex); + view_port_input_callback_set(view_port, input_callback, event_queue); + + Gui* gui = furi_record_open(RECORD_GUI); + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + if (players.ball_X_direction == 0) notification_message(notification, &sequence_set_only_red_255); + else notification_message(notification, &sequence_set_only_blue_255); + + FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, event_queue); + furi_timer_start(timer, 1000/FPS); + + while(1) + { + FuriStatus event_status = furi_message_queue_get(event_queue, &event, FuriWaitForever); + Players* playersMutex = (Players*)acquire_mutex_block(&state_mutex); + + if (event_status == FuriStatusOk) + { + if(event.type == EventTypeInput) + { + if(event.input.key == InputKeyBack) + { + release_mutex(&state_mutex, playersMutex); + notification_message(notification, &sequence_set_only_green_255); + break; + } + else if(event.input.key == InputKeyUp) + { + if (playersMutex->player1_Y >= 1+PLAYER1_PAD_SPEED) playersMutex->player1_Y -= PLAYER1_PAD_SPEED; + else playersMutex->player1_Y = 1; + } + else if(event.input.key == InputKeyDown) + { + if (playersMutex->player1_Y <= SCREEN_SIZE_Y - PAD_SIZE_Y - PLAYER1_PAD_SPEED -1) playersMutex->player1_Y += PLAYER1_PAD_SPEED; + else playersMutex->player1_Y = SCREEN_SIZE_Y - PAD_SIZE_Y - 1; + } + } + else if (event.type == ClockEventTypeTick) + { + + if (playersMutex->ball_X + BALL_SIZE/2 <= SCREEN_SIZE_X*0.35 && playersMutex->ball_X_direction == 0) + { + if (playersMutex->ball_Y + BALL_SIZE/2 < playersMutex->player2_Y + PAD_SIZE_Y/2) + { + if (playersMutex->player2_Y >= 1+PLAYER2_PAD_SPEED) playersMutex->player2_Y -= PLAYER2_PAD_SPEED; + else playersMutex->player2_Y= 1; + } + else if (playersMutex->ball_Y + BALL_SIZE/2 > playersMutex->player2_Y + PAD_SIZE_Y/2) + { + if (playersMutex->player2_Y <= SCREEN_SIZE_Y - PAD_SIZE_Y - PLAYER2_PAD_SPEED -1) playersMutex->player2_Y += PLAYER2_PAD_SPEED; + else playersMutex->player2_Y = SCREEN_SIZE_Y - PAD_SIZE_Y - 1; + } + } + + uint8_t ball_corner_X[4] = {playersMutex->ball_X, playersMutex->ball_X + BALL_SIZE, playersMutex->ball_X + BALL_SIZE, playersMutex->ball_X}; + uint8_t ball_corner_Y[4] = {playersMutex->ball_Y, playersMutex->ball_Y, playersMutex->ball_Y + BALL_SIZE, playersMutex->ball_Y + BALL_SIZE}; + bool insidePlayer1 = false, insidePlayer2 = false; + + for (int i=0;i<4;i++) + { + if (insidePad(ball_corner_X[i], ball_corner_Y[i], playersMutex->player1_X, playersMutex->player1_Y) == true) + { + insidePlayer1 = true; + break; + } + + if (insidePad(ball_corner_X[i], ball_corner_Y[i], playersMutex->player2_X, playersMutex->player2_Y) == true) + { + insidePlayer2 = true; + break; + } + } + + if (insidePlayer1 == true) + { + playersMutex->ball_X_direction = 0; + playersMutex->ball_X -= playersMutex->ball_X_speed; + playersMutex->ball_X_speed = changeSpeed(); + playersMutex->ball_Y_speed = changeSpeed(); + notification_message(notification, &sequence_set_only_red_255); + } + else if (insidePlayer2 == true) + { + playersMutex->ball_X_direction = 1; + playersMutex->ball_X += playersMutex->ball_X_speed; + playersMutex->ball_X_speed = changeSpeed(); + playersMutex->ball_Y_speed = changeSpeed(); + notification_message(notification, &sequence_set_only_blue_255); + } + else + { + if (playersMutex->ball_X_direction == 1) + { + + if (playersMutex->ball_X <= SCREEN_SIZE_X - BALL_SIZE - 1 - playersMutex->ball_X_speed) + { + playersMutex->ball_X += playersMutex->ball_X_speed; + } + else + { + playersMutex->ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2; + playersMutex->ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2; + playersMutex->ball_X_speed = 1; + playersMutex->ball_Y_speed = 1; + playersMutex->ball_X_direction = 0; + playersMutex->player2_score++; + notification_message(notification, &sequence_set_only_red_255); + } + } + else + { + if (playersMutex->ball_X >= 1 + playersMutex->ball_X_speed) + { + playersMutex->ball_X -= playersMutex->ball_X_speed; + } + else + { + playersMutex->ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2; + playersMutex->ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2; + playersMutex->ball_X_speed = 1; + playersMutex->ball_Y_speed = 1; + playersMutex->ball_X_direction = 1; + playersMutex->player1_score++; + notification_message(notification, &sequence_set_only_blue_255); + } + } + } + + if (playersMutex->ball_Y_direction == 1) + { + if (playersMutex->ball_Y <= SCREEN_SIZE_Y - BALL_SIZE - 1 - playersMutex->ball_Y_speed) + { + playersMutex->ball_Y += playersMutex->ball_Y_speed; + } + else + { + playersMutex->ball_Y = SCREEN_SIZE_Y - BALL_SIZE - 1; + playersMutex->ball_X_speed = changeSpeed(); + playersMutex->ball_Y_speed = changeSpeed(); + playersMutex->ball_Y_direction = 0; + } + } + else + { + if (playersMutex->ball_Y >= 1 + playersMutex->ball_Y_speed) + { + playersMutex->ball_Y -= playersMutex->ball_Y_speed; + } + else + { + playersMutex->ball_Y = 1; + playersMutex->ball_X_speed = changeSpeed(); + playersMutex->ball_Y_speed = changeSpeed(); + playersMutex->ball_Y_direction = 1; + } + } + } + } + + release_mutex(&state_mutex, playersMutex); + view_port_update(view_port); + } + + furi_message_queue_free(event_queue); + delete_mutex(&state_mutex); + gui_remove_view_port(gui, view_port); + view_port_free(view_port); + furi_timer_free(timer); + furi_record_close(RECORD_GUI); + furi_record_close(RECORD_NOTIFICATION); + + return 0; +} \ No newline at end of file diff --git a/applications/plugins/pong/pong.png b/applications/plugins/pong/pong.png new file mode 100644 index 0000000000000000000000000000000000000000..507ce711c0f97ee36d4c2d76800d0ba5463974da GIT binary patch literal 6459 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 literal 0 HcmV?d00001