minesweeper.c 15 KB

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