Skip to content

Commit

Permalink
Flipper Zero GUI
Browse files Browse the repository at this point in the history
  • Loading branch information
dimat committed Feb 5, 2023
1 parent 7a82900 commit 88ecbdd
Show file tree
Hide file tree
Showing 10 changed files with 578 additions and 214 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
reversi
compile_flags.txt
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
# Reversi game for Flipper Zero
## Prerequisites
TBD

![Game screen](images/game.png)
![Menu screen](images/menu.png)

## Compile
TBD
1. Clone [firmware](https://github.com/flipperdevices/flipperzero-firmware)
2. Go to the `applications_user` directory
3. Create a symlink to this repo assuming that these two repos are on the same level:
```sh
ln -s ../../flipperzero-reversi flipperzero-reversi
```
4. From the main directory of the firmware run:
```sh
./fbt faps
```

## Install
TBD
1. Open folder `./build/f7-firmware-D/.extapps/` from the firmware directory
2. Open qFlipper
3. Open File Manager, SD Card/apps/games
4. Drag `game_reversi.fapp` to qFlipper

## Thanks to:
- [2048 game](https://github.com/eugene-kirzhanov/flipper-zero-2048-game)
Expand Down
14 changes: 14 additions & 0 deletions application.fam
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
App(
appid="game_reversi",
name="Reversi",
apptype=FlipperAppType.EXTERNAL,
entry_point="game_reversi_app",
cdefines=["APP_GAME_REVERSI"],
requires=[
"gui",
],
stack_size=1 * 1024,
order=90,
fap_icon="game_reversi.png",
fap_category="Games"
)
347 changes: 347 additions & 0 deletions game_reversi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
// Game "Reversi" for Flipper Zero
// Copyright 2023 Dmitry Matyukhin

#include <stdio.h>
#include <furi.h>
#include <gui/gui.h>
#include <input/input.h>
#include <storage/storage.h>
#include "reversi.h"

#define FRAME_LEFT 3
#define FRAME_TOP 3
#define FRAME_CELL_SIZE 7

#define SAVING_DIRECTORY "/ext/apps/Games"
#define SAVING_FILENAME SAVING_DIRECTORY "/game_reversi.save"

typedef enum {
AppScreenGame,
AppScreenMenu
} AppScreen;

typedef struct {
GameState game;
AppScreen screen;
uint8_t selected_menu_item;
} AppState;

#define MENU_ITEMS_COUNT 2
static const char* popup_menu_strings[] = {"Resume", "New Game"};

static void draw_menu(Canvas* const canvas, const AppState* app_state);
static void gray_canvas(Canvas* const canvas);

static void input_callback(InputEvent* input_event, void* ctx) {
furi_assert(ctx);
FuriMessageQueue* event_queue = ctx;
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
}

static void draw_callback(Canvas *const canvas, void *ctx) {
furi_assert(ctx);

const AppState *app_state = acquire_mutex((ValueMutex *)ctx, 25);
if (app_state == NULL)
return;
const GameState *game_state = &app_state->game;

canvas_clear(canvas);
canvas_set_color(canvas, ColorBlack);

for (uint8_t i = 0; i <= BOARD_SIZE; i++) {
canvas_draw_line(canvas, FRAME_LEFT + FRAME_CELL_SIZE * i, FRAME_TOP,
FRAME_LEFT + FRAME_CELL_SIZE * i,
FRAME_TOP + FRAME_CELL_SIZE * BOARD_SIZE);
canvas_draw_line(canvas, FRAME_LEFT, FRAME_TOP + FRAME_CELL_SIZE * i,
FRAME_LEFT + FRAME_CELL_SIZE * BOARD_SIZE,
FRAME_TOP + FRAME_CELL_SIZE * i);
}
//
// draw cursor
canvas_set_color(canvas, ColorWhite);
canvas_draw_frame(canvas, FRAME_LEFT + FRAME_CELL_SIZE * game_state->cursor_x,
FRAME_TOP + FRAME_CELL_SIZE * game_state->cursor_y,
FRAME_CELL_SIZE + 1, FRAME_CELL_SIZE + 1);

canvas_set_color(canvas, ColorBlack);
// draw pieces
int blacks = 0, whites = 0;
const int radius = FRAME_CELL_SIZE >> 1;
for (uint8_t i = 0; i < BOARD_SIZE; i++) {
for (uint8_t j = 0; j < BOARD_SIZE; j++) {
if (!game_state->board[i][j]) {
continue;
}
if (game_state->board[i][j] == BLACK) {
canvas_draw_disc(
canvas,
FRAME_LEFT + FRAME_CELL_SIZE * i + radius + 1,
FRAME_TOP + FRAME_CELL_SIZE * j + radius + 1,
radius);
blacks++;
} else {
canvas_draw_circle(
canvas,
FRAME_LEFT + FRAME_CELL_SIZE * i + radius + 1,
FRAME_TOP + FRAME_CELL_SIZE * j + radius + 1,
radius);
whites++;
}
}
}

canvas_set_font(canvas, FontPrimary);
// draw score
char score_str[10];
memset(score_str, 0, sizeof(score_str));
snprintf(score_str, sizeof(score_str), "%d - %d", whites, blacks);

canvas_draw_str_aligned(canvas, 70, 3, AlignLeft, AlignTop, score_str);

canvas_set_font(canvas, FontSecondary);
if (game_state->is_game_over) {
canvas_draw_str_aligned(canvas, 70, 20, AlignLeft, AlignTop, "Game over");

canvas_draw_str_aligned(canvas,
70,
FRAME_TOP + FRAME_CELL_SIZE * BOARD_SIZE,
AlignLeft, AlignBottom,
"Press OK");

canvas_set_font(canvas, FontPrimary);

if (whites == blacks) {
canvas_draw_str_aligned(canvas, 70, 30, AlignLeft, AlignTop, "DRAW");
} else if (((game_state->human_color == WHITE) && whites > blacks) ||
((game_state->human_color == BLACK) && blacks > whites)) {
canvas_draw_str_aligned(canvas, 70, 30, AlignLeft, AlignTop, "YOU WIN");
} else {
canvas_draw_str_aligned(canvas, 70, 30, AlignLeft, AlignTop, "YOU LOSE");
}
} else if (game_state->current_player == game_state->human_color) {
canvas_draw_str_aligned(canvas, 70, 12, AlignLeft, AlignTop, "Your turn");
} else {
canvas_draw_str_aligned(canvas, 70, 12, AlignLeft, AlignTop,
"Computer turn");
}

if (app_state->screen == AppScreenMenu) {
draw_menu(canvas, app_state);
}

release_mutex((ValueMutex *)ctx, app_state);
}

static void draw_menu(Canvas* const canvas, const AppState* app_state) {
gray_canvas(canvas);
canvas_set_color(canvas, ColorWhite);
canvas_draw_rbox(canvas, 28, 16, 72, 32, 4);
canvas_set_color(canvas, ColorBlack);
canvas_draw_rframe(canvas, 28, 16, 72, 32, 4);

for (int i = 0; i < MENU_ITEMS_COUNT; i++) {
if (i == app_state->selected_menu_item) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(canvas, 34, 20 + 12 * i, 60, 12);
}

canvas_set_color(canvas, i == app_state->selected_menu_item ? ColorWhite
: ColorBlack);
canvas_draw_str_aligned(canvas, 64, 26 + 12 * i, AlignCenter, AlignCenter,
popup_menu_strings[i]);
}
}

static void gray_canvas(Canvas* const canvas) {
canvas_set_color(canvas, ColorWhite);
for(int x = 0; x < 128; x += 2) {
for(int y = 0; y < 64; y++) {
canvas_draw_dot(canvas, x + (y % 2 == 1 ? 0 : 1), y);
}
}
}

bool load_game(GameState* game_state) {
Storage* storage = furi_record_open(RECORD_STORAGE);

File* file = storage_file_alloc(storage);
uint16_t bytes_readed = 0;
if(storage_file_open(file, SAVING_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
bytes_readed = storage_file_read(file, game_state, sizeof(GameState));
}
storage_file_close(file);
storage_file_free(file);

furi_record_close(RECORD_STORAGE);

return bytes_readed == sizeof(GameState);
}

void save_game(const GameState* game_state) {
Storage* storage = furi_record_open(RECORD_STORAGE);

if(storage_common_stat(storage, SAVING_DIRECTORY, NULL) == FSE_NOT_EXIST) {
if(!storage_simply_mkdir(storage, SAVING_DIRECTORY)) {
return;
}
}

File* file = storage_file_alloc(storage);
if(storage_file_open(file, SAVING_FILENAME, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
storage_file_write(file, game_state, sizeof(GameState));
}
storage_file_close(file);
storage_file_free(file);

furi_record_close(RECORD_STORAGE);
}

bool handle_key_game(GameState *game_state, InputKey key) {
switch (key) {
case InputKeyBack:
save_game(game_state);
return false;
break;
case InputKeyOk:
if (game_state->is_game_over) {
init_game(game_state);
save_game(game_state);
} else {
human_move(game_state);
}
break;
case InputKeyUp:
if (game_state->cursor_y > 0) {
game_state->cursor_y--;
} else {
game_state->cursor_y = BOARD_SIZE - 1;
}
break;
case InputKeyDown:
if (game_state->cursor_y < BOARD_SIZE - 1) {
game_state->cursor_y++;
} else {
game_state->cursor_y = 0;
}
break;
case InputKeyLeft:
if (game_state->cursor_x > 0) {
game_state->cursor_x--;
} else {
game_state->cursor_x = BOARD_SIZE - 1;
}
break;
case InputKeyRight:
if (game_state->cursor_x < BOARD_SIZE - 1) {
game_state->cursor_x++;
} else {
game_state->cursor_x = 0;
}
break;
default:
break;
}
return true;
}

bool handle_key_menu(AppState *app_state, InputKey key) {
switch (key) {
case InputKeyUp:
if (app_state->selected_menu_item > 0) {
app_state->selected_menu_item--;
}
break;
case InputKeyDown:
if (app_state->selected_menu_item <= MENU_ITEMS_COUNT) {
app_state->selected_menu_item++;
}
break;
case InputKeyOk:
if (app_state->selected_menu_item == 1) {
// new game
init_game(&app_state->game);
save_game(&app_state->game);
}
app_state->screen = AppScreenGame;
break;
default:
break;
}
return true;
}

// returns `true` if the event loop should keep going
bool handle_key(AppState* app_state, InputKey key) {
GameState* game_state = &app_state->game;

switch (app_state->screen) {
case AppScreenGame:
return handle_key_game(game_state, key);
break;
case AppScreenMenu:
return handle_key_menu(app_state, key);
break;
}
return true;
}

int32_t game_reversi_app() {
AppState app_state;
app_state.screen = AppScreenGame;
if (!load_game(&app_state.game)) {
init_game(&app_state.game);
}

ValueMutex state_mutex;
if (!init_mutex(&state_mutex, &app_state, sizeof(AppState))) {
return 255;
}
InputEvent input;
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));

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);
bool is_finished = false;

while(!is_finished) {
// check if it's computer's turn
if (!app_state.game.is_game_over && (app_state.game.current_player != app_state.game.human_color)) {
computer_move(&app_state.game);
}
FuriStatus event_status = furi_message_queue_get(event_queue, &input, FuriWaitForever);
if(event_status == FuriStatusOk) {
// handle only press event, ignore repeat/release events

if (input.type == InputTypeLong && input.key == InputKeyOk && app_state.screen == AppScreenGame) {
AppState *app_state = (AppState *)acquire_mutex_block(&state_mutex);
app_state->selected_menu_item = 0;
app_state->screen = AppScreenMenu;
view_port_update(view_port);
release_mutex(&state_mutex, app_state);
continue;
}
if (input.type != InputTypePress) continue;

AppState* app_state = (AppState*)acquire_mutex_block(&state_mutex);
is_finished = !handle_key(app_state, input.key);
view_port_update(view_port);
release_mutex(&state_mutex, app_state);
}
}

gui_remove_view_port(gui, view_port);
furi_record_close(RECORD_GUI);

view_port_free(view_port);

furi_message_queue_free(event_queue);

delete_mutex(&state_mutex);

return 0;
}
Binary file added game_reversi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/game.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 88ecbdd

Please sign in to comment.