Просмотр исходного кода

initial release of Simon Says game

SimplyMinimal 3 лет назад
Родитель
Сommit
d0a8c25923
1 измененных файлов с 514 добавлено и 99 удалено
  1. 514 99
      simon_says.c

+ 514 - 99
simon_says.c

@@ -13,58 +13,134 @@
 /* generated by fbt from .png files in images folder */
 #include <simon_says_icons.h>
 
-#define WIDTH 64
-#define HEIGHT 144
+#define TAG "Simon" // Used for logging
+#define DEBUG_MSG 1
+#define SCREEN_XRES 128
+#define SCREEN_YRES 64
+#define BOARD_X 72
+#define BOARD_Y 8
+#define GAME_START_LIVES 3
+#define SAVING_DIRECTORY "/ext/apps/Games"
+#define SAVING_FILENAME SAVING_DIRECTORY "/game_simon_says.save"
+const int brush_size = 2;
 
-enum {
-    up,
-    down,
-    left,
-    right,
-} direction;
+/* ============================ 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 {
-    bool isPlayerTurn;
-} SimonSaysData;
+    /* 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
+
+    /* 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
+    uint32_t last_shape_displayed_ms; // This is used to time the interval between displaying shapes
+    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 shapeConsumed; // Tracks whether shape has been checked or not
+    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
+
+    enum shape_names selected;
+    enum shape_names board_state; // TODO: This may be redundant by selected
+
+    uint32_t last_button_press_tick;
+    NotificationApp* notification;
+} SimonData;
+
+/* ============================== Sequences ============================== */
+
+const NotificationSequence sequence_wrong_move = {
+    &message_red_255,
 
-// Sequence to indicate that this is the beginning of a turn
-const NotificationSequence sequence_begin_turn = {
-    &message_display_backlight_on,
     &message_vibro_on,
-    &message_note_g5,
-    &message_delay_50,
-    &message_note_c6,
-    &message_delay_50,
-    &message_note_e5,
+    // &message_note_g5, // Play sound but currently disabled
+    &message_delay_25,
+    // &message_note_e5,
     &message_vibro_off,
     &message_sound_off,
     NULL,
 };
 
-// sequence to indicate that we've reached the end of a turn
-const NotificationSequence sequence_end_turn = {
-    &message_display_backlight_on,
-    &message_red_0,
+const NotificationSequence sequence_player_submit_move = {
     &message_vibro_on,
-    &message_note_g5,
-    &message_delay_50,
-    &message_note_e5,
-    &message_delay_50,
+    // &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,
-    &message_do_not_reset,
     NULL,
 };
 
-// Indicate that drawing is enabled.
-const NotificationSequence sequence_player_turn_enabled = {
+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 drawing is disabled.
-const NotificationSequence sequence_player_turn_disabled = {
+// Indicate that it's the Player's turn
+const NotificationSequence sequence_player_is_playing = {
     &message_red_0,
     &message_do_not_reset,
     NULL,
@@ -79,119 +155,458 @@ const NotificationSequence sequence_cleanup = {
     NULL,
 };
 
-void simon_says_draw_callback(Canvas* canvas, void* ctx) {
-    const SimonSaysData* game_state = acquire_mutex((ValueMutex*)ctx, 25);
-    UNUSED(ctx);
+/* ============================ 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);
+}
 
+/* Main Render Function */
+void simon_draw_callback(Canvas* canvas, void* ctx) {
+    const SimonData* simon_state = acquire_mutex((ValueMutex*)ctx, 25);
+    if(simon_state == NULL) {
+        if(DEBUG_MSG) FURI_LOG_E(TAG, "[simon_draw_callback] Null simon state");
+        return;
+    }
+
+    UNUSED(ctx);
     canvas_clear(canvas);
 
-    canvas_draw_icon(canvas, 2, 2, &I_board); // Draw board
+    // ######################### 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);
 
-    //release the mutex
-    release_mutex((ValueMutex*)ctx, game_state);
-}
+        // Draw Lives
+        draw_remaining_lives(canvas, simon_state);
 
-void game_tick(void* ctx) {
-    SimonSaysData* game_state = acquire_mutex((ValueMutex*)ctx, 25);
-    UNUSED(ctx);
+        // Draw Simon Pose
+        if(simon_state->activePlayer == player) {
+            // Player's turn
+            canvas_draw_icon(canvas, 0, 4, &I_DolphinWait_61x59);
+        } 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
+        } else {
+            switch(simon_state->selectedShape) {
+            case up:
+                canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_up); // Draw Down
+                break;
+            case down:
+                canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_down); // Draw Down
+                break;
+            case left:
+                canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_left); // Draw Left
+                break;
+            case right:
+                canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_right); // Draw Right
+                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) {
+        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
-    release_mutex((ValueMutex*)ctx, game_state);
+    release_mutex((ValueMutex*)ctx, simon_state);
 }
 
-void simon_says_input_callback(InputEvent* input_event, void* ctx) {
+/* ======================== 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);
 }
 
-int32_t simon_says_app(void* p) {
+/* ======================== Simon Game Engine ======================== */
+
+bool load_game(SimonData* app) {
+    //TODO
+    UNUSED(app);
+    return true;
+    // Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    // File* file = storage_file_alloc(storage);
+    // uint16_t bytes_readed = 0;
+    // if(storage_file_open(file, SAVING_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
+    //     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) {
+    //TODO
+    UNUSED(app);
+
+    // Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    // if(storage_common_stat(storage, SAVING_DIRECTORY, NULL) == FSE_NOT_EXIST) {
+    //     if(!storage_simply_mkdir(storage, SAVING_DIRECTORY)) {
+    //         return;
+    //     }
+    // }
+
+    // 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 = 1000;
+    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->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;
+                if(simon_state->currentScore > simon_state->highScore) {
+                    simon_state->highScore = simon_state->currentScore;
+                    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));
 
-    SimonSaysData* simon_says_state = malloc(sizeof(SimonSaysData));
-    ValueMutex simon_value_mutex;
-    if(!init_mutex(&simon_value_mutex, simon_says_state, sizeof(SimonSaysData))) {
-        FURI_LOG_E("simon_says", "cannot create mutex\r\n");
-        free(simon_says_state);
+    SimonData* simon_state = malloc(sizeof(SimonData));
+    ValueMutex simon_state_value_mutex;
+    if(!init_mutex(&simon_state_value_mutex, simon_state, sizeof(SimonData))) {
+        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_says_draw_callback, &simon_value_mutex);
-    view_port_input_callback_set(view_port, simon_says_input_callback, event_queue);
+    view_port_draw_callback_set(view_port, simon_draw_callback, &simon_state_value_mutex);
+    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 event;
+    InputEvent input;
 
-    /* Create a timer. We do data analysis in the callback. */
-    FuriTimer* timer = furi_timer_alloc(game_tick, FuriTimerTypePeriodic, simon_says_state);
-    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 10);
+    // Show Main Menu Screen
+    simon_state->gameState = mainMenu;
 
-    while(furi_message_queue_get(event_queue, &event, FuriWaitForever) == FuriStatusOk) {
-        //break out of the loop if the back key is pressed
-        if(event.key == InputKeyBack && event.type == InputTypeLong) {
-            break;
-        }
+    while(true) {
+        if(furi_message_queue_get(event_queue, &input, 500) == 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) {
+                break;
+            }
 
-        // Placholder button states
-        if(event.key == InputKeyBack && event.type == InputTypeLong) {
-            view_port_update(view_port);
-        }
+            if(input.type == InputTypeLong) {
+                // Do Nothing. Ignore long press events
+            }
 
-        // Keep LED on while drawing
-        if(simon_says_state->isPlayerTurn) {
-            notification_message(notification, &sequence_player_turn_enabled);
+            //@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:
+                        break;
+                    }
+                } else {
+                    FURI_LOG_D(TAG, "Input type is not short");
+                    simon_state->set_board_neutral = true;
+                }
+            }
         } else {
-            notification_message(notification, &sequence_player_turn_disabled);
+            FURI_LOG_E(TAG, "cannot get message from queue\r\n");
         }
 
-        // Placholder button states
-        if(event.key == InputKeyOk && event.type == InputTypeShort) {
-            // Do Nothing
-        }
+        // @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;
+        // }
 
-        // Placholder button states
-        if(event.key == InputKeyOk && event.type == InputTypeLong) {
-            // notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_begin_turn);
-            notification_message(notification, &sequence_begin_turn);
-            view_port_update(view_port);
-        }
+        game_tick(simon_state);
 
-        // Placholder button states
-        if(event.type == InputTypeShort || event.type == InputTypeRepeat ||
-           event.type == InputTypeLong) {
-            switch(event.key) {
-            case InputKeyUp:
-                break;
-            case InputKeyDown:
-                break;
-            case InputKeyLeft:
-                break;
-            case InputKeyRight:
-                break;
-            default:
-                break;
-            }
-
-            view_port_update(view_port);
-        }
+        view_port_update(view_port);
     }
 
-    furi_timer_free(timer);
     notification_message(notification, &sequence_cleanup);
     gui_remove_view_port(gui, view_port);
     view_port_free(view_port);
     furi_message_queue_free(event_queue);
-    free(simon_says_state);
+    free(simon_state);
     furi_record_close(RECORD_NOTIFICATION);
     furi_record_close(RECORD_GUI);
 
     return 0;
-}
+}