From fda2fbcdbba337a8f808977f8025bcaee87d001a Mon Sep 17 00:00:00 2001 From: Derek Jamison Date: Mon, 6 Mar 2023 20:51:41 -0500 Subject: [PATCH] Show past games. --- subghz/plugins/rock_paper_scissors/README.md | 4 +- .../rock_paper_scissors/rock_paper_scissors.c | 253 +++++++++++++++++- .../rock_paper_scissors/rock_paper_scissors.h | 55 +++- 3 files changed, 307 insertions(+), 5 deletions(-) diff --git a/subghz/plugins/rock_paper_scissors/README.md b/subghz/plugins/rock_paper_scissors/README.md index 8fdc73e..37e447f 100644 --- a/subghz/plugins/rock_paper_scissors/README.md +++ b/subghz/plugins/rock_paper_scissors/README.md @@ -19,12 +19,12 @@ Completed work: - Config - Allow changing frequency. - Receiving a Join game does an ACK (to cause game on joiner to start). - Log game results & contact info onto SD card. +- Allow viewing past games/scores. 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. +- Config - Allow changing hard-coded CONTACT_INFO message. - Refactor the code, so it has less duplication. - Write tutorial. - Add game ending animations. diff --git a/subghz/plugins/rock_paper_scissors/rock_paper_scissors.c b/subghz/plugins/rock_paper_scissors/rock_paper_scissors.c index fd8342c..7899a59 100644 --- a/subghz/plugins/rock_paper_scissors/rock_paper_scissors.c +++ b/subghz/plugins/rock_paper_scissors/rock_paper_scissors.c @@ -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 render the UI on this callback thread. // @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); } else if(game_context->data->screen_state == ScreenMainMenu) { 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, MAJOR_VERSION, data->game_number, - CONTACT_INFO, + furi_string_get_cstr(data->local_contact), furi_hal_version_get_name_ptr()); rps_broadcast(game_context, data->buffer); } @@ -771,7 +819,7 @@ static void rps_broadcast_join_acknowledge(GameContext* game_context) { GameRfPurposeJoinAcknowledge, MAJOR_VERSION, data->game_number, - CONTACT_INFO, + furi_string_get_cstr(data->local_contact), furi_hal_version_get_name_ptr()); 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_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_free(games_file); furi_record_close(RECORD_STORAGE); @@ -1268,10 +1333,149 @@ static void save_result(GameContext* game_context) { 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. int32_t rock_paper_scissors_app(void* p) { UNUSED(p); + UNUSED(contact_list); + // Configure our initial data. GameContext* game_context = malloc(sizeof(GameContext)); 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->screen_state = ScreenMainMenu; 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 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->screen_state = ScreenJoinGame; } 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; } else if(game_context->data->local_player == StateMainMenuMessage) { 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); 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; case GameEventPlaySong: @@ -1711,6 +1959,7 @@ int32_t rock_paper_scissors_app(void* p) { furi_message_queue_free(game_context->queue); furi_mutex_free(game_context->mutex); furi_string_free(game_context->data->buffer); + furi_string_free(game_context->data->local_contact); if(game_context->data->remote_name) { furi_string_free(game_context->data->remote_name); } diff --git a/subghz/plugins/rock_paper_scissors/rock_paper_scissors.h b/subghz/plugins/rock_paper_scissors/rock_paper_scissors.h index c6682d2..08388b7 100644 --- a/subghz/plugins/rock_paper_scissors/rock_paper_scissors.h +++ b/subghz/plugins/rock_paper_scissors/rock_paper_scissors.h @@ -27,7 +27,7 @@ // Name for "N", followed by your name without any spaces. #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. #define MESSAGE_MAX_LEN 60 @@ -181,6 +181,26 @@ typedef enum { GameEventPlaySong, } 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. // Some fields may be null, they are only set for particular events. typedef struct { @@ -199,6 +219,25 @@ typedef struct GameInfo { struct GameInfo* next_game; } 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. typedef struct { FuriString* buffer; @@ -213,6 +252,9 @@ typedef struct { struct GameInfo* remote_selected_game; FuriString* remote_name; FuriString* remote_contact; + FuriString* local_contact; + struct PlayerStats* player_stats; + struct PlayerStats* viewing_player_stats; } GameData; // 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_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 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. int32_t rock_paper_scissors_app(void* p); \ No newline at end of file