snake_game.c 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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. // https://melmagazine.com/en-us/story/snake-nokia-6110-oral-history-taneli-armanto
  19. // Armanto: While testing the early versions of the game, I noticed it was hard
  20. // to control the snake upon getting close to and edge but not crashing — especially
  21. // in the highest speed levels. I wanted the highest level to be as fast as I could
  22. // possibly make the device "run," but on the other hand, I wanted to be friendly
  23. // and help the player manage that level. Otherwise it might not be fun to play. So
  24. // I implemented a little delay. A few milliseconds of extra time right before
  25. // the player crashes, during which she can still change the directions. And if
  26. // she does, the game continues.
  27. GameStateLastChance,
  28. GameStateGameOver,
  29. } GameState;
  30. // Note: do not change without purpose. Current values are used in smart
  31. // orthogonality calculation in `snake_game_get_turn_snake`.
  32. typedef enum {
  33. DirectionUp,
  34. DirectionRight,
  35. DirectionDown,
  36. DirectionLeft,
  37. } Direction;
  38. #define MAX_SNAKE_LEN 128 * 64 / 4
  39. typedef struct {
  40. Point points[MAX_SNAKE_LEN];
  41. uint16_t len;
  42. Direction currentMovement;
  43. Direction nextMovement; // if backward of currentMovement, ignore
  44. Point fruit;
  45. GameState state;
  46. FuriMutex* mutex;
  47. } SnakeState;
  48. typedef enum {
  49. EventTypeTick,
  50. EventTypeKey,
  51. } EventType;
  52. typedef struct {
  53. EventType type;
  54. InputEvent input;
  55. } SnakeEvent;
  56. const NotificationSequence sequence_fail = {
  57. &message_vibro_on,
  58. &message_note_ds4,
  59. &message_delay_10,
  60. &message_sound_off,
  61. &message_delay_10,
  62. &message_note_ds4,
  63. &message_delay_10,
  64. &message_sound_off,
  65. &message_delay_10,
  66. &message_note_ds4,
  67. &message_delay_10,
  68. &message_sound_off,
  69. &message_delay_10,
  70. &message_vibro_off,
  71. NULL,
  72. };
  73. const NotificationSequence sequence_eat = {
  74. &message_note_c7,
  75. &message_delay_50,
  76. &message_sound_off,
  77. NULL,
  78. };
  79. static void snake_game_render_callback(Canvas* const canvas, void* ctx) {
  80. furi_assert(ctx);
  81. const SnakeState* snake_state = ctx;
  82. furi_mutex_acquire(snake_state->mutex, FuriWaitForever);
  83. // Frame
  84. canvas_draw_frame(canvas, 0, 0, 128, 64);
  85. // Fruit
  86. Point f = snake_state->fruit;
  87. f.x = f.x * 4 + 1;
  88. f.y = f.y * 4 + 1;
  89. canvas_draw_rframe(canvas, f.x, f.y, 6, 6, 2);
  90. // Snake
  91. for(uint16_t i = 0; i < snake_state->len; i++) {
  92. Point p = snake_state->points[i];
  93. p.x = p.x * 4 + 2;
  94. p.y = p.y * 4 + 2;
  95. canvas_draw_box(canvas, p.x, p.y, 4, 4);
  96. }
  97. // Game Over banner
  98. if(snake_state->state == GameStateGameOver) {
  99. // Screen is 128x64 px
  100. canvas_set_color(canvas, ColorWhite);
  101. canvas_draw_box(canvas, 34, 20, 62, 24);
  102. canvas_set_color(canvas, ColorBlack);
  103. canvas_draw_frame(canvas, 34, 20, 62, 24);
  104. canvas_set_font(canvas, FontPrimary);
  105. canvas_draw_str(canvas, 37, 31, "Game Over");
  106. canvas_set_font(canvas, FontSecondary);
  107. char buffer[12];
  108. snprintf(buffer, sizeof(buffer), "Score: %u", snake_state->len - 7U);
  109. canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignBottom, buffer);
  110. }
  111. furi_mutex_release(snake_state->mutex);
  112. }
  113. static void snake_game_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
  114. furi_assert(event_queue);
  115. SnakeEvent event = {.type = EventTypeKey, .input = *input_event};
  116. furi_message_queue_put(event_queue, &event, FuriWaitForever);
  117. }
  118. static void snake_game_update_timer_callback(FuriMessageQueue* event_queue) {
  119. furi_assert(event_queue);
  120. SnakeEvent event = {.type = EventTypeTick};
  121. furi_message_queue_put(event_queue, &event, 0);
  122. }
  123. static void snake_game_init_game(SnakeState* const snake_state) {
  124. Point p[] = {{8, 6}, {7, 6}, {6, 6}, {5, 6}, {4, 6}, {3, 6}, {2, 6}};
  125. memcpy(snake_state->points, p, sizeof(p)); //-V1086
  126. snake_state->len = 7;
  127. snake_state->currentMovement = DirectionRight;
  128. snake_state->nextMovement = DirectionRight;
  129. Point f = {18, 6};
  130. snake_state->fruit = f;
  131. snake_state->state = GameStateLife;
  132. }
  133. static Point snake_game_get_new_fruit(SnakeState const* const snake_state) {
  134. // 1 bit for each point on the playing field where the snake can turn
  135. // and where the fruit can appear
  136. uint16_t buffer[8];
  137. memset(buffer, 0, sizeof(buffer));
  138. uint8_t empty = 8 * 16;
  139. for(uint16_t i = 0; i < snake_state->len; i++) {
  140. Point p = snake_state->points[i];
  141. if(p.x % 2 != 0 || p.y % 2 != 0) {
  142. continue;
  143. }
  144. p.x /= 2;
  145. p.y /= 2;
  146. buffer[p.y] |= 1 << p.x;
  147. empty--;
  148. }
  149. // Bit set if snake use that playing field
  150. uint16_t newFruit = rand() % empty;
  151. // Skip random number of _empty_ fields
  152. for(uint8_t y = 0; y < 8; y++) {
  153. for(uint16_t x = 0, mask = 1; x < 16; x += 1, mask <<= 1) {
  154. if((buffer[y] & mask) == 0) {
  155. if(newFruit == 0) {
  156. Point p = {
  157. .x = x * 2,
  158. .y = y * 2,
  159. };
  160. return p;
  161. }
  162. newFruit--;
  163. }
  164. }
  165. }
  166. // We will never be here
  167. Point p = {0, 0};
  168. return p;
  169. }
  170. static bool snake_game_collision_with_frame(Point const next_step) {
  171. // if x == 0 && currentMovement == left then x - 1 == 255 ,
  172. // so check only x > right border
  173. return next_step.x > 30 || next_step.y > 14;
  174. }
  175. static bool
  176. snake_game_collision_with_tail(SnakeState const* const snake_state, Point const next_step) {
  177. for(uint16_t i = 0; i < snake_state->len; i++) {
  178. Point p = snake_state->points[i];
  179. if(p.x == next_step.x && p.y == next_step.y) {
  180. return true;
  181. }
  182. }
  183. return false;
  184. }
  185. static Direction snake_game_get_turn_snake(SnakeState const* const snake_state) {
  186. // Sum of two `Direction` lies between 0 and 6, odd values indicate orthogonality.
  187. bool is_orthogonal = (snake_state->currentMovement + snake_state->nextMovement) % 2 == 1;
  188. return is_orthogonal ? snake_state->nextMovement : snake_state->currentMovement;
  189. }
  190. static Point snake_game_get_next_step(SnakeState const* const snake_state) {
  191. Point next_step = snake_state->points[0];
  192. switch(snake_state->currentMovement) {
  193. // +-----x
  194. // |
  195. // |
  196. // y
  197. case DirectionUp:
  198. next_step.y--;
  199. break;
  200. case DirectionRight:
  201. next_step.x++;
  202. break;
  203. case DirectionDown:
  204. next_step.y++;
  205. break;
  206. case DirectionLeft:
  207. next_step.x--;
  208. break;
  209. }
  210. return next_step;
  211. }
  212. static void snake_game_move_snake(SnakeState* const snake_state, Point const next_step) {
  213. memmove(snake_state->points + 1, snake_state->points, snake_state->len * sizeof(Point));
  214. snake_state->points[0] = next_step;
  215. }
  216. static void
  217. snake_game_process_game_step(SnakeState* const snake_state, NotificationApp* notification) {
  218. if(snake_state->state == GameStateGameOver) {
  219. return;
  220. }
  221. snake_state->currentMovement = snake_game_get_turn_snake(snake_state);
  222. Point next_step = snake_game_get_next_step(snake_state);
  223. bool crush = snake_game_collision_with_frame(next_step);
  224. if(crush) {
  225. if(snake_state->state == GameStateLife) {
  226. snake_state->state = GameStateLastChance;
  227. return;
  228. } else if(snake_state->state == GameStateLastChance) {
  229. snake_state->state = GameStateGameOver;
  230. notification_message_block(notification, &sequence_fail);
  231. return;
  232. }
  233. } else {
  234. if(snake_state->state == GameStateLastChance) {
  235. snake_state->state = GameStateLife;
  236. }
  237. }
  238. crush = snake_game_collision_with_tail(snake_state, next_step);
  239. if(crush) {
  240. snake_state->state = GameStateGameOver;
  241. notification_message_block(notification, &sequence_fail);
  242. return;
  243. }
  244. bool eatFruit = (next_step.x == snake_state->fruit.x) && (next_step.y == snake_state->fruit.y);
  245. if(eatFruit) {
  246. snake_state->len++;
  247. if(snake_state->len >= MAX_SNAKE_LEN) {
  248. snake_state->state = GameStateGameOver;
  249. notification_message_block(notification, &sequence_fail);
  250. return;
  251. }
  252. }
  253. snake_game_move_snake(snake_state, next_step);
  254. if(eatFruit) {
  255. snake_state->fruit = snake_game_get_new_fruit(snake_state);
  256. notification_message(notification, &sequence_eat);
  257. }
  258. }
  259. int32_t snake_game_app(void* p) {
  260. UNUSED(p);
  261. FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(SnakeEvent));
  262. SnakeState* snake_state = malloc(sizeof(SnakeState));
  263. snake_game_init_game(snake_state);
  264. snake_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
  265. if(!snake_state->mutex) {
  266. FURI_LOG_E("SnakeGame", "cannot create mutex\r\n");
  267. furi_message_queue_free(event_queue);
  268. free(snake_state);
  269. return 255;
  270. }
  271. ViewPort* view_port = view_port_alloc();
  272. view_port_draw_callback_set(view_port, snake_game_render_callback, snake_state);
  273. view_port_input_callback_set(view_port, snake_game_input_callback, event_queue);
  274. FuriTimer* timer =
  275. furi_timer_alloc(snake_game_update_timer_callback, FuriTimerTypePeriodic, event_queue);
  276. furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);
  277. // Open GUI and register view_port
  278. Gui* gui = furi_record_open(RECORD_GUI);
  279. gui_add_view_port(gui, view_port, GuiLayerFullscreen);
  280. NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
  281. notification_message_block(notification, &sequence_display_backlight_enforce_on);
  282. dolphin_deed(DolphinDeedPluginGameStart);
  283. SnakeEvent event;
  284. for(bool processing = true; processing;) {
  285. FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
  286. furi_mutex_acquire(snake_state->mutex, FuriWaitForever);
  287. if(event_status == FuriStatusOk) {
  288. // press events
  289. if(event.type == EventTypeKey) {
  290. if(event.input.type == InputTypePress) {
  291. switch(event.input.key) {
  292. case InputKeyUp:
  293. snake_state->nextMovement = DirectionUp;
  294. break;
  295. case InputKeyDown:
  296. snake_state->nextMovement = DirectionDown;
  297. break;
  298. case InputKeyRight:
  299. snake_state->nextMovement = DirectionRight;
  300. break;
  301. case InputKeyLeft:
  302. snake_state->nextMovement = DirectionLeft;
  303. break;
  304. case InputKeyOk:
  305. if(snake_state->state == GameStateGameOver) {
  306. snake_game_init_game(snake_state);
  307. }
  308. break;
  309. case InputKeyBack:
  310. processing = false;
  311. break;
  312. default:
  313. break;
  314. }
  315. }
  316. } else if(event.type == EventTypeTick) {
  317. snake_game_process_game_step(snake_state, notification);
  318. }
  319. } else {
  320. // event timeout
  321. }
  322. furi_mutex_release(snake_state->mutex);
  323. view_port_update(view_port);
  324. }
  325. // Return backlight to normal state
  326. notification_message(notification, &sequence_display_backlight_enforce_auto);
  327. furi_timer_free(timer);
  328. view_port_enabled_set(view_port, false);
  329. gui_remove_view_port(gui, view_port);
  330. furi_record_close(RECORD_GUI);
  331. furi_record_close(RECORD_NOTIFICATION);
  332. view_port_free(view_port);
  333. furi_message_queue_free(event_queue);
  334. furi_mutex_free(snake_state->mutex);
  335. free(snake_state);
  336. return 0;
  337. }