Explorar o código

Add solitaire from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: solitaire
git-subtree-mainline: c6044f62795ae7ce2c620f913939fd78d67d18bf
git-subtree-split: d5beb1782b0b8d89c460f92f86f6fbdb2b5b757e
Willy-JL %!s(int64=2) %!d(string=hai) anos
pai
achega
ffe4ad5be5

+ 1 - 0
solitaire/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev base_pack/solitaire

+ 21 - 0
solitaire/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Tibor Tálosi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 15 - 0
solitaire/application.fam

@@ -0,0 +1,15 @@
+App(
+    appid="solitaire",
+    name="Solitaire",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="solitaire_app",
+    requires=["gui","storage","canvas"],
+    stack_size=2 * 1024,
+    order=30,
+    fap_icon="solitaire_10px.png",
+    fap_category="Games",
+    fap_icon_assets="assets",
+    fap_author="@teeebor",
+    fap_version="1.1",
+    fap_description="Solitaire game",
+)

BIN=BIN
solitaire/assets/card_graphics.png


BIN=BIN
solitaire/assets/solitaire_main.png


+ 353 - 0
solitaire/common/card.c

@@ -0,0 +1,353 @@
+#include "card.h"
+#include "dml.h"
+#include "ui.h"
+
+#define CARD_DRAW_X_START 108
+#define CARD_DRAW_Y_START 38
+#define CARD_DRAW_X_SPACE 10
+#define CARD_DRAW_Y_SPACE 8
+#define CARD_DRAW_X_OFFSET 4
+#define CARD_DRAW_FIRST_ROW_LENGTH 7
+
+uint8_t pips[4][3] = {
+    {21, 10, 7}, //spades
+    {7, 10, 7}, //hearts
+    {0, 10, 7}, //diamonds
+    {14, 10, 7}, //clubs
+};
+uint8_t letters[13][3] = {
+    {0, 0, 5},
+    {5, 0, 5},
+    {10, 0, 5},
+    {15, 0, 5},
+    {20, 0, 5},
+    {25, 0, 5},
+    {30, 0, 5},
+    {0, 5, 5},
+    {5, 5, 5},
+    {10, 5, 5},
+    {15, 5, 5},
+    {20, 5, 5},
+    {25, 5, 5},
+};
+
+//region Player card positions
+uint8_t playerCardPositions[22][4] = {
+    //first row
+    {108, 38},
+    {98, 38},
+    {88, 38},
+    {78, 38},
+    {68, 38},
+    {58, 38},
+    {48, 38},
+    {38, 38},
+    //second row
+    {104, 26},
+    {94, 26},
+    {84, 26},
+    {74, 26},
+    {64, 26},
+    {54, 26},
+    {44, 26},
+    //third row
+    {99, 14},
+    {89, 14},
+    {79, 14},
+    {69, 14},
+    {59, 14},
+    {49, 14},
+};
+//endregion
+Icon* card_graphics = NULL;
+
+void set_card_graphics(const Icon* graphics) {
+    card_graphics = (Icon*)graphics;
+}
+
+void draw_card_at_colored(
+    int8_t pos_x,
+    int8_t pos_y,
+    uint8_t pip,
+    uint8_t character,
+    bool inverted,
+    Canvas* const canvas) {
+    DrawMode primary = inverted ? Black : White;
+    DrawMode secondary = inverted ? White : Black;
+    draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, primary);
+    draw_rounded_box_frame(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
+
+    uint8_t* drawInfo = pips[pip];
+    uint8_t px = drawInfo[0], py = drawInfo[1], s = drawInfo[2];
+
+    uint8_t left = pos_x + 2;
+    uint8_t right = (pos_x + CARD_WIDTH - s - 2);
+    uint8_t top = pos_y + 2;
+    uint8_t bottom = (pos_y + CARD_HEIGHT - s - 2);
+
+    draw_icon_clip(canvas, card_graphics, right, top, px, py, s, s, secondary);
+    draw_icon_clip_flipped(canvas, card_graphics, left, bottom, px, py, s, s, secondary);
+
+    drawInfo = letters[character];
+    px = drawInfo[0], py = drawInfo[1], s = drawInfo[2];
+    left = pos_x + 2;
+    right = (pos_x + CARD_WIDTH - s - 2);
+    top = pos_y + 2;
+    bottom = (pos_y + CARD_HEIGHT - s - 2);
+
+    draw_icon_clip(canvas, card_graphics, left, top + 1, px, py, s, s, secondary);
+    draw_icon_clip_flipped(canvas, card_graphics, right, bottom - 1, px, py, s, s, secondary);
+}
+
+void draw_card_at(int8_t pos_x, int8_t pos_y, uint8_t pip, uint8_t character, Canvas* const canvas) {
+    draw_card_at_colored(pos_x, pos_y, pip, character, false, canvas);
+}
+
+void draw_deck(const Card* cards, uint8_t count, Canvas* const canvas) {
+    for(int i = count - 1; i >= 0; i--) {
+        draw_card_at(
+            playerCardPositions[i][0],
+            playerCardPositions[i][1],
+            cards[i].pip,
+            cards[i].character,
+            canvas);
+    }
+}
+
+Vector card_pos_at_index(uint8_t index) {
+    return (Vector){playerCardPositions[index][0], playerCardPositions[index][1]};
+}
+
+void draw_card_back_at(int8_t pos_x, int8_t pos_y, Canvas* const canvas) {
+    draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, White);
+    draw_rounded_box_frame(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
+
+    draw_icon_clip(canvas, card_graphics, pos_x + 1, pos_y + 1, 35, 0, 15, 21, Black);
+}
+
+void generate_deck(Deck* deck_ptr, uint8_t deck_count) {
+    uint16_t counter = 0;
+    if(deck_ptr->cards != NULL) {
+        free(deck_ptr->cards);
+    }
+
+    deck_ptr->deck_count = deck_count;
+    deck_ptr->card_count = deck_count * 52;
+    deck_ptr->cards = malloc(sizeof(Card) * deck_ptr->card_count);
+
+    for(uint8_t deck = 0; deck < deck_count; deck++) {
+        for(uint8_t pip = 0; pip < 4; pip++) {
+            for(uint8_t label = 0; label < 13; label++) {
+                deck_ptr->cards[counter] = (Card){pip, label, false, false};
+                counter++;
+            }
+        }
+    }
+}
+
+void shuffle_deck(Deck* deck_ptr) {
+    srand(DWT->CYCCNT);
+    deck_ptr->index = 0;
+    int max = deck_ptr->deck_count * 52;
+    for(int i = 0; i < max; i++) {
+        int r = i + (rand() % (max - i));
+        Card tmp = deck_ptr->cards[i];
+        deck_ptr->cards[i] = deck_ptr->cards[r];
+        deck_ptr->cards[r] = tmp;
+    }
+}
+
+uint8_t hand_count(const Card* cards, uint8_t count) {
+    uint8_t aceCount = 0;
+    uint8_t score = 0;
+
+    for(uint8_t i = 0; i < count; i++) {
+        if(cards[i].character == 12)
+            aceCount++;
+        else {
+            if(cards[i].character > 8)
+                score += 10;
+            else
+                score += cards[i].character + 2;
+        }
+    }
+
+    for(uint8_t i = 0; i < aceCount; i++) {
+        if((score + 11) <= 21)
+            score += 11;
+        else
+            score++;
+    }
+
+    return score;
+}
+
+void draw_card_animation(
+    Card animatingCard,
+    Vector from,
+    Vector control,
+    Vector to,
+    float t,
+    bool extra_margin,
+    Canvas* const canvas) {
+    float time = t;
+    if(extra_margin) {
+        time += 0.2;
+    }
+
+    Vector currentPos = quadratic_2d(from, control, to, time);
+    if(t > 1) {
+        draw_card_at(
+            currentPos.x, currentPos.y, animatingCard.pip, animatingCard.character, canvas);
+    } else {
+        if(t < 0.5)
+            draw_card_back_at(currentPos.x, currentPos.y, canvas);
+        else
+            draw_card_at(
+                currentPos.x, currentPos.y, animatingCard.pip, animatingCard.character, canvas);
+    }
+}
+
+void init_hand(Hand* hand_ptr, uint8_t count) {
+    hand_ptr->cards = malloc(sizeof(Card) * count);
+    hand_ptr->index = 0;
+    hand_ptr->max = count;
+}
+
+void free_hand(Hand* hand_ptr) {
+    FURI_LOG_D("CARD", "Freeing hand");
+    free(hand_ptr->cards);
+}
+
+void add_to_hand(Hand* hand_ptr, Card card) {
+    FURI_LOG_D("CARD", "Adding to hand");
+    if(hand_ptr->index < hand_ptr->max) {
+        hand_ptr->cards[hand_ptr->index] = card;
+        hand_ptr->index++;
+    }
+}
+
+void draw_card_space(int16_t pos_x, int16_t pos_y, bool highlighted, Canvas* const canvas) {
+    if(highlighted) {
+        draw_rounded_box_frame(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
+        draw_rounded_box_frame(
+            canvas, pos_x + 2, pos_y + 2, CARD_WIDTH - 4, CARD_HEIGHT - 4, White);
+    } else {
+        draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
+        draw_rounded_box_frame(
+            canvas, pos_x + 2, pos_y + 2, CARD_WIDTH - 4, CARD_HEIGHT - 4, White);
+    }
+}
+
+int first_non_flipped_card(Hand hand) {
+    for(int i = 0; i < hand.index; i++) {
+        if(!hand.cards[i].flipped) {
+            return i;
+        }
+    }
+    return hand.index;
+}
+
+void draw_hand_column(
+    Hand hand,
+    int16_t pos_x,
+    int16_t pos_y,
+    int8_t highlight,
+    Canvas* const canvas) {
+    if(hand.index == 0) {
+        draw_card_space(pos_x, pos_y, highlight > 0, canvas);
+        if(highlight == 0)
+            draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Inverse);
+        return;
+    }
+
+    int loopEnd = hand.index;
+    int hStart = max(loopEnd - 4, 0);
+    int pos = 0;
+    int first = first_non_flipped_card(hand);
+    bool wastop = false;
+    if(first >= 0 && first <= hStart && highlight != first) {
+        if(first > 0) {
+            draw_card_back_at(pos_x, pos_y + pos, canvas);
+            pos += 4;
+            hStart++;
+            wastop = true;
+        }
+        draw_card_at_colored(
+            pos_x, pos_y + pos, hand.cards[first].pip, hand.cards[first].character, false, canvas);
+        pos += 8;
+        hStart++;
+    }
+    if(hStart > highlight && highlight >= 0) {
+        if(!wastop && first > 0) {
+            draw_card_back_at(pos_x, pos_y + pos, canvas);
+            pos += 4;
+            hStart++;
+        }
+        draw_card_at_colored(
+            pos_x,
+            pos_y + pos,
+            hand.cards[highlight].pip,
+            hand.cards[highlight].character,
+            true,
+            canvas);
+        pos += 8;
+        hStart++;
+    }
+    for(int i = hStart; i < loopEnd; i++, pos += 4) {
+        if(hand.cards[i].flipped) {
+            draw_card_back_at(pos_x, pos_y + pos, canvas);
+            if(i == highlight)
+                draw_rounded_box(
+                    canvas, pos_x + 1, pos_y + pos + 1, CARD_WIDTH - 2, CARD_HEIGHT - 2, Inverse);
+        } else {
+            draw_card_at_colored(
+                pos_x,
+                pos_y + pos,
+                hand.cards[i].pip,
+                hand.cards[i].character,
+                (i == highlight),
+                canvas);
+            if(i == highlight || i == first) pos += 4;
+        }
+    }
+}
+
+Card remove_from_deck(uint16_t index, Deck* deck) {
+    FURI_LOG_D("CARD", "Removing from deck");
+    Card result = {0, 0, true, false};
+    if(deck->card_count > 0) {
+        deck->card_count--;
+        for(int i = 0, curr_index = 0; i <= deck->card_count; i++) {
+            if(i != index) {
+                deck->cards[curr_index] = deck->cards[i];
+                curr_index++;
+            } else {
+                result = deck->cards[i];
+            }
+        }
+        if(deck->index >= 0) {
+            deck->index--;
+        }
+    }
+    return result;
+}
+
+void extract_hand_region(Hand* hand, Hand* to, uint8_t start_index) {
+    FURI_LOG_D("CARD", "Extracting hand region");
+    if(start_index >= hand->index) return;
+
+    for(uint8_t i = start_index; i < hand->index; i++) {
+        add_to_hand(to, hand->cards[i]);
+    }
+    hand->index = start_index;
+}
+
+void add_hand_region(Hand* to, Hand* from) {
+    FURI_LOG_D("CARD", "Adding hand region");
+    if((to->index + from->index) <= to->max) {
+        for(int i = 0; i < from->index; i++) {
+            add_to_hand(to, from->cards[i]);
+        }
+    }
+}

+ 192 - 0
solitaire/common/card.h

@@ -0,0 +1,192 @@
+#pragma once
+
+#include <gui/gui.h>
+#include <math.h>
+#include <stdlib.h>
+#include "dml.h"
+
+#define CARD_HEIGHT 23
+#define CARD_HALF_HEIGHT 11
+#define CARD_WIDTH 17
+#define CARD_HALF_WIDTH 8
+
+//region types
+typedef struct {
+    uint8_t pip; //Pip index 0:spades, 1:hearths, 2:diamonds, 3:clubs
+    uint8_t character; //Card letter [0-12], 0 means 2, 12 is Ace
+    bool disabled;
+    bool flipped;
+} Card;
+
+typedef struct {
+    uint8_t deck_count; //Number of decks used
+    Card* cards; //Cards in the deck
+    int card_count;
+    int index; //Card index (to know where we at in the deck)
+} Deck;
+
+typedef struct {
+    Card* cards; //Cards in the deck
+    uint8_t index; //Current index
+    uint8_t max; //How many cards we want to store
+} Hand;
+//endregion
+
+void set_card_graphics(const Icon* graphics);
+
+/**
+ * Gets card coordinates at the index (range: 0-20).
+ *
+ * @param index Index to check 0-20
+ * @return      Position of the card
+ */
+Vector card_pos_at_index(uint8_t index);
+
+/**
+ * Draws card at a given coordinate (top-left corner)
+ *
+ * @param pos_x         X position
+ * @param pos_y         Y position
+ * @param pip           Pip index 0:spades, 1:hearths, 2:diamonds, 3:clubs
+ * @param character     Letter [0-12] 0 is 2, 12 is A
+ * @param canvas        Pointer to Flipper's canvas object
+ */
+void draw_card_at(int8_t pos_x, int8_t pos_y, uint8_t pip, uint8_t character, Canvas* const canvas);
+
+/**
+ * Draws card at a given coordinate (top-left corner)
+ *
+ * @param pos_x         X position
+ * @param pos_y         Y position
+ * @param pip           Pip index 0:spades, 1:hearths, 2:diamonds, 3:clubs
+ * @param character     Letter [0-12] 0 is 2, 12 is A
+ * @param inverted      Invert colors
+ * @param canvas        Pointer to Flipper's canvas object
+ */
+void draw_card_at_colored(
+    int8_t pos_x,
+    int8_t pos_y,
+    uint8_t pip,
+    uint8_t character,
+    bool inverted,
+    Canvas* const canvas);
+
+/**
+ * Draws 'count' cards at the bottom right corner
+ *
+ * @param cards     List of cards
+ * @param count     Count of cards
+ * @param canvas    Pointer to Flipper's canvas object
+ */
+void draw_deck(const Card* cards, uint8_t count, Canvas* const canvas);
+
+/**
+ * Draws card back at a given coordinate (top-left corner)
+ *
+ * @param pos_x     X coordinate
+ * @param pos_y     Y coordinate
+ * @param canvas    Pointer to Flipper's canvas object
+ */
+void draw_card_back_at(int8_t pos_x, int8_t pos_y, Canvas* const canvas);
+
+/**
+ * Generates the deck
+ *
+ * @param deck_ptr      Pointer to the deck
+ * @param deck_count    Number of decks
+ */
+void generate_deck(Deck* deck_ptr, uint8_t deck_count);
+
+/**
+ * Shuffles the deck
+ *
+ * @param deck_ptr Pointer to the deck
+ */
+void shuffle_deck(Deck* deck_ptr);
+
+/**
+ * Calculates the hand count for blackjack
+ *
+ * @param cards     List of cards
+ * @param count     Count of cards
+ * @return          Hand value
+ */
+uint8_t hand_count(const Card* cards, uint8_t count);
+
+/**
+ * Draws card animation
+ *
+ * @param animatingCard Card to animate
+ * @param from          Starting position
+ * @param control       Quadratic lerp control point
+ * @param to            End point
+ * @param t             Current time (0-1)
+ * @param extra_margin  Use extra margin at the end (arrives 0.2 unit before the end so it can stay there a bit)
+ * @param canvas        Pointer to Flipper's canvas object
+ */
+void draw_card_animation(
+    Card animatingCard,
+    Vector from,
+    Vector control,
+    Vector to,
+    float t,
+    bool extra_margin,
+    Canvas* const canvas);
+
+/**
+ * Init hand pointer
+ * @param hand_ptr   Pointer to hand
+ * @param count      Number of cards we want to store
+ */
+void init_hand(Hand* hand_ptr, uint8_t count);
+
+/**
+ * Free hand resources
+ * @param hand_ptr  Pointer to hand
+ */
+void free_hand(Hand* hand_ptr);
+
+/**
+ * Add card to hand
+ * @param hand_ptr  Pointer to hand
+ * @param card      Card to add
+ */
+void add_to_hand(Hand* hand_ptr, Card card);
+
+/**
+ * Draw card placement position at coordinate
+ * @param pos_x     X coordinate
+ * @param pos_y     Y coordinate
+ * @param highlighted   Apply highlight effect
+ * @param canvas    Canvas object
+ */
+void draw_card_space(int16_t pos_x, int16_t pos_y, bool highlighted, Canvas* const canvas);
+
+/**
+ * Draws a column of card, displaying the last [max_cards] cards on the list
+ * @param hand              Hand object
+ * @param pos_x             X coordinate to draw
+ * @param pos_y             Y coordinate to draw
+ * @param highlight         Index to highlight, negative means no highlight
+ * @param canvas            Canvas object
+ */
+void draw_hand_column(
+    Hand hand,
+    int16_t pos_x,
+    int16_t pos_y,
+    int8_t highlight,
+    Canvas* const canvas);
+
+/**
+ * Removes a card from the deck (Be aware, if you remove the first item, the deck index will be at -1 so you have to handle that)
+ * @param index Index to remove
+ * @param deck  Deck reference
+ * @return      The removed card
+ */
+Card remove_from_deck(uint16_t index, Deck* deck);
+
+int first_non_flipped_card(Hand hand);
+
+void extract_hand_region(Hand* hand, Hand* to, uint8_t start_index);
+
+void add_hand_region(Hand* to, Hand* from);

+ 53 - 0
solitaire/common/dml.c

@@ -0,0 +1,53 @@
+#include "dml.h"
+#include <math.h>
+
+float lerp(float v0, float v1, float t) {
+    if(t > 1) return v1;
+    return (1 - t) * v0 + t * v1;
+}
+
+Vector lerp_2d(Vector start, Vector end, float t) {
+    return (Vector){
+        lerp(start.x, end.x, t),
+        lerp(start.y, end.y, t),
+    };
+}
+
+Vector quadratic_2d(Vector start, Vector control, Vector end, float t) {
+    return lerp_2d(lerp_2d(start, control, t), lerp_2d(control, end, t), t);
+}
+
+Vector vector_add(Vector a, Vector b) {
+    return (Vector){a.x + b.x, a.y + b.y};
+}
+
+Vector vector_sub(Vector a, Vector b) {
+    return (Vector){a.x - b.x, a.y - b.y};
+}
+
+Vector vector_mul_components(Vector a, Vector b) {
+    return (Vector){a.x * b.x, a.y * b.y};
+}
+
+Vector vector_div_components(Vector a, Vector b) {
+    return (Vector){a.x / b.x, a.y / b.y};
+}
+
+Vector vector_normalized(Vector a) {
+    float length = vector_magnitude(a);
+    return (Vector){a.x / length, a.y / length};
+}
+
+float vector_magnitude(Vector a) {
+    return sqrt(a.x * a.x + a.y * a.y);
+}
+
+float vector_distance(Vector a, Vector b) {
+    return vector_magnitude(vector_sub(a, b));
+}
+
+float vector_dot(Vector a, Vector b) {
+    Vector _a = vector_normalized(a);
+    Vector _b = vector_normalized(b);
+    return _a.x * _b.x + _a.y * _b.y;
+}

+ 116 - 0
solitaire/common/dml.h

@@ -0,0 +1,116 @@
+//
+// Doofy's Math library
+//
+
+#pragma once
+
+typedef struct {
+    float x;
+    float y;
+} Vector;
+
+#define min(a, b) ((a) < (b) ? (a) : (b))
+#define max(a, b) ((a) > (b) ? (a) : (b))
+#define abs(x) ((x) > 0 ? (x) : -(x))
+
+/**
+ * Lerp function
+ *
+ * @param v0    Start value
+ * @param v1    End value
+ * @param t     Time (0-1 range)
+ * @return      Point between v0-v1 at a given time
+ */
+float lerp(float v0, float v1, float t);
+
+/**
+ * 2D lerp function
+ *
+ * @param start Start vector
+ * @param end   End vector
+ * @param t     Time (0-1 range)
+ * @return      2d Vector between start and end at time
+ */
+Vector lerp_2d(Vector start, Vector end, float t);
+
+/**
+ * Quadratic lerp function
+ *
+ * @param start     Start vector
+ * @param control   Control point
+ * @param end       End vector
+ * @param t         Time (0-1 range)
+ * @return          2d Vector at time
+ */
+Vector quadratic_2d(Vector start, Vector control, Vector end, float t);
+
+/**
+ * Add vector components together
+ *
+ * @param a     First vector
+ * @param b     Second vector
+ * @return      Resulting vector
+ */
+Vector vector_add(Vector a, Vector b);
+
+/**
+ * Subtract vector components together
+ *
+ * @param a First vector
+ * @param b Second vector
+ * @return  Resulting vector
+ */
+Vector vector_sub(Vector a, Vector b);
+
+/**
+ * Multiplying vector components together
+ *
+ * @param a First vector
+ * @param b Second vector
+ * @return  Resulting vector
+ */
+Vector vector_mul_components(Vector a, Vector b);
+
+/**
+ * Dividing vector components
+ *
+ * @param a First vector
+ * @param b Second vector
+ * @return  Resulting vector
+ */
+Vector vector_div_components(Vector a, Vector b);
+
+/**
+ * Calculating Vector length
+ *
+ * @param a Direction vector
+ * @return  Length of the vector
+ */
+float vector_magnitude(Vector a);
+
+/**
+ * Get a normalized vector (length of 1)
+ *
+ * @param a Direction vector
+ * @return  Normalized vector
+ */
+Vector vector_normalized(Vector a);
+
+/**
+ * Calculate two vector's distance
+ *
+ * @param a First vector
+ * @param b Second vector
+ * @return  Distance between vectors
+ */
+float vector_distance(Vector a, Vector b);
+
+/**
+ * Calculate the dot product of the vectors.
+ * No need to normalize, it will do it
+ *
+ * @param a First vector
+ * @param b Second vector
+ * @return  value from -1 to 1
+ */
+float vector_dot(Vector a, Vector b);

+ 103 - 0
solitaire/common/menu.c

@@ -0,0 +1,103 @@
+#include "menu.h"
+
+void add_menu(Menu* menu, const char* name, void (*callback)(void*)) {
+    MenuItem* items = menu->items;
+
+    menu->items = malloc(sizeof(MenuItem) * (menu->menu_count + 1));
+    for(uint8_t i = 0; i < menu->menu_count; i++) {
+        menu->items[i] = items[i];
+    }
+    free(items);
+
+    menu->items[menu->menu_count] = (MenuItem){name, true, callback};
+    menu->menu_count++;
+}
+
+void free_menu(Menu* menu) {
+    free(menu->items);
+    free(menu);
+}
+
+void set_menu_state(Menu* menu, uint8_t index, bool state) {
+    if(menu->menu_count > index) {
+        menu->items[index].enabled = state;
+    }
+    if(!state && menu->current_menu == index) move_menu(menu, 1);
+}
+
+void move_menu(Menu* menu, int8_t direction) {
+    if(!menu->enabled) return;
+    int max = menu->menu_count;
+    for(int8_t i = 0; i < max; i++) {
+        FURI_LOG_D(
+            "MENU",
+            "Iteration %i, current %i, direction %i, state %i",
+            i,
+            menu->current_menu,
+            direction,
+            menu->items[menu->current_menu].enabled ? 1 : 0);
+        if(direction < 0 && menu->current_menu == 0) {
+            menu->current_menu = menu->menu_count - 1;
+        } else {
+            menu->current_menu = (menu->current_menu + direction) % menu->menu_count;
+        }
+        FURI_LOG_D(
+            "MENU",
+            "After process current %i, direction %i, state %i",
+            menu->current_menu,
+            direction,
+            menu->items[menu->current_menu].enabled ? 1 : 0);
+        if(menu->items[menu->current_menu].enabled) {
+            FURI_LOG_D("MENU", "Next menu %i", menu->current_menu);
+            return;
+        }
+    }
+    FURI_LOG_D("MENU", "Not found, setting false");
+    menu->enabled = false;
+}
+
+void activate_menu(Menu* menu, void* state) {
+    if(!menu->enabled) return;
+    menu->items[menu->current_menu].callback(state);
+}
+
+void render_menu(Menu* menu, Canvas* canvas, uint8_t pos_x, uint8_t pos_y) {
+    if(!menu->enabled) return;
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_rbox(canvas, pos_x, pos_y, menu->menu_width + 2, 10, 2);
+
+    uint8_t w = pos_x + menu->menu_width;
+    uint8_t h = pos_y + 10;
+    uint8_t p1x = pos_x + 2;
+    uint8_t p2x = pos_x + menu->menu_width - 2;
+    uint8_t p1y = pos_y + 2;
+    uint8_t p2y = pos_y + 8;
+
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_line(canvas, p1x, pos_y, p2x, pos_y);
+    canvas_draw_line(canvas, p1x, h, p2x, h);
+    canvas_draw_line(canvas, pos_x, p1y, pos_x, p2y);
+    canvas_draw_line(canvas, w, p1y, w, p2y);
+    canvas_draw_dot(canvas, pos_x + 1, pos_y + 1);
+    canvas_draw_dot(canvas, w - 1, pos_y + 1);
+    canvas_draw_dot(canvas, w - 1, h - 1);
+    canvas_draw_dot(canvas, pos_x + 1, h - 1);
+
+    //    canvas_draw_rbox(canvas, pos_x, pos_y, menu->menu_width + 2, 10, 2);
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(
+        canvas,
+        pos_x + menu->menu_width / 2,
+        pos_y + 6,
+        AlignCenter,
+        AlignCenter,
+        menu->items[menu->current_menu].name);
+    //9*5
+    int center = pos_x + menu->menu_width / 2;
+    for(uint8_t i = 0; i < 4; i++) {
+        for(int8_t j = -i; j <= i; j++) {
+            canvas_draw_dot(canvas, center + j, pos_y - 4 + i);
+            canvas_draw_dot(canvas, center + j, pos_y + 14 - i);
+        }
+    }
+}

+ 77 - 0
solitaire/common/menu.h

@@ -0,0 +1,77 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/gui.h>
+
+typedef struct {
+    const char* name; //Name of the menu
+    bool enabled; //Is the menu item enabled (it will not render, you cannot select it)
+
+    void (*callback)(
+        void* state); //Callback for when the activate_menu is called while this menu is selected
+} MenuItem;
+
+typedef struct {
+    MenuItem* items; //list of menu items
+    uint8_t menu_count; //count of menu items (do not change)
+    uint8_t current_menu; //currently selected menu item
+    uint8_t menu_width; //width of the menu
+    bool enabled; //is the menu enabled (it will not render and accept events when disabled)
+} Menu;
+
+/**
+ * Cleans up the pointers used by the menu
+ *
+ * @param menu Pointer of the menu to clean up
+ */
+void free_menu(Menu* menu);
+
+/**
+ * Add a new menu item
+ *
+ * @param menu      Pointer of the menu
+ * @param name      Name of the menu item
+ * @param callback  Callback called on activation
+ */
+void add_menu(Menu* menu, const char* name, void (*callback)(void*));
+
+/**
+ * Setting menu item to be enabled/disabled
+ *
+ * @param menu  Pointer of the menu
+ * @param index Menu index to set
+ * @param state Enabled (true), Disabled(false)
+ */
+void set_menu_state(Menu* menu, uint8_t index, bool state);
+
+/**
+ * Moves selection up or down
+ *
+ * @param menu      Pointer of the menu
+ * @param direction Direction to move -1 down, 1 up
+ */
+void move_menu(Menu* menu, int8_t direction);
+
+/**
+ * Triggers the current menu callback
+ *
+ * @param menu  Pointer of the menu
+ * @param state Usually your application state
+ */
+void activate_menu(Menu* menu, void* state);
+
+/**
+ * Renders the menu at a coordinate (call it in your render function).
+ *
+ * Keep in mind that Flipper has a 128x64 pixel screen resolution and the coordinate
+ * you give is the menu's rectangle top-left corner (arrows not included).
+ * The rectangle height is 10 px, the arrows have a 4 pixel height. Space needed is 18px.
+ * The width of the menu can be configured in the menu object.
+ *
+ *
+ * @param menu      Pointer of the menu
+ * @param canvas    Flippers Canvas pointer
+ * @param pos_x     X position to draw
+ * @param pos_y     Y position to draw
+ */
+void render_menu(Menu* menu, Canvas* canvas, uint8_t pos_x, uint8_t pos_y);

+ 69 - 0
solitaire/common/queue.c

@@ -0,0 +1,69 @@
+#include "queue.h"
+
+void render_queue(const QueueState* queue_state, const void* app_state, Canvas* const canvas) {
+    if(queue_state->current != NULL && queue_state->current->render != NULL)
+        ((QueueItem*)queue_state->current)->render(app_state, canvas);
+}
+
+bool run_queue(QueueState* queue_state, void* app_state) {
+    if(queue_state->current != NULL) {
+        queue_state->running = true;
+        if((furi_get_tick() - queue_state->start) >= queue_state->current->duration)
+            dequeue(queue_state, app_state);
+
+        return true;
+    }
+    return false;
+}
+
+void dequeue(QueueState* queue_state, void* app_state) {
+    ((QueueItem*)queue_state->current)->callback(app_state);
+    QueueItem* f = queue_state->current;
+    queue_state->current = f->next;
+    free(f);
+    if(queue_state->current != NULL) {
+        if(queue_state->current->start != NULL) queue_state->current->start(app_state);
+        queue_state->start = furi_get_tick();
+    } else {
+        queue_state->running = false;
+    }
+}
+
+void queue_clear(QueueState* queue_state) {
+    queue_state->running = false;
+    QueueItem* curr = queue_state->current;
+    while(curr != NULL) {
+        QueueItem* f = curr;
+        curr = curr->next;
+        free(f);
+    }
+}
+
+void enqueue(
+    QueueState* queue_state,
+    void* app_state,
+    void (*done)(void* state),
+    void (*start)(void* state),
+    void (*render)(const void* state, Canvas* const canvas),
+    uint32_t duration) {
+    QueueItem* next;
+    if(queue_state->current == NULL) {
+        queue_state->start = furi_get_tick();
+        queue_state->current = malloc(sizeof(QueueItem));
+        next = queue_state->current;
+        if(next->start != NULL) next->start(app_state);
+
+    } else {
+        next = queue_state->current;
+        while(next->next != NULL) {
+            next = (QueueItem*)(next->next);
+        }
+        next->next = malloc(sizeof(QueueItem));
+        next = next->next;
+    }
+    next->callback = done;
+    next->render = render;
+    next->start = start;
+    next->duration = duration;
+    next->next = NULL;
+}

+ 70 - 0
solitaire/common/queue.h

@@ -0,0 +1,70 @@
+#pragma once
+
+#include <gui/gui.h>
+#include <furi.h>
+
+typedef struct {
+    void (*callback)(void* state); //Callback for when the item is dequeued
+    void (*render)(
+        const void* state,
+        Canvas* const canvas); //Callback for the rendering loop while this item is running
+    void (*start)(void* state); //Callback when this item is started running
+    void* next; //Pointer to the next item
+    uint32_t duration; //duration of the item
+} QueueItem;
+
+typedef struct {
+    unsigned int start; //current queue item start time
+    QueueItem* current; //current queue item
+    bool running; //is the queue running
+} QueueState;
+
+/**
+ * Enqueue a new item.
+ *
+ * @param queue_state   The queue state pointer
+ * @param app_state     Your app state
+ * @param done          Callback for dequeue event
+ * @param start         Callback for when the item is activated
+ * @param render        Callback to render loop if needed
+ * @param duration      Length of the item
+ */
+void enqueue(
+    QueueState* queue_state,
+    void* app_state,
+    void (*done)(void* state),
+    void (*start)(void* state),
+    void (*render)(const void* state, Canvas* const canvas),
+    uint32_t duration);
+/**
+ * Clears all queue items
+ *
+ * @param queue_state   The queue state pointer
+ */
+void queue_clear(QueueState* queue_state);
+
+/**
+ * Dequeues the active queue item. Usually you don't need to call it directly.
+ *
+ * @param queue_state   The queue state pointer
+ * @param app_state     Your application state
+ */
+void dequeue(QueueState* queue_state, void* app_state);
+
+/**
+ * Runs the queue logic (place it in your tick function)
+ *
+ * @param queue_state   The queue state pointer
+ * @param app_state     Your application state
+ * @return              FALSE when there is nothing to run, TRUE otherwise
+ */
+bool run_queue(QueueState* queue_state, void* app_state);
+
+/**
+ * Calls the currently active queue items render callback (if there is any)
+ *
+ * @param queue_state   The queue state pointer
+ * @param app_state     Your application state
+ * @param canvas        Pointer to Flipper's canvas object
+ */
+void render_queue(const QueueState* queue_state, const void* app_state, Canvas* const canvas);

+ 257 - 0
solitaire/common/ui.c

@@ -0,0 +1,257 @@
+#include "ui.h"
+#include <gui/canvas_i.h>
+#include <u8g2_glue.h>
+#include <gui/icon_animation_i.h>
+#include <gui/icon.h>
+#include <gui/icon_i.h>
+#include <furi_hal.h>
+
+TileMap* tileMap;
+uint8_t tileMapCount = 0;
+
+void ui_cleanup() {
+    if(tileMap != NULL) {
+        for(uint8_t i = 0; i < tileMapCount; i++) {
+            if(tileMap[i].data != NULL) free(tileMap[i].data);
+        }
+        free(tileMap);
+    }
+}
+
+void add_new_tilemap(uint8_t* data, unsigned long iconId) {
+    TileMap* old = tileMap;
+    tileMapCount++;
+    tileMap = malloc(sizeof(TileMap) * tileMapCount);
+    if(tileMapCount > 1) {
+        for(uint8_t i = 0; i < tileMapCount; i++) tileMap[i] = old[i];
+    }
+    tileMap[tileMapCount - 1] = (TileMap){data, iconId};
+}
+
+uint8_t* get_tilemap(unsigned long icon_id) {
+    for(uint8_t i = 0; i < tileMapCount; i++) {
+        if(tileMap[i].iconId == icon_id) return tileMap[i].data;
+    }
+
+    return NULL;
+}
+
+uint32_t pixel_index(uint8_t x, uint8_t y) {
+    return y * SCREEN_WIDTH + x;
+}
+
+bool in_screen(int16_t x, int16_t y) {
+    return x >= 0 && x < SCREEN_WIDTH && y >= 0 && y < SCREEN_HEIGHT;
+}
+
+unsigned flipBit(uint8_t x, uint8_t bit) {
+    return x ^ (1 << bit);
+}
+
+unsigned setBit(uint8_t x, uint8_t bit) {
+    return x | (1 << bit);
+}
+
+unsigned unsetBit(uint8_t x, uint8_t bit) {
+    return x & ~(1 << bit);
+}
+
+bool test_pixel(uint8_t* data, uint8_t x, uint8_t y, uint8_t w) {
+    uint8_t current_bit = (y % 8);
+    uint8_t current_row = ((y - current_bit) / 8);
+    uint8_t current_value = data[current_row * w + x];
+    return current_value & (1 << current_bit);
+}
+
+uint8_t* get_buffer(Canvas* const canvas) {
+    return canvas->fb.tile_buf_ptr;
+    //  return canvas_get_buffer(canvas);
+}
+uint8_t* make_buffer() {
+    return malloc(sizeof(uint8_t) * 8 * 128);
+}
+void clone_buffer(uint8_t* canvas, uint8_t* data) {
+    for(int i = 0; i < 1024; i++) {
+        data[i] = canvas[i];
+    }
+}
+
+bool read_pixel(Canvas* const canvas, int16_t x, int16_t y) {
+    if(in_screen(x, y)) {
+        return test_pixel(get_buffer(canvas), x, y, SCREEN_WIDTH);
+    }
+    return false;
+}
+
+void set_pixel(Canvas* const canvas, int16_t x, int16_t y, DrawMode draw_mode) {
+    if(in_screen(x, y)) {
+        uint8_t current_bit = (y % 8);
+        uint8_t current_row = ((y - current_bit) / 8);
+        uint32_t i = pixel_index(x, current_row);
+        uint8_t* buffer = get_buffer(canvas);
+
+        uint8_t current_value = buffer[i];
+        if(draw_mode == Inverse) {
+            buffer[i] = flipBit(current_value, current_bit);
+        } else {
+            if(draw_mode == White) {
+                buffer[i] = unsetBit(current_value, current_bit);
+            } else {
+                buffer[i] = setBit(current_value, current_bit);
+            }
+        }
+    }
+}
+
+void draw_line(
+    Canvas* const canvas,
+    int16_t x1,
+    int16_t y1,
+    int16_t x2,
+    int16_t y2,
+    DrawMode draw_mode) {
+    for(int16_t x = x2; x >= x1; x--) {
+        for(int16_t y = y2; y >= y1; y--) {
+            set_pixel(canvas, x, y, draw_mode);
+        }
+    }
+}
+
+void draw_rounded_box_frame(
+    Canvas* const canvas,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode draw_mode) {
+    int16_t xMinCorner = x + 1;
+    int16_t xMax = x + w - 1;
+    int16_t xMaxCorner = x + w - 2;
+    int16_t yMinCorner = y + 1;
+    int16_t yMax = y + h - 1;
+    int16_t yMaxCorner = y + h - 2;
+    draw_line(canvas, xMinCorner, y, xMaxCorner, y, draw_mode);
+    draw_line(canvas, xMinCorner, yMax, xMaxCorner, yMax, draw_mode);
+    draw_line(canvas, x, yMinCorner, x, yMaxCorner, draw_mode);
+    draw_line(canvas, xMax, yMinCorner, xMax, yMaxCorner, draw_mode);
+}
+
+void draw_rounded_box(
+    Canvas* const canvas,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode draw_mode) {
+    for(int16_t o = w - 2; o >= 1; o--) {
+        for(int16_t p = h - 2; p >= 1; p--) {
+            set_pixel(canvas, x + o, y + p, draw_mode);
+        }
+    }
+    draw_rounded_box_frame(canvas, x, y, w, h, draw_mode);
+}
+
+void invert_shape(Canvas* const canvas, uint8_t* data, int16_t x, int16_t y, uint8_t w, uint8_t h) {
+    draw_pixels(canvas, data, x, y, w, h, Inverse);
+}
+
+void draw_pixels(
+    Canvas* const canvas,
+    uint8_t* data,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode) {
+    for(int8_t o = 0; o < w; o++) {
+        for(int8_t p = 0; p < h; p++) {
+            if(in_screen(o + x, p + y) && data[p * w + o] == 1)
+                set_pixel(canvas, o + x, p + y, drawMode);
+        }
+    }
+}
+
+void draw_rectangle(
+    Canvas* const canvas,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode) {
+    for(int8_t o = 0; o < w; o++) {
+        for(int8_t p = 0; p < h; p++) {
+            if(in_screen(o + x, p + y)) {
+                set_pixel(canvas, o + x, p + y, drawMode);
+            }
+        }
+    }
+}
+
+void invert_rectangle(Canvas* const canvas, int16_t x, int16_t y, uint8_t w, uint8_t h) {
+    draw_rectangle(canvas, x, y, w, h, Inverse);
+}
+
+uint8_t* image_data(Canvas* const canvas, const Icon* icon) {
+    uint8_t* data = malloc(sizeof(uint8_t) * 8 * 128);
+    uint8_t* screen = canvas->fb.tile_buf_ptr;
+    canvas->fb.tile_buf_ptr = data;
+    canvas_draw_icon(canvas, 0, 0, icon);
+    canvas->fb.tile_buf_ptr = screen;
+    return data;
+}
+
+uint8_t* getOrAddIconData(Canvas* const canvas, const Icon* icon) {
+    uint8_t* icon_data = get_tilemap((unsigned long)icon);
+    if(icon_data == NULL) {
+        icon_data = image_data(canvas, icon);
+        add_new_tilemap(icon_data, (unsigned long)icon);
+    }
+    return icon_data;
+}
+
+void draw_icon_clip(
+    Canvas* const canvas,
+    const Icon* icon,
+    int16_t x,
+    int16_t y,
+    uint8_t left,
+    uint8_t top,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode) {
+    uint8_t* icon_data = getOrAddIconData(canvas, icon);
+
+    for(int i = 0; i < w; i++) {
+        for(int j = 0; j < h; j++) {
+            bool on = test_pixel(icon_data, left + i, top + j, SCREEN_WIDTH);
+            if(drawMode == Filled) {
+                set_pixel(canvas, x + i, y + j, on ? Black : White);
+            } else if(on)
+                set_pixel(canvas, x + i, y + j, drawMode);
+        }
+    }
+}
+
+void draw_icon_clip_flipped(
+    Canvas* const canvas,
+    const Icon* icon,
+    int16_t x,
+    int16_t y,
+    uint8_t left,
+    uint8_t top,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode) {
+    uint8_t* icon_data = getOrAddIconData(canvas, icon);
+
+    for(int i = 0; i < w; i++) {
+        for(int j = 0; j < h; j++) {
+            bool on = test_pixel(icon_data, left + i, top + j, SCREEN_WIDTH);
+
+            if(drawMode == Filled) {
+                set_pixel(canvas, x + w - i - 1, y + h - j - 1, on ? Black : White);
+            } else if(on)
+                set_pixel(canvas, x + w - i - 1, y + h - j - 1, drawMode);
+        }
+    }
+}

+ 105 - 0
solitaire/common/ui.h

@@ -0,0 +1,105 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/canvas.h>
+
+#define SCREEN_WIDTH 128
+#define SCREEN_HEIGHT 64
+
+typedef enum {
+    Black,
+    White,
+    Inverse,
+    Filled //Currently only for Icon clip drawing
+} DrawMode;
+
+// size is the screen size
+
+typedef struct {
+    uint8_t* data;
+    unsigned long iconId;
+} TileMap;
+
+bool test_pixel(uint8_t* data, uint8_t x, uint8_t y, uint8_t w);
+
+uint8_t* image_data(Canvas* const canvas, const Icon* icon);
+
+uint32_t pixel_index(uint8_t x, uint8_t y);
+
+void draw_icon_clip(
+    Canvas* const canvas,
+    const Icon* icon,
+    int16_t x,
+    int16_t y,
+    uint8_t left,
+    uint8_t top,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode);
+
+void draw_icon_clip_flipped(
+    Canvas* const canvas,
+    const Icon* icon,
+    int16_t x,
+    int16_t y,
+    uint8_t left,
+    uint8_t top,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode);
+
+void draw_rounded_box(
+    Canvas* const canvas,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode);
+
+void draw_rounded_box_frame(
+    Canvas* const canvas,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode);
+
+void draw_rectangle(
+    Canvas* const canvas,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode);
+
+void invert_rectangle(Canvas* const canvas, int16_t x, int16_t y, uint8_t w, uint8_t h);
+
+void invert_shape(Canvas* const canvas, uint8_t* data, int16_t x, int16_t y, uint8_t w, uint8_t h);
+
+void draw_pixels(
+    Canvas* const canvas,
+    uint8_t* data,
+    int16_t x,
+    int16_t y,
+    uint8_t w,
+    uint8_t h,
+    DrawMode drawMode);
+
+bool read_pixel(Canvas* const canvas, int16_t x, int16_t y);
+
+void set_pixel(Canvas* const canvas, int16_t x, int16_t y, DrawMode draw_mode);
+
+void draw_line(
+    Canvas* const canvas,
+    int16_t x1,
+    int16_t y1,
+    int16_t x2,
+    int16_t y2,
+    DrawMode draw_mode);
+
+bool in_screen(int16_t x, int16_t y);
+
+void ui_cleanup();
+uint8_t* get_buffer(Canvas* const canvas);
+uint8_t* make_buffer();
+void clone_buffer(uint8_t* canvas, uint8_t* data);

+ 63 - 0
solitaire/defines.h

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

+ 562 - 0
solitaire/solitaire.c

@@ -0,0 +1,562 @@
+#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 <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);
+        }
+
+        int8_t pos[2] = {columns[game_state->selectColumn][0],
+                         columns[game_state->selectColumn][game_state->selectRow + 1]};
+
+        /*     draw_icon_clip(canvas, &I_card_graphics, pos[0] + CARD_HALF_WIDTH, pos[1] + CARD_HALF_HEIGHT, 30, 5, 5, 5,
+                            Filled);*/
+
+        if (game_state->dragging_hand.index > 0) {
+            draw_hand_column(game_state->dragging_hand,
+                             pos[0] + CARD_HALF_WIDTH + 3, pos[1] + CARD_HALF_HEIGHT + 3,
+                             -1, canvas);
+        }
+
+        clone_buffer(get_buffer(canvas), game_state->animation.buffer);
+    }else{
+        clone_buffer(game_state->animation.buffer, get_buffer(canvas));
+    }
+}
+
+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);
+
+    }
+
+    clone_buffer(get_buffer(canvas), game_state->animation.buffer);
+}
+
+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;
+    }
+
+    switch (game_state->state) {
+        case GameStateAnimate:
+            draw_animation(canvas, game_state);
+            break;
+        case GameStateStart:
+            canvas_draw_icon(canvas, 0, 0, &I_solitaire_main);
+            break;
+        case GameStatePlay:
+            draw_scene(canvas, game_state);
+            break;
+        default:
+            break;
+    }
+
+    furi_mutex_release(game_state->mutex);
+
+}
+
+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;
+}
+
+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;
+}
+
+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;
+                        }
+                    }
+                }
+            }
+
+
+            if (!wasAction) {
+                notification_message(notification, &sequence_fail);
+            }
+        }
+    }
+    if (game_state->state == GameStateAnimate) {
+        if (game_state->animation.started && !game_state->longPress && game_state->input==InputKeyOk) {
+            init(game_state);
+            game_state->state = GameStateStart;
+        }
+
+        game_state->animation.started = true;
+        if (game_state->animation.x < -20 || game_state->animation.x > 128) {
+            game_state->animation.deck++;
+            if (game_state->animation.deck > 3)
+                game_state->animation.deck = 0;
+            int8_t cardIndex = 11 - game_state->animation.indexes[game_state->animation.deck];
+
+            if (game_state->animation.indexes[0] == 13 &&
+                game_state->animation.indexes[1] == 13 &&
+                game_state->animation.indexes[2] == 13 &&
+                game_state->animation.indexes[3] == 13) {
+                init(game_state);
+                game_state->state = GameStateStart;
+                return;
+            }
+
+            if (cardIndex == -1)
+                cardIndex = 12;
+            game_state->animation.card = (Card) {
+                    game_state->top_cards[game_state->animation.deck].pip,
+                    cardIndex,
+                    false, false
+            };
+            game_state->animation.indexes[game_state->animation.deck]++;
+            game_state->animation.vx = -(rand() % 3 + 1) * (rand() % 2 == 1 ? 1 : -1);
+            game_state->animation.vy = (rand() % 3 + 1);
+            game_state->animation.x = columns[game_state->animation.deck + 3][0];
+            game_state->animation.y = columns[game_state->animation.deck + 3][1];
+        }
+        game_state->animation.x += game_state->animation.vx;
+        game_state->animation.y -= game_state->animation.vy;
+        game_state->animation.vy -= 1;
+        if (game_state->animation.vy < -10)game_state->animation.vy = -10;
+
+        if (game_state->animation.y > 41) {
+            game_state->animation.y = 41;
+            game_state->animation.vy = -(game_state->animation.vy * 0.7f);
+        }
+    }
+}
+
+void init(GameState *game_state) {
+    dolphin_deed(DolphinDeedPluginGameStart);
+    game_state->selectColumn = 0;
+    game_state->selected_card = 0;
+    game_state->selectRow = 0;
+    generate_deck(&(game_state->deck), 1);
+    shuffle_deck(&(game_state->deck));
+    game_state->dragging_deck = false;
+    game_state->animation.started = false;
+    game_state->animation.deck = -1;
+    game_state->animation.x = -21;
+    game_state->state = GameStatePlay;
+    game_state->dragging_column = 8;
+
+    for (uint8_t i = 0; i < 7; i++) {
+        free_hand(&(game_state->bottom_columns[i]));
+        init_hand(&(game_state->bottom_columns[i]), 21);
+        game_state->bottom_columns[i].index = 0;
+        for (uint8_t j = 0; j <= i; j++) {
+            Card cur = remove_from_deck(0, &(game_state->deck));
+            cur.flipped = i != j;
+            add_to_hand(&(game_state->bottom_columns[i]), cur);
+        }
+    }
+
+    for (uint8_t i = 0; i < 4; i++) {
+        game_state->animation.indexes[i] = 0;
+        game_state->top_cards[i] = (Card) {0, 0, true, false};
+    }
+    game_state->deck.index = -1;
+}
+
+void init_start(GameState *game_state) {
+    game_state->input = InputKeyMAX;
+    for (uint8_t i = 0; i < 7; i++)
+        init_hand(&(game_state->bottom_columns[i]), 21);
+
+    init_hand(&(game_state->dragging_hand), 13);
+    game_state->animation.buffer = make_buffer();
+
+}
+
+
+static void input_callback(InputEvent *input_event, FuriMessageQueue *event_queue) {
+    furi_assert(event_queue);
+    AppEvent event = {.type = EventTypeKey, .input = *input_event};
+    furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void update_timer_callback(FuriMessageQueue *event_queue) {
+    furi_assert(event_queue);
+    AppEvent event = {.type = EventTypeTick};
+    furi_message_queue_put(event_queue, &event, 0);
+}
+
+int32_t solitaire_app(void *p) {
+    UNUSED(p);
+    int32_t return_code = 0;
+    FuriMessageQueue *event_queue = furi_message_queue_alloc(8, sizeof(AppEvent));
+    GameState *game_state = malloc(sizeof(GameState));
+    init_start(game_state);
+    set_card_graphics(&I_card_graphics);
+
+    game_state->state = GameStateStart;
+
+    game_state->processing = true;
+    game_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    if (!game_state->mutex) {
+        FURI_LOG_E(APP_NAME, "cannot create mutex\r\n");
+        return_code = 255;
+        goto free_and_exit;
+    }
+    NotificationApp *notification = furi_record_open(RECORD_NOTIFICATION);
+
+    notification_message_block(notification, &sequence_display_backlight_enforce_on);
+
+    ViewPort *view_port = view_port_alloc();
+    view_port_draw_callback_set(view_port, render_callback, game_state);
+    view_port_input_callback_set(view_port, input_callback, event_queue);
+
+    FuriTimer *timer =
+            furi_timer_alloc(update_timer_callback, FuriTimerTypePeriodic, event_queue);
+    furi_timer_start(timer, furi_kernel_get_tick_frequency() / 30);
+
+    Gui *gui = furi_record_open("gui");
+    gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+    AppEvent event;
+    for (bool processing = true; processing;) {
+        FuriStatus event_status = furi_message_queue_get(event_queue, &event, 150);
+        furi_mutex_acquire(game_state->mutex, FuriWaitForever);
+        game_state->had_change = false;
+        if (event_status == FuriStatusOk) {
+            if (event.type == EventTypeKey) {
+                game_state->had_change = true;
+                if (event.input.type == InputTypeLong) {
+                    game_state->longPress = true;
+                    switch (event.input.key) {
+                        case InputKeyUp:
+                        case InputKeyDown:
+                        case InputKeyRight:
+                        case InputKeyLeft:
+                        case InputKeyOk:
+                            game_state->input = event.input.key;
+                            break;
+                        case InputKeyBack:
+                            processing = false;
+                            return_code = 1;
+                        default:
+                            break;
+                    }
+                } else if (event.input.type == InputTypePress) {
+                    game_state->longPress = false;
+                    switch (event.input.key) {
+                        case InputKeyUp:
+                        case InputKeyDown:
+                        case InputKeyRight:
+                        case InputKeyLeft:
+                        case InputKeyOk:
+                            if (event.input.key == InputKeyOk && game_state->state == GameStateStart) {
+                                game_state->state = GameStatePlay;
+                                init(game_state);
+                            }
+                            else {
+                                game_state->input = event.input.key;
+                            }
+                            break;
+                        case InputKeyBack:
+                            init(game_state);
+                            processing = false;
+                            return_code = 1;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            } else if (event.type == EventTypeTick) {
+                tick(game_state, notification);
+                processing = game_state->processing;
+                game_state->input = InputKeyMAX;
+            }
+        }
+
+        furi_mutex_release(game_state->mutex);
+        view_port_update(view_port);
+    }
+
+
+    notification_message_block(notification, &sequence_display_backlight_enforce_auto);
+    furi_timer_free(timer);
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_NOTIFICATION);
+    view_port_free(view_port);
+    furi_mutex_free(game_state->mutex);
+
+    free_and_exit:
+    free(game_state->animation.buffer);
+    ui_cleanup();
+    for (uint8_t i = 0; i < 7; i++)
+        free_hand(&(game_state->bottom_columns[i]));
+
+    free(game_state->deck.cards);
+    free(game_state);
+    furi_message_queue_free(event_queue);
+
+    return return_code;
+}

BIN=BIN
solitaire/solitaire_10px.png