minesweeper_game_screen.c 60 KB

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