Browse Source

Merge solitaire from https://github.com/doofy-dev/flipper_solitaire

Willy-JL 1 year ago
parent
commit
e02545b525
66 changed files with 3363 additions and 628 deletions
  1. 35 18
      solitaire/README.md
  2. 6 5
      solitaire/application.fam
  3. 356 0
      solitaire/assets.c
  4. 281 0
      solitaire/assets.h
  5. BIN
      solitaire/assets/10.png
  6. BIN
      solitaire/assets/2.png
  7. BIN
      solitaire/assets/3.png
  8. BIN
      solitaire/assets/4.png
  9. BIN
      solitaire/assets/5.png
  10. BIN
      solitaire/assets/6.png
  11. BIN
      solitaire/assets/7.png
  12. BIN
      solitaire/assets/8.png
  13. BIN
      solitaire/assets/9.png
  14. BIN
      solitaire/assets/A.png
  15. BIN
      solitaire/assets/J.png
  16. BIN
      solitaire/assets/K.png
  17. BIN
      solitaire/assets/Q.png
  18. BIN
      solitaire/assets/card_graphics.png
  19. BIN
      solitaire/assets/clubs.png
  20. BIN
      solitaire/assets/diamonds.png
  21. BIN
      solitaire/assets/hearths.png
  22. BIN
      solitaire/assets/joker.png
  23. BIN
      solitaire/assets/logo.png
  24. BIN
      solitaire/assets/main_image.png
  25. BIN
      solitaire/assets/pattern_big.png
  26. BIN
      solitaire/assets/pattern_small.png
  27. BIN
      solitaire/assets/solitaire_main.png
  28. BIN
      solitaire/assets/solve.png
  29. BIN
      solitaire/assets/spades.png
  30. BIN
      solitaire/assets/start.png
  31. 0 63
      solitaire/defines.h
  32. 25 0
      solitaire/docs/CHANGELOG.md
  33. 31 0
      solitaire/docs/README.md
  34. 43 0
      solitaire/game_state.h
  35. BIN
      solitaire/screenshots/catalog_1.png
  36. BIN
      solitaire/screenshots/catalog_2.png
  37. BIN
      solitaire/screenshots/catalog_3.png
  38. BIN
      solitaire/screenshots/catalog_4.png
  39. BIN
      solitaire/screenshots/catalog_5.png
  40. BIN
      solitaire/screenshots/catalog_6.png
  41. BIN
      solitaire/screenshots/solitaire.png
  42. 140 542
      solitaire/solitaire.c
  43. 105 0
      solitaire/src/scene/falling_card.c
  44. 11 0
      solitaire/src/scene/falling_card.h
  45. 121 0
      solitaire/src/scene/intro_animation.c
  46. 11 0
      solitaire/src/scene/intro_animation.h
  47. 104 0
      solitaire/src/scene/main_screen.c
  48. 16 0
      solitaire/src/scene/main_screen.h
  49. 373 0
      solitaire/src/scene/play_screen.c
  50. 13 0
      solitaire/src/scene/play_screen.h
  51. 73 0
      solitaire/src/scene/result_screen.c
  52. 11 0
      solitaire/src/scene/result_screen.h
  53. 55 0
      solitaire/src/scene/scene_setup.h
  54. 213 0
      solitaire/src/scene/solve_screen.c
  55. 8 0
      solitaire/src/scene/solve_screen.h
  56. 293 0
      solitaire/src/util/buffer.c
  57. 100 0
      solitaire/src/util/buffer.h
  58. 249 0
      solitaire/src/util/card.c
  59. 73 0
      solitaire/src/util/card.h
  60. 6 0
      solitaire/src/util/game_loop.h
  61. 42 0
      solitaire/src/util/helpers.c
  62. 32 0
      solitaire/src/util/helpers.h
  63. 301 0
      solitaire/src/util/list.c
  64. 48 0
      solitaire/src/util/list.h
  65. 127 0
      solitaire/src/util/vector.c
  66. 61 0
      solitaire/src/util/vector.h

+ 35 - 18
solitaire/README.md

@@ -3,28 +3,45 @@
 [![issues - flipper-zero_authenticator](https://img.shields.io/github/issues/teeebor/flipper_solitaire)](https://github.com/teeebor/flipper_solitaire/issues)
 ![maintained - yes](https://img.shields.io/badge/maintained-yes-blue)
 ![contributions - welcome](https://img.shields.io/badge/contributions-welcome-blue)
-# Solitaire game for Flipper Zero
+# Solitaire - Klondike for Flipper Zero
 
-![Play screen](screenshots/solitaire.png)
 
-![Play screen](screenshots/solitaire.gif)
+![Play buffer](screenshots/solitaire.png)
+
+![Play buffer](screenshots/solitaire.gif)
+
+## Features
+
+* **Auto-Solve:** Ability to automatically solve the game when all cards are flipped.
+* **Animated Card Movements:** Animated transitions during solve and deal.
+* **Time Tracking:** Displays the time it took to solve at the end of each game.
+* **Falling Cards:** Enjoy a visually satisfying cascade of cards when you win.
+
+## Shortcuts
+
+* **Long Press Any Arrow:** Jump to the furthest point in that direction.
+* **Long Press Center:** Automatically place the card in the top right section.
+* **Long Press Back:** Close the application instantly.
+
+## Rules
+- **Empty Tableau Spots:** Only a King can be placed in an empty tableau spot.
+- **Tableau Arrangement:** Cards must be arranged in descending order and alternating colors (e.g., red ten on black nine) within the tableau.
+- **Empty Foundation Piles:** Only an Ace can be placed in an empty foundation pile.
+- **Foundation Pile Arrangement:** Cards must be in ascending order and in the same suit.
+
+## How to Play
+
+- Use the directional arrows to navigate and move cards.
+- Pick up cards with the center button.
+- Aim to build up four foundation piles in ascending order, separated by suit.
+- Flip and move cards within the tableau to reveal hidden cards.
+- You can place back cards in the same spot you picked them up.
 
-### Shortcuts
-* Long press up skips the navigation inside the bottom column
-* Long press center to automatically place the card to the top rigth section
 
 ## Building
 > The app should be compatible with the official and custom flipper firmwares. If not, follow these steps to build it
 > yourself
-* Download your firmware's source code
-* Clone the repository recursively `git clone REPO_URL --recursive` into the firmware's applications_user folder
-* Navigate into the firmwares root folder
-* Make sure you can use
-  the [Fipper build tool](https://github.com/flipperdevices/flipperzero-firmware/blob/dev/documentation/fbt.md)
-* To build the project, type this into your console:
-  #### Linux
-  > ./fbt fap_{APP_NAME}
-  #### Windows
-  > fbt.cmd fap_{APP_NAME}
-* the finished build will be in the following location, copy this into your SD card:
-  > build\f7-firmware-D\.extapps\blackjack.fap
+* Set up [uFBT](https://pypi.org/project/ufbt/) if you haven't already
+* Navigate into the folder of the game
+* Run `ufbt`
+* the finished build will be in the dist folder, copy this the fap file into your SD card

+ 6 - 5
solitaire/application.fam

@@ -3,13 +3,14 @@ App(
     name="Solitaire",
     apptype=FlipperAppType.EXTERNAL,
     entry_point="solitaire_app",
-    requires=["gui", "storage", "canvas"],
+    cdefines=["APP_SOLITAIRE"],
+    requires=["gui"],
     stack_size=2 * 1024,
     order=30,
     fap_icon="solitaire_10px.png",
     fap_category="Games",
-    fap_icon_assets="assets",
-    fap_author="@teeebor",
-    fap_version="1.1",
-    fap_description="Solitaire game",
+    fap_author="@doofy_dev",
+    fap_weburl="https://github.com/doofy-dev/flipper_solitaire",
+    fap_version="2.0",
+    fap_description="Klondike Solitaire card game",
 )

+ 356 - 0
solitaire/assets.c

@@ -0,0 +1,356 @@
+#include "assets.h"
+
+/*
+      ██████   
+   ███   ███   
+███      ███   
+████████████   
+         ███   
+*/
+const Buffer sprite_4 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0xc, 0xa, 0x9, 0xf, 0x8}};
+/*
+████████████   
+         ███   
+      ███      
+   ███         
+   ███         
+*/
+const Buffer sprite_7 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0xf, 0x8, 0x4, 0x2, 0x2}};
+/*
+   ███         ███   
+███   ███   ███   ███
+███      ███      ███
+███               ███
+   ███         ███   
+      ███   ███      
+         ███         
+*/
+const Buffer sprite_hearths =
+    (Buffer){.width = 8, .height = 7, .data = (uint8_t[]){0x22, 0x55, 0x49, 0x41, 0x22, 0x14, 0x8}};
+/*
+   ██████      
+███      ███   
+████████████   
+███      ███   
+███      ███   
+*/
+const Buffer sprite_A =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x6, 0x9, 0xf, 0x9, 0x9}};
+/*
+                     
+      ███   ███   ███
+   ███   ███   ███   
+      ███   ███   ███
+   ███   ███   ███   
+      ███   ███   ███
+   ███   ███   ███   
+*/
+const Buffer sprite_pattern_small =
+    (Buffer){.width = 8, .height = 7, .data = (uint8_t[]){0x0, 0x54, 0x2a, 0x54, 0x2a, 0x54, 0x2a}};
+/*
+                  ███                        
+   ███      ████████████                     
+   ████████████████████████      ███         
+   ████████████████████████   ███   ███      
+   ███      ████████████         ███         
+                                             
+         ███            ███                  
+      ███   ███      ████████████      ███   
+         ███      ████████████████████████   
+                  ████████████████████████   
+                     ████████████      ███   
+                                             
+                  ███            ███         
+   ███      ████████████      ███   ███      
+   ████████████████████████      ███         
+   ████████████████████████            ███   
+   ███      ████████████            ███      
+                                    ███      
+         ███         ███            ███      
+      ███            ███   ███      ███      
+      ███         ███      ███   ███         
+*/
+const Buffer sprite_pattern_big = (Buffer){
+    .width = 16,
+    .height = 21,
+    .data = (uint8_t[]){0x40, 0x0, 0xf2, 0x0,  0xfe, 0x9,  0xfe, 0x15, 0xf2, 0x8,  0x0,
+                        0x0,  0x8, 0x1,  0x94, 0x27, 0xc8, 0x3f, 0xc0, 0x3f, 0x80, 0x27,
+                        0x0,  0x0, 0x40, 0x8,  0xf2, 0x14, 0xfe, 0x9,  0xfe, 0x21, 0xf2,
+                        0x10, 0x0, 0x10, 0x88, 0x10, 0x84, 0x12, 0x44, 0xa}};
+/*
+                                          █████████████████████████████████████████████            
+                                       ███                                             ███         
+                                       █████████████████████████████████████████████   ███         
+                                    ███                                             ██████         
+                                    █████████████████████████████████████████████   ██████         
+                                 ███                                             █████████         
+                                 ███   ███      ███               ███            █████████         
+                                 ███   ███   ███   ███         █████████         █████████         
+                                 █████████████████████████████████████████████   █████████         
+                              ███                                             ████████████         
+                              ███   ███      ███               ███            ████████████         
+                              ███   ███   ███   ███         █████████         ████████████         
+            ███████████████████████████████████████████████████████████████   ████████████         
+         ███                                             ███               ███████████████         
+         ███   ███      ███               ███            ██████            ███████████████         
+         ███   ███   ███   ███         █████████         █████████         ███████████████         
+         ███   ███   ███   ███      ███████████████      ███████████████   ███████████████         
+         ███   ███   ███   ███   █████████████████████   ██████         ██████████████████         
+         ███   ███   ███   ███      ███████████████      █████████      ██████████████████         
+         ███   ███      ███            █████████         █████████      ██████████████████         
+         ███                              ███            █████████      ██████████████████         
+         ███                                             ████████████   ███████████████            
+         ███                                             ████████████   ███████████████            
+         ███                                             ███████████████████████████               
+         ███                                             ███████████████████████████               
+         ███            ███                              ████████████████████████                  
+         ███         █████████            ███      ███   ████████████████████████                  
+         ███      ███████████████      ███   ███   ███   ████████████████████████                  
+         ███   █████████████████████   ███   ███   ███   ████████████████████████                  
+         ███      ███████████████      ███   ███   ███   █████████████████████                     
+         ███         █████████         ███   ███   ███   █████████████████████                     
+         ███            ███               ███      ███   █████████████████████                     
+         ███                                             █████████████████████                     
+            █████████████████████████████████████████████   ███████████████                        
+            ███                                             ███████████████                        
+               █████████████████████████████████████████████   ████████████                        
+               ███            ███               ███      ███   ████████████                        
+               ███                                             █████████                           
+                  █████████████████████████████████████████████   ██████                           
+                  ███            ███               ███      ███   ██████                           
+                  ███                                             ██████                           
+                     █████████████████████████████████████████████   ███                           
+                     ███                                             ███                           
+                        █████████████████████████████████████████████                              
+*/
+const Buffer sprite_main_image = (Buffer){
+    .width = 40,
+    .height = 44,
+    .data = (uint8_t[]){
+        0x0,  0xc0, 0xff, 0x1f, 0x0, 0x0,  0x20, 0x0,  0x20, 0x0, 0x0,  0xe0, 0xff, 0x2f, 0x0,
+        0x0,  0x10, 0x0,  0x30, 0x0, 0x0,  0xf0, 0xff, 0x37, 0x0, 0x0,  0x8,  0x0,  0x38, 0x0,
+        0x0,  0x28, 0x41, 0x38, 0x0, 0x0,  0xa8, 0xe2, 0x38, 0x0, 0x0,  0xf8, 0xff, 0x3b, 0x0,
+        0x0,  0x4,  0x0,  0x3c, 0x0, 0x0,  0x94, 0x20, 0x3c, 0x0, 0x0,  0x54, 0x71, 0x3c, 0x0,
+        0xf0, 0xff, 0xff, 0x3d, 0x0, 0x8,  0x0,  0x8,  0x3e, 0x0, 0x28, 0x41, 0x18, 0x3e, 0x0,
+        0xa8, 0xe2, 0x38, 0x3e, 0x0, 0xa8, 0xf2, 0xf9, 0x3e, 0x0, 0xa8, 0xfa, 0x1b, 0x3f, 0x0,
+        0xa8, 0xf2, 0x39, 0x3f, 0x0, 0x28, 0xe1, 0x38, 0x3f, 0x0, 0x8,  0x40, 0x38, 0x3f, 0x0,
+        0x8,  0x0,  0x78, 0x1f, 0x0, 0x8,  0x0,  0x78, 0x1f, 0x0, 0x8,  0x0,  0xf8, 0xf,  0x0,
+        0x8,  0x0,  0xf8, 0xf,  0x0, 0x8,  0x1,  0xf8, 0x7,  0x0, 0x88, 0x43, 0xfa, 0x7,  0x0,
+        0xc8, 0xa7, 0xfa, 0x7,  0x0, 0xe8, 0xaf, 0xfa, 0x7,  0x0, 0xc8, 0xa7, 0xfa, 0x3,  0x0,
+        0x88, 0xa3, 0xfa, 0x3,  0x0, 0x8,  0x41, 0xfa, 0x3,  0x0, 0x8,  0x0,  0xf8, 0x3,  0x0,
+        0xf0, 0xff, 0xf7, 0x1,  0x0, 0x10, 0x0,  0xf0, 0x1,  0x0, 0xe0, 0xff, 0xef, 0x1,  0x0,
+        0x20, 0x4,  0xe9, 0x1,  0x0, 0x20, 0x0,  0xe0, 0x0,  0x0, 0xc0, 0xff, 0xdf, 0x0,  0x0,
+        0x40, 0x8,  0xd2, 0x0,  0x0, 0x40, 0x0,  0xc0, 0x0,  0x0, 0x80, 0xff, 0xbf, 0x0,  0x0,
+        0x80, 0x0,  0x80, 0x0,  0x0, 0x0,  0xff, 0x7f, 0x0,  0x0}};
+/*
+   ██████      
+███      ███   
+███      ███   
+███   ███      
+   ███   ███   
+*/
+const Buffer sprite_Q =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x6, 0x9, 0x9, 0x5, 0xa}};
+/*
+   ██████      
+███      ███   
+      ███      
+   ███         
+████████████   
+*/
+const Buffer sprite_2 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x6, 0x9, 0x4, 0x2, 0xf}};
+/*
+█████████      
+         ███   
+   ██████      
+         ███   
+█████████      
+*/
+const Buffer sprite_3 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x7, 0x8, 0x6, 0x8, 0x7}};
+/*
+███      ███   
+███   ███   ███
+███   ███   ███
+███   ███   ███
+███      ███   
+*/
+const Buffer sprite_10 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x9, 0x15, 0x15, 0x15, 0x9}};
+/*
+███      ███   
+███   ███      
+██████         
+███   ███      
+███      ███   
+*/
+const Buffer sprite_K =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x9, 0x5, 0x3, 0x5, 0x9}};
+/*
+████████████   
+███            
+█████████      
+         ███   
+████████████   
+*/
+const Buffer sprite_5 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0xf, 0x1, 0x7, 0x8, 0xf}};
+/*
+   ██████      
+███            
+█████████      
+███      ███   
+   ██████      
+*/
+const Buffer sprite_6 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x6, 0x1, 0x7, 0x9, 0x6}};
+/*
+   ██████      
+███      ███   
+   █████████   
+         ███   
+   ██████      
+*/
+const Buffer sprite_9 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x6, 0x9, 0xe, 0x8, 0x6}};
+/*
+         ███         
+      ███   ███      
+   ███         ███   
+███               ███
+   ███         ███   
+      ███   ███      
+         ███         
+*/
+const Buffer sprite_diamonds =
+    (Buffer){.width = 8, .height = 7, .data = (uint8_t[]){0x8, 0x14, 0x22, 0x41, 0x22, 0x14, 0x8}};
+/*
+████████████   
+███      ███   
+████████████   
+███      ███   
+████████████   
+*/
+const Buffer sprite_8 =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0xf, 0x9, 0xf, 0x9, 0xf}};
+/*
+         ███   
+         ███   
+         ███   
+███      ███   
+   ██████      
+*/
+const Buffer sprite_J =
+    (Buffer){.width = 8, .height = 5, .data = (uint8_t[]){0x8, 0x8, 0x8, 0x9, 0x6}};
+/*
+         ███         
+      █████████      
+   ███████████████   
+█████████████████████
+██████   ███   ██████
+         ███         
+      █████████      
+*/
+const Buffer sprite_spades =
+    (Buffer){.width = 8, .height = 7, .data = (uint8_t[]){0x8, 0x1c, 0x3e, 0x7f, 0x6b, 0x8, 0x1c}};
+/*
+      ████████████               ████████████      ███                                    ███   
+   ██████████████████         ███            ███   ███                                    ███   
+████████████████████████      ███               █████████      █████████      ███   ████████████
+████████████████████████         ██████            ███      ███         ███   ██████      ███   
+████████████████████████               ██████      ███         ████████████   ███         ███   
+████████████████████████                     ███   ███      ███         ███   ███         ███   
+   ██████████████████         ███            ███   ███      ███      ██████   ███         ███   
+      ████████████               ████████████      ██████   █████████   ███   ███         ██████
+*/
+const Buffer sprite_start = (Buffer){
+    .width = 32,
+    .height = 8,
+    .data = (uint8_t[]){0x3c, 0x78, 0x2,  0x40, 0x7e, 0x84, 0x2,  0x40, 0xff, 0x4,  0xe7,
+                        0xf4, 0xff, 0x18, 0x12, 0x4d, 0xff, 0x60, 0xe2, 0x45, 0xff, 0x80,
+                        0x12, 0x45, 0x7e, 0x84, 0x92, 0x45, 0x3c, 0x78, 0x76, 0xc5}};
+/*
+███         ███                     ███                  ███            ████████████               ███                                                                  ███                                       
+███         ███                     ███                  ███         ██████████████████            ███                                                                  ███                                       
+███         ███      █████████      ███         ████████████      ████████████████████████      █████████      █████████                  ████████████   █████████      ███      ███            ███   █████████   
+███████████████   ███         ███   ███      ███         ███      ████████████████████████         ███      ███         ███            ███            ███         ███   ███         ███      ███   ███         ███
+███         ███   ███         ███   ███      ███         ███      ████████████████████████         ███      ███         ███               █████████   ███         ███   ███         ███      ███   ███████████████
+███         ███   ███         ███   ███      ███         ███         ██████████████████            ███      ███         ███                        ██████         ███   ███            ██████      ███            
+███         ███      █████████      ███         ████████████            ████████████               ██████      █████████               ████████████      █████████      ███            ██████         ████████████
+*/
+const Buffer sprite_solve = (Buffer){
+    .width = 72,
+    .height = 7,
+    .data = (uint8_t[]){0x11, 0x10, 0x8,  0xf,  0x2,  0x0,  0x0,  0x1,  0x0,  0x11, 0x10,
+                        0x88, 0x1f, 0x2,  0x0,  0x0,  0x1,  0x0,  0x91, 0x13, 0xcf, 0x3f,
+                        0xe7, 0xc0, 0x3b, 0x9,  0x1d, 0x5f, 0x94, 0xc8, 0x3f, 0x12, 0x21,
+                        0x44, 0x91, 0x22, 0x51, 0x94, 0xc8, 0x3f, 0x12, 0xc1, 0x45, 0x91,
+                        0x3e, 0x51, 0x94, 0x88, 0x1f, 0x12, 0x1,  0x46, 0x61, 0x2,  0x91,
+                        0x13, 0xf,  0xf,  0xe6, 0xe0, 0x39, 0x61, 0x3c}};
+/*
+      █████████      
+      █████████      
+██████   ███   ██████
+█████████████████████
+██████   ███   ██████
+         ███         
+      █████████      
+*/
+const Buffer sprite_clubs =
+    (Buffer){.width = 8, .height = 7, .data = (uint8_t[]){0x1c, 0x1c, 0x6b, 0x7f, 0x6b, 0x8, 0x1c}};
+/*
+██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
+███                                                                                                                                                                                                            ███
+███                                                                                                                                                                                                            ███
+███               ███████████████                                       ██████         ██████            ███                                    ██████                                                         ███
+███            █████████████████████                                    ██████         ██████         ██████                                    ██████                                                         ███
+███         █████████         █████████                                 ██████         ██████         ██████                                    ██████                                                         ███
+███         ██████               ██████                                 ██████                        ██████                                                                                                   ███
+███         ██████                              ████████████            ██████         ██████      ███████████████      ███████████████         ██████         ██████   ██████         █████████               ███
+███         █████████                        ██████████████████         ██████         ██████      ███████████████      ██████████████████      ██████         ███████████████      ███████████████            ███
+███            ███████████████            █████████      █████████      ██████         ██████         ██████         ██████         ██████      ██████         ██████            ██████         ██████         ███
+███                  ███████████████      ██████            ██████      ██████         ██████         ██████                        ██████      ██████         ██████            ██████         ██████         ███
+███                           █████████   ██████            ██████      ██████         ██████         ██████                  ████████████      ██████         ██████            █████████████████████         ███
+███         ██████               ██████   ██████            ██████      ██████         ██████         ██████            ██████████████████      ██████         ██████            █████████████████████         ███
+███         ██████               ██████   ██████            ██████      ██████         ██████         ██████         █████████      ██████      ██████         ██████            ██████                        ███
+███         █████████         █████████   █████████      █████████      ██████         ██████         ██████         ██████         ██████      ██████         ██████            ██████         ██████         ███
+███            █████████████████████         ██████████████████         ██████         ██████         ████████████   █████████████████████      ██████         ██████               ███████████████            ███
+███               ███████████████               ████████████            ██████         ██████            █████████      █████████   ██████      ██████         ██████                  █████████               ███
+███                                                                                                                                                                                                            ███
+███                                                                                                                                                                                                            ███
+██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
+                                                                                                      ███                                                                                          ███            
+      ███         ███               ███                     ███                  █████████            ███                                                                        ███               ███            
+   ███   ███   ███   ███         ███   ███               █████████               █████████            ███   ███                        █████████                              ███                  ███            
+   ███      ███      ███      ███         ███         ███████████████      ██████   ███   ██████      ███   ██████      ███   ███      ███      ███      ███         ███   █████████   ███   ███   ███            
+   ███               ███   ███               ███   █████████████████████   █████████████████████      ███   ███   ███   ███   ███      ███      ███   ███   ███   ███   ███   ███      ███   ███   ███            
+      ███         ███         ███         ███      ██████   ███   ██████   ██████   ███   ██████      ███   ███   ███      ███         ███      ███   ███   ███   ███   ███   ███         ███      ███            
+         ███   ███               ███   ███                  ███                     ███               ███   ██████         ███         █████████         ███         ███      ███         ███      ███            
+            ███                     ███                  █████████               █████████            ███                                                                                          ███            
+                                                                                                      ████████████████████████████████████████████████████████████████████████████████████████████████            
+*/
+const Buffer sprite_logo = (Buffer){
+    .width = 72,
+    .height = 29,
+    .data = (uint8_t[]){
+        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x1,  0x0,  0x0,  0x0,  0x0,  0x0,
+        0x0,  0x0,  0x20, 0x1,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x20, 0xc1, 0x7,  0x0,
+        0x63, 0x8,  0x0,  0x3,  0x0,  0x20, 0xe1, 0xf,  0x0,  0x63, 0xc,  0x0,  0x3,  0x0,  0x20,
+        0x71, 0x1c, 0x0,  0x63, 0xc,  0x0,  0x3,  0x0,  0x20, 0x31, 0x18, 0x0,  0x3,  0xc,  0x0,
+        0x0,  0x0,  0x20, 0x31, 0x0,  0xf,  0x63, 0x3e, 0x1f, 0x63, 0xe3, 0x20, 0x71, 0x80, 0x1f,
+        0x63, 0x3e, 0x3f, 0xe3, 0xf3, 0x21, 0xe1, 0xc3, 0x39, 0x63, 0x8c, 0x31, 0x63, 0x18, 0x23,
+        0x81, 0xcf, 0x30, 0x63, 0xc,  0x30, 0x63, 0x18, 0x23, 0x1,  0xdc, 0x30, 0x63, 0xc,  0x3c,
+        0x63, 0xf8, 0x23, 0x31, 0xd8, 0x30, 0x63, 0xc,  0x3f, 0x63, 0xf8, 0x23, 0x31, 0xd8, 0x30,
+        0x63, 0x8c, 0x33, 0x63, 0x18, 0x20, 0x71, 0xdc, 0x39, 0x63, 0x8c, 0x31, 0x63, 0x18, 0x23,
+        0xe1, 0x8f, 0x1f, 0x63, 0xbc, 0x3f, 0x63, 0xf0, 0x21, 0xc1, 0x7,  0xf,  0x63, 0x38, 0x37,
+        0x63, 0xe0, 0x20, 0x1,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x20, 0x1,  0x0,  0x0,
+        0x0,  0x0,  0x0,  0x0,  0x0,  0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
+        0x0,  0x0,  0x0,  0x0,  0x4,  0x0,  0x0,  0x0,  0x2,  0x44, 0x10, 0x10, 0x38, 0x4,  0x0,
+        0x0,  0x8,  0x2,  0xaa, 0x28, 0x38, 0x38, 0x14, 0xe0, 0x0,  0x4,  0x2,  0x92, 0x44, 0x7c,
+        0xd6, 0x34, 0x25, 0x89, 0xae, 0x2,  0x82, 0x82, 0xfe, 0xfe, 0x54, 0x25, 0x55, 0xa5, 0x2,
+        0x44, 0x44, 0xd6, 0xd6, 0x54, 0x22, 0x55, 0x45, 0x2,  0x28, 0x28, 0x10, 0x10, 0x34, 0xe2,
+        0x88, 0x44, 0x2,  0x10, 0x10, 0x38, 0x38, 0x4,  0x0,  0x0,  0x0,  0x2,  0x0,  0x0,  0x0,
+        0x0,  0xfc, 0xff, 0xff, 0xff, 0x3}};

+ 281 - 0
solitaire/assets.h

@@ -0,0 +1,281 @@
+#include <furi.h>
+#include "src/util/buffer.h"
+
+/*
+      ██████   
+   ███   ███   
+███      ███   
+████████████   
+         ███   
+*/
+extern const Buffer sprite_4;
+/*
+████████████   
+         ███   
+      ███      
+   ███         
+   ███         
+*/
+extern const Buffer sprite_7;
+/*
+   ███         ███   
+███   ███   ███   ███
+███      ███      ███
+███               ███
+   ███         ███   
+      ███   ███      
+         ███         
+*/
+extern const Buffer sprite_hearths;
+/*
+   ██████      
+███      ███   
+████████████   
+███      ███   
+███      ███   
+*/
+extern const Buffer sprite_A;
+/*
+                     
+      ███   ███   ███
+   ███   ███   ███   
+      ███   ███   ███
+   ███   ███   ███   
+      ███   ███   ███
+   ███   ███   ███   
+*/
+extern const Buffer sprite_pattern_small;
+/*
+                  ███                        
+   ███      ████████████                     
+   ████████████████████████      ███         
+   ████████████████████████   ███   ███      
+   ███      ████████████         ███         
+                                             
+         ███            ███                  
+      ███   ███      ████████████      ███   
+         ███      ████████████████████████   
+                  ████████████████████████   
+                     ████████████      ███   
+                                             
+                  ███            ███         
+   ███      ████████████      ███   ███      
+   ████████████████████████      ███         
+   ████████████████████████            ███   
+   ███      ████████████            ███      
+                                    ███      
+         ███         ███            ███      
+      ███            ███   ███      ███      
+      ███         ███      ███   ███         
+*/
+extern const Buffer sprite_pattern_big;
+/*
+                                          █████████████████████████████████████████████            
+                                       ███                                             ███         
+                                       █████████████████████████████████████████████   ███         
+                                    ███                                             ██████         
+                                    █████████████████████████████████████████████   ██████         
+                                 ███                                             █████████         
+                                 ███   ███      ███               ███            █████████         
+                                 ███   ███   ███   ███         █████████         █████████         
+                                 █████████████████████████████████████████████   █████████         
+                              ███                                             ████████████         
+                              ███   ███      ███               ███            ████████████         
+                              ███   ███   ███   ███         █████████         ████████████         
+            ███████████████████████████████████████████████████████████████   ████████████         
+         ███                                             ███               ███████████████         
+         ███   ███      ███               ███            ██████            ███████████████         
+         ███   ███   ███   ███         █████████         █████████         ███████████████         
+         ███   ███   ███   ███      ███████████████      ███████████████   ███████████████         
+         ███   ███   ███   ███   █████████████████████   ██████         ██████████████████         
+         ███   ███   ███   ███      ███████████████      █████████      ██████████████████         
+         ███   ███      ███            █████████         █████████      ██████████████████         
+         ███                              ███            █████████      ██████████████████         
+         ███                                             ████████████   ███████████████            
+         ███                                             ████████████   ███████████████            
+         ███                                             ███████████████████████████               
+         ███                                             ███████████████████████████               
+         ███            ███                              ████████████████████████                  
+         ███         █████████            ███      ███   ████████████████████████                  
+         ███      ███████████████      ███   ███   ███   ████████████████████████                  
+         ███   █████████████████████   ███   ███   ███   ████████████████████████                  
+         ███      ███████████████      ███   ███   ███   █████████████████████                     
+         ███         █████████         ███   ███   ███   █████████████████████                     
+         ███            ███               ███      ███   █████████████████████                     
+         ███                                             █████████████████████                     
+            █████████████████████████████████████████████   ███████████████                        
+            ███                                             ███████████████                        
+               █████████████████████████████████████████████   ████████████                        
+               ███            ███               ███      ███   ████████████                        
+               ███                                             █████████                           
+                  █████████████████████████████████████████████   ██████                           
+                  ███            ███               ███      ███   ██████                           
+                  ███                                             ██████                           
+                     █████████████████████████████████████████████   ███                           
+                     ███                                             ███                           
+                        █████████████████████████████████████████████                              
+*/
+extern const Buffer sprite_main_image;
+/*
+   ██████      
+███      ███   
+███      ███   
+███   ███      
+   ███   ███   
+*/
+extern const Buffer sprite_Q;
+/*
+   ██████      
+███      ███   
+      ███      
+   ███         
+████████████   
+*/
+extern const Buffer sprite_2;
+/*
+█████████      
+         ███   
+   ██████      
+         ███   
+█████████      
+*/
+extern const Buffer sprite_3;
+/*
+███      ███   
+███   ███   ███
+███   ███   ███
+███   ███   ███
+███      ███   
+*/
+extern const Buffer sprite_10;
+/*
+███      ███   
+███   ███      
+██████         
+███   ███      
+███      ███   
+*/
+extern const Buffer sprite_K;
+/*
+████████████   
+███            
+█████████      
+         ███   
+████████████   
+*/
+extern const Buffer sprite_5;
+/*
+   ██████      
+███            
+█████████      
+███      ███   
+   ██████      
+*/
+extern const Buffer sprite_6;
+/*
+   ██████      
+███      ███   
+   █████████   
+         ███   
+   ██████      
+*/
+extern const Buffer sprite_9;
+/*
+         ███         
+      ███   ███      
+   ███         ███   
+███               ███
+   ███         ███   
+      ███   ███      
+         ███         
+*/
+extern const Buffer sprite_diamonds;
+/*
+████████████   
+███      ███   
+████████████   
+███      ███   
+████████████   
+*/
+extern const Buffer sprite_8;
+/*
+         ███   
+         ███   
+         ███   
+███      ███   
+   ██████      
+*/
+extern const Buffer sprite_J;
+/*
+         ███         
+      █████████      
+   ███████████████   
+█████████████████████
+██████   ███   ██████
+         ███         
+      █████████      
+*/
+extern const Buffer sprite_spades;
+/*
+      ████████████               ████████████      ███                                    ███   
+   ██████████████████         ███            ███   ███                                    ███   
+████████████████████████      ███               █████████      █████████      ███   ████████████
+████████████████████████         ██████            ███      ███         ███   ██████      ███   
+████████████████████████               ██████      ███         ████████████   ███         ███   
+████████████████████████                     ███   ███      ███         ███   ███         ███   
+   ██████████████████         ███            ███   ███      ███      ██████   ███         ███   
+      ████████████               ████████████      ██████   █████████   ███   ███         ██████
+*/
+extern const Buffer sprite_start;
+/*
+███         ███                     ███                  ███            ████████████               ███                                                                  ███                                       
+███         ███                     ███                  ███         ██████████████████            ███                                                                  ███                                       
+███         ███      █████████      ███         ████████████      ████████████████████████      █████████      █████████                  ████████████   █████████      ███      ███            ███   █████████   
+███████████████   ███         ███   ███      ███         ███      ████████████████████████         ███      ███         ███            ███            ███         ███   ███         ███      ███   ███         ███
+███         ███   ███         ███   ███      ███         ███      ████████████████████████         ███      ███         ███               █████████   ███         ███   ███         ███      ███   ███████████████
+███         ███   ███         ███   ███      ███         ███         ██████████████████            ███      ███         ███                        ██████         ███   ███            ██████      ███            
+███         ███      █████████      ███         ████████████            ████████████               ██████      █████████               ████████████      █████████      ███            ██████         ████████████
+*/
+extern const Buffer sprite_solve;
+/*
+      █████████      
+      █████████      
+██████   ███   ██████
+█████████████████████
+██████   ███   ██████
+         ███         
+      █████████      
+*/
+extern const Buffer sprite_clubs;
+/*
+██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
+███                                                                                                                                                                                                            ███
+███                                                                                                                                                                                                            ███
+███               ███████████████                                       ██████         ██████            ███                                    ██████                                                         ███
+███            █████████████████████                                    ██████         ██████         ██████                                    ██████                                                         ███
+███         █████████         █████████                                 ██████         ██████         ██████                                    ██████                                                         ███
+███         ██████               ██████                                 ██████                        ██████                                                                                                   ███
+███         ██████                              ████████████            ██████         ██████      ███████████████      ███████████████         ██████         ██████   ██████         █████████               ███
+███         █████████                        ██████████████████         ██████         ██████      ███████████████      ██████████████████      ██████         ███████████████      ███████████████            ███
+███            ███████████████            █████████      █████████      ██████         ██████         ██████         ██████         ██████      ██████         ██████            ██████         ██████         ███
+███                  ███████████████      ██████            ██████      ██████         ██████         ██████                        ██████      ██████         ██████            ██████         ██████         ███
+███                           █████████   ██████            ██████      ██████         ██████         ██████                  ████████████      ██████         ██████            █████████████████████         ███
+███         ██████               ██████   ██████            ██████      ██████         ██████         ██████            ██████████████████      ██████         ██████            █████████████████████         ███
+███         ██████               ██████   ██████            ██████      ██████         ██████         ██████         █████████      ██████      ██████         ██████            ██████                        ███
+███         █████████         █████████   █████████      █████████      ██████         ██████         ██████         ██████         ██████      ██████         ██████            ██████         ██████         ███
+███            █████████████████████         ██████████████████         ██████         ██████         ████████████   █████████████████████      ██████         ██████               ███████████████            ███
+███               ███████████████               ████████████            ██████         ██████            █████████      █████████   ██████      ██████         ██████                  █████████               ███
+███                                                                                                                                                                                                            ███
+███                                                                                                                                                                                                            ███
+██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
+                                                                                                      ███                                                                                          ███            
+      ███         ███               ███                     ███                  █████████            ███                                                                        ███               ███            
+   ███   ███   ███   ███         ███   ███               █████████               █████████            ███   ███                        █████████                              ███                  ███            
+   ███      ███      ███      ███         ███         ███████████████      ██████   ███   ██████      ███   ██████      ███   ███      ███      ███      ███         ███   █████████   ███   ███   ███            
+   ███               ███   ███               ███   █████████████████████   █████████████████████      ███   ███   ███   ███   ███      ███      ███   ███   ███   ███   ███   ███      ███   ███   ███            
+      ███         ███         ███         ███      ██████   ███   ██████   ██████   ███   ██████      ███   ███   ███      ███         ███      ███   ███   ███   ███   ███   ███         ███      ███            
+         ███   ███               ███   ███                  ███                     ███               ███   ██████         ███         █████████         ███         ███      ███         ███      ███            
+            ███                     ███                  █████████               █████████            ███                                                                                          ███            
+                                                                                                      ████████████████████████████████████████████████████████████████████████████████████████████████            
+*/
+extern const Buffer sprite_logo;

BIN
solitaire/assets/10.png


BIN
solitaire/assets/2.png


BIN
solitaire/assets/3.png


BIN
solitaire/assets/4.png


BIN
solitaire/assets/5.png


BIN
solitaire/assets/6.png


BIN
solitaire/assets/7.png


BIN
solitaire/assets/8.png


BIN
solitaire/assets/9.png


BIN
solitaire/assets/A.png


BIN
solitaire/assets/J.png


BIN
solitaire/assets/K.png


BIN
solitaire/assets/Q.png


BIN
solitaire/assets/card_graphics.png


BIN
solitaire/assets/clubs.png


BIN
solitaire/assets/diamonds.png


BIN
solitaire/assets/hearths.png


BIN
solitaire/assets/joker.png


BIN
solitaire/assets/logo.png


BIN
solitaire/assets/main_image.png


BIN
solitaire/assets/pattern_big.png


BIN
solitaire/assets/pattern_small.png


BIN
solitaire/assets/solitaire_main.png


BIN
solitaire/assets/solve.png


BIN
solitaire/assets/spades.png


BIN
solitaire/assets/start.png


+ 0 - 63
solitaire/defines.h

@@ -1,63 +0,0 @@
-#pragma once
-#include <furi.h>
-#include <input/input.h>
-#include <gui/elements.h>
-#include <flipper_format/flipper_format.h>
-#include <flipper_format/flipper_format_i.h>
-#include "common/card.h"
-#include "common/queue.h"
-
-#define APP_NAME "Solitaire"
-
-typedef enum {
-    EventTypeTick,
-    EventTypeKey,
-} EventType;
-
-typedef struct {
-    EventType type;
-    InputEvent input;
-} AppEvent;
-
-typedef enum {
-    GameStateGameOver,
-    GameStateStart,
-    GameStatePlay,
-    GameStateAnimate,
-} PlayState;
-
-typedef struct {
-    uint8_t* buffer;
-    Card card;
-    int8_t deck;
-    int indexes[4];
-    float x;
-    float y;
-    float vx;
-    float vy;
-    bool started;
-} CardAnimation;
-
-typedef struct {
-    Deck deck;
-    Hand bottom_columns[7];
-    Card top_cards[4];
-    bool dragging_deck;
-    uint8_t dragging_column;
-    Hand dragging_hand;
-
-    InputKey input;
-
-    bool started;
-    bool had_change;
-    bool processing;
-    bool longPress;
-    PlayState state;
-    unsigned int last_tick;
-    uint8_t selectRow;
-    uint8_t selectColumn;
-    int8_t selected_card;
-    CardAnimation animation;
-    uint8_t* buffer;
-    FuriMutex* mutex;
-} GameState;

+ 25 - 0
solitaire/docs/CHANGELOG.md

@@ -0,0 +1,25 @@
+## v2.0.0
+
+- App rewrite
+- Added quick solve
+- New effects and sounds
+- Removed hacky canvas manipulation
+
+## v1.1.3
+
+- Aligned with the latest mutex updates
+ 
+## v1.1.0
+
+- Fixed ability to place ♠3 on empty foundations
+- Implemented Dolphin Deeds
+- Aligned with the latest mutex updates
+
+## v1.0.1
+
+- Falling cards now working as intended
+- Fixed game state reset issues
+
+## v1.0
+
+- Initial release 

+ 31 - 0
solitaire/docs/README.md

@@ -0,0 +1,31 @@
+# Solitaire - Klondike for Flipper Zero
+
+Solitaire, the classic Klondike version, now available on your Flipper Zero. 
+
+## Features
+
+* **Auto-Solve:** Ability to automatically solve the game when all cards are flipped.
+* **Animated Card Movements:** Animated transitions during solve and deal.
+* **Time Tracking:** Displays the time it took to solve at the end of each game.
+* **Falling Cards:** Enjoy a visually satisfying cascade of cards when you win.
+
+## Shortcuts
+
+* **Long Press Any Arrow:** Jump to the furthest point in that direction.
+* **Long Press Center:** Automatically place the card in the top right section.
+* **Long Press Back:** Close the application instantly.
+
+## Rules
+
+- **Empty Tableau Spots:** Only a King can be placed in an empty tableau spot.
+- **Tableau Arrangement:** Cards must be arranged in descending order and alternating colors (e.g., red ten on black nine) within the tableau.
+- **Empty Foundation Piles:** Only an Ace can be placed in an empty foundation pile.
+- **Foundation Pile Arrangement:** Cards must be in ascending order and in the same suit.
+
+## How to Play
+
+- Use the directional arrows to navigate and move cards.
+- Pick up cards with the center button.
+- Aim to build up four foundation piles in ascending order, separated by suit.
+- Flip and move cards within the tableau to reveal hidden cards.
+- You can place back cards in the same spot you picked them up.

+ 43 - 0
solitaire/game_state.h

@@ -0,0 +1,43 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/canvas.h>
+#include <gui/gui.h>
+#include "src/util/buffer.h"
+#include "src/util/list.h"
+#include "src/util/card.h"
+#include <notification/notification.h>
+
+typedef struct {
+    Card* card;
+    Vector position;
+    Vector velocity;
+} AnimatedCard;
+
+typedef struct {
+    Canvas* canvas;
+    Gui* gui;
+    FuriPubSub* input;
+    FuriPubSubSubscription* input_subscription;
+    bool exit;
+    bool isDirty;
+    bool clearBuffer;
+    bool lateRender;
+    uint8_t scene_switch;
+    Buffer* buffer;
+    NotificationApp* notification_app;
+    uint8_t selected[2];
+    uint8_t selected_card;
+
+    List* deck;
+    List* waste;
+    List* hand;
+    List* foundation[4];
+    List* tableau[7];
+
+    AnimatedCard animated_card;
+    double delta_time;
+    size_t game_start;
+    size_t game_end;
+
+} GameState;

BIN
solitaire/screenshots/catalog_1.png


BIN
solitaire/screenshots/catalog_2.png


BIN
solitaire/screenshots/catalog_3.png


BIN
solitaire/screenshots/catalog_4.png


BIN
solitaire/screenshots/catalog_5.png


BIN
solitaire/screenshots/catalog_6.png


BIN
solitaire/screenshots/solitaire.png


+ 140 - 542
solitaire/solitaire.c

@@ -1,583 +1,181 @@
-#include <stdlib.h>
-#include <dolphin/dolphin.h>
 #include <furi.h>
-#include <gui/canvas_i.h>
-#include "defines.h"
-#include "common/ui.h"
-#include "solitaire_icons.h"
-#include <notification/notification.h>
+#include <gui/gui.h>
+#include <input/input.h>
 #include <notification/notification_messages.h>
-void init(GameState* game_state);
 
-const NotificationSequence sequence_fail = {
-    &message_vibro_on,
-    &message_note_c4,
-    &message_delay_10,
-    &message_vibro_off,
-    &message_sound_off,
-    &message_delay_10,
+#include "game_state.h"
+#include "src/util/list.h"
+#include "src/scene/scene_setup.h"
+#include "src/util/helpers.h"
 
-    &message_vibro_on,
-    &message_note_a3,
-    &message_delay_10,
-    &message_vibro_off,
-    &message_sound_off,
-    NULL,
-};
-int8_t columns[7][3] = {
-    {1, 1, 25},
-    {19, 1, 25},
-    {37, 1, 25},
-    {55, 1, 25},
-    {73, 1, 25},
-    {91, 1, 25},
-    {109, 1, 25},
-};
+static List* game_logic;
+static ListItem* current_state;
+static FuriMutex* update_mutex;
 
-bool can_place_card(Card where, Card what) {
-    bool a_black = where.pip == 0 || where.pip == 3;
-    bool b_black = what.pip == 0 || what.pip == 3;
-    if(a_black == b_black) return false;
+static void gui_input_events_callback(const void* value, void* ctx) {
+    furi_mutex_acquire(update_mutex, FuriWaitForever);
+    GameState* instance = ctx;
+    const InputEvent* event = value;
 
-    int8_t a_letter = (int8_t)where.character;
-    int8_t b_letter = (int8_t)what.character;
-    if(a_letter == 12) a_letter = -1;
-    if(b_letter == 12) b_letter = -1;
+    if(event->key == InputKeyBack && event->type == InputTypeLong) {
+        FURI_LOG_W("INPUT", "EXIT");
+        instance->exit = true;
+    }
 
-    return (a_letter - 1) == b_letter;
+    if(current_state) {
+        ((GameLogic*)current_state->data)->input(instance, event->key, event->type);
+    }
+    furi_mutex_release(update_mutex);
 }
 
-static void draw_scene(Canvas* const canvas, const GameState* game_state) {
-    if(game_state->had_change) {
-        int deckIndex = game_state->deck.index;
-        if(game_state->dragging_deck) deckIndex--;
-
-        if((game_state->deck.index < (game_state->deck.card_count - 1) ||
-            game_state->deck.index == -1) &&
-           game_state->deck.card_count > 0) {
-            draw_card_back_at(columns[0][0], columns[0][1], canvas);
-            if(game_state->selectRow == 0 && game_state->selectColumn == 0) {
-                draw_rounded_box(
-                    canvas,
-                    columns[0][0] + 1,
-                    columns[0][1] + 1,
-                    CARD_WIDTH - 2,
-                    CARD_HEIGHT - 2,
-                    Inverse);
-            }
-        } else
-            draw_card_space(
-                columns[0][0],
-                columns[0][1],
-                game_state->selectRow == 0 && game_state->selectColumn == 0,
-                canvas);
-        //deck side
-        if(deckIndex >= 0) {
-            Card c = game_state->deck.cards[deckIndex];
-            draw_card_at_colored(
-                columns[1][0],
-                columns[1][1],
-                c.pip,
-                c.character,
-                game_state->selectRow == 0 && game_state->selectColumn == 1,
-                canvas);
-        } else
-            draw_card_space(
-                columns[1][0],
-                columns[1][1],
-                game_state->selectRow == 0 && game_state->selectColumn == 1,
-                canvas);
-
-        for(uint8_t i = 0; i < 4; i++) {
-            Card current = game_state->top_cards[i];
-            bool selected = game_state->selectRow == 0 && game_state->selectColumn == (i + 3);
-            if(current.disabled) {
-                draw_card_space(columns[i + 3][0], columns[i + 3][1], selected, canvas);
-            } else {
-                draw_card_at(
-                    columns[i + 3][0], columns[i + 3][1], current.pip, current.character, canvas);
-                if(selected) {
-                    draw_rounded_box(
-                        canvas,
-                        columns[i + 3][0],
-                        columns[i + 3][1],
-                        CARD_WIDTH,
-                        CARD_HEIGHT,
-                        Inverse);
-                }
-            }
-        }
-
-        for(uint8_t i = 0; i < 7; i++) {
-            bool selected = game_state->selectRow == 1 && game_state->selectColumn == i;
-            int8_t index = (game_state->bottom_columns[i].index - 1 - game_state->selected_card);
-            if(index < 0) index = 0;
-            draw_hand_column(
-                game_state->bottom_columns[i],
-                columns[i][0],
-                columns[i][2],
-                selected ? index : -1,
-                canvas);
-        }
-
-        int8_t pos[2] = {
-            columns[game_state->selectColumn][0],
-            columns[game_state->selectColumn][game_state->selectRow + 1]};
-
-        /*     draw_icon_clip(canvas, &I_card_graphics, pos[0] + CARD_HALF_WIDTH, pos[1] + CARD_HALF_HEIGHT, 30, 5, 5, 5,
-                            Filled);*/
-
-        if(game_state->dragging_hand.index > 0) {
-            draw_hand_column(
-                game_state->dragging_hand,
-                pos[0] + CARD_HALF_WIDTH + 3,
-                pos[1] + CARD_HALF_HEIGHT + 3,
-                -1,
-                canvas);
+static GameState* prepare() {
+    game_logic = list_make();
+    update_mutex = (FuriMutex*)furi_mutex_alloc(FuriMutexTypeNormal);
+
+    //Add scenes to the logic list
+    list_push_back(&main_screen, game_logic);
+    list_push_back(&intro_screen, game_logic);
+    list_push_back(&play_screen, game_logic);
+    list_push_back(&solve_screen, game_logic);
+    list_push_back(&falling_screen, game_logic);
+    list_push_back(&result_screen, game_logic);
+
+    current_state = game_logic->head;
+
+    GameState* instance = malloc(sizeof(GameState));
+    ((GameLogic*)current_state->data)->start(instance);
+
+    instance->hand = list_make();
+    instance->deck = list_make();
+    instance->waste = list_make();
+    for(int i = 0; i < 7; i++) {
+        if(i < 4) {
+            instance->foundation[i] = list_make();
         }
-
-        clone_buffer(get_buffer(canvas), game_state->animation.buffer);
-    } else {
-        clone_buffer(game_state->animation.buffer, get_buffer(canvas));
+        instance->tableau[i] = list_make();
     }
-}
 
-static void draw_animation(Canvas* const canvas, const GameState* game_state) {
-    if(!game_state->animation.started) {
-        draw_scene(canvas, game_state);
-    } else {
-        clone_buffer(game_state->animation.buffer, get_buffer(canvas));
+    instance->animated_card.position = VECTOR_ZERO;
+    instance->animated_card.velocity = VECTOR_ZERO;
 
-        draw_card_at(
-            (int8_t)game_state->animation.x,
-            (int8_t)game_state->animation.y,
-            game_state->animation.card.pip,
-            game_state->animation.card.character,
-            canvas);
-    }
-
-    clone_buffer(get_buffer(canvas), game_state->animation.buffer);
-}
+    instance->buffer = buffer_create(SCREEN_WIDTH, SCREEN_HEIGHT, false);
+    instance->input = furi_record_open(RECORD_INPUT_EVENTS);
+    instance->gui = furi_record_open(RECORD_GUI);
+    instance->canvas = gui_direct_draw_acquire(instance->gui);
+    instance->notification_app = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION);
+    notification_message_block(instance->notification_app, &sequence_display_backlight_enforce_on);
 
-static void render_callback(Canvas* const canvas, void* ctx) {
-    const GameState* game_state = ctx;
-    furi_mutex_acquire(game_state->mutex, 25);
-    if(game_state == NULL) {
-        return;
-    }
+    instance->input_subscription =
+        furi_pubsub_subscribe(instance->input, gui_input_events_callback, instance);
 
-    switch(game_state->state) {
-    case GameStateAnimate:
-        draw_animation(canvas, game_state);
-        break;
-    case GameStateStart:
-        canvas_draw_icon(canvas, 0, 0, &I_solitaire_main);
-        break;
-    case GameStatePlay:
-        draw_scene(canvas, game_state);
-        break;
-    default:
-        break;
-    }
-
-    furi_mutex_release(game_state->mutex);
+    return instance;
 }
 
-void remove_drag(GameState* game_state) {
-    if(game_state->dragging_deck) {
-        remove_from_deck(game_state->deck.index, &(game_state->deck));
-        game_state->dragging_deck = false;
-    } else if(game_state->dragging_column < 7) {
-        game_state->dragging_column = 8;
+static void cleanup(GameState* instance) {
+    furi_pubsub_unsubscribe(instance->input, instance->input_subscription);
+    notification_message_block(
+        instance->notification_app, &sequence_display_backlight_enforce_auto);
+
+    list_free(instance->hand);
+    list_free(instance->deck);
+    list_free(instance->waste);
+    for(int i = 0; i < 7; i++) {
+        if(i < 4) {
+            list_free(instance->foundation[i]);
+        }
+        list_free(instance->tableau[i]);
     }
-    game_state->dragging_hand.index = 0;
+
+    furi_mutex_free(update_mutex);
+    instance->canvas = NULL;
+    gui_direct_draw_release(instance->gui);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_INPUT_EVENTS);
+    list_clear(game_logic);
+    buffer_release(instance->buffer);
+    free(instance);
 }
 
-bool handleInput(GameState* game_state) {
-    Hand currentHand = game_state->bottom_columns[game_state->selectColumn];
-    switch(game_state->input) {
-    case InputKeyUp:
-        if(game_state->selectRow > 0) {
-            int first = first_non_flipped_card(currentHand);
-            first = currentHand.index - first;
-            if((first - 1) > game_state->selected_card && game_state->dragging_hand.index == 0 &&
-               !game_state->longPress) {
-                game_state->selected_card++;
-            } else {
-                game_state->selectRow--;
-                game_state->selected_card = 0;
-            }
-        }
-        break;
-    case InputKeyDown:
-        if(game_state->selectRow < 1) {
-            game_state->selectRow++;
-            game_state->selected_card = 0;
-        } else {
-            if(game_state->selected_card > 0) {
-                if(game_state->longPress)
-                    game_state->selected_card = 0;
-                else
-                    game_state->selected_card--;
-            }
-        }
-        break;
-    case InputKeyRight:
-        if(game_state->selectColumn < 6) {
-            game_state->selectColumn++;
-            game_state->selected_card = 0;
-        }
-        break;
-    case InputKeyLeft:
-        if(game_state->selectColumn > 0) {
-            game_state->selectColumn--;
-            game_state->selected_card = 0;
-        }
-        break;
-    case InputKeyOk:
-        return true;
-        break;
-    default:
-        break;
-    }
-    if(game_state->selectRow == 0 && game_state->selectColumn == 2) {
-        if(game_state->input == InputKeyRight)
-            game_state->selectColumn++;
-        else
-            game_state->selectColumn--;
+static void next_scene(GameState* instance) {
+    FURI_LOG_W("SCENE", "Next scene");
+    current_state = current_state->next;
+    if(current_state == NULL) {
+        current_state = game_logic->head;
     }
-    if(game_state->dragging_hand.index > 0) game_state->selected_card = 0;
-    return false;
+    ((GameLogic*)current_state->data)->start(instance);
 }
 
-bool place_on_top(Card* where, Card what) {
-    if(where->disabled && what.character == 12) {
-        where->disabled = what.disabled;
-        where->pip = what.pip;
-        where->character = what.character;
-        return true;
-    } else if(where->pip == what.pip) {
-        int8_t a_letter = (int8_t)where->character;
-        int8_t b_letter = (int8_t)what.character;
-        if(a_letter == 12) a_letter = -1;
-        if(b_letter == 12) b_letter = -1;
-        if(where->disabled && b_letter != -1) return false;
-
-        if((a_letter + 1) == b_letter) {
-            where->disabled = what.disabled;
-            where->pip = what.pip;
-            where->character = what.character;
-            return true;
-        }
+static void prev_scene(GameState* instance) {
+    FURI_LOG_W("SCENE", "Prev scene");
+    current_state = game_logic->head;
+    if(current_state->prev == NULL) {
+        instance->exit = true;
+        return;
     }
-    return false;
+    ((GameLogic*)current_state->data)->start(instance);
 }
 
-void tick(GameState* game_state, NotificationApp* notification) {
-    game_state->last_tick = furi_get_tick();
-    uint8_t row = game_state->selectRow;
-    uint8_t column = game_state->selectColumn;
-    if(game_state->state != GameStatePlay && game_state->state != GameStateAnimate) return;
-    bool wasAction = false;
-    if(game_state->state == GameStatePlay) {
-        if(game_state->top_cards[0].character == 11 && game_state->top_cards[1].character == 11 &&
-           game_state->top_cards[2].character == 11 && game_state->top_cards[3].character == 11) {
-            game_state->state = GameStateAnimate;
-            game_state->had_change = true;
-            dolphin_deed(DolphinDeedPluginGameWin);
-
-            return;
+static void direct_draw_run(GameState* instance) {
+    if(!check_pointer(instance)) return;
+
+    size_t currFrameTime;
+    size_t lastFrameTime = curr_time();
+    instance->lateRender = false;
+
+    furi_thread_set_current_priority(FuriThreadPriorityIdle);
+    do {
+        FuriStatus status = furi_mutex_acquire(update_mutex, 20);
+        if(!status) continue;
+
+        GameLogic* curr_state = (GameLogic*)current_state->data;
+        currFrameTime = curr_time();
+        instance->delta_time = (currFrameTime - lastFrameTime) / 64000000.0f;
+        lastFrameTime = currFrameTime;
+
+        check_pointer(curr_state);
+        curr_state->update(instance);
+        if(instance->scene_switch == 1) {
+            next_scene(instance);
+        } else if(instance->scene_switch == 2) {
+            prev_scene(instance);
         }
-    }
-    if(handleInput(game_state)) {
-        if(game_state->state == GameStatePlay) {
-            if(game_state->longPress && game_state->dragging_hand.index == 1) {
-                for(uint8_t i = 0; i < 4; i++) {
-                    if(place_on_top(
-                           &(game_state->top_cards[i]), game_state->dragging_hand.cards[0])) {
-                        remove_drag(game_state);
-                        wasAction = true;
-                        break;
-                    }
-                }
+        check_pointer(curr_state);
+        check_pointer(instance);
+        check_pointer(instance->canvas);
+        check_pointer(instance->buffer);
+        instance->scene_switch = 0;
+        if(curr_state && instance->isDirty && instance->canvas && instance->buffer) {
+            canvas_reset(instance->canvas);
+
+            if(instance->lateRender) {
+                buffer_swap_back(instance->buffer);
+                buffer_render(instance->buffer, instance->canvas);
+                curr_state->render(instance);
             } else {
-                if(row == 0 && column == 0 && game_state->dragging_hand.index == 0) {
-                    FURI_LOG_D(APP_NAME, "Drawing card");
-                    game_state->deck.index++;
-                    wasAction = true;
-                    if(game_state->deck.index >= (game_state->deck.card_count))
-                        game_state->deck.index = -1;
-                }
-                //pick/place from deck
-                else if(row == 0 && column == 1) {
-                    //place
-                    if(game_state->dragging_deck) {
-                        wasAction = true;
-                        game_state->dragging_deck = false;
-                        game_state->dragging_hand.index = 0;
-                    }
-                    //pick
-                    else {
-                        if(game_state->dragging_hand.index == 0 && game_state->deck.index >= 0) {
-                            wasAction = true;
-                            game_state->dragging_deck = true;
-                            add_to_hand(
-                                &(game_state->dragging_hand),
-                                game_state->deck.cards[game_state->deck.index]);
-                        }
-                    }
-                }
-                //place on top row
-                else if(row == 0 && game_state->dragging_hand.index == 1) {
-                    column -= 3;
-                    Card currCard = game_state->dragging_hand.cards[0];
-                    wasAction = place_on_top(&(game_state->top_cards[column]), currCard);
-                    if(wasAction) remove_drag(game_state);
-                }
-                //pick/place from bottom
-                else if(row == 1) {
-                    Hand* curr_hand = &(game_state->bottom_columns[column]);
-                    //pick up
-                    if(game_state->dragging_hand.index == 0) {
-                        Card curr_card = curr_hand->cards[curr_hand->index - 1];
-                        if(curr_card.flipped) {
-                            curr_hand->cards[curr_hand->index - 1].flipped = false;
-                            wasAction = true;
-                        } else {
-                            if(curr_hand->index > 0) {
-                                extract_hand_region(
-                                    curr_hand,
-                                    &(game_state->dragging_hand),
-                                    curr_hand->index - game_state->selected_card - 1);
-                                game_state->selected_card = 0;
-                                game_state->dragging_column = column;
-                                wasAction = true;
-                            }
-                        }
-                    }
-                    //place
-                    else {
-                        Card first = game_state->dragging_hand.cards[0];
-                        if(game_state->dragging_column == column ||
-                           (curr_hand->index == 0 && first.character == 11) ||
-                           can_place_card(curr_hand->cards[curr_hand->index - 1], first)) {
-                            add_hand_region(curr_hand, &(game_state->dragging_hand));
-                            remove_drag(game_state);
-                            wasAction = true;
-                        }
-                    }
-                }
+                curr_state->render(instance);
+                buffer_swap_back(instance->buffer);
+                buffer_render(instance->buffer, instance->canvas);
             }
+            canvas_commit(instance->canvas);
 
-            if(!wasAction) {
-                notification_message(notification, &sequence_fail);
-            }
-        }
-    }
-    if(game_state->state == GameStateAnimate) {
-        if(game_state->animation.started && !game_state->longPress &&
-           game_state->input == InputKeyOk) {
-            init(game_state);
-            game_state->state = GameStateStart;
-        }
-
-        game_state->animation.started = true;
-        if(game_state->animation.x < -20 || game_state->animation.x > 128) {
-            game_state->animation.deck++;
-            if(game_state->animation.deck > 3) game_state->animation.deck = 0;
-            int8_t cardIndex = 11 - game_state->animation.indexes[game_state->animation.deck];
-
-            if(game_state->animation.indexes[0] == 13 && game_state->animation.indexes[1] == 13 &&
-               game_state->animation.indexes[2] == 13 && game_state->animation.indexes[3] == 13) {
-                init(game_state);
-                game_state->state = GameStateStart;
-                return;
-            }
-
-            if(cardIndex == -1) cardIndex = 12;
-            game_state->animation.card = (Card){
-                game_state->top_cards[game_state->animation.deck].pip, cardIndex, false, false};
-            game_state->animation.indexes[game_state->animation.deck]++;
-            game_state->animation.vx = -(rand() % 3 + 1) * (rand() % 2 == 1 ? 1 : -1);
-            game_state->animation.vy = (rand() % 3 + 1);
-            game_state->animation.x = columns[game_state->animation.deck + 3][0];
-            game_state->animation.y = columns[game_state->animation.deck + 3][1];
-        }
-        game_state->animation.x += game_state->animation.vx;
-        game_state->animation.y -= game_state->animation.vy;
-        game_state->animation.vy -= 1;
-        if(game_state->animation.vy < -10) game_state->animation.vy = -10;
+            if(instance->clearBuffer) buffer_clear(instance->buffer);
 
-        if(game_state->animation.y > 41) {
-            game_state->animation.y = 41;
-            game_state->animation.vy = -(game_state->animation.vy * 0.7f);
+            instance->clearBuffer = true;
+            instance->lateRender = false;
+            instance->isDirty = false;
         }
-    }
-}
-
-void init(GameState* game_state) {
-    dolphin_deed(DolphinDeedPluginGameStart);
-    game_state->selectColumn = 0;
-    game_state->selected_card = 0;
-    game_state->selectRow = 0;
-    generate_deck(&(game_state->deck), 1);
-    shuffle_deck(&(game_state->deck));
-    game_state->dragging_deck = false;
-    game_state->animation.started = false;
-    game_state->animation.deck = -1;
-    game_state->animation.x = -21;
-    game_state->state = GameStatePlay;
-    game_state->dragging_column = 8;
-
-    for(uint8_t i = 0; i < 7; i++) {
-        free_hand(&(game_state->bottom_columns[i]));
-        init_hand(&(game_state->bottom_columns[i]), 21);
-        game_state->bottom_columns[i].index = 0;
-        for(uint8_t j = 0; j <= i; j++) {
-            Card cur = remove_from_deck(0, &(game_state->deck));
-            cur.flipped = i != j;
-            add_to_hand(&(game_state->bottom_columns[i]), cur);
-        }
-    }
-
-    for(uint8_t i = 0; i < 4; i++) {
-        game_state->animation.indexes[i] = 0;
-        game_state->top_cards[i] = (Card){0, 0, true, false};
-    }
-    game_state->deck.index = -1;
-}
-
-void init_start(GameState* game_state) {
-    game_state->input = InputKeyMAX;
-    for(uint8_t i = 0; i < 7; i++)
-        init_hand(&(game_state->bottom_columns[i]), 21);
-
-    init_hand(&(game_state->dragging_hand), 13);
-    game_state->animation.buffer = make_buffer();
-}
-
-static void input_callback(InputEvent* input_event, void* ctx) {
-    FuriMessageQueue* event_queue = ctx;
-    furi_assert(event_queue);
-    AppEvent event = {.type = EventTypeKey, .input = *input_event};
-    furi_message_queue_put(event_queue, &event, FuriWaitForever);
-}
-
-static void update_timer_callback(void* ctx) {
-    FuriMessageQueue* event_queue = ctx;
-    furi_assert(event_queue);
-    AppEvent event = {.type = EventTypeTick};
-    furi_message_queue_put(event_queue, &event, 0);
+        furi_mutex_release(update_mutex);
+        furi_thread_yield();
+    } while(!instance->exit);
 }
 
 int32_t solitaire_app(void* p) {
     UNUSED(p);
     int32_t return_code = 0;
-    FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(AppEvent));
-    GameState* game_state = malloc(sizeof(GameState));
-    init_start(game_state);
-    set_card_graphics(&I_card_graphics);
-
-    game_state->state = GameStateStart;
-
-    game_state->processing = true;
-    game_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
-    if(!game_state->mutex) {
-        FURI_LOG_E(APP_NAME, "cannot create mutex\r\n");
-        return_code = 255;
-        goto free_and_exit;
-    }
-    NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
-
-    notification_message_block(notification, &sequence_display_backlight_enforce_on);
-
-    ViewPort* view_port = view_port_alloc();
-    view_port_draw_callback_set(view_port, render_callback, game_state);
-    view_port_input_callback_set(view_port, input_callback, event_queue);
-
-    FuriTimer* timer = furi_timer_alloc(update_timer_callback, FuriTimerTypePeriodic, event_queue);
-    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 30);
-
-    Gui* gui = furi_record_open("gui");
-    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
-
-    AppEvent event;
-    for(bool processing = true; processing;) {
-        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 150);
-        furi_mutex_acquire(game_state->mutex, FuriWaitForever);
-        game_state->had_change = false;
-        if(event_status == FuriStatusOk) {
-            if(event.type == EventTypeKey) {
-                game_state->had_change = true;
-                if(event.input.type == InputTypeLong) {
-                    game_state->longPress = true;
-                    switch(event.input.key) {
-                    case InputKeyUp:
-                    case InputKeyDown:
-                    case InputKeyRight:
-                    case InputKeyLeft:
-                    case InputKeyOk:
-                        game_state->input = event.input.key;
-                        break;
-                    case InputKeyBack:
-                        processing = false;
-                        return_code = 1;
-                    default:
-                        break;
-                    }
-                } else if(event.input.type == InputTypePress) {
-                    game_state->longPress = false;
-                    switch(event.input.key) {
-                    case InputKeyUp:
-                    case InputKeyDown:
-                    case InputKeyRight:
-                    case InputKeyLeft:
-                    case InputKeyOk:
-                        if(event.input.key == InputKeyOk && game_state->state == GameStateStart) {
-                            game_state->state = GameStatePlay;
-                            init(game_state);
-                        } else {
-                            game_state->input = event.input.key;
-                        }
-                        break;
-                    case InputKeyBack:
-                        init(game_state);
-                        processing = false;
-                        return_code = 1;
-                        break;
-                    default:
-                        break;
-                    }
-                }
-            } else if(event.type == EventTypeTick) {
-                tick(game_state, notification);
-                processing = game_state->processing;
-                game_state->input = InputKeyMAX;
-            }
-        }
-
-        furi_mutex_release(game_state->mutex);
-        view_port_update(view_port);
-    }
-
-    notification_message_block(notification, &sequence_display_backlight_enforce_auto);
-    furi_timer_free(timer);
-    view_port_enabled_set(view_port, false);
-    gui_remove_view_port(gui, view_port);
-    furi_record_close(RECORD_GUI);
-    furi_record_close(RECORD_NOTIFICATION);
-    view_port_free(view_port);
-    furi_mutex_free(game_state->mutex);
-
-free_and_exit:
-    free(game_state->animation.buffer);
-    ui_cleanup();
-    for(uint8_t i = 0; i < 7; i++)
-        free_hand(&(game_state->bottom_columns[i]));
+    GameState* instance = prepare();
 
-    free(game_state->deck.cards);
-    free(game_state);
-    furi_message_queue_free(event_queue);
+    direct_draw_run(instance);
 
+    cleanup(instance);
     return return_code;
 }

+ 105 - 0
solitaire/src/scene/falling_card.c

@@ -0,0 +1,105 @@
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+#include "falling_card.h"
+#include "../../game_state.h"
+#include "play_screen.h"
+#include "../util/helpers.h"
+
+static const NotificationSequence sequence_bounce = {
+    &message_vibro_on,
+    //    &message_note_c4,
+    &message_delay_10,
+    &message_vibro_off,
+    //    &message_sound_off,
+    NULL,
+};
+static uint8_t start_index = 0;
+static size_t tempTime = 0;
+
+void start_falling_screen(void* data) {
+    //draw the play screen once (without any other thing to have the initial state)
+    GameState* state = (GameState*)data;
+    buffer_clear(state->buffer);
+    render_play_screen(data);
+    state->clearBuffer = false;
+    state->isDirty = true;
+    start_index = 0;
+    tempTime = 0;
+    state->game_end = furi_get_tick();
+}
+
+void render_falling_screen(void* data) {
+    GameState* state = (GameState*)data;
+    if(state->animated_card.card != NULL) {
+        card_render_front(
+            state->animated_card.card,
+            state->animated_card.position.x,
+            state->animated_card.position.y,
+            false,
+            state->buffer,
+            22);
+    }
+}
+
+void update_falling_screen(void* data) {
+    GameState* state = (GameState*)data;
+    state->clearBuffer = false;
+    state->isDirty = true;
+
+    if(state->animated_card.card) {
+        if((curr_time() - tempTime) > 12) {
+            tempTime = curr_time();
+            state->animated_card.position.x += state->animated_card.velocity.x;
+            state->animated_card.position.y -= state->animated_card.velocity.y;
+
+            //bounce on the bottom
+            if(state->animated_card.position.y > 41) {
+                state->animated_card.velocity.y *= -0.8f;
+                state->animated_card.position.y = 41;
+                notification_message(
+                    state->notification_app, (const NotificationSequence*)&sequence_bounce);
+            } else {
+                state->animated_card.velocity.y--;
+                if(state->animated_card.velocity.y < -10) state->animated_card.velocity.y = -10;
+            }
+
+            //delete the card if it is outside the screen
+            if(state->animated_card.position.x < -18 || state->animated_card.position.x > 128) {
+                free(state->animated_card.card);
+                state->animated_card.card = NULL;
+            }
+        }
+
+    } else {
+        //When we find a foundation without any card means that we finished the animation
+        if(state->foundation[start_index]->count == 0) {
+            state->scene_switch = 1;
+            return;
+        }
+
+        //start with the next card
+        state->animated_card.card = list_pop_back(state->foundation[start_index]);
+        state->animated_card.position = (Vector){56 + start_index * 18, 1};
+
+        float r1 = 2.0 * (float)(rand() % 2) - 1.0; // random number in range -1 to 1
+        if(r1 == 0) r1 = 0.1;
+        float r2 = inverse_tanh(r1);
+        float vx = (float)(tanh(r2)) * (rand() % 3 + 1);
+        state->animated_card.velocity.x = vx == 0 ? 1 : vx;
+        state->animated_card.velocity.y = (rand() % 3 + 1);
+
+        start_index = (start_index + 1) % 4;
+    }
+}
+
+void input_falling_screen(void* data, InputKey key, InputType type) {
+    GameState* state = (GameState*)data;
+    if(key == InputKeyOk && type == InputTypePress) {
+        //remove card that is currently animated
+        if(state->animated_card.card != NULL) {
+            free(state->animated_card.card);
+            state->animated_card.card = NULL;
+        }
+        state->scene_switch = 1;
+    }
+}

+ 11 - 0
solitaire/src/scene/falling_card.h

@@ -0,0 +1,11 @@
+#pragma once
+#include <furi.h>
+#include <input/input.h>
+
+void start_falling_screen(void* data);
+
+void render_falling_screen(void* data);
+
+void update_falling_screen(void* data);
+
+void input_falling_screen(void* data, InputKey key, InputType type);

+ 121 - 0
solitaire/src/scene/intro_animation.c

@@ -0,0 +1,121 @@
+#include "./intro_animation.h"
+
+#include <dolphin/dolphin.h>
+
+#include "../../game_state.h"
+#include "../util/helpers.h"
+#include "play_screen.h"
+
+static bool animation_running = true;
+static int8_t curr_tableau = 0;
+static Vector animation_target = VECTOR_ZERO;
+static Vector animation_from = VECTOR_ZERO;
+static double accumulated_delta = 0;
+
+void start_animation(GameState* state) {
+    accumulated_delta = 0;
+    check_pointer(state->deck->tail);
+    state->animated_card.card = list_peek_back(state->deck);
+    check_pointer(state->animated_card.card);
+    animation_from = (Vector){2, 1};
+    animation_target.x = 2.0f + (float)curr_tableau * 18;
+    check_pointer(state->tableau[curr_tableau]);
+    animation_target.y = MIN(25.0f + (float)state->tableau[curr_tableau]->count * 4, 36);
+}
+
+void start_intro_screen(void* data) {
+    curr_tableau = 0;
+    animation_running = true;
+    dolphin_deed(DolphinDeedPluginGameStart);
+    GameState* state = (GameState*)data;
+    check_pointer(state->deck);
+    list_free(state->deck);
+    check_pointer(state->hand);
+    list_free_data(state->hand);
+    check_pointer(state->waste);
+    list_free_data(state->waste);
+    for(int i = 0; i < 7; i++) {
+        if(i < 4) {
+            check_pointer(state->foundation[i]);
+            list_free_data(state->foundation[i]);
+        }
+        check_pointer(state->tableau[i]);
+        list_free_data(state->tableau[i]);
+    }
+
+    state->deck = deck_generate(1);
+    check_pointer(state->deck->tail);
+    start_animation(state);
+}
+
+void render_intro_screen(void* data) {
+    GameState* state = (GameState*)data;
+    render_play_screen(data);
+
+    if(state->animated_card.card) {
+        card_render_back(
+            state->animated_card.position.x,
+            state->animated_card.position.y,
+            false,
+            state->buffer,
+            22);
+    }
+}
+
+static bool animation_done(GameState* state) {
+    accumulated_delta += state->delta_time * 4;
+    vector_lerp(
+        &(animation_from), &animation_target, accumulated_delta, &(state->animated_card.position));
+    double dist = vector_distance(&(state->animated_card.position), &animation_target);
+    return dist < 1;
+}
+
+void update_intro_screen(void* data) {
+    GameState* state = (GameState*)data;
+    state->isDirty = true;
+    if(curr_tableau < 7 && animation_running) {
+        if(animation_done(state)) {
+            if(curr_tableau < 7) {
+                check_pointer(state->deck);
+                check_pointer(state->deck->tail);
+                check_pointer(state->tableau);
+                check_pointer(state->tableau[curr_tableau]);
+                list_push_back(list_pop_back(state->deck), state->tableau[curr_tableau]);
+                if((curr_tableau + 1) == (uint8_t)state->tableau[curr_tableau]->count) {
+                    Card* c = ((Card*)list_peek_back(state->tableau[curr_tableau]));
+                    check_pointer(c);
+                    c->exposed = true;
+                    curr_tableau++;
+                }
+                start_animation(state);
+            }
+        }
+    } else {
+        state->animated_card.card = NULL;
+        state->scene_switch = 1;
+        return;
+    }
+}
+
+static void quick_finish(GameState* state) {
+    if(state->animated_card.card) {
+        list_push_back(list_pop_back(state->deck), state->tableau[curr_tableau]);
+    }
+
+    while(curr_tableau < 7) {
+        while((uint8_t)(state->tableau[curr_tableau]->count) < (curr_tableau + 1)) {
+            list_push_back(list_pop_back(state->deck), state->tableau[curr_tableau]);
+        }
+        ((Card*)list_peek_back(state->tableau[curr_tableau]))->exposed = true;
+        curr_tableau++;
+    }
+    animation_running = false;
+}
+
+void input_intro_screen(void* data, InputKey key, InputType type) {
+    GameState* state = (GameState*)data;
+    if(key == InputKeyOk && type == InputTypePress) {
+        quick_finish(state);
+        animation_running = false;
+    }
+}

+ 11 - 0
solitaire/src/scene/intro_animation.h

@@ -0,0 +1,11 @@
+#pragma once
+#include <furi.h>
+#include <input/input.h>
+
+void start_intro_screen(void* data);
+
+void render_intro_screen(void* data);
+
+void update_intro_screen(void* data);
+
+void input_intro_screen(void* data, InputKey key, InputType type);

+ 104 - 0
solitaire/src/scene/main_screen.c

@@ -0,0 +1,104 @@
+#include <notification/notification_messages.h>
+#include "main_screen.h"
+
+static bool is_dirty = false;
+static size_t last_start = 0;
+static int8_t note = 0;
+static const float VOLUME = 0.25f;
+
+static const NotificationMessage note_e4 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 329.63f,
+    .data.sound.volume = VOLUME,
+};
+
+static const NotificationMessage note_g4 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 392.0f,
+    .data.sound.volume = VOLUME,
+};
+
+static const NotificationMessage note_c5 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 523.25f,
+    .data.sound.volume = VOLUME,
+};
+
+static const NotificationMessage note_d4 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 293.66f,
+    .data.sound.volume = VOLUME,
+};
+
+static const NotificationMessage note_f4 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 349.23f,
+    .data.sound.volume = VOLUME,
+};
+
+static const NotificationMessage note_b4 = {
+    .type = NotificationMessageTypeSoundOn,
+    .data.sound.frequency = 493.88f,
+    .data.sound.volume = VOLUME,
+};
+
+static const NotificationMessage* music_notes[] = {
+    &note_e4, &note_g4, &note_c5, &message_sound_off, &note_g4, &message_sound_off,
+
+    &note_e4, &note_g4, &note_c5, &message_sound_off, &note_g4, &message_sound_off,
+
+    &note_d4, &note_f4, &note_b4, &message_sound_off, &note_f4, &message_sound_off,
+
+    &note_d4, &note_f4, &note_b4, &message_sound_off, &note_f4, &message_sound_off,
+};
+
+static NotificationSequence music = {NULL, &message_delay_250, &message_sound_off, NULL};
+
+void start_main_screen(void* data) {
+    is_dirty = true;
+    GameState* state = (GameState*)data;
+    last_start = 0;
+    note = -1;
+    state->lateRender = false;
+    state->isDirty = true;
+    state->clearBuffer = true;
+}
+
+void render_main_screen(void* data) {
+    GameState* state = (GameState*)data;
+    Vector logo_pos = (Vector){60, 30};
+    Vector main_img_pos = (Vector){115, 25};
+    Vector start_text_pos = (Vector){64, 55};
+    buffer_draw_all(state->buffer, (Buffer*)&sprite_logo, &logo_pos, 0);
+    buffer_draw_all(state->buffer, (Buffer*)&sprite_main_image, &main_img_pos, 0);
+    buffer_draw_all(state->buffer, (Buffer*)&sprite_start, &start_text_pos, 0);
+}
+
+void update_main_screen(void* data) {
+    GameState* state = (GameState*)data;
+    UNUSED(state);
+    size_t t = curr_time();
+    //Play the menu music one note at a time to not block the app
+    if((last_start - t) > 250) {
+        if(note >= 0) {
+            music[0] = music_notes[note];
+
+            notification_message_block(
+                state->notification_app, (const NotificationSequence*)&music);
+
+            last_start = t;
+        }
+        note = (note + 1) % 24;
+    }
+
+    state->isDirty = is_dirty;
+    is_dirty = false;
+}
+
+void input_main_screen(void* data, InputKey key, InputType type) {
+    GameState* state = (GameState*)data;
+
+    if(key == InputKeyOk && type == InputTypePress) {
+        state->scene_switch = 1;
+    }
+}

+ 16 - 0
solitaire/src/scene/main_screen.h

@@ -0,0 +1,16 @@
+#pragma once
+
+#include <furi.h>
+#include <input/input.h>
+
+#include "../../game_state.h"
+#include "../../assets.h"
+#include "../util/helpers.h"
+
+void start_main_screen(void* data);
+
+void render_main_screen(void* data);
+
+void update_main_screen(void* data);
+
+void input_main_screen(void* data, InputKey key, InputType type);

+ 373 - 0
solitaire/src/scene/play_screen.c

@@ -0,0 +1,373 @@
+#include <notification/notification_messages.h>
+#include "./play_screen.h"
+#include "../../game_state.h"
+#include "../util/helpers.h"
+#include "../../assets.h"
+
+static bool can_quick_solve = false;
+static bool solved = false;
+static bool started = false;
+int8_t picked_from[2] = {-1, -1};
+static const NotificationSequence sequence_fail = {
+    &message_vibro_on,
+    &message_note_c4,
+    &message_delay_10,
+    &message_vibro_off,
+    &message_sound_off,
+    &message_delay_10,
+
+    &message_vibro_on,
+    &message_note_a3,
+    &message_delay_10,
+    &message_vibro_off,
+    &message_sound_off,
+    NULL,
+};
+
+void end_play_screen(GameState* state) {
+    //put back the card from the hand to allow quick solve
+    if(state->hand->count) {
+        if(picked_from[1] == 1) {
+            while(state->hand->tail) {
+                list_push_back(list_pop_front(state->hand), state->tableau[picked_from[1]]);
+            }
+        }
+        if(picked_from[1] == 0) {
+            list_push_back(list_pop_front(state->hand), state->waste);
+        }
+    }
+
+    state->selected[0] = 0;
+    state->selected[1] = 0;
+    started = false;
+    solved = false;
+    state->scene_switch = 1;
+}
+
+void check_quick_solve(GameState* state) {
+    uint8_t c = 0;
+    for(uint8_t i = 0; i < 7; i++) {
+        Card* front = list_peek_front(state->tableau[i]);
+        if(!front || front->exposed) c++;
+    }
+    can_quick_solve = c == 7;
+}
+
+void start_play_screen(void* data) {
+    GameState* state = (GameState*)data;
+    picked_from[0] = -1;
+    picked_from[1] = -1;
+    state->selected[0] = 0;
+    state->selected[1] = 0;
+    state->selected_card = 0;
+    state->isDirty = true;
+    can_quick_solve = false;
+    solved = false;
+    started = true;
+    state->game_start = furi_get_tick();
+}
+
+bool check_finish(void* data) {
+    GameState* state = (GameState*)data;
+    if(state->waste->count > 0 || state->deck->count > 0) return false;
+
+    for(uint8_t i = 0; i < 7; i++) {
+        Card* front = list_peek_front(state->tableau[i]);
+        if(front) return false;
+    }
+
+    return true;
+}
+
+void reset_picked() {
+    picked_from[0] = -1;
+    picked_from[1] = -1;
+}
+
+bool is_picked_from(uint8_t x, uint8_t y) {
+    return picked_from[0] == x && picked_from[1] == y;
+}
+
+void set_picked_from(int8_t x, int8_t y) {
+    picked_from[0] = x;
+    picked_from[1] = y;
+}
+
+void render_play_screen(void* data) {
+    GameState* state = (GameState*)data;
+
+    check_pointer(state->deck);
+    check_pointer(state->waste);
+
+    //Render deck, if there is more than one card left, simulate a bit of depth
+    if(state->deck->count > 1) {
+        card_render_slot(2, 1, false, state->buffer);
+        deck_render(
+            state->deck,
+            Normal,
+            1,
+            0,
+            state->selected[0] == 0 && state->selected[1] == 0,
+            true,
+            state->buffer);
+    } else {
+        deck_render(
+            state->deck,
+            Normal,
+            2,
+            1,
+            state->selected[0] == 0 && state->selected[1] == 0,
+            true,
+            state->buffer);
+    }
+
+    //Render waste pile
+    deck_render(
+        state->waste,
+        Normal,
+        20,
+        1,
+        state->selected[0] == 1 && state->selected[1] == 0,
+        true,
+        state->buffer);
+
+    //Render tableau and foundation
+    for(uint8_t x = 0; x < 7; x++) {
+        if(x < 4) {
+            check_pointer(state->foundation[x]);
+            deck_render(
+                state->foundation[x],
+                Normal,
+                56 + x * 18,
+                1,
+                state->selected[0] == x + 3 && state->selected[1] == 0,
+                true,
+                state->buffer);
+        }
+        check_pointer(state->tableau[x]);
+        deck_render(
+            state->tableau[x],
+            Vertical,
+            2 + x * 18,
+            25,
+            (state->selected[0] == x && state->selected[1] == 1) ? state->selected_card : 0,
+            true,
+            state->buffer);
+    }
+
+    uint8_t h = state->selected[1] == 1 ?
+                    (MIN((uint8_t)state->tableau[state->selected[0]]->count, 4) * 4 + 15) :
+                    0;
+
+    //render cards in hand
+    deck_render(
+        state->hand, Vertical, 10 + state->selected[0] * 18, h + 10, false, false, state->buffer);
+
+    if(started && can_quick_solve) {
+        buffer_draw_rbox(state->buffer, 26, 53, 100, 64, White);
+        buffer_draw_rbox_frame(state->buffer, 25, 52, 101, 65, Black);
+        Vector pos = (Vector){64, 58};
+        buffer_draw_all(state->buffer, (Buffer*)&sprite_solve, &pos, 0);
+    }
+}
+
+void update_play_screen(void* data) {
+    GameState* state = (GameState*)data;
+    if(solved) {
+        end_play_screen(state);
+    }
+}
+
+void input_play_screen(void* data, InputKey key, InputType type) {
+    GameState* state = (GameState*)data;
+    state->isDirty = true;
+
+    if(type == InputTypePress) {
+        switch(key) {
+        case InputKeyLeft:
+            if(state->selected[0] > 0) state->selected[0]--;
+            if(state->selected[0] == 2 && state->selected[1] == 0) state->selected[0]--;
+            state->selected_card = 1;
+            return;
+            break;
+        case InputKeyRight:
+            if(state->selected[0] < 6) state->selected[0]++;
+            if(state->selected[0] == 2 && state->selected[1] == 0) state->selected[0]++;
+            state->selected_card = 1;
+            return;
+            break;
+        case InputKeyUp:
+            //try to move selection inside the tableau
+            if(state->selected[1] == 1) {
+                //check if highlight can move up in the tableau
+                int8_t id, id_flipped;
+                deck_first_non_flipped(state->tableau[state->selected[0]], &id);
+                id_flipped = state->tableau[state->selected[0]]->count - id;
+                //move up until it reaches the last exposed card, disable when there is something in hand or no card is exposed
+                if(state->selected_card < id_flipped && id >= 0 && state->hand->count == 0) {
+                    state->selected_card++;
+                }
+                //move to the top row
+                else {
+                    state->selected[1] = 0;
+                    state->selected_card = 1;
+                    if(state->selected[0] == 2) state->selected[0]--;
+                }
+            }
+            return;
+            break;
+        case InputKeyDown:
+            if(state->selected[1] == 0) {
+                state->selected_card = 1;
+                state->selected[1] = 1;
+            } else if(state->selected_card > 1) {
+                state->selected_card--;
+            }
+            return;
+            break;
+        case InputKeyOk:
+
+            //cycle deck
+            if(state->selected[0] == 0 && state->selected[1] == 0) {
+                if(state->deck->count > 0 || state->waste->count > 0) {
+                    if(state->deck->count > 0) {
+                        Card* c = list_pop_back(state->deck);
+                        c->exposed = true;
+                        list_push_back(c, state->waste);
+                        return;
+                    } else {
+                        while(state->waste->count) {
+                            Card* c = list_pop_back(state->waste);
+                            c->exposed = false;
+                            list_push_back(c, state->deck);
+                        }
+                        return;
+                    }
+                }
+                if(can_quick_solve) return;
+            } else if(state->selected[0] == 1 && state->selected[1] == 0) {
+                //pick from waste
+                if(state->hand->count == 0 && state->waste->count > 0) {
+                    list_push_back(list_pop_back(state->waste), state->hand);
+                    set_picked_from(0, 1);
+                    return;
+                } else if(is_picked_from(0, 1)) { //put back to waste
+                    list_push_back(list_pop_back(state->hand), state->waste);
+                    reset_picked();
+                    return;
+                }
+
+            }
+            //test if it can be put to the foundation (only if 1 card is in hand)
+            else if(state->hand->count == 1 && state->selected[1] == 0 && state->selected[0] > 2) {
+                List* foundation = state->foundation[state->selected[0] - 3];
+                check_pointer(foundation);
+                if(card_test_foundation(list_peek_front(state->hand), list_peek_back(foundation))) {
+                    list_push_back(list_pop_front(state->hand), foundation);
+                    reset_picked();
+                    solved = check_finish(state);
+                    return;
+                }
+            } else if(state->selected[1] == 1) { //Pick from tableau or flip card
+                //store a reference to the tableau, doesn't matter if we are over them or not, it can be indexed
+                List* tbl = state->tableau[state->selected[0]];
+                //pick cards
+                if(state->hand->count == 0) {
+                    Card* last = list_peek_back(tbl);
+                    if(last) {
+                        //Flip card if not exposed
+                        if(!last->exposed) {
+                            last->exposed = true;
+                            check_quick_solve(state);
+                            return;
+                        }
+                        //Pick cards
+                        else {
+                            for(uint8_t i = 0; i < state->selected_card && tbl->count > 0; i++) {
+                                list_push_front(list_pop_back(tbl), state->hand);
+                            }
+                            set_picked_from(state->selected[0], 1);
+                            state->selected_card = 1;
+                            return;
+                        }
+                    }
+                    if(can_quick_solve) return;
+                }
+                //try to place hand
+                else {
+                    Card* last = list_peek_back(tbl);
+                    //place back from where you picked up
+                    if(is_picked_from(state->selected[0], 1)) {
+                        while(state->hand->count) {
+                            list_push_back(list_pop_front(state->hand), tbl);
+                        }
+                        reset_picked();
+                        return;
+                    }
+                    //test if the hand can be placed at one of the tableau columns
+                    else if(
+                        (!last || last->exposed) &&
+                        card_test_column(list_peek_front(state->hand), last)) {
+                        while(state->hand->count) {
+                            list_push_back(list_pop_front(state->hand), tbl);
+                        }
+                        reset_picked();
+                        return;
+                    }
+                }
+            }
+            break;
+        default:
+            return;
+        }
+        notification_message(state->notification_app, &sequence_fail);
+    } else if(type == InputTypeLong) {
+        switch(key) {
+        case InputKeyLeft:
+            state->selected_card = 1;
+            state->selected[0] = 0;
+            return;
+            break;
+        case InputKeyRight:
+            state->selected_card = 1;
+            state->selected[0] = 6;
+            return;
+            break;
+        case InputKeyUp:
+            state->selected[1] = 0;
+            state->selected_card = 1;
+            if(state->selected[0] == 2) state->selected[0]--;
+            return;
+            break;
+        case InputKeyDown:
+            state->selected_card = 1;
+            state->selected[1] = 1;
+            return;
+            break;
+        case InputKeyOk:
+            if(can_quick_solve) {
+                end_play_screen(state);
+                return;
+            }
+
+            //try to quick place to the foundation
+            if(state->hand->count == 1) {
+                Card* c = list_peek_back(state->hand);
+                for(int8_t i = 0; i < 4; i++) {
+                    if(card_test_foundation(c, list_peek_back(state->foundation[i]))) {
+                        list_push_back(list_pop_back(state->hand), state->foundation[i]);
+                        state->selected_card = 1;
+                        solved = check_finish(state);
+                        return;
+                    }
+                }
+            }
+            break;
+        case InputKeyBack:
+            return;
+        default:
+            break;
+        }
+        notification_message(state->notification_app, &sequence_fail);
+    }
+}

+ 13 - 0
solitaire/src/scene/play_screen.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <input/input.h>
+#include <furi.h>
+
+void start_play_screen(void* data);
+
+void render_play_screen(void* data);
+
+void update_play_screen(void* data);
+
+void input_play_screen(void* data, InputKey key, InputType type);
+bool check_finish(void* data);

+ 73 - 0
solitaire/src/scene/result_screen.c

@@ -0,0 +1,73 @@
+#include "result_screen.h"
+#include "../../game_state.h"
+#include "../util/helpers.h"
+#include <dolphin/dolphin.h>
+#include <notification/notification_messages.h>
+
+static int hours, minutes, seconds;
+static bool isStarted = false;
+static char timeString[24];
+static const NotificationSequence sequence_cheer = {
+    &message_note_c4,
+    &message_delay_100,
+    &message_note_e4,
+    &message_delay_100,
+    &message_note_g4,
+    &message_delay_100,
+    &message_note_a4,
+    &message_delay_100,
+    &message_sound_off,
+    NULL,
+};
+
+void start_result_screen(void* data) {
+    GameState* state = (GameState*)data;
+    dolphin_deed(DolphinDeedPluginGameWin);
+    size_t diff = (state->game_end - state->game_start) / furi_kernel_get_tick_frequency();
+    hours = (int)(diff / 3600);
+    minutes = (int)(diff % 3600) / 60;
+    seconds = (int)(diff % 60);
+    state->lateRender = true;
+    state->isDirty = true;
+    state->clearBuffer = false;
+    isStarted = false;
+    notification_message(state->notification_app, (const NotificationSequence*)&sequence_cheer);
+}
+
+void render_result_screen(void* data) {
+    GameState* state = (GameState*)data;
+
+    canvas_set_color(state->canvas, ColorWhite);
+    canvas_draw_box(state->canvas, 22, 14, 85, 30);
+    canvas_set_color(state->canvas, ColorBlack);
+    canvas_draw_frame(state->canvas, 21, 13, 87, 32);
+
+    canvas_set_font(state->canvas, FontPrimary);
+    canvas_draw_str_aligned(state->canvas, 64, 15, AlignCenter, AlignTop, "Congratulations!");
+    canvas_set_font(state->canvas, FontSecondary);
+    canvas_draw_str_aligned(state->canvas, 64, 26, AlignCenter, AlignTop, "Solve time:");
+
+    if(hours > 0)
+        snprintf(timeString, sizeof(timeString), "%02d:%02d:%02d", hours, minutes, seconds);
+    else
+        snprintf(timeString, sizeof(timeString), "%02d:%02d", minutes, seconds);
+    canvas_set_font(state->canvas, FontSecondary);
+    canvas_draw_str_aligned(state->canvas, 64, 35, AlignCenter, AlignTop, timeString);
+}
+
+void update_result_screen(void* data) {
+    GameState* state = (GameState*)data;
+    state->clearBuffer = false;
+    state->lateRender = true;
+    if(!isStarted) {
+        state->isDirty = true;
+        isStarted = true;
+    }
+}
+
+void input_result_screen(void* data, InputKey key, InputType type) {
+    GameState* state = (GameState*)data;
+    if(key == InputKeyOk && type == InputTypePress) {
+        state->scene_switch = 1;
+    }
+}

+ 11 - 0
solitaire/src/scene/result_screen.h

@@ -0,0 +1,11 @@
+#pragma once
+#include <furi.h>
+#include <input/input.h>
+
+void start_result_screen(void* data);
+
+void render_result_screen(void* data);
+
+void update_result_screen(void* data);
+
+void input_result_screen(void* data, InputKey key, InputType type);

+ 55 - 0
solitaire/src/scene/scene_setup.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <furi.h>
+#include "../util/list.h"
+#include "main_screen.h"
+#include "intro_animation.h"
+#include "play_screen.h"
+#include "solve_screen.h"
+#include "result_screen.h"
+#include "falling_card.h"
+
+typedef struct {
+    void (*start)(void* data);
+    void (*render)(void* data);
+
+    void (*update)(void* data);
+
+    void (*input)(void* data, InputKey key, InputType type);
+} GameLogic;
+
+GameLogic main_screen = (GameLogic){
+    .start = start_main_screen,
+    .render = render_main_screen,
+    .update = update_main_screen,
+    .input = input_main_screen};
+
+GameLogic intro_screen = (GameLogic){
+    .start = start_intro_screen,
+    .render = render_intro_screen,
+    .update = update_intro_screen,
+    .input = input_intro_screen};
+
+GameLogic play_screen = (GameLogic){
+    .start = start_play_screen,
+    .render = render_play_screen,
+    .update = update_play_screen,
+    .input = input_play_screen};
+
+GameLogic solve_screen = (GameLogic){
+    .start = start_solve_screen,
+    .render = render_solve_screen,
+    .update = update_solve_screen,
+    .input = input_solve_screen};
+
+GameLogic falling_screen = (GameLogic){
+    .start = start_falling_screen,
+    .render = render_falling_screen,
+    .update = update_falling_screen,
+    .input = input_falling_screen};
+
+GameLogic result_screen = (GameLogic){
+    .start = start_result_screen,
+    .render = render_result_screen,
+    .update = update_result_screen,
+    .input = input_result_screen};

+ 213 - 0
solitaire/src/scene/solve_screen.c

@@ -0,0 +1,213 @@
+#include "solve_screen.h"
+#include "../../game_state.h"
+#include "play_screen.h"
+
+uint8_t target_foundation;
+static double accumulated_delta = 0;
+static Vector animation_target = VECTOR_ZERO;
+static Vector animation_from = VECTOR_ZERO;
+
+void start_solve_screen(void* data) {
+    UNUSED(data);
+    accumulated_delta = 0;
+    target_foundation = 0;
+}
+
+void render_solve_screen(void* data) {
+    GameState* state = (GameState*)data;
+
+    render_play_screen(state);
+
+    if(state->animated_card.card) {
+        card_render_front(
+            state->animated_card.card,
+            (uint8_t)state->animated_card.position.x,
+            (uint8_t)state->animated_card.position.y,
+            false,
+            state->buffer,
+            22);
+    }
+}
+
+bool end_solve_screen(GameState* state) {
+    for(uint8_t i = 0; i < 4; i++) {
+        Card* c = list_peek_back(state->foundation[i]);
+        if(!c || c->value != KING) return false;
+    }
+
+    return true;
+}
+
+static int8_t missing_suit(GameState* state) {
+    bool found[4] = {false, false, false, false};
+    for(uint8_t i = 0; i < 4; i++) {
+        if(state->foundation[i]->count) {
+            Card* c = list_peek_back(state->foundation[i]);
+            found[c->suit] = true;
+        }
+    }
+    for(int8_t i = 0; i < 4; i++) {
+        if(!found[i]) return i;
+    }
+
+    return -1;
+}
+
+static void quick_solve(GameState* state) {
+    //remove all cards that are not placed to the foundation yet
+    list_free_data(state->deck);
+    list_free_data(state->waste);
+    for(uint8_t i = 0; i < 7; i++) {
+        list_free_data(state->tableau[i]);
+    }
+    list_free_data(state->hand);
+
+    state->animated_card.card = NULL;
+
+    //fill up the foundations with cards
+    for(uint8_t i = 0; i < 4; i++) {
+        //add ace to the start
+        if(state->foundation[i]->count == 0) {
+            Card* c = malloc(sizeof(Card));
+            c->value = ACE;
+            c->suit = missing_suit(state);
+            c->exposed = true;
+            list_push_back(c, state->foundation[i]);
+        }
+
+        //fill up the rest
+        for(uint8_t v = state->foundation[i]->count; v < 13; v++) {
+            Card* c = malloc(sizeof(Card));
+            c->value = v - 1;
+            c->suit = ((Card*)list_peek_back(state->foundation[i]))->suit;
+            c->exposed = true;
+            list_push_back(c, state->foundation[i]);
+        }
+    }
+
+    end_solve_screen(state);
+}
+
+static bool animation_done(GameState* state) {
+    if(!state->animated_card.card) return true;
+
+    accumulated_delta += state->delta_time * 4;
+    vector_lerp(
+        &(animation_from), &animation_target, accumulated_delta, &(state->animated_card.position));
+
+    double dist = vector_distance(&(state->animated_card.position), &animation_target);
+    return dist < 1;
+}
+
+static Card* find_and_remove(List* list, uint8_t suit, uint8_t value) {
+    //go reversed order because tableau will always have at the end
+    ListItem* current = list->tail;
+    while(current) {
+        if(((Card*)current->data)->value == value && ((Card*)current->data)->suit == suit) {
+            Card* c = current->data;
+            list_remove_item(c, list);
+            return c;
+        }
+        current = current->prev;
+    }
+
+    return NULL;
+}
+
+static uint8_t positions[9] = {2, 20, 2, 20, 38, 56, 74, 92, 110};
+
+static void find_next_card(GameState* state) {
+    List* order[9] = {
+        state->deck,
+        state->waste,
+        state->tableau[0],
+        state->tableau[1],
+        state->tableau[2],
+        state->tableau[3],
+        state->tableau[4],
+        state->tableau[5],
+        state->tableau[6]};
+
+    int8_t missing = missing_suit(state);
+    if(missing >= 0) {
+        //find the missing ACE
+        for(uint8_t id = 0; id < 9; id++) {
+            Card* ace = find_and_remove(order[id], missing, ACE);
+            if(ace) {
+                ace->exposed = true;
+                for(uint8_t i = 0; i < 4; i++) {
+                    if(state->foundation[i]->count == 0) {
+                        state->animated_card.card = ace;
+                        target_foundation = i;
+                        animation_from.x = positions[id];
+                        animation_from.y =
+                            id < 2 ? 1 :
+                                     ((float)MIN(
+                                          (uint8_t)state->tableau[state->selected[0]]->count, 4) *
+                                          4 +
+                                      15);
+                        state->animated_card.position = animation_from;
+
+                        animation_target.x = (float)(56 + target_foundation * 18);
+                        animation_target.y = 1;
+                    }
+                }
+            }
+        }
+    } else {
+        uint8_t lowestValue = 14, lowestSuit = 0;
+        //get the lowest value
+        for(uint8_t i = 0; i < 4; i++) {
+            Card* c = list_peek_back(state->foundation[i]);
+            if(lowestValue > ((c->value + 1) % 13)) {
+                lowestValue = ((c->value + 1) % 13);
+                target_foundation = i;
+                lowestSuit = c->suit;
+            }
+        }
+        //store that card to animate
+        for(uint8_t id = 0; id < 9; id++) {
+            Card* c = find_and_remove(order[id], lowestSuit, lowestValue);
+            if(c) {
+                state->animated_card.card = c;
+
+                animation_from.x = positions[id];
+                animation_from.y =
+                    id < 2 ?
+                        1 :
+                        ((float)MIN((uint8_t)state->tableau[state->selected[0]]->count, 4) * 4 +
+                         15);
+                state->animated_card.position = animation_from;
+
+                animation_target.x = (float)(56 + target_foundation * 18);
+                animation_target.y = 1;
+                break;
+            }
+        }
+    }
+}
+
+void update_solve_screen(void* data) {
+    GameState* state = (GameState*)data;
+    state->isDirty = true;
+
+    if(!end_solve_screen(state)) {
+        if(animation_done(state)) {
+            if(state->animated_card.card) {
+                state->animated_card.card->exposed = true;
+                list_push_back(state->animated_card.card, state->foundation[target_foundation]);
+                state->animated_card.card = NULL;
+                accumulated_delta = 0;
+            }
+            find_next_card(state);
+        }
+    } else {
+        state->scene_switch = 1;
+    }
+}
+
+void input_solve_screen(void* data, InputKey key, InputType type) {
+    if(key == InputKeyOk && type == InputTypePress) {
+        quick_solve(data);
+    }
+}

+ 8 - 0
solitaire/src/scene/solve_screen.h

@@ -0,0 +1,8 @@
+#pragma once
+#include <furi.h>
+#include <input/input.h>
+
+void start_solve_screen(void* data);
+void render_solve_screen(void* data);
+void update_solve_screen(void* data);
+void input_solve_screen(void* data, InputKey key, InputType type);

+ 293 - 0
solitaire/src/util/buffer.c

@@ -0,0 +1,293 @@
+#include "buffer.h"
+#include "helpers.h"
+#include <memory.h>
+#include <furi.h>
+
+static RenderSettings default_render = DEFAULT_RENDER;
+
+uint16_t pixel(uint8_t x, uint8_t y, uint8_t w) {
+    return (y * w + x) / 8;
+}
+
+unsigned long buffer_size(uint8_t width, uint8_t height) {
+    return sizeof(uint8_t) * (int)ceil(width / 8.0) * ceil(height);
+}
+
+uint8_t* malloc_buffer(uint8_t width, uint8_t height) {
+    return (uint8_t*)malloc(buffer_size(width, height));
+}
+
+Buffer* buffer_create(uint8_t width, uint8_t height, bool double_buffered) {
+    Buffer* b = (Buffer*)malloc(sizeof(Buffer));
+    b->double_buffered = double_buffered;
+    b->width = width;
+    b->height = height;
+    b->data = malloc_buffer(width, height);
+    if(double_buffered)
+        b->back_buffer = malloc_buffer(width, height);
+    else
+        b->back_buffer = NULL;
+    return b;
+}
+
+void buffer_release(Buffer* buffer) {
+    free(buffer->data);
+    if(buffer->double_buffered) free(buffer->back_buffer);
+    free(buffer);
+}
+
+bool buffer_test_coordinate(Buffer* const buffer, int x, int y) {
+    return (x >= 0 && x < buffer->width && y >= 0 && y < buffer->height);
+}
+
+bool buffer_get_pixel(Buffer* const buffer, int x, int y) {
+    return buffer->data[pixel(x, y, buffer->width)] & (1 << (x & 7));
+}
+
+void buffer_set_pixel(Buffer* buffer, int16_t x, int16_t y, enum PixelColor draw_mode) {
+    uint8_t bit = 1 << (x & 7);
+    uint8_t* p = &(buffer->data[pixel(x, y, buffer->width)]);
+
+    switch(draw_mode) {
+    case Black:
+        *p |= bit;
+        break;
+    case White:
+        *p &= ~bit;
+        break;
+    case Flip:
+        *p ^= bit;
+        break;
+    }
+}
+
+Buffer* buffer_copy(Buffer* buffer) {
+    Buffer* new_buffer = (Buffer*)malloc(sizeof(Buffer));
+    new_buffer->double_buffered = buffer->double_buffered;
+    new_buffer->width = buffer->width;
+    new_buffer->height = buffer->height;
+    new_buffer->data = malloc_buffer(buffer->width, buffer->height);
+    memcpy(new_buffer->data, buffer->data, buffer_size(buffer->width, buffer->height));
+    if(buffer->double_buffered) {
+        new_buffer->back_buffer = malloc_buffer(buffer->width, buffer->height);
+        memcpy(
+            new_buffer->back_buffer,
+            buffer->back_buffer,
+            buffer_size(buffer->width, buffer->height));
+    } else {
+        new_buffer->back_buffer = NULL;
+    }
+    return new_buffer;
+}
+
+void buffer_swap_back(Buffer* buffer) {
+    check_pointer(buffer);
+    check_pointer(buffer->data);
+    if(buffer->double_buffered) {
+        check_pointer(buffer->back_buffer);
+        uint8_t* temp = buffer->data;
+        buffer->data = buffer->back_buffer;
+        buffer->back_buffer = temp;
+    }
+}
+
+void buffer_clear(Buffer* buffer) {
+    check_pointer(buffer);
+    check_pointer(buffer->data);
+    buffer->data = (uint8_t*)memset(buffer->data, 0, buffer_size(buffer->width, buffer->height));
+}
+
+void buffer_swap_with(Buffer* buffer_a, Buffer* buffer_b) {
+    check_pointer(buffer_a);
+    check_pointer(buffer_b);
+    uint8_t* temp = buffer_a->data;
+    buffer_a->data = buffer_b->data;
+    buffer_b->data = temp;
+}
+
+void buffer_draw_internal(
+    Buffer* target,
+    Buffer* const sprite,
+    bool is_black,
+    enum PixelColor color,
+    Vector* const position,
+    uint8_t x_cap,
+    uint8_t y_cap,
+    float rotation,
+    Vector anchor) {
+    Vector center = {
+        .x = anchor.x * sprite->width,
+        .y = anchor.y * sprite->height,
+    };
+    Vector transform;
+    int max_w = fmin(sprite->width, x_cap);
+    int max_h = fmin(sprite->height, y_cap);
+    bool isOn;
+    int16_t finalX, finalY;
+    for(int y = 0; y < max_h; y++) {
+        for(int x = 0; x < max_w; x++) {
+            Vector curr = {x, y};
+            vector_sub(&curr, &center, &transform);
+            vector_rotate(&transform, rotation, &transform);
+            vector_add(&transform, position, &transform);
+
+            finalX = (int16_t)roundf(transform.x);
+            finalY = (int16_t)roundf(transform.y);
+            if(buffer_test_coordinate(target, finalX, finalY)) {
+                isOn = buffer_get_pixel(sprite, x, y) == is_black;
+                if(isOn) buffer_set_pixel(target, finalX, finalY, color);
+            }
+        }
+    }
+}
+
+void buffer_draw_all(Buffer* target, Buffer* const sprite, Vector* position, float rotation) {
+    check_pointer(target);
+    check_pointer(sprite);
+    check_pointer(position);
+    buffer_draw(
+        target, sprite, position, sprite->width, sprite->height, rotation, &default_render);
+}
+
+void buffer_draw(
+    Buffer* target,
+    Buffer* const sprite,
+    Vector* position,
+    uint8_t x_cap,
+    uint8_t y_cap,
+    float rotation,
+    RenderSettings* settings) {
+    check_pointer(target);
+    check_pointer(sprite);
+    check_pointer(position);
+    switch(settings->drawMode) {
+    default:
+    case BlackOnly:
+        buffer_draw_internal(
+            target, sprite, true, Black, position, x_cap, y_cap, rotation, settings->anchor);
+        break;
+    case WhiteOnly:
+        buffer_draw_internal(
+            target, sprite, false, White, position, x_cap, y_cap, rotation, settings->anchor);
+        break;
+    case WhiteAsBlack:
+        buffer_draw_internal(
+            target, sprite, false, Black, position, x_cap, y_cap, rotation, settings->anchor);
+        break;
+    case BlackAsWhite:
+        buffer_draw_internal(
+            target, sprite, true, White, position, x_cap, y_cap, rotation, settings->anchor);
+        break;
+    case WhiteAsInverted:
+        buffer_draw_internal(
+            target, sprite, false, Flip, position, x_cap, y_cap, rotation, settings->anchor);
+        break;
+    case BlackAsInverted:
+        buffer_draw_internal(
+            target, sprite, true, Flip, position, x_cap, y_cap, rotation, settings->anchor);
+        break;
+    }
+}
+
+void buffer_render(Buffer* buffer, Canvas* const canvas) {
+    check_pointer(buffer);
+    canvas_draw_xbm(canvas, 0, 0, buffer->width, buffer->height, buffer->data);
+}
+
+void buffer_draw_line(Buffer* buffer, int x0, int y0, int x1, int y1, enum PixelColor draw_mode) {
+    check_pointer(buffer);
+    int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
+    int dy = abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
+    int err = (dx > dy ? dx : -dy) / 2;
+
+    while(true) {
+        if(buffer_test_coordinate(buffer, x0, y0)) {
+            buffer_set_pixel(buffer, x0, y0, draw_mode);
+        }
+        if(x0 == x1 && y0 == y1) break;
+        int e2 = err;
+        if(e2 > -dx) {
+            err -= dy;
+            x0 += sx;
+        }
+        if(e2 < dy) {
+            err += dx;
+            y0 += sy;
+        }
+    }
+}
+
+void buffer_draw_rbox(
+    Buffer* buffer,
+    int16_t x0,
+    int16_t y0,
+    int16_t x1,
+    int16_t y1,
+    enum PixelColor draw_mode) {
+    for(int16_t x = x0; x < x1; x++) {
+        for(int16_t y = y0; y < y1; y++) {
+            if(((x == x0 || x == x1 - 1) && (y == y0 || y == y1 - 1)) ||
+               !buffer_test_coordinate(buffer, x, y))
+                continue;
+            buffer_set_pixel(buffer, x, y, draw_mode);
+        }
+    }
+}
+
+void buffer_draw_rbox_frame(
+    Buffer* buffer,
+    int16_t x0,
+    int16_t y0,
+    int16_t x1,
+    int16_t y1,
+    enum PixelColor draw_mode) {
+    buffer_draw_line(buffer, x0 + 1, y0, x1 - 1, y0, draw_mode);
+    buffer_draw_line(buffer, x0 + 1, y1, x1 - 1, y1, draw_mode);
+
+    buffer_draw_line(buffer, x0, y0 + 1, x0, y1 - 1, draw_mode);
+    buffer_draw_line(buffer, x1, y0 + 1, x1, y1 - 1, draw_mode);
+}
+
+void buffer_draw_box(
+    Buffer* buffer,
+    int16_t x0,
+    int16_t y0,
+    int16_t x1,
+    int16_t y1,
+    enum PixelColor draw_mode) {
+    for(int16_t x = x0 + 1; x < x1 - 1; x++) {
+        for(int16_t y = y0 + 1; y < y1 - 1; y++) {
+            if(!buffer_test_coordinate(buffer, x, y)) continue;
+            buffer_set_pixel(buffer, x, y, draw_mode);
+        }
+    }
+}
+
+void buffer_set_pixel_with_check(Buffer* buffer, int16_t x, int16_t y, enum PixelColor draw_mode) {
+    if(buffer_test_coordinate(buffer, x, y)) buffer_set_pixel(buffer, x, y, draw_mode);
+}
+
+void buffer_draw_circle(Buffer* buffer, int x, int y, int r, enum PixelColor color) {
+    int16_t a = r;
+    int16_t b = 0;
+    int16_t decision = 1 - a;
+
+    while(b <= a) {
+        buffer_set_pixel_with_check(buffer, a + x, b + y, color);
+        buffer_set_pixel_with_check(buffer, b + x, a + y, color);
+        buffer_set_pixel_with_check(buffer, -a + x, b + y, color);
+        buffer_set_pixel_with_check(buffer, -b + x, a + y, color);
+        buffer_set_pixel_with_check(buffer, -a + x, -b + y, color);
+        buffer_set_pixel_with_check(buffer, -b + x, -a + y, color);
+        buffer_set_pixel_with_check(buffer, a + x, -b + y, color);
+        buffer_set_pixel_with_check(buffer, b + x, -a + y, color);
+
+        b++;
+        if(decision <= 0) {
+            decision += 2 * b + 1;
+        } else {
+            a--;
+            decision += 2 * (b - a) + 1;
+        }
+    }
+}

+ 100 - 0
solitaire/src/util/buffer.h

@@ -0,0 +1,100 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/canvas.h>
+#include "vector.h"
+
+enum DrawMode {
+    WhiteOnly,
+    BlackOnly,
+    WhiteAsBlack,
+    BlackAsWhite,
+    WhiteAsInverted,
+    BlackAsInverted,
+};
+
+typedef struct {
+    uint8_t* data;
+    uint8_t* back_buffer;
+    uint8_t width;
+    uint8_t height;
+    bool double_buffered;
+} Buffer;
+
+enum PixelColor {
+    Black, //or
+    White, //
+    Flip //not
+};
+
+typedef struct {
+    const Vector anchor;
+    enum DrawMode drawMode;
+} RenderSettings;
+
+#define SCREEN_WIDTH  128
+#define SCREEN_HEIGHT 64
+
+#define DEFAULT_RENDER                                          \
+    (RenderSettings) {                                          \
+        .anchor = {.x = 0.5f, .y = 0.5f}, .drawMode = BlackOnly \
+    }
+
+Buffer* buffer_create(uint8_t width, uint8_t height, bool double_buffered);
+
+void buffer_release(Buffer* buffer);
+
+bool buffer_test_coordinate(Buffer* const buffer, int x, int y);
+
+bool buffer_get_pixel(Buffer* const buffer, int x, int y);
+
+void buffer_set_pixel(Buffer* buffer, int16_t x, int16_t y, enum PixelColor draw_mode);
+
+Buffer* buffer_copy(Buffer* buffer);
+
+void buffer_swap_back(Buffer* buffer);
+
+void buffer_swap_with(Buffer* buffer_a, Buffer* buffer_b);
+
+void buffer_draw_all(Buffer* target, Buffer* const sprite, Vector* position, float rotation);
+
+void buffer_draw(
+    Buffer* target,
+    Buffer* const sprite,
+    Vector* position,
+    uint8_t x_cap,
+    uint8_t y_cap,
+    float rotation,
+    RenderSettings* settings);
+
+//void buffer_draw_internal(Buffer *target, Buffer* sprite, bool is_black, enum PixelColor color, Vector *const position, uint8_t x_cap, uint8_t y_cap, float rotation);
+void buffer_render(Buffer* buffer, Canvas* const canvas);
+void buffer_clear(Buffer* buffer);
+
+void buffer_draw_line(Buffer* buffer, int x0, int y0, int x1, int y1, enum PixelColor draw_mode);
+
+void buffer_draw_rbox(
+    Buffer* buffer,
+    int16_t x0,
+    int16_t y0,
+    int16_t x1,
+    int16_t y1,
+    enum PixelColor draw_mode);
+
+void buffer_draw_rbox_frame(
+    Buffer* buffer,
+    int16_t x0,
+    int16_t y0,
+    int16_t x1,
+    int16_t y1,
+    enum PixelColor draw_mode);
+
+void buffer_draw_box(
+    Buffer* buffer,
+    int16_t x0,
+    int16_t y0,
+    int16_t x1,
+    int16_t y1,
+    enum PixelColor draw_mode);
+
+void buffer_draw_circle(Buffer* buffer, int x, int y, int r, enum PixelColor draw_mode);

+ 249 - 0
solitaire/src/util/card.c

@@ -0,0 +1,249 @@
+#include "card.h"
+#include "../../assets.h"
+#include "helpers.h"
+
+static RenderSettings default_render = DEFAULT_RENDER;
+
+static Buffer* letters[] = {
+    (Buffer*)&sprite_2,
+    (Buffer*)&sprite_3,
+    (Buffer*)&sprite_4,
+    (Buffer*)&sprite_5,
+    (Buffer*)&sprite_6,
+    (Buffer*)&sprite_7,
+    (Buffer*)&sprite_8,
+    (Buffer*)&sprite_9,
+    (Buffer*)&sprite_10,
+    (Buffer*)&sprite_J,
+    (Buffer*)&sprite_Q,
+    (Buffer*)&sprite_K,
+    (Buffer*)&sprite_A,
+};
+
+static Buffer* suits[] = {
+    (Buffer*)&sprite_hearths,
+    (Buffer*)&sprite_spades,
+    (Buffer*)&sprite_diamonds,
+    (Buffer*)&sprite_clubs};
+
+static Buffer* backSide = (Buffer*)&sprite_pattern_big;
+
+void card_render_front(
+    Card* c,
+    int16_t x,
+    int16_t y,
+    bool selected,
+    Buffer* buffer,
+    uint8_t size_limit) {
+    uint8_t height = y + fmin(size_limit, 22);
+
+    buffer_draw_rbox(buffer, x, y, x + 16, height, White);
+    buffer_draw_rbox_frame(buffer, x, y, x + 16, height, Black);
+
+    Vector p = (Vector){(float)x + 6, (float)y + 5};
+    buffer_draw_all(buffer, letters[c->value], &p, 0);
+
+    p = (Vector){(float)x + 12, (float)y + 5};
+    buffer_draw_all(buffer, suits[c->suit], &p, 0);
+
+    if(size_limit > 8) {
+        p = (Vector){(float)x + 10, (float)y + 16};
+        buffer_draw_all(buffer, letters[c->value], &p, M_PI);
+        p = (Vector){(float)x + 4, (float)y + 16};
+        buffer_draw_all(buffer, suits[c->suit], &p, M_PI);
+    }
+    if(selected) {
+        buffer_draw_box(buffer, x, y, x + 17, height + 1, Flip);
+    }
+}
+
+void card_render_slot(int16_t x, int16_t y, bool selected, Buffer* buffer) {
+    buffer_draw_rbox(buffer, x, y, x + 17, y + 23, Black);
+    buffer_draw_rbox_frame(buffer, x + 2, y + 2, x + 14, y + 20, White);
+    if(selected) buffer_draw_rbox(buffer, x + 1, y + 1, x + 16, y + 22, Flip);
+}
+
+void card_render_back(int16_t x, int16_t y, bool selected, Buffer* buffer, uint8_t size_limit) {
+    uint8_t height = y + fmin(size_limit, 22);
+
+    buffer_draw_rbox(buffer, x + 1, y + 1, x + 16, height, White);
+    buffer_draw_rbox_frame(buffer, x, y, x + 16, height, Black);
+    Vector pos = (Vector){(float)x + 9, (float)y + 11};
+    check_pointer(buffer);
+    check_pointer(backSide);
+    check_pointer(&pos);
+    check_pointer(&default_render);
+    buffer_draw(buffer, backSide, &pos, 15, (int)fmin(size_limit, 22), 0, &default_render);
+    if(selected) {
+        buffer_draw_box(buffer, x, y, x + 17, height + 1, Flip);
+    }
+}
+
+void card_try_render(
+    Card* c,
+    int16_t x,
+    int16_t y,
+    bool selected,
+    Buffer* buffer,
+    uint8_t size_limit) {
+    if(c) {
+        if(c->exposed)
+            card_render_front(c, x, y, selected, buffer, size_limit);
+        else
+            card_render_back(x, y, selected, buffer, size_limit);
+    } else {
+        card_render_slot(x, y, selected, buffer);
+    }
+}
+
+bool card_test_foundation(Card* data, Card* target) {
+    if(!target || (target->value == -1 && target->suit == data->suit)) {
+        return data->value == ACE;
+    }
+    return target->suit == data->suit && ((target->value + 1) % 13 == data->value % 13);
+}
+
+bool card_test_column(Card* data, Card* target) {
+    if(!target) return data->value == KING;
+    return target->suit % 2 == (data->suit + 1) % 2 && (data->value + 1) == target->value;
+}
+
+List* deck_generate(uint8_t deck_count) {
+    List* deck = list_make();
+    int cards_count = 52 * deck_count;
+    uint8_t cards[cards_count];
+    for(int i = 0; i < cards_count; i++)
+        cards[i] = i % 52;
+    srand(curr_time());
+
+    //reorder
+    for(int i = 0; i < cards_count; i++) {
+        int r = i + (rand() % (cards_count - i));
+        uint8_t card = cards[i];
+        cards[i] = cards[r];
+        cards[r] = card;
+    }
+
+    //Init deck list
+    for(int i = 0; i < cards_count; i++) {
+        Card* c = malloc(sizeof(Card));
+        c->value = cards[i] % 13;
+        c->suit = cards[i] / 13;
+        c->exposed = false;
+        list_push_back(c, deck);
+    }
+
+    return deck;
+}
+
+void deck_free(List* deck) {
+    list_free(deck);
+}
+
+void deck_render_vertical(List* deck, uint8_t x, uint8_t y, int8_t selected, Buffer* buffer) {
+    check_pointer(deck);
+    check_pointer(buffer);
+    uint8_t loop_end = deck->count;
+    int8_t selection = loop_end - selected;
+    uint8_t loop_start = MAX(loop_end - 4, 0);
+    uint8_t position = 0;
+    int8_t first_non_flipped;
+    Card* first_non_flipped_card = deck_first_non_flipped(deck, &first_non_flipped);
+
+    bool had_top = false;
+    bool showDark = selection >= 0;
+
+    if(first_non_flipped <= loop_start && selection != first_non_flipped &&
+       first_non_flipped_card) {
+        // Draw a card back if it is not the first card
+        if(first_non_flipped > 0) {
+            card_render_back(x, y + position, false, buffer, 5);
+            // Increment loop start index and position
+            position += 4;
+            loop_start++;
+            had_top = true;
+        }
+
+        // Draw the front side of the first non-flipped card
+        card_try_render(
+            first_non_flipped_card, x, y + position, false, buffer, deck->count == 1 ? 22 : 9);
+
+        position += 8;
+        loop_start++; // Increment loop start index
+    }
+
+    // Draw the selected card with adjusted visibility
+    if(loop_start > selection) {
+        if(!had_top && first_non_flipped > 0) {
+            card_render_back(x, y + position, false, buffer, 5);
+            position += 4;
+            loop_start++;
+        }
+
+        Card* selected_card = (Card*)list_peek_index(deck, selection);
+        check_pointer(selected_card);
+        // Draw the front side of the selected card
+        card_try_render(selected_card, x, y + position, showDark, buffer, 9);
+        position += 8;
+        loop_start++; // Increment loop start index
+    }
+
+    int height = 5;
+    ListItem* curr = list_get_index(deck, loop_start);
+    for(uint8_t i = loop_start; i < loop_end; i++) {
+        check_pointer(curr);
+        if(!curr) break;
+
+        if(i >= loop_start && i < loop_end) {
+            height = 5;
+            if((i + 1) == loop_end)
+                height = 22;
+            else if(i == selection || i == first_non_flipped)
+                height = 9;
+            Card* c = (Card*)curr->data;
+            check_pointer(c);
+            card_try_render(c, x, y + position, i == selection && showDark, buffer, height);
+            if(i == selection || i == first_non_flipped) position += 4;
+            position += 4;
+        }
+        curr = curr->next;
+    }
+}
+
+void deck_render(
+    List* deck,
+    DeckType type,
+    int16_t x,
+    int16_t y,
+    int8_t selected,
+    bool draw_empty,
+    Buffer* buffer) {
+    switch(type) {
+    case Normal:
+        card_try_render(list_peek_back(deck), x, y, selected == 1, buffer, 22);
+        break;
+    case Vertical:
+        if(deck && deck->count > 0)
+            deck_render_vertical(deck, x, y, selected, buffer);
+        else if(draw_empty)
+            card_render_slot(x, y, selected == 1, buffer);
+        break;
+    case Pile:
+        break;
+    }
+}
+
+Card* deck_first_non_flipped(List* deck, int8_t* index) {
+    ListItem* curr = deck->head;
+    for(int8_t i = 0; i < (int8_t)deck->count; i++) {
+        if(!curr->data) break;
+        Card* card = (Card*)curr->data;
+        if(card->exposed) {
+            (*index) = i;
+            return card;
+        }
+        curr = curr->next;
+    }
+    (*index) = -1;
+    return NULL;
+}

+ 73 - 0
solitaire/src/util/card.h

@@ -0,0 +1,73 @@
+#pragma once
+
+#include <furi.h>
+#include "buffer.h"
+#include "list.h"
+
+typedef enum {
+    NONE = -1,
+    TWO = 0, //1
+    THREE = 1, //2
+    FOUR = 2, //3
+    FIVE = 3, //4
+    SIX = 4, //5
+    SEVEN = 5, //6
+    EIGHT = 6, //7
+    NINE = 7, //8
+    TEN = 8, //9
+    JACK = 9, //10
+    QUEEN = 10, //11
+    KING = 11, //12
+    ACE = 12, //13
+} CardValue;
+
+typedef struct {
+    uint8_t suit;
+    CardValue value;
+    bool exposed;
+} Card;
+
+typedef enum {
+    Normal,
+    Vertical,
+    Pile
+} DeckType;
+
+void card_render_front(
+    Card* c,
+    int16_t x,
+    int16_t y,
+    bool selected,
+    Buffer* buffer,
+    uint8_t size_limit);
+
+void card_render_slot(int16_t x, int16_t y, bool selected, Buffer* buffer);
+
+void card_render_back(int16_t x, int16_t y, bool selected, Buffer* buffer, uint8_t size_limit);
+
+void card_try_render(
+    Card* c,
+    int16_t x,
+    int16_t y,
+    bool selected,
+    Buffer* buffer,
+    uint8_t size_limit);
+
+bool card_test_foundation(Card* data, Card* target);
+
+bool card_test_column(Card* data, Card* target);
+
+List* deck_generate(uint8_t deck_count);
+
+void deck_free(List* deck);
+
+Card* deck_first_non_flipped(List* deck, int8_t* index);
+
+void deck_render(
+    List* deck,
+    DeckType type,
+    int16_t x,
+    int16_t y,
+    int8_t selected,
+    bool draw_empty,
+    Buffer* buffer);

+ 6 - 0
solitaire/src/util/game_loop.h

@@ -0,0 +1,6 @@
+#pragma once
+#include <furi.h>
+
+struct GameState {
+    bool (*update)(void* inputState);
+};

+ 42 - 0
solitaire/src/util/helpers.c

@@ -0,0 +1,42 @@
+#include "helpers.h"
+#include <furi.h>
+#include <math.h>
+
+float inverse_tanh(double x) {
+    return 0.5f * (float)log((1 + x) / (1 - x));
+}
+
+float lerp_number(float a, float b, float t) {
+    if(t >= 1) return b;
+    if(t <= 0) return a;
+    return (1 - t) * a + t * b;
+}
+
+bool _test_ptr(void* p) {
+    return p != NULL;
+}
+
+bool _check_ptr(void* p, const char* file, int line, const char* func) {
+    UNUSED(file);
+    UNUSED(line);
+    UNUSED(func);
+    if(p == NULL) {
+        FURI_LOG_W("App", "[NULLPTR] %s:%s():%i", get_basename((char*)file), func, line);
+    }
+
+    return _test_ptr(p);
+}
+
+char* get_basename(const char* path) {
+    const char* base = path;
+    while(*path) {
+        if(*path++ == '/') {
+            base = path;
+        }
+    }
+    return (char*)base;
+}
+
+size_t curr_time() {
+    return DWT->CYCCNT;
+}

+ 32 - 0
solitaire/src/util/helpers.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include <furi.h>
+//#define DEBUG_BUILD
+
+#define M_PIX2    6.28318530717958647692 /* 2 pi */
+#define l_abs(x)  ((x) < 0 ? -(x) : (x))
+#define DEG_2_RAD 0.01745329251994329576f
+#define RAD_2_DEG 565.48667764616278292327f
+
+#ifdef DEBUG_BUILD
+#define check_pointer(X) _check_ptr(X, __FILE__, __LINE__, __FUNCTION__)
+#else
+#define check_pointer(X) _test_ptr(X)
+#define trace(X)         while(0)
+#endif
+
+char* get_basename(const char* path);
+
+#ifndef basename
+#define basename(path) get_basename(path)
+#endif
+
+bool _test_ptr(void* p);
+
+bool _check_ptr(void* p, const char* file, int line, const char* func);
+
+float inverse_tanh(double x);
+
+float lerp_number(float a, float b, float t);
+
+size_t curr_time();

+ 301 - 0
solitaire/src/util/list.c

@@ -0,0 +1,301 @@
+#include "list.h"
+#include "helpers.h"
+
+List* list_make() {
+    List* list = malloc(sizeof(List));
+    if(list != NULL) {
+        list->head = NULL;
+        list->tail = NULL;
+        list->count = 0;
+    } else {
+        FURI_LOG_W("LIST", "Failed to create list");
+    }
+    return list;
+}
+
+void list_free(List* list) {
+    if(list == NULL) return;
+
+    ListItem* start = list->head;
+    while(start) {
+        ListItem* next = start->next;
+        free(start->data);
+        free(start);
+        start = next;
+    }
+    free(list);
+}
+
+void list_free_data(List* list) {
+    if(list == NULL) return;
+
+    ListItem* start = list->head;
+    while(start) {
+        ListItem* next = start->next;
+        free(start->data);
+        free(start);
+        start = next;
+    }
+
+    list->head = NULL;
+    list->tail = NULL;
+    list->count = 0;
+}
+
+void list_clear(List* list) {
+    if(list == NULL) return;
+    ListItem* start = list->head;
+    while(start) {
+        ListItem* next = start->next;
+        free(start);
+        start = next;
+    }
+    list->head = NULL;
+    list->tail = NULL;
+    list->count = 0;
+}
+
+void list_push_back(void* data, List* list) {
+    if(list == NULL) {
+        FURI_LOG_W("LIST", "List not initialized, cannot push data");
+        return;
+    }
+    ListItem* newItem = malloc(sizeof(ListItem));
+    if(newItem != NULL) {
+        newItem->data = data;
+        newItem->next = NULL;
+        newItem->prev = list->tail;
+
+        if(list->tail == NULL) {
+            list->head = newItem;
+            list->tail = newItem;
+        } else {
+            list->tail->next = newItem;
+            list->tail = newItem;
+        }
+        list->count++;
+    }
+}
+
+void list_push_front(void* data, List* list) {
+    if(list == NULL) {
+        FURI_LOG_W("LIST", "List not initialized, cannot push data");
+        return;
+    }
+    ListItem* newItem = malloc(sizeof(ListItem));
+    if(newItem != NULL) {
+        newItem->data = data;
+        newItem->next = list->head;
+        if(list->head == NULL) {
+            list->head = newItem;
+            list->tail = newItem;
+        } else {
+            list->head->prev = newItem;
+            list->head = newItem;
+        }
+        list->count++;
+    }
+}
+
+void* list_pop_back(List* list) {
+    if(!check_pointer(list)) {
+        FURI_LOG_W("LIST", "List not initialized, cannot pop data");
+        return NULL;
+    }
+
+    if(!check_pointer(list->tail)) {
+        FURI_LOG_W("LIST", "List empty, cannot pop");
+        return NULL;
+    }
+    void* data = list->tail->data;
+    check_pointer(data);
+    ListItem* prev = list->tail->prev;
+    check_pointer(prev);
+    if(prev) {
+        prev->next = NULL;
+    } else {
+        list->head = NULL;
+    }
+    free(list->tail);
+    list->tail = prev;
+    list->count--;
+    return data;
+}
+
+void* list_pop_front(List* list) {
+    if(!check_pointer(list)) {
+        FURI_LOG_W("LIST", "List not initialized, cannot pop data");
+        return NULL;
+    }
+    if(!check_pointer(list->head)) {
+        FURI_LOG_W("LIST", "List empty, cannot pop");
+        return NULL;
+    }
+    void* data = list->head->data;
+    check_pointer(data);
+    ListItem* next = list->head->next;
+    if(next) {
+        next->prev = NULL;
+    } else {
+        list->tail = NULL;
+    }
+    free(list->head);
+    list->head = next;
+    list->count--;
+    return data;
+}
+
+void* list_pop_at(size_t index, List* list) {
+    if(!check_pointer(list)) {
+        FURI_LOG_W("LIST", "List not initialized, cannot pop data");
+        return NULL;
+    }
+    if(index >= list->count) {
+        FURI_LOG_W("LIST", "Out of range, cannot pop");
+        return NULL;
+    }
+    if(index == 0) {
+        return list_pop_front(list);
+    }
+    if(index == list->count - 1) {
+        return list_pop_back(list);
+    }
+    ListItem* current = list->head;
+    check_pointer(current);
+    for(size_t i = 0; i < index; i++) {
+        current = current->next;
+    }
+    check_pointer(current);
+    void* data = current->data;
+    check_pointer(data);
+    current->prev->next = current->next;
+    current->next->prev = current->prev;
+    free(current);
+    list->count--;
+    return data;
+}
+
+void list_remove_item(void* data, List* list) {
+    if(list == NULL) {
+        return;
+    }
+    ListItem* current = list->head;
+    while(current != NULL) {
+        if(current->data == data) {
+            if(current->prev != NULL) {
+                current->prev->next = current->next;
+            } else {
+                list->head = current->next;
+            }
+            if(current->next != NULL) {
+                current->next->prev = current->prev;
+            } else {
+                list->tail = current->prev;
+            }
+            free(current);
+            list->count--;
+            break;
+        }
+        current = current->next;
+    }
+}
+
+void list_remove_at(size_t index, List* list) {
+    if(list == NULL) {
+        return;
+    }
+    void* d = list_pop_at(index, list);
+    free(d);
+}
+
+List* list_splice(size_t index, size_t count, List* list) {
+    if(list == NULL) {
+        FURI_LOG_W("LIST", "List not initialized, cannot splice");
+        return NULL;
+    }
+    List* newList = list_make();
+    if(index >= list->count || count == 0) {
+        return newList;
+    }
+    if(index == 0 && count >= list->count) {
+        newList->head = list->head;
+        newList->tail = list->tail;
+        newList->count = list->count;
+        list->head = NULL;
+        list->tail = NULL;
+        list->count = 0;
+        return newList;
+    }
+
+    ListItem* start = list->head;
+    for(size_t i = 0; i < index; i++) {
+        start = start->next;
+    }
+
+    size_t c = count;
+    if(c > list->count) c = list->count - index;
+
+    ListItem* end = start;
+    for(size_t i = 1; i < c && end->next; i++) {
+        end = end->next;
+        if(end->next == NULL) c = i;
+    }
+
+    newList->head = start;
+    newList->tail = end;
+    newList->count = c;
+
+    if(end->next != NULL) {
+        end->next->prev = start->prev;
+    } else {
+        list->tail = start->prev;
+    }
+
+    if(start->prev != NULL) {
+        start->prev->next = end->next;
+    } else {
+        list->head = end->next;
+    }
+
+    start->prev = NULL;
+    end->next = NULL;
+    list->count -= c;
+    return newList;
+}
+
+void* list_peek_front(List* list) {
+    if(list == NULL || list->head == NULL) {
+        return NULL;
+    }
+    return list->head->data;
+}
+
+ListItem* list_get_index(List* list, size_t index) {
+    check_pointer(list);
+    ListItem* curr = list->head;
+    check_pointer(curr);
+    if(index > list->count || !curr) return NULL;
+    for(size_t i = 0; i < index; i++) {
+        if(!curr) return NULL;
+        curr = curr->next;
+    }
+    return curr;
+}
+
+void* list_peek_index(List* list, size_t index) {
+    ListItem* curr = list_get_index(list, index);
+    check_pointer(curr);
+    if(curr) {
+        check_pointer(curr->data);
+        return curr->data;
+    }
+    return NULL;
+}
+
+void* list_peek_back(List* list) {
+    if(!list || !list->tail) {
+        return NULL;
+    }
+    check_pointer(list->tail->data);
+    return list->tail->data;
+}

+ 48 - 0
solitaire/src/util/list.h

@@ -0,0 +1,48 @@
+#pragma once
+
+#include <furi.h>
+
+struct ListItem;
+
+typedef struct ListItem {
+    void* data;
+    struct ListItem* next;
+    struct ListItem* prev;
+} ListItem;
+
+typedef struct {
+    ListItem* head;
+    ListItem* tail;
+    size_t count;
+} List;
+
+List* list_make();
+//frees everything
+void list_free(List* list);
+//clears the list, but not data
+void list_clear(List* list);
+
+//clears the list, and data
+void list_free_data(List* list);
+
+void list_push_back(void* data, List* list);
+
+void list_push_front(void* data, List* list);
+
+void* list_pop_back(List* list);
+
+void* list_pop_front(List* list);
+
+void* list_pop_at(size_t index, List* list);
+
+void list_remove_item(void* data, List* list);
+
+void list_remove_at(size_t index, List* list);
+
+List* list_splice(size_t index, size_t count, List* list);
+
+void* list_peek_front(List* list);
+void* list_peek_index(List* list, size_t index);
+ListItem* list_get_index(List* list, size_t index);
+
+void* list_peek_back(List* list);

+ 127 - 0
solitaire/src/util/vector.c

@@ -0,0 +1,127 @@
+#include "vector.h"
+
+#include <math.h>
+#include "helpers.h"
+
+Vector vector_copy(Vector* const other) {
+    return (Vector){.x = other->x, .y = other->y};
+}
+
+//basic math
+
+void vector_add(Vector* const a, Vector* const b, Vector* target) {
+    target->x = a->x + b->x;
+    target->y = a->y + b->y;
+}
+
+void vector_sub(Vector* const a, Vector* const b, Vector* target) {
+    target->x = a->x - b->x;
+    target->y = a->y - b->y;
+}
+
+void vector_mul(Vector* const a, Vector* const b, Vector* target) {
+    target->x = a->x * b->x;
+    target->y = a->y * b->y;
+}
+
+void vector_div(Vector* const a, Vector* const b, Vector* target) {
+    target->x = a->x / b->x;
+    target->y = a->y / b->y;
+}
+
+// computations
+
+float vector_length_sqrt(Vector* const a) {
+    return a->x * a->x + a->y * a->y;
+}
+
+float vector_length(Vector* const v) {
+    return sqrtf(vector_length_sqrt(v));
+}
+
+float vector_distance(Vector* const a, Vector* const b) {
+    Vector v;
+    vector_sub(a, b, &v);
+    return vector_length(&v);
+}
+
+void vector_normalized(Vector* const v, Vector* target) {
+    float length = vector_length(v);
+    if(length == 0) {
+        target->x = 0;
+        target->y = 0;
+    } else {
+        target->x = v->x / length;
+        target->y = v->y / length;
+    }
+}
+
+void vector_inverse(Vector* const v, Vector* target) {
+    target->x = -v->x;
+    target->y = -v->y;
+}
+
+float vector_dot(Vector* const a, Vector* const b) {
+    return a->x * b->x + a->y * b->y;
+}
+
+void vector_rotate(Vector* const v, float deg, Vector* target) {
+    float tx = v->x;
+    float ty = v->y;
+    target->x = (float)(cosf(deg) * tx - sinf(deg) * ty);
+    target->y = (float)(sinf(deg) * tx + cosf(deg) * ty);
+}
+
+void vector_rounded(Vector* const source, Vector* target) {
+    target->x = roundf(source->x);
+    target->y = roundf(source->y);
+}
+
+float vector_cross(Vector* const a, Vector* const b) {
+    return a->x * b->y - a->y * b->x;
+}
+
+void vector_perpendicular(Vector* const v, Vector* target) {
+    target->x = -v->y;
+    target->y = v->x;
+}
+
+void vector_project(
+    Vector* point,
+    Vector* const line_a,
+    Vector* const line_b,
+    Vector* result,
+    bool* success) {
+    Vector ab;
+    vector_sub(line_b, line_a, &ab);
+    Vector ap;
+    vector_sub(point, line_a, &ap);
+    float dot = vector_dot(&ap, &ab);
+    float length_squared = vector_length_sqrt(&ab);
+    float t = dot / length_squared;
+    if(t < 0 || t > 1) {
+        *success = false;
+    } else {
+        result->x = line_a->x + ab.x * t;
+        result->y = line_a->y + ab.y * t;
+        *success = true;
+    }
+}
+
+void vector_lerp(Vector* const start, Vector* const end, float time, Vector* result) {
+    result->x = lerp_number(start->x, end->x, time);
+    result->y = lerp_number(start->y, end->y, time);
+}
+
+void vector_quadratic(
+    Vector* const start,
+    Vector* const control,
+    Vector* const end,
+    float time,
+    Vector* result) {
+    Vector a;
+    vector_lerp(start, control, time, &a);
+    Vector b;
+    vector_lerp(control, end, time, &b);
+    vector_lerp(&a, &b, time, result);
+}

+ 61 - 0
solitaire/src/util/vector.h

@@ -0,0 +1,61 @@
+#pragma once
+#include <furi.h>
+
+typedef struct {
+    float x, y;
+} Vector;
+
+#define VECTOR_ZERO    \
+    (Vector) {         \
+        .x = 0, .y = 0 \
+    }
+
+Vector vector_copy(Vector* const other);
+
+//basic math
+
+void vector_add(Vector* const a, Vector* const b, Vector* target);
+
+void vector_sub(Vector* const a, Vector* const b, Vector* target);
+
+void vector_mul(Vector* const a, Vector* const b, Vector* target);
+
+void vector_div(Vector* const a, Vector* const b, Vector* target);
+
+// computations
+
+float vector_length_sqrt(Vector* const a);
+
+float vector_length(Vector* const v);
+
+float vector_distance(Vector* const a, Vector* const b);
+
+void vector_normalized(Vector* const v, Vector* target);
+
+void vector_inverse(Vector* const v, Vector* target);
+
+float vector_dot(Vector* const a, Vector* const b);
+
+void vector_rotate(Vector* const v, float deg, Vector* target);
+
+void vector_rounded(Vector* const source, Vector* target);
+
+float vector_cross(Vector* const a, Vector* const b);
+
+void vector_perpendicular(Vector* const v, Vector* target);
+
+void vector_project(
+    Vector* point,
+    Vector* const line_a,
+    Vector* const line_b,
+    Vector* result,
+    bool* success);
+
+void vector_lerp(Vector* const start, Vector* const end, float time, Vector* result);
+
+void vector_quadratic(
+    Vector* const start,
+    Vector* const control,
+    Vector* const end,
+    float time,
+    Vector* result);