Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snake Plugin: Store game state on close and restore it on restart, show highscore #1922

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions applications/plugins/snake_game/application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ App(
apptype=FlipperAppType.PLUGIN,
entry_point="snake_game_app",
cdefines=["APP_SNAKE_GAME"],
requires=["gui"],
stack_size=1 * 1024,
requires=[
"gui",
"notification",
"storage",
],
stack_size=2 * 1024,
order=30,
fap_icon="snake_10px.png",
fap_category="Games",
Expand Down
181 changes: 181 additions & 0 deletions applications/plugins/snake_game/helpers/snake_file_handler.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#include "snake_file_handler.h"

#include <furi.h>
#include <flipper_format/flipper_format.h>

static void snake_game_close_file(FlipperFormat* file) {
if(file == NULL) {
furi_record_close(RECORD_STORAGE);
return;
}
flipper_format_file_close(file);
flipper_format_free(file);
furi_record_close(RECORD_STORAGE);
}

static FlipperFormat* snake_game_open_file() {
Storage* storage = furi_record_open(RECORD_STORAGE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening and closing storage record doesn't looks good here. The best option is to do it in allocator/deallocator of snake app itself and then pass it here.

FlipperFormat* file = flipper_format_file_alloc(storage);

if(storage_common_stat(storage, SNAKE_GAME_FILE_PATH, NULL) == FSE_OK) {
if(!flipper_format_file_open_existing(file, SNAKE_GAME_FILE_PATH)) {
snake_game_close_file(file);
return NULL;
}
} else {
if(storage_common_stat(storage, APPS_DATA, NULL) == FSE_NOT_EXIST) {
if(!storage_simply_mkdir(storage, APPS_DATA)) {
return NULL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to leak file if something will go wrong here.
Whole way of working with file looks quite dangerous, please check how it's done in other apps.

}
}
if(storage_common_stat(storage, SNAKE_GAME_FILE_DIR_PATH, NULL) == FSE_NOT_EXIST) {
if(!storage_simply_mkdir(storage, SNAKE_GAME_FILE_DIR_PATH)) {
return NULL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leak here too

}
}

if(!flipper_format_file_open_new(file, SNAKE_GAME_FILE_PATH)) {
snake_game_close_file(file);
return NULL;
}

flipper_format_write_header_cstr(
file, SNAKE_GAME_FILE_HEADER, SNAKE_GAME_FILE_ACTUAL_VERSION);
flipper_format_rewind(file);
}
return file;
}

void snake_game_save_score_to_file(int16_t highscore) {
FlipperFormat* file = snake_game_open_file();
if(file != NULL) {
uint32_t temp = highscore;
if(!flipper_format_insert_or_update_uint32(file, SNAKE_GAME_CONFIG_HIGHSCORE, &temp, 1)) {
snake_game_close_file(file);
return;
}
snake_game_close_file(file);
}
}

void snake_game_save_game_to_file(SnakeState* const snake_state) {
FlipperFormat* file = snake_game_open_file();

if(file != NULL) {
uint32_t temp = snake_state->len;
if(!flipper_format_insert_or_update_uint32(file, SNAKE_GAME_CONFIG_KEY_LEN, &temp, 1)) {
snake_game_close_file(file);
return;
}

uint16_t array_size = snake_state->len * 2;
uint32_t temp_array[array_size];
for(int16_t i = 0, a = 0; a < array_size && i < snake_state->len; i++) {
temp_array[a++] = snake_state->points[i].x;
temp_array[a++] = snake_state->points[i].y;
}
if(!flipper_format_insert_or_update_uint32(
file, SNAKE_GAME_CONFIG_KEY_POINTS, temp_array, array_size)) {
snake_game_close_file(file);
return;
}

temp = snake_state->currentMovement;
if(!flipper_format_insert_or_update_uint32(
file, SNAKE_GAME_CONFIG_KEY_CURRENT_MOVEMENT, &temp, 1)) {
snake_game_close_file(file);
return;
}

temp = snake_state->nextMovement;
if(!flipper_format_insert_or_update_uint32(
file, SNAKE_GAME_CONFIG_KEY_NEXT_MOVEMENT, &temp, 1)) {
snake_game_close_file(file);
return;
}

array_size = 2;
uint32_t temp_point_array[array_size];
temp_point_array[0] = snake_state->fruit.x;
temp_point_array[1] = snake_state->fruit.y;
if(!flipper_format_insert_or_update_uint32(
file, SNAKE_GAME_CONFIG_KEY_FRUIT_POINTS, temp_point_array, array_size)) {
snake_game_close_file(file);
return;
}

snake_game_close_file(file);
}
}

bool snake_game_init_game_from_file(SnakeState* const snake_state) {
FlipperFormat* file = snake_game_open_file();

if(file != NULL) {
FuriString* file_type = furi_string_alloc();
uint32_t version = 1;
if(!flipper_format_read_header(file, file_type, &version)) {
furi_string_free(file_type);
snake_game_close_file(file);
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually do {} while(0) and break. Please check how other apps do that

}
furi_string_free(file_type);

uint32_t temp;
snake_state->highscore =
(flipper_format_read_uint32(file, SNAKE_GAME_CONFIG_HIGHSCORE, &temp, 1)) ? temp : 0;
flipper_format_rewind(file);

if(!flipper_format_read_uint32(file, SNAKE_GAME_CONFIG_KEY_LEN, &temp, 1)) {
snake_game_close_file(file);
return false;
}
snake_state->len = temp;
flipper_format_delete_key(file, SNAKE_GAME_CONFIG_KEY_LEN);

uint16_t array_size = snake_state->len * 2;
uint32_t temp_array[array_size];
if(!flipper_format_read_uint32(
file, SNAKE_GAME_CONFIG_KEY_POINTS, temp_array, array_size)) {
snake_game_close_file(file);
return false;
}

for(int16_t i = 0, a = 0; a < array_size && i < snake_state->len; i++) {
snake_state->points[i].x = temp_array[a++];
snake_state->points[i].y = temp_array[a++];
}
flipper_format_delete_key(file, SNAKE_GAME_CONFIG_KEY_POINTS);

if(!flipper_format_read_uint32(file, SNAKE_GAME_CONFIG_KEY_CURRENT_MOVEMENT, &temp, 1)) {
snake_game_close_file(file);
return false;
}
snake_state->currentMovement = temp;
flipper_format_delete_key(file, SNAKE_GAME_CONFIG_KEY_CURRENT_MOVEMENT);

if(!flipper_format_read_uint32(file, SNAKE_GAME_CONFIG_KEY_NEXT_MOVEMENT, &temp, 1)) {
snake_game_close_file(file);
return false;
}
snake_state->nextMovement = temp;
flipper_format_delete_key(file, SNAKE_GAME_CONFIG_KEY_NEXT_MOVEMENT);

array_size = 2;
uint32_t temp_point_array[array_size];
if(!flipper_format_read_uint32(
file, SNAKE_GAME_CONFIG_KEY_FRUIT_POINTS, temp_point_array, array_size)) {
snake_game_close_file(file);
return false;
}
snake_state->fruit.x = temp_point_array[0];
snake_state->fruit.y = temp_point_array[1];
flipper_format_delete_key(file, SNAKE_GAME_CONFIG_KEY_FRUIT_POINTS);

snake_game_close_file(file);

return true;
}

return false;
}
25 changes: 25 additions & 0 deletions applications/plugins/snake_game/helpers/snake_file_handler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include "snake_types.h"
#include <furi.h>
#include <flipper_format/flipper_format.h>

#define APPS_DATA EXT_PATH("apps_data")
#define SNAKE_GAME_FILE_DIR_PATH APPS_DATA "/snake_game"
#define SNAKE_GAME_FILE_PATH SNAKE_GAME_FILE_DIR_PATH "/.snake"

#define SNAKE_GAME_FILE_HEADER "Flipper Snake plugin run file"
#define SNAKE_GAME_FILE_ACTUAL_VERSION 1

#define SNAKE_GAME_CONFIG_KEY_POINTS "SnakePoints"
#define SNAKE_GAME_CONFIG_KEY_LEN "SnakeLen"
#define SNAKE_GAME_CONFIG_KEY_CURRENT_MOVEMENT "CurrentMovement"
#define SNAKE_GAME_CONFIG_KEY_NEXT_MOVEMENT "NextMovement"
#define SNAKE_GAME_CONFIG_KEY_FRUIT_POINTS "FruitPoints"
#define SNAKE_GAME_CONFIG_HIGHSCORE "Highscore"

void snake_game_save_score_to_file(int16_t highscore);

void snake_game_save_game_to_file(SnakeState* const snake_state);

bool snake_game_init_game_from_file(SnakeState* const snake_state);
51 changes: 51 additions & 0 deletions applications/plugins/snake_game/helpers/snake_types.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#pragma once

#include <furi.h>

typedef struct {
// +-----x
// |
// |
// y
uint8_t x;
uint8_t y;
} Point;

typedef enum {
GameStateLife,

// https://melmagazine.com/en-us/story/snake-nokia-6110-oral-history-taneli-armanto
// Armanto: While testing the early versions of the game, I noticed it was hard
// to control the snake upon getting close to and edge but not crashing — especially
// in the highest speed levels. I wanted the highest level to be as fast as I could
// possibly make the device "run," but on the other hand, I wanted to be friendly
// and help the player manage that level. Otherwise it might not be fun to play. So
// I implemented a little delay. A few milliseconds of extra time right before
// the player crashes, during which she can still change the directions. And if
// she does, the game continues.
GameStateLastChance,

GameStateGameOver,
} GameState;

// Note: do not change without purpose. Current values are used in smart
// orthogonality calculation in `snake_game_get_turn_snake`.
typedef enum {
DirectionUp,
DirectionRight,
DirectionDown,
DirectionLeft,
} Direction;

#define MAX_SNAKE_LEN 253

typedef struct {
Point points[MAX_SNAKE_LEN];
uint16_t len;
bool isNewHighscore;
int16_t highscore;
Direction currentMovement;
Direction nextMovement; // if backward of currentMovement, ignore
Point fruit;
GameState state;
} SnakeState;
Loading