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
|
||||
|
||||
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.)
|
||||
- UI to show Flipper images instead of just debug text.
|
||||
- Vibro on button press (valid move).
|
||||
- "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):
|
||||
|
||||
- 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 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.
|
||||
- Refactor the code, so it has less duplication.
|
||||
- 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.
|
||||
|
||||
- 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:
|
||||
|
||||
@ -68,7 +79,9 @@ These directions assume you are starting at the flipper desktop. If not, please
|
||||
- Scissors beat Paper.
|
||||
- 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
|
||||
|
||||
@ -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 we use the GUI.
|
||||
- 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
|
||||
|
||||
- The icon for our application that shows up in the "Misc" folder.
|
||||
- The icon for our application.
|
||||
|
||||
- rock_paper_scissors.c
|
||||
- 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