snake_20.c 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. #include <furi.h>
  2. #include <gui/gui.h>
  3. #include <input/input.h>
  4. #include <stdlib.h>
  5. #include <dolphin/dolphin.h>
  6. #include <notification/notification.h>
  7. #include <notification/notification_messages.h>
  8. typedef struct {
  9. // +-----x
  10. // |
  11. // |
  12. // y
  13. uint8_t x;
  14. uint8_t y;
  15. } Point;
  16. typedef enum {
  17. GameStateLife,
  18. GameStatePause,
  19. // https://melmagazine.com/en-us/story/snake-nokia-6110-oral-history-taneli-armanto
  20. // Armanto: While testing the early versions of the game, I noticed it was hard
  21. // to control the snake upon getting close to and edge but not crashing — especially
  22. // in the highest speed levels. I wanted the highest level to be as fast as I could
  23. // possibly make the device "run," but on the other hand, I wanted to be friendly
  24. // and help the player manage that level. Otherwise it might not be fun to play. So
  25. // I implemented a little delay. A few milliseconds of extra time right before
  26. // the player crashes, during which she can still change the directions. And if
  27. // she does, the game continues.
  28. GameStateLastChance,
  29. GameStateGameOver,
  30. } GameState;
  31. // Note: do not change without purpose. Current values are used in smart
  32. // orthogonality calculation in `snake_game_get_turn_snake`.
  33. typedef enum {
  34. DirectionUp,
  35. DirectionRight,
  36. DirectionDown,
  37. DirectionLeft,
  38. } Direction;
  39. #define MAX_SNAKE_LEN 15 * 31 //128 * 64 / 4
  40. #define x_back_symbol 50
  41. #define y_back_symbol 9
  42. typedef struct {
  43. FuriMutex* mutex;
  44. Point points[MAX_SNAKE_LEN];
  45. uint16_t len;
  46. Direction currentMovement;
  47. Direction nextMovement; // if backward of currentMovement, ignore
  48. Point fruit;
  49. GameState state;
  50. } SnakeState;
  51. typedef enum {
  52. EventTypeTick,
  53. EventTypeKey,
  54. } EventType;
  55. typedef struct {
  56. EventType type;
  57. InputEvent input;
  58. } SnakeEvent;
  59. const NotificationSequence sequence_fail = {
  60. &message_vibro_on,
  61. &message_note_ds4,
  62. &message_delay_10,
  63. &message_sound_off,
  64. &message_delay_10,
  65. &message_note_ds4,
  66. &message_delay_10,
  67. &message_sound_off,
  68. &message_delay_10,
  69. &message_note_ds4,
  70. &message_delay_10,
  71. &message_sound_off,
  72. &message_delay_10,
  73. &message_vibro_off,
  74. NULL,
  75. };
  76. const NotificationSequence sequence_eat = {
  77. &message_vibro_on,
  78. &message_note_c7,
  79. &message_delay_50,
  80. &message_sound_off,
  81. &message_vibro_off,
  82. NULL,
  83. };
  84. static void snake_game_render_callback(Canvas* const canvas, void* ctx) {
  85. furi_assert(ctx);
  86. const SnakeState* snake_state = ctx;
  87. furi_mutex_acquire(snake_state->mutex, FuriWaitForever);
  88. // Before the function is called, the state is set with the canvas_reset(canvas)
  89. // Frame
  90. canvas_draw_frame(canvas, 0, 0, 128, 64);
  91. // Fruit
  92. Point f = snake_state->fruit;
  93. f.x = f.x * 4 + 1;
  94. f.y = f.y * 4 + 1;
  95. canvas_draw_rframe(canvas, f.x, f.y, 6, 6, 2);
  96. canvas_draw_dot(canvas, f.x + 3, f.y - 1);
  97. canvas_draw_dot(canvas, f.x + 4, f.y - 2);
  98. //canvas_draw_dot(canvas,f.x+4,f.y-3);
  99. // Snake
  100. for(uint16_t i = 0; i < snake_state->len; i++) {
  101. Point p = snake_state->points[i];
  102. p.x = p.x * 4 + 2;
  103. p.y = p.y * 4 + 2;
  104. canvas_draw_box(canvas, p.x, p.y, 4, 4);
  105. if(i == 0) {
  106. canvas_set_color(canvas, ColorWhite);
  107. canvas_draw_box(canvas, p.x + 1, p.y + 1, 2, 2);
  108. canvas_set_color(canvas, ColorBlack);
  109. }
  110. }
  111. // Pause and GameOver banner
  112. if(snake_state->state == GameStatePause || snake_state->state == GameStateGameOver) {
  113. // Screen is 128x64 px
  114. canvas_set_color(canvas, ColorWhite);
  115. canvas_draw_box(canvas, 33, 19, 64, 26);
  116. canvas_set_color(canvas, ColorBlack);
  117. canvas_draw_frame(canvas, 34, 20, 62, 24);
  118. canvas_set_font(canvas, FontPrimary);
  119. if(snake_state->state == GameStateGameOver) {
  120. canvas_draw_str_aligned(canvas, 65, 31, AlignCenter, AlignBottom, "Game Over");
  121. }
  122. if(snake_state->state == GameStatePause) {
  123. canvas_draw_str_aligned(canvas, 65, 31, AlignCenter, AlignBottom, "Pause");
  124. }
  125. canvas_set_font(canvas, FontSecondary);
  126. char buffer[20];
  127. snprintf(buffer, sizeof(buffer), "Score: %u", snake_state->len - 7U);
  128. canvas_draw_str_aligned(canvas, 65, 41, AlignCenter, AlignBottom, buffer);
  129. // Painting "back"-symbol, Help message for Exit App, ProgressBar
  130. canvas_set_color(canvas, ColorWhite);
  131. canvas_draw_box(canvas, 25, 2, 81, 11);
  132. canvas_draw_box(canvas, 28, 54, 73, 9);
  133. canvas_set_color(canvas, ColorBlack);
  134. canvas_draw_str_aligned(
  135. canvas, 65, 10, AlignCenter, AlignBottom, "Hold to Exit App");
  136. snprintf(
  137. buffer, sizeof(buffer), "Complete: %-5.1f%%", (double)((snake_state->len - 7U) / 4.58));
  138. canvas_draw_str_aligned(canvas, 65, 62, AlignCenter, AlignBottom, buffer);
  139. {
  140. canvas_draw_dot(canvas, x_back_symbol + 0, y_back_symbol);
  141. canvas_draw_dot(canvas, x_back_symbol + 1, y_back_symbol);
  142. canvas_draw_dot(canvas, x_back_symbol + 2, y_back_symbol);
  143. canvas_draw_dot(canvas, x_back_symbol + 3, y_back_symbol);
  144. canvas_draw_dot(canvas, x_back_symbol + 4, y_back_symbol);
  145. canvas_draw_dot(canvas, x_back_symbol + 5, y_back_symbol - 1);
  146. canvas_draw_dot(canvas, x_back_symbol + 6, y_back_symbol - 2);
  147. canvas_draw_dot(canvas, x_back_symbol + 6, y_back_symbol - 3);
  148. canvas_draw_dot(canvas, x_back_symbol + 5, y_back_symbol - 4);
  149. canvas_draw_dot(canvas, x_back_symbol + 4, y_back_symbol - 5);
  150. canvas_draw_dot(canvas, x_back_symbol + 3, y_back_symbol - 5);
  151. canvas_draw_dot(canvas, x_back_symbol + 2, y_back_symbol - 5);
  152. canvas_draw_dot(canvas, x_back_symbol + 1, y_back_symbol - 5);
  153. canvas_draw_dot(canvas, x_back_symbol + 0, y_back_symbol - 5);
  154. canvas_draw_dot(canvas, x_back_symbol - 1, y_back_symbol - 5);
  155. canvas_draw_dot(canvas, x_back_symbol - 2, y_back_symbol - 5);
  156. canvas_draw_dot(canvas, x_back_symbol - 3, y_back_symbol - 5);
  157. canvas_draw_dot(canvas, x_back_symbol - 2, y_back_symbol - 6);
  158. canvas_draw_dot(canvas, x_back_symbol - 2, y_back_symbol - 4);
  159. canvas_draw_dot(canvas, x_back_symbol - 1, y_back_symbol - 6);
  160. canvas_draw_dot(canvas, x_back_symbol - 1, y_back_symbol - 4);
  161. canvas_draw_dot(canvas, x_back_symbol - 1, y_back_symbol - 7);
  162. canvas_draw_dot(canvas, x_back_symbol - 1, y_back_symbol - 3);
  163. }
  164. }
  165. furi_mutex_release(snake_state->mutex);
  166. }
  167. static void snake_game_input_callback(InputEvent* input_event, void* ctx) {
  168. FuriMessageQueue* event_queue = ctx;
  169. furi_assert(event_queue);
  170. SnakeEvent event = {.type = EventTypeKey, .input = *input_event};
  171. furi_message_queue_put(event_queue, &event, FuriWaitForever);
  172. }
  173. static void snake_game_update_timer_callback(void* ctx) {
  174. FuriMessageQueue* event_queue = ctx;
  175. furi_assert(event_queue);
  176. SnakeEvent event = {.type = EventTypeTick};
  177. furi_message_queue_put(event_queue, &event, 0);
  178. }
  179. static void snake_game_init_game(SnakeState* const snake_state) {
  180. Point p[] = {{8, 6}, {7, 6}, {6, 6}, {5, 6}, {4, 6}, {3, 6}, {2, 6}};
  181. memcpy(snake_state->points, p, sizeof(p)); //-V1086
  182. snake_state->len = 7;
  183. snake_state->currentMovement = DirectionRight;
  184. snake_state->nextMovement = DirectionRight;
  185. Point f = {18, 6};
  186. snake_state->fruit = f;
  187. snake_state->state = GameStateLife;
  188. }
  189. static Point snake_game_get_new_fruit(SnakeState const* const snake_state) {
  190. // 1 bit for each point on the playing field where the snake can turn
  191. // and where the fruit can appear
  192. uint16_t buffer[8];
  193. memset(buffer, 0, sizeof(buffer));
  194. uint8_t empty = 8 * 16;
  195. for(uint16_t i = 0; i < snake_state->len; i++) {
  196. Point p = snake_state->points[i];
  197. if(p.x % 2 != 0 || p.y % 2 != 0) {
  198. continue;
  199. }
  200. p.x /= 2;
  201. p.y /= 2;
  202. buffer[p.y] |= 1 << p.x;
  203. empty--;
  204. }
  205. // Bit set if snake use that playing field
  206. uint16_t newFruit = rand() % empty;
  207. // Skip random number of _empty_ fields
  208. for(uint8_t y = 0; y < 8; y++) {
  209. for(uint16_t x = 0, mask = 1; x < 16; x += 1, mask <<= 1) {
  210. if((buffer[y] & mask) == 0) {
  211. if(newFruit == 0) {
  212. Point p = {
  213. .x = x * 2,
  214. .y = y * 2,
  215. };
  216. return p;
  217. }
  218. newFruit--;
  219. }
  220. }
  221. }
  222. // We will never be here
  223. Point p = {0, 0};
  224. return p;
  225. }
  226. static bool snake_game_collision_with_frame(Point const next_step) {
  227. // if x == 0 && currentMovement == left then x - 1 == 255 ,
  228. // so check only x > right border
  229. return next_step.x > 30 || next_step.y > 14;
  230. }
  231. static bool
  232. snake_game_collision_with_tail(SnakeState const* const snake_state, Point const next_step) {
  233. for(uint16_t i = 0; i < snake_state->len; i++) {
  234. Point p = snake_state->points[i];
  235. if(p.x == next_step.x && p.y == next_step.y) {
  236. return true;
  237. }
  238. }
  239. return false;
  240. }
  241. static Direction snake_game_get_turn_snake(SnakeState const* const snake_state) {
  242. // Sum of two `Direction` lies between 0 and 6, odd values indicate orthogonality.
  243. bool is_orthogonal = (snake_state->currentMovement + snake_state->nextMovement) % 2 == 1;
  244. return is_orthogonal ? snake_state->nextMovement : snake_state->currentMovement;
  245. }
  246. static Point snake_game_get_next_step(SnakeState const* const snake_state) {
  247. Point next_step = snake_state->points[0];
  248. switch(snake_state->currentMovement) {
  249. // +-----x
  250. // |
  251. // |
  252. // y
  253. case DirectionUp:
  254. next_step.y--;
  255. break;
  256. case DirectionRight:
  257. next_step.x++;
  258. break;
  259. case DirectionDown:
  260. next_step.y++;
  261. break;
  262. case DirectionLeft:
  263. next_step.x--;
  264. break;
  265. }
  266. return next_step;
  267. }
  268. static void snake_game_move_snake(SnakeState* const snake_state, Point const next_step) {
  269. memmove(snake_state->points + 1, snake_state->points, snake_state->len * sizeof(Point));
  270. snake_state->points[0] = next_step;
  271. }
  272. static void
  273. snake_game_process_game_step(SnakeState* const snake_state, NotificationApp* notification) {
  274. if(snake_state->state == GameStateGameOver) {
  275. return;
  276. }
  277. snake_state->currentMovement = snake_game_get_turn_snake(snake_state);
  278. Point next_step = snake_game_get_next_step(snake_state);
  279. bool crush = snake_game_collision_with_frame(next_step);
  280. if(crush) {
  281. if(snake_state->state == GameStateLife) {
  282. snake_state->state = GameStateLastChance;
  283. return;
  284. } else if(snake_state->state == GameStateLastChance) {
  285. snake_state->state = GameStateGameOver;
  286. notification_message_block(notification, &sequence_fail);
  287. return;
  288. }
  289. } else {
  290. if(snake_state->state == GameStateLastChance) {
  291. snake_state->state = GameStateLife;
  292. }
  293. }
  294. crush = snake_game_collision_with_tail(snake_state, next_step);
  295. if(crush) {
  296. snake_state->state = GameStateGameOver;
  297. notification_message_block(notification, &sequence_fail);
  298. return;
  299. }
  300. bool eatFruit = (next_step.x == snake_state->fruit.x) && (next_step.y == snake_state->fruit.y);
  301. if(eatFruit) {
  302. snake_state->len++;
  303. if(snake_state->len >= MAX_SNAKE_LEN) {
  304. //You win!!!
  305. snake_state->state = GameStateGameOver;
  306. notification_message_block(notification, &sequence_fail);
  307. return;
  308. }
  309. }
  310. snake_game_move_snake(snake_state, next_step);
  311. if(eatFruit) {
  312. snake_state->fruit = snake_game_get_new_fruit(snake_state);
  313. notification_message(notification, &sequence_eat);
  314. notification_message(notification, &sequence_blink_red_100);
  315. }
  316. }
  317. int32_t snake_20_app(void* p) {
  318. UNUSED(p);
  319. FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(SnakeEvent));
  320. SnakeState* snake_state = malloc(sizeof(SnakeState));
  321. snake_game_init_game(snake_state);
  322. snake_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
  323. if(!snake_state->mutex) {
  324. FURI_LOG_E("SnakeGame", "cannot create mutex\r\n");
  325. furi_message_queue_free(event_queue);
  326. free(snake_state);
  327. return 255;
  328. }
  329. ViewPort* view_port = view_port_alloc();
  330. view_port_draw_callback_set(view_port, snake_game_render_callback, snake_state);
  331. view_port_input_callback_set(view_port, snake_game_input_callback, event_queue);
  332. FuriTimer* timer =
  333. furi_timer_alloc(snake_game_update_timer_callback, FuriTimerTypePeriodic, event_queue);
  334. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);
  335. // Open GUI and register view_port
  336. Gui* gui = furi_record_open(RECORD_GUI);
  337. gui_add_view_port(gui, view_port, GuiLayerFullscreen);
  338. NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
  339. notification_message_block(notification, &sequence_display_backlight_enforce_on);
  340. dolphin_deed(DolphinDeedPluginGameStart);
  341. SnakeEvent event;
  342. for(bool processing = true; processing;) {
  343. FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
  344. furi_mutex_acquire(snake_state->mutex, FuriWaitForever);
  345. if(event_status == FuriStatusOk) {
  346. if(event.type == EventTypeKey) {
  347. // press events
  348. if(event.input.type == InputTypePress) {
  349. switch(event.input.key) {
  350. case InputKeyUp:
  351. if(snake_state->state != GameStatePause) {
  352. snake_state->nextMovement = DirectionUp;
  353. }
  354. break;
  355. case InputKeyDown:
  356. if(snake_state->state != GameStatePause) {
  357. snake_state->nextMovement = DirectionDown;
  358. }
  359. break;
  360. case InputKeyRight:
  361. if(snake_state->state != GameStatePause) {
  362. snake_state->nextMovement = DirectionRight;
  363. }
  364. break;
  365. case InputKeyLeft:
  366. if(snake_state->state != GameStatePause) {
  367. snake_state->nextMovement = DirectionLeft;
  368. }
  369. break;
  370. case InputKeyOk:
  371. if(snake_state->state == GameStateGameOver) {
  372. snake_game_init_game(snake_state);
  373. }
  374. if(snake_state->state == GameStatePause) {
  375. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);
  376. snake_state->state = GameStateLife;
  377. }
  378. break;
  379. case InputKeyBack:
  380. if(snake_state->state == GameStateLife) {
  381. furi_timer_stop(timer);
  382. snake_state->state = GameStatePause;
  383. break;
  384. }
  385. if(snake_state->state == GameStatePause) {
  386. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);
  387. snake_state->state = GameStateLife;
  388. break;
  389. }
  390. if(snake_state->state == GameStateGameOver) {
  391. snake_game_init_game(snake_state);
  392. }
  393. default:
  394. break;
  395. }
  396. }
  397. //LongPress Events
  398. if(event.input.type == InputTypeLong) {
  399. switch(event.input.key) {
  400. case InputKeyUp:
  401. if(snake_state->state != GameStatePause) {
  402. snake_state->nextMovement = DirectionUp;
  403. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 8);
  404. }
  405. break;
  406. case InputKeyDown:
  407. if(snake_state->state != GameStatePause) {
  408. snake_state->nextMovement = DirectionDown;
  409. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 8);
  410. }
  411. break;
  412. case InputKeyRight:
  413. if(snake_state->state != GameStatePause) {
  414. snake_state->nextMovement = DirectionRight;
  415. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 8);
  416. }
  417. break;
  418. case InputKeyLeft:
  419. if(snake_state->state != GameStatePause) {
  420. snake_state->nextMovement = DirectionLeft;
  421. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 8);
  422. }
  423. break;
  424. case InputKeyBack:
  425. processing = false;
  426. break;
  427. default:
  428. break;
  429. }
  430. }
  431. //ReleaseKey Event
  432. if(event.input.type == InputTypeRelease) {
  433. if(snake_state->state != GameStatePause) {
  434. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);
  435. }
  436. }
  437. } else if(event.type == EventTypeTick) {
  438. snake_game_process_game_step(snake_state, notification);
  439. }
  440. } else {
  441. // event timeout
  442. }
  443. furi_mutex_release(snake_state->mutex);
  444. view_port_update(view_port);
  445. }
  446. // Wait for all notifications to be played and return backlight to normal state
  447. notification_message_block(notification, &sequence_display_backlight_enforce_auto);
  448. furi_timer_free(timer);
  449. view_port_enabled_set(view_port, false);
  450. gui_remove_view_port(gui, view_port);
  451. furi_record_close(RECORD_GUI);
  452. furi_record_close(RECORD_NOTIFICATION);
  453. view_port_free(view_port);
  454. furi_message_queue_free(event_queue);
  455. furi_mutex_free(snake_state->mutex);
  456. free(snake_state);
  457. return 0;
  458. }