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

Add blackjack from https://github.com/doofy-dev/flipper_blackjack

git-subtree-dir: blackjack
git-subtree-mainline: 49e515422ae903cde199460d2fc5a7582d170b56
git-subtree-split: 76a3685f19cab14e88b784c41c04ecf9c260ec75
Willy-JL 2 лет назад
Родитель
Сommit
59e852af06

+ 3 - 0
blackjack/.gitmodules

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

+ 1 - 0
blackjack/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/doofy-dev/flipper_blackjack main

+ 21 - 0
blackjack/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.

+ 25 - 0
blackjack/README.md

@@ -0,0 +1,25 @@
+[![GitHub release](https://img.shields.io/github/release/teeebor/flipper_blackjack?include_prereleases=&sort=semver&color=blue)](https://github.com/teeebor/flipper_blackjack/releases/)
+[![License](https://img.shields.io/badge/License-MIT-blue)](/LICENSE)
+[![issues - flipper-zero_authenticator](https://img.shields.io/github/issues/teeebor/flipper_blackjack)](https://github.com/teeebor/flipper_blackjack/issues)
+![maintained - yes](https://img.shields.io/badge/maintained-yes-blue)
+![contributions - welcome](https://img.shields.io/badge/contributions-welcome-blue)
+# Blackjack for Flipper Zero
+
+
+![Play screen](screenshots/blackjack.gif)
+
+## 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

+ 13 - 0
blackjack/application.fam

@@ -0,0 +1,13 @@
+App(
+    appid="blackjack",
+    name="Blackjack",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="blackjack_app",
+    cdefines=["APP_BLACKJACK"],
+    requires=["gui","storage","canvas"],
+    stack_size=2 * 1024,
+    order=30,
+    fap_icon="blackjack_10px.png",
+    fap_category="Games",
+    fap_icon_assets="assets"
+)

BIN
blackjack/assets/blackjack.png


BIN
blackjack/assets/card_graphics.png


BIN
blackjack/assets/endscreen.png


BIN
blackjack/assets/poker.png


BIN
blackjack/assets/poker2.png


BIN
blackjack/assets/poker_design.png


BIN
blackjack/assets/poker_home.png


+ 571 - 0
blackjack/blackjack.c

@@ -0,0 +1,571 @@
+
+#include <gui/gui.h>
+#include <stdlib.h>
+#include <dolphin/dolphin.h>
+#include <dialogs/dialogs.h>
+#include <gui/canvas_i.h>
+
+#include <math.h>
+#include "util.h"
+#include "defines.h"
+#include "common/card.h"
+#include "common/dml.h"
+#include "common/queue.h"
+#include "util.h"
+#include "ui.h"
+
+#include "blackjack_icons.h"
+
+#define DEALER_MAX 17
+
+void start_round(GameState *game_state);
+
+void init(GameState *game_state);
+
+static void draw_ui(Canvas *const canvas, const GameState *game_state) {
+
+    draw_money(canvas, game_state->player_score);
+
+    draw_score(canvas, true, hand_count(game_state->player_cards, game_state->player_card_count));
+
+    if (!game_state->queue_state.running && game_state->state == GameStatePlay) {
+        render_menu(game_state->menu,canvas, 2, 47);
+    }
+}
+
+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;
+    }
+
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_frame(canvas, 0, 0, 128, 64);
+
+    if (game_state->state == GameStateStart) {
+        canvas_draw_icon(canvas, 0, 0, &I_blackjack);
+    }
+    if (game_state->state == GameStateGameOver) {
+        canvas_draw_icon(canvas, 0, 0, &I_endscreen);
+    }
+
+    if (game_state->state == GameStatePlay || game_state->state == GameStateDealer) {
+        if (game_state->state == GameStatePlay)
+            draw_player_scene(canvas, game_state);
+        else
+            draw_dealer_scene(canvas, game_state);
+        render_queue(&(game_state->queue_state), game_state, canvas);
+        draw_ui(canvas, game_state);
+    } else if (game_state->state == GameStateSettings) {
+        settings_page(canvas, game_state);
+    }
+
+    furi_mutex_release(game_state->mutex);
+}
+
+//region card draw
+Card draw_card(GameState *game_state) {
+    Card c = game_state->deck.cards[game_state->deck.index];
+    game_state->deck.index++;
+    return c;
+}
+
+
+void drawPlayerCard(void *ctx) {
+    GameState *game_state = ctx;
+    Card c = draw_card(game_state);
+    game_state->player_cards[game_state->player_card_count] = c;
+    game_state->player_card_count++;
+    if(game_state->player_score < game_state->settings.round_price || game_state->doubled){
+        set_menu_state(game_state->menu, 0, false);
+    }
+}
+
+void drawDealerCard(void *ctx) {
+    GameState *game_state = ctx;
+    Card c = draw_card(game_state);
+    game_state->dealer_cards[game_state->dealer_card_count] = c;
+    game_state->dealer_card_count++;
+}
+//endregion
+
+//region queue callbacks
+void to_lose_state(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    if (game_state->settings.message_duration == 0)
+        return;
+    popup_frame(canvas);
+    elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "You lost");
+}
+
+void to_bust_state(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    if (game_state->settings.message_duration == 0)
+        return;
+    popup_frame(canvas);
+    elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Busted!");
+}
+
+void to_draw_state(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    if (game_state->settings.message_duration == 0)
+        return;
+    popup_frame(canvas);
+    elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Draw");
+}
+
+void to_dealer_turn(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    if (game_state->settings.message_duration == 0)
+        return;
+    popup_frame(canvas);
+    elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Dealers turn");
+}
+
+void to_win_state(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    if (game_state->settings.message_duration == 0)
+        return;
+    popup_frame(canvas);
+    elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "You win");
+}
+
+void to_start(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    if (game_state->settings.message_duration == 0)
+        return;
+    popup_frame(canvas);
+    elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Round started");
+}
+
+void before_start(void *ctx) {
+    GameState *game_state = ctx;
+    game_state->dealer_card_count = 0;
+    game_state->player_card_count = 0;
+}
+
+
+void start(void *ctx) {
+    GameState *game_state = ctx;
+    start_round(game_state);
+}
+
+void draw(void *ctx) {
+    GameState *game_state = ctx;
+    game_state->player_score += game_state->bet;
+    game_state->bet = 0;
+    enqueue(&(game_state->queue_state), game_state, start, before_start, to_start,
+            game_state->settings.message_duration);
+}
+
+void game_over(void *ctx) {
+    GameState *game_state = ctx;
+    game_state->state = GameStateGameOver;
+}
+
+void lose(void *ctx) {
+    GameState *game_state = ctx;
+    game_state->state = GameStatePlay;
+    game_state->bet = 0;
+    if (game_state->player_score >= game_state->settings.round_price) {
+        enqueue(&(game_state->queue_state), game_state, start, before_start, to_start,
+                game_state->settings.message_duration);
+    } else {
+        enqueue(&(game_state->queue_state), game_state, game_over, NULL, NULL,
+                0);
+    }
+}
+
+void win(void *ctx) {
+    dolphin_deed(DolphinDeedPluginGameWin);
+    GameState *game_state = ctx;
+    game_state->state = GameStatePlay;
+    game_state->player_score += game_state->bet * 2;
+    game_state->bet = 0;
+    enqueue(&(game_state->queue_state), game_state, start, before_start, to_start,
+            game_state->settings.message_duration);
+}
+
+
+void dealerTurn(void *ctx) {
+    GameState *game_state = ctx;
+    game_state->state = GameStateDealer;
+}
+
+float animationTime(const GameState *game_state){
+    return (float) (furi_get_tick() - game_state->queue_state.start) /
+           (float) (game_state->settings.animation_duration);
+}
+
+void dealer_card_animation(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    float t = animationTime(game_state);
+
+    Card animatingCard = game_state->deck.cards[game_state->deck.index];
+    if (game_state->dealer_card_count > 1) {
+        Vector end = card_pos_at_index(game_state->dealer_card_count);
+        draw_card_animation(animatingCard,
+                            (Vector) {0, 64},
+                            (Vector) {0, 32},
+                            end,
+                            t,
+                            true,
+                            canvas);
+    } else {
+        draw_card_animation(animatingCard,
+                            (Vector) {32, -CARD_HEIGHT},
+                            (Vector) {64, 32},
+                            (Vector) {2, 2},
+                            t,
+                            false,
+                            canvas);
+    }
+}
+
+void dealer_back_card_animation(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    float t = animationTime(game_state);
+
+    Vector currentPos = quadratic_2d((Vector) {32, -CARD_HEIGHT}, (Vector) {64, 32}, (Vector) {13, 5}, t);
+    draw_card_back_at(currentPos.x, currentPos.y, canvas);
+}
+
+void player_card_animation(const void *ctx, Canvas *const canvas) {
+    const GameState *game_state = ctx;
+    float t = animationTime(game_state);
+
+    Card animatingCard = game_state->deck.cards[game_state->deck.index];
+    Vector end = card_pos_at_index(game_state->player_card_count);
+
+    draw_card_animation(animatingCard,
+                        (Vector) {32, -CARD_HEIGHT},
+                        (Vector) {0, 32},
+                        end,
+                        t,
+                        true,
+                        canvas);
+}
+//endregion
+
+void player_tick(GameState *game_state) {
+    uint8_t score = hand_count(game_state->player_cards, game_state->player_card_count);
+    if ((game_state->doubled && score <= 21) || score == 21) {
+        enqueue(&(game_state->queue_state), game_state, dealerTurn, NULL, to_dealer_turn,
+                game_state->settings.message_duration);
+    } else if (score > 21) {
+        enqueue(&(game_state->queue_state), game_state, lose, NULL, to_bust_state,
+                game_state->settings.message_duration);
+    } else {
+        if(game_state->selectDirection == DirectionUp || game_state->selectDirection == DirectionDown){
+            move_menu(game_state->menu, game_state->selectDirection == DirectionUp ? -1 : 1);
+        }
+
+        if (game_state->selectDirection == Select){
+            activate_menu(game_state->menu, game_state);
+
+        }
+    }
+}
+
+void dealer_tick(GameState *game_state) {
+    uint8_t dealer_score = hand_count(game_state->dealer_cards, game_state->dealer_card_count);
+    uint8_t player_score = hand_count(game_state->player_cards, game_state->player_card_count);
+
+    if (dealer_score >= DEALER_MAX) {
+        if (dealer_score > 21 || dealer_score < player_score) {
+            enqueue(&(game_state->queue_state), game_state, win, NULL, to_win_state,
+                    game_state->settings.message_duration);
+        } else if (dealer_score > player_score) {
+            enqueue(&(game_state->queue_state), game_state, lose, NULL, to_lose_state,
+                    game_state->settings.message_duration);
+        } else if (dealer_score == player_score) {
+            enqueue(&(game_state->queue_state), game_state, draw, NULL, to_draw_state,
+                    game_state->settings.message_duration);
+        }
+    } else {
+        enqueue(&(game_state->queue_state), game_state, drawDealerCard, NULL, dealer_card_animation,
+                game_state->settings.animation_duration);
+    }
+}
+
+void settings_tick(GameState *game_state) {
+    if (game_state->selectDirection == DirectionDown && game_state->selectedMenu < 4) {
+        game_state->selectedMenu++;
+    }
+    if (game_state->selectDirection == DirectionUp && game_state->selectedMenu > 0) {
+        game_state->selectedMenu--;
+    }
+
+    if (game_state->selectDirection == DirectionLeft || game_state->selectDirection == DirectionRight) {
+        int nextScore = 0;
+        switch (game_state->selectedMenu) {
+            case 0:
+                nextScore = game_state->settings.starting_money;
+                if (game_state->selectDirection == DirectionLeft)
+                    nextScore -= 10;
+                else
+                    nextScore += 10;
+                if (nextScore >= (int) game_state->settings.round_price && nextScore < 400)
+                    game_state->settings.starting_money = nextScore;
+                break;
+            case 1:
+                nextScore = game_state->settings.round_price;
+                if (game_state->selectDirection == DirectionLeft)
+                    nextScore -= 10;
+                else
+                    nextScore += 10;
+                if (nextScore >= 5 && nextScore <= (int) game_state->settings.starting_money)
+                    game_state->settings.round_price = nextScore;
+                break;
+            case 2:
+                nextScore = game_state->settings.animation_duration;
+                if (game_state->selectDirection == DirectionLeft)
+                    nextScore -= 100;
+                else
+                    nextScore += 100;
+                if (nextScore >= 0 && nextScore < 2000)
+                    game_state->settings.animation_duration = nextScore;
+                break;
+            case 3:
+                nextScore = game_state->settings.message_duration;
+                if (game_state->selectDirection == DirectionLeft)
+                    nextScore -= 100;
+                else
+                    nextScore += 100;
+                if (nextScore >= 0 && nextScore < 2000)
+                    game_state->settings.message_duration = nextScore;
+                break;
+            case 4:
+                game_state->settings.sound_effects = !game_state->settings.sound_effects;
+            default:
+                break;
+        }
+    }
+
+}
+
+void tick(GameState *game_state) {
+    game_state->last_tick = furi_get_tick();
+    bool queue_ran = run_queue(&(game_state->queue_state), game_state);
+
+    switch (game_state->state) {
+        case GameStateGameOver:
+        case GameStateStart:
+            if (game_state->selectDirection == Select)
+                init(game_state);
+            else if (game_state->selectDirection == DirectionRight) {
+                game_state->selectedMenu = 0;
+                game_state->state = GameStateSettings;
+            }
+            break;
+        case GameStatePlay:
+            if (!game_state->started) {
+                game_state->selectedMenu = 0;
+                game_state->started = true;
+                enqueue(&(game_state->queue_state), game_state, drawDealerCard, NULL, dealer_back_card_animation,
+                        game_state->settings.animation_duration);
+                enqueue(&(game_state->queue_state), game_state, drawPlayerCard, NULL, player_card_animation,
+                        game_state->settings.animation_duration);
+                enqueue(&(game_state->queue_state), game_state, drawDealerCard, NULL, dealer_card_animation,
+                        game_state->settings.animation_duration);
+                enqueue(&(game_state->queue_state), game_state, drawPlayerCard, NULL, player_card_animation,
+                        game_state->settings.animation_duration);
+            }
+            if (!queue_ran)
+                player_tick(game_state);
+            break;
+        case GameStateDealer:
+            if (!queue_ran)
+                dealer_tick(game_state);
+            break;
+        case GameStateSettings:
+            settings_tick(game_state);
+            break;
+        default:
+            break;
+    }
+
+    game_state->selectDirection = None;
+
+}
+
+void start_round(GameState *game_state) {
+    game_state->menu->current_menu=1;
+    game_state->player_card_count = 0;
+    game_state->dealer_card_count = 0;
+    set_menu_state(game_state->menu, 0, true);
+    game_state->menu->enabled=true;
+    game_state->started = false;
+    game_state->doubled = false;
+    game_state->queue_state.running = true;
+    shuffle_deck(&(game_state->deck));
+    game_state->doubled = false;
+    game_state->bet = game_state->settings.round_price;
+    if (game_state->player_score < game_state->settings.round_price) {
+        game_state->state = GameStateGameOver;
+    } else {
+        game_state->player_score -= game_state->settings.round_price;
+    }
+    game_state->state = GameStatePlay;
+}
+
+void init(GameState *game_state) {
+    set_menu_state(game_state->menu, 0, true);
+    game_state->menu->enabled=true;
+    game_state->menu->current_menu=1;
+    game_state->settings = load_settings();
+    game_state->last_tick = 0;
+    game_state->processing = true;
+    game_state->selectedMenu = 0;
+    game_state->player_score = game_state->settings.starting_money;
+    generate_deck(&(game_state->deck), 6);
+    start_round(game_state);
+}
+
+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);
+}
+
+void doubleAction(void *state){
+    GameState *game_state = state;
+    if (!game_state->doubled &&  game_state->player_score >= game_state->settings.round_price) {
+        game_state->player_score -= game_state->settings.round_price;
+        game_state->bet += game_state->settings.round_price;
+        game_state->doubled = true;
+        enqueue(&(game_state->queue_state), game_state, drawPlayerCard, NULL, player_card_animation,
+                game_state->settings.animation_duration);
+        game_state->player_cards[game_state->player_card_count] = game_state->deck.cards[game_state->deck.index];
+        uint8_t score = hand_count(game_state->player_cards, game_state->player_card_count + 1);
+        if (score > 21) {
+            enqueue(&(game_state->queue_state), game_state, lose, NULL, to_bust_state,
+                    game_state->settings.message_duration);
+        } else {
+            enqueue(&(game_state->queue_state), game_state, dealerTurn, NULL, to_dealer_turn,
+                    game_state->settings.message_duration);
+        }
+        set_menu_state(game_state->menu, 0, false);
+    }
+}
+
+void hitAction(void *state){
+    GameState *game_state = state;
+    enqueue(&(game_state->queue_state), game_state, drawPlayerCard, NULL, player_card_animation,
+            game_state->settings.animation_duration);
+}
+void stayAction(void *state){
+    GameState *game_state = state;
+    enqueue(&(game_state->queue_state), game_state, dealerTurn, NULL, to_dealer_turn,
+            game_state->settings.message_duration);
+}
+
+int32_t blackjack_app(void *p) {
+    UNUSED(p);
+
+    int32_t return_code = 0;
+
+    FuriMessageQueue *event_queue = furi_message_queue_alloc(8, sizeof(AppEvent));
+    dolphin_deed(DolphinDeedPluginGameStart);
+    GameState *game_state = malloc(sizeof(GameState));
+    game_state->menu= malloc(sizeof(Menu));
+    game_state->menu->menu_width=40;
+    init(game_state);
+    add_menu(game_state->menu, "Double", doubleAction);
+    add_menu(game_state->menu, "Hit", hitAction);
+    add_menu(game_state->menu, "Stay", stayAction);
+    set_card_graphics(&I_card_graphics);
+
+    game_state->state = GameStateStart;
+
+    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;
+    }
+
+    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() / 25);
+
+    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, 100);
+        furi_mutex_acquire(game_state->mutex, FuriWaitForever);
+        if (event_status == FuriStatusOk) {
+            if (event.type == EventTypeKey) {
+
+                if (event.input.type == InputTypePress) {
+                    switch (event.input.key) {
+                        case InputKeyUp:
+                            game_state->selectDirection = DirectionUp;
+                            break;
+                        case InputKeyDown:
+                            game_state->selectDirection = DirectionDown;
+                            break;
+                        case InputKeyRight:
+                            game_state->selectDirection = DirectionRight;
+                            break;
+                        case InputKeyLeft:
+                            game_state->selectDirection = DirectionLeft;
+                            break;
+                        case InputKeyBack:
+                            if (game_state->state == GameStateSettings) {
+                                game_state->state = GameStateStart;
+                                save_settings(game_state->settings);
+                            } else
+                                processing = false;
+                            break;
+                        case InputKeyOk:
+                            game_state->selectDirection = Select;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            } else if (event.type == EventTypeTick) {
+                tick(game_state);
+                processing = game_state->processing;
+            }
+        } else {
+            FURI_LOG_D(APP_NAME, "osMessageQueue: event timeout");
+            // event timeout
+        }
+        view_port_update(view_port);
+        furi_mutex_release(game_state->mutex);
+    }
+
+
+    furi_timer_free(timer);
+    view_port_enabled_set(view_port, false);
+    gui_remove_view_port(gui, view_port);
+    furi_record_close(RECORD_GUI);
+    view_port_free(view_port);
+    furi_mutex_free(game_state->mutex);
+
+    free_and_exit:
+    free(game_state->deck.cards);
+    free_menu(game_state->menu);
+    queue_clear(&(game_state->queue_state));
+    free(game_state);
+    furi_message_queue_free(event_queue);
+
+    return return_code;
+}

BIN
blackjack/blackjack_10px.png


+ 1 - 0
blackjack/common

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

+ 78 - 0
blackjack/defines.h

@@ -0,0 +1,78 @@
+#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"
+#include "common/menu.h"
+
+#define APP_NAME "Blackjack"
+
+#define CONF_ANIMATION_DURATION "AnimationDuration"
+#define CONF_MESSAGE_DURATION "MessageDuration"
+#define CONF_STARTING_MONEY "StartingMoney"
+#define CONF_ROUND_PRICE "RoundPrice"
+#define CONF_SOUND_EFFECTS "SoundEffects"
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+typedef struct{
+    uint32_t animation_duration;
+    uint32_t message_duration;
+    uint32_t starting_money;
+    uint32_t round_price;
+    bool sound_effects;
+} Settings;
+
+typedef struct {
+    EventType type;
+    InputEvent input;
+} AppEvent;
+
+typedef enum {
+    GameStateGameOver,
+    GameStateStart,
+    GameStatePlay,
+    GameStateSettings,
+    GameStateDealer,
+} PlayState;
+
+typedef enum {
+    DirectionUp,
+    DirectionDown,
+    DirectionRight,
+    DirectionLeft,
+    Select,
+    Back,
+    None
+} Direction;
+
+typedef struct {
+    Card player_cards[21];
+    Card dealer_cards[21];
+    uint8_t player_card_count;
+    uint8_t dealer_card_count;
+
+    Direction selectDirection;
+    Settings settings;
+
+    uint32_t player_score;
+    uint32_t bet;
+    uint8_t selectedMenu;
+    bool doubled;
+    bool started;
+    bool processing;
+    Deck deck;
+    PlayState state;
+    QueueState queue_state;
+    Menu *menu;
+    unsigned int last_tick;
+    FuriMutex* mutex;
+} GameState;
+

BIN
blackjack/screenshots/blackjack.gif


BIN
blackjack/screenshots/play_scene.png


BIN
blackjack/screenshots/welcome.png


+ 180 - 0
blackjack/ui.c

@@ -0,0 +1,180 @@
+#include <math.h>
+#include <notification/notification_messages.h>
+
+#include "ui.h"
+
+#define LINE_HEIGHT 16
+#define ITEM_PADDING 4
+
+const char MoneyMul[4] = {
+        'K', 'B', 'T', 'S'
+};
+
+void draw_player_scene(Canvas *const canvas, const GameState *game_state) {
+    int max_card = game_state->player_card_count;
+
+    if (max_card > 0)
+        draw_deck((game_state->player_cards), max_card, canvas);
+
+    if (game_state->dealer_card_count > 0)
+        draw_card_back_at(13, 5, canvas);
+
+    max_card = game_state->dealer_card_count;
+    if (max_card > 1) {
+        draw_card_at(2, 2, game_state->dealer_cards[1].pip, game_state->dealer_cards[1].character,
+                   canvas);
+    }
+}
+
+void draw_dealer_scene(Canvas *const canvas, const GameState *game_state) {
+    uint8_t max_card = game_state->dealer_card_count;
+    draw_deck((game_state->dealer_cards), max_card, canvas);
+}
+
+void popup_frame(Canvas *const canvas) {
+    canvas_set_color(canvas, ColorWhite);
+    canvas_draw_box(canvas, 32, 15, 66, 13);
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_frame(canvas, 32, 15, 66, 13);
+    canvas_set_font(canvas, FontSecondary);
+}
+
+
+void draw_play_menu(Canvas *const canvas, const GameState *game_state) {
+    const char *menus[3] = {"Double", "Hit", "Stay"};
+    for (uint8_t m = 0; m < 3; m++) {
+        if (m == 0 && (game_state->doubled || game_state->player_score < game_state->settings.round_price)) continue;
+        int y = m * 13 + 25;
+        canvas_set_color(canvas, ColorBlack);
+
+        if (game_state->selectedMenu == m) {
+            canvas_set_color(canvas, ColorBlack);
+            canvas_draw_box(canvas, 1, y, 31, 12);
+        } else {
+            canvas_set_color(canvas, ColorWhite);
+            canvas_draw_box(canvas, 1, y, 31, 12);
+            canvas_set_color(canvas, ColorBlack);
+            canvas_draw_frame(canvas, 1, y, 31, 12);
+        }
+
+        if (game_state->selectedMenu == m)
+            canvas_set_color(canvas, ColorWhite);
+        else
+            canvas_set_color(canvas, ColorBlack);
+        canvas_draw_str_aligned(canvas, 16, y + 6, AlignCenter, AlignCenter, menus[m]);
+    }
+}
+
+void draw_screen(Canvas *const canvas, const bool *points) {
+    for (uint8_t x = 0; x < 128; x++) {
+        for (uint8_t y = 0; y < 64; y++) {
+            if (points[y * 128 + x])
+                canvas_draw_dot(canvas, x, y);
+        }
+    }
+}
+
+void draw_score(Canvas *const canvas, bool top, uint8_t amount) {
+    char drawChar[20];
+    snprintf(drawChar, sizeof(drawChar), "Player score: %i", amount);
+    if (top)
+        canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, drawChar);
+    else
+        canvas_draw_str_aligned(canvas, 64, 62, AlignCenter, AlignBottom, drawChar);
+}
+
+void draw_money(Canvas *const canvas, uint32_t score) {
+    canvas_set_font(canvas, FontSecondary);
+    char drawChar[10];
+    uint32_t currAmount = score;
+    if (currAmount < 1000) {
+        snprintf(drawChar, sizeof(drawChar), "$%lu", currAmount);
+    } else {
+        char c = 'K';
+        for (uint8_t i = 0; i < 4; i++) {
+            currAmount = currAmount / 1000;
+            if (currAmount < 1000) {
+                c = MoneyMul[i];
+                break;
+            }
+        }
+
+        snprintf(drawChar, sizeof(drawChar), "$%lu %c", currAmount, c);
+    }
+    canvas_draw_str_aligned(canvas, 126, 2, AlignRight, AlignTop, drawChar);
+}
+
+
+void draw_menu(Canvas *const canvas, const char *text, const char *value, int8_t y, bool left_caret, bool right_caret,
+               bool selected) {
+    UNUSED(selected);
+    if (y < 0 || y >= 64) return;
+
+    if (selected) {
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_box(canvas, 0, y, 122, LINE_HEIGHT);
+        canvas_set_color(canvas, ColorWhite);
+    }
+
+    canvas_draw_str_aligned(canvas, 4, y + ITEM_PADDING, AlignLeft, AlignTop, text);
+    if (left_caret)
+        canvas_draw_str_aligned(canvas, 80, y + ITEM_PADDING, AlignLeft, AlignTop, "<");
+    canvas_draw_str_aligned(canvas, 100, y + ITEM_PADDING, AlignCenter, AlignTop, value);
+    if (right_caret)
+        canvas_draw_str_aligned(canvas, 120, y + ITEM_PADDING, AlignRight, AlignTop, ">");
+
+    canvas_set_color(canvas, ColorBlack);
+}
+
+void settings_page(Canvas *const canvas, const GameState *gameState) {
+    char drawChar[10];
+    int startY = 0;
+    if (LINE_HEIGHT * (gameState->selectedMenu + 1) >= 64) {
+        startY -= (LINE_HEIGHT * (gameState->selectedMenu + 1)) - 64;
+    }
+
+    int scrollHeight = round(64 / 6.0) + ITEM_PADDING * 2;
+    int scrollPos = 64 / (6.0 / (gameState->selectedMenu + 1)) - ITEM_PADDING * 2;
+
+
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_box(canvas, 123, scrollPos, 4, scrollHeight);
+    canvas_draw_box(canvas, 125, 0, 1, 64);
+
+    snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.starting_money);
+    draw_menu(canvas, "Start money", drawChar,
+              0 * LINE_HEIGHT + startY,
+              gameState->settings.starting_money > gameState->settings.round_price,
+              gameState->settings.starting_money < 400,
+              gameState->selectedMenu == 0
+    );
+    snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.round_price);
+    draw_menu(canvas, "Round price", drawChar,
+              1 * LINE_HEIGHT + startY,
+              gameState->settings.round_price > 10,
+              gameState->settings.round_price < gameState->settings.starting_money,
+              gameState->selectedMenu == 1
+    );
+
+    snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.animation_duration);
+    draw_menu(canvas, "Anim. length", drawChar,
+              2 * LINE_HEIGHT + startY,
+              gameState->settings.animation_duration > 0,
+              gameState->settings.animation_duration < 2000,
+              gameState->selectedMenu == 2
+    );
+    snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.message_duration);
+    draw_menu(canvas, "Popup time", drawChar,
+              3 * LINE_HEIGHT + startY,
+              gameState->settings.message_duration > 0,
+              gameState->settings.message_duration < 2000,
+              gameState->selectedMenu == 3
+    );
+//    draw_menu(canvas, "Sound", gameState->settings.sound_effects ? "Yes" : "No",
+//              5 * LINE_HEIGHT + startY,
+//              true,
+//              true,
+//              gameState->selectedMenu == 5
+//    );
+
+}

+ 18 - 0
blackjack/ui.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#include "defines.h"
+#include <gui/gui.h>
+
+void draw_player_scene(Canvas *const canvas, const GameState *game_state);
+
+void draw_dealer_scene(Canvas *const canvas, const GameState *game_state);
+
+void draw_play_menu(Canvas *const canvas, const GameState *game_state);
+
+void draw_score(Canvas *const canvas, bool top, uint8_t amount);
+
+void draw_money(Canvas *const canvas, uint32_t score);
+void settings_page(Canvas *const canvas, const GameState * gameState);
+
+void popup_frame(Canvas *const canvas);
+void draw_screen(Canvas *const canvas, const bool* points);

+ 123 - 0
blackjack/util.c

@@ -0,0 +1,123 @@
+#include <storage/storage.h>
+#include "util.h"
+
+const char *CONFIG_FILE_PATH = EXT_PATH(".blackjack.settings");
+
+void save_settings(Settings settings) {
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat *file = flipper_format_file_alloc(storage);
+    FURI_LOG_D(APP_NAME, "Saving config");
+    if (flipper_format_file_open_existing(file, CONFIG_FILE_PATH)) {
+        FURI_LOG_D(APP_NAME, "Saving %s: %ld", CONF_ANIMATION_DURATION, settings.animation_duration);
+        flipper_format_update_uint32(file, CONF_ANIMATION_DURATION, &(settings.animation_duration), 1);
+
+
+        FURI_LOG_D(APP_NAME, "Saving %s: %ld", CONF_MESSAGE_DURATION, settings.message_duration);
+        flipper_format_update_uint32(file, CONF_MESSAGE_DURATION, &(settings.message_duration), 1);
+
+        FURI_LOG_D(APP_NAME, "Saving %s: %ld", CONF_STARTING_MONEY, settings.starting_money);
+        flipper_format_update_uint32(file, CONF_STARTING_MONEY, &(settings.starting_money), 1);
+
+        FURI_LOG_D(APP_NAME, "Saving %s: %ld", CONF_ROUND_PRICE, settings.round_price);
+        flipper_format_update_uint32(file, CONF_ROUND_PRICE, &(settings.round_price), 1);
+
+        FURI_LOG_D(APP_NAME, "Saving %s: %i", CONF_SOUND_EFFECTS, settings.sound_effects?1:0);
+        flipper_format_update_bool(file, CONF_SOUND_EFFECTS, &(settings.sound_effects), 1);
+        FURI_LOG_D(APP_NAME, "Config saved");
+    }else{
+        FURI_LOG_E(APP_NAME, "Save error");
+    }
+    flipper_format_file_close(file);
+    flipper_format_free(file);
+    furi_record_close(RECORD_STORAGE);
+}
+
+void save_settings_file(FlipperFormat *file, Settings *settings) {
+    flipper_format_write_header_cstr(file, CONFIG_FILE_HEADER, CONFIG_FILE_VERSION);
+    flipper_format_write_comment_cstr(file, "Card animation duration in ms");
+    flipper_format_write_uint32(file, CONF_ANIMATION_DURATION, &(settings->animation_duration), 1);
+    flipper_format_write_comment_cstr(file, "Popup message duration in ms");
+    flipper_format_write_uint32(file, CONF_MESSAGE_DURATION, &(settings->message_duration), 1);
+    flipper_format_write_comment_cstr(file, "Player's starting money");
+    flipper_format_write_uint32(file, CONF_STARTING_MONEY, &(settings->starting_money), 1);
+    flipper_format_write_comment_cstr(file, "Round price");
+    flipper_format_write_uint32(file, CONF_ROUND_PRICE, &(settings->round_price), 1);
+    flipper_format_write_comment_cstr(file, "Enable sound effects");
+    flipper_format_write_bool(file, CONF_SOUND_EFFECTS, &(settings->sound_effects), 1);
+}
+
+Settings load_settings() {
+    Settings settings;
+
+    FURI_LOG_D(APP_NAME, "Loading default settings");
+    settings.animation_duration = 800;
+    settings.message_duration = 1500;
+    settings.starting_money = 200;
+    settings.round_price = 10;
+    settings.sound_effects = true;
+
+    FURI_LOG_D(APP_NAME, "Opening storage");
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    FURI_LOG_D(APP_NAME, "Allocating file");
+    FlipperFormat *file = flipper_format_file_alloc(storage);
+
+    FURI_LOG_D(APP_NAME, "Allocating string");
+    FuriString *string_value;
+    string_value = furi_string_alloc();
+
+    if (storage_common_stat(storage, CONFIG_FILE_PATH, NULL) != FSE_OK) {
+        FURI_LOG_D(APP_NAME, "Config file %s not found, creating new one...", CONFIG_FILE_PATH);
+        if (!flipper_format_file_open_new(file, CONFIG_FILE_PATH)) {
+            FURI_LOG_E(APP_NAME, "Error creating new file %s", CONFIG_FILE_PATH);
+            flipper_format_file_close(file);
+        } else {
+            save_settings_file(file, &settings);
+        }
+    } else {
+        if (!flipper_format_file_open_existing(file, CONFIG_FILE_PATH)) {
+            FURI_LOG_E(APP_NAME, "Error opening existing file %s", CONFIG_FILE_PATH);
+            flipper_format_file_close(file);
+        }
+        else {
+            uint32_t value;
+            bool valueBool;
+            FURI_LOG_D(APP_NAME, "Checking version");
+            if (!flipper_format_read_header(file, string_value, &value)) {
+                FURI_LOG_E(APP_NAME, "Config file mismatch");
+            } else {
+                FURI_LOG_D(APP_NAME, "Loading %s", CONF_ANIMATION_DURATION);
+                if (flipper_format_read_uint32(file, CONF_ANIMATION_DURATION, &value, 1)) {
+                    settings.animation_duration = value;
+                    FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_ANIMATION_DURATION, value);
+                }
+                FURI_LOG_D(APP_NAME, "Loading %s", CONF_MESSAGE_DURATION);
+                if (flipper_format_read_uint32(file, CONF_MESSAGE_DURATION, &value, 1)) {
+                    settings.message_duration = value;
+                    FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_MESSAGE_DURATION, value);
+                }
+                FURI_LOG_D(APP_NAME, "Loading %s", CONF_STARTING_MONEY);
+                if (flipper_format_read_uint32(file, CONF_STARTING_MONEY, &value, 1)) {
+                    settings.starting_money = value;
+                    FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_STARTING_MONEY, value);
+                }
+                FURI_LOG_D(APP_NAME, "Loading %s", CONF_ROUND_PRICE);
+                if (flipper_format_read_uint32(file, CONF_ROUND_PRICE, &value, 1)) {
+                    settings.round_price = value;
+                    FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_ROUND_PRICE, value);
+                }
+                FURI_LOG_D(APP_NAME, "Loading %s", CONF_SOUND_EFFECTS);
+                if (flipper_format_read_bool(file, CONF_SOUND_EFFECTS, &valueBool, 1)) {
+                    settings.sound_effects = valueBool;
+                    FURI_LOG_D(APP_NAME, "Loaded %s: %i", CONF_ROUND_PRICE, valueBool?1:0);
+                }
+            }
+            flipper_format_file_close(file);
+        }
+    }
+
+    furi_string_free(string_value);
+//        flipper_format_file_close(file);
+    flipper_format_free(file);
+    furi_record_close(RECORD_STORAGE);
+    return settings;
+}

+ 7 - 0
blackjack/util.h

@@ -0,0 +1,7 @@
+#pragma once
+#include "defines.h"
+#define CONFIG_FILE_HEADER "Blackjack config file"
+#define CONFIG_FILE_VERSION 1
+
+void save_settings(Settings settings);
+Settings load_settings();