minesweeper.c 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. #include <furi.h>
  2. #include <furi_hal.h>
  3. #include <gui/gui.h>
  4. #include <input/input.h>
  5. #include <stdlib.h>
  6. #include <notification/notification_messages.h>
  7. #include <dialogs/dialogs.h>
  8. #include <m-string.h>
  9. #include "assets.h"
  10. #define PLAYFIELD_WIDTH 16
  11. #define PLAYFIELD_HEIGHT 7
  12. #define TILE_WIDTH 8
  13. #define TILE_HEIGHT 8
  14. #define MINECOUNT 24
  15. typedef enum {
  16. EventTypeTick,
  17. EventTypeKey,
  18. } EventType;
  19. typedef struct {
  20. EventType type;
  21. InputEvent input;
  22. } PluginEvent;
  23. typedef enum {
  24. TileType0, // this HAS to be in order, for hint assignment to be ez pz
  25. TileType1,
  26. TileType2,
  27. TileType3,
  28. TileType4,
  29. TileType5,
  30. TileType6,
  31. TileType7,
  32. TileType8,
  33. TileTypeUncleared,
  34. TileTypeFlag,
  35. TileTypeMine
  36. } TileType;
  37. typedef enum {
  38. FieldEmpty, // <-- same goes for this
  39. FieldMine
  40. } Field;
  41. typedef struct {
  42. Field minefield[PLAYFIELD_WIDTH][PLAYFIELD_HEIGHT];
  43. TileType playfield[PLAYFIELD_WIDTH][PLAYFIELD_HEIGHT];
  44. int cursor_x;
  45. int cursor_y;
  46. int mines_left;
  47. int fields_cleared;
  48. int flags_set;
  49. bool game_started;
  50. bool showing_dialog;
  51. uint32_t game_started_tick;
  52. } Minesweeper;
  53. static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
  54. furi_assert(event_queue);
  55. PluginEvent event = {.type = EventTypeKey, .input = *input_event};
  56. furi_message_queue_put(event_queue, &event, FuriWaitForever);
  57. }
  58. static void render_callback(Canvas* const canvas, void* ctx) {
  59. const Minesweeper* minesweeper_state = acquire_mutex((ValueMutex*)ctx, 25);
  60. if (minesweeper_state == NULL) {
  61. return;
  62. }
  63. if (minesweeper_state->showing_dialog) {
  64. release_mutex((ValueMutex*)ctx, minesweeper_state);
  65. return;
  66. }
  67. string_t tempStr;
  68. string_init(tempStr);
  69. string_printf(tempStr, "Mines: %d", MINECOUNT - minesweeper_state->flags_set);
  70. canvas_set_font(canvas, FontSecondary);
  71. canvas_draw_str_aligned(canvas, 0, 0, AlignLeft, AlignTop, string_get_cstr(tempStr));
  72. string_clear(tempStr);
  73. int seconds = 0;
  74. int minutes = 0;
  75. if (minesweeper_state->game_started) {
  76. uint32_t ticks_elapsed = furi_get_tick() - minesweeper_state->game_started_tick;
  77. seconds = (int) ticks_elapsed / furi_kernel_get_tick_frequency();
  78. minutes = (int) seconds / 60;
  79. seconds = seconds % 60;
  80. }
  81. string_printf(tempStr, "%01d:%02d", minutes, seconds);
  82. canvas_draw_str_aligned(canvas, 128, 0, AlignRight, AlignTop, string_get_cstr(tempStr));
  83. string_clear(tempStr);
  84. for (int y = 0; y < PLAYFIELD_HEIGHT; y++) {
  85. for (int x = 0; x < PLAYFIELD_WIDTH; x++) {
  86. if ( x == minesweeper_state->cursor_x && y == minesweeper_state->cursor_y) {
  87. canvas_invert_color(canvas);
  88. }
  89. switch (minesweeper_state->playfield[x][y]) {
  90. case TileType0:
  91. canvas_draw_xbm(
  92. canvas,
  93. x*TILE_HEIGHT, // x
  94. 8 + (y * TILE_WIDTH), // y
  95. TILE_WIDTH,
  96. TILE_HEIGHT,
  97. tile_0_bits);
  98. break;
  99. case TileType1:
  100. canvas_draw_xbm(
  101. canvas,
  102. x*TILE_HEIGHT, // x
  103. 8 + (y * TILE_WIDTH), // y
  104. TILE_WIDTH,
  105. TILE_HEIGHT,
  106. tile_1_bits);
  107. break;
  108. case TileType2:
  109. canvas_draw_xbm(
  110. canvas,
  111. x*TILE_HEIGHT, // x
  112. 8 + (y * TILE_WIDTH), // y
  113. TILE_WIDTH,
  114. TILE_HEIGHT,
  115. tile_2_bits);
  116. break;
  117. case TileType3:
  118. canvas_draw_xbm(
  119. canvas,
  120. x*TILE_HEIGHT, // x
  121. 8 + (y * TILE_WIDTH), // y
  122. TILE_WIDTH,
  123. TILE_HEIGHT,
  124. tile_3_bits);
  125. break;
  126. case TileType4:
  127. canvas_draw_xbm(
  128. canvas,
  129. x*TILE_HEIGHT, // x
  130. 8 + (y * TILE_WIDTH), // y
  131. TILE_WIDTH,
  132. TILE_HEIGHT,
  133. tile_4_bits);
  134. break;
  135. case TileType5:
  136. canvas_draw_xbm(
  137. canvas,
  138. x*TILE_HEIGHT, // x
  139. 8 + (y * TILE_WIDTH), // y
  140. TILE_WIDTH,
  141. TILE_HEIGHT,
  142. tile_5_bits);
  143. break;
  144. case TileType6:
  145. canvas_draw_xbm(
  146. canvas,
  147. x*TILE_HEIGHT, // x
  148. 8 + (y * TILE_WIDTH), // y
  149. TILE_WIDTH,
  150. TILE_HEIGHT,
  151. tile_6_bits);
  152. break;
  153. case TileType7:
  154. canvas_draw_xbm(
  155. canvas,
  156. x*TILE_HEIGHT, // x
  157. 8 + (y * TILE_WIDTH), // y
  158. TILE_WIDTH,
  159. TILE_HEIGHT,
  160. tile_7_bits);
  161. break;
  162. case TileType8:
  163. canvas_draw_xbm(
  164. canvas,
  165. x*TILE_HEIGHT, // x
  166. 8 + (y * TILE_WIDTH), // y
  167. TILE_WIDTH,
  168. TILE_HEIGHT,
  169. tile_8_bits);
  170. break;
  171. case TileTypeFlag:
  172. canvas_draw_xbm(
  173. canvas,
  174. x*TILE_HEIGHT, // x
  175. 8 + (y * TILE_WIDTH), // y
  176. TILE_WIDTH,
  177. TILE_HEIGHT,
  178. tile_flag_bits);
  179. break;
  180. case TileTypeUncleared:
  181. canvas_draw_xbm(
  182. canvas,
  183. x*TILE_HEIGHT, // x
  184. 8 + (y * TILE_WIDTH), // y
  185. TILE_WIDTH,
  186. TILE_HEIGHT,
  187. tile_uncleared_bits);
  188. break;
  189. case TileTypeMine:
  190. canvas_draw_xbm(
  191. canvas,
  192. x*TILE_HEIGHT, // x
  193. 8 + (y * TILE_WIDTH), // y
  194. TILE_WIDTH,
  195. TILE_HEIGHT,
  196. tile_mine_bits);
  197. break;
  198. }
  199. if ( x == minesweeper_state->cursor_x && y == minesweeper_state->cursor_y) {
  200. canvas_invert_color(canvas);
  201. }
  202. }
  203. }
  204. string_clear(tempStr);
  205. release_mutex((ValueMutex*)ctx, minesweeper_state);
  206. }
  207. static void setup_playfield(Minesweeper* minesweeper_state) {
  208. int mines_left = MINECOUNT;
  209. for (int y = 0; y < PLAYFIELD_HEIGHT; y++) {
  210. for (int x = 0; x < PLAYFIELD_WIDTH; x++){
  211. minesweeper_state->minefield[x][y] = FieldEmpty;
  212. minesweeper_state->playfield[x][y] = TileTypeUncleared;
  213. }
  214. }
  215. while(mines_left > 0) {
  216. int rand_x = rand() % PLAYFIELD_WIDTH;
  217. int rand_y = rand() % PLAYFIELD_HEIGHT;
  218. // make sure first guess isn't a mine
  219. if (minesweeper_state->minefield[rand_x][rand_y] == FieldEmpty &&
  220. (minesweeper_state->cursor_x != rand_x && minesweeper_state->cursor_y != rand_y )) {
  221. minesweeper_state->minefield[rand_x][rand_y] = FieldMine;
  222. mines_left--;
  223. }
  224. minesweeper_state->mines_left = MINECOUNT;
  225. minesweeper_state->fields_cleared = 0;
  226. minesweeper_state->flags_set = 0;
  227. minesweeper_state->game_started_tick = furi_get_tick();
  228. }
  229. }
  230. static void place_flag(Minesweeper* minesweeper_state) {
  231. if (minesweeper_state->playfield[minesweeper_state->cursor_x][minesweeper_state->cursor_y] == TileTypeUncleared) {
  232. minesweeper_state->playfield[minesweeper_state->cursor_x][minesweeper_state->cursor_y] = TileTypeFlag;
  233. minesweeper_state->flags_set++;
  234. } else if (minesweeper_state->playfield[minesweeper_state->cursor_x][minesweeper_state->cursor_y] == TileTypeFlag) {
  235. minesweeper_state->playfield[minesweeper_state->cursor_x][minesweeper_state->cursor_y] = TileTypeUncleared;
  236. minesweeper_state->flags_set--;
  237. }
  238. }
  239. static bool game_lost(Minesweeper* minesweeper_state) {
  240. // returns true if the player wants to restart, otherwise false
  241. minesweeper_state->showing_dialog = true;
  242. NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION);
  243. DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS);
  244. DialogMessage* message = dialog_message_alloc();
  245. const char* header_text = "Game Over";
  246. const char* message_text = "You hit a mine!";
  247. dialog_message_set_header(message, header_text, 64, 3, AlignCenter, AlignTop);
  248. dialog_message_set_text(message, message_text, 64, 32, AlignCenter, AlignCenter);
  249. dialog_message_set_buttons(message, NULL, "Play again", NULL);
  250. // TODO: create icon
  251. dialog_message_set_icon(message, NULL, 72, 17);
  252. notification_message(notifications, &sequence_set_vibro_on);
  253. furi_delay_ms(200);
  254. notification_message(notifications, &sequence_reset_vibro);
  255. DialogMessageButton choice = dialog_message_show(dialogs, message);
  256. dialog_message_free(message);
  257. minesweeper_state->showing_dialog = false;
  258. furi_record_close(RECORD_NOTIFICATION);
  259. return choice == DialogMessageButtonCenter;
  260. }
  261. static bool game_won(Minesweeper* minesweeper_state) {
  262. DialogsApp *dialogs = furi_record_open(RECORD_DIALOGS);
  263. minesweeper_state->showing_dialog = true;
  264. string_t tempStr;
  265. string_init(tempStr);
  266. int seconds = 0;
  267. int minutes = 0;
  268. uint32_t ticks_elapsed = furi_get_tick() - minesweeper_state->game_started_tick;
  269. seconds = (int) ticks_elapsed / furi_kernel_get_tick_frequency();
  270. minutes = (int) seconds / 60;
  271. seconds = seconds % 60;
  272. DialogMessage* message = dialog_message_alloc();
  273. const char* header_text = "Game won!";
  274. string_printf(tempStr, "Minefield cleared in %01d:%02d", minutes, seconds);
  275. dialog_message_set_header(message, header_text, 64, 3, AlignCenter, AlignTop);
  276. dialog_message_set_text(message, string_get_cstr(tempStr), 64, 32, AlignCenter, AlignCenter);
  277. dialog_message_set_buttons(message, NULL, "Play again", NULL);
  278. // TODO: create icon
  279. dialog_message_set_icon(message, NULL, 72, 17);
  280. DialogMessageButton choice = dialog_message_show(dialogs, message);
  281. dialog_message_free(message);
  282. minesweeper_state->showing_dialog = false;
  283. string_clear(tempStr);
  284. string_reset(tempStr);
  285. furi_record_close(RECORD_DIALOGS);
  286. return choice == DialogMessageButtonCenter;
  287. }
  288. static bool play_move(Minesweeper* minesweeper_state, int cursor_x, int cursor_y) {
  289. if (minesweeper_state->playfield[cursor_x][cursor_y] != TileTypeUncleared) {
  290. // we're on an already uncovered field
  291. return true;
  292. }
  293. if (minesweeper_state->minefield[cursor_x][cursor_y] == FieldMine) {
  294. // TODO: player loses!
  295. minesweeper_state->playfield[cursor_x][cursor_y] = TileTypeMine;
  296. return false;
  297. } else {
  298. // get number of surrounding mines.
  299. int hint = 0;
  300. for (int y = cursor_y-1; y <= cursor_y+1; y++) {
  301. for (int x = cursor_x-1; x <= cursor_x+1; x++) {
  302. if ( x == cursor_x && y == cursor_y ) {
  303. // we're on the cell the user selected, so ignore.
  304. continue;
  305. }
  306. // make sure we don't go OOB
  307. if ( x >= 0 && x < PLAYFIELD_WIDTH && y >= 0 && y < PLAYFIELD_HEIGHT) {
  308. if(minesweeper_state->minefield[x][y] == FieldMine) {
  309. hint ++;
  310. }
  311. }
  312. }
  313. }
  314. // 〜( ̄▽ ̄〜) don't judge me (〜 ̄▽ ̄)〜
  315. minesweeper_state->playfield[cursor_x][cursor_y] = hint;
  316. minesweeper_state->fields_cleared++;
  317. FURI_LOG_D("Minesweeper", "Setting %d,%d to %d", cursor_x, cursor_y, hint);
  318. if (hint == 0) {
  319. // auto open surrounding fields.
  320. for (int auto_y = cursor_y-1; auto_y <= cursor_y+1; auto_y++) {
  321. for (int auto_x = cursor_x-1; auto_x <= cursor_x+1; auto_x++) {
  322. if ( auto_x == cursor_x && auto_y == cursor_y ) {
  323. continue;
  324. }
  325. if ( auto_x >= 0 && auto_x < PLAYFIELD_WIDTH && auto_y >= 0 && auto_y < PLAYFIELD_HEIGHT) {
  326. if (minesweeper_state->playfield[auto_x][auto_y] == TileTypeUncleared) {
  327. play_move(minesweeper_state, auto_x, auto_y);
  328. }
  329. }
  330. }
  331. }
  332. }
  333. return true;
  334. }
  335. }
  336. static void minesweeper_state_init(Minesweeper* const minesweeper_state) {
  337. minesweeper_state->cursor_x = minesweeper_state->cursor_y = 0;
  338. minesweeper_state->game_started = false;
  339. minesweeper_state->showing_dialog = false;
  340. for (int y = 0; y < PLAYFIELD_HEIGHT; y++) {
  341. for (int x = 0; x < PLAYFIELD_WIDTH; x++){
  342. minesweeper_state->playfield[x][y] = TileTypeUncleared;
  343. }
  344. }
  345. }
  346. int32_t minesweeper_app(void* p) {
  347. UNUSED(p);
  348. FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
  349. Minesweeper* minesweeper_state = malloc(sizeof(Minesweeper));
  350. // setup
  351. minesweeper_state_init(minesweeper_state);
  352. ValueMutex state_mutex;
  353. if (!init_mutex(&state_mutex, minesweeper_state, sizeof(minesweeper_state))) {
  354. FURI_LOG_E("Minesweeper", "cannot create mutex\r\n");
  355. free(minesweeper_state);
  356. return 255;
  357. }
  358. // BEGIN IMPLEMENTATION
  359. // Set system callbacks
  360. ViewPort* view_port = view_port_alloc();
  361. view_port_draw_callback_set(view_port, render_callback, &state_mutex);
  362. view_port_input_callback_set(view_port, input_callback, event_queue);
  363. // Open GUI and register view_port
  364. Gui* gui = furi_record_open("gui");
  365. gui_add_view_port(gui, view_port, GuiLayerFullscreen);
  366. PluginEvent event;
  367. for (bool processing = true; processing;) {
  368. FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
  369. Minesweeper* minesweeper_state = (Minesweeper*)acquire_mutex_block(&state_mutex);
  370. if(event_status == FuriStatusOk) {
  371. // press events
  372. if(event.type == EventTypeKey) {
  373. if(event.input.type == InputTypeShort) {
  374. switch(event.input.key) {
  375. case InputKeyUp:
  376. minesweeper_state->cursor_y--;
  377. if(minesweeper_state->cursor_y < 0) {
  378. minesweeper_state->cursor_y = 0;
  379. }
  380. break;
  381. case InputKeyDown:
  382. minesweeper_state->cursor_y++;
  383. if(minesweeper_state->cursor_y >= PLAYFIELD_HEIGHT) {
  384. minesweeper_state->cursor_y = PLAYFIELD_HEIGHT-1;
  385. }
  386. break;
  387. case InputKeyRight:
  388. minesweeper_state->cursor_x++;
  389. if(minesweeper_state->cursor_x >= PLAYFIELD_WIDTH) {
  390. minesweeper_state->cursor_x = PLAYFIELD_WIDTH-1;
  391. }
  392. break;
  393. case InputKeyLeft:
  394. minesweeper_state->cursor_x--;
  395. if(minesweeper_state->cursor_x < 0) {
  396. minesweeper_state->cursor_x = 0;
  397. }
  398. break;
  399. case InputKeyOk:
  400. if (!minesweeper_state->game_started) {
  401. setup_playfield(minesweeper_state);
  402. minesweeper_state->game_started = true;
  403. }
  404. if (!play_move(minesweeper_state, minesweeper_state->cursor_x, minesweeper_state->cursor_y)) {
  405. // ooops. looks like we hit a mine!
  406. if (game_lost(minesweeper_state)) {
  407. // player wants to restart.
  408. setup_playfield(minesweeper_state);
  409. } else {
  410. // player wants to exit :(
  411. processing = false;
  412. }
  413. } else {
  414. // check win condition.
  415. if (minesweeper_state->fields_cleared == (PLAYFIELD_HEIGHT*PLAYFIELD_WIDTH) - MINECOUNT){
  416. if (game_won(minesweeper_state)) {
  417. //player wants to restart
  418. setup_playfield(minesweeper_state);
  419. } else {
  420. processing = false;
  421. }
  422. }
  423. }
  424. break;
  425. case InputKeyBack:
  426. // Exit the plugin
  427. processing = false;
  428. break;
  429. }
  430. } else if (event.input.type == InputTypeLong) {
  431. // hold events
  432. FURI_LOG_D("Minesweeper", "Got a long press!");
  433. switch(event.input.key) {
  434. case InputKeyUp:
  435. case InputKeyDown:
  436. case InputKeyRight:
  437. case InputKeyLeft:
  438. break;
  439. case InputKeyOk:
  440. FURI_LOG_D("Minesweeper", "Toggling flag");
  441. place_flag(minesweeper_state);
  442. break;
  443. case InputKeyBack:
  444. processing = false;
  445. break;
  446. }
  447. }
  448. }
  449. } else {
  450. // event timeout
  451. ;
  452. }
  453. view_port_update(view_port);
  454. release_mutex(&state_mutex, minesweeper_state);
  455. }
  456. view_port_enabled_set(view_port, false);
  457. gui_remove_view_port(gui, view_port);
  458. furi_record_close("gui");
  459. view_port_free(view_port);
  460. furi_message_queue_free(event_queue);
  461. delete_mutex(&state_mutex);
  462. free(minesweeper_state);
  463. return 0;
  464. }