add tetris game & games menu & 2 icons
@ -21,7 +21,6 @@ extern int32_t accessor_app(void* p);
|
|||||||
extern int32_t archive_app(void* p);
|
extern int32_t archive_app(void* p);
|
||||||
extern int32_t bad_usb_app(void* p);
|
extern int32_t bad_usb_app(void* p);
|
||||||
extern int32_t u2f_app(void* p);
|
extern int32_t u2f_app(void* p);
|
||||||
extern int32_t wav_player_app(void* p);
|
|
||||||
extern int32_t uart_echo_app(void* p);
|
extern int32_t uart_echo_app(void* p);
|
||||||
extern int32_t blink_test_app(void* p);
|
extern int32_t blink_test_app(void* p);
|
||||||
extern int32_t bt_debug_app(void* p);
|
extern int32_t bt_debug_app(void* p);
|
||||||
@ -49,7 +48,11 @@ extern int32_t file_browser_app(void* p);
|
|||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
extern int32_t music_player_app(void* p);
|
extern int32_t music_player_app(void* p);
|
||||||
|
extern int32_t wav_player_app(void* p);
|
||||||
|
|
||||||
|
// Games
|
||||||
extern int32_t snake_game_app(void* p);
|
extern int32_t snake_game_app(void* p);
|
||||||
|
extern int32_t tetris_game_app(void *p);
|
||||||
|
|
||||||
// On system start hooks declaration
|
// On system start hooks declaration
|
||||||
extern void bt_on_system_start();
|
extern void bt_on_system_start();
|
||||||
@ -344,14 +347,6 @@ const FlipperApplication FLIPPER_PLUGINS[] = {
|
|||||||
.flags = FlipperApplicationFlagDefault},
|
.flags = FlipperApplicationFlagDefault},
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef APP_SNAKE_GAME
|
|
||||||
{.app = snake_game_app,
|
|
||||||
.name = "Snake Game",
|
|
||||||
.stack_size = 1024,
|
|
||||||
.icon = &A_Plugins_14,
|
|
||||||
.flags = FlipperApplicationFlagDefault},
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef APP_WAV_PLAYER
|
#ifdef APP_WAV_PLAYER
|
||||||
{.app = wav_player_app,
|
{.app = wav_player_app,
|
||||||
.name = "WAV Player",
|
.name = "WAV Player",
|
||||||
@ -364,7 +359,30 @@ const FlipperApplication FLIPPER_PLUGINS[] = {
|
|||||||
|
|
||||||
const size_t FLIPPER_PLUGINS_COUNT = COUNT_OF(FLIPPER_PLUGINS);
|
const size_t FLIPPER_PLUGINS_COUNT = COUNT_OF(FLIPPER_PLUGINS);
|
||||||
|
|
||||||
// Plugin menu
|
// Games menu
|
||||||
|
const FlipperApplication FLIPPER_GAMES[] = {
|
||||||
|
|
||||||
|
#ifdef APP_SNAKE_GAME
|
||||||
|
{.app = snake_game_app,
|
||||||
|
.name = "Snake",
|
||||||
|
.stack_size = 1024,
|
||||||
|
.icon = &A_Snake_14,
|
||||||
|
.flags = FlipperApplicationFlagDefault},
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef APP_TETRIS_GAME
|
||||||
|
{.app = tetris_game_app,
|
||||||
|
.name = "Tetris",
|
||||||
|
.stack_size = 1024,
|
||||||
|
.icon = &A_Tetris_14,
|
||||||
|
.flags = FlipperApplicationFlagDefault},
|
||||||
|
#endif
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const size_t FLIPPER_GAMES_COUNT = COUNT_OF(FLIPPER_GAMES);
|
||||||
|
|
||||||
|
// Debug menu
|
||||||
const FlipperApplication FLIPPER_DEBUG_APPS[] = {
|
const FlipperApplication FLIPPER_DEBUG_APPS[] = {
|
||||||
#ifdef APP_BLINK
|
#ifdef APP_BLINK
|
||||||
{.app = blink_test_app,
|
{.app = blink_test_app,
|
||||||
|
@ -42,6 +42,12 @@ extern const size_t FLIPPER_ON_SYSTEM_START_COUNT;
|
|||||||
extern const FlipperApplication FLIPPER_PLUGINS[];
|
extern const FlipperApplication FLIPPER_PLUGINS[];
|
||||||
extern const size_t FLIPPER_PLUGINS_COUNT;
|
extern const size_t FLIPPER_PLUGINS_COUNT;
|
||||||
|
|
||||||
|
/* Games list
|
||||||
|
* Spawned by loader
|
||||||
|
*/
|
||||||
|
extern const FlipperApplication FLIPPER_GAMES[];
|
||||||
|
extern const size_t FLIPPER_GAMES_COUNT;
|
||||||
|
|
||||||
/* Debug menu apps
|
/* Debug menu apps
|
||||||
* Spawned by loader
|
* Spawned by loader
|
||||||
*/
|
*/
|
||||||
|
@ -48,6 +48,7 @@ APP_UPDATER = 1
|
|||||||
APP_MUSIC_PLAYER = 1
|
APP_MUSIC_PLAYER = 1
|
||||||
APP_SNAKE_GAME = 1
|
APP_SNAKE_GAME = 1
|
||||||
APP_WAV_PLAYER = 1
|
APP_WAV_PLAYER = 1
|
||||||
|
APP_TETRIS_GAME = 1
|
||||||
|
|
||||||
# Debug
|
# Debug
|
||||||
APP_ACCESSOR = 1
|
APP_ACCESSOR = 1
|
||||||
@ -247,6 +248,11 @@ CFLAGS += -DAPP_WAV_PLAYER
|
|||||||
SRV_GUI = 1
|
SRV_GUI = 1
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
APP_TETRIS_GAME ?= 0
|
||||||
|
ifeq ($(APP_TETRIS_GAME), 1)
|
||||||
|
CFLAGS += -DAPP_TETRIS_GAME
|
||||||
|
SRV_GUI = 1
|
||||||
|
endif
|
||||||
|
|
||||||
APP_IBUTTON ?= 0
|
APP_IBUTTON ?= 0
|
||||||
ifeq ($(APP_IBUTTON), 1)
|
ifeq ($(APP_IBUTTON), 1)
|
||||||
|
@ -86,6 +86,10 @@ const FlipperApplication* loader_find_application_by_name(const char* name) {
|
|||||||
application =
|
application =
|
||||||
loader_find_application_by_name_in_list(name, FLIPPER_PLUGINS, FLIPPER_PLUGINS_COUNT);
|
loader_find_application_by_name_in_list(name, FLIPPER_PLUGINS, FLIPPER_PLUGINS_COUNT);
|
||||||
}
|
}
|
||||||
|
if(!application) {
|
||||||
|
application =
|
||||||
|
loader_find_application_by_name_in_list(name, FLIPPER_GAMES, FLIPPER_GAMES_COUNT);
|
||||||
|
}
|
||||||
if(!application) {
|
if(!application) {
|
||||||
application = loader_find_application_by_name_in_list(
|
application = loader_find_application_by_name_in_list(
|
||||||
name, FLIPPER_SETTINGS_APPS, FLIPPER_SETTINGS_APPS_COUNT);
|
name, FLIPPER_SETTINGS_APPS, FLIPPER_SETTINGS_APPS_COUNT);
|
||||||
@ -155,6 +159,11 @@ void loader_cli_list(Cli* cli, string_t args, Loader* instance) {
|
|||||||
printf("\t%s\r\n", FLIPPER_PLUGINS[i].name);
|
printf("\t%s\r\n", FLIPPER_PLUGINS[i].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printf("Games:\r\n");
|
||||||
|
for(size_t i = 0; i < FLIPPER_GAMES_COUNT; i++) {
|
||||||
|
printf("\t%s\r\n", FLIPPER_GAMES[i].name);
|
||||||
|
}
|
||||||
|
|
||||||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
|
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
|
||||||
printf("Debug:\r\n");
|
printf("Debug:\r\n");
|
||||||
for(size_t i = 0; i < FLIPPER_DEBUG_APPS_COUNT; i++) {
|
for(size_t i = 0; i < FLIPPER_DEBUG_APPS_COUNT; i++) {
|
||||||
@ -321,6 +330,13 @@ static Loader* loader_alloc() {
|
|||||||
instance->view_dispatcher,
|
instance->view_dispatcher,
|
||||||
LoaderMenuViewPlugins,
|
LoaderMenuViewPlugins,
|
||||||
submenu_get_view(instance->plugins_menu));
|
submenu_get_view(instance->plugins_menu));
|
||||||
|
// Games menu
|
||||||
|
instance->games_menu = submenu_alloc();
|
||||||
|
view_set_context(submenu_get_view(instance->games_menu), instance->games_menu);
|
||||||
|
view_set_previous_callback(
|
||||||
|
submenu_get_view(instance->games_menu), loader_back_to_primary_menu);
|
||||||
|
view_dispatcher_add_view(
|
||||||
|
instance->view_dispatcher, LoaderMenuViewGames, submenu_get_view(instance->games_menu));
|
||||||
// Debug menu
|
// Debug menu
|
||||||
instance->debug_menu = submenu_alloc();
|
instance->debug_menu = submenu_alloc();
|
||||||
view_set_context(submenu_get_view(instance->debug_menu), instance->debug_menu);
|
view_set_context(submenu_get_view(instance->debug_menu), instance->debug_menu);
|
||||||
@ -358,6 +374,8 @@ static void loader_free(Loader* instance) {
|
|||||||
view_dispatcher_remove_view(loader_instance->view_dispatcher, LoaderMenuViewPrimary);
|
view_dispatcher_remove_view(loader_instance->view_dispatcher, LoaderMenuViewPrimary);
|
||||||
submenu_free(loader_instance->plugins_menu);
|
submenu_free(loader_instance->plugins_menu);
|
||||||
view_dispatcher_remove_view(loader_instance->view_dispatcher, LoaderMenuViewPlugins);
|
view_dispatcher_remove_view(loader_instance->view_dispatcher, LoaderMenuViewPlugins);
|
||||||
|
submenu_free(loader_instance->games_menu);
|
||||||
|
view_dispatcher_remove_view(loader_instance->view_dispatcher, LoaderMenuViewGames);
|
||||||
submenu_free(loader_instance->debug_menu);
|
submenu_free(loader_instance->debug_menu);
|
||||||
view_dispatcher_remove_view(loader_instance->view_dispatcher, LoaderMenuViewDebug);
|
view_dispatcher_remove_view(loader_instance->view_dispatcher, LoaderMenuViewDebug);
|
||||||
submenu_free(loader_instance->settings_menu);
|
submenu_free(loader_instance->settings_menu);
|
||||||
@ -391,6 +409,15 @@ static void loader_build_menu() {
|
|||||||
loader_submenu_callback,
|
loader_submenu_callback,
|
||||||
(void*)LoaderMenuViewPlugins);
|
(void*)LoaderMenuViewPlugins);
|
||||||
}
|
}
|
||||||
|
if(FLIPPER_GAMES_COUNT != 0) {
|
||||||
|
menu_add_item(
|
||||||
|
loader_instance->primary_menu,
|
||||||
|
"Games",
|
||||||
|
&A_Games_14,
|
||||||
|
i++,
|
||||||
|
loader_submenu_callback,
|
||||||
|
(void*)LoaderMenuViewGames);
|
||||||
|
}
|
||||||
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
|
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
|
||||||
menu_add_item(
|
menu_add_item(
|
||||||
loader_instance->primary_menu,
|
loader_instance->primary_menu,
|
||||||
@ -421,6 +448,17 @@ static void loader_build_submenu() {
|
|||||||
(void*)&FLIPPER_PLUGINS[i]);
|
(void*)&FLIPPER_PLUGINS[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FURI_LOG_I(TAG, "Building games menu");
|
||||||
|
size_t i;
|
||||||
|
for(i = 0; i < FLIPPER_GAMES_COUNT; i++) {
|
||||||
|
submenu_add_item(
|
||||||
|
loader_instance->games_menu,
|
||||||
|
FLIPPER_GAMES[i].name,
|
||||||
|
i,
|
||||||
|
loader_menu_callback,
|
||||||
|
(void*)&FLIPPER_GAMES[i]);
|
||||||
|
}
|
||||||
|
|
||||||
FURI_LOG_I(TAG, "Building debug menu");
|
FURI_LOG_I(TAG, "Building debug menu");
|
||||||
for(i = 0; i < FLIPPER_DEBUG_APPS_COUNT; i++) {
|
for(i = 0; i < FLIPPER_DEBUG_APPS_COUNT; i++) {
|
||||||
submenu_add_item(
|
submenu_add_item(
|
||||||
|
@ -27,6 +27,7 @@ struct Loader {
|
|||||||
ViewDispatcher* view_dispatcher;
|
ViewDispatcher* view_dispatcher;
|
||||||
Menu* primary_menu;
|
Menu* primary_menu;
|
||||||
Submenu* plugins_menu;
|
Submenu* plugins_menu;
|
||||||
|
Submenu* games_menu;
|
||||||
Submenu* debug_menu;
|
Submenu* debug_menu;
|
||||||
Submenu* settings_menu;
|
Submenu* settings_menu;
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ struct Loader {
|
|||||||
typedef enum {
|
typedef enum {
|
||||||
LoaderMenuViewPrimary,
|
LoaderMenuViewPrimary,
|
||||||
LoaderMenuViewPlugins,
|
LoaderMenuViewPlugins,
|
||||||
|
LoaderMenuViewGames,
|
||||||
LoaderMenuViewDebug,
|
LoaderMenuViewDebug,
|
||||||
LoaderMenuViewSettings,
|
LoaderMenuViewSettings,
|
||||||
} LoaderMenuView;
|
} LoaderMenuView;
|
||||||
|
479
applications/tetris_game/tetris_game.c
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
#include <furi.h>
|
||||||
|
#include <gui/gui.h>
|
||||||
|
#include <input/input.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <furi_hal_resources.h>
|
||||||
|
#include <furi_hal_gpio.h>
|
||||||
|
|
||||||
|
#define BORDER_OFFSET 1
|
||||||
|
#define MARGIN_OFFSET 3
|
||||||
|
#define BLOCK_HEIGHT 6
|
||||||
|
#define BLOCK_WIDTH 6
|
||||||
|
|
||||||
|
#define FIELD_WIDTH 11
|
||||||
|
#define FIELD_HEIGHT 24
|
||||||
|
|
||||||
|
typedef struct Point {
|
||||||
|
// Also used for offset data, which is sometimes negative
|
||||||
|
int8_t x, y;
|
||||||
|
} Point;
|
||||||
|
|
||||||
|
// Rotation logic taken from
|
||||||
|
// https://www.youtube.com/watch?v=yIpk5TJ_uaI
|
||||||
|
typedef enum {
|
||||||
|
OffsetTypeCommon,
|
||||||
|
OffsetTypeI,
|
||||||
|
OffsetTypeO
|
||||||
|
} OffsetType;
|
||||||
|
|
||||||
|
// Since we only support rotating clockwise, these are actual translation values,
|
||||||
|
// not values to be subtracted to get translation values
|
||||||
|
|
||||||
|
static const Point rotOffsetTranslation[3][4][5] = {
|
||||||
|
{
|
||||||
|
{ {0,0}, {-1,0}, {-1,-1}, {0,2}, {-1,2} },
|
||||||
|
{ {0,0}, {1,0}, {1,1}, {0,-2}, {1,-2} },
|
||||||
|
{ {0,0}, {1,0}, {1,-1}, {0,2}, {1,2} },
|
||||||
|
{ {0,0}, {-1,0}, {-1,1}, {0,-2}, {-1,-2} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{ {1,0}, {-1,0}, {2,0}, {-1,1}, {2,-2} },
|
||||||
|
{ {0,1}, {-1,1}, {2,1}, {-1,-1}, {2,2} },
|
||||||
|
{ {-1,0}, {1,0}, {-2,0}, {1,-1}, {-2,2} },
|
||||||
|
{ {0,-1}, {1,-1}, {-2,-1}, {1,1}, {-2,-2} }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{ {0,-1}, {0,0}, {0,0}, {0,0}, {0,0} },
|
||||||
|
{ {1,0}, {0,0}, {0,0}, {0,0}, {0,0} },
|
||||||
|
{ {0,1}, {0,0}, {0,0}, {0,0}, {0,0} },
|
||||||
|
{ {-1,0}, {0,0}, {0,0}, {0,0}, {0,0} }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Point p[4];
|
||||||
|
uint8_t rotIdx;
|
||||||
|
OffsetType offsetType;
|
||||||
|
} Piece;
|
||||||
|
|
||||||
|
// Shapes @ spawn locations, rotation point first
|
||||||
|
static Piece shapes[] = {
|
||||||
|
{ .p = {{5, 1}, {4, 0}, {5, 0}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeCommon }, // Z
|
||||||
|
{ .p = {{5, 1}, {4, 1}, {5, 0}, {6, 0}}, .rotIdx = 0, .offsetType = OffsetTypeCommon }, // S
|
||||||
|
{ .p = {{5, 1}, {4, 1}, {6, 1}, {6, 0}}, .rotIdx = 0, .offsetType = OffsetTypeCommon }, // L
|
||||||
|
{ .p = {{5, 1}, {4, 0}, {4, 1}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeCommon }, // J
|
||||||
|
{ .p = {{5, 1}, {4, 1}, {5, 0}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeCommon }, // T
|
||||||
|
{ .p = {{5, 1}, {4, 1}, {6, 1}, {7, 1}}, .rotIdx = 0, .offsetType = OffsetTypeI }, // I
|
||||||
|
{ .p = {{5, 1}, {5, 0}, {6, 0}, {6, 1}}, .rotIdx = 0, .offsetType = OffsetTypeO } // O
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GameStatePlaying,
|
||||||
|
GameStateGameOver
|
||||||
|
} GameState;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool playField[FIELD_HEIGHT][FIELD_WIDTH];
|
||||||
|
Piece currPiece;
|
||||||
|
uint16_t numLines;
|
||||||
|
uint16_t fallSpeed;
|
||||||
|
GameState gameState;
|
||||||
|
osTimerId_t timer;
|
||||||
|
} TetrisState;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
EventTypeTick,
|
||||||
|
EventTypeKey,
|
||||||
|
} EventType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
EventType type;
|
||||||
|
InputEvent input;
|
||||||
|
} TetrisEvent;
|
||||||
|
|
||||||
|
static void tetris_game_draw_border(Canvas* const canvas) {
|
||||||
|
canvas_draw_line(canvas, 0, 0, 0, 127);
|
||||||
|
canvas_draw_line(canvas, 0, 127, 63, 127);
|
||||||
|
canvas_draw_line(canvas, 63, 127, 63, 0);
|
||||||
|
|
||||||
|
canvas_draw_line(canvas, 2, 0, 2, 125);
|
||||||
|
canvas_draw_line(canvas, 2, 125, 61, 125);
|
||||||
|
canvas_draw_line(canvas, 61, 125, 61, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_draw_playfield(Canvas* const canvas, const TetrisState* tetris_state) {
|
||||||
|
// Playfield: 11 x 24
|
||||||
|
|
||||||
|
for (int y = 0; y < FIELD_HEIGHT; y++) {
|
||||||
|
for (int x = 0; x < FIELD_WIDTH; x++) {
|
||||||
|
if (tetris_state->playField[y][x]) {
|
||||||
|
uint16_t xOffset = x * 5;
|
||||||
|
uint16_t yOffset = y * 5;
|
||||||
|
|
||||||
|
canvas_draw_rframe(
|
||||||
|
canvas,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + xOffset,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + yOffset - 1,
|
||||||
|
BLOCK_WIDTH,
|
||||||
|
BLOCK_HEIGHT,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
canvas_draw_dot(
|
||||||
|
canvas,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + xOffset + 2,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + yOffset + 1
|
||||||
|
);
|
||||||
|
canvas_draw_dot(
|
||||||
|
canvas,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + xOffset + 3,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + yOffset + 1
|
||||||
|
);
|
||||||
|
canvas_draw_dot(
|
||||||
|
canvas,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + xOffset + 2,
|
||||||
|
BORDER_OFFSET + MARGIN_OFFSET + yOffset + 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_render_callback(Canvas* const canvas, void* ctx) {
|
||||||
|
const TetrisState* tetris_state = acquire_mutex((ValueMutex*)ctx, 25);
|
||||||
|
if(tetris_state == NULL) {
|
||||||
|
FURI_LOG_E("TetrisGame", "it null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tetris_game_draw_border(canvas);
|
||||||
|
tetris_game_draw_playfield(canvas, tetris_state);
|
||||||
|
|
||||||
|
if(tetris_state->gameState == GameStateGameOver) {
|
||||||
|
// 128 x 64
|
||||||
|
canvas_set_color(canvas, ColorWhite);
|
||||||
|
canvas_draw_box(canvas, 1, 52, 62, 24);
|
||||||
|
|
||||||
|
canvas_set_color(canvas, ColorBlack);
|
||||||
|
canvas_draw_frame(canvas, 1, 52, 62, 24);
|
||||||
|
|
||||||
|
canvas_set_font(canvas, FontPrimary);
|
||||||
|
canvas_draw_str(canvas, 4, 63, "Game Over");
|
||||||
|
|
||||||
|
char buffer[13];
|
||||||
|
snprintf(buffer, sizeof(buffer), "Lines: %u", tetris_state->numLines);
|
||||||
|
canvas_set_font(canvas, FontSecondary);
|
||||||
|
canvas_draw_str_aligned(canvas, 32, 73, AlignCenter, AlignBottom, buffer);
|
||||||
|
}
|
||||||
|
release_mutex((ValueMutex *)ctx, tetris_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_input_callback(InputEvent* input_event, osMessageQueueId_t event_queue) {
|
||||||
|
furi_assert(event_queue);
|
||||||
|
|
||||||
|
TetrisEvent event = {.type = EventTypeKey, .input = *input_event};
|
||||||
|
osMessageQueuePut(event_queue, &event, 0, osWaitForever);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_init_state(TetrisState* tetris_state) {
|
||||||
|
tetris_state->gameState = GameStatePlaying;
|
||||||
|
tetris_state->numLines = 0;
|
||||||
|
tetris_state->fallSpeed = 500;
|
||||||
|
memset(tetris_state->playField, 0, sizeof(tetris_state->playField));
|
||||||
|
|
||||||
|
memcpy(&tetris_state->currPiece, &shapes[rand() % 7], sizeof(tetris_state->currPiece));
|
||||||
|
|
||||||
|
osTimerStart(tetris_state->timer, tetris_state->fallSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_remove_curr_piece(TetrisState* tetris_state) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
uint8_t x = tetris_state->currPiece.p[i].x;
|
||||||
|
uint8_t y = tetris_state->currPiece.p[i].y;
|
||||||
|
|
||||||
|
tetris_state->playField[y][x] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_render_curr_piece(TetrisState* tetris_state) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
uint8_t x = tetris_state->currPiece.p[i].x;
|
||||||
|
uint8_t y = tetris_state->currPiece.p[i].y;
|
||||||
|
|
||||||
|
tetris_state->playField[y][x] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_rotate_shape(Point currShape[], Point newShape[]) {
|
||||||
|
// Copy shape data
|
||||||
|
for(int i = 0; i < 4; i++) {
|
||||||
|
newShape[i] = currShape[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i < 4; i++) {
|
||||||
|
int8_t relX = currShape[i].x - currShape[0].x;
|
||||||
|
int8_t relY = currShape[i].y - currShape[0].y;
|
||||||
|
|
||||||
|
// Matrix rotation thing
|
||||||
|
int8_t newRelX = (relX * 0) + (relY * -1);
|
||||||
|
int8_t newRelY = (relX * 1) + (relY * 0);
|
||||||
|
|
||||||
|
newShape[i].x = currShape[0].x + newRelX;
|
||||||
|
newShape[i].y = currShape[0].y + newRelY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_apply_kick(Point points[], Point kick) {
|
||||||
|
for(int i = 0; i < 4; i++) {
|
||||||
|
points[i].x += kick.x;
|
||||||
|
points[i].y += kick.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool tetris_game_is_valid_pos(TetrisState* tetris_state, Point* shape) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if(shape[i].x < 0 || shape[i].x > (FIELD_WIDTH - 1) || tetris_state->playField[shape[i].y][shape[i].x] == true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_try_rotation(TetrisState* tetris_state, Piece *newPiece) {
|
||||||
|
uint8_t currRotIdx = tetris_state->currPiece.rotIdx;
|
||||||
|
|
||||||
|
Point *rotatedShape = malloc(sizeof(Point) * 4);
|
||||||
|
Point *kickedShape = malloc(sizeof(Point) * 4);
|
||||||
|
|
||||||
|
memcpy(rotatedShape, &tetris_state->currPiece.p, sizeof(tetris_state->currPiece.p));
|
||||||
|
|
||||||
|
tetris_game_rotate_shape(tetris_state->currPiece.p, rotatedShape);
|
||||||
|
|
||||||
|
for(int i = 0; i < 5; i++) {
|
||||||
|
memcpy(kickedShape, rotatedShape, (sizeof(Point) * 4));
|
||||||
|
tetris_game_apply_kick(kickedShape, rotOffsetTranslation[newPiece->offsetType][currRotIdx][i]);
|
||||||
|
|
||||||
|
if(tetris_game_is_valid_pos(tetris_state, kickedShape)) {
|
||||||
|
memcpy(&newPiece->p, kickedShape, sizeof(newPiece->p));
|
||||||
|
newPiece->rotIdx = (newPiece->rotIdx + 1) % 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(rotatedShape);
|
||||||
|
free(kickedShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool tetris_game_row_is_line(bool row[]) {
|
||||||
|
for(int i = 0; i < FIELD_WIDTH; i++) {
|
||||||
|
if(row[i] == false)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_check_for_lines(TetrisState* tetris_state, uint8_t* lines, uint8_t* numLines) {
|
||||||
|
for(int i = 0; i < FIELD_HEIGHT; i++) {
|
||||||
|
if(tetris_game_row_is_line(tetris_state->playField[i])) {
|
||||||
|
*(lines++) = i;
|
||||||
|
*numLines += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool tetris_game_piece_at_bottom(TetrisState* tetris_state, Piece* newPiece) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
Point *pos = (Point *)&newPiece->p;
|
||||||
|
if (pos[i].y >= FIELD_HEIGHT || tetris_state->playField[pos[i].y][pos[i].x] == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_update_timer_callback(osMessageQueueId_t event_queue) {
|
||||||
|
furi_assert(event_queue);
|
||||||
|
|
||||||
|
TetrisEvent event = {.type = EventTypeTick};
|
||||||
|
osMessageQueuePut(event_queue, &event, 0, osWaitForever);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tetris_game_process_step(TetrisState* tetris_state, Piece* newPiece, bool wasDownMove) {
|
||||||
|
if(tetris_state->gameState == GameStateGameOver)
|
||||||
|
return;
|
||||||
|
|
||||||
|
tetris_game_remove_curr_piece(tetris_state);
|
||||||
|
|
||||||
|
if(wasDownMove) {
|
||||||
|
if(tetris_game_piece_at_bottom(tetris_state, newPiece)) {
|
||||||
|
osTimerStop(tetris_state->timer);
|
||||||
|
|
||||||
|
tetris_game_render_curr_piece(tetris_state);
|
||||||
|
uint8_t numLines = 0;
|
||||||
|
uint8_t lines[] = { 0,0,0,0 };
|
||||||
|
|
||||||
|
tetris_game_check_for_lines(tetris_state, lines, &numLines);
|
||||||
|
if(numLines > 0) {
|
||||||
|
for(int i = 0; i < numLines; i++) {
|
||||||
|
|
||||||
|
// zero out row
|
||||||
|
for(int j = 0; j < FIELD_WIDTH; j++) {
|
||||||
|
tetris_state->playField[lines[i]][j] = false;
|
||||||
|
}
|
||||||
|
// move all above rows down
|
||||||
|
for(int k = lines[i]; k >= 0 ; k--) {
|
||||||
|
for(int m = 0; m < FIELD_WIDTH; m++) {
|
||||||
|
tetris_state->playField[k][m] = (k == 0) ? false : tetris_state->playField[k-1][m];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t oldNumLines = tetris_state->numLines;
|
||||||
|
tetris_state->numLines += numLines;
|
||||||
|
if((oldNumLines / 10) % 10 != (tetris_state->numLines / 10) % 10) {
|
||||||
|
tetris_state->fallSpeed -= 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
Piece* spawnedPiece = &shapes[rand() % 7];
|
||||||
|
if(!tetris_game_is_valid_pos(tetris_state, spawnedPiece->p)) {
|
||||||
|
tetris_state->gameState = GameStateGameOver;
|
||||||
|
} else {
|
||||||
|
memcpy(&tetris_state->currPiece, spawnedPiece, sizeof(tetris_state->currPiece));
|
||||||
|
osTimerStart(tetris_state->timer, tetris_state->fallSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tetris_game_is_valid_pos(tetris_state, newPiece->p)) {
|
||||||
|
memcpy(&tetris_state->currPiece, newPiece, sizeof(tetris_state->currPiece));
|
||||||
|
}
|
||||||
|
|
||||||
|
tetris_game_render_curr_piece(tetris_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int32_t tetris_game_app() {
|
||||||
|
srand(DWT->CYCCNT);
|
||||||
|
|
||||||
|
osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(TetrisEvent), NULL);
|
||||||
|
|
||||||
|
TetrisState* tetris_state = malloc(sizeof(TetrisState));
|
||||||
|
|
||||||
|
ValueMutex state_mutex;
|
||||||
|
if(!init_mutex(&state_mutex, tetris_state, sizeof(TetrisState))) {
|
||||||
|
FURI_LOG_E("TetrisGame", "cannot create mutex\r\n");
|
||||||
|
free(tetris_state);
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not doing this eventually causes issues with TimerSvc due to not sleeping/yielding enough in this task
|
||||||
|
TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME);
|
||||||
|
TaskHandle_t curr_task = xTaskGetHandle("Tetris Game");
|
||||||
|
|
||||||
|
uint32_t origTimerPrio = uxTaskPriorityGet(timer_task);
|
||||||
|
uint32_t myPrio = uxTaskPriorityGet(curr_task);
|
||||||
|
vTaskPrioritySet(timer_task, myPrio + 1);
|
||||||
|
|
||||||
|
ViewPort* view_port = view_port_alloc();
|
||||||
|
view_port_set_orientation(view_port, ViewPortOrientationVertical);
|
||||||
|
view_port_draw_callback_set(view_port, tetris_game_render_callback, &state_mutex);
|
||||||
|
view_port_input_callback_set(view_port, tetris_game_input_callback, event_queue);
|
||||||
|
|
||||||
|
// Open GUI and register view_port
|
||||||
|
Gui* gui = furi_record_open("gui");
|
||||||
|
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||||
|
|
||||||
|
tetris_state->timer = osTimerNew(tetris_game_update_timer_callback, osTimerPeriodic, event_queue, NULL);
|
||||||
|
tetris_game_init_state(tetris_state);
|
||||||
|
|
||||||
|
TetrisEvent event;
|
||||||
|
|
||||||
|
Piece *newPiece = malloc(sizeof(Piece));
|
||||||
|
uint8_t downRepeatCounter = 0;
|
||||||
|
|
||||||
|
for(bool processing = true; processing;) {
|
||||||
|
// This 10U implicitly sets the game loop speed. downRepeatCounter relies on this value
|
||||||
|
osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 10U);
|
||||||
|
|
||||||
|
TetrisState* tetris_state = (TetrisState*)acquire_mutex_block(&state_mutex);
|
||||||
|
|
||||||
|
memcpy(newPiece, &tetris_state->currPiece, sizeof(tetris_state->currPiece));
|
||||||
|
bool wasDownMove = false;
|
||||||
|
|
||||||
|
if(!furi_hal_gpio_read(&gpio_button_right)) {
|
||||||
|
if(downRepeatCounter > 3) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
newPiece->p[i].y += 1;
|
||||||
|
}
|
||||||
|
downRepeatCounter = 0;
|
||||||
|
wasDownMove = true;
|
||||||
|
} else {
|
||||||
|
downRepeatCounter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event_status == osOK) {
|
||||||
|
if(event.type == EventTypeKey) {
|
||||||
|
if(event.input.type == InputTypePress || event.input.type == InputTypeLong || event.input.type == InputTypeRepeat) {
|
||||||
|
switch(event.input.key) {
|
||||||
|
case InputKeyUp:
|
||||||
|
break;
|
||||||
|
case InputKeyDown:
|
||||||
|
break;
|
||||||
|
case InputKeyRight:
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
newPiece->p[i].x += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case InputKeyLeft:
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
newPiece->p[i].x -= 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case InputKeyOk:
|
||||||
|
if(tetris_state->gameState == GameStatePlaying) {
|
||||||
|
tetris_game_remove_curr_piece(tetris_state);
|
||||||
|
tetris_game_try_rotation(tetris_state, newPiece);
|
||||||
|
tetris_game_render_curr_piece(tetris_state);
|
||||||
|
} else {
|
||||||
|
tetris_game_init_state(tetris_state);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case InputKeyBack:
|
||||||
|
processing = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(event.type == EventTypeTick) {
|
||||||
|
// TODO: This is inverted. it returns true when the button is not pressed.
|
||||||
|
// see macro in input.c and do that
|
||||||
|
if(furi_hal_gpio_read(&gpio_button_right)) {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
newPiece->p[i].y += 1;
|
||||||
|
}
|
||||||
|
wasDownMove = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tetris_game_process_step(tetris_state, newPiece, wasDownMove);
|
||||||
|
|
||||||
|
view_port_update(view_port);
|
||||||
|
release_mutex(&state_mutex, tetris_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
osTimerDelete(tetris_state->timer);
|
||||||
|
view_port_enabled_set(view_port, false);
|
||||||
|
gui_remove_view_port(gui, view_port);
|
||||||
|
furi_record_close("gui");
|
||||||
|
view_port_free(view_port);
|
||||||
|
osMessageQueueDelete(event_queue);
|
||||||
|
delete_mutex(&state_mutex);
|
||||||
|
vTaskPrioritySet(timer_task, origTimerPrio);
|
||||||
|
free(newPiece);
|
||||||
|
free(tetris_state);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
BIN
assets/icons/MainMenu/Snake_14/frame_01.png
Normal file
After Width: | Height: | Size: 146 B |
BIN
assets/icons/MainMenu/Snake_14/frame_02.png
Normal file
After Width: | Height: | Size: 148 B |
BIN
assets/icons/MainMenu/Snake_14/frame_03.png
Normal file
After Width: | Height: | Size: 146 B |
BIN
assets/icons/MainMenu/Snake_14/frame_04.png
Normal file
After Width: | Height: | Size: 148 B |
BIN
assets/icons/MainMenu/Snake_14/frame_05.png
Normal file
After Width: | Height: | Size: 160 B |
BIN
assets/icons/MainMenu/Snake_14/frame_06.png
Normal file
After Width: | Height: | Size: 148 B |
BIN
assets/icons/MainMenu/Snake_14/frame_07.png
Normal file
After Width: | Height: | Size: 160 B |
BIN
assets/icons/MainMenu/Snake_14/frame_08.png
Normal file
After Width: | Height: | Size: 96 B |
BIN
assets/icons/MainMenu/Snake_14/frame_09.png
Normal file
After Width: | Height: | Size: 114 B |
BIN
assets/icons/MainMenu/Snake_14/frame_10.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
assets/icons/MainMenu/Snake_14/frame_11.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
assets/icons/MainMenu/Snake_14/frame_12.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
assets/icons/MainMenu/Snake_14/frame_13.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
assets/icons/MainMenu/Snake_14/frame_14.png
Normal file
After Width: | Height: | Size: 140 B |
BIN
assets/icons/MainMenu/Snake_14/frame_15.png
Normal file
After Width: | Height: | Size: 141 B |
1
assets/icons/MainMenu/Snake_14/frame_rate
Normal file
@ -0,0 +1 @@
|
|||||||
|
3
|
BIN
assets/icons/MainMenu/Tetris_14/frame_01.png
Normal file
After Width: | Height: | Size: 161 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_02.png
Normal file
After Width: | Height: | Size: 159 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_03.png
Normal file
After Width: | Height: | Size: 153 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_04.png
Normal file
After Width: | Height: | Size: 141 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_05.png
Normal file
After Width: | Height: | Size: 124 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_06.png
Normal file
After Width: | Height: | Size: 144 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_07.png
Normal file
After Width: | Height: | Size: 124 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_08.png
Normal file
After Width: | Height: | Size: 144 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_09.png
Normal file
After Width: | Height: | Size: 129 B |
BIN
assets/icons/MainMenu/Tetris_14/frame_10.png
Normal file
After Width: | Height: | Size: 154 B |
1
assets/icons/MainMenu/Tetris_14/frame_rate
Normal file
@ -0,0 +1 @@
|
|||||||
|
3
|