Просмотр исходного кода

Merge branch 'doofy-dev/solitaire_2.0' into main

# Conflicts:
#	defines.h
#	solitaire.c
Tibor Tálosi 1 год назад
Родитель
Сommit
fad579378e
69 измененных файлов с 3400 добавлено и 610 удалено
  1. 0 3
      .gitmodules
  2. 35 18
      README.md
  3. 6 3
      application.fam
  4. 512 0
      assets.c
  5. 282 0
      assets.h
  6. BIN
      assets/10.png
  7. BIN
      assets/2.png
  8. BIN
      assets/3.png
  9. BIN
      assets/4.png
  10. BIN
      assets/5.png
  11. BIN
      assets/6.png
  12. BIN
      assets/7.png
  13. BIN
      assets/8.png
  14. BIN
      assets/9.png
  15. BIN
      assets/A.png
  16. BIN
      assets/J.png
  17. BIN
      assets/K.png
  18. BIN
      assets/Q.png
  19. BIN
      assets/card_graphics.png
  20. BIN
      assets/clubs.png
  21. BIN
      assets/diamonds.png
  22. BIN
      assets/hearths.png
  23. BIN
      assets/joker.png
  24. BIN
      assets/logo.png
  25. BIN
      assets/main_image.png
  26. BIN
      assets/pattern_big.png
  27. BIN
      assets/pattern_small.png
  28. BIN
      assets/solitaire_main.png
  29. BIN
      assets/solve.png
  30. BIN
      assets/spades.png
  31. BIN
      assets/start.png
  32. 0 1
      common
  33. 0 63
      defines.h
  34. 20 0
      docs/CHANGELOG.md
  35. 30 0
      docs/README.md
  36. 0 0
      docs/changelog.md
  37. 44 0
      game_state.h
  38. BIN
      screenshots/catalog_1.png
  39. BIN
      screenshots/catalog_2.png
  40. BIN
      screenshots/catalog_3.png
  41. BIN
      screenshots/catalog_4.png
  42. BIN
      screenshots/catalog_5.png
  43. BIN
      screenshots/catalog_6.png
  44. BIN
      screenshots/solitaire.png
  45. 139 522
      solitaire.c
  46. 103 0
      src/scene/falling_card.c
  47. 11 0
      src/scene/falling_card.h
  48. 119 0
      src/scene/intro_animation.c
  49. 12 0
      src/scene/intro_animation.h
  50. 130 0
      src/scene/main_screen.c
  51. 16 0
      src/scene/main_screen.h
  52. 341 0
      src/scene/play_screen.c
  53. 14 0
      src/scene/play_screen.h
  54. 74 0
      src/scene/result_screen.c
  55. 12 0
      src/scene/result_screen.h
  56. 61 0
      src/scene/scene_setup.h
  57. 204 0
      src/scene/solve_screen.c
  58. 8 0
      src/scene/solve_screen.h
  59. 258 0
      src/util/buffer.c
  60. 74 0
      src/util/buffer.h
  61. 230 0
      src/util/card.c
  62. 55 0
      src/util/card.h
  63. 6 0
      src/util/game_loop.h
  64. 45 0
      src/util/helpers.c
  65. 39 0
      src/util/helpers.h
  66. 304 0
      src/util/list.c
  67. 48 0
      src/util/list.h
  68. 119 0
      src/util/vector.c
  69. 49 0
      src/util/vector.h

+ 0 - 3
.gitmodules

@@ -1,3 +0,0 @@
-[submodule "common"]
-	path = common
-	url = https://github.com/teeebor/flipper_helpers.git

+ 35 - 18
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 - 3
application.fam

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

+ 512 - 0
assets.c

@@ -0,0 +1,512 @@
+#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
+}};

+ 282 - 0
assets.h

@@ -0,0 +1,282 @@
+#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
assets/card_graphics.png


BIN
assets/clubs.png


BIN
assets/diamonds.png


BIN
assets/hearths.png


BIN
assets/joker.png


BIN
assets/logo.png


BIN
assets/main_image.png


BIN
assets/pattern_big.png


BIN
assets/pattern_small.png


BIN
assets/solitaire_main.png


BIN
assets/solve.png


BIN
assets/spades.png


BIN
assets/start.png


+ 0 - 1
common

@@ -1 +0,0 @@
-Subproject commit 4ef796c450428521fc576c8e5c993d027061414d

+ 0 - 63
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;

+ 20 - 0
docs/CHANGELOG.md

@@ -0,0 +1,20 @@
+## 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 

+ 30 - 0
docs/README.md

@@ -0,0 +1,30 @@
+# 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.

+ 0 - 0
docs/changelog.md


+ 44 - 0
game_state.h

@@ -0,0 +1,44 @@
+#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
screenshots/catalog_1.png


BIN
screenshots/catalog_2.png


BIN
screenshots/catalog_3.png


BIN
screenshots/catalog_4.png


BIN
screenshots/catalog_5.png


BIN
screenshots/catalog_6.png


BIN
screenshots/solitaire.png


+ 139 - 522
solitaire.c

@@ -1,567 +1,184 @@
-#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,
-
-        &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},
-};
-
-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;
-
-    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;
-
-    return (a_letter - 1) == b_letter;
-}
-
-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);
-        }
+#include "game_state.h"
+#include "src/util/list.h"
+#include "src/scene/scene_setup.h"
+#include "src/util/helpers.h"
 
-        int8_t pos[2] = {columns[game_state->selectColumn][0],
-                         columns[game_state->selectColumn][game_state->selectRow + 1]};
+static List *game_logic;
+static ListItem *current_state;
+static FuriMutex *update_mutex;
 
-        /*     draw_icon_clip(canvas, &I_card_graphics, pos[0] + CARD_HALF_WIDTH, pos[1] + CARD_HALF_HEIGHT, 30, 5, 5, 5,
-                            Filled);*/
+static void gui_input_events_callback(const void *value, void *ctx) {
+    furi_mutex_acquire(update_mutex, FuriWaitForever);
+    GameState *instance = ctx;
+    const InputEvent *event = value;
 
-        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);
-        }
+    if (event->key == InputKeyBack && event->type == InputTypeLong) {
+        FURI_LOG_W("INPUT", "EXIT");
+        instance->exit = true;
+    }
 
-        clone_buffer(get_buffer(canvas), game_state->animation.buffer);
-    }else{
-        clone_buffer(game_state->animation.buffer, get_buffer(canvas));
+    if (current_state) {
+        ((GameLogic *) current_state->data)->input(instance, event->key, event->type);
     }
+    furi_mutex_release(update_mutex);
 }
 
-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));
 
-        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);
+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);
 
-    clone_buffer(get_buffer(canvas), game_state->animation.buffer);
-}
+    current_state = game_logic->head;
 
-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;
-    }
+    GameState *instance = malloc(sizeof(GameState));
+    ((GameLogic *) current_state->data)->start(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;
+    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();
+        }
+        instance->tableau[i] = list_make();
     }
 
-    furi_mutex_release(game_state->mutex);
+    instance->animated_card.position = VECTOR_ZERO;
+    instance->animated_card.velocity = VECTOR_ZERO;
 
-}
 
-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;
-    }
-    game_state->dragging_hand.index = 0;
-}
+    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);
 
-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--;
-    }
-    if (game_state->dragging_hand.index > 0)
-        game_state->selected_card = 0;
-    return false;
-}
 
-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;
-        }
-    }
-    return false;
-}
+    instance->input_subscription =
+        furi_pubsub_subscribe(instance->input, gui_input_events_callback, 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;
-        }
-    }
-    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;
-                    }
-                }
-            } 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;
-                        }
-                    }
-                }
-            }
+    return instance;
+}
 
+static void cleanup(GameState *instance) {
+    furi_pubsub_unsubscribe(instance->input, instance->input_subscription);
+    notification_message_block(instance->notification_app, &sequence_display_backlight_enforce_auto);
 
-            if (!wasAction) {
-                notification_message(notification, &sequence_fail);
-            }
+    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]);
     }
-    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 (game_state->animation.y > 41) {
-            game_state->animation.y = 41;
-            game_state->animation.vy = -(game_state->animation.vy * 0.7f);
-        }
-    }
+    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);
 }
 
-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);
-        }
+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;
     }
-
-    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;
+    ((GameLogic *) current_state->data)->start(instance);
 }
 
-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 prev_scene(GameState *instance) {
+    FURI_LOG_W("SCENE", "Prev scene");
+    current_state = game_logic->head;
+    if (current_state->prev == NULL) {
+        instance->exit = true;
+        return;
+    }
+    ((GameLogic *) current_state->data)->start(instance);
 }
 
+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);
+        }
+        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{
+                curr_state->render(instance);
+                buffer_swap_back(instance->buffer);
+                buffer_render(instance->buffer, instance->canvas);
+            }
+            canvas_commit(instance->canvas);
 
-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);
-}
+            if (instance->clearBuffer)
+                buffer_clear(instance->buffer);
 
-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);
+            instance->clearBuffer = true;
+            instance->lateRender = false;
+            instance->isDirty = false;
+        }
+        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;
-            }
-        } else {
-            FURI_LOG_W(APP_NAME, "osMessageQueue: event timeout");
-            // event timeout
-        }
-
-        view_port_update(view_port);
-        furi_mutex_release(game_state->mutex);
-    }
-
-
-    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;
 }

+ 103 - 0
src/scene/falling_card.c

@@ -0,0 +1,103 @@
+#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
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);

+ 119 - 0
src/scene/intro_animation.c

@@ -0,0 +1,119 @@
+#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) {
+    FURI_LOG_W("DELTA", "%f", (double)state->delta_time);
+    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;
+    }
+
+}

+ 12 - 0
src/scene/intro_animation.h

@@ -0,0 +1,12 @@
+#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);

+ 130 - 0
src/scene/main_screen.c

@@ -0,0 +1,130 @@
+#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
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);

+ 341 - 0
src/scene/play_screen.c

@@ -0,0 +1,341 @@
+#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);
+    }
+}

+ 14 - 0
src/scene/play_screen.h

@@ -0,0 +1,14 @@
+#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);

+ 74 - 0
src/scene/result_screen.c

@@ -0,0 +1,74 @@
+#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;
+    }
+}

+ 12 - 0
src/scene/result_screen.h

@@ -0,0 +1,12 @@
+#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);

+ 61 - 0
src/scene/scene_setup.h

@@ -0,0 +1,61 @@
+#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
+};

+ 204 - 0
src/scene/solve_screen.c

@@ -0,0 +1,204 @@
+#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
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);

+ 258 - 0
src/util/buffer.c

@@ -0,0 +1,258 @@
+#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;
+        }
+    }
+}

+ 74 - 0
src/util/buffer.h

@@ -0,0 +1,74 @@
+#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);
+
+

+ 230 - 0
src/util/card.c

@@ -0,0 +1,230 @@
+#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;
+}

+ 55 - 0
src/util/card.h

@@ -0,0 +1,55 @@
+#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
src/util/game_loop.h

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

+ 45 - 0
src/util/helpers.c

@@ -0,0 +1,45 @@
+#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;
+}
+
+void _log_location(const char *p, const char *file, int line, const char *func) {
+    FURI_LOG_W("App", "[TRACE][%s] %s:%s():%i", p, get_basename((char *) file), func, line);
+}
+
+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; }
+

+ 39 - 0
src/util/helpers.h

@@ -0,0 +1,39 @@
+#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__)
+#define log_location(X) _log_location( X, __FILE__, __LINE__, __FUNCTION__)
+#else
+#define check_pointer(X) _test_ptr(X)
+#define trace(X) while(0)
+#endif
+
+#define CHECK_HEAP() FURI_LOG_I("Solitaire", "Free/total heap: %zu / %zu", memmgr_get_free_heap(), memmgr_get_total_heap())
+
+
+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);
+
+void _log_location(const char *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();

+ 304 - 0
src/util/list.c

@@ -0,0 +1,304 @@
+#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;
+    FURI_LOG_W("LIST", "list_clear");
+    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
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);

+ 119 - 0
src/util/vector.c

@@ -0,0 +1,119 @@
+#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);
+}

+ 49 - 0
src/util/vector.h

@@ -0,0 +1,49 @@
+#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);