-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Changes from 17 commits
7ffa5fd
1a34dac
47cbdac
87a5651
65fdda1
8304605
b97affa
b5899e8
b807a08
7776c36
831518d
3ed547f
86c8108
4617054
0b3551a
55085f2
77535aa
093c46a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going to leak file if something will go wrong here. |
||
} | ||
} | ||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
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); |
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; |
There was a problem hiding this comment.
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.