Show past games.

This commit is contained in:
Derek Jamison 2023-03-06 20:51:41 -05:00
parent 615e307bf5
commit fda2fbcdbb
3 changed files with 307 additions and 5 deletions

View File

@ -19,12 +19,12 @@ Completed work:
- Config - Allow changing frequency. - Config - Allow changing frequency.
- Receiving a Join game does an ACK (to cause game on joiner to start). - Receiving a Join game does an ACK (to cause game on joiner to start).
- Log game results & contact info onto SD card. - Log game results & contact info onto SD card.
- Allow viewing past games/scores.
Remaining work (for subghz version): Remaining work (for subghz version):
- Config - Allow changing hard-coded CONTACT_INFO message.
- Allow viewing past games/scores.
- A join ACK removes it from the list of available games. - A join ACK removes it from the list of available games.
- Config - Allow changing hard-coded CONTACT_INFO message.
- Refactor the code, so it has less duplication. - Refactor the code, so it has less duplication.
- Write tutorial. - Write tutorial.
- Add game ending animations. - Add game ending animations.

View File

@ -645,6 +645,52 @@ static void rps_render_main_menu(Canvas* canvas, void* ctx) {
} }
} }
// Render UI when we are showing previous games.
// @param canvas rendering surface of the Flipper Zero.
// @param ctx pointer to a GameContext.
static void rps_render_past_games(Canvas* canvas, void* ctx) {
GameContext* game_context = ctx;
canvas_set_font(canvas, FontPrimary);
PlayerStats* stats = game_context->data->viewing_player_stats;
if(!stats) {
canvas_draw_str_aligned(canvas, 10, 30, AlignLeft, AlignTop, "NO GAMES PLAYED.");
} else {
canvas_draw_str_aligned(
canvas, 0, 0, AlignLeft, AlignTop, furi_string_get_cstr(stats->last_played));
furi_string_printf(
game_context->data->buffer,
"Win:%d Lost:%d Tied:%d",
stats->win_count,
stats->loss_count,
stats->tie_count);
canvas_draw_str_aligned(
canvas, 0, 12, AlignLeft, AlignTop, furi_string_get_cstr(game_context->data->buffer));
canvas_draw_str_aligned(
canvas, 0, 24, AlignLeft, AlignTop, furi_string_get_cstr(stats->flipper_name));
canvas_set_font(canvas, FontSecondary);
char ch = furi_string_get_char(stats->contact, 0);
for(unsigned int i = 0; i < sizeof(contact_list) / sizeof(contact_list[0]); i++) {
if(contact_list[i][0] == ch) {
canvas_draw_str_aligned(canvas, 64, 24, AlignLeft, AlignTop, contact_list[i] + 1);
ch = 0;
break;
}
}
if(ch) {
char id[2] = {ch, 0};
canvas_draw_str_aligned(canvas, 64, 24, AlignLeft, AlignTop, id);
}
canvas_draw_str_aligned(
canvas, 0, 36, AlignLeft, AlignTop, furi_string_get_cstr(stats->contact) + 1);
}
}
// We register this callback to get invoked whenever we need to render the screen. // We register this callback to get invoked whenever we need to render the screen.
// We render the UI on this callback thread. // We render the UI on this callback thread.
// @param canvas rendering surface of the Flipper Zero. // @param canvas rendering surface of the Flipper Zero.
@ -663,6 +709,8 @@ static void rps_render_callback(Canvas* canvas, void* ctx) {
rps_render_join_game(canvas, game_context); rps_render_join_game(canvas, game_context);
} else if(game_context->data->screen_state == ScreenMainMenu) { } else if(game_context->data->screen_state == ScreenMainMenu) {
rps_render_main_menu(canvas, game_context); rps_render_main_menu(canvas, game_context);
} else if(game_context->data->screen_state == ScreenPastGames) {
rps_render_past_games(canvas, game_context);
} }
} }
@ -750,7 +798,7 @@ static void rps_broadcast_join(GameContext* game_context) {
GameRfPurposeJoin, GameRfPurposeJoin,
MAJOR_VERSION, MAJOR_VERSION,
data->game_number, data->game_number,
CONTACT_INFO, furi_string_get_cstr(data->local_contact),
furi_hal_version_get_name_ptr()); furi_hal_version_get_name_ptr());
rps_broadcast(game_context, data->buffer); rps_broadcast(game_context, data->buffer);
} }
@ -771,7 +819,7 @@ static void rps_broadcast_join_acknowledge(GameContext* game_context) {
GameRfPurposeJoinAcknowledge, GameRfPurposeJoinAcknowledge,
MAJOR_VERSION, MAJOR_VERSION,
data->game_number, data->game_number,
CONTACT_INFO, furi_string_get_cstr(data->local_contact),
furi_hal_version_get_name_ptr()); furi_hal_version_get_name_ptr());
rps_broadcast(game_context, data->buffer); rps_broadcast(game_context, data->buffer);
} }
@ -1261,6 +1309,23 @@ static void save_result(GameContext* game_context) {
FURI_LOG_E(TAG, "Failed to open file: %s", RPS_GAME_PATH); FURI_LOG_E(TAG, "Failed to open file: %s", RPS_GAME_PATH);
} }
furi_string_printf(
game_context->data->buffer,
"%04d-%02d-%02dT%02d:%02d:%02d",
datetime.year,
datetime.month,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second);
update_player_stats(
game_context,
game_context->data->remote_player,
furi_string_get_cstr(game_context->data->remote_name),
furi_string_get_cstr(game_context->data->remote_contact),
furi_string_get_cstr(game_context->data->buffer));
storage_file_close(games_file); storage_file_close(games_file);
storage_file_free(games_file); storage_file_free(games_file);
furi_record_close(RECORD_STORAGE); furi_record_close(RECORD_STORAGE);
@ -1268,10 +1333,149 @@ static void save_result(GameContext* game_context) {
furi_mutex_release(game_context->mutex); furi_mutex_release(game_context->mutex);
} }
static void update_player_stats(
GameContext* game_context,
GameState remote_player,
const char* remote_name,
const char* remote_contact,
const char* datetime) {
PlayerStats* stat = game_context->data->player_stats;
FURI_LOG_I(TAG, "Searching for player: %s", remote_name);
while(stat) {
if(furi_string_cmp_str(stat->flipper_name, remote_name) == 0) {
break;
}
stat = stat->next;
}
if(!stat) {
FURI_LOG_I(TAG, "Not found player: %s", remote_name);
stat = malloc(sizeof(PlayerStats));
stat->loss_count += isLoss((GameState)remote_player) ? 1 : 0;
stat->win_count += isWin((GameState)remote_player) ? 1 : 0;
stat->tie_count += isTie((GameState)remote_player) ? 1 : 0;
stat->next = NULL;
stat->prev = NULL;
stat->flipper_name = furi_string_alloc();
furi_string_set_str(stat->flipper_name, remote_name);
stat->contact = furi_string_alloc();
furi_string_set_str(stat->contact, remote_contact);
stat->last_played = furi_string_alloc();
furi_string_set_str(stat->last_played, datetime);
furi_string_set_char(stat->last_played, 10, ' ');
} else {
FURI_LOG_I(TAG, "Found player: %s", remote_name);
stat->loss_count += isLoss((GameState)remote_player) ? 1 : 0;
stat->win_count += isWin((GameState)remote_player) ? 1 : 0;
stat->tie_count += isTie((GameState)remote_player) ? 1 : 0;
furi_string_set_str(stat->last_played, datetime);
furi_string_set_char(stat->last_played, 10, ' ');
}
// Remove the stat from the list, if it is connected.
if(game_context->data->player_stats && game_context->data->player_stats != stat) {
if(stat->prev) {
FURI_LOG_I(TAG, "Setting stat->prev->next.");
stat->prev->next = stat->next;
}
if(stat->next) {
FURI_LOG_I(TAG, "Setting stat->next->next.");
stat->next->prev = stat->prev;
}
FURI_LOG_I(TAG, "Setting player_stats->prev.");
stat->prev = NULL;
game_context->data->player_stats->prev = stat;
stat->next = game_context->data->player_stats;
game_context->data->player_stats = stat;
} else if(game_context->data->player_stats && game_context->data->player_stats == stat) {
// We are already at the start of the list.
} else {
// This is the first stat.
game_context->data->player_stats = stat;
}
FURI_LOG_I(
TAG,
"Added %s w:%d l:%d t:%d",
furi_string_get_cstr(stat->flipper_name),
stat->win_count,
stat->loss_count,
stat->tie_count);
}
static void load_player_stats(GameContext* game_context) {
game_context->data->player_stats = NULL;
Storage* storage = furi_record_open(RECORD_STORAGE);
File* games_file = storage_file_alloc(storage);
if(storage_file_open(games_file, RPS_GAME_PATH, FSAM_READ, FSOM_OPEN_EXISTING)) {
FURI_LOG_E(TAG, "Opened file: %s", RPS_GAME_PATH);
while(!storage_file_eof(games_file)) {
char ch;
furi_string_reset(game_context->data->buffer);
while(storage_file_read(games_file, &ch, 1) && !storage_file_eof(games_file)) {
furi_string_push_back(game_context->data->buffer, ch);
if(ch == '\n') {
break;
}
}
char local_player;
char remote_player;
char datetime[20];
char remote_name[32];
char remote_contact[64];
int parsed = sscanf(
furi_string_get_cstr(game_context->data->buffer),
"%c%c\t%s\t%s\t%s",
&local_player,
&remote_player,
datetime,
remote_name,
remote_contact);
if(parsed != 5) {
FURI_LOG_I(
TAG,
"Failed to parse entry: %s count was %d",
furi_string_get_cstr(game_context->data->buffer),
parsed);
} else {
FURI_LOG_I(
TAG,
"Parsed entry: %c %c\t%s\t%s\t%s",
local_player,
remote_player,
datetime,
remote_name,
remote_contact);
update_player_stats(
game_context, remote_player, remote_name, remote_contact, datetime);
}
}
FURI_LOG_I(TAG, "Finished parsing file.");
} else {
FURI_LOG_E(TAG, "Failed to open file: %s", RPS_GAME_PATH);
}
storage_file_close(games_file);
storage_file_free(games_file);
furi_record_close(RECORD_STORAGE);
}
// This is the entry point for our application, which should match the application.fam file. // This is the entry point for our application, which should match the application.fam file.
int32_t rock_paper_scissors_app(void* p) { int32_t rock_paper_scissors_app(void* p) {
UNUSED(p); UNUSED(p);
UNUSED(contact_list);
// Configure our initial data. // Configure our initial data.
GameContext* game_context = malloc(sizeof(GameContext)); GameContext* game_context = malloc(sizeof(GameContext));
game_context->mutex = furi_mutex_alloc(FuriMutexTypeNormal); game_context->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
@ -1285,6 +1489,10 @@ int32_t rock_paper_scissors_app(void* p) {
game_context->data->remote_player = StateUnknown; game_context->data->remote_player = StateUnknown;
game_context->data->screen_state = ScreenMainMenu; game_context->data->screen_state = ScreenMainMenu;
game_context->data->remote_games = NULL; game_context->data->remote_games = NULL;
game_context->data->local_contact = furi_string_alloc();
furi_string_set(game_context->data->local_contact, CONTACT_INFO);
load_player_stats(game_context);
// Queue for events // Queue for events
game_context->queue = furi_message_queue_alloc(8, sizeof(GameEvent)); game_context->queue = furi_message_queue_alloc(8, sizeof(GameEvent));
@ -1569,6 +1777,8 @@ int32_t rock_paper_scissors_app(void* p) {
game_context->data->remote_move_tick = furi_get_tick(); game_context->data->remote_move_tick = furi_get_tick();
game_context->data->screen_state = ScreenJoinGame; game_context->data->screen_state = ScreenJoinGame;
} else if(game_context->data->local_player == StateMainMenuPastGames) { } else if(game_context->data->local_player == StateMainMenuPastGames) {
game_context->data->viewing_player_stats =
game_context->data->player_stats;
game_context->data->screen_state = ScreenPastGames; game_context->data->screen_state = ScreenPastGames;
} else if(game_context->data->local_player == StateMainMenuMessage) { } else if(game_context->data->local_player == StateMainMenuMessage) {
game_context->data->screen_state = ScreenEditMessage; game_context->data->screen_state = ScreenEditMessage;
@ -1578,6 +1788,44 @@ int32_t rock_paper_scissors_app(void* p) {
FURI_LOG_T(TAG, "No support for key %d", event.input.key); FURI_LOG_T(TAG, "No support for key %d", event.input.key);
break; break;
} }
} else if(
game_context->data->screen_state == ScreenPastGames &&
event.input.type == InputTypeShort) {
switch(event.input.key) {
case InputKeyLeft:
if(game_context->data->viewing_player_stats) {
if(game_context->data->viewing_player_stats->prev) {
game_context->data->viewing_player_stats =
game_context->data->viewing_player_stats->prev;
FURI_LOG_I(
TAG,
"Moved to item %s.",
furi_string_get_cstr(
game_context->data->viewing_player_stats->flipper_name));
} else {
FURI_LOG_I(TAG, "Viewing first item in list.");
}
}
break;
case InputKeyRight:
if(game_context->data->viewing_player_stats) {
if(game_context->data->viewing_player_stats->next) {
game_context->data->viewing_player_stats =
game_context->data->viewing_player_stats->next;
FURI_LOG_I(
TAG,
"Moved to item %s.",
furi_string_get_cstr(
game_context->data->viewing_player_stats->flipper_name));
} else {
FURI_LOG_I(TAG, "Viewing last item in list.");
}
}
break;
default:
FURI_LOG_T(TAG, "No support for key %d", event.input.key);
break;
}
} }
break; break;
case GameEventPlaySong: case GameEventPlaySong:
@ -1711,6 +1959,7 @@ int32_t rock_paper_scissors_app(void* p) {
furi_message_queue_free(game_context->queue); furi_message_queue_free(game_context->queue);
furi_mutex_free(game_context->mutex); furi_mutex_free(game_context->mutex);
furi_string_free(game_context->data->buffer); furi_string_free(game_context->data->buffer);
furi_string_free(game_context->data->local_contact);
if(game_context->data->remote_name) { if(game_context->data->remote_name) {
furi_string_free(game_context->data->remote_name); furi_string_free(game_context->data->remote_name);
} }

View File

@ -27,7 +27,7 @@
// Name for "N", followed by your name without any spaces. // Name for "N", followed by your name without any spaces.
#define CONTACT_INFO "NYourNameHere" #define CONTACT_INFO "NYourNameHere"
#define CONTACT_INFO_NONE "NNone" #define CONTACT_INFO_NONE "E"
// The message max length should be no larger than a value around 60 to 64. // The message max length should be no larger than a value around 60 to 64.
#define MESSAGE_MAX_LEN 60 #define MESSAGE_MAX_LEN 60
@ -181,6 +181,26 @@ typedef enum {
GameEventPlaySong, GameEventPlaySong,
} GameEventType; } GameEventType;
static const char* contact_list[] = {
"CBuy me a coffee",
"DDiscord",
"EEmpty",
"FFacebook",
"GGithub",
"HHandle",
"IInstagram",
"LLinkedin",
"MMobile",
"NName",
"PPinterest",
"RReddit",
"KTikTok",
"UTinyUrl.com",
"WTwitch",
"TTwitter",
"YYouTube",
};
// An item in the event queue has both the type and its associated data. // An item in the event queue has both the type and its associated data.
// Some fields may be null, they are only set for particular events. // Some fields may be null, they are only set for particular events.
typedef struct { typedef struct {
@ -199,6 +219,25 @@ typedef struct GameInfo {
struct GameInfo* next_game; struct GameInfo* next_game;
} GameInfo; } GameInfo;
typedef struct PlayerStats {
// Line 1:
FuriString* last_played;
// Line 2:
uint16_t win_count;
uint16_t loss_count;
uint16_t tie_count;
// Line 3:
FuriString* flipper_name;
// Line 3+4: contact type, contact
FuriString* contact;
struct PlayerStats* prev;
struct PlayerStats* next;
} PlayerStats;
// This is the data for our application. // This is the data for our application.
typedef struct { typedef struct {
FuriString* buffer; FuriString* buffer;
@ -213,6 +252,9 @@ typedef struct {
struct GameInfo* remote_selected_game; struct GameInfo* remote_selected_game;
FuriString* remote_name; FuriString* remote_name;
FuriString* remote_contact; FuriString* remote_contact;
FuriString* local_contact;
struct PlayerStats* player_stats;
struct PlayerStats* viewing_player_stats;
} GameData; } GameData;
// This is our application context. // This is our application context.
@ -378,7 +420,18 @@ static void remote_games_next(GameContext* game_context);
static void remote_games_previous(GameContext* game_context); static void remote_games_previous(GameContext* game_context);
static void remote_games_add(GameContext* game_context, GameEvent* game_event); static void remote_games_add(GameContext* game_context, GameEvent* game_event);
// Saves a game result to the file system.
// @param game_context pointer to a GameContext.
static void save_result(GameContext* game_context); static void save_result(GameContext* game_context);
static void update_player_stats(
GameContext* game_context,
GameState remote_player,
const char* remote_name,
const char* remote_contact,
const char* datetime);
static void load_player_stats(GameContext* game_context);
// This is the entry point for our application, which should match the application.fam file. // This is the entry point for our application, which should match the application.fam file.
int32_t rock_paper_scissors_app(void* p); int32_t rock_paper_scissors_app(void* p);