Initial version of rock paper scissors.
This commit is contained in:
parent
4a63e040e8
commit
ca5515c8b9
82
subghz/plugins/rock_paper_scissors/README.md
Normal file
82
subghz/plugins/rock_paper_scissors/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Rock Papper Scissors
|
||||
## Status
|
||||
This is current a work in progress.
|
||||
- Most of the game logic is complete (two flippers should be able to play each other.)
|
||||
|
||||
Remaining work:
|
||||
- UI to show images instead of text.
|
||||
- Tone when button pressed.
|
||||
- Show games found & let user pick the game to join.
|
||||
- Log joined game into SD card.
|
||||
- Log game results into SD card.
|
||||
- Allow viewing past games/scores.
|
||||
- Config - Allow changing game number.
|
||||
- Config - Allow changing frequency.
|
||||
- Config - Allow changing message on join.
|
||||
|
||||
## 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.
|
||||
|
||||
## Installation Directions
|
||||
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.
|
||||
- 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.
|
||||
- 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"?
|
||||
|
||||
## 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.
|
||||
|
||||
- Press the OK button on the flipper to pull up the main menu.
|
||||
- Choose "Applications" from the menu.
|
||||
- Choose "Games" from the sub-menu.
|
||||
- Choose "Rock Paper Scissors"
|
||||
|
||||
- Do the same steps on your second Flipper.
|
||||
|
||||
- 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 "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!
|
||||
- Rules:
|
||||
- Rock beats Scissors.
|
||||
- Paper beats Rock.
|
||||
- Scissors beat Paper.
|
||||
- Two identical items tie.
|
||||
|
||||
- Press the BACK button to exit.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
- 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
|
||||
```
|
||||
|
||||
- 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
|
||||
```
|
||||
|
||||
- What I typically do is:
|
||||
- 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.)
|
||||
|
||||
|
||||
|
||||
## Details about the project files
|
||||
- application.fam
|
||||
- specifies the name of our application.
|
||||
- specifies the entry point for our application.
|
||||
- specifies we use the GUI.
|
||||
- specifies our icon is the rock_paper_scissors.png file.
|
||||
- specifies our application can be found in the "Misc" category.
|
||||
|
||||
- rock_paper_scissors.png
|
||||
- The icon for our application that shows up in the "Misc" folder.
|
||||
|
||||
- rock_paper_scissors.c
|
||||
- This is the game applcation.
|
10
subghz/plugins/rock_paper_scissors/application.fam
Normal file
10
subghz/plugins/rock_paper_scissors/application.fam
Normal file
@ -0,0 +1,10 @@
|
||||
App(
|
||||
appid="Rock_Paper_Scissors",
|
||||
name="Rock Paper Scissors",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="rock_paper_scissors_app",
|
||||
requires=["gui", "subghz"],
|
||||
stack_size=2 * 1024,
|
||||
fap_icon="rock_paper_scissors.png",
|
||||
fap_category="Games",
|
||||
)
|
855
subghz/plugins/rock_paper_scissors/rock_paper_scissors.c
Normal file
855
subghz/plugins/rock_paper_scissors/rock_paper_scissors.c
Normal file
@ -0,0 +1,855 @@
|
||||
/*
|
||||
@CodeAllNight
|
||||
https://github.com/jamisonderek/flipper-zero-tutorials
|
||||
|
||||
This is a two person game of Rock, Paper, Scissors. It uses the subghz_tx_rx worker library to
|
||||
send and receive messages.
|
||||
|
||||
Features:
|
||||
OK - 1
|
||||
OK - 2
|
||||
UP - Rock
|
||||
Right - Paper
|
||||
Down - Scissors
|
||||
|
||||
*/
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <gui/gui.h>
|
||||
#include <locale/locale.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.
|
||||
#define RPS_GAME_NAME "RPS:"
|
||||
#define TAG "rock_paper_scissors_app"
|
||||
|
||||
// Name for "N", followed by your name without any spaces.
|
||||
#define CONTACT_INFO "NYourNameHere"
|
||||
|
||||
// The message max length should be no larger than a value around 60 to 64.
|
||||
#define MESSAGE_MAX_LEN 60
|
||||
|
||||
// How often to send a beacon.
|
||||
#define BEACON_DURATION 3
|
||||
|
||||
// The major version must be a single character (it can be anything - like '1' or 'A' or 'a').
|
||||
#define MAJOR_VERSION 'A'
|
||||
|
||||
// The various moves a player can make.
|
||||
// Some moves may be invalid depending on the current game state.
|
||||
typedef enum {
|
||||
MoveUnknown = '-',
|
||||
MoveCount = 'C',
|
||||
MoveCount1 = '1',
|
||||
MoveCount2 = '2',
|
||||
MoveRock = 'R',
|
||||
MovePaper = 'P',
|
||||
MoveScissors = 'S',
|
||||
} Move;
|
||||
|
||||
// The various states a player in the game can be in.
|
||||
typedef enum {
|
||||
StateLookingForPlayer = '*',
|
||||
StateReady = 'G',
|
||||
StateCount1 = MoveCount1, // 1
|
||||
StateCount2 = MoveCount2, // 2
|
||||
StatePaper = MovePaper, // P
|
||||
StateRock = MoveRock, // R
|
||||
StateScissors = MoveScissors, // S
|
||||
StateLost = 'L',
|
||||
StateTie = 'T',
|
||||
StateWon = 'W',
|
||||
StateErrorRemoteTimeout = '7', // Joined but didn't make any moves.
|
||||
StateErrorRemoteFast = '8', // Remote user sent moves after than local user.
|
||||
StateErrorLocalFast = '9', // Local user sent moves after than remote user.
|
||||
StateError = 'E',
|
||||
} GameState;
|
||||
|
||||
// When an RF message is sent, it includes a purpose so the receiving application
|
||||
// can decide if it should process the message.
|
||||
typedef enum {
|
||||
GameRfPurposeBeacon = 'B', // Beacon.
|
||||
GameRfPurposeJoin = 'J', // Join a game.
|
||||
GameRfPurposeMove = 'M', // Player move.
|
||||
} GameRfPurpose;
|
||||
|
||||
// Messages in our event queue are one of the following types.
|
||||
typedef enum {
|
||||
GameEventTypeTimer,
|
||||
GameEventTypeKey,
|
||||
GameEventDataDetected,
|
||||
GameEventRemoteBeacon,
|
||||
GameEventRemoteJoined,
|
||||
GameEventLocalMove,
|
||||
GameEventRemoteMove,
|
||||
GameEventSendMove,
|
||||
} GameEventType;
|
||||
|
||||
// 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 {
|
||||
GameEventType type; // The reason for this event.
|
||||
InputEvent input; // Key-press input events.
|
||||
Move move; // The move associated with the event.
|
||||
uint32_t tick; // The time the event originated (furi_get_tick()).
|
||||
unsigned int gameNumber; // The game number for the message.
|
||||
FuriString* senderName; // If not null, be sure to release this string.
|
||||
} GameEvent;
|
||||
|
||||
// This is the data for our application.
|
||||
typedef struct {
|
||||
FuriString* buffer;
|
||||
unsigned int gameNumber;
|
||||
GameState localPlayer;
|
||||
GameState remotePlayer;
|
||||
uint32_t localMoveTick; // local & remote need to press buttons near the same time.
|
||||
uint32_t remoteMoveTick;
|
||||
} GameData;
|
||||
|
||||
// This is our application context.
|
||||
typedef struct {
|
||||
FuriMessageQueue* queue; // Message queue (GameEvent items to process).
|
||||
FuriMutex* mutex; // Used to provide thread safe access to data.
|
||||
GameData* data; // Data accessed by multiple threads (acquire the mutex before accessing!)
|
||||
SubGhzTxRxWorker* subghz_txrx;
|
||||
} GameContext;
|
||||
|
||||
//
|
||||
// We register this callback to get invoked whenever new subghz data is received.
|
||||
// Queue a GameEventDataDetected message.
|
||||
// @param ctx pointer to a GameContext
|
||||
static void rps_worker_update_rx_event_callback(void* ctx) {
|
||||
furi_assert(ctx);
|
||||
GameContext* game_context = ctx;
|
||||
GameEvent event = {.type = GameEventDataDetected, .tick = furi_get_tick()};
|
||||
furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
//
|
||||
// We register this callback to get invoked whenever the timer triggers.
|
||||
// Queue a GameEventTypeTimer message.
|
||||
// @param ctx pointer to a GameContext
|
||||
static void rps_timer_callback(void* ctx) {
|
||||
furi_assert(ctx);
|
||||
GameContext* game_context = ctx;
|
||||
GameEvent event = {.type = GameEventTypeTimer};
|
||||
furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
// This gets invoked when we process a GameEventDataDetected event.
|
||||
// Read the message using subghz_tx_rx_worker_read & determine if valid format.
|
||||
// If valid, we queue a message for further processing.
|
||||
// @param game_context pointer to a GameContext
|
||||
// @param time (furi_get_tick) when event was initially made
|
||||
static void rps_receive_data(GameContext* game_context, uint32_t tick) {
|
||||
uint8_t message[MESSAGE_MAX_LEN] = {0};
|
||||
memset(message, 0x00, 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);
|
||||
if (len < (game_name_len + 2)) {
|
||||
FURI_LOG_D(TAG, "Message not long enough. >%s<", message);
|
||||
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"
|
||||
if (strcmp(RPS_GAME_NAME, (const char*)message)) {
|
||||
FURI_LOG_D(TAG, "Got message >%s<", message);
|
||||
|
||||
// The purpose immediately follows the game name.
|
||||
GameRfPurpose purpose = message[game_name_len];
|
||||
// The version follows the purpose.
|
||||
uint8_t version = message[game_name_len+1];
|
||||
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.
|
||||
message[MESSAGE_MAX_LEN - 1] = 0;
|
||||
|
||||
unsigned int gameNumber;
|
||||
char randomInfo[MESSAGE_MAX_LEN];
|
||||
char senderName[9];
|
||||
char tmp;
|
||||
Move move = MoveUnknown;
|
||||
switch (purpose) {
|
||||
case GameRfPurposeMove:
|
||||
// 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) {
|
||||
move = (Move)tmp;
|
||||
// IMPORTANT: The code processing the event needs to furi_string_free the senderName!
|
||||
FuriString* name = furi_string_alloc();
|
||||
furi_string_set(name, senderName);
|
||||
|
||||
GameEvent event = {.type = GameEventRemoteMove, .move = move, .tick = tick, .senderName = name, .gameNumber = gameNumber };
|
||||
furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
|
||||
} else {
|
||||
FURI_LOG_W(TAG, "Failed to parse move message. >%s<", message);
|
||||
}
|
||||
break;
|
||||
|
||||
case GameRfPurposeBeacon:
|
||||
// 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) {
|
||||
// IMPORTANT: The code processing the event needs to furi_string_free the senderName!
|
||||
FuriString* name = furi_string_alloc();
|
||||
furi_string_set(name, senderName);
|
||||
|
||||
GameEvent event = {.type = GameEventRemoteBeacon, .senderName = name, .gameNumber = gameNumber };
|
||||
furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
|
||||
} else {
|
||||
FURI_LOG_W(TAG, "Failed to parse beacon message. >%s<", message);
|
||||
}
|
||||
break;
|
||||
|
||||
case GameRfPurposeJoin:
|
||||
// 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) {
|
||||
FURI_LOG_T(TAG, "Join had randomInfo of >%s<", randomInfo);
|
||||
|
||||
// IMPORTANT: The code processing the event needs to furi_string_free the senderName!
|
||||
FuriString* name = furi_string_alloc();
|
||||
furi_string_set(name, senderName);
|
||||
|
||||
GameEvent event = {.type = GameEventRemoteJoined, .senderName = name, .gameNumber = gameNumber };
|
||||
furi_message_queue_put(game_context->queue, &event, FuriWaitForever);
|
||||
} else {
|
||||
FURI_LOG_W(TAG, "Failed to parse join message. >%s<", message);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (version <= MAJOR_VERSION) {
|
||||
// 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);
|
||||
} else {
|
||||
// The version is newer, so it's not surprising we don't know about the purpose.
|
||||
FURI_LOG_T(TAG, "Message purpose not handled. >%s<", message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_D(TAG, "Message not for our application. >%s<", message);
|
||||
}
|
||||
}
|
||||
|
||||
// This gets invoked when input (button press) is detected.
|
||||
// We queue a GameEventTypeKey message with the input event data.
|
||||
// @param input_event event information, such as key that was pressed.
|
||||
// @param ctx_q message queue.
|
||||
static void rps_input_callback(InputEvent* input_event, void* ctx_q) {
|
||||
furi_assert(ctx_q);
|
||||
FuriMessageQueue* queue = ctx_q;
|
||||
GameEvent event = {.type = GameEventTypeKey, .tick = furi_get_tick(), .input = *input_event};
|
||||
furi_message_queue_put(queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// @param ctx pointer to a GameContext.
|
||||
static void rps_render_callback(Canvas* canvas, void* ctx) {
|
||||
furi_assert(ctx);
|
||||
GameContext* game_context = ctx;
|
||||
|
||||
// Attempt to aquire context, so we can read the data.
|
||||
if(furi_mutex_acquire(game_context->mutex, 200) != FuriStatusOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameData* data = game_context->data;
|
||||
GameState localPlayer = data->localPlayer;
|
||||
GameState remotePlayer = data->remotePlayer;
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
// Temporary - Show game state.
|
||||
furi_string_printf(data->buffer, "State: %c - %c", localPlayer, remotePlayer);
|
||||
canvas_draw_str_aligned(canvas, 5, 8, AlignLeft, AlignTop, furi_string_get_cstr(data->buffer));
|
||||
|
||||
// Temporary - Just show game text.
|
||||
switch (localPlayer) {
|
||||
case StateLookingForPlayer:
|
||||
furi_string_printf(data->buffer, "Waiting for join: %d", data->gameNumber);
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, furi_string_get_cstr(data->buffer));
|
||||
break;
|
||||
|
||||
case StateReady:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Press OK for 1.");
|
||||
break;
|
||||
|
||||
case StateCount1:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Press OK for 2.");
|
||||
break;
|
||||
|
||||
case StateCount2:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Pick U:Rock R:Paper D:Scissors");
|
||||
break;
|
||||
|
||||
case StateRock:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Rock");
|
||||
break;
|
||||
|
||||
case StatePaper:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Paper");
|
||||
break;
|
||||
|
||||
case StateScissors:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Scissors");
|
||||
break;
|
||||
|
||||
case StateWon:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "You won!!!");
|
||||
break;
|
||||
|
||||
case StateTie:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "You tied!");
|
||||
break;
|
||||
|
||||
case StateLost:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "You lost.");
|
||||
break;
|
||||
|
||||
case StateError:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Unknown error");
|
||||
break;
|
||||
|
||||
case StateErrorLocalFast:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Too fast!");
|
||||
break;
|
||||
|
||||
case StateErrorRemoteFast:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Remote too fast!");
|
||||
break;
|
||||
|
||||
case StateErrorRemoteTimeout:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Remote timeout.");
|
||||
break;
|
||||
|
||||
default:
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignTop, "Unexpected.");
|
||||
break;
|
||||
}
|
||||
|
||||
furi_mutex_release(game_context->mutex);
|
||||
}
|
||||
|
||||
// This is a helper method that broadcasts a buffer.
|
||||
// If the message is too large, the message will get truncated.
|
||||
// @param game_context pointer to a GameContext.
|
||||
// @param buffer string to broadcast.
|
||||
static void rps_broadcast(GameContext* game_context, FuriString* buffer) {
|
||||
uint8_t* message = (uint8_t*)furi_string_get_cstr(buffer);
|
||||
FURI_LOG_I(TAG, "Broadcast message >%s<", message);
|
||||
|
||||
// Make sure our message will fit into a packet; if not truncate it.
|
||||
size_t length = strlen((char*)message);
|
||||
if (length>MESSAGE_MAX_LEN) {
|
||||
// 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);
|
||||
|
||||
// Add \r\n(null) to the end of the 0-indexed string.
|
||||
message[MESSAGE_MAX_LEN-1] = 0;
|
||||
message[MESSAGE_MAX_LEN-2] = '\n';
|
||||
message[MESSAGE_MAX_LEN-3] = '\r';
|
||||
length = MESSAGE_MAX_LEN;
|
||||
}
|
||||
|
||||
while(!subghz_tx_rx_worker_write(game_context->subghz_txrx, message, length)) {
|
||||
// Wait a few milliseconds on failure before trying to send again.
|
||||
furi_delay_ms(20);
|
||||
}
|
||||
}
|
||||
|
||||
// Our GameEventSendCounter handler invokes this method.
|
||||
// We broadcast - "RPS:" + move"M" + version"A" + game"###" + move"R" + ":" + "YourFlip" + "\r\n"
|
||||
// @param game_context pointer to a GameContext.
|
||||
// @param moveToSend the move to send to the remote player.
|
||||
static void rps_broadcast_move(GameContext* game_context, Move moveToSend) {
|
||||
GameData* data = game_context->data;
|
||||
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"
|
||||
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);
|
||||
}
|
||||
|
||||
// Our GameEventTypeTimer handler invokes this method.
|
||||
// We broadcast - "RPS:" + beacon"B" + version"A" + game"###" + ":" + "YourFlip" + "\r\n"
|
||||
// @param game_context pointer to a GameContext.
|
||||
static void rps_broadcast_beacon(GameContext* game_context) {
|
||||
GameData* data = game_context->data;
|
||||
FURI_LOG_I(TAG, "Sending beacon");
|
||||
|
||||
// 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());
|
||||
rps_broadcast(game_context, data->buffer);
|
||||
}
|
||||
|
||||
// Temporary - the KeyLeft button handler invokes this method.
|
||||
// We broadcast - "RPS:" + join"J" + version"A" + game"###" + "NYourNameHere" + " :" + "YourFlip" + "\r\n"
|
||||
// @param game_context pointer to a GameContext.
|
||||
// @param gameNumber the game to join (from previous beacon).
|
||||
static void rps_broadcast_join(GameContext* game_context, unsigned int gameNumber) {
|
||||
GameData* data = game_context->data;
|
||||
FURI_LOG_I(TAG, "Joining game %d.", gameNumber);
|
||||
|
||||
// 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());
|
||||
rps_broadcast(game_context, data->buffer);
|
||||
}
|
||||
|
||||
// Calculates the elapsed duration (in ticks) since a previous tick.
|
||||
// @param tick previous tick obtained from furi_get_tick().
|
||||
static uint32_t duration(uint32_t tick) {
|
||||
uint32_t current = furi_get_tick();
|
||||
// Every 55 days the tick could wrap.
|
||||
if (current < tick) {
|
||||
FURI_LOG_T(TAG, "tick count wrapped! current:%ld prev:%ld", current, tick);
|
||||
return current + (UINT32_MAX - tick);
|
||||
}
|
||||
|
||||
return current - tick;
|
||||
}
|
||||
|
||||
// Temporary timings, since I don't have second Flipper & send commands via laptop.
|
||||
#define DURATION_NO_MOVE_DETECTED_ERROR 60000
|
||||
#define DURATION_SHOW_ERROR 15000
|
||||
#define DURATION_SHOW_MOVES 5000
|
||||
#define DURATION_WIN_LOSS_TIE 10000
|
||||
|
||||
// Updates the state machine, if needed.
|
||||
// @param game_context pointer to a GameContext.
|
||||
static void rps_state_machine_update(GameContext* game_context) {
|
||||
GameData* d = game_context->data;
|
||||
FURI_LOG_I(TAG, "Validating game state. local:%c Remote:%c", d->localPlayer, d->remotePlayer);
|
||||
|
||||
// Did player leave after joining?
|
||||
if ((StateReady == d->remotePlayer) &&
|
||||
(duration(d->remoteMoveTick) > DURATION_NO_MOVE_DETECTED_ERROR)) {
|
||||
d->remotePlayer = StateLookingForPlayer;
|
||||
d->remoteMoveTick = furi_get_tick();
|
||||
d->localPlayer = StateErrorRemoteTimeout;
|
||||
d->localMoveTick = furi_get_tick();
|
||||
// Should we tell other player we timed out?
|
||||
FURI_LOG_I(TAG, "Timed out after joining.");
|
||||
return;
|
||||
}
|
||||
|
||||
// TEMP - After Error, we reset back to Looking for player.
|
||||
if ((StateErrorRemoteTimeout == d->localPlayer) &&
|
||||
(duration(d->localMoveTick) > DURATION_SHOW_ERROR)) {
|
||||
d->remotePlayer = StateLookingForPlayer;
|
||||
d->remoteMoveTick = furi_get_tick();
|
||||
d->localPlayer = StateLookingForPlayer;
|
||||
d->localMoveTick = furi_get_tick();
|
||||
FURI_LOG_I(TAG, "Reset from Error to Looking for player.");
|
||||
return;
|
||||
}
|
||||
|
||||
// TEMP - After Win, Loss, Tie - we reset back to Ready.
|
||||
if (((StateWon == d->localPlayer) || (StateLost == d->localPlayer) || (StateTie == d->localPlayer)) &&
|
||||
(duration(d->localMoveTick) > DURATION_WIN_LOSS_TIE)) {
|
||||
d->remotePlayer = StateReady;
|
||||
d->remoteMoveTick = furi_get_tick();
|
||||
d->localPlayer = StateReady;
|
||||
d->localMoveTick = furi_get_tick();
|
||||
// Should we tell other player we are Ready?
|
||||
FURI_LOG_I(TAG, "Ready for next game.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for winner.
|
||||
if ((duration(d->localMoveTick) > DURATION_SHOW_MOVES) &&
|
||||
(duration(d->remoteMoveTick) > DURATION_SHOW_MOVES)) {
|
||||
if ((d->localPlayer == d->remotePlayer) &&
|
||||
((d->localPlayer == StateRock) ||
|
||||
(d->localPlayer == StatePaper) ||
|
||||
(d->localPlayer == StateScissors))) {
|
||||
d->localPlayer = StateTie;
|
||||
d->localMoveTick = furi_get_tick();
|
||||
d->remotePlayer = StateTie;
|
||||
d->remoteMoveTick = furi_get_tick();
|
||||
FURI_LOG_I(TAG, "Tie game.");
|
||||
return;
|
||||
} else if (((d->localPlayer == StateRock) && (d->remotePlayer == StateScissors)) ||
|
||||
((d->localPlayer == StateScissors) && (d->remotePlayer == StatePaper)) ||
|
||||
((d->localPlayer == StatePaper) && (d->remotePlayer == StateRock))) {
|
||||
d->localPlayer = StateWon;
|
||||
d->localMoveTick = furi_get_tick();
|
||||
d->remotePlayer = StateLost;
|
||||
d->remoteMoveTick = furi_get_tick();
|
||||
FURI_LOG_I(TAG, "Local won.");
|
||||
return;
|
||||
} else if (((d->remotePlayer == StateRock) && (d->localPlayer == StateScissors)) ||
|
||||
((d->remotePlayer == StateScissors) && (d->localPlayer == StatePaper)) ||
|
||||
((d->remotePlayer == StatePaper) && (d->localPlayer == StateRock))) {
|
||||
d->remotePlayer = StateWon;
|
||||
d->remoteMoveTick = furi_get_tick();
|
||||
d->localPlayer = StateLost;
|
||||
d->localMoveTick = furi_get_tick();
|
||||
FURI_LOG_I(TAG, "Remote won.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the state machine to reflect that a remote user joined the game.
|
||||
// @param game_context pointer to a GameContext.
|
||||
static void rps_state_machine_remote_joined(GameContext* game_context) {
|
||||
if (StateLookingForPlayer == game_context->data->localPlayer) {
|
||||
FURI_LOG_I(TAG, "Remote player joined our game!");
|
||||
game_context->data->remotePlayer = StateReady;
|
||||
game_context->data->remoteMoveTick = furi_get_tick();
|
||||
game_context->data->localPlayer = StateReady;
|
||||
game_context->data->localMoveTick = furi_get_tick();
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "Remote requested join, but we are state %c!", game_context->data->localPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the state machine to reflect the local user's move.
|
||||
// @param game_context pointer to a GameContext.
|
||||
// @param move local user move.
|
||||
static bool rps_state_machine_local_moved(GameContext* game_context, Move move) {
|
||||
FURI_LOG_I(TAG, "Local move %c.", move);
|
||||
|
||||
Move localMove = MoveUnknown;
|
||||
GameState localState = StateReady;
|
||||
|
||||
if (MoveCount == move && StateReady == game_context->data->localPlayer) {
|
||||
localMove = MoveCount1;
|
||||
localState = StateCount1;
|
||||
} else if (MoveCount == move && StateCount1 == game_context->data->localPlayer) {
|
||||
if ((StateCount1 == game_context->data->remotePlayer) ||
|
||||
(StateCount2 == game_context->data->remotePlayer)) {
|
||||
localMove = MoveCount2;
|
||||
localState = StateCount2;
|
||||
} else {
|
||||
localState = StateErrorLocalFast;
|
||||
FURI_LOG_I(TAG, "Local count sync error. remote is %c.", game_context->data->remotePlayer);
|
||||
}
|
||||
} else if (MoveRock == move && StateCount2 == game_context->data->localPlayer) {
|
||||
if ((StateCount2 == game_context->data->remotePlayer) ||
|
||||
(StateRock == game_context->data->remotePlayer) ||
|
||||
(StatePaper == game_context->data->remotePlayer) ||
|
||||
(StateScissors == game_context->data->remotePlayer)) {
|
||||
localMove = MoveRock;
|
||||
localState = StateRock;
|
||||
} else {
|
||||
localState = StateErrorLocalFast;
|
||||
FURI_LOG_I(TAG, "Local rock sync error. remote is %c.", game_context->data->remotePlayer);
|
||||
}
|
||||
} else if (MovePaper == move && StateCount2 == game_context->data->localPlayer) {
|
||||
if ((StateCount2 == game_context->data->remotePlayer) ||
|
||||
(StateRock == game_context->data->remotePlayer) ||
|
||||
(StatePaper == game_context->data->remotePlayer) ||
|
||||
(StateScissors == game_context->data->remotePlayer)) {
|
||||
localMove = MovePaper;
|
||||
localState = StatePaper;
|
||||
} else {
|
||||
localState = StateErrorLocalFast;
|
||||
FURI_LOG_I(TAG, "Local paper sync error. remote is %c.", game_context->data->remotePlayer);
|
||||
}
|
||||
} else if (MoveScissors == move && StateCount2 == game_context->data->localPlayer) {
|
||||
if ((StateCount2 == game_context->data->remotePlayer) ||
|
||||
(StateRock == game_context->data->remotePlayer) ||
|
||||
(StatePaper == game_context->data->remotePlayer) ||
|
||||
(StateScissors == game_context->data->remotePlayer)) {
|
||||
localMove = MoveScissors;
|
||||
localState = StateScissors;
|
||||
} else {
|
||||
localState = StateErrorLocalFast;
|
||||
FURI_LOG_I(TAG, "Local scissors sync error. remote is %c.", game_context->data->remotePlayer);
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Local move '%c' error. lState=%c. rState=%c.", move, game_context->data->localPlayer, game_context->data->remotePlayer);
|
||||
localState = StateError;
|
||||
}
|
||||
|
||||
if (MoveUnknown != localMove) {
|
||||
rps_broadcast_move(game_context, localMove);
|
||||
}
|
||||
|
||||
if (StateReady != localState) {
|
||||
game_context->data->localPlayer = localState;
|
||||
game_context->data->localMoveTick = furi_get_tick();
|
||||
}
|
||||
|
||||
return StateReady != localState;
|
||||
}
|
||||
|
||||
// Update the state machine to reflect the remote user's move.
|
||||
// @param game_context pointer to a GameContext.
|
||||
// @param move remote user move.
|
||||
static bool rps_state_machine_remote_moved(GameContext* game_context, Move move) {
|
||||
GameState remoteState = StateReady;
|
||||
FURI_LOG_I(TAG, "Remote move %c.", move);
|
||||
|
||||
if (MoveCount1 == move && StateReady == game_context->data->remotePlayer) {
|
||||
remoteState = StateCount1;
|
||||
} else if (MoveCount2 == move && StateCount1 == game_context->data->remotePlayer) {
|
||||
if ((StateCount1 == game_context->data->localPlayer) ||
|
||||
(StateCount2 == game_context->data->localPlayer)) {
|
||||
remoteState = StateCount2;
|
||||
} else {
|
||||
remoteState = StateErrorRemoteFast;
|
||||
FURI_LOG_I(TAG, "Remote count sync error. local is %c.", game_context->data->localPlayer);
|
||||
}
|
||||
} else if (MoveRock == move && StateCount2 == game_context->data->remotePlayer) {
|
||||
if ((StateCount2 == game_context->data->localPlayer) ||
|
||||
(StateRock == game_context->data->localPlayer) ||
|
||||
(StatePaper == game_context->data->localPlayer) ||
|
||||
(StateScissors == game_context->data->localPlayer)) {
|
||||
remoteState = StateRock;
|
||||
} else {
|
||||
remoteState = StateErrorRemoteFast;
|
||||
FURI_LOG_I(TAG, "Remote rock sync error. local is %c.", game_context->data->localPlayer);
|
||||
}
|
||||
} else if (MovePaper == move && StateCount2 == game_context->data->remotePlayer) {
|
||||
if ((StateCount2 == game_context->data->localPlayer) ||
|
||||
(StateRock == game_context->data->localPlayer) ||
|
||||
(StatePaper == game_context->data->localPlayer) ||
|
||||
(StateScissors == game_context->data->localPlayer)) {
|
||||
remoteState = StatePaper;
|
||||
} else {
|
||||
remoteState = StateErrorRemoteFast;
|
||||
FURI_LOG_I(TAG, "Remote paper sync error. local is %c.", game_context->data->localPlayer);
|
||||
}
|
||||
} else if (MoveScissors == move && StateCount2 == game_context->data->remotePlayer) {
|
||||
if ((StateCount2 == game_context->data->localPlayer) ||
|
||||
(StateRock == game_context->data->localPlayer) ||
|
||||
(StatePaper == game_context->data->localPlayer) ||
|
||||
(StateScissors == game_context->data->localPlayer)) {
|
||||
remoteState = StateScissors;
|
||||
} else {
|
||||
remoteState = StateErrorRemoteFast;
|
||||
FURI_LOG_I(TAG, "Remote scissors sync error. local is %c.", game_context->data->localPlayer);
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Remote move '%c' error. lState=%c. rState=%c.", move, game_context->data->localPlayer, game_context->data->remotePlayer);
|
||||
remoteState = StateError;
|
||||
}
|
||||
|
||||
if (StateReady != remoteState) {
|
||||
game_context->data->remotePlayer = remoteState;
|
||||
game_context->data->remoteMoveTick = furi_get_tick();
|
||||
}
|
||||
|
||||
return StateReady != remoteState;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// For this game we hardcode to 433.92MHz.
|
||||
uint32_t frequency = 433920000;
|
||||
|
||||
// TODO: Have an ordered list of frequencies we try, instead of just 1 frequency.
|
||||
|
||||
// Since this game transmits RF, we see if it is allowed.
|
||||
if(!furi_hal_subghz_is_tx_allowed(frequency)) {
|
||||
FURI_LOG_E(TAG, "Transmit on frequency %ld not allowed", frequency);
|
||||
|
||||
// For this game we don't show a friendly error about not being
|
||||
// allowed to broadcast on this frequency. Instead the application
|
||||
// just exits.
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Configure our initial data.
|
||||
GameContext* game_context = malloc(sizeof(GameContext));
|
||||
game_context->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
game_context->data = malloc(sizeof(GameData));
|
||||
game_context->data->buffer = furi_string_alloc();
|
||||
game_context->data->gameNumber = 42;
|
||||
game_context->data->localMoveTick = 0;
|
||||
game_context->data->localPlayer = StateLookingForPlayer;
|
||||
game_context->data->remoteMoveTick = 0;
|
||||
game_context->data->remotePlayer = StateLookingForPlayer;
|
||||
|
||||
// Queue for events
|
||||
game_context->queue = furi_message_queue_alloc(8, sizeof(GameEvent));
|
||||
|
||||
// Subghz worker.
|
||||
game_context->subghz_txrx = subghz_tx_rx_worker_alloc();
|
||||
|
||||
// Try to start the TX/RX on the frequency and configure our callback
|
||||
// whenever new data is received.
|
||||
if(subghz_tx_rx_worker_start(game_context->subghz_txrx, frequency)) {
|
||||
subghz_tx_rx_worker_set_callback_have_read(
|
||||
game_context->subghz_txrx, rps_worker_update_rx_event_callback, game_context);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to start subghz_tx_rx_worker.");
|
||||
|
||||
// For this game we don't show a friendly error about not being
|
||||
// allowed to broadcast on this frequency. Instead the application
|
||||
// just exits.
|
||||
if(subghz_tx_rx_worker_is_running(game_context->subghz_txrx)) {
|
||||
subghz_tx_rx_worker_stop(game_context->subghz_txrx);
|
||||
}
|
||||
subghz_tx_rx_worker_free(game_context->subghz_txrx);
|
||||
furi_message_queue_free(game_context->queue);
|
||||
furi_mutex_free(game_context->mutex);
|
||||
furi_string_free(game_context->data->buffer);
|
||||
free(game_context->data);
|
||||
free(game_context);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// All the subghz CLI apps disable charging; so our game does it too.
|
||||
furi_hal_power_suppress_charge_enter();
|
||||
|
||||
// Set ViewPort callbacks
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(view_port, rps_render_callback, game_context);
|
||||
view_port_input_callback_set(view_port, rps_input_callback, game_context->queue);
|
||||
|
||||
// Open GUI and register view_port
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
// Run routine once a second.
|
||||
FuriTimer* timer = furi_timer_alloc(rps_timer_callback, FuriTimerTypePeriodic, game_context);
|
||||
furi_timer_start(timer, 1000);
|
||||
|
||||
// Main loop
|
||||
GameEvent event;
|
||||
uint8_t beaconCounter = 0;
|
||||
bool processing = true;
|
||||
do {
|
||||
if (furi_message_queue_get(game_context->queue, &event, FuriWaitForever) == FuriStatusOk) {
|
||||
switch (event.type) {
|
||||
case GameEventTypeKey:
|
||||
if((event.input.type == InputTypeShort) &&
|
||||
(event.input.key == InputKeyBack)) {
|
||||
processing = false;
|
||||
} else if (event.input.type == InputTypeShort) {
|
||||
unsigned int joinGameNumber;
|
||||
GameEvent newEvent = {
|
||||
.type = GameEventLocalMove,
|
||||
.tick = furi_get_tick(),
|
||||
.move = MoveUnknown
|
||||
};
|
||||
switch (event.input.key) {
|
||||
case InputKeyOk:
|
||||
newEvent.move = MoveCount;
|
||||
break;
|
||||
case InputKeyUp:
|
||||
newEvent.move = MoveRock;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
newEvent.move = MovePaper;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
newEvent.move = MoveScissors;
|
||||
break;
|
||||
|
||||
case InputKeyLeft:
|
||||
// Temporary: For now, we send "Join" when left button clicked.
|
||||
joinGameNumber = game_context->data->gameNumber;
|
||||
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
|
||||
rps_broadcast_join(game_context, joinGameNumber);
|
||||
furi_mutex_release(game_context->mutex);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to aquire mutex.");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
FURI_LOG_T(TAG, "No support for key %d", event.input.key);
|
||||
break;
|
||||
}
|
||||
|
||||
if (newEvent.move != MoveUnknown) {
|
||||
furi_message_queue_put(game_context->queue, &newEvent, FuriWaitForever);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GameEventDataDetected:
|
||||
rps_receive_data(game_context, event.tick);
|
||||
break;
|
||||
case GameEventTypeTimer:
|
||||
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
|
||||
if (StateLookingForPlayer == game_context->data->localPlayer &&
|
||||
++beaconCounter >= BEACON_DURATION) {
|
||||
rps_broadcast_beacon(game_context);
|
||||
beaconCounter = 0;
|
||||
}
|
||||
rps_state_machine_update(game_context);
|
||||
furi_mutex_release(game_context->mutex);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to aquire mutex.");
|
||||
}
|
||||
break;
|
||||
case GameEventRemoteBeacon:
|
||||
FURI_LOG_I(TAG, "Remote beacon detected. game number %03u", event.gameNumber);
|
||||
break;
|
||||
case GameEventRemoteJoined:
|
||||
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
|
||||
if (event.gameNumber == game_context->data->gameNumber) {
|
||||
rps_state_machine_remote_joined(game_context);
|
||||
} else {
|
||||
FURI_LOG_T(TAG, "Remote joining another Flipper on game %03u.", event.gameNumber);
|
||||
}
|
||||
furi_mutex_release(game_context->mutex);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to aquire mutex.");
|
||||
}
|
||||
break;
|
||||
case GameEventLocalMove:
|
||||
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
|
||||
rps_state_machine_local_moved(game_context, event.move);
|
||||
furi_mutex_release(game_context->mutex);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to aquire mutex for local move.");
|
||||
}
|
||||
break;
|
||||
case GameEventRemoteMove:
|
||||
if(furi_mutex_acquire(game_context->mutex, FuriWaitForever) == FuriStatusOk) {
|
||||
rps_state_machine_remote_moved(game_context, event.move);
|
||||
furi_mutex_release(game_context->mutex);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to aquire mutex for remote move.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
FURI_LOG_E(TAG, "Queue had unknown message type: %u", event.type);
|
||||
break;
|
||||
}
|
||||
|
||||
// If message contains a sender name furi_string, free it.
|
||||
if (event.senderName) {
|
||||
furi_string_free(event.senderName);
|
||||
}
|
||||
|
||||
// Send signal to update the screen (callback will get invoked at some point later.)
|
||||
view_port_update(view_port);
|
||||
} else {
|
||||
// We had an issue getting message from the queue, so exit application.
|
||||
FURI_LOG_E(TAG, "Issue encountered reading from queue. Exiting application.");
|
||||
processing = false;
|
||||
}
|
||||
} while (processing);
|
||||
|
||||
// Free resources
|
||||
furi_timer_free(timer);
|
||||
if(subghz_tx_rx_worker_is_running(game_context->subghz_txrx)) {
|
||||
subghz_tx_rx_worker_stop(game_context->subghz_txrx);
|
||||
}
|
||||
subghz_tx_rx_worker_free(game_context->subghz_txrx);
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
view_port_free(view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_message_queue_free(game_context->queue);
|
||||
furi_mutex_free(game_context->mutex);
|
||||
furi_string_free(game_context->data->buffer);
|
||||
free(game_context->data);
|
||||
free(game_context);
|
||||
|
||||
// Reenable charging.
|
||||
furi_hal_power_suppress_charge_exit();
|
||||
|
||||
return 0;
|
||||
}
|
BIN
subghz/plugins/rock_paper_scissors/rock_paper_scissors.png
Normal file
BIN
subghz/plugins/rock_paper_scissors/rock_paper_scissors.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
Loading…
x
Reference in New Issue
Block a user