minesweeper_game_screen.c 56 KB

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