| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664 |
- #include <furi.h>
- #include <furi_hal.h>
- #include <storage/storage.h>
- #include <gui/gui.h>
- #include <gui/elements.h>
- #include <gui/icon.h>
- #include <input/input.h>
- #include <notification/notification.h>
- #include <notification/notification_messages.h>
- #include <stdbool.h> // Header-file for boolean data-type.
- #include <stdio.h>
- #include <string.h>
- /* generated by fbt from .png files in images folder */
- #include <simon_says_icons.h>
- #include <assets_icons.h>
- #define TAG "Simon" // Used for logging
- #define DEBUG_MSG 1
- #define SCREEN_XRES 128
- #define SCREEN_YRES 64
- #define BOARD_X 72 // Used for board placement
- #define BOARD_Y 8
- #define GAME_START_LIVES 3
- #define SAVING_DIRECTORY STORAGE_APP_DATA_PATH_PREFIX
- #define SAVING_FILENAME SAVING_DIRECTORY "/game_simon_says.save"
- // Define Notes
- // Shamelessly stolen from Ocarina application
- // https://github.com/invalidna-me/flipperzero-ocarina
- #define NOTE_UP 587.33f
- #define NOTE_LEFT 493.88f
- #define NOTE_RIGHT 440.00f
- #define NOTE_DOWN 349.23
- #define NOTE_OK 293.66f
- /* ============================ Data structures ============================= */
- typedef enum game_state { preloading, mainMenu, inGame, gameOver, gameVictory } game_state;
- typedef enum difficulty_mode { normal, hard } difficulty_mode;
- typedef enum shape_names { up, down, left, right, number_of_shapes } Direction;
- typedef enum currently_playing { simon, player } currently_playing;
- typedef struct {
- /* Game state. */
- enum game_state gameState; // This is the current game state
- bool gameover; /* if true then switch to the game over state */
- bool is_wrong_direction; /* Is the last direction wrong? */
- enum currently_playing activePlayer; // This is used to track who is playing at the moment
- uint32_t lives; /* Number of lives in the current game. */
- enum difficulty_mode difficultyMode; // This is the difficulty mode for the current game
- bool sound_enabled; // This is the sound enabled flag for the current game
- float volume; // This is the volume for the current game
- /* Handle Score */
- int currentScore; // This is the score for the current
- int highScore; /* Highscore. Shown on Game Over Screen */
- bool is_new_highscore; /* Is the last score a new highscore? */
- /* Handle Shape Display */
- uint32_t numberOfMillisecondsBeforeShapeDisappears; // This defines the speed of the game
- enum shape_names simonMoves[1000]; // Store the sequence of shapes that Simon plays
- enum shape_names selectedShape; // This is used to track the shape that the player has selected
- bool set_board_neutral; // This is used to track if the board should be neutral or not
- int moveIndex; // This is used to track the current move in the sequence
- uint32_t last_button_press_tick;
- NotificationApp* notification;
- FuriMutex* mutex;
- } SimonData;
- /* ============================== Sequences ============================== */
- const NotificationSequence sequence_wrong_move = {
- &message_red_255,
- &message_vibro_on,
- // &message_note_g5, // Play sound but currently disabled
- &message_delay_25,
- // &message_note_e5,
- &message_vibro_off,
- &message_sound_off,
- NULL,
- };
- const NotificationSequence sequence_player_submit_move = {
- &message_vibro_on,
- // &message_note_g5, // Play sound but currently disabled. Need On/Off menu setting
- &message_delay_10,
- &message_delay_1,
- &message_delay_1,
- &message_delay_1,
- &message_delay_1,
- &message_delay_1,
- // &message_note_e5,
- &message_vibro_off,
- &message_sound_off,
- NULL,
- };
- const NotificationSequence sequence_up = {
- // &message_vibro_on,
- &message_note_g4,
- &message_delay_100,
- // &message_vibro_off,
- &message_sound_off,
- NULL,
- };
- const NotificationSequence sequence_down = {
- // &message_vibro_on,
- &message_note_c3,
- &message_delay_100,
- // &message_vibro_off,
- &message_sound_off,
- NULL,
- };
- const NotificationSequence sequence_left = {
- // &message_vibro_on,
- &message_note_e3,
- &message_delay_100,
- // &message_vibro_off,
- &message_sound_off,
- NULL,
- };
- const NotificationSequence sequence_right = {
- // &message_vibro_on,
- &message_note_g3,
- &message_delay_100,
- // &message_vibro_off,
- &message_sound_off,
- NULL,
- };
- // Indicate that it's Simon's turn
- const NotificationSequence sequence_simon_is_playing = {
- &message_red_255,
- &message_do_not_reset,
- NULL,
- };
- // Indicate that it's the Player's turn
- const NotificationSequence sequence_player_is_playing = {
- &message_red_0,
- &message_do_not_reset,
- NULL,
- };
- const NotificationSequence sequence_cleanup = {
- &message_red_0,
- &message_green_0,
- &message_blue_0,
- &message_sound_off,
- &message_vibro_off,
- NULL,
- };
- /* ============================ 2D drawing ================================== */
- /* Display remaining lives in the center of the board */
- void draw_remaining_lives(Canvas* canvas, const SimonData* simon_state) {
- // Convert score to string
- // int length = snprintf(NULL, 0, "%lu", simon_state->lives);
- // char* str_lives_remaining = malloc(length + 1);
- // snprintf(str_lives_remaining, length + 1, "%lu", simon_state->lives);
- // TODO: Make it a Simon Says icon on top right
- canvas_set_color(canvas, ColorBlack);
- canvas_set_font(canvas, FontSecondary);
- int x = SCREEN_XRES - 6;
- int lives = simon_state->lives;
- while(lives--) {
- canvas_draw_str(canvas, x, 8, "*");
- x -= 7;
- }
- }
- void draw_current_score(Canvas* canvas, const SimonData* simon_data) {
- /* Draw Game Score. */
- canvas_set_color(canvas, ColorXOR);
- canvas_set_font(canvas, FontSecondary);
- char str_score[32];
- snprintf(str_score, sizeof(str_score), "%i", simon_data->currentScore);
- canvas_draw_str_aligned(canvas, SCREEN_XRES / 2 + 4, 2, AlignCenter, AlignTop, str_score);
- }
- void play_sound_up(const SimonData* app) {
- if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
- furi_hal_speaker_start(NOTE_UP, app->volume);
- }
- }
- void play_sound_down(const SimonData* app) {
- if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
- furi_hal_speaker_start(NOTE_DOWN, app->volume);
- }
- }
- void play_sound_left(const SimonData* app) {
- if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
- furi_hal_speaker_start(NOTE_LEFT, app->volume);
- }
- }
- void play_sound_right(const SimonData* app) {
- if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
- furi_hal_speaker_start(NOTE_RIGHT, app->volume);
- }
- }
- void stop_sound() {
- if(furi_hal_speaker_is_mine()) {
- furi_hal_speaker_stop();
- furi_hal_speaker_release();
- }
- }
- /* Main Render Function */
- void simon_draw_callback(Canvas* canvas, void* ctx) {
- furi_assert(ctx);
- const SimonData* simon_state = ctx;
- furi_mutex_acquire(simon_state->mutex, FuriWaitForever);
- canvas_clear(canvas);
- // ######################### Main Menu #########################
- // Show Main Menu
- if(simon_state->gameState == mainMenu) {
- // Draw border frame
- canvas_draw_frame(canvas, 1, 1, SCREEN_XRES - 1, SCREEN_YRES - 1); // Border
- // Draw Simon text banner
- canvas_set_font(canvas, FontSecondary);
- canvas_set_color(canvas, ColorBlack);
- canvas_draw_str_aligned(
- canvas,
- SCREEN_XRES / 2,
- SCREEN_YRES / 2 - 4,
- AlignCenter,
- AlignCenter,
- "Welcome to Simon Says");
- // Display Press OK to start below title
- canvas_set_color(canvas, ColorXOR);
- canvas_draw_str_aligned(
- canvas,
- SCREEN_XRES / 2,
- SCREEN_YRES / 2 + 10,
- AlignCenter,
- AlignCenter,
- "Press OK to start");
- }
- // ######################### in Game #########################
- //@todo Render Callback
- // We're in an active game
- if(simon_state->gameState == inGame) {
- // Draw Current Score
- draw_current_score(canvas, simon_state);
- // Draw Lives
- draw_remaining_lives(canvas, simon_state);
- // Draw Simon Pose
- if(simon_state->activePlayer == player) {
- // Player's turn
- canvas_draw_icon(canvas, 0, 9, &I_DolphinWait_59x54);
- } else {
- // Simon's turn
- canvas_draw_icon(canvas, 0, 4, &I_DolphinTalking_59x63);
- }
- if(simon_state->set_board_neutral) {
- // Draw Neutral Board
- canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_board); // Draw Board
- // Stop Sound TODO: Move this to a better place
- //@todo Sound
- stop_sound();
- } else {
- switch(simon_state->selectedShape) {
- case up:
- canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_up); // Draw Up
- play_sound_up(simon_state);
- break;
- case down:
- canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_down); // Draw Down
- play_sound_down(simon_state);
- break;
- case left:
- canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_left); // Draw Left
- play_sound_left(simon_state);
- break;
- case right:
- canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_right); // Draw Right
- play_sound_right(simon_state);
- break;
- default:
- if(DEBUG_MSG)
- FURI_LOG_E(
- TAG, "Invalid shape: %d", simon_state->simonMoves[simon_state->moveIndex]);
- break;
- }
- }
- }
- // ######################### Game Over #########################
- if(simon_state->gameState == gameOver) {
- stop_sound(); //TODO: Make a game over sequence
- canvas_set_color(canvas, ColorXOR);
- canvas_set_font(canvas, FontPrimary);
- // TODO: if new highscore, display blinking "New High Score"
- // Display High Score Text
- if(simon_state->is_new_highscore) {
- canvas_draw_str_aligned(
- canvas, SCREEN_XRES / 2, 6, AlignCenter, AlignTop, "New High Score!");
- } else {
- canvas_draw_str_aligned(
- canvas, SCREEN_XRES / 2, 6, AlignCenter, AlignTop, "High Score");
- }
- // Convert highscore to string
- int length = snprintf(NULL, 0, "%i", simon_state->highScore);
- char* str_high_score = malloc(length + 1);
- snprintf(str_high_score, length + 1, "%i", simon_state->highScore);
- // Display High Score
- canvas_draw_str_aligned(
- canvas, SCREEN_XRES / 2, 22, AlignCenter, AlignCenter, str_high_score);
- free(str_high_score);
- // Display Game Over
- canvas_draw_str_aligned(
- canvas, SCREEN_XRES / 2, SCREEN_YRES / 2 + 2, AlignCenter, AlignCenter, "GAME OVER");
- // Display Press OK to restart below title
- canvas_set_font(canvas, FontSecondary);
- canvas_draw_str_aligned(
- canvas,
- SCREEN_XRES / 2,
- SCREEN_YRES / 2 + 15,
- AlignCenter,
- AlignCenter,
- "Press OK to restart");
- }
- // ######################### Victory #########################
- //Player Beat Simon beyond limit! A word record holder here!
- //TODO
- //release the mutex
- furi_mutex_release(simon_state->mutex);
- }
- /* ======================== Input Handling ============================== */
- void simon_input_callback(InputEvent* input_event, void* ctx) {
- furi_assert(ctx);
- FuriMessageQueue* event_queue = ctx;
- furi_message_queue_put(event_queue, input_event, FuriWaitForever);
- }
- /* ======================== Simon Game Engine ======================== */
- bool load_game(SimonData* app) {
- Storage* storage = furi_record_open(RECORD_STORAGE);
- storage_common_migrate(storage, EXT_PATH("apps/Games/game_simon_says.save"), SAVING_FILENAME);
- File* file = storage_file_alloc(storage);
- uint16_t bytes_readed = 0;
- if(storage_file_open(file, SAVING_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
- if(storage_file_size(file) > sizeof(SimonData)) {
- storage_simply_remove(storage, SAVING_FILENAME);
- FURI_LOG_E(
- TAG, "Error: file is larger than the data structure! The file has been deleted.");
- } else {
- bytes_readed = storage_file_read(file, app, sizeof(SimonData));
- }
- storage_file_close(file);
- storage_file_free(file);
- }
- furi_record_close(RECORD_STORAGE);
- return bytes_readed == sizeof(SimonData);
- }
- void save_game(SimonData* app) {
- Storage* storage = furi_record_open(RECORD_STORAGE);
- File* file = storage_file_alloc(storage);
- if(storage_file_open(file, SAVING_FILENAME, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
- storage_file_write(file, app, sizeof(SimonData));
- }
- storage_file_close(file);
- storage_file_free(file);
- furi_record_close(RECORD_STORAGE);
- }
- int getRandomIntInRange(int lower, int upper) {
- return (rand() % (upper - lower + 1)) + lower;
- }
- void play_sound_sequence_correct() {
- notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_success);
- }
- void play_sound_wrong_move() {
- //TODO: play wrong sound: Try sequence_audiovisual_alert
- notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_error);
- }
- /* Restart game and give player a chance to try again on same sequence */
- // @todo restartGame
- void resetGame(SimonData* app) {
- app->moveIndex = 0;
- app->numberOfMillisecondsBeforeShapeDisappears = 500;
- app->activePlayer = simon;
- app->is_wrong_direction = false;
- app->last_button_press_tick = 0;
- app->set_board_neutral = true;
- app->activePlayer = simon;
- }
- /* Set gameover state */
- void game_over(SimonData* app) {
- if(app->is_new_highscore) save_game(app); // Save highscore but only on change
- app->gameover = true;
- app->lives = GAME_START_LIVES; // Show 3 lives in game over screen to match new game start
- app->gameState = gameOver;
- }
- /* Called after gameover to restart the game. This function
- * also calls restart_game(). */
- void restart_game_after_gameover(SimonData* app) {
- app->volume = 1.0f; //TODO: make this a setting
- app->gameState = inGame;
- app->gameover = false;
- app->currentScore = 0;
- app->is_new_highscore = false;
- app->lives = GAME_START_LIVES;
- app->simonMoves[0] = rand() % number_of_shapes;
- resetGame(app);
- }
- void addNewSimonMove(int addAtIndex, SimonData* app) {
- app->simonMoves[addAtIndex] = getRandomIntInRange(0, 3);
- }
- void startNewRound(SimonData* app) {
- addNewSimonMove(app->currentScore, app);
- app->moveIndex = 0;
- app->activePlayer = simon;
- }
- void onPlayerAnsweredCorrect(SimonData* app) {
- app->moveIndex++;
- }
- void onPlayerAnsweredWrong(SimonData* app) {
- if(app->lives > 0) {
- app->lives--;
- // Play the wrong sound
- if(app->sound_enabled) {
- play_sound_wrong_move();
- }
- resetGame(app);
- } else {
- // The player has no lives left
- // Game over
- game_over(app);
- //TODO: Play unique game over sound
- }
- }
- bool isRoundComplete(SimonData* app) {
- return app->moveIndex == app->currentScore;
- }
- enum shape_names getCurrentSimonMove(SimonData* app) {
- return app->simonMoves[app->moveIndex];
- }
- void onPlayerSelectedShapeCallback(enum shape_names shape, SimonData* app) {
- if(shape == getCurrentSimonMove(app)) {
- onPlayerAnsweredCorrect(app);
- } else {
- onPlayerAnsweredWrong(app);
- }
- }
- //@todo gametick
- void game_tick(SimonData* simon_state) {
- if(simon_state->gameState == inGame) {
- if(simon_state->activePlayer == simon) {
- // ############### Simon Turn ###############
- notification_message(simon_state->notification, &sequence_simon_is_playing);
- //@todo Gameplay
- if(simon_state->set_board_neutral) {
- if(simon_state->moveIndex < simon_state->currentScore) {
- simon_state->selectedShape = getCurrentSimonMove(simon_state);
- simon_state->set_board_neutral = false;
- simon_state->moveIndex++;
- } else {
- simon_state->activePlayer = player;
- simon_state->set_board_neutral = true;
- simon_state->moveIndex = 0;
- }
- } else {
- simon_state->set_board_neutral = true;
- }
- } else {
- // ############### Player Turn ###############
- notification_message(simon_state->notification, &sequence_player_is_playing);
- // It's Player's Turn
- if(isRoundComplete(simon_state)) {
- simon_state->activePlayer = simon;
- simon_state->currentScore++;
- // app->numberOfMillisecondsBeforeShapeDisappears -= 50;
- //TODO: Hacky way of handling highscore by subtracting 1 to account for the first move
- if(simon_state->currentScore - 1 > simon_state->highScore) {
- simon_state->highScore = simon_state->currentScore - 1;
- simon_state->is_new_highscore = true;
- }
- if(simon_state->sound_enabled) {
- play_sound_sequence_correct();
- }
- startNewRound(simon_state);
- }
- }
- }
- }
- /* ======================== Main Entry Point ============================== */
- int32_t simon_says_app_entry(void* p) {
- UNUSED(p);
- FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
- SimonData* simon_state = malloc(sizeof(SimonData));
- simon_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
- if(!simon_state->mutex) {
- FURI_LOG_E(TAG, "cannot create mutex\r\n");
- free(simon_state);
- return -1;
- }
- // Configure view port
- ViewPort* view_port = view_port_alloc();
- view_port_draw_callback_set(view_port, simon_draw_callback, simon_state);
- view_port_input_callback_set(view_port, simon_input_callback, event_queue);
- // Register view port in GUI
- Gui* gui = furi_record_open(RECORD_GUI);
- gui_add_view_port(gui, view_port, GuiLayerFullscreen);
- NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
- simon_state->notification = notification;
- InputEvent input;
- // Show Main Menu Screen
- //load_game(simon_state);
- restart_game_after_gameover(simon_state);
- simon_state->gameState = mainMenu;
- while(true) {
- game_tick(simon_state);
- FuriStatus q_status = furi_message_queue_get(
- event_queue, &input, simon_state->numberOfMillisecondsBeforeShapeDisappears);
- furi_mutex_acquire(simon_state->mutex, FuriWaitForever);
- if(q_status == FuriStatusOk) {
- //FURI_LOG_D(TAG, "Got input event: %d", input.key);
- //break out of the loop if the back key is pressed
- if(input.key == InputKeyBack && input.type == InputTypeLong) {
- // Save high score before quitting
- //if(simon_state->is_new_highscore) {
- // save_game(simon_state);
- //}
- break;
- }
- //@todo Set Game States
- if(input.key == InputKeyOk && simon_state->gameState != inGame) {
- restart_game_after_gameover(simon_state);
- // Set Simon Board state
- startNewRound(simon_state);
- view_port_update(view_port);
- }
- // Keep LED on if it is Simon's turn
- if(simon_state->activePlayer == player) {
- notification_message(notification, &sequence_player_is_playing);
- if(input.type == InputTypePress) {
- simon_state->set_board_neutral = false;
- switch(input.key) {
- case InputKeyUp:
- simon_state->selectedShape = up;
- onPlayerSelectedShapeCallback(up, simon_state);
- break;
- case InputKeyDown:
- simon_state->selectedShape = down;
- onPlayerSelectedShapeCallback(down, simon_state);
- break;
- case InputKeyLeft:
- simon_state->selectedShape = left;
- onPlayerSelectedShapeCallback(left, simon_state);
- break;
- case InputKeyRight:
- simon_state->selectedShape = right;
- onPlayerSelectedShapeCallback(right, simon_state);
- break;
- default:
- simon_state->set_board_neutral = true;
- break;
- }
- } else {
- //FURI_LOG_D(TAG, "Input type is not short");
- simon_state->set_board_neutral = true;
- }
- }
- }
- // @todo Animation Loop for debug
- // if(simon_state->gameState == inGame && simon_state->activePlayer == simon) {
- // simon_state->currentScore++;
- // simon_state->set_board_neutral = !simon_state->set_board_neutral;
- // }
- furi_mutex_release(simon_state->mutex);
- view_port_update(view_port);
- }
- stop_sound();
- notification_message(notification, &sequence_cleanup);
- gui_remove_view_port(gui, view_port);
- view_port_free(view_port);
- furi_message_queue_free(event_queue);
- furi_mutex_free(simon_state->mutex);
- furi_record_close(RECORD_NOTIFICATION);
- furi_record_close(RECORD_GUI);
- free(simon_state);
- return 0;
- }
|