minesweeper_game_screen.c 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647
  1. #include "minesweeper_game_screen.h"
  2. #include "minesweeper_redux_icons.h"
  3. #include <gui/elements.h>
  4. #include <gui/icon_animation.h>
  5. #include <input/input.h>
  6. #include <furi.h>
  7. #include <furi_hal.h>
  8. static const Icon* tile_icons[13] = {
  9. &I_tile_empty_8x8,
  10. &I_tile_0_8x8,
  11. &I_tile_1_8x8,
  12. &I_tile_2_8x8,
  13. &I_tile_3_8x8,
  14. &I_tile_4_8x8,
  15. &I_tile_5_8x8,
  16. &I_tile_6_8x8,
  17. &I_tile_7_8x8,
  18. &I_tile_8_8x8,
  19. &I_tile_mine_8x8,
  20. &I_tile_flag_8x8,
  21. &I_tile_uncleared_8x8,
  22. };
  23. // They way this enum is set up allows us to index the Icon* array above for some mine types
  24. typedef enum {
  25. MineSweeperGameScreenTileNone = 0,
  26. MineSweeperGameScreenTileZero,
  27. MineSweeperGameScreenTileOne,
  28. MineSweeperGameScreenTileTwo,
  29. MineSweeperGameScreenTileThree,
  30. MineSweeperGameScreenTileFour,
  31. MineSweeperGameScreenTileFive,
  32. MineSweeperGameScreenTileSix,
  33. MineSweeperGameScreenTileSeven,
  34. MineSweeperGameScreenTileEight,
  35. MineSweeperGameScreenTileMine,
  36. MineSweeperGameScreenTileTypeCount,
  37. } MineSweeperGameScreenTileType;
  38. typedef enum {
  39. MineSweeperGameScreenTileStateFlagged,
  40. MineSweeperGameScreenTileStateUncleared,
  41. MineSweeperGameScreenTileStateCleared,
  42. } MineSweeperGameScreenTileState;
  43. struct MineSweeperGameScreen {
  44. View* view;
  45. void* context;
  46. GameScreenInputCallback input_callback;
  47. };
  48. typedef struct {
  49. int16_t x_abs, y_abs;
  50. } CurrentPosition;
  51. typedef struct {
  52. uint16_t x_abs, y_abs;
  53. const Icon* icon;
  54. } IconElement;
  55. typedef struct {
  56. IconElement icon_element;
  57. MineSweeperGameScreenTileState tile_state;
  58. MineSweeperGameScreenTileType tile_type;
  59. } MineSweeperTile;
  60. typedef struct {
  61. MineSweeperTile board[ MINESWEEPER_BOARD_MAX_TILES ];
  62. CurrentPosition curr_pos;
  63. uint8_t right_boundary, bottom_boundary,
  64. board_width, board_height, board_difficulty;
  65. uint16_t mines_left;
  66. uint16_t flags_left;
  67. uint16_t tiles_left;
  68. uint32_t start_tick;
  69. FuriString* info_str;
  70. bool ensure_solvable_board;
  71. bool is_making_first_move;
  72. bool is_holding_down_button;
  73. } MineSweeperGameScreenModel;
  74. // Multipliers for ratio of mines to tiles
  75. static const float difficulty_multiplier[3] = {
  76. 0.15f,
  77. 0.17f,
  78. 0.19f,
  79. };
  80. // Offsets array used consistently when checking surrounding tiles
  81. static const int8_t offsets[8][2] = {
  82. {-1,1},
  83. {0,1},
  84. {1,1},
  85. {1,0},
  86. {1,-1},
  87. {0,-1},
  88. {-1,-1},
  89. {-1,0},
  90. };
  91. static MineSweeperTile board_t[MINESWEEPER_BOARD_MAX_TILES];
  92. /****************************************************************
  93. * Function declarations
  94. *
  95. * Non public function declarations
  96. ***************************************************************/
  97. // Static helper functions
  98. static void setup_board(MineSweeperGameScreen* instance);
  99. static bool check_board_with_verifier(
  100. MineSweeperTile* board,
  101. const uint8_t board_width,
  102. const uint8_t board_height,
  103. uint16_t total_mines);
  104. static inline void bfs_tile_clear_verifier(
  105. MineSweeperTile* board,
  106. const uint8_t board_width,
  107. const uint8_t board_height,
  108. const uint16_t x,
  109. const uint16_t y,
  110. point_deq_t* edges,
  111. point_set_t* visited);
  112. static uint16_t bfs_tile_clear(MineSweeperTile* board,
  113. const uint8_t board_width,
  114. const uint8_t board_height,
  115. const uint16_t x,
  116. const uint16_t y);
  117. static void mine_sweeper_game_screen_set_board_information(
  118. MineSweeperGameScreen* instance,
  119. const uint8_t width,
  120. const uint8_t height,
  121. const uint8_t difficulty,
  122. bool is_solvable);
  123. static bool try_clear_surrounding_tiles(MineSweeperGameScreenModel* model);
  124. static Point bfs_to_closest_tile(MineSweeperGameScreenModel* model);
  125. // Currently not using enter/exit callback
  126. static void mine_sweeper_game_screen_view_enter(void* context);
  127. static void mine_sweeper_game_screen_view_exit(void* context);
  128. // Different input/draw callbacks for play/win/lose state
  129. static void mine_sweeper_game_screen_view_win_draw_callback(Canvas* canvas, void* _model);
  130. static void mine_sweeper_game_screen_view_lose_draw_callback(Canvas* canvas, void* _model);
  131. static void mine_sweeper_game_screen_view_play_draw_callback(Canvas* canvas, void* _model);
  132. static bool mine_sweeper_game_screen_view_end_input_callback(InputEvent* event, void* context);
  133. static bool mine_sweeper_game_screen_view_play_input_callback(InputEvent* event, void* context);
  134. /**************************************************************
  135. * Function definitions
  136. *************************************************************/
  137. /**
  138. * This function is called on alloc, reset, and win/lose condition.
  139. * It sets up a random board to be checked by the verifier
  140. */
  141. static void setup_board(MineSweeperGameScreen* instance) {
  142. furi_assert(instance);
  143. uint16_t board_tile_count = 0;
  144. uint8_t board_width = 0, board_height = 0, board_difficulty = 0;
  145. with_view_model(
  146. instance->view,
  147. MineSweeperGameScreenModel * model,
  148. {
  149. board_width = model->board_width;
  150. board_height = model->board_height;
  151. board_tile_count = (model->board_width*model->board_height);
  152. board_difficulty = model->board_difficulty;
  153. },
  154. false
  155. );
  156. uint16_t num_mines = board_tile_count * difficulty_multiplier[ board_difficulty ];
  157. /** We can use a temporary buffer to set the tile types initially
  158. * and manipulate then save to actual model
  159. */
  160. MineSweeperGameScreenTileType tiles[MINESWEEPER_BOARD_MAX_TILES];
  161. memset(&tiles, MineSweeperGameScreenTileNone, sizeof(tiles));
  162. // Randomly place tiles except in the corners to help guarantee solvability
  163. for (uint16_t i = 0; i < num_mines; i++) {
  164. uint16_t rand_pos;
  165. uint16_t x;
  166. uint16_t y;
  167. bool is_invalid_position;
  168. do {
  169. rand_pos = furi_hal_random_get() % board_tile_count;
  170. x = rand_pos / board_width;
  171. y = rand_pos % board_width;
  172. is_invalid_position = ((rand_pos == 0) ||
  173. (x==0 && y==1) ||
  174. (x==1 && y==0) ||
  175. rand_pos == board_tile_count-1 ||
  176. (x==0 && y==board_width-1) ||
  177. (x==board_height-1 && y==0));
  178. } while (tiles[rand_pos] == MineSweeperGameScreenTileMine || is_invalid_position);
  179. tiles[rand_pos] = MineSweeperGameScreenTileMine;
  180. }
  181. /** All mines are set so we look at each tile for surrounding mines */
  182. for (uint16_t i = 0; i < board_tile_count; i++) {
  183. MineSweeperGameScreenTileType tile_type = tiles[i];
  184. if (tile_type == MineSweeperGameScreenTileMine) {
  185. continue;
  186. }
  187. uint16_t mine_count = 0;
  188. uint16_t x = i / board_width;
  189. uint16_t y = i % board_width;
  190. for (uint8_t j = 0; j < 8; j++) {
  191. int16_t dx = x + (int16_t)offsets[j][0];
  192. int16_t dy = y + (int16_t)offsets[j][1];
  193. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  194. continue;
  195. }
  196. uint16_t pos = dx * board_width + dy;
  197. if (tiles[pos] == MineSweeperGameScreenTileMine) {
  198. mine_count++;
  199. }
  200. }
  201. tiles[i] = (MineSweeperGameScreenTileType) mine_count+1;
  202. }
  203. // Save tiles to view model
  204. // Because of way tile enum and tile_icons array is set up we can
  205. // index tile_icons with the enum type to get the correct Icon*
  206. with_view_model(
  207. instance->view,
  208. MineSweeperGameScreenModel * model,
  209. {
  210. for (uint16_t i = 0; i < board_tile_count; i++) {
  211. model->board[i].tile_type = tiles[i];
  212. model->board[i].tile_state = MineSweeperGameScreenTileStateUncleared;
  213. model->board[i].icon_element.icon = tile_icons[ tiles[i] ];
  214. model->board[i].icon_element.x_abs = (i/model->board_width);
  215. model->board[i].icon_element.y_abs = (i%model->board_width);
  216. }
  217. model->mines_left = num_mines;
  218. model->flags_left = num_mines;
  219. model->tiles_left = (model->board_width * model->board_height) - model->mines_left;
  220. model->curr_pos.x_abs = 0;
  221. model->curr_pos.y_abs = 0;
  222. model->right_boundary = MINESWEEPER_SCREEN_TILE_WIDTH;
  223. model->bottom_boundary = MINESWEEPER_SCREEN_TILE_HEIGHT;
  224. model->is_making_first_move = true;
  225. },
  226. true
  227. );
  228. }
  229. /**
  230. * This function serves as the verifier for a board to check whether it has to be solved ambiguously or not
  231. *
  232. * Returns true if it is unambiguously solvable.
  233. */
  234. static bool check_board_with_verifier(
  235. MineSweeperTile* board,
  236. const uint8_t board_width,
  237. const uint8_t board_height,
  238. uint16_t total_mines) {
  239. furi_assert(board);
  240. // Double ended queue used to track edges.
  241. point_deq_t deq;
  242. point_set_t visited;
  243. // Ordered Set for visited points
  244. point_deq_init(deq);
  245. point_set_init(visited);
  246. bool is_solvable = false;
  247. // Point_t pos will be used to keep track of the current point
  248. Point_t pos;
  249. pointobj_init(pos);
  250. // Starting position is 0,0
  251. Point start_pos = (Point){.x = 0, .y = 0};
  252. pointobj_set_point(pos, start_pos);
  253. // Initially bfs clear from 0,0 as it is safe. We should push all 'edges' found
  254. // into the deq and this will be where we start off from
  255. bfs_tile_clear_verifier(board, board_width, board_height, 0, 0, &deq, &visited);
  256. //While we have valid edges to check and have not solved the board
  257. while (!is_solvable && point_deq_size(deq) > 0) {
  258. bool is_stuck = true; // This variable will track if any flag was placed for any edge to see if we are stuck
  259. uint16_t deq_size = point_deq_size(deq);
  260. // Iterate through all edge tiles and push new ones on
  261. while (deq_size-- > 0) {
  262. // Pop point and get 1d position in buffer
  263. point_deq_pop_front(&pos, deq);
  264. Point curr_pos = pointobj_get_point(pos);
  265. uint16_t curr_pos_1d = curr_pos.x * board_width + curr_pos.y;
  266. // Get tile at 1d position
  267. MineSweeperTile tile = board[curr_pos_1d];
  268. uint8_t tile_num = tile.tile_type - 1;
  269. // Track total surrounding tiles and flagged tiles
  270. uint8_t num_surrounding_tiles = 0;
  271. uint8_t num_flagged_tiles = 0;
  272. for (uint8_t j = 0; j < 8; j++) {
  273. int16_t dx = curr_pos.x + (int16_t)offsets[j][0];
  274. int16_t dy = curr_pos.y + (int16_t)offsets[j][1];
  275. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  276. continue;
  277. }
  278. uint16_t pos = dx * board_width + dy;
  279. if (board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
  280. num_surrounding_tiles++;
  281. } else if (board[pos].tile_state == MineSweeperGameScreenTileStateFlagged) {
  282. num_surrounding_tiles++;
  283. num_flagged_tiles++;
  284. }
  285. }
  286. if (num_flagged_tiles == tile_num) {
  287. // If the tile has the same number of surrounding flags as its type we bfs clear the uncleared surrounding tiles
  288. // pushing new unvisited edges on deq
  289. for (uint8_t j = 0; j < 8; j++) {
  290. int16_t dx = curr_pos.x + (int16_t)offsets[j][0];
  291. int16_t dy = curr_pos.y + (int16_t)offsets[j][1];
  292. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  293. continue;
  294. }
  295. uint16_t pos = dx * board_width + dy;
  296. if (board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
  297. bfs_tile_clear_verifier(board, board_width, board_height, dx, dy, &deq, &visited);
  298. }
  299. }
  300. is_stuck = false;
  301. } else if (num_surrounding_tiles == tile_num) {
  302. // If the number of surrounding tiles is the tile num it is unambiguous so we place a flag on those tiles,
  303. // decrement the mine count appropriately and check win condition, and then mark stuck as false
  304. for (uint8_t j = 0; j < 8; j++) {
  305. int16_t dx = curr_pos.x + (int16_t)offsets[j][0];
  306. int16_t dy = curr_pos.y + (int16_t)offsets[j][1];
  307. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  308. continue;
  309. }
  310. uint16_t pos = dx * board_width + dy;
  311. if (board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
  312. board[pos].tile_state = MineSweeperGameScreenTileStateFlagged;
  313. }
  314. }
  315. total_mines -= (num_surrounding_tiles - num_flagged_tiles);
  316. if (total_mines == 0) is_solvable = true;
  317. is_stuck = false;
  318. } else if (num_surrounding_tiles != 0) {
  319. // If we have tiles around this position but the number of flagged tiles != tile num
  320. // and the surrounding tiles != tile num this means the tile is ambiguous. We can push
  321. // it back on the deq to be reprocessed with any other new edges
  322. point_deq_push_back(deq, pos);
  323. }
  324. }
  325. // If we are stuck we break as it is an ambiguous map generation
  326. if (is_stuck) {
  327. break;
  328. }
  329. }
  330. point_set_clear(visited);
  331. point_deq_clear(deq);
  332. return is_solvable;
  333. }
  334. /**
  335. * This is a bfs_tile clear used by the verifier which performs the normal tile clear
  336. * but also pushes new edges to the deq passed in. There is a separate function used
  337. * for the bfs_tile_clear used on the user click
  338. */
  339. static inline void bfs_tile_clear_verifier(
  340. MineSweeperTile* board,
  341. const uint8_t board_width,
  342. const uint8_t board_height,
  343. const uint16_t x,
  344. const uint16_t y,
  345. point_deq_t* edges,
  346. point_set_t* visited) {
  347. furi_assert(board);
  348. furi_assert(edges);
  349. furi_assert(visited);
  350. // Init both the set and dequeue
  351. point_deq_t deq;
  352. point_set_t set;
  353. point_deq_init(deq);
  354. point_set_init(set);
  355. // Point_t pos will be used to keep track of the current point
  356. Point_t pos;
  357. pointobj_init(pos);
  358. // Starting position is current pos
  359. Point start_pos = (Point){.x = x, .y = y};
  360. pointobj_set_point(pos, start_pos);
  361. point_deq_push_back(deq, pos);
  362. while (point_deq_size(deq) > 0) {
  363. point_deq_pop_front(&pos, deq);
  364. Point curr_pos = pointobj_get_point(pos);
  365. uint16_t curr_pos_1d = curr_pos.x * board_width + curr_pos.y;
  366. // If in visited set
  367. if (point_set_cget(set, pos) != NULL || point_set_cget(*visited, pos) != NULL) {
  368. }
  369. // If it is cleared continue
  370. if (board[curr_pos_1d].tile_state == MineSweeperGameScreenTileStateCleared) {
  371. continue;
  372. }
  373. // Else set tile to cleared
  374. board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateCleared;
  375. // Add point to visited set
  376. point_set_push(set, pos);
  377. //When we hit a potential edge
  378. if (board[curr_pos_1d].tile_type != MineSweeperGameScreenTileZero) {
  379. // We can push this edge into edges if it is not in visited, as it is a new edge
  380. // and also add to the visited set
  381. if (point_set_cget(*visited, pos) == NULL) {
  382. point_deq_push_back(*edges, pos);
  383. point_set_push(*visited, pos);
  384. }
  385. // Continue processing next point for bfs tile clear
  386. continue;
  387. }
  388. // Process all surrounding neighbors and add valid to dequeue
  389. for (uint8_t i = 0; i < 8; i++) {
  390. int16_t dx = curr_pos.x + (int16_t)offsets[i][0];
  391. int16_t dy = curr_pos.y + (int16_t)offsets[i][1];
  392. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  393. continue;
  394. }
  395. Point neighbor = (Point) {.x = dx, .y = dy};
  396. pointobj_set_point(pos, neighbor);
  397. if (point_set_cget(set, pos) != NULL || point_set_cget(*visited, pos) != NULL) continue;
  398. point_deq_push_back(deq, pos);
  399. }
  400. }
  401. point_set_clear(set);
  402. point_deq_clear(deq);
  403. }
  404. /**
  405. * This is a bfs_tile clear used in the input callbacks to clear the board on user input
  406. */
  407. static inline uint16_t bfs_tile_clear(
  408. MineSweeperTile* board,
  409. const uint8_t board_width,
  410. const uint8_t board_height,
  411. const uint16_t x,
  412. const uint16_t y) {
  413. furi_assert(board);
  414. // We will return this number as the number of tiles cleared
  415. uint16_t ret = 0;
  416. // Init both the set and dequeue
  417. point_deq_t deq;
  418. point_set_t set;
  419. point_deq_init(deq);
  420. point_set_init(set);
  421. // Point_t pos will be used to keep track of the current point
  422. Point_t pos;
  423. pointobj_init(pos);
  424. // Starting position is current pos
  425. Point start_pos = (Point){.x = x, .y = y};
  426. pointobj_set_point(pos, start_pos);
  427. point_deq_push_back(deq, pos);
  428. while (point_deq_size(deq) > 0) {
  429. point_deq_pop_front(&pos, deq);
  430. Point curr_pos = pointobj_get_point(pos);
  431. uint16_t curr_pos_1d = curr_pos.x * board_width + curr_pos.y;
  432. // If in visited set
  433. if (point_set_cget(set, pos) != NULL) {
  434. continue;
  435. }
  436. // If it is cleared continue
  437. if (board[curr_pos_1d].tile_state == MineSweeperGameScreenTileStateCleared) {
  438. continue;
  439. }
  440. // Else set tile to cleared
  441. board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateCleared;
  442. // Increment total number of cleared tiles
  443. ret++;
  444. // Add point to visited set
  445. point_set_push(set, pos);
  446. // If it is not a zero tile continue
  447. if (board[curr_pos_1d].tile_type != MineSweeperGameScreenTileZero) {
  448. continue;
  449. }
  450. // Process all surrounding neighbors and add valid to dequeue
  451. for (uint8_t i = 0; i < 8; i++) {
  452. int16_t dx = curr_pos.x + (int16_t)offsets[i][0];
  453. int16_t dy = curr_pos.y + (int16_t)offsets[i][1];
  454. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  455. continue;
  456. }
  457. Point neighbor = (Point) {.x = dx, .y = dy};
  458. pointobj_set_point(pos, neighbor);
  459. if (point_set_cget(set, pos) != NULL) continue;
  460. point_deq_push_back(deq, pos);
  461. }
  462. }
  463. point_set_clear(set);
  464. point_deq_clear(deq);
  465. return ret;
  466. }
  467. static void mine_sweeper_game_screen_set_board_information(
  468. MineSweeperGameScreen* instance,
  469. uint8_t width,
  470. uint8_t height,
  471. uint8_t difficulty,
  472. bool is_solvable) {
  473. furi_assert(instance);
  474. // These are the min/max values that can actually be set
  475. if (width > 146) {width = 146;}
  476. if (width < 16 ) {width = 16;}
  477. if (height > 64 ) {height = 64;}
  478. if (height < 7 ) {height = 7;}
  479. if (difficulty > 2 ) {difficulty = 2;}
  480. with_view_model(
  481. instance->view,
  482. MineSweeperGameScreenModel * model,
  483. {
  484. model->board_width = width;
  485. model->board_height = height;
  486. model->board_difficulty = difficulty;
  487. model->ensure_solvable_board = is_solvable;
  488. },
  489. true
  490. );
  491. }
  492. // THIS FUNCTION CAN TRIGGER THE LOSE CONDITION
  493. static bool try_clear_surrounding_tiles(MineSweeperGameScreenModel* model) {
  494. furi_assert(model);
  495. uint8_t curr_x = model->curr_pos.x_abs;
  496. uint8_t curr_y = model->curr_pos.y_abs;
  497. uint8_t board_width = model->board_width;
  498. uint8_t board_height = model->board_height;
  499. uint16_t curr_pos_1d = curr_x * board_width + curr_y;
  500. MineSweeperTile tile = model->board[curr_pos_1d];
  501. // Return true if tile is zero tile or not cleared
  502. if (tile.tile_state != MineSweeperGameScreenTileStateCleared || tile.tile_type == MineSweeperGameScreenTileZero) {
  503. return false;
  504. }
  505. uint8_t num_surrounding_flagged = 0;
  506. bool was_mine_found = false;
  507. bool is_lose_condition_triggered = false;
  508. for (uint8_t j = 0; j < 8; j++) {
  509. int16_t dx = curr_x + (int16_t)offsets[j][0];
  510. int16_t dy = curr_y + (int16_t)offsets[j][1];
  511. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  512. continue;
  513. }
  514. uint16_t pos = dx * board_width + dy;
  515. if (model->board[pos].tile_state == MineSweeperGameScreenTileStateFlagged) {
  516. num_surrounding_flagged++;
  517. } else if (!was_mine_found && model->board[pos].tile_type == MineSweeperGameScreenTileMine
  518. && model->board[pos].tile_state != MineSweeperGameScreenTileStateFlagged) {
  519. was_mine_found = true;
  520. }
  521. }
  522. // We clear surrounding tile
  523. if (num_surrounding_flagged >= tile.tile_type-1) {
  524. if (was_mine_found) is_lose_condition_triggered = true;
  525. for (uint8_t j = 0; j < 8; j++) {
  526. int16_t dx = curr_x + (int16_t)offsets[j][0];
  527. int16_t dy = curr_y + (int16_t)offsets[j][1];
  528. if (dx < 0 || dy < 0 || dx >= board_height || dy >= board_width) {
  529. continue;
  530. }
  531. uint16_t pos = dx * board_width + dy;
  532. if (model->board[pos].tile_state == MineSweeperGameScreenTileStateUncleared) {
  533. // Decrement tiles left by the amount cleared
  534. uint16_t tiles_cleared = bfs_tile_clear(model->board, model->board_width, model->board_height, dx, dy);
  535. model->tiles_left -= tiles_cleared;
  536. }
  537. }
  538. }
  539. return is_lose_condition_triggered;
  540. }
  541. /**
  542. * Function is used on a long backpress on a cleared tile and returns the position
  543. * of the first found uncleared tile using a bfs search
  544. */
  545. static inline Point bfs_to_closest_tile(MineSweeperGameScreenModel* model) {
  546. furi_assert(model);
  547. // Init both the set and dequeue
  548. point_deq_t deq;
  549. point_set_t set;
  550. point_deq_init(deq);
  551. point_set_init(set);
  552. // Return the value in this point
  553. Point result;
  554. // Point_t pos will be used to keep track of the current point
  555. Point_t pos;
  556. pointobj_init(pos);
  557. // Starting position is current pos
  558. Point start_pos = (Point){.x = model->curr_pos.x_abs, .y = model->curr_pos.y_abs};
  559. pointobj_set_point(pos, start_pos);
  560. point_deq_push_back(deq, pos);
  561. while (point_deq_size(deq) > 0) {
  562. point_deq_pop_front(&pos, deq);
  563. Point curr_pos = pointobj_get_point(pos);
  564. uint16_t curr_pos_1d = curr_pos.x * model->board_width + curr_pos.y;
  565. // If the current tile is uncleared and not start pos we save result
  566. // to this position and break
  567. if (model->board[curr_pos_1d].tile_state == MineSweeperGameScreenTileStateUncleared &&
  568. !(start_pos.x == curr_pos.x && start_pos.y == curr_pos.y)) {
  569. result = curr_pos;
  570. break;
  571. }
  572. // If in visited set continue
  573. if (point_set_cget(set, pos) != NULL) {
  574. continue;
  575. }
  576. // Add point to visited set
  577. point_set_push(set, pos);
  578. // Process all surrounding neighbors and add valid to dequeue
  579. for (uint8_t i = 0; i < 8; i++) {
  580. int16_t dx = curr_pos.x + (int16_t)offsets[i][0];
  581. int16_t dy = curr_pos.y + (int16_t)offsets[i][1];
  582. if (dx < 0 || dy < 0 || dx >= model->board_height || dy >= model->board_width) {
  583. continue;
  584. }
  585. Point neighbor = (Point) {.x = dx, .y = dy};
  586. pointobj_set_point(pos, neighbor);
  587. point_deq_push_back(deq, pos);
  588. }
  589. }
  590. point_set_clear(set);
  591. point_deq_clear(deq);
  592. return result;
  593. }
  594. static void mine_sweeper_game_screen_view_enter(void* context) {
  595. furi_assert(context);
  596. UNUSED(context);
  597. }
  598. static void mine_sweeper_game_screen_view_exit(void* context) {
  599. furi_assert(context);
  600. UNUSED(context);
  601. }
  602. static void mine_sweeper_game_screen_view_win_draw_callback(Canvas* canvas, void* _model) {
  603. furi_assert(canvas);
  604. furi_assert(_model);
  605. MineSweeperGameScreenModel* model = _model;
  606. canvas_clear(canvas);
  607. canvas_set_color(canvas, ColorBlack);
  608. uint16_t cursor_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
  609. for (uint8_t x_rel = 0; x_rel < MINESWEEPER_SCREEN_TILE_HEIGHT; x_rel++) {
  610. uint16_t x_abs = (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) + x_rel;
  611. for (uint8_t y_rel = 0; y_rel < MINESWEEPER_SCREEN_TILE_WIDTH; y_rel++) {
  612. uint16_t y_abs = (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) + y_rel;
  613. uint16_t curr_rendering_tile_pos_1d = x_abs * model->board_width + y_abs;
  614. MineSweeperTile tile = model->board[curr_rendering_tile_pos_1d];
  615. if (cursor_pos_1d == curr_rendering_tile_pos_1d) {
  616. canvas_set_color(canvas, ColorWhite);
  617. } else {
  618. canvas_set_color(canvas, ColorBlack);
  619. }
  620. canvas_draw_icon(
  621. canvas,
  622. y_rel * icon_get_width(tile.icon_element.icon),
  623. x_rel * icon_get_height(tile.icon_element.icon),
  624. tile.icon_element.icon);
  625. }
  626. }
  627. canvas_set_color(canvas, ColorBlack);
  628. // If any borders are at the limits of the game board we draw a border line
  629. // Right border
  630. if (model->right_boundary == model->board_width) {
  631. canvas_draw_line(canvas, 127,0,127,63-8);
  632. }
  633. // Left border
  634. if ((model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) == 0) {
  635. canvas_draw_line(canvas, 0,0,0,63-8);
  636. }
  637. // Bottom border
  638. if (model->bottom_boundary == model->board_height) {
  639. canvas_draw_line(canvas, 0,63-8,127,63-8);
  640. }
  641. // Top border
  642. if ((model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) == 0) {
  643. canvas_draw_line(canvas, 0,0,127,0);
  644. }
  645. // Draw win text
  646. furi_string_printf(
  647. model->info_str,
  648. "YOU WIN!");
  649. canvas_draw_str_aligned(
  650. canvas,
  651. 0,
  652. 64-7,
  653. AlignLeft,
  654. AlignTop,
  655. furi_string_get_cstr(model->info_str));
  656. // Draw time text
  657. uint32_t ticks_elapsed = furi_get_tick() - model->start_tick;
  658. uint32_t sec = ticks_elapsed / furi_kernel_get_tick_frequency();
  659. uint32_t minutes = sec / 60;
  660. sec = sec % 60;
  661. furi_string_printf(
  662. model->info_str,
  663. "%02ld:%02ld",
  664. minutes,
  665. sec);
  666. canvas_draw_str_aligned(
  667. canvas,
  668. 126 - canvas_string_width(canvas, furi_string_get_cstr(model->info_str)),
  669. 64 - 7,
  670. AlignLeft,
  671. AlignTop,
  672. furi_string_get_cstr(model->info_str));
  673. }
  674. static void mine_sweeper_game_screen_view_lose_draw_callback(Canvas* canvas, void* _model) {
  675. furi_assert(canvas);
  676. furi_assert(_model);
  677. MineSweeperGameScreenModel* model = _model;
  678. canvas_clear(canvas);
  679. uint16_t cursor_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
  680. for (uint8_t x_rel = 0; x_rel < MINESWEEPER_SCREEN_TILE_HEIGHT; x_rel++) {
  681. uint16_t x_abs = (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) + x_rel;
  682. for (uint8_t y_rel = 0; y_rel < MINESWEEPER_SCREEN_TILE_WIDTH; y_rel++) {
  683. uint16_t y_abs = (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) + y_rel;
  684. uint16_t curr_rendering_tile_pos_1d = x_abs * model->board_width + y_abs;
  685. MineSweeperTile tile = model->board[curr_rendering_tile_pos_1d];
  686. if (cursor_pos_1d == curr_rendering_tile_pos_1d) {
  687. canvas_set_color(canvas, ColorWhite);
  688. } else {
  689. canvas_set_color(canvas, ColorBlack);
  690. }
  691. canvas_draw_icon(
  692. canvas,
  693. y_rel * icon_get_width(tile.icon_element.icon),
  694. x_rel * icon_get_height(tile.icon_element.icon),
  695. tile.icon_element.icon);
  696. }
  697. }
  698. canvas_set_color(canvas, ColorBlack);
  699. // If any borders are at the limits of the game board we draw a border line
  700. // Right border
  701. if (model->right_boundary == model->board_width) {
  702. canvas_draw_line(canvas, 127,0,127,63-8);
  703. }
  704. // Left border
  705. if ((model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) == 0) {
  706. canvas_draw_line(canvas, 0,0,0,63-8);
  707. }
  708. // Bottom border
  709. if (model->bottom_boundary == model->board_height) {
  710. canvas_draw_line(canvas, 0,63-8,127,63-8);
  711. }
  712. // Top border
  713. if ((model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) == 0) {
  714. canvas_draw_line(canvas, 0,0,127,0);
  715. }
  716. // Draw lose text
  717. furi_string_printf(
  718. model->info_str,
  719. "YOU LOSE!");
  720. canvas_draw_str_aligned(
  721. canvas,
  722. 0,
  723. 64-7,
  724. AlignLeft,
  725. AlignTop,
  726. furi_string_get_cstr(model->info_str));
  727. // Draw time text
  728. uint32_t ticks_elapsed = furi_get_tick() - model->start_tick;
  729. uint32_t sec = ticks_elapsed / furi_kernel_get_tick_frequency();
  730. uint32_t minutes = sec / 60;
  731. sec = sec % 60;
  732. furi_string_printf(
  733. model->info_str,
  734. "%02ld:%02ld",
  735. minutes,
  736. sec);
  737. canvas_draw_str_aligned(
  738. canvas,
  739. 126 - canvas_string_width(canvas, furi_string_get_cstr(model->info_str)),
  740. 64 - 7,
  741. AlignLeft,
  742. AlignTop,
  743. furi_string_get_cstr(model->info_str));
  744. }
  745. static void mine_sweeper_game_screen_view_play_draw_callback(Canvas* canvas, void* _model) {
  746. furi_assert(canvas);
  747. furi_assert(_model);
  748. MineSweeperGameScreenModel* model = _model;
  749. canvas_clear(canvas);
  750. uint16_t cursor_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
  751. for (uint8_t x_rel = 0; x_rel < MINESWEEPER_SCREEN_TILE_HEIGHT; x_rel++) {
  752. uint16_t x_abs = (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) + x_rel;
  753. for (uint8_t y_rel = 0; y_rel < MINESWEEPER_SCREEN_TILE_WIDTH; y_rel++) {
  754. uint16_t y_abs = (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) + y_rel;
  755. uint16_t curr_rendering_tile_pos_1d = x_abs * model->board_width + y_abs;
  756. MineSweeperTile tile = model->board[curr_rendering_tile_pos_1d];
  757. if (cursor_pos_1d == curr_rendering_tile_pos_1d) {
  758. canvas_set_color(canvas, ColorWhite);
  759. } else {
  760. canvas_set_color(canvas, ColorBlack);
  761. }
  762. switch (tile.tile_state) {
  763. case MineSweeperGameScreenTileStateFlagged :
  764. canvas_draw_icon(
  765. canvas,
  766. y_rel * icon_get_width(tile.icon_element.icon),
  767. x_rel * icon_get_height(tile.icon_element.icon),
  768. tile_icons[11]);
  769. break;
  770. case MineSweeperGameScreenTileStateUncleared :
  771. canvas_draw_icon(
  772. canvas,
  773. y_rel * icon_get_width(tile.icon_element.icon),
  774. x_rel * icon_get_height(tile.icon_element.icon),
  775. tile_icons[12]);
  776. break;
  777. case MineSweeperGameScreenTileStateCleared :
  778. canvas_draw_icon(
  779. canvas,
  780. y_rel * icon_get_width(tile.icon_element.icon),
  781. x_rel * icon_get_height(tile.icon_element.icon),
  782. tile.icon_element.icon);
  783. break;
  784. default:
  785. break;
  786. }
  787. }
  788. }
  789. canvas_set_color(canvas, ColorBlack);
  790. // If any borders are at the limits of the game board we draw a border line
  791. // Right border
  792. if (model->right_boundary == model->board_width) {
  793. canvas_draw_line(canvas, 127,0,127,63-8);
  794. }
  795. // Left border
  796. if ((model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH) == 0) {
  797. canvas_draw_line(canvas, 0,0,0,63-8);
  798. }
  799. // Bottom border
  800. if (model->bottom_boundary == model->board_height) {
  801. canvas_draw_line(canvas, 0,63-8,127,63-8);
  802. }
  803. // Top border
  804. if ((model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT) == 0) {
  805. canvas_draw_line(canvas, 0,0,127,0);
  806. }
  807. // Draw X Position Text
  808. furi_string_printf(
  809. model->info_str,
  810. "X:%03hhd",
  811. model->curr_pos.x_abs);
  812. canvas_draw_str_aligned(
  813. canvas,
  814. 0,
  815. 64-7,
  816. AlignLeft,
  817. AlignTop,
  818. furi_string_get_cstr(model->info_str));
  819. // Draw Y Position Text
  820. furi_string_printf(
  821. model->info_str,
  822. "Y:%03hhd",
  823. model->curr_pos.y_abs);
  824. canvas_draw_str_aligned(
  825. canvas,
  826. 33,
  827. 64-7,
  828. AlignLeft,
  829. AlignTop,
  830. furi_string_get_cstr(model->info_str));
  831. // Draw flag text
  832. furi_string_printf(
  833. model->info_str,
  834. "F:%03hd",
  835. model->flags_left);
  836. canvas_draw_str_aligned(
  837. canvas,
  838. 66,
  839. 64 - 7,
  840. AlignLeft,
  841. AlignTop,
  842. furi_string_get_cstr(model->info_str));
  843. // Draw time text
  844. uint32_t ticks_elapsed = furi_get_tick() - model->start_tick;
  845. uint32_t sec = ticks_elapsed / furi_kernel_get_tick_frequency();
  846. uint32_t minutes = sec / 60;
  847. sec = sec % 60;
  848. furi_string_printf(
  849. model->info_str,
  850. "%02ld:%02ld",
  851. minutes,
  852. sec);
  853. canvas_draw_str_aligned(
  854. canvas,
  855. 126 - canvas_string_width(canvas, furi_string_get_cstr(model->info_str)),
  856. 64 - 7,
  857. AlignLeft,
  858. AlignTop,
  859. furi_string_get_cstr(model->info_str));
  860. }
  861. static bool mine_sweeper_game_screen_view_end_input_callback(InputEvent* event, void* context) {
  862. furi_assert(context);
  863. furi_assert(event);
  864. MineSweeperGameScreen* instance = context;
  865. bool consumed = false;
  866. with_view_model(
  867. instance->view,
  868. MineSweeperGameScreenModel * model,
  869. {
  870. if (event->type == InputTypeRelease) {
  871. model->is_holding_down_button = false;
  872. consumed = true;
  873. } else if (!model->is_holding_down_button && (event->type == InputTypePress || event->type == InputTypeRepeat)) {
  874. bool is_outside_boundary;
  875. switch (event->key) {
  876. case InputKeyUp :
  877. model->curr_pos.x_abs = (model->curr_pos.x_abs-1 < 0) ? 0 : model->curr_pos.x_abs-1;
  878. is_outside_boundary = model->curr_pos.x_abs <
  879. (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT);
  880. if (is_outside_boundary) {
  881. model->bottom_boundary--;
  882. }
  883. consumed = true;
  884. break;
  885. case InputKeyDown :
  886. model->curr_pos.x_abs = (model->curr_pos.x_abs+1 >= model->board_height) ?
  887. model->board_height-1 : model->curr_pos.x_abs+1;
  888. is_outside_boundary = model->curr_pos.x_abs >= model->bottom_boundary;
  889. if (is_outside_boundary) {
  890. model->bottom_boundary++;
  891. }
  892. consumed = true;
  893. break;
  894. case InputKeyLeft :
  895. model->curr_pos.y_abs = (model->curr_pos.y_abs-1 < 0) ? 0 : model->curr_pos.y_abs-1;
  896. is_outside_boundary = model->curr_pos.y_abs <
  897. (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH);
  898. if (is_outside_boundary) {
  899. model->right_boundary--;
  900. }
  901. consumed = true;
  902. break;
  903. case InputKeyRight :
  904. model->curr_pos.y_abs = (model->curr_pos.y_abs+1 >= model->board_width) ?
  905. model->board_width-1 : model->curr_pos.y_abs+1;
  906. is_outside_boundary = model->curr_pos.y_abs >= model->right_boundary;
  907. if (is_outside_boundary) {
  908. model->right_boundary++;
  909. }
  910. consumed = true;
  911. break;
  912. default: // Anything other than movement around the screen should restart game
  913. mine_sweeper_game_screen_reset_clock(instance);
  914. view_set_draw_callback(
  915. instance->view,
  916. mine_sweeper_game_screen_view_play_draw_callback);
  917. view_set_input_callback(
  918. instance->view,
  919. mine_sweeper_game_screen_view_play_input_callback);
  920. // Here we are going to generate a valid map for the player
  921. bool is_valid_board = false;
  922. size_t memsz = sizeof(MineSweeperTile) * MINESWEEPER_BOARD_MAX_TILES;
  923. do {
  924. setup_board(instance);
  925. memset(board_t, 0, memsz);
  926. memcpy(board_t, model->board, sizeof(MineSweeperTile) * (model->board_width * model->board_height));
  927. is_valid_board = check_board_with_verifier(
  928. board_t,
  929. model->board_width,
  930. model->board_height,
  931. model->mines_left);
  932. } while (model->ensure_solvable_board && !is_valid_board);
  933. consumed = true;
  934. break;
  935. }
  936. consumed = true;
  937. }
  938. },
  939. false
  940. );
  941. return consumed;
  942. }
  943. static bool mine_sweeper_game_screen_view_play_input_callback(InputEvent* event, void* context) {
  944. furi_assert(context);
  945. furi_assert(event);
  946. MineSweeperGameScreen* instance = context;
  947. bool consumed = false;
  948. // Checking button types
  949. if (event->key == InputKeyOk) { // Attempt to Clear Space !! THIS CAN BE A LOSE CONDITION
  950. bool is_lose_condition_triggered = false;
  951. bool is_win_condition_triggered = false;
  952. with_view_model(
  953. instance->view,
  954. MineSweeperGameScreenModel * model,
  955. {
  956. if (event->type == InputTypeRelease) {
  957. model->is_holding_down_button = false;
  958. } else if (!model->is_holding_down_button && event->type == InputTypePress) {
  959. uint16_t curr_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
  960. MineSweeperGameScreenTileState state = model->board[curr_pos_1d].tile_state;
  961. MineSweeperGameScreenTileType type = model->board[curr_pos_1d].tile_type;
  962. // LOSE/WIN CONDITION OR TILE CLEAR
  963. if (state == MineSweeperGameScreenTileStateUncleared && type == MineSweeperGameScreenTileMine) {
  964. is_lose_condition_triggered = true;
  965. model->board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateCleared;
  966. } else if (state == MineSweeperGameScreenTileStateUncleared) {
  967. uint16_t tiles_cleared = bfs_tile_clear(
  968. model->board,
  969. model->board_width,
  970. model->board_height,
  971. (uint16_t)model->curr_pos.x_abs,
  972. (uint16_t)model->curr_pos.y_abs);
  973. model->tiles_left -= tiles_cleared;
  974. // Check win condition
  975. if (model->mines_left == 0 && model->flags_left == 0 && model->tiles_left == 0) {
  976. is_win_condition_triggered = true;
  977. }
  978. }
  979. // LOSE/WIN CONDITION OR CLEAR SURROUNDING
  980. } else if (!model->is_holding_down_button && event->type == InputTypeLong) {
  981. // Try to clear surrounding tiles if correct number is flagged.
  982. is_lose_condition_triggered = try_clear_surrounding_tiles(model);
  983. model->is_holding_down_button = true;
  984. // Check win condition
  985. if (model->mines_left == 0 && model->flags_left == 0 && model->tiles_left == 0) {
  986. is_win_condition_triggered = true;
  987. }
  988. }
  989. },
  990. true
  991. );
  992. // Check if win or lose condition was triggered on OK press
  993. if (is_lose_condition_triggered) {
  994. view_set_draw_callback(instance->view, mine_sweeper_game_screen_view_lose_draw_callback);
  995. view_set_input_callback(instance->view, mine_sweeper_game_screen_view_end_input_callback);
  996. } else if (is_win_condition_triggered) {
  997. view_set_draw_callback(instance->view, mine_sweeper_game_screen_view_win_draw_callback);
  998. view_set_input_callback(instance->view, mine_sweeper_game_screen_view_end_input_callback);
  999. }
  1000. consumed = true;
  1001. }
  1002. if (!consumed && (event->key == InputKeyBack)) { // We can use holding the back button for either
  1003. // Setting a flag on a covered tile, or moving to
  1004. // the next closest covered tile on when on a uncovered
  1005. // tile
  1006. if (event->type == InputTypeRelease) {
  1007. with_view_model(
  1008. instance->view,
  1009. MineSweeperGameScreenModel * model,
  1010. {
  1011. model->is_holding_down_button = false;
  1012. },
  1013. true
  1014. );
  1015. consumed = true;
  1016. } else if (event->type == InputTypeLong || event->type == InputTypeRepeat) { // Only process longer back keys;
  1017. // short presses should take
  1018. // us to the menu
  1019. with_view_model(
  1020. instance->view,
  1021. MineSweeperGameScreenModel * model,
  1022. {
  1023. uint16_t curr_pos_1d = model->curr_pos.x_abs * model->board_width + model->curr_pos.y_abs;
  1024. MineSweeperGameScreenTileState state = model->board[curr_pos_1d].tile_state;
  1025. if (state == MineSweeperGameScreenTileStateCleared) {
  1026. // BFS to closest uncovered position
  1027. Point res = bfs_to_closest_tile(model);
  1028. // Save cursor to new closest tile position
  1029. // If the cursor moves outisde of the model boundaries we need to
  1030. // move the boundary appropriately
  1031. model->curr_pos.x_abs = res.x;
  1032. model->curr_pos.y_abs = res.y;
  1033. bool is_outside_top_boundary = model->curr_pos.x_abs <
  1034. (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT);
  1035. bool is_outside_bottom_boundary = model->curr_pos.x_abs >=
  1036. model->bottom_boundary;
  1037. bool is_outside_left_boundary = model->curr_pos.y_abs <
  1038. (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH);
  1039. bool is_outside_right_boundary = model->curr_pos.y_abs >=
  1040. model->right_boundary;
  1041. if (is_outside_top_boundary) {
  1042. model->bottom_boundary = model->curr_pos.x_abs + MINESWEEPER_SCREEN_TILE_HEIGHT;
  1043. } else if (is_outside_bottom_boundary) {
  1044. model->bottom_boundary = model->curr_pos.x_abs+1;
  1045. }
  1046. if (is_outside_right_boundary) {
  1047. model->right_boundary = model->curr_pos.y_abs+1;
  1048. } else if (is_outside_left_boundary) {
  1049. model->right_boundary = model->curr_pos.y_abs + MINESWEEPER_SCREEN_TILE_WIDTH;
  1050. }
  1051. model->is_holding_down_button = true;
  1052. // Flag or Unflag tile and check win condition
  1053. } else if (!model->is_holding_down_button && (state == MineSweeperGameScreenTileStateUncleared || state == MineSweeperGameScreenTileStateFlagged)) {
  1054. if (state == MineSweeperGameScreenTileStateFlagged) {
  1055. if (model->board[curr_pos_1d].tile_type == MineSweeperGameScreenTileMine) model->mines_left++;
  1056. model->board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateUncleared;
  1057. model->flags_left++;
  1058. model->is_holding_down_button = true;
  1059. } else if (model->flags_left > 0) {
  1060. if (model->board[curr_pos_1d].tile_type == MineSweeperGameScreenTileMine) model->mines_left--;
  1061. model->board[curr_pos_1d].tile_state = MineSweeperGameScreenTileStateFlagged;
  1062. model->flags_left--;
  1063. model->is_holding_down_button = true;
  1064. }
  1065. // WIN CONDITION
  1066. // This can be a win condition where the non-mine tiles are cleared and they place the last flag
  1067. if (model->flags_left == 0 && model->mines_left == 0 && model->tiles_left == 0) {
  1068. view_set_draw_callback(instance->view, mine_sweeper_game_screen_view_win_draw_callback);
  1069. view_set_input_callback(instance->view, mine_sweeper_game_screen_view_end_input_callback);
  1070. }
  1071. }
  1072. },
  1073. false
  1074. );
  1075. consumed = true;
  1076. }
  1077. }
  1078. if (!consumed && (event->type == InputTypePress || event->type == InputTypeRepeat)) { // Finally handle move
  1079. with_view_model(
  1080. instance->view,
  1081. MineSweeperGameScreenModel * model,
  1082. {
  1083. bool is_outside_boundary;
  1084. switch (event->key) {
  1085. case InputKeyUp :
  1086. model->curr_pos.x_abs = (model->curr_pos.x_abs-1 < 0) ? 0 : model->curr_pos.x_abs-1;
  1087. is_outside_boundary = model->curr_pos.x_abs <
  1088. (model->bottom_boundary - MINESWEEPER_SCREEN_TILE_HEIGHT);
  1089. if (is_outside_boundary) {
  1090. model->bottom_boundary--;
  1091. }
  1092. consumed = true;
  1093. break;
  1094. case InputKeyDown :
  1095. model->curr_pos.x_abs = (model->curr_pos.x_abs+1 >= model->board_height) ?
  1096. model->board_height-1 : model->curr_pos.x_abs+1;
  1097. is_outside_boundary = model->curr_pos.x_abs >= model->bottom_boundary;
  1098. if (is_outside_boundary) {
  1099. model->bottom_boundary++;
  1100. }
  1101. consumed = true;
  1102. break;
  1103. case InputKeyLeft :
  1104. model->curr_pos.y_abs = (model->curr_pos.y_abs-1 < 0) ? 0 : model->curr_pos.y_abs-1;
  1105. is_outside_boundary = model->curr_pos.y_abs <
  1106. (model->right_boundary - MINESWEEPER_SCREEN_TILE_WIDTH);
  1107. if (is_outside_boundary) {
  1108. model->right_boundary--;
  1109. }
  1110. consumed = true;
  1111. break;
  1112. case InputKeyRight :
  1113. model->curr_pos.y_abs = (model->curr_pos.y_abs+1 >= model->board_width) ?
  1114. model->board_width-1 : model->curr_pos.y_abs+1;
  1115. is_outside_boundary = model->curr_pos.y_abs >= model->right_boundary;
  1116. if (is_outside_boundary) {
  1117. model->right_boundary++;
  1118. }
  1119. consumed = true;
  1120. break;
  1121. default:
  1122. consumed = true;
  1123. break;
  1124. }
  1125. },
  1126. true
  1127. );
  1128. }
  1129. if (!consumed && instance->input_callback != NULL) {
  1130. consumed = instance->input_callback(event, instance->context);
  1131. }
  1132. return consumed;
  1133. }
  1134. MineSweeperGameScreen* mine_sweeper_game_screen_alloc(uint8_t width, uint8_t height, uint8_t difficulty, bool ensure_solvable) {
  1135. MineSweeperGameScreen* mine_sweeper_game_screen = (MineSweeperGameScreen*)malloc(sizeof(MineSweeperGameScreen));
  1136. mine_sweeper_game_screen->view = view_alloc();
  1137. view_set_context(mine_sweeper_game_screen->view, mine_sweeper_game_screen);
  1138. view_allocate_model(mine_sweeper_game_screen->view, ViewModelTypeLocking, sizeof(MineSweeperGameScreenModel));
  1139. view_set_draw_callback(mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_play_draw_callback);
  1140. view_set_input_callback(mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_play_input_callback);
  1141. // This are currently unused
  1142. view_set_enter_callback(mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_enter);
  1143. view_set_exit_callback(mine_sweeper_game_screen->view, mine_sweeper_game_screen_view_exit);
  1144. // Not being used
  1145. mine_sweeper_game_screen->input_callback = NULL;
  1146. // Allocate strings in model
  1147. with_view_model(
  1148. mine_sweeper_game_screen->view,
  1149. MineSweeperGameScreenModel * model,
  1150. {
  1151. model->info_str = furi_string_alloc();
  1152. model->is_holding_down_button = false;
  1153. },
  1154. true
  1155. );
  1156. // Reset the clock - This will set the start time at the allocation of the game screen
  1157. // but this is a public api as well and can be called in a scene for more accurate start times
  1158. mine_sweeper_game_screen_reset_clock(mine_sweeper_game_screen);
  1159. // We need to initize board width and height before setup
  1160. mine_sweeper_game_screen_set_board_information(mine_sweeper_game_screen, width, height, difficulty, ensure_solvable);
  1161. // Here we are going to generate a valid map for the player
  1162. bool is_valid_board = false;
  1163. size_t memsz = sizeof(MineSweeperTile) * MINESWEEPER_BOARD_MAX_TILES;
  1164. do {
  1165. setup_board(mine_sweeper_game_screen);
  1166. uint16_t num_mines = 1;
  1167. uint16_t board_width = 16; //default values
  1168. uint16_t board_height = 7; //default values
  1169. with_view_model(
  1170. mine_sweeper_game_screen->view,
  1171. MineSweeperGameScreenModel * model,
  1172. {
  1173. num_mines = model->mines_left;
  1174. board_width = model->board_width;
  1175. board_height = model->board_height;
  1176. memset(board_t, 0, memsz);
  1177. memcpy(board_t, model->board, sizeof(MineSweeperTile) * (board_width * board_height));
  1178. },
  1179. true
  1180. );
  1181. is_valid_board = check_board_with_verifier(board_t, board_width, board_height, num_mines);
  1182. } while (ensure_solvable && !is_valid_board);
  1183. return mine_sweeper_game_screen;
  1184. }
  1185. void mine_sweeper_game_screen_free(MineSweeperGameScreen* instance) {
  1186. furi_assert(instance);
  1187. // Dealloc strings in model
  1188. with_view_model(
  1189. instance->view,
  1190. MineSweeperGameScreenModel * model,
  1191. {
  1192. furi_string_free(model->info_str);
  1193. },
  1194. false
  1195. );
  1196. // Free view and any dynamically allocated members in main struct
  1197. view_free(instance->view);
  1198. free(instance);
  1199. }
  1200. // This function should be called whenever you want to reset the game state
  1201. // This should NOT be called in the on_exit in the game scene
  1202. void mine_sweeper_game_screen_reset(MineSweeperGameScreen* instance, uint8_t width, uint8_t height, uint8_t difficulty, bool ensure_solvable) {
  1203. furi_assert(instance);
  1204. instance->input_callback = NULL;
  1205. // We need to initize board width and height before setup
  1206. mine_sweeper_game_screen_set_board_information(instance, width, height, difficulty, ensure_solvable);
  1207. mine_sweeper_game_screen_reset_clock(instance);
  1208. // Here we are going to generate a valid map for the player
  1209. bool is_valid_board = false;
  1210. size_t memsz = sizeof(MineSweeperTile) * MINESWEEPER_BOARD_MAX_TILES;
  1211. do {
  1212. setup_board(instance);
  1213. uint16_t num_mines = 1;
  1214. uint16_t board_width = 16; //default values
  1215. uint16_t board_height = 7; //default values
  1216. with_view_model(
  1217. instance->view,
  1218. MineSweeperGameScreenModel * model,
  1219. {
  1220. num_mines = model->mines_left;
  1221. board_width = model->board_width;
  1222. board_height = model->board_height;
  1223. memset(board_t, 0, memsz);
  1224. memcpy(board_t, model->board, sizeof(MineSweeperTile) * (board_width * board_height));
  1225. },
  1226. true
  1227. );
  1228. is_valid_board = check_board_with_verifier(board_t, board_width, board_height, num_mines);
  1229. } while (ensure_solvable && !is_valid_board);
  1230. }
  1231. // This function should be called when you want to reset the game clock
  1232. // Already called in reset and alloc function for game, but can be called from
  1233. // other scenes that need it like a start scene that plays after alloc
  1234. void mine_sweeper_game_screen_reset_clock(MineSweeperGameScreen* instance) {
  1235. furi_assert(instance);
  1236. with_view_model(
  1237. instance->view,
  1238. MineSweeperGameScreenModel * model,
  1239. {
  1240. model->start_tick = furi_get_tick();
  1241. },
  1242. true
  1243. );
  1244. }
  1245. View* mine_sweeper_game_screen_get_view(MineSweeperGameScreen* instance) {
  1246. furi_assert(instance);
  1247. return instance->view;
  1248. }
  1249. void mine_sweeper_game_screen_set_context(MineSweeperGameScreen* instance, void* context) {
  1250. furi_assert(instance);
  1251. instance->context = context;
  1252. }