Added main menu + join feature.
This commit is contained in:
parent
272a7bc594
commit
74f931e1f6
@ -6,21 +6,24 @@ This is game of Rock, Paper, Scissors. In version 1, we use the subghz radio to
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
This is currently a work in progress...
|
This is currently a work in progress!
|
||||||
|
|
||||||
|
Completed work:
|
||||||
|
|
||||||
- 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).
|
- Vibro on button press (valid move).
|
||||||
- "Song" when win/loss/draw.
|
- "Song" when win/loss/draw.
|
||||||
|
- Show games found & let user pick the game to join.
|
||||||
|
- Config - Allow changing game number.
|
||||||
|
- Config - Allow changing frequency.
|
||||||
|
|
||||||
Remaining work (for subghz version):
|
Remaining work (for subghz version):
|
||||||
|
|
||||||
- Show games found & let user pick the game to join.
|
- Receiving a Join game should do an ACK (to cause game on joiner to start).
|
||||||
- Log joined game into SD card.
|
- Log joined game into SD card.
|
||||||
- Log game results into SD card.
|
- Log game results into SD card.
|
||||||
- Allow viewing past games/scores.
|
- Allow viewing past games/scores.
|
||||||
- Config - Allow changing game number.
|
|
||||||
- Config - Allow changing frequency.
|
|
||||||
- Config - Allow changing message on join.
|
- Config - Allow changing message on join.
|
||||||
- Refactor the code, so it has less duplication.
|
- Refactor the code, so it has less duplication.
|
||||||
- Write tutorial.
|
- Write tutorial.
|
||||||
@ -55,7 +58,15 @@ These directions assume you are starting at the flipper desktop. If not, please
|
|||||||
|
|
||||||
- Do the same steps on your second Flipper.
|
- Do the same steps on your second Flipper.
|
||||||
|
|
||||||
- On one of the flippers press Left arrow to join the game.
|
- On Flipper 1, choose "Host game".
|
||||||
|
- select a valid frequency. (like "433.92" in US region)
|
||||||
|
- choose a game number.
|
||||||
|
- click OK button to start game.
|
||||||
|
|
||||||
|
- On Flipper 2, choose "Join game".
|
||||||
|
- select the same valid frequency as Flipper 1.
|
||||||
|
- "game none" should change to show the game number from Flipper 1 & its name.
|
||||||
|
-click OK button to join game.
|
||||||
|
|
||||||
- Once two players are joined:
|
- Once two players are joined:
|
||||||
|
|
||||||
@ -68,7 +79,9 @@ These directions assume you are starting at the flipper desktop. If not, please
|
|||||||
- Scissors beat Paper.
|
- Scissors beat Paper.
|
||||||
- Two identical items tie.
|
- Two identical items tie.
|
||||||
|
|
||||||
- Press the BACK button to exit.
|
- Short press the BACK button for the main menu.
|
||||||
|
|
||||||
|
- Long press the BACK button to exit.
|
||||||
|
|
||||||
## HackRF One
|
## HackRF One
|
||||||
|
|
||||||
@ -98,11 +111,11 @@ sudo hackrf_transfer -r flipper-chat.rf -f 433920000 -s 8000000 -x 47
|
|||||||
- specifies the entry point for our application.
|
- specifies the entry point for our application.
|
||||||
- specifies we use the GUI.
|
- specifies we use the GUI.
|
||||||
- specifies our icon is the rock_paper_scissors.png file.
|
- specifies our icon is the rock_paper_scissors.png file.
|
||||||
- specifies our application can be found in the "Misc" category.
|
- specifies our application can be found in the "Game" 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.
|
||||||
|
|
||||||
- rock_paper_scissors.c
|
- rock_paper_scissors.c
|
||||||
- This is the game applcation.
|
- This is the game applcation.
|
||||||
|
File diff suppressed because it is too large
Load Diff
364
subghz/plugins/rock_paper_scissors/rock_paper_scissors.h
Normal file
364
subghz/plugins/rock_paper_scissors/rock_paper_scissors.h
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "rock_paper_scissors_icons.h"
|
||||||
|
#include <furi.h>
|
||||||
|
#include <furi_hal.h>
|
||||||
|
#include <furi_hal_resources.h>
|
||||||
|
#include <gui/gui.h>
|
||||||
|
#include <gui/icon.h>
|
||||||
|
#include <locale/locale.h>
|
||||||
|
|
||||||
|
#include <notification/notification.h>
|
||||||
|
#include <notification/notification_messages.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'
|
||||||
|
|
||||||
|
// Temporary timings, since I don't have second Flipper & send commands via laptop.
|
||||||
|
#define DURATION_NO_MOVE_DETECTED_ERROR 60000
|
||||||
|
#define DURATION_SHOW_ERROR 3000
|
||||||
|
#define DURATION_SHOW_MOVES 500
|
||||||
|
#define DURATION_WIN_LOSS_TIE 10000
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
DolphinLocalLooking = 0,
|
||||||
|
DolphinLocalReady,
|
||||||
|
DolphinLocalCount,
|
||||||
|
DolphinLocalRock,
|
||||||
|
DolphinLocalPaper,
|
||||||
|
DolphinLocalScissors,
|
||||||
|
DolphinRemoteReady,
|
||||||
|
DolphinRemoteCount,
|
||||||
|
DolphinRemoteRock,
|
||||||
|
DolphinRemotePaper,
|
||||||
|
DolphinRemoteScissors,
|
||||||
|
} DolphinImageIndex;
|
||||||
|
|
||||||
|
static const uint32_t frequency_list[] = {
|
||||||
|
/* 300 - 348 */
|
||||||
|
300000000,
|
||||||
|
303875000,
|
||||||
|
304250000,
|
||||||
|
310000000,
|
||||||
|
315000000,
|
||||||
|
318000000,
|
||||||
|
|
||||||
|
/* 387 - 464 */
|
||||||
|
390000000,
|
||||||
|
418000000,
|
||||||
|
433075000,
|
||||||
|
433420000,
|
||||||
|
433920000,
|
||||||
|
434420000,
|
||||||
|
434775000,
|
||||||
|
438900000,
|
||||||
|
|
||||||
|
/* 779 - 928 */
|
||||||
|
868350000,
|
||||||
|
915000000,
|
||||||
|
925000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon* images[] = {
|
||||||
|
&I_Local_Looking,
|
||||||
|
&I_Local_Ready,
|
||||||
|
&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.
|
||||||
|
// 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 {
|
||||||
|
// ScreenHostGame states:
|
||||||
|
StateHostingSetGameNumber,
|
||||||
|
StateHostingSetFrequency,
|
||||||
|
StateHostingBadFrequency,
|
||||||
|
StateHostingLookingForPlayer = '*',
|
||||||
|
// ScreenJoinGame states:
|
||||||
|
StateJoiningSetGameNumber,
|
||||||
|
StateJoiningSetFrequency,
|
||||||
|
StateJoiningBadFrequency,
|
||||||
|
// ScreenPlayingGame states:
|
||||||
|
StateReady = 'G',
|
||||||
|
StateCount1 = MoveCount1, // 1
|
||||||
|
StateCount2 = MoveCount2, // 2
|
||||||
|
StatePaper = MovePaper, // P
|
||||||
|
StateRock = MoveRock, // R
|
||||||
|
StateScissors = MoveScissors, // S
|
||||||
|
StateLostRock = 'L',
|
||||||
|
StateLostPaper = 'l',
|
||||||
|
StateLostScissors = '-',
|
||||||
|
StateTieRock = 'T',
|
||||||
|
StateTiePaper = 't',
|
||||||
|
StateTieScissors = 'x',
|
||||||
|
StateWonRock = 'W',
|
||||||
|
StateWonPaper = 'w',
|
||||||
|
StateWonScissors = '+',
|
||||||
|
// ScreenError states:
|
||||||
|
StateError = 'E',
|
||||||
|
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.
|
||||||
|
// ScrenMainMenu states:
|
||||||
|
StateUnknown = '?',
|
||||||
|
StateMainMenuHost,
|
||||||
|
StateMainMenuJoin,
|
||||||
|
StateMainMenuPastGames,
|
||||||
|
StateMainMenuMessage,
|
||||||
|
} GameState;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ScreenMainMenu,
|
||||||
|
ScreenHostGame,
|
||||||
|
ScreenPlayingGame,
|
||||||
|
ScreenError,
|
||||||
|
ScreenJoinGame,
|
||||||
|
ScreenEditMessage,
|
||||||
|
ScreenPastGames,
|
||||||
|
} ScreenState;
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
GameEventPlaySong,
|
||||||
|
} 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()).
|
||||||
|
uint16_t game_number; // The game number for the message.
|
||||||
|
FuriString* sender_name; // If not null, be sure to release this string.
|
||||||
|
} GameEvent;
|
||||||
|
|
||||||
|
typedef struct GameInfo {
|
||||||
|
uint16_t game_number;
|
||||||
|
FuriString* sender_name;
|
||||||
|
struct GameInfo* next_game;
|
||||||
|
} GameInfo;
|
||||||
|
|
||||||
|
// This is the data for our application.
|
||||||
|
typedef struct {
|
||||||
|
FuriString* buffer;
|
||||||
|
ScreenState screen_state;
|
||||||
|
uint16_t game_number;
|
||||||
|
uint8_t frequency_index;
|
||||||
|
GameState local_player;
|
||||||
|
GameState remote_player;
|
||||||
|
uint32_t local_move_tick; // local & remote need to press buttons near the same time.
|
||||||
|
uint32_t remote_move_tick;
|
||||||
|
struct GameInfo* remote_games;
|
||||||
|
struct GameInfo* remote_selected_game;
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
// Checks if game state is winner.
|
||||||
|
// @param state GameState to check.
|
||||||
|
// @returns true if game state is a winner.
|
||||||
|
static bool isWin(GameState 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);
|
||||||
|
|
||||||
|
// Checks if game state is tie.
|
||||||
|
// @param state GameState to check.
|
||||||
|
// @returns true if game state is a tie.
|
||||||
|
static bool isTie(GameState state);
|
||||||
|
|
||||||
|
// Checks if game state is result (win/loss/tie).
|
||||||
|
// @param state GameState to check.
|
||||||
|
// @returns true if game state is a win, loss or tie.
|
||||||
|
static bool isResult(GameState state);
|
||||||
|
|
||||||
|
// Checks if game state is final move (rock/paper/scissors).
|
||||||
|
// @param state GameState to check.
|
||||||
|
// @returns true if game state is a rock, paper, scissors.
|
||||||
|
static bool isFinalMove(GameState state);
|
||||||
|
|
||||||
|
static bool isError(GameState state);
|
||||||
|
|
||||||
|
// When user makes a move, we briefly pulse the vibro motor.
|
||||||
|
static void single_vibro();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Play a song
|
||||||
|
static void play_song(GameState state);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Render UI when we are hosting the game.
|
||||||
|
// @param canvas rendering surface of the Flipper Zero.
|
||||||
|
// @param ctx pointer to a GameContext.
|
||||||
|
static void rps_render_host_game(Canvas* canvas, void* ctx);
|
||||||
|
|
||||||
|
// Render UI when we are joining a game.
|
||||||
|
// @param canvas rendering surface of the Flipper Zero.
|
||||||
|
// @param ctx pointer to a GameContext.
|
||||||
|
static void rps_render_join_game(Canvas* canvas, void* ctx);
|
||||||
|
|
||||||
|
// Render UI when we are playing the game.
|
||||||
|
// @param canvas rendering surface of the Flipper Zero.
|
||||||
|
// @param ctx pointer to a GameContext.
|
||||||
|
static void rps_render_playing_game(Canvas* canvas, void* ctx);
|
||||||
|
|
||||||
|
// Render UI when we encounter an error in the game.
|
||||||
|
// @param canvas rendering surface of the Flipper Zero.
|
||||||
|
// @param ctx pointer to a GameContext.
|
||||||
|
static void rps_render_error(Canvas* canvas, void* ctx);
|
||||||
|
|
||||||
|
// Render UI when we are hosting the game.
|
||||||
|
// @param canvas rendering surface of the Flipper Zero.
|
||||||
|
// @param ctx pointer to a GameContext.
|
||||||
|
static void rps_render_main_menu(Canvas* canvas, void* ctx);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Updates the state machine, if needed.
|
||||||
|
// @param game_context pointer to a GameContext.
|
||||||
|
static void rps_state_machine_update(GameContext* game_context);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
static bool update_frequency(GameContext* game_context);
|
||||||
|
|
||||||
|
static void remote_games_clear(GameContext* game_context);
|
||||||
|
static GameInfo* remote_games_current(GameContext* game_context);
|
||||||
|
static GameInfo* remote_games_find(GameContext* game_context, uint16_t game_number);
|
||||||
|
static bool remote_games_has_next(GameContext* game_context);
|
||||||
|
static bool remote_games_has_previous(GameContext* game_context);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// This is the entry point for our application, which should match the application.fam file.
|
||||||
|
int32_t rock_paper_scissors_app(void* p);
|
Loading…
Reference in New Issue
Block a user