minesweeper_game_screen.c 54 KB

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