Add vibro on move. Song on game end.

This commit is contained in:
Derek Jamison 2023-03-04 16:17:45 -05:00
parent b8db448de7
commit 72b1d89078
2 changed files with 555 additions and 413 deletions

View File

@ -1,14 +1,20 @@
# Rock Papper Scissors # Rock Papper Scissors
## Introduction ## Introduction
This is game of Rock, Paper, Scissors. In version 1, we use the subghz radio to find other players, communicate moves, and exchange contact information. This is game of Rock, Paper, Scissors. In version 1, we use the subghz radio to find other players, communicate moves, and exchange contact information.
## Status ## Status
This is currently a work in progress... This is currently a work in progress...
- Most of the game logic is complete (two flippers should be able to play against each other.) - Most of the game logic is complete (two flippers should be able to play against each other.)
- UI to show Flipper images instead of just debug text. - UI to show Flipper images instead of just debug text.
- Vibro on button press (valid move).
- "Song" when win/loss/draw.
Remaining work (for subghz version): Remaining work (for subghz version):
- Tone when button pressed.
- Show games found & let user pick the game to join. - Show games found & let user pick the game to join.
- Log joined game into SD card. - Log joined game into SD card.
- Log game results into SD card. - Log game results into SD card.
@ -21,20 +27,25 @@ Remaining work (for subghz version):
- Add game ending animations. - Add game ending animations.
Future ideas: Future ideas:
- Uses Princeton signals for second player. - Uses Princeton signals for second player.
- Create stand-alone hardware badges that can play the game too. - Create stand-alone hardware badges that can play the game too.
- Instead of subghz, use IR. - Instead of subghz, use IR.
- Instead of subghz, use GPIO. - Instead of subghz, use GPIO.
## Installation Directions ## Installation Directions
This project is intended to be overlayed on top of an existing firmware repo. This project is intended to be overlayed on top of an existing firmware repo.
- Clone, Build & Deploy an existing flipper zero firmware repo. See this [tutorial](/firmware/updating/README.md) for updating firmware. - Clone, Build & Deploy an existing flipper zero firmware repo. See this [tutorial](/firmware/updating/README.md) for updating firmware.
- Copy the "rock_paper_scissors" [folder](..) to the \applications\plugins\rock_paper_scissors folder in your firmware. - Copy the "rock_paper_scissors" [folder](..) to the \applications\plugins\rock_paper_scissors folder in your firmware.
- Build & deploy the firmware. See this [tutorial](/firmware/updating/README.md) for updating firmware. - Build & deploy the firmware. See this [tutorial](/firmware/updating/README.md) for updating firmware.
- NOTE: You can also extract the rock_paper_scissors.FAP from resources.tar file and use qFlipper to copy the file to the SD Card/apps/Misc folder. - NOTE: You can also extract the rock_paper_scissors.FAP from resources.tar file and use qFlipper to copy the file to the SD Card/apps/Misc folder.
### Open question - Should we use "applications" or "applications_user"? ### Open question - Should we use "applications" or "applications_user"?
## Running the updated firmware ## Running the updated firmware
These directions assume you are starting at the flipper desktop. If not, please press the back button until you are at the desktop. These directions assume you are starting at the flipper desktop. If not, please press the back button until you are at the desktop.
- Press the OK button on the flipper to pull up the main menu. - Press the OK button on the flipper to pull up the main menu.
@ -47,6 +58,7 @@ These directions assume you are starting at the flipper desktop. If not, please
- On one of the flippers press Left arrow to join the game. - On one of the flippers press Left arrow to join the game.
- Once two players are joined: - Once two players are joined:
- Press "OK" to send "1". The other player should also press "OK" to send "1" back (at the same time!) - Press "OK" to send "1". The other player should also press "OK" to send "1" back (at the same time!)
- Press "OK" to send "2". The other player should also press "OK" to send "2" back (at the same time!) - Press "OK" to send "2". The other player should also press "OK" to send "2" back (at the same time!)
- Press "UP" to send a "Rock", or "RIGHT" to send a "Paper", or "DOWN" to send a "Scissors". The other player should send at same time! - Press "UP" to send a "Rock", or "RIGHT" to send a "Paper", or "DOWN" to send a "Scissors". The other player should send at same time!
@ -58,16 +70,18 @@ These directions assume you are starting at the flipper desktop. If not, please
- Press the BACK button to exit. - Press the BACK button to exit.
## HackRF One ## HackRF One
- If you do not have two Flipper Zero devices, you can use a HackRF One to record messages & broadcast those messages at a future date. I made a [YouTube](https://www.youtube.com/watch?v=S0sgcDQrVOc) video demo of how to record and broadcast messages. - If you do not have two Flipper Zero devices, you can use a HackRF One to record messages & broadcast those messages at a future date. I made a [YouTube](https://www.youtube.com/watch?v=S0sgcDQrVOc) video demo of how to record and broadcast messages.
- To record a message: (replace "flipper-chat.rf" with the file name you want to use, such as "select-rock.rf".) - To record a message: (replace "flipper-chat.rf" with the file name you want to use, such as "select-rock.rf".)
``` ```
sudo hackrf_transfer -r flipper-chat.rf -f 433920000 -s 8000000 sudo hackrf_transfer -r flipper-chat.rf -f 433920000 -s 8000000
``` ```
- To broadcast a message: (replace "flipper-chat.rf" with the saved file name.) - To broadcast a message: (replace "flipper-chat.rf" with the saved file name.)
``` ```
sudo hackrf_transfer -r flipper-chat.rf -f 433920000 -s 8000000 -x 47 sudo hackrf_transfer -r flipper-chat.rf -f 433920000 -s 8000000 -x 47
``` ```
@ -76,10 +90,10 @@ sudo hackrf_transfer -r flipper-chat.rf -f 433920000 -s 8000000 -x 47
- Use the Flipper Zero to send a messasge that I record, then I play back that message at a later time when I want to pretend the other Flipper Zero is sending a message. You can use the chat app in https://lab.flipper.net/cli, like shown in the video to send a specific packet (or you can use your own code to create the packet.) - Use the Flipper Zero to send a messasge that I record, then I play back that message at a later time when I want to pretend the other Flipper Zero is sending a message. You can use the chat app in https://lab.flipper.net/cli, like shown in the video to send a specific packet (or you can use your own code to create the packet.)
- Use the HackRF One to record the message from my Fliiper Zero. Then later I use the chat app in https://lab.flipper.net/cli, to see what the message was (or I replay the message to see how my application would respond.) - Use the HackRF One to record the message from my Fliiper Zero. Then later I use the chat app in https://lab.flipper.net/cli, to see what the message was (or I replay the message to see how my application would respond.)
## Details about the project files ## Details about the project files
- application.fam - application.fam
- specifies the name of our application. - specifies the name of our application.
- specifies the entry point for our application. - specifies the entry point for our application.
- specifies we use the GUI. - specifies we use the GUI.
@ -87,6 +101,7 @@ sudo hackrf_transfer -r flipper-chat.rf -f 433920000 -s 8000000 -x 47
- specifies our application can be found in the "Misc" category. - specifies our application can be found in the "Misc" category.
- rock_paper_scissors.png - rock_paper_scissors.png
- The icon for our application that shows up in the "Misc" folder. - The icon for our application that shows up in the "Misc" folder.
- rock_paper_scissors.c - rock_paper_scissors.c

View File

@ -22,6 +22,9 @@ Down - Scissors
#include <gui/icon.h> #include <gui/icon.h>
#include <locale/locale.h> #include <locale/locale.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <lib/subghz/subghz_tx_rx_worker.h> #include <lib/subghz/subghz_tx_rx_worker.h>
// This is sent at the beginning of all RF messages. NOTE: It must end with the ':' character. // This is sent at the beginning of all RF messages. NOTE: It must end with the ':' character.
@ -55,9 +58,17 @@ typedef enum {
} DolphinImageIndex; } DolphinImageIndex;
const Icon* images[] = { const Icon* images[] = {
&I_Local_Looking, &I_Local_Ready, &I_Local_Looking,
&I_Local_Count, &I_Local_Rock, &I_Local_Paper, &I_Local_Scissors, &I_Local_Ready,
&I_Remote_Ready, &I_Remote_Count, &I_Remote_Rock, &I_Remote_Paper, &I_Remote_Scissors }; &I_Local_Count,
&I_Local_Rock,
&I_Local_Paper,
&I_Local_Scissors,
&I_Remote_Ready,
&I_Remote_Count,
&I_Remote_Rock,
&I_Remote_Paper,
&I_Remote_Scissors};
// The various moves a player can make. // The various moves a player can make.
// Some moves may be invalid depending on the current game state. // Some moves may be invalid depending on the current game state.
@ -113,6 +124,7 @@ typedef enum {
GameEventLocalMove, GameEventLocalMove,
GameEventRemoteMove, GameEventRemoteMove,
GameEventSendMove, GameEventSendMove,
GameEventPlaySong,
} GameEventType; } GameEventType;
// 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.
@ -144,7 +156,78 @@ typedef struct {
SubGhzTxRxWorker* subghz_txrx; SubGhzTxRxWorker* subghz_txrx;
} GameContext; } GameContext;
// // Checks if game state is winner.
// @param state GameState to check.
// @returns true if game state is a winner.
static bool isWin(GameState state) {
return (StateWonPaper == state) || (StateWonRock == state) || (StateWonScissors == state);
}
// Checks if game state is lost.
// @param state GameState to check.
// @returns true if game state is a loss.
static bool isLoss(GameState state) {
return (StateLostPaper == state) || (StateLostRock == state) || (StateLostScissors == state);
}
// Checks if game state is tie.
// @param state GameState to check.
// @returns true if game state is a tie.
static bool isTie(GameState state) {
return (StateTiePaper == state) || (StateTieRock == state) || (StateTieScissors == state);
}
// When user makes a move, we briefly pulse the vibro motor.
static void single_vibro() {
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
notification_message(notification, &sequence_single_vibro);
furi_record_close(RECORD_NOTIFICATION);
}
// Plays a note. You must acquire the speaker before invoking.
// @frequency the frequency of the note in Hz.
// @volume the volume of the note from 0.0 to 1.0
// @durationPlay the duration of the note in ms.
// @durationPause the duration after the note to be silent.
static void
play_note(float frequency, float volume, uint32_t durationPlay, uint32_t durationPause) {
furi_hal_speaker_start(frequency, volume);
uint32_t n = furi_get_tick();
while(furi_get_tick() < n + durationPlay) {
furi_thread_yield();
}
furi_hal_speaker_stop();
n = furi_get_tick();
while(furi_get_tick() < n + durationPause) {
furi_thread_yield();
}
}
// Play a song
static void play_song(GameState state) {
if(furi_hal_speaker_acquire(1000)) {
const float volume = 1.0f;
const uint32_t playQtr = 500;
const uint32_t delayQtr = 100;
if(isWin(state)) {
play_note(523.25f, volume, playQtr, delayQtr);
play_note(659.25f, volume, playQtr, delayQtr);
play_note(783.99f, volume, playQtr, delayQtr);
} else if(isLoss(state)) {
play_note(783.99f, volume, playQtr * 2, delayQtr);
play_note(523.25f, volume, playQtr, delayQtr);
} else if(isTie(state)) {
play_note(783.99f, volume, playQtr, delayQtr);
play_note(523.25f, volume, playQtr, delayQtr);
play_note(783.99f, volume, playQtr, delayQtr);
}
furi_hal_speaker_stop();
furi_hal_speaker_release();
}
}
// We register this callback to get invoked whenever new subghz data is received. // We register this callback to get invoked whenever new subghz data is received.
// Queue a GameEventDataDetected message. // Queue a GameEventDataDetected message.
// @param ctx pointer to a GameContext // @param ctx pointer to a GameContext
@ -176,19 +259,19 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
memset(message, 0x00, MESSAGE_MAX_LEN); memset(message, 0x00, MESSAGE_MAX_LEN);
size_t len = subghz_tx_rx_worker_read(game_context->subghz_txrx, message, MESSAGE_MAX_LEN); size_t len = subghz_tx_rx_worker_read(game_context->subghz_txrx, message, MESSAGE_MAX_LEN);
size_t game_name_len = strlen(RPS_GAME_NAME); size_t game_name_len = strlen(RPS_GAME_NAME);
if (len < (game_name_len + 2)) { if(len < (game_name_len + 2)) {
FURI_LOG_D(TAG, "Message not long enough. >%s<", message); FURI_LOG_D(TAG, "Message not long enough. >%s<", message);
return; return;
} }
// The message for a move (M) (like 'R' for Rock) using version (A) should be "RPS:" + "M" + "A" + game"###" + move"R" + ":" + "YourFlip" + "\r\n" // The message for a move (M) (like 'R' for Rock) using version (A) should be "RPS:" + "M" + "A" + game"###" + move"R" + ":" + "YourFlip" + "\r\n"
if (strcmp(RPS_GAME_NAME, (const char*)message)) { if(strcmp(RPS_GAME_NAME, (const char*)message)) {
FURI_LOG_D(TAG, "Got message >%s<", message); FURI_LOG_D(TAG, "Got message >%s<", message);
// The purpose immediately follows the game name. // The purpose immediately follows the game name.
GameRfPurpose purpose = message[game_name_len]; GameRfPurpose purpose = message[game_name_len];
// The version follows the purpose. // The version follows the purpose.
uint8_t version = message[game_name_len+1]; uint8_t version = message[game_name_len + 1];
FURI_LOG_T(TAG, "Purpose is %c and version is %c", purpose, version); FURI_LOG_T(TAG, "Purpose is %c and version is %c", purpose, version);
// Null terminate buffer at the end of message so we can't overrun the buffer. // Null terminate buffer at the end of message so we can't overrun the buffer.
@ -199,16 +282,26 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
char senderName[9]; char senderName[9];
char tmp; char tmp;
Move move = MoveUnknown; Move move = MoveUnknown;
switch (purpose) { switch(purpose) {
case GameRfPurposeMove: case GameRfPurposeMove:
// We expect this mesage to the game number, move and sender name. // We expect this mesage to the game number, move and sender name.
if (sscanf((const char*)message+game_name_len+2, "%03u%c:%8s", &gameNumber, &tmp, senderName) == 3) { if(sscanf(
(const char*)message + game_name_len + 2,
"%03u%c:%8s",
&gameNumber,
&tmp,
senderName) == 3) {
move = (Move)tmp; move = (Move)tmp;
// IMPORTANT: The code processing the event needs to furi_string_free the senderName! // IMPORTANT: The code processing the event needs to furi_string_free the senderName!
FuriString* name = furi_string_alloc(); FuriString* name = furi_string_alloc();
furi_string_set(name, senderName); furi_string_set(name, senderName);
GameEvent event = {.type = GameEventRemoteMove, .move = move, .tick = tick, .senderName = name, .gameNumber = gameNumber }; GameEvent event = {
.type = GameEventRemoteMove,
.move = move,
.tick = tick,
.senderName = name,
.gameNumber = gameNumber};
furi_message_queue_put(game_context->queue, &event, FuriWaitForever); furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
} else { } else {
FURI_LOG_W(TAG, "Failed to parse move message. >%s<", message); FURI_LOG_W(TAG, "Failed to parse move message. >%s<", message);
@ -217,12 +310,15 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
case GameRfPurposeBeacon: case GameRfPurposeBeacon:
// We expect this mesage to the game number, move and sender name. // We expect this mesage to the game number, move and sender name.
if (sscanf((const char*)message+game_name_len+2, "%03u:%8s", &gameNumber, senderName) == 2) { if(sscanf(
(const char*)message + game_name_len + 2, "%03u:%8s", &gameNumber, senderName) ==
2) {
// IMPORTANT: The code processing the event needs to furi_string_free the senderName! // IMPORTANT: The code processing the event needs to furi_string_free the senderName!
FuriString* name = furi_string_alloc(); FuriString* name = furi_string_alloc();
furi_string_set(name, senderName); furi_string_set(name, senderName);
GameEvent event = {.type = GameEventRemoteBeacon, .senderName = name, .gameNumber = gameNumber }; GameEvent event = {
.type = GameEventRemoteBeacon, .senderName = name, .gameNumber = gameNumber};
furi_message_queue_put(game_context->queue, &event, FuriWaitForever); furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
} else { } else {
FURI_LOG_W(TAG, "Failed to parse beacon message. >%s<", message); FURI_LOG_W(TAG, "Failed to parse beacon message. >%s<", message);
@ -231,14 +327,20 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
case GameRfPurposeJoin: case GameRfPurposeJoin:
// We expect this mesage to the game number, move and sender name. // We expect this mesage to the game number, move and sender name.
if (sscanf((const char*)message+game_name_len+2, "%03u%s :%8s", &gameNumber, randomInfo, senderName) == 3) { if(sscanf(
(const char*)message + game_name_len + 2,
"%03u%s :%8s",
&gameNumber,
randomInfo,
senderName) == 3) {
FURI_LOG_T(TAG, "Join had randomInfo of >%s<", randomInfo); FURI_LOG_T(TAG, "Join had randomInfo of >%s<", randomInfo);
// IMPORTANT: The code processing the event needs to furi_string_free the senderName! // IMPORTANT: The code processing the event needs to furi_string_free the senderName!
FuriString* name = furi_string_alloc(); FuriString* name = furi_string_alloc();
furi_string_set(name, senderName); furi_string_set(name, senderName);
GameEvent event = {.type = GameEventRemoteJoined, .senderName = name, .gameNumber = gameNumber }; GameEvent event = {
.type = GameEventRemoteJoined, .senderName = name, .gameNumber = gameNumber};
furi_message_queue_put(game_context->queue, &event, FuriWaitForever); furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
} else { } else {
FURI_LOG_W(TAG, "Failed to parse join message. >%s<", message); FURI_LOG_W(TAG, "Failed to parse join message. >%s<", message);
@ -246,7 +348,7 @@ static void rps_receive_data(GameContext* game_context, uint32_t tick) {
break; break;
default: default:
if (version <= MAJOR_VERSION) { if(version <= MAJOR_VERSION) {
// The version is same or less than ours, so we should know about the message purpose. // The version is same or less than ours, so we should know about the message purpose.
FURI_LOG_E(TAG, "Message purpose not handled for known version. >%s<", message); FURI_LOG_E(TAG, "Message purpose not handled for known version. >%s<", message);
} else { } else {
@ -290,7 +392,7 @@ static void rps_render_callback(Canvas* canvas, void* ctx) {
canvas_set_font(canvas, FontSecondary); canvas_set_font(canvas, FontSecondary);
switch (remotePlayer) { switch(remotePlayer) {
case StateReady: case StateReady:
canvas_draw_icon(canvas, 64, 0, images[DolphinRemoteReady]); canvas_draw_icon(canvas, 64, 0, images[DolphinRemoteReady]);
break; break;
@ -309,7 +411,7 @@ static void rps_render_callback(Canvas* canvas, void* ctx) {
case StateTieRock: case StateTieRock:
case StateWonRock: case StateWonRock:
case StateLostRock: case StateLostRock:
if (StateCount2 != localPlayer) { if(StateCount2 != localPlayer) {
canvas_draw_icon(canvas, 64, 0, images[DolphinRemoteRock]); canvas_draw_icon(canvas, 64, 0, images[DolphinRemoteRock]);
canvas_draw_str_aligned(canvas, 70, 55, AlignLeft, AlignTop, "Rock"); canvas_draw_str_aligned(canvas, 70, 55, AlignLeft, AlignTop, "Rock");
} else { } else {
@ -321,7 +423,7 @@ static void rps_render_callback(Canvas* canvas, void* ctx) {
case StateTiePaper: case StateTiePaper:
case StateWonPaper: case StateWonPaper:
case StateLostPaper: case StateLostPaper:
if (StateCount2 != localPlayer) { if(StateCount2 != localPlayer) {
canvas_draw_icon(canvas, 64, 0, images[DolphinRemotePaper]); canvas_draw_icon(canvas, 64, 0, images[DolphinRemotePaper]);
canvas_draw_str_aligned(canvas, 70, 55, AlignLeft, AlignTop, "Paper"); canvas_draw_str_aligned(canvas, 70, 55, AlignLeft, AlignTop, "Paper");
} else { } else {
@ -333,7 +435,7 @@ static void rps_render_callback(Canvas* canvas, void* ctx) {
case StateTieScissors: case StateTieScissors:
case StateWonScissors: case StateWonScissors:
case StateLostScissors: case StateLostScissors:
if (StateCount2 != localPlayer) { if(StateCount2 != localPlayer) {
canvas_draw_icon(canvas, 64, 0, images[DolphinRemoteScissors]); canvas_draw_icon(canvas, 64, 0, images[DolphinRemoteScissors]);
canvas_draw_str_aligned(canvas, 70, 55, AlignLeft, AlignTop, "Scissors"); canvas_draw_str_aligned(canvas, 70, 55, AlignLeft, AlignTop, "Scissors");
} else { } else {
@ -345,11 +447,12 @@ static void rps_render_callback(Canvas* canvas, void* ctx) {
break; break;
} }
switch (localPlayer) { switch(localPlayer) {
case StateLookingForPlayer: case StateLookingForPlayer:
canvas_draw_icon(canvas, 0, 0, images[DolphinLocalLooking]); canvas_draw_icon(canvas, 0, 0, images[DolphinLocalLooking]);
furi_string_printf(data->buffer, "Waiting for player, game %03d.", data->gameNumber); furi_string_printf(data->buffer, "Waiting for player, game %03d.", data->gameNumber);
canvas_draw_str_aligned(canvas, 0, 55, AlignLeft, AlignTop, furi_string_get_cstr(data->buffer)); canvas_draw_str_aligned(
canvas, 0, 55, AlignLeft, AlignTop, furi_string_get_cstr(data->buffer));
break; break;
case StateReady: case StateReady:
@ -467,14 +570,15 @@ static void rps_broadcast(GameContext* game_context, FuriString* buffer) {
// Make sure our message will fit into a packet; if not truncate it. // Make sure our message will fit into a packet; if not truncate it.
size_t length = strlen((char*)message); size_t length = strlen((char*)message);
if (length>MESSAGE_MAX_LEN) { if(length > MESSAGE_MAX_LEN) {
// SECURITY REVIEW - Is it okay to log, or do we need to truncate first? // SECURITY REVIEW - Is it okay to log, or do we need to truncate first?
FURI_LOG_E(TAG, "Outgoing message bigger than %d bytes! >%s<", MESSAGE_MAX_LEN, (char*)message); FURI_LOG_E(
TAG, "Outgoing message bigger than %d bytes! >%s<", MESSAGE_MAX_LEN, (char*)message);
// Add \r\n(null) to the end of the 0-indexed string. // Add \r\n(null) to the end of the 0-indexed string.
message[MESSAGE_MAX_LEN-1] = 0; message[MESSAGE_MAX_LEN - 1] = 0;
message[MESSAGE_MAX_LEN-2] = '\n'; message[MESSAGE_MAX_LEN - 2] = '\n';
message[MESSAGE_MAX_LEN-3] = '\r'; message[MESSAGE_MAX_LEN - 3] = '\r';
length = MESSAGE_MAX_LEN; length = MESSAGE_MAX_LEN;
} }
@ -493,7 +597,15 @@ static void rps_broadcast_move(GameContext* game_context, Move moveToSend) {
FURI_LOG_I(TAG, "Sending move %c", moveToSend); FURI_LOG_I(TAG, "Sending move %c", moveToSend);
// The message for game 42 with a move with value Rock should look like... "RPS:MA042R:YourFlip\r\n" // The message for game 42 with a move with value Rock should look like... "RPS:MA042R:YourFlip\r\n"
furi_string_printf(data->buffer, "%s%c%c%03u%c:%s\r\n", RPS_GAME_NAME, GameRfPurposeMove, MAJOR_VERSION, data->gameNumber, moveToSend, furi_hal_version_get_name_ptr()); furi_string_printf(
data->buffer,
"%s%c%c%03u%c:%s\r\n",
RPS_GAME_NAME,
GameRfPurposeMove,
MAJOR_VERSION,
data->gameNumber,
moveToSend,
furi_hal_version_get_name_ptr());
rps_broadcast(game_context, data->buffer); rps_broadcast(game_context, data->buffer);
} }
@ -505,7 +617,14 @@ static void rps_broadcast_beacon(GameContext* game_context) {
FURI_LOG_I(TAG, "Sending beacon"); FURI_LOG_I(TAG, "Sending beacon");
// The message for game 42 should look like... "RPS:BA042:YourFlip\r\n" // The message for game 42 should look like... "RPS:BA042:YourFlip\r\n"
furi_string_printf(data->buffer, "%s%c%c%03u:%s\r\n", RPS_GAME_NAME, GameRfPurposeBeacon, MAJOR_VERSION, data->gameNumber, furi_hal_version_get_name_ptr()); furi_string_printf(
data->buffer,
"%s%c%c%03u:%s\r\n",
RPS_GAME_NAME,
GameRfPurposeBeacon,
MAJOR_VERSION,
data->gameNumber,
furi_hal_version_get_name_ptr());
rps_broadcast(game_context, data->buffer); rps_broadcast(game_context, data->buffer);
} }
@ -518,7 +637,15 @@ static void rps_broadcast_join(GameContext* game_context, unsigned int gameNumbe
FURI_LOG_I(TAG, "Joining game %d.", gameNumber); FURI_LOG_I(TAG, "Joining game %d.", gameNumber);
// The message for game 42 should look like... "RPS:JA042NYourNameHere :YourFlip\r\n" // The message for game 42 should look like... "RPS:JA042NYourNameHere :YourFlip\r\n"
furi_string_printf(data->buffer, "%s%c%c%03u%s :%s\r\n", RPS_GAME_NAME, GameRfPurposeJoin, MAJOR_VERSION, data->gameNumber, CONTACT_INFO, furi_hal_version_get_name_ptr()); furi_string_printf(
data->buffer,
"%s%c%c%03u%s :%s\r\n",
RPS_GAME_NAME,
GameRfPurposeJoin,
MAJOR_VERSION,
data->gameNumber,
CONTACT_INFO,
furi_hal_version_get_name_ptr());
rps_broadcast(game_context, data->buffer); rps_broadcast(game_context, data->buffer);
} }
@ -527,7 +654,7 @@ static void rps_broadcast_join(GameContext* game_context, unsigned int gameNumbe
static uint32_t duration(uint32_t tick) { static uint32_t duration(uint32_t tick) {
uint32_t current = furi_get_tick(); uint32_t current = furi_get_tick();
// Every 55 days the tick could wrap. // Every 55 days the tick could wrap.
if (current < tick) { if(current < tick) {
FURI_LOG_T(TAG, "tick count wrapped! current:%ld prev:%ld", current, tick); FURI_LOG_T(TAG, "tick count wrapped! current:%ld prev:%ld", current, tick);
return current + (UINT32_MAX - tick); return current + (UINT32_MAX - tick);
} }
@ -535,33 +662,6 @@ static uint32_t duration(uint32_t tick) {
return current - tick; return current - tick;
} }
// Checks if game state is winner.
// @param state GameState to check.
// @returns true if game state is a winner.
static bool isWin(GameState state) {
return (StateWonPaper == state) ||
(StateWonRock == state) ||
(StateWonScissors == state);
}
// Checks if game state is lost.
// @param state GameState to check.
// @returns true if game state is a loss.
static bool isLoss(GameState state) {
return (StateLostPaper == state) ||
(StateLostRock == state) ||
(StateLostScissors == state);
}
// Checks if game state is tie.
// @param state GameState to check.
// @returns true if game state is a tie.
static bool isTie(GameState state) {
return (StateTiePaper == state) ||
(StateTieRock == state) ||
(StateTieScissors == state);
}
// Checks if game state is result (win/loss/tie). // Checks if game state is result (win/loss/tie).
// @param state GameState to check. // @param state GameState to check.
// @returns true if game state is a win, loss or tie. // @returns true if game state is a win, loss or tie.
@ -573,16 +673,12 @@ static bool isResult(GameState state) {
// @param state GameState to check. // @param state GameState to check.
// @returns true if game state is a rock, paper, scissors. // @returns true if game state is a rock, paper, scissors.
static bool isFinalMove(GameState state) { static bool isFinalMove(GameState state) {
return (StateRock == state) || return (StateRock == state) || (StatePaper == state) || (StateScissors == state);
(StatePaper == state) ||
(StateScissors == state);
} }
static bool isError(GameState state) { static bool isError(GameState state) {
return (StateError == state) || return (StateError == state) || (StateErrorLocalFast == state) ||
(StateErrorLocalFast == state) || (StateErrorRemoteFast == state) || (StateErrorRemoteTimeout == state);
(StateErrorRemoteFast == state) ||
(StateErrorRemoteTimeout == state);
} }
// Temporary timings, since I don't have second Flipper & send commands via laptop. // Temporary timings, since I don't have second Flipper & send commands via laptop.
@ -598,7 +694,7 @@ static void rps_state_machine_update(GameContext* game_context) {
FURI_LOG_I(TAG, "Validating game state. local:%c Remote:%c", d->localPlayer, d->remotePlayer); FURI_LOG_I(TAG, "Validating game state. local:%c Remote:%c", d->localPlayer, d->remotePlayer);
// Did player leave after joining? // Did player leave after joining?
if ((StateReady == d->remotePlayer) && if((StateReady == d->remotePlayer) &&
(duration(d->remoteMoveTick) > DURATION_NO_MOVE_DETECTED_ERROR)) { (duration(d->remoteMoveTick) > DURATION_NO_MOVE_DETECTED_ERROR)) {
d->remotePlayer = StateLookingForPlayer; d->remotePlayer = StateLookingForPlayer;
d->remoteMoveTick = furi_get_tick(); d->remoteMoveTick = furi_get_tick();
@ -610,8 +706,7 @@ static void rps_state_machine_update(GameContext* game_context) {
} }
// TEMP - After Error, we reset back to Looking for player. // TEMP - After Error, we reset back to Looking for player.
if (isError(d->localPlayer) && if(isError(d->localPlayer) && (duration(d->localMoveTick) > DURATION_SHOW_ERROR)) {
(duration(d->localMoveTick) > DURATION_SHOW_ERROR)) {
d->remotePlayer = StateLookingForPlayer; d->remotePlayer = StateLookingForPlayer;
d->remoteMoveTick = furi_get_tick(); d->remoteMoveTick = furi_get_tick();
d->localPlayer = StateLookingForPlayer; d->localPlayer = StateLookingForPlayer;
@ -621,8 +716,7 @@ static void rps_state_machine_update(GameContext* game_context) {
} }
// TEMP - After Win, Loss, Tie - we reset back to Ready. // TEMP - After Win, Loss, Tie - we reset back to Ready.
if (isResult(d->localPlayer) && if(isResult(d->localPlayer) && (duration(d->localMoveTick) > DURATION_WIN_LOSS_TIE)) {
(duration(d->localMoveTick) > DURATION_WIN_LOSS_TIE)) {
d->remotePlayer = StateReady; d->remotePlayer = StateReady;
d->remoteMoveTick = furi_get_tick(); d->remoteMoveTick = furi_get_tick();
d->localPlayer = StateReady; d->localPlayer = StateReady;
@ -633,40 +727,40 @@ static void rps_state_machine_update(GameContext* game_context) {
} }
// Check for winner. // Check for winner.
if (isFinalMove(d->localPlayer) && isFinalMove(d->remotePlayer) && if(isFinalMove(d->localPlayer) && isFinalMove(d->remotePlayer) &&
(duration(d->localMoveTick) > DURATION_SHOW_MOVES)) { (duration(d->localMoveTick) > DURATION_SHOW_MOVES)) {
d->localMoveTick = furi_get_tick(); d->localMoveTick = furi_get_tick();
d->remoteMoveTick = furi_get_tick(); d->remoteMoveTick = furi_get_tick();
if ((d->localPlayer == StateRock) && (d->remotePlayer == StateScissors)) { if((d->localPlayer == StateRock) && (d->remotePlayer == StateScissors)) {
d->localPlayer = StateWonRock; d->localPlayer = StateWonRock;
d->remotePlayer = StateLostScissors; d->remotePlayer = StateLostScissors;
FURI_LOG_I(TAG, "Local won w/Rock."); FURI_LOG_I(TAG, "Local won w/Rock.");
} else if ((d->localPlayer == StateScissors) && (d->remotePlayer == StatePaper)) { } else if((d->localPlayer == StateScissors) && (d->remotePlayer == StatePaper)) {
d->localPlayer = StateWonScissors; d->localPlayer = StateWonScissors;
d->remotePlayer = StateLostPaper; d->remotePlayer = StateLostPaper;
FURI_LOG_I(TAG, "Local won w/Scissors."); FURI_LOG_I(TAG, "Local won w/Scissors.");
} else if ((d->localPlayer == StatePaper) && (d->remotePlayer == StateRock)) { } else if((d->localPlayer == StatePaper) && (d->remotePlayer == StateRock)) {
d->localPlayer = StateWonPaper; d->localPlayer = StateWonPaper;
d->remotePlayer = StateLostRock; d->remotePlayer = StateLostRock;
FURI_LOG_I(TAG, "Local won w/Paper."); FURI_LOG_I(TAG, "Local won w/Paper.");
} else if ((d->localPlayer == StateRock) && (d->remotePlayer == StatePaper)) { } else if((d->localPlayer == StateRock) && (d->remotePlayer == StatePaper)) {
d->localPlayer = StateLostRock; d->localPlayer = StateLostRock;
d->remotePlayer = StateWonPaper; d->remotePlayer = StateWonPaper;
FURI_LOG_I(TAG, "Remote won w/Paper."); FURI_LOG_I(TAG, "Remote won w/Paper.");
} else if ((d->localPlayer == StateScissors) && (d->remotePlayer == StateRock)) { } else if((d->localPlayer == StateScissors) && (d->remotePlayer == StateRock)) {
d->localPlayer = StateLostScissors; d->localPlayer = StateLostScissors;
d->remotePlayer = StateWonRock; d->remotePlayer = StateWonRock;
FURI_LOG_I(TAG, "Remote won w/Rock."); FURI_LOG_I(TAG, "Remote won w/Rock.");
} else if ((d->localPlayer == StatePaper) && (d->remotePlayer == StateScissors)) { } else if((d->localPlayer == StatePaper) && (d->remotePlayer == StateScissors)) {
d->localPlayer = StateLostPaper; d->localPlayer = StateLostPaper;
d->remotePlayer = StateWonScissors; d->remotePlayer = StateWonScissors;
FURI_LOG_I(TAG, "Remote won w/Scissors."); FURI_LOG_I(TAG, "Remote won w/Scissors.");
} else { } else {
FURI_LOG_I(TAG, "Tie game."); FURI_LOG_I(TAG, "Tie game.");
if (d->localPlayer == StateRock) { if(d->localPlayer == StateRock) {
d->localPlayer = StateTieRock; d->localPlayer = StateTieRock;
d->remotePlayer = StateTieRock; d->remotePlayer = StateTieRock;
} else if (d->localPlayer == StatePaper) { } else if(d->localPlayer == StatePaper) {
d->localPlayer = StateTiePaper; d->localPlayer = StateTiePaper;
d->remotePlayer = StateTiePaper; d->remotePlayer = StateTiePaper;
} else { } else {
@ -674,20 +768,24 @@ static void rps_state_machine_update(GameContext* game_context) {
d->remotePlayer = StateTieScissors; d->remotePlayer = StateTieScissors;
} }
} }
GameEvent event = {.type = GameEventPlaySong};
furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
} }
} }
// Update the state machine to reflect that a remote user joined the game. // Update the state machine to reflect that a remote user joined the game.
// @param game_context pointer to a GameContext. // @param game_context pointer to a GameContext.
static void rps_state_machine_remote_joined(GameContext* game_context) { static void rps_state_machine_remote_joined(GameContext* game_context) {
if (StateLookingForPlayer == game_context->data->localPlayer) { if(StateLookingForPlayer == game_context->data->localPlayer) {
FURI_LOG_I(TAG, "Remote player joined our game!"); FURI_LOG_I(TAG, "Remote player joined our game!");
game_context->data->remotePlayer = StateReady; game_context->data->remotePlayer = StateReady;
game_context->data->remoteMoveTick = furi_get_tick(); game_context->data->remoteMoveTick = furi_get_tick();
game_context->data->localPlayer = StateReady; game_context->data->localPlayer = StateReady;
game_context->data->localMoveTick = furi_get_tick(); game_context->data->localMoveTick = furi_get_tick();
} else { } else {
FURI_LOG_I(TAG, "Remote requested join, but we are state %c!", game_context->data->localPlayer); FURI_LOG_I(
TAG, "Remote requested join, but we are state %c!", game_context->data->localPlayer);
} }
} }
@ -700,58 +798,77 @@ static bool rps_state_machine_local_moved(GameContext* game_context, Move move)
Move localMove = MoveUnknown; Move localMove = MoveUnknown;
GameState localState = StateReady; GameState localState = StateReady;
if (MoveCount == move && StateReady == game_context->data->localPlayer) { if(MoveCount == move && StateReady == game_context->data->localPlayer) {
localMove = MoveCount1; localMove = MoveCount1;
localState = StateCount1; localState = StateCount1;
} else if (MoveCount == move && StateCount1 == game_context->data->localPlayer) { } else if(MoveCount == move && StateCount1 == game_context->data->localPlayer) {
if ((StateCount1 == game_context->data->remotePlayer) || if((StateCount1 == game_context->data->remotePlayer) ||
(StateCount2 == game_context->data->remotePlayer)) { (StateCount2 == game_context->data->remotePlayer)) {
localMove = MoveCount2; localMove = MoveCount2;
localState = StateCount2; localState = StateCount2;
} else { } else {
localState = StateErrorLocalFast; localState = StateErrorLocalFast;
FURI_LOG_I(TAG, "Local count sync error. remote is %c.", game_context->data->remotePlayer); FURI_LOG_I(
TAG, "Local count sync error. remote is %c.", game_context->data->remotePlayer);
} }
} else if (StateCount2 == game_context->data->localPlayer) { } else if(StateCount2 == game_context->data->localPlayer) {
if (MoveRock == move) { if(MoveRock == move) {
if ((StateCount2 == game_context->data->remotePlayer) || if((StateCount2 == game_context->data->remotePlayer) ||
isFinalMove(game_context->data->remotePlayer)) { isFinalMove(game_context->data->remotePlayer)) {
localMove = MoveRock; localMove = MoveRock;
localState = StateRock; localState = StateRock;
} else { } else {
localState = StateErrorLocalFast; localState = StateErrorLocalFast;
FURI_LOG_I(TAG, "Local rock sync error. remote is %c.", game_context->data->remotePlayer); FURI_LOG_I(
TAG, "Local rock sync error. remote is %c.", game_context->data->remotePlayer);
} }
} else if (MovePaper == move) { } else if(MovePaper == move) {
if ((StateCount2 == game_context->data->remotePlayer) || if((StateCount2 == game_context->data->remotePlayer) ||
isFinalMove(game_context->data->remotePlayer)) { isFinalMove(game_context->data->remotePlayer)) {
localMove = MovePaper; localMove = MovePaper;
localState = StatePaper; localState = StatePaper;
} else { } else {
localState = StateErrorLocalFast; localState = StateErrorLocalFast;
FURI_LOG_I(TAG, "Local paper sync error. remote is %c.", game_context->data->remotePlayer); FURI_LOG_I(
TAG,
"Local paper sync error. remote is %c.",
game_context->data->remotePlayer);
} }
} else if (MoveScissors == move) { } else if(MoveScissors == move) {
if ((StateCount2 == game_context->data->remotePlayer) || if((StateCount2 == game_context->data->remotePlayer) ||
isFinalMove(game_context->data->remotePlayer)) { isFinalMove(game_context->data->remotePlayer)) {
localMove = MoveScissors; localMove = MoveScissors;
localState = StateScissors; localState = StateScissors;
} else { } else {
localState = StateErrorLocalFast; localState = StateErrorLocalFast;
FURI_LOG_I(TAG, "Local scissors sync error. remote is %c.", game_context->data->remotePlayer); FURI_LOG_I(
TAG,
"Local scissors sync error. remote is %c.",
game_context->data->remotePlayer);
} }
} else { } else {
FURI_LOG_E(TAG, "Invalid Local move '%c' error. lState=%c. rState=%c.", move, game_context->data->localPlayer, game_context->data->remotePlayer); FURI_LOG_E(
TAG,
"Invalid Local move '%c' error. lState=%c. rState=%c.",
move,
game_context->data->localPlayer,
game_context->data->remotePlayer);
} }
} else { } else {
FURI_LOG_E(TAG, "Invalid Local move '%c' error. lState=%c. rState=%c.", move, game_context->data->localPlayer, game_context->data->remotePlayer); FURI_LOG_E(
TAG,
"Invalid Local move '%c' error. lState=%c. rState=%c.",
move,
game_context->data->localPlayer,
game_context->data->remotePlayer);
} }
if (MoveUnknown != localMove) { if(MoveUnknown != localMove) {
single_vibro();
rps_broadcast_move(game_context, localMove); rps_broadcast_move(game_context, localMove);
} }
if (StateReady != localState) { if(StateReady != localState) {
game_context->data->localPlayer = localState; game_context->data->localPlayer = localState;
game_context->data->localMoveTick = furi_get_tick(); game_context->data->localMoveTick = furi_get_tick();
} }
@ -766,46 +883,55 @@ static bool rps_state_machine_remote_moved(GameContext* game_context, Move move)
GameState remoteState = StateReady; GameState remoteState = StateReady;
FURI_LOG_I(TAG, "Remote move %c.", move); FURI_LOG_I(TAG, "Remote move %c.", move);
if (MoveCount1 == move && StateReady == game_context->data->remotePlayer) { if(MoveCount1 == move && StateReady == game_context->data->remotePlayer) {
remoteState = StateCount1; remoteState = StateCount1;
} else if (MoveCount2 == move && StateCount1 == game_context->data->remotePlayer) { } else if(MoveCount2 == move && StateCount1 == game_context->data->remotePlayer) {
if ((StateCount1 == game_context->data->localPlayer) || if((StateCount1 == game_context->data->localPlayer) ||
(StateCount2 == game_context->data->localPlayer)) { (StateCount2 == game_context->data->localPlayer)) {
remoteState = StateCount2; remoteState = StateCount2;
} else { } else {
remoteState = StateErrorRemoteFast; remoteState = StateErrorRemoteFast;
FURI_LOG_I(TAG, "Remote count sync error. local is %c.", game_context->data->localPlayer); FURI_LOG_I(
TAG, "Remote count sync error. local is %c.", game_context->data->localPlayer);
} }
} else if (MoveRock == move && StateCount2 == game_context->data->remotePlayer) { } else if(MoveRock == move && StateCount2 == game_context->data->remotePlayer) {
if ((StateCount2 == game_context->data->localPlayer) || if((StateCount2 == game_context->data->localPlayer) ||
isFinalMove(game_context->data->localPlayer)) { isFinalMove(game_context->data->localPlayer)) {
remoteState = StateRock; remoteState = StateRock;
} else { } else {
remoteState = StateErrorRemoteFast; remoteState = StateErrorRemoteFast;
FURI_LOG_I(TAG, "Remote rock sync error. local is %c.", game_context->data->localPlayer); FURI_LOG_I(
TAG, "Remote rock sync error. local is %c.", game_context->data->localPlayer);
} }
} else if (MovePaper == move && StateCount2 == game_context->data->remotePlayer) { } else if(MovePaper == move && StateCount2 == game_context->data->remotePlayer) {
if ((StateCount2 == game_context->data->localPlayer) || if((StateCount2 == game_context->data->localPlayer) ||
isFinalMove(game_context->data->localPlayer)) { isFinalMove(game_context->data->localPlayer)) {
remoteState = StatePaper; remoteState = StatePaper;
} else { } else {
remoteState = StateErrorRemoteFast; remoteState = StateErrorRemoteFast;
FURI_LOG_I(TAG, "Remote paper sync error. local is %c.", game_context->data->localPlayer); FURI_LOG_I(
TAG, "Remote paper sync error. local is %c.", game_context->data->localPlayer);
} }
} else if (MoveScissors == move && StateCount2 == game_context->data->remotePlayer) { } else if(MoveScissors == move && StateCount2 == game_context->data->remotePlayer) {
if ((StateCount2 == game_context->data->localPlayer) || if((StateCount2 == game_context->data->localPlayer) ||
isFinalMove(game_context->data->localPlayer)) { isFinalMove(game_context->data->localPlayer)) {
remoteState = StateScissors; remoteState = StateScissors;
} else { } else {
remoteState = StateErrorRemoteFast; remoteState = StateErrorRemoteFast;
FURI_LOG_I(TAG, "Remote scissors sync error. local is %c.", game_context->data->localPlayer); FURI_LOG_I(
TAG, "Remote scissors sync error. local is %c.", game_context->data->localPlayer);
} }
} else { } else {
FURI_LOG_E(TAG, "Remote move '%c' error. lState=%c. rState=%c.", move, game_context->data->localPlayer, game_context->data->remotePlayer); FURI_LOG_E(
TAG,
"Remote move '%c' error. lState=%c. rState=%c.",
move,
game_context->data->localPlayer,
game_context->data->remotePlayer);
remoteState = StateError; remoteState = StateError;
} }
if (StateReady != remoteState) { if(StateReady != remoteState) {
game_context->data->remotePlayer = remoteState; game_context->data->remotePlayer = remoteState;
game_context->data->remoteMoveTick = furi_get_tick(); game_context->data->remoteMoveTick = furi_get_tick();
} }
@ -883,20 +1009,16 @@ int32_t rock_paper_scissors_app(void* p) {
uint8_t beaconCounter = 0; uint8_t beaconCounter = 0;
bool processing = true; bool processing = true;
do { do {
if (furi_message_queue_get(game_context->queue, &event, FuriWaitForever) == FuriStatusOk) { if(furi_message_queue_get(game_context->queue, &event, FuriWaitForever) == FuriStatusOk) {
switch (event.type) { switch(event.type) {
case GameEventTypeKey: case GameEventTypeKey:
if((event.input.type == InputTypeShort) && if((event.input.type == InputTypeShort) && (event.input.key == InputKeyBack)) {
(event.input.key == InputKeyBack)) {
processing = false; processing = false;
} else if (event.input.type == InputTypeShort) { } else if(event.input.type == InputTypeShort) {
unsigned int joinGameNumber; unsigned int joinGameNumber;
GameEvent newEvent = { GameEvent newEvent = {
.type = GameEventLocalMove, .type = GameEventLocalMove, .tick = furi_get_tick(), .move = MoveUnknown};
.tick = furi_get_tick(), switch(event.input.key) {
.move = MoveUnknown
};
switch (event.input.key) {
case InputKeyOk: case InputKeyOk:
newEvent.move = MoveCount; newEvent.move = MoveCount;
break; break;
@ -913,7 +1035,8 @@ int32_t rock_paper_scissors_app(void* p) {
case InputKeyLeft: case InputKeyLeft:
// Temporary: For now, we send "Join" when left button clicked. // Temporary: For now, we send "Join" when left button clicked.
joinGameNumber = game_context->data->gameNumber; joinGameNumber = game_context->data->gameNumber;
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) { if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) ==
FuriStatusOk) {
rps_broadcast_join(game_context, joinGameNumber); rps_broadcast_join(game_context, joinGameNumber);
rps_state_machine_remote_joined(game_context); rps_state_machine_remote_joined(game_context);
furi_mutex_release(game_context->mutex); furi_mutex_release(game_context->mutex);
@ -927,17 +1050,20 @@ int32_t rock_paper_scissors_app(void* p) {
break; break;
} }
if (newEvent.move != MoveUnknown) { if(newEvent.move != MoveUnknown) {
furi_message_queue_put(game_context->queue, &newEvent, FuriWaitForever); furi_message_queue_put(game_context->queue, &newEvent, FuriWaitForever);
} }
} }
break; break;
case GameEventPlaySong:
play_song(game_context->data->localPlayer);
break;
case GameEventDataDetected: case GameEventDataDetected:
rps_receive_data(game_context, event.tick); rps_receive_data(game_context, event.tick);
break; break;
case GameEventTypeTimer: case GameEventTypeTimer:
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) { if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
if (StateLookingForPlayer == game_context->data->localPlayer && if(StateLookingForPlayer == game_context->data->localPlayer &&
++beaconCounter >= BEACON_DURATION) { ++beaconCounter >= BEACON_DURATION) {
rps_broadcast_beacon(game_context); rps_broadcast_beacon(game_context);
beaconCounter = 0; beaconCounter = 0;
@ -953,10 +1079,11 @@ int32_t rock_paper_scissors_app(void* p) {
break; break;
case GameEventRemoteJoined: case GameEventRemoteJoined:
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) { if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
if (event.gameNumber == game_context->data->gameNumber) { if(event.gameNumber == game_context->data->gameNumber) {
rps_state_machine_remote_joined(game_context); rps_state_machine_remote_joined(game_context);
} else { } else {
FURI_LOG_T(TAG, "Remote joining another Flipper on game %03u.", event.gameNumber); FURI_LOG_T(
TAG, "Remote joining another Flipper on game %03u.", event.gameNumber);
} }
furi_mutex_release(game_context->mutex); furi_mutex_release(game_context->mutex);
} else { } else {
@ -985,7 +1112,7 @@ int32_t rock_paper_scissors_app(void* p) {
} }
// If message contains a sender name furi_string, free it. // If message contains a sender name furi_string, free it.
if (event.senderName) { if(event.senderName) {
furi_string_free(event.senderName); furi_string_free(event.senderName);
} }
@ -996,7 +1123,7 @@ int32_t rock_paper_scissors_app(void* p) {
FURI_LOG_E(TAG, "Issue encountered reading from queue. Exiting application."); FURI_LOG_E(TAG, "Issue encountered reading from queue. Exiting application.");
processing = false; processing = false;
} }
} while (processing); } while(processing);
// Free resources // Free resources
furi_timer_free(timer); furi_timer_free(timer);