simon_says.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. #include <furi.h>
  2. #include <furi_hal.h>
  3. #include <gui/gui.h>
  4. #include <gui/elements.h>
  5. #include <gui/icon.h>
  6. #include <input/input.h>
  7. #include <notification/notification.h>
  8. #include <notification/notification_messages.h>
  9. #include <stdbool.h> // Header-file for boolean data-type.
  10. #include <stdio.h>
  11. #include <string.h>
  12. /* generated by fbt from .png files in images folder */
  13. #include <simon_says_icons.h>
  14. #define TAG "Simon" // Used for logging
  15. #define DEBUG_MSG 1
  16. #define SCREEN_XRES 128
  17. #define SCREEN_YRES 64
  18. #define BOARD_X 72
  19. #define BOARD_Y 8
  20. #define GAME_START_LIVES 3
  21. #define SAVING_DIRECTORY "/ext/apps/Games"
  22. #define SAVING_FILENAME SAVING_DIRECTORY "/game_simon_says.save"
  23. const int brush_size = 2;
  24. /* ============================ Data structures ============================= */
  25. typedef enum game_state { preloading, mainMenu, inGame, gameOver, gameVictory } game_state;
  26. typedef enum difficulty_mode { normal, hard } difficulty_mode;
  27. typedef enum shape_names { up, down, left, right, number_of_shapes } Direction;
  28. typedef enum currently_playing { simon, player } currently_playing;
  29. typedef struct {
  30. /* Game state. */
  31. enum game_state gameState; // This is the current game state
  32. bool gameover; /* if true then switch to the game over state */
  33. bool is_wrong_direction; /* Is the last direction wrong? */
  34. enum currently_playing activePlayer; // This is used to track who is playing at the moment
  35. uint32_t lives; /* Number of lives in the current game. */
  36. enum difficulty_mode difficultyMode; // This is the difficulty mode for the current game
  37. bool sound_enabled; // This is the sound enabled flag for the current game
  38. /* Handle Score */
  39. int currentScore; // This is the score for the current
  40. int highScore; /* Highscore. Shown on Game Over Screen */
  41. bool is_new_highscore; /* Is the last score a new highscore? */
  42. /* Handle Shape Display */
  43. uint32_t numberOfMillisecondsBeforeShapeDisappears; // This defines the speed of the game
  44. uint32_t last_shape_displayed_ms; // This is used to time the interval between displaying shapes
  45. enum shape_names simonMoves[1000]; // Store the sequence of shapes that Simon plays
  46. enum shape_names selectedShape; // This is used to track the shape that the player has selected
  47. bool shapeConsumed; // Tracks whether shape has been checked or not
  48. bool set_board_neutral; // This is used to track if the board should be neutral or not
  49. int moveIndex; // This is used to track the current move in the sequence
  50. enum shape_names selected;
  51. enum shape_names board_state; // TODO: This may be redundant by selected
  52. uint32_t last_button_press_tick;
  53. NotificationApp* notification;
  54. } SimonData;
  55. /* ============================== Sequences ============================== */
  56. const NotificationSequence sequence_wrong_move = {
  57. &message_red_255,
  58. &message_vibro_on,
  59. // &message_note_g5, // Play sound but currently disabled
  60. &message_delay_25,
  61. // &message_note_e5,
  62. &message_vibro_off,
  63. &message_sound_off,
  64. NULL,
  65. };
  66. const NotificationSequence sequence_player_submit_move = {
  67. &message_vibro_on,
  68. // &message_note_g5, // Play sound but currently disabled. Need On/Off menu setting
  69. &message_delay_10,
  70. &message_delay_1,
  71. &message_delay_1,
  72. &message_delay_1,
  73. &message_delay_1,
  74. &message_delay_1,
  75. // &message_note_e5,
  76. &message_vibro_off,
  77. &message_sound_off,
  78. NULL,
  79. };
  80. const NotificationSequence sequence_up = {
  81. // &message_vibro_on,
  82. &message_note_g4,
  83. &message_delay_100,
  84. // &message_vibro_off,
  85. &message_sound_off,
  86. NULL,
  87. };
  88. const NotificationSequence sequence_down = {
  89. // &message_vibro_on,
  90. &message_note_c3,
  91. &message_delay_100,
  92. // &message_vibro_off,
  93. &message_sound_off,
  94. NULL,
  95. };
  96. const NotificationSequence sequence_left = {
  97. // &message_vibro_on,
  98. &message_note_e3,
  99. &message_delay_100,
  100. // &message_vibro_off,
  101. &message_sound_off,
  102. NULL,
  103. };
  104. const NotificationSequence sequence_right = {
  105. // &message_vibro_on,
  106. &message_note_g3,
  107. &message_delay_100,
  108. // &message_vibro_off,
  109. &message_sound_off,
  110. NULL,
  111. };
  112. // Indicate that it's Simon's turn
  113. const NotificationSequence sequence_simon_is_playing = {
  114. &message_red_255,
  115. &message_do_not_reset,
  116. NULL,
  117. };
  118. // Indicate that it's the Player's turn
  119. const NotificationSequence sequence_player_is_playing = {
  120. &message_red_0,
  121. &message_do_not_reset,
  122. NULL,
  123. };
  124. const NotificationSequence sequence_cleanup = {
  125. &message_red_0,
  126. &message_green_0,
  127. &message_blue_0,
  128. &message_sound_off,
  129. &message_vibro_off,
  130. NULL,
  131. };
  132. /* ============================ 2D drawing ================================== */
  133. /* Display remaining lives in the center of the board */
  134. void draw_remaining_lives(Canvas* canvas, const SimonData* simon_state) {
  135. // Convert score to string
  136. // int length = snprintf(NULL, 0, "%lu", simon_state->lives);
  137. // char* str_lives_remaining = malloc(length + 1);
  138. // snprintf(str_lives_remaining, length + 1, "%lu", simon_state->lives);
  139. // TODO: Make it a Simon Says icon on top right
  140. canvas_set_color(canvas, ColorBlack);
  141. canvas_set_font(canvas, FontSecondary);
  142. int x = SCREEN_XRES - 6;
  143. int lives = simon_state->lives;
  144. while(lives--) {
  145. canvas_draw_str(canvas, x, 8, "*");
  146. x -= 7;
  147. }
  148. }
  149. void draw_current_score(Canvas* canvas, const SimonData* simon_data) {
  150. /* Draw Game Score. */
  151. canvas_set_color(canvas, ColorXOR);
  152. canvas_set_font(canvas, FontSecondary);
  153. char str_score[32];
  154. snprintf(str_score, sizeof(str_score), "%i", simon_data->currentScore);
  155. canvas_draw_str_aligned(canvas, SCREEN_XRES / 2 + 4, 2, AlignCenter, AlignTop, str_score);
  156. }
  157. /* Main Render Function */
  158. void simon_draw_callback(Canvas* canvas, void* ctx) {
  159. const SimonData* simon_state = acquire_mutex((ValueMutex*)ctx, 25);
  160. if(simon_state == NULL) {
  161. if(DEBUG_MSG) FURI_LOG_E(TAG, "[simon_draw_callback] Null simon state");
  162. return;
  163. }
  164. UNUSED(ctx);
  165. canvas_clear(canvas);
  166. // ######################### Main Menu #########################
  167. // Show Main Menu
  168. if(simon_state->gameState == mainMenu) {
  169. // Draw border frame
  170. canvas_draw_frame(canvas, 1, 1, SCREEN_XRES - 1, SCREEN_YRES - 1); // Border
  171. // Draw Simon text banner
  172. canvas_set_font(canvas, FontSecondary);
  173. canvas_set_color(canvas, ColorBlack);
  174. canvas_draw_str_aligned(
  175. canvas,
  176. SCREEN_XRES / 2,
  177. SCREEN_YRES / 2 - 4,
  178. AlignCenter,
  179. AlignCenter,
  180. "Welcome to Simon Says");
  181. // Display Press OK to start below title
  182. canvas_set_color(canvas, ColorXOR);
  183. canvas_draw_str_aligned(
  184. canvas,
  185. SCREEN_XRES / 2,
  186. SCREEN_YRES / 2 + 10,
  187. AlignCenter,
  188. AlignCenter,
  189. "Press OK to start");
  190. }
  191. // ######################### in Game #########################
  192. //@todo Render Callback
  193. // We're in an active game
  194. if(simon_state->gameState == inGame) {
  195. // Draw Current Score
  196. draw_current_score(canvas, simon_state);
  197. // Draw Lives
  198. draw_remaining_lives(canvas, simon_state);
  199. // Draw Simon Pose
  200. if(simon_state->activePlayer == player) {
  201. // Player's turn
  202. canvas_draw_icon(canvas, 0, 4, &I_DolphinWait_61x59);
  203. } else {
  204. // Simon's turn
  205. canvas_draw_icon(canvas, 0, 4, &I_DolphinTalking_59x63);
  206. }
  207. if(simon_state->set_board_neutral) {
  208. // Draw Neutral Board
  209. canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_board); // Draw Board
  210. } else {
  211. switch(simon_state->selectedShape) {
  212. case up:
  213. canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_up); // Draw Down
  214. break;
  215. case down:
  216. canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_down); // Draw Down
  217. break;
  218. case left:
  219. canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_left); // Draw Left
  220. break;
  221. case right:
  222. canvas_draw_icon(canvas, BOARD_X, BOARD_Y, &I_right); // Draw Right
  223. break;
  224. default:
  225. if(DEBUG_MSG)
  226. FURI_LOG_E(
  227. TAG, "Invalid shape: %d", simon_state->simonMoves[simon_state->moveIndex]);
  228. break;
  229. }
  230. }
  231. }
  232. // ######################### Game Over #########################
  233. if(simon_state->gameState == gameOver) {
  234. canvas_set_color(canvas, ColorXOR);
  235. canvas_set_font(canvas, FontPrimary);
  236. // TODO: if new highscore, display blinking "New High Score"
  237. // Display High Score Text
  238. if(simon_state->is_new_highscore) {
  239. canvas_draw_str_aligned(
  240. canvas, SCREEN_XRES / 2, 6, AlignCenter, AlignTop, "New High Score!");
  241. } else {
  242. canvas_draw_str_aligned(
  243. canvas, SCREEN_XRES / 2, 6, AlignCenter, AlignTop, "High Score");
  244. }
  245. // Convert highscore to string
  246. int length = snprintf(NULL, 0, "%i", simon_state->highScore);
  247. char* str_high_score = malloc(length + 1);
  248. snprintf(str_high_score, length + 1, "%i", simon_state->highScore);
  249. // Display High Score
  250. canvas_draw_str_aligned(
  251. canvas, SCREEN_XRES / 2, 22, AlignCenter, AlignCenter, str_high_score);
  252. free(str_high_score);
  253. // Display Game Over
  254. canvas_draw_str_aligned(
  255. canvas, SCREEN_XRES / 2, SCREEN_YRES / 2 + 2, AlignCenter, AlignCenter, "GAME OVER");
  256. // Display Press OK to restart below title
  257. canvas_set_font(canvas, FontSecondary);
  258. canvas_draw_str_aligned(
  259. canvas,
  260. SCREEN_XRES / 2,
  261. SCREEN_YRES / 2 + 15,
  262. AlignCenter,
  263. AlignCenter,
  264. "Press OK to restart");
  265. }
  266. // ######################### Victory #########################
  267. //Player Beat Simon beyond limit! A word record holder here!
  268. //TODO
  269. //release the mutex
  270. release_mutex((ValueMutex*)ctx, simon_state);
  271. }
  272. /* ======================== Input Handling ============================== */
  273. void simon_input_callback(InputEvent* input_event, void* ctx) {
  274. furi_assert(ctx);
  275. FuriMessageQueue* event_queue = ctx;
  276. furi_message_queue_put(event_queue, input_event, FuriWaitForever);
  277. }
  278. /* ======================== Simon Game Engine ======================== */
  279. bool load_game(SimonData* app) {
  280. //TODO
  281. UNUSED(app);
  282. return true;
  283. // Storage* storage = furi_record_open(RECORD_STORAGE);
  284. // File* file = storage_file_alloc(storage);
  285. // uint16_t bytes_readed = 0;
  286. // if(storage_file_open(file, SAVING_FILENAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
  287. // bytes_readed = storage_file_read(file, app, sizeof(SimonData));
  288. // }
  289. // storage_file_close(file);
  290. // storage_file_free(file);
  291. // furi_record_close(RECORD_STORAGE);
  292. // return bytes_readed == sizeof(SimonData);
  293. }
  294. void save_game(SimonData* app) {
  295. //TODO
  296. UNUSED(app);
  297. // Storage* storage = furi_record_open(RECORD_STORAGE);
  298. // if(storage_common_stat(storage, SAVING_DIRECTORY, NULL) == FSE_NOT_EXIST) {
  299. // if(!storage_simply_mkdir(storage, SAVING_DIRECTORY)) {
  300. // return;
  301. // }
  302. // }
  303. // File* file = storage_file_alloc(storage);
  304. // if(storage_file_open(file, SAVING_FILENAME, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
  305. // storage_file_write(file, app, sizeof(SimonData));
  306. // }
  307. // storage_file_close(file);
  308. // storage_file_free(file);
  309. // furi_record_close(RECORD_STORAGE);
  310. }
  311. int getRandomIntInRange(int lower, int upper) {
  312. return (rand() % (upper - lower + 1)) + lower;
  313. }
  314. void play_sound_sequence_correct() {
  315. notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_success);
  316. }
  317. void play_sound_wrong_move() {
  318. //TODO: play wrong sound: Try sequence_audiovisual_alert
  319. notification_message(furi_record_open(RECORD_NOTIFICATION), &sequence_error);
  320. }
  321. /* Restart game and give player a chance to try again on same sequence */
  322. // @todo restartGame
  323. void resetGame(SimonData* app) {
  324. app->moveIndex = 0;
  325. app->numberOfMillisecondsBeforeShapeDisappears = 1000;
  326. app->activePlayer = simon;
  327. app->is_wrong_direction = false;
  328. app->last_button_press_tick = 0;
  329. app->set_board_neutral = true;
  330. app->activePlayer = simon;
  331. }
  332. /* Set gameover state */
  333. void game_over(SimonData* app) {
  334. if(app->is_new_highscore) save_game(app); // Save highscore but only on change
  335. app->gameover = true;
  336. app->lives = GAME_START_LIVES; // Show 3 lives in game over screen to match new game start
  337. app->gameState = gameOver;
  338. }
  339. /* Called after gameover to restart the game. This function
  340. * also calls restart_game(). */
  341. void restart_game_after_gameover(SimonData* app) {
  342. app->gameState = inGame;
  343. app->gameover = false;
  344. app->currentScore = 0;
  345. app->is_new_highscore = false;
  346. app->lives = GAME_START_LIVES;
  347. app->simonMoves[0] = rand() % number_of_shapes;
  348. resetGame(app);
  349. }
  350. void addNewSimonMove(int addAtIndex, SimonData* app) {
  351. app->simonMoves[addAtIndex] = getRandomIntInRange(0, 3);
  352. }
  353. void startNewRound(SimonData* app) {
  354. addNewSimonMove(app->currentScore, app);
  355. app->moveIndex = 0;
  356. app->activePlayer = simon;
  357. }
  358. void onPlayerAnsweredCorrect(SimonData* app) {
  359. app->moveIndex++;
  360. }
  361. void onPlayerAnsweredWrong(SimonData* app) {
  362. if(app->lives > 0) {
  363. app->lives--;
  364. // Play the wrong sound
  365. if(app->sound_enabled) {
  366. play_sound_wrong_move();
  367. }
  368. resetGame(app);
  369. } else {
  370. // The player has no lives left
  371. // Game over
  372. game_over(app);
  373. //TODO: Play unique game over sound
  374. }
  375. }
  376. bool isRoundComplete(SimonData* app) {
  377. return app->moveIndex == app->currentScore;
  378. }
  379. enum shape_names getCurrentSimonMove(SimonData* app) {
  380. return app->simonMoves[app->moveIndex];
  381. }
  382. void onPlayerSelectedShapeCallback(enum shape_names shape, SimonData* app) {
  383. if(shape == getCurrentSimonMove(app)) {
  384. onPlayerAnsweredCorrect(app);
  385. } else {
  386. onPlayerAnsweredWrong(app);
  387. }
  388. }
  389. //@todo gametick
  390. void game_tick(SimonData* simon_state) {
  391. if(simon_state->gameState == inGame) {
  392. if(simon_state->activePlayer == simon) {
  393. // ############### Simon Turn ###############
  394. notification_message(simon_state->notification, &sequence_simon_is_playing);
  395. //@todo Gameplay
  396. if(simon_state->set_board_neutral) {
  397. if(simon_state->moveIndex < simon_state->currentScore) {
  398. simon_state->selectedShape = getCurrentSimonMove(simon_state);
  399. simon_state->set_board_neutral = false;
  400. simon_state->moveIndex++;
  401. } else {
  402. simon_state->activePlayer = player;
  403. simon_state->set_board_neutral = true;
  404. simon_state->moveIndex = 0;
  405. }
  406. } else {
  407. simon_state->set_board_neutral = true;
  408. }
  409. } else {
  410. // ############### Player Turn ###############
  411. notification_message(simon_state->notification, &sequence_player_is_playing);
  412. // It's Player's Turn
  413. if(isRoundComplete(simon_state)) {
  414. simon_state->activePlayer = simon;
  415. simon_state->currentScore++;
  416. // app->numberOfMillisecondsBeforeShapeDisappears -= 50;
  417. if(simon_state->currentScore > simon_state->highScore) {
  418. simon_state->highScore = simon_state->currentScore;
  419. simon_state->is_new_highscore = true;
  420. }
  421. if(simon_state->sound_enabled) {
  422. play_sound_sequence_correct();
  423. }
  424. startNewRound(simon_state);
  425. }
  426. }
  427. }
  428. }
  429. /* ======================== Main Entry Point ============================== */
  430. int32_t simon_says_app_entry(void* p) {
  431. UNUSED(p);
  432. FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
  433. SimonData* simon_state = malloc(sizeof(SimonData));
  434. ValueMutex simon_state_value_mutex;
  435. if(!init_mutex(&simon_state_value_mutex, simon_state, sizeof(SimonData))) {
  436. FURI_LOG_E(TAG, "cannot create mutex\r\n");
  437. free(simon_state);
  438. return -1;
  439. }
  440. // Configure view port
  441. ViewPort* view_port = view_port_alloc();
  442. view_port_draw_callback_set(view_port, simon_draw_callback, &simon_state_value_mutex);
  443. view_port_input_callback_set(view_port, simon_input_callback, event_queue);
  444. // Register view port in GUI
  445. Gui* gui = furi_record_open(RECORD_GUI);
  446. gui_add_view_port(gui, view_port, GuiLayerFullscreen);
  447. NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
  448. simon_state->notification = notification;
  449. InputEvent input;
  450. // Show Main Menu Screen
  451. simon_state->gameState = mainMenu;
  452. while(true) {
  453. if(furi_message_queue_get(event_queue, &input, 500) == FuriStatusOk) {
  454. FURI_LOG_D(TAG, "Got input event: %d", input.key);
  455. //break out of the loop if the back key is pressed
  456. if(input.key == InputKeyBack && input.type == InputTypeLong) {
  457. break;
  458. }
  459. if(input.type == InputTypeLong) {
  460. // Do Nothing. Ignore long press events
  461. }
  462. //@todo Set Game States
  463. if(input.key == InputKeyOk && simon_state->gameState != inGame) {
  464. restart_game_after_gameover(simon_state);
  465. // Set Simon Board state
  466. startNewRound(simon_state);
  467. view_port_update(view_port);
  468. }
  469. // Keep LED on if it is Simon's turn
  470. if(simon_state->activePlayer == player) {
  471. notification_message(notification, &sequence_player_is_playing);
  472. if(input.type == InputTypePress) {
  473. simon_state->set_board_neutral = false;
  474. switch(input.key) {
  475. case InputKeyUp:
  476. simon_state->selectedShape = up;
  477. onPlayerSelectedShapeCallback(up, simon_state);
  478. break;
  479. case InputKeyDown:
  480. simon_state->selectedShape = down;
  481. onPlayerSelectedShapeCallback(down, simon_state);
  482. break;
  483. case InputKeyLeft:
  484. simon_state->selectedShape = left;
  485. onPlayerSelectedShapeCallback(left, simon_state);
  486. break;
  487. case InputKeyRight:
  488. simon_state->selectedShape = right;
  489. onPlayerSelectedShapeCallback(right, simon_state);
  490. break;
  491. default:
  492. break;
  493. }
  494. } else {
  495. FURI_LOG_D(TAG, "Input type is not short");
  496. simon_state->set_board_neutral = true;
  497. }
  498. }
  499. } else {
  500. FURI_LOG_E(TAG, "cannot get message from queue\r\n");
  501. }
  502. // @todo Animation Loop for debug
  503. // if(simon_state->gameState == inGame && simon_state->activePlayer == simon) {
  504. // simon_state->currentScore++;
  505. // simon_state->set_board_neutral = !simon_state->set_board_neutral;
  506. // }
  507. game_tick(simon_state);
  508. view_port_update(view_port);
  509. }
  510. notification_message(notification, &sequence_cleanup);
  511. gui_remove_view_port(gui, view_port);
  512. view_port_free(view_port);
  513. furi_message_queue_free(event_queue);
  514. free(simon_state);
  515. furi_record_close(RECORD_NOTIFICATION);
  516. furi_record_close(RECORD_GUI);
  517. return 0;
  518. }