minesweeper_game_screen.c 58 KB

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