diff --git a/applications/plugins/blackjack/application.fam b/applications/plugins/blackjack/application.fam new file mode 100644 index 000000000..d365cb895 --- /dev/null +++ b/applications/plugins/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" +) \ No newline at end of file diff --git a/applications/plugins/blackjack/assets/blackjack.png b/applications/plugins/blackjack/assets/blackjack.png new file mode 100644 index 000000000..bb367f28e Binary files /dev/null and b/applications/plugins/blackjack/assets/blackjack.png differ diff --git a/applications/plugins/blackjack/assets/card_graphics.png b/applications/plugins/blackjack/assets/card_graphics.png new file mode 100644 index 000000000..8b00e351f Binary files /dev/null and b/applications/plugins/blackjack/assets/card_graphics.png differ diff --git a/applications/plugins/blackjack/assets/endscreen.png b/applications/plugins/blackjack/assets/endscreen.png new file mode 100644 index 000000000..7a3abc927 Binary files /dev/null and b/applications/plugins/blackjack/assets/endscreen.png differ diff --git a/applications/plugins/blackjack/blackjack.c b/applications/plugins/blackjack/blackjack.c new file mode 100644 index 000000000..bcc192f83 --- /dev/null +++ b/applications/plugins/blackjack/blackjack.c @@ -0,0 +1,634 @@ + +#include +#include +#include +#include +#include + +#include +#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 = acquire_mutex((ValueMutex*)ctx, 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); + } + + release_mutex((ValueMutex*)ctx, game_state); +} + +//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) { + 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)); + + 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; + + ValueMutex state_mutex; + if(!init_mutex(&state_mutex, game_state, sizeof(GameState))) { + 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, &state_mutex); + 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); + GameState* localstate = (GameState*)acquire_mutex_block(&state_mutex); + if(event_status == FuriStatusOk) { + if(event.type == EventTypeKey) { + if(event.input.type == InputTypePress) { + switch(event.input.key) { + case InputKeyUp: + localstate->selectDirection = DirectionUp; + break; + case InputKeyDown: + localstate->selectDirection = DirectionDown; + break; + case InputKeyRight: + localstate->selectDirection = DirectionRight; + break; + case InputKeyLeft: + localstate->selectDirection = DirectionLeft; + break; + case InputKeyBack: + if(localstate->state == GameStateSettings) { + localstate->state = GameStateStart; + save_settings(localstate->settings); + } else + processing = false; + break; + case InputKeyOk: + localstate->selectDirection = Select; + break; + default: + break; + } + } + } else if(event.type == EventTypeTick) { + tick(localstate); + processing = localstate->processing; + } + } else { + //FURI_LOG_D(APP_NAME, "osMessageQueue: event timeout"); + // event timeout + } + view_port_update(view_port); + release_mutex(&state_mutex, localstate); + } + + 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); + delete_mutex(&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; +} \ No newline at end of file diff --git a/applications/plugins/blackjack/blackjack_10px.png b/applications/plugins/blackjack/blackjack_10px.png new file mode 100644 index 000000000..7382d6358 Binary files /dev/null and b/applications/plugins/blackjack/blackjack_10px.png differ diff --git a/applications/plugins/blackjack/common/card.c b/applications/plugins/blackjack/common/card.c new file mode 100644 index 000000000..199135bb5 --- /dev/null +++ b/applications/plugins/blackjack/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]); + } + } +} \ No newline at end of file diff --git a/applications/plugins/blackjack/common/card.h b/applications/plugins/blackjack/common/card.h new file mode 100644 index 000000000..8e5e23bbf --- /dev/null +++ b/applications/plugins/blackjack/common/card.h @@ -0,0 +1,192 @@ +#pragma once + +#include +#include +#include +#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); \ No newline at end of file diff --git a/applications/plugins/blackjack/common/dml.c b/applications/plugins/blackjack/common/dml.c new file mode 100644 index 000000000..b9a0e395f --- /dev/null +++ b/applications/plugins/blackjack/common/dml.c @@ -0,0 +1,53 @@ +#include "dml.h" +#include + +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; +} \ No newline at end of file diff --git a/applications/plugins/blackjack/common/dml.h b/applications/plugins/blackjack/common/dml.h new file mode 100644 index 000000000..0e1a23e23 --- /dev/null +++ b/applications/plugins/blackjack/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); diff --git a/applications/plugins/blackjack/common/menu.c b/applications/plugins/blackjack/common/menu.c new file mode 100644 index 000000000..ffc3921b7 --- /dev/null +++ b/applications/plugins/blackjack/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); + } + } +} \ No newline at end of file diff --git a/applications/plugins/blackjack/common/menu.h b/applications/plugins/blackjack/common/menu.h new file mode 100644 index 000000000..9f2852522 --- /dev/null +++ b/applications/plugins/blackjack/common/menu.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +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); \ No newline at end of file diff --git a/applications/plugins/blackjack/common/queue.c b/applications/plugins/blackjack/common/queue.c new file mode 100644 index 000000000..a80373460 --- /dev/null +++ b/applications/plugins/blackjack/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; +} \ No newline at end of file diff --git a/applications/plugins/blackjack/common/queue.h b/applications/plugins/blackjack/common/queue.h new file mode 100644 index 000000000..dcfe0c091 --- /dev/null +++ b/applications/plugins/blackjack/common/queue.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +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); \ No newline at end of file diff --git a/applications/plugins/blackjack/common/ui.c b/applications/plugins/blackjack/common/ui.c new file mode 100644 index 000000000..032877a9e --- /dev/null +++ b/applications/plugins/blackjack/common/ui.c @@ -0,0 +1,257 @@ +#include "ui.h" +#include +#include +#include +#include +#include +#include + +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); + } + } +} \ No newline at end of file diff --git a/applications/plugins/blackjack/common/ui.h b/applications/plugins/blackjack/common/ui.h new file mode 100644 index 000000000..13d8da257 --- /dev/null +++ b/applications/plugins/blackjack/common/ui.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +#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); \ No newline at end of file diff --git a/applications/plugins/blackjack/defines.h b/applications/plugins/blackjack/defines.h new file mode 100644 index 000000000..b400badfb --- /dev/null +++ b/applications/plugins/blackjack/defines.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include +#include +#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; +} GameState; diff --git a/applications/plugins/blackjack/ui.c b/applications/plugins/blackjack/ui.c new file mode 100644 index 000000000..d4ee82191 --- /dev/null +++ b/applications/plugins/blackjack/ui.c @@ -0,0 +1,186 @@ +#include +#include + +#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[11]; + 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 + // ); +} \ No newline at end of file diff --git a/applications/plugins/blackjack/ui.h b/applications/plugins/blackjack/ui.h new file mode 100644 index 000000000..51b388010 --- /dev/null +++ b/applications/plugins/blackjack/ui.h @@ -0,0 +1,18 @@ +#pragma once + +#include "defines.h" +#include + +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); diff --git a/applications/plugins/blackjack/util.c b/applications/plugins/blackjack/util.c new file mode 100644 index 000000000..8e88c2231 --- /dev/null +++ b/applications/plugins/blackjack/util.c @@ -0,0 +1,123 @@ +#include +#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; +} \ No newline at end of file diff --git a/applications/plugins/blackjack/util.h b/applications/plugins/blackjack/util.h new file mode 100644 index 000000000..4bcc4d890 --- /dev/null +++ b/applications/plugins/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(); \ No newline at end of file diff --git a/applications/plugins/hex_viewer/application.fam b/applications/plugins/hex_viewer/application.fam new file mode 100644 index 000000000..7204e07c8 --- /dev/null +++ b/applications/plugins/hex_viewer/application.fam @@ -0,0 +1,16 @@ +App( + appid="hex_viewer", + name="HEX Viewer", + apptype=FlipperAppType.EXTERNAL, + entry_point="hex_viewer_app", + cdefines=["APP_HEX_VIEWER"], + requires=[ + "gui", + "dialogs", + ], + stack_size=2 * 1024, + order=20, + fap_icon="icons/hex_10px.png", + fap_category="Misc", + fap_icon_assets="icons", +) diff --git a/applications/plugins/hex_viewer/hex_viewer.c b/applications/plugins/hex_viewer/hex_viewer.c new file mode 100644 index 000000000..5289c8654 --- /dev/null +++ b/applications/plugins/hex_viewer/hex_viewer.c @@ -0,0 +1,285 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "HexViewer" + +#define HEX_VIEWER_APP_PATH_FOLDER "/any" +#define HEX_VIEWER_APP_EXTENSION "*" + +#define HEX_VIEWER_BYTES_PER_ROW 4 +#define HEX_VIEWER_ROW_COUNT 4 +#define HEX_VIEWER_BUF_SIZE (HEX_VIEWER_BYTES_PER_ROW * HEX_VIEWER_ROW_COUNT) + +typedef struct { + uint8_t file_bytes[HEX_VIEWER_ROW_COUNT][HEX_VIEWER_ROW_COUNT]; + uint32_t line; + uint32_t read_bytes; + uint32_t file_size; + Stream* stream; + bool mode; // Print address or content +} HexViewerModel; + +typedef struct { + HexViewerModel* model; + FuriMutex** mutex; + + FuriMessageQueue* input_queue; + + ViewPort* view_port; + Gui* gui; + Storage* storage; +} HexViewer; + +static void render_callback(Canvas* canvas, void* ctx) { + HexViewer* hex_viewer = ctx; + furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk); + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + + elements_button_left(canvas, hex_viewer->model->mode ? "Addr" : "Text"); + elements_button_right(canvas, "Info"); + + int ROW_HEIGHT = 12; + int TOP_OFFSET = 10; + int LEFT_OFFSET = 3; + + uint32_t line_count = hex_viewer->model->file_size / HEX_VIEWER_BYTES_PER_ROW; + if(hex_viewer->model->file_size % HEX_VIEWER_BYTES_PER_ROW != 0) line_count += 1; + if(line_count > HEX_VIEWER_ROW_COUNT) { + uint8_t width = canvas_width(canvas); + elements_scrollbar_pos( + canvas, + width, + 0, + ROW_HEIGHT * HEX_VIEWER_ROW_COUNT, + hex_viewer->model->line, + line_count - (HEX_VIEWER_ROW_COUNT - 1)); + } + + char temp_buf[32]; + uint32_t row_iters = hex_viewer->model->read_bytes / HEX_VIEWER_BYTES_PER_ROW; + if(hex_viewer->model->read_bytes % HEX_VIEWER_BYTES_PER_ROW != 0) row_iters += 1; + + for(uint32_t i = 0; i < row_iters; ++i) { + uint32_t bytes_left_per_row = hex_viewer->model->read_bytes - i * HEX_VIEWER_BYTES_PER_ROW; + if(bytes_left_per_row > HEX_VIEWER_BYTES_PER_ROW) + bytes_left_per_row = HEX_VIEWER_BYTES_PER_ROW; + + if(hex_viewer->model->mode) { + memcpy(temp_buf, hex_viewer->model->file_bytes[i], bytes_left_per_row); + temp_buf[bytes_left_per_row] = '\0'; + for(uint32_t j = 0; j < bytes_left_per_row; ++j) + if(!isprint((int)temp_buf[j])) temp_buf[j] = '.'; + + canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } else { + int addr = (i + hex_viewer->model->line) * HEX_VIEWER_BYTES_PER_ROW; + snprintf(temp_buf, 32, "%04X", addr); + + canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } + + char* p = temp_buf; + for(uint32_t j = 0; j < bytes_left_per_row; ++j) + p += snprintf(p, 32, "%02X ", hex_viewer->model->file_bytes[i][j]); + + canvas_set_font(canvas, FontKeyboard); + canvas_draw_str(canvas, LEFT_OFFSET + 41, TOP_OFFSET + i * ROW_HEIGHT, temp_buf); + } + + furi_mutex_release(hex_viewer->mutex); +} + +static void input_callback(InputEvent* input_event, void* ctx) { + HexViewer* hex_viewer = ctx; + if(input_event->type == InputTypeShort) { + furi_message_queue_put(hex_viewer->input_queue, input_event, 0); + } +} + +static HexViewer* hex_viewer_alloc() { + HexViewer* instance = malloc(sizeof(HexViewer)); + + instance->model = malloc(sizeof(HexViewerModel)); + memset(instance->model, 0x0, sizeof(HexViewerModel)); + + instance->mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + instance->view_port = view_port_alloc(); + view_port_draw_callback_set(instance->view_port, render_callback, instance); + view_port_input_callback_set(instance->view_port, input_callback, instance); + + instance->gui = furi_record_open(RECORD_GUI); + gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen); + + instance->storage = furi_record_open(RECORD_STORAGE); + + return instance; +} + +static void hex_viewer_free(HexViewer* instance) { + furi_record_close(RECORD_STORAGE); + + gui_remove_view_port(instance->gui, instance->view_port); + furi_record_close(RECORD_GUI); + view_port_free(instance->view_port); + + furi_message_queue_free(instance->input_queue); + + furi_mutex_free(instance->mutex); + + if(instance->model->stream) buffered_file_stream_close(instance->model->stream); + + free(instance->model); + free(instance); +} + +static bool hex_viewer_open_file(HexViewer* hex_viewer, const char* file_path) { + furi_assert(hex_viewer); + furi_assert(file_path); + + hex_viewer->model->stream = buffered_file_stream_alloc(hex_viewer->storage); + bool isOk = true; + + do { + if(!buffered_file_stream_open( + hex_viewer->model->stream, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_E(TAG, "Unable to open stream: %s", file_path); + isOk = false; + break; + }; + + hex_viewer->model->file_size = stream_size(hex_viewer->model->stream); + } while(false); + + return isOk; +} + +static bool hex_viewer_read_file(HexViewer* hex_viewer) { + furi_assert(hex_viewer); + furi_assert(hex_viewer->model->stream); + + memset(hex_viewer->model->file_bytes, 0x0, HEX_VIEWER_BUF_SIZE); + bool isOk = true; + + do { + uint32_t offset = hex_viewer->model->line * HEX_VIEWER_BYTES_PER_ROW; + if(!stream_seek(hex_viewer->model->stream, offset, true)) { + FURI_LOG_E(TAG, "Unable to seek stream"); + isOk = false; + break; + } + + hex_viewer->model->read_bytes = stream_read( + hex_viewer->model->stream, + (uint8_t*)hex_viewer->model->file_bytes, + HEX_VIEWER_BUF_SIZE); + } while(false); + + return isOk; +} + +int32_t hex_viewer_app(void* p) { + HexViewer* hex_viewer = hex_viewer_alloc(); + + FuriString* file_path; + file_path = furi_string_alloc(); + + do { + if(p && strlen(p)) { + furi_string_set(file_path, (const char*)p); + } else { + furi_string_set(file_path, HEX_VIEWER_APP_PATH_FOLDER); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options( + &browser_options, HEX_VIEWER_APP_EXTENSION, &I_hex_10px); + browser_options.hide_ext = false; + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options); + + furi_record_close(RECORD_DIALOGS); + if(!res) { + FURI_LOG_I(TAG, "No file selected"); + break; + } + } + + if(!hex_viewer_open_file(hex_viewer, furi_string_get_cstr(file_path))) break; + hex_viewer_read_file(hex_viewer); + + InputEvent input; + while(furi_message_queue_get(hex_viewer->input_queue, &input, FuriWaitForever) == + FuriStatusOk) { + if(input.key == InputKeyBack) { + break; + } else if(input.key == InputKeyUp) { + furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk); + if(hex_viewer->model->line > 0) { + hex_viewer->model->line--; + + if(!hex_viewer_read_file(hex_viewer)) break; + } + furi_mutex_release(hex_viewer->mutex); + } else if(input.key == InputKeyDown) { + furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk); + uint32_t cur_pos = hex_viewer->model->line * HEX_VIEWER_BYTES_PER_ROW + + hex_viewer->model->read_bytes; + + if(hex_viewer->model->file_size > cur_pos) { + hex_viewer->model->line++; + if(!hex_viewer_read_file(hex_viewer)) break; + } + furi_mutex_release(hex_viewer->mutex); + } else if(input.key == InputKeyLeft) { + furi_check(furi_mutex_acquire(hex_viewer->mutex, FuriWaitForever) == FuriStatusOk); + hex_viewer->model->mode = !hex_viewer->model->mode; + furi_mutex_release(hex_viewer->mutex); + } else if(input.key == InputKeyRight) { + FuriString* buffer; + buffer = furi_string_alloc(); + furi_string_printf( + buffer, + "File path: %s\nFile size: %lu bytes", + furi_string_get_cstr(file_path), + hex_viewer->model->file_size); + + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Hex Viewer", 16, 2, AlignLeft, AlignTop); + dialog_message_set_icon(message, &I_hex_10px, 3, 2); + dialog_message_set_text( + message, furi_string_get_cstr(buffer), 3, 16, AlignLeft, AlignTop); + dialog_message_set_buttons(message, NULL, NULL, "Back"); + dialog_message_show(dialogs, message); + + furi_string_free(buffer); + dialog_message_free(message); + furi_record_close(RECORD_DIALOGS); + } + + view_port_update(hex_viewer->view_port); + } + } while(false); + + furi_string_free(file_path); + hex_viewer_free(hex_viewer); + + return 0; +} diff --git a/applications/plugins/hex_viewer/icons/hex_10px.png b/applications/plugins/hex_viewer/icons/hex_10px.png new file mode 100644 index 000000000..582e288c6 Binary files /dev/null and b/applications/plugins/hex_viewer/icons/hex_10px.png differ diff --git a/applications/plugins/solitaire/application.fam b/applications/plugins/solitaire/application.fam new file mode 100644 index 000000000..66e4567ec --- /dev/null +++ b/applications/plugins/solitaire/application.fam @@ -0,0 +1,13 @@ +App( + appid="Solitaire", + name="Solitaire", + apptype=FlipperAppType.EXTERNAL, + entry_point="solitaire_app", + cdefines=["APP_SOLITAIRE"], + requires=["gui","storage","canvas"], + stack_size=2 * 1024, + order=30, + fap_icon="solitaire_10px.png", + fap_category="Games", + fap_icon_assets="assets" +) \ No newline at end of file diff --git a/applications/plugins/solitaire/assets/card_graphics.png b/applications/plugins/solitaire/assets/card_graphics.png new file mode 100644 index 000000000..8b00e351f Binary files /dev/null and b/applications/plugins/solitaire/assets/card_graphics.png differ diff --git a/applications/plugins/solitaire/assets/solitaire_main.png b/applications/plugins/solitaire/assets/solitaire_main.png new file mode 100644 index 000000000..996556d57 Binary files /dev/null and b/applications/plugins/solitaire/assets/solitaire_main.png differ diff --git a/applications/plugins/solitaire/common/card.c b/applications/plugins/solitaire/common/card.c new file mode 100644 index 000000000..199135bb5 --- /dev/null +++ b/applications/plugins/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]); + } + } +} \ No newline at end of file diff --git a/applications/plugins/solitaire/common/card.h b/applications/plugins/solitaire/common/card.h new file mode 100644 index 000000000..8e5e23bbf --- /dev/null +++ b/applications/plugins/solitaire/common/card.h @@ -0,0 +1,192 @@ +#pragma once + +#include +#include +#include +#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); \ No newline at end of file diff --git a/applications/plugins/solitaire/common/dml.c b/applications/plugins/solitaire/common/dml.c new file mode 100644 index 000000000..b9a0e395f --- /dev/null +++ b/applications/plugins/solitaire/common/dml.c @@ -0,0 +1,53 @@ +#include "dml.h" +#include + +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; +} \ No newline at end of file diff --git a/applications/plugins/solitaire/common/dml.h b/applications/plugins/solitaire/common/dml.h new file mode 100644 index 000000000..0e1a23e23 --- /dev/null +++ b/applications/plugins/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); diff --git a/applications/plugins/solitaire/common/menu.c b/applications/plugins/solitaire/common/menu.c new file mode 100644 index 000000000..ffc3921b7 --- /dev/null +++ b/applications/plugins/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); + } + } +} \ No newline at end of file diff --git a/applications/plugins/solitaire/common/menu.h b/applications/plugins/solitaire/common/menu.h new file mode 100644 index 000000000..9f2852522 --- /dev/null +++ b/applications/plugins/solitaire/common/menu.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +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); \ No newline at end of file diff --git a/applications/plugins/solitaire/common/queue.c b/applications/plugins/solitaire/common/queue.c new file mode 100644 index 000000000..a80373460 --- /dev/null +++ b/applications/plugins/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; +} \ No newline at end of file diff --git a/applications/plugins/solitaire/common/queue.h b/applications/plugins/solitaire/common/queue.h new file mode 100644 index 000000000..dcfe0c091 --- /dev/null +++ b/applications/plugins/solitaire/common/queue.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +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); \ No newline at end of file diff --git a/applications/plugins/solitaire/common/ui.c b/applications/plugins/solitaire/common/ui.c new file mode 100644 index 000000000..032877a9e --- /dev/null +++ b/applications/plugins/solitaire/common/ui.c @@ -0,0 +1,257 @@ +#include "ui.h" +#include +#include +#include +#include +#include +#include + +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); + } + } +} \ No newline at end of file diff --git a/applications/plugins/solitaire/common/ui.h b/applications/plugins/solitaire/common/ui.h new file mode 100644 index 000000000..13d8da257 --- /dev/null +++ b/applications/plugins/solitaire/common/ui.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +#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); \ No newline at end of file diff --git a/applications/plugins/solitaire/defines.h b/applications/plugins/solitaire/defines.h new file mode 100644 index 000000000..1456f1964 --- /dev/null +++ b/applications/plugins/solitaire/defines.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include +#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 processing; + bool longPress; + PlayState state; + unsigned int last_tick; + uint8_t selectRow; + uint8_t selectColumn; + int8_t selected_card; + CardAnimation animation; + uint8_t* buffer; +} GameState; \ No newline at end of file diff --git a/applications/plugins/solitaire/solitaire.c b/applications/plugins/solitaire/solitaire.c new file mode 100644 index 000000000..e1fffbc8a --- /dev/null +++ b/applications/plugins/solitaire/solitaire.c @@ -0,0 +1,568 @@ +#include +#include +#include +#include +#include "defines.h" +#include "common/ui.h" +#include "Solitaire_icons.h" +#include +#include +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) { + FURI_LOG_D( + APP_NAME, + "TESTING pip %i, letter %i with pip %i, letter %i", + where.pip, + where.character, + what.pip, + what.character); + 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) { + int deckIndex = game_state->deck.index; + if(game_state->dragging_deck) deckIndex--; + + if((game_state->deck.index < (game_state->deck.card_count - 1) || + game_state->deck.index == -1) && + game_state->deck.card_count > 0) { + draw_card_back_at(columns[0][0], columns[0][1], canvas); + if(game_state->selectRow == 0 && game_state->selectColumn == 0) { + draw_rounded_box( + canvas, + columns[0][0] + 1, + columns[0][1] + 1, + CARD_WIDTH - 2, + CARD_HEIGHT - 2, + Inverse); + } + } else + draw_card_space( + columns[0][0], + columns[0][1], + game_state->selectRow == 0 && game_state->selectColumn == 0, + canvas); + //deck side + if(deckIndex >= 0) { + Card c = game_state->deck.cards[deckIndex]; + draw_card_at_colored( + columns[1][0], + columns[1][1], + c.pip, + c.character, + game_state->selectRow == 0 && game_state->selectColumn == 1, + canvas); + } else + draw_card_space( + columns[1][0], + columns[1][1], + game_state->selectRow == 0 && game_state->selectColumn == 1, + canvas); + + for(uint8_t i = 0; i < 4; i++) { + Card current = game_state->top_cards[i]; + bool selected = game_state->selectRow == 0 && game_state->selectColumn == (i + 3); + if(current.disabled) { + draw_card_space(columns[i + 3][0], columns[i + 3][1], selected, canvas); + } else { + draw_card_at( + columns[i + 3][0], columns[i + 3][1], current.pip, current.character, canvas); + if(selected) { + draw_rounded_box( + canvas, columns[i + 3][0], columns[i + 3][1], CARD_WIDTH, CARD_HEIGHT, Inverse); + } + } + } + + for(uint8_t i = 0; i < 7; i++) { + bool selected = game_state->selectRow == 1 && game_state->selectColumn == i; + int8_t index = (game_state->bottom_columns[i].index - 1 - game_state->selected_card); + if(index < 0) index = 0; + draw_hand_column( + game_state->bottom_columns[i], + columns[i][0], + columns[i][2], + selected ? index : -1, + canvas); + } + + int8_t pos[2] = { + columns[game_state->selectColumn][0], + columns[game_state->selectColumn][game_state->selectRow + 1]}; + + /* draw_icon_clip(canvas, &I_card_graphics, pos[0] + CARD_HALF_WIDTH, pos[1] + CARD_HALF_HEIGHT, 30, 5, 5, 5, + Filled);*/ + + if(game_state->dragging_hand.index > 0) { + draw_hand_column( + game_state->dragging_hand, + pos[0] + CARD_HALF_WIDTH + 3, + pos[1] + CARD_HALF_HEIGHT + 3, + -1, + canvas); + } +} + +static 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 = acquire_mutex((ValueMutex*)ctx, 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; + } + + release_mutex((ValueMutex*)ctx, game_state); +} + +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((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; + 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) { + 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; + ValueMutex state_mutex; + if(!init_mutex(&state_mutex, game_state, sizeof(GameState))) { + 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, &state_mutex); + 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); + GameState* localstate = (GameState*)acquire_mutex_block(&state_mutex); + bool hadChange = false; + if(event_status == FuriStatusOk) { + if(event.type == EventTypeKey) { + if(event.input.type == InputTypeLong) { + game_state->longPress = true; + switch(event.input.key) { + case InputKeyUp: + case InputKeyDown: + case InputKeyRight: + case InputKeyLeft: + case InputKeyOk: + localstate->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 && localstate->state == GameStateStart) { + localstate->state = GameStatePlay; + init(game_state); + } else { + hadChange = true; + localstate->input = event.input.key; + } + break; + case InputKeyBack: + init(game_state); + processing = false; + return_code = 1; + break; + default: + break; + } + } + } else if(event.type == EventTypeTick) { + tick(localstate, notification); + processing = localstate->processing; + localstate->input = InputKeyMAX; + } + } else { + //FURI_LOG_W(APP_NAME, "osMessageQueue: event timeout"); + // event timeout + } + if(hadChange || game_state->state == GameStateAnimate) view_port_update(view_port); + release_mutex(&state_mutex, localstate); + } + + 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); + delete_mutex(&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; +} \ No newline at end of file diff --git a/applications/plugins/solitaire/solitaire_10px.png b/applications/plugins/solitaire/solitaire_10px.png new file mode 100644 index 000000000..3c5669dd2 Binary files /dev/null and b/applications/plugins/solitaire/solitaire_10px.png differ