From 431a5b1a12d89e353cf257a8c3727d5c97105015 Mon Sep 17 00:00:00 2001 From: Haseo Date: Thu, 13 Oct 2022 20:57:04 +0200 Subject: [PATCH 1/2] Music Beeper By Haseo Music Beeper By Haseo Thx to Majik --- applications/plugins/application.fam | 4 +- .../plugins/music_beeper/application.fam | 24 + .../plugins/music_beeper/music_beeper.c | 367 +++++++++++++ .../plugins/music_beeper/music_beeper_cli.c | 48 ++ .../music_beeper/music_beeper_worker.c | 506 ++++++++++++++++++ .../music_beeper/music_beeper_worker.h | 46 ++ 6 files changed, 994 insertions(+), 1 deletion(-) create mode 100644 applications/plugins/music_beeper/application.fam create mode 100644 applications/plugins/music_beeper/music_beeper.c create mode 100644 applications/plugins/music_beeper/music_beeper_cli.c create mode 100644 applications/plugins/music_beeper/music_beeper_worker.c create mode 100644 applications/plugins/music_beeper/music_beeper_worker.h diff --git a/applications/plugins/application.fam b/applications/plugins/application.fam index 6d25e45aa..3e5a654aa 100644 --- a/applications/plugins/application.fam +++ b/applications/plugins/application.fam @@ -3,7 +3,9 @@ App( name="Basic applications for plug-in menu", apptype=FlipperAppType.METAPACKAGE, provides=[ - "music_player", + "music_player", + "music_beeper", "snake_game", + "bt_hid", ], ) diff --git a/applications/plugins/music_beeper/application.fam b/applications/plugins/music_beeper/application.fam new file mode 100644 index 000000000..4c74df5fd --- /dev/null +++ b/applications/plugins/music_beeper/application.fam @@ -0,0 +1,24 @@ +App( + appid="Music_Beeper", + name="Music Beeper", + apptype=FlipperAppType.EXTERNAL, + entry_point="music_beeper_app", + cdefines=["APP_MUSIC_BEEPER"], + requires=[ + "gui", + "dialogs", + ], + provides=["music_beeper_start"], + stack_size=2 * 1024, + order=45, + fap_icon="../../../assets/icons/Archive/music_10px.png", + fap_category="Music", +) + +App( + appid="music_beeper_start", + apptype=FlipperAppType.STARTUP, + entry_point="music_beeper_on_system_start", + requires=["music_beeper"], + order=30, +) diff --git a/applications/plugins/music_beeper/music_beeper.c b/applications/plugins/music_beeper/music_beeper.c new file mode 100644 index 000000000..a09cad226 --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper.c @@ -0,0 +1,367 @@ +#include "music_beeper_worker.h" + +#include +#include + +#include +#include +#include +#include + +#define TAG "MusicBeeper" + +#define MUSIC_BEEPER_APP_PATH_FOLDER ANY_PATH("music_beeper") +#define MUSIC_BEEPER_APP_EXTENSION "*" + +#define MUSIC_BEEPER_SEMITONE_HISTORY_SIZE 4 + +typedef struct { + uint8_t semitone_history[MUSIC_BEEPER_SEMITONE_HISTORY_SIZE]; + uint8_t duration_history[MUSIC_BEEPER_SEMITONE_HISTORY_SIZE]; + + uint8_t volume; + uint8_t semitone; + uint8_t dots; + uint8_t duration; + float position; +} MusicBeeperModel; + +typedef struct { + MusicBeeperModel* model; + FuriMutex** model_mutex; + + FuriMessageQueue* input_queue; + + ViewPort* view_port; + Gui* gui; + + MusicBeeperWorker* worker; +} MusicBeeper; + +static const float MUSIC_BEEPER_VOLUMES[] = {0, .25, .5, .75, 1}; + +static const char* semitone_to_note(int8_t semitone) { + switch(semitone) { + case 0: + return "C"; + case 1: + return "C#"; + case 2: + return "D"; + case 3: + return "D#"; + case 4: + return "E"; + case 5: + return "F"; + case 6: + return "F#"; + case 7: + return "G"; + case 8: + return "G#"; + case 9: + return "A"; + case 10: + return "A#"; + case 11: + return "B"; + default: + return "--"; + } +} + +static bool is_white_note(uint8_t semitone, uint8_t id) { + switch(semitone) { + case 0: + if(id == 0) return true; + break; + case 2: + if(id == 1) return true; + break; + case 4: + if(id == 2) return true; + break; + case 5: + if(id == 3) return true; + break; + case 7: + if(id == 4) return true; + break; + case 9: + if(id == 5) return true; + break; + case 11: + if(id == 6) return true; + break; + default: + break; + } + + return false; +} + +static bool is_black_note(uint8_t semitone, uint8_t id) { + switch(semitone) { + case 1: + if(id == 0) return true; + break; + case 3: + if(id == 1) return true; + break; + case 6: + if(id == 3) return true; + break; + case 8: + if(id == 4) return true; + break; + case 10: + if(id == 5) return true; + break; + default: + break; + } + + return false; +} + +static void render_callback(Canvas* canvas, void* ctx) { + MusicBeeper* music_beeper = ctx; + furi_check(furi_mutex_acquire(music_beeper->model_mutex, FuriWaitForever) == FuriStatusOk); + + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 0, 12, "MusicBeeper"); + + uint8_t x_pos = 0; + uint8_t y_pos = 24; + const uint8_t white_w = 10; + const uint8_t white_h = 40; + + const int8_t black_x = 6; + const int8_t black_y = -5; + const uint8_t black_w = 8; + const uint8_t black_h = 32; + + // white keys + for(size_t i = 0; i < 7; i++) { + if(is_white_note(music_beeper->model->semitone, i)) { + canvas_draw_box(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); + } else { + canvas_draw_frame(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h); + } + } + + // black keys + for(size_t i = 0; i < 7; i++) { + if(i != 2 && i != 6) { + canvas_set_color(canvas, ColorWhite); + canvas_draw_box( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + canvas_set_color(canvas, ColorBlack); + if(is_black_note(music_beeper->model->semitone, i)) { + canvas_draw_box( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + } else { + canvas_draw_frame( + canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h); + } + } + } + + // volume view_port + x_pos = 124; + y_pos = 0; + const uint8_t volume_h = + (64 / (COUNT_OF(MUSIC_BEEPER_VOLUMES) - 1)) * music_beeper->model->volume; + canvas_draw_frame(canvas, x_pos, y_pos, 4, 64); + canvas_draw_box(canvas, x_pos, y_pos + (64 - volume_h), 4, volume_h); + + // note stack view_port + x_pos = 73; + y_pos = 0; + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontPrimary); + canvas_draw_frame(canvas, x_pos, y_pos, 49, 64); + canvas_draw_line(canvas, x_pos + 28, 0, x_pos + 28, 64); + + char duration_text[16]; + for(uint8_t i = 0; i < MUSIC_BEEPER_SEMITONE_HISTORY_SIZE; i++) { + if(music_beeper->model->duration_history[i] == 0xFF) { + snprintf(duration_text, 15, "--"); + } else { + snprintf(duration_text, 15, "%d", music_beeper->model->duration_history[i]); + } + + if(i == 0) { + canvas_draw_box(canvas, x_pos, y_pos + 48, 49, 16); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_set_color(canvas, ColorBlack); + } + canvas_draw_str( + canvas, + x_pos + 4, + 64 - 16 * i - 3, + semitone_to_note(music_beeper->model->semitone_history[i])); + canvas_draw_str(canvas, x_pos + 31, 64 - 16 * i - 3, duration_text); + canvas_draw_line(canvas, x_pos, 64 - 16 * i, x_pos + 48, 64 - 16 * i); + } + + furi_mutex_release(music_beeper->model_mutex); +} + +static void input_callback(InputEvent* input_event, void* ctx) { + MusicBeeper* music_beeper = ctx; + if(input_event->type == InputTypeShort) { + furi_message_queue_put(music_beeper->input_queue, input_event, 0); + } +} + +static void music_beeper_worker_callback( + uint8_t semitone, + uint8_t dots, + uint8_t duration, + float position, + void* context) { + MusicBeeper* music_beeper = context; + furi_check(furi_mutex_acquire(music_beeper->model_mutex, FuriWaitForever) == FuriStatusOk); + + for(size_t i = 0; i < MUSIC_BEEPER_SEMITONE_HISTORY_SIZE - 1; i++) { + size_t r = MUSIC_BEEPER_SEMITONE_HISTORY_SIZE - 1 - i; + music_beeper->model->duration_history[r] = music_beeper->model->duration_history[r - 1]; + music_beeper->model->semitone_history[r] = music_beeper->model->semitone_history[r - 1]; + } + + semitone = (semitone == 0xFF) ? 0xFF : semitone % 12; + + music_beeper->model->semitone = semitone; + music_beeper->model->dots = dots; + music_beeper->model->duration = duration; + music_beeper->model->position = position; + + music_beeper->model->semitone_history[0] = semitone; + music_beeper->model->duration_history[0] = duration; + + furi_mutex_release(music_beeper->model_mutex); + view_port_update(music_beeper->view_port); +} + +void music_beeper_clear(MusicBeeper* instance) { + memset(instance->model->duration_history, 0xff, MUSIC_BEEPER_SEMITONE_HISTORY_SIZE); + memset(instance->model->semitone_history, 0xff, MUSIC_BEEPER_SEMITONE_HISTORY_SIZE); + music_beeper_worker_clear(instance->worker); +} + +MusicBeeper* music_beeper_alloc() { + MusicBeeper* instance = malloc(sizeof(MusicBeeper)); + + instance->model = malloc(sizeof(MusicBeeperModel)); + instance->model->volume = 4; + + instance->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal); + + instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + instance->worker = music_beeper_worker_alloc(); + music_beeper_worker_set_volume( + instance->worker, MUSIC_BEEPER_VOLUMES[instance->model->volume]); + music_beeper_worker_set_callback(instance->worker, music_beeper_worker_callback, instance); + + music_beeper_clear(instance); + + 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); + + // Open GUI and register view_port + instance->gui = furi_record_open(RECORD_GUI); + gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen); + + return instance; +} + +void music_beeper_free(MusicBeeper* instance) { + gui_remove_view_port(instance->gui, instance->view_port); + furi_record_close(RECORD_GUI); + view_port_free(instance->view_port); + + music_beeper_worker_free(instance->worker); + + furi_message_queue_free(instance->input_queue); + + furi_mutex_free(instance->model_mutex); + + free(instance->model); + free(instance); +} + +int32_t music_beeper_app(void* p) { + MusicBeeper* music_beeper = music_beeper_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, MUSIC_BEEPER_APP_PATH_FOLDER); + + DialogsFileBrowserOptions browser_options; + dialog_file_browser_set_basic_options( + &browser_options, MUSIC_BEEPER_APP_EXTENSION, &I_music_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_E(TAG, "No file selected"); + break; + } + } + + if(!music_beeper_worker_load(music_beeper->worker, furi_string_get_cstr(file_path))) { + FURI_LOG_E(TAG, "Unable to load file"); + break; + } + + music_beeper_worker_start(music_beeper->worker); + + InputEvent input; + while(furi_message_queue_get(music_beeper->input_queue, &input, FuriWaitForever) == + FuriStatusOk) { + furi_check( + furi_mutex_acquire(music_beeper->model_mutex, FuriWaitForever) == FuriStatusOk); + + if(input.key == InputKeyBack) { + furi_mutex_release(music_beeper->model_mutex); + break; + } else if(input.key == InputKeyUp) { + if(music_beeper->model->volume < COUNT_OF(MUSIC_BEEPER_VOLUMES) - 1) + music_beeper->model->volume++; + music_beeper_worker_set_volume( + music_beeper->worker, MUSIC_BEEPER_VOLUMES[music_beeper->model->volume]); + } else if(input.key == InputKeyDown) { + if(music_beeper->model->volume > 0) music_beeper->model->volume--; + music_beeper_worker_set_volume( + music_beeper->worker, MUSIC_BEEPER_VOLUMES[music_beeper->model->volume]); + } + + furi_mutex_release(music_beeper->model_mutex); + view_port_update(music_beeper->view_port); + } + + music_beeper_worker_stop(music_beeper->worker); + if(p && strlen(p)) break; // Exit instead of going to browser if launched with arg + music_beeper_clear(music_beeper); + } while(1); + + furi_string_free(file_path); + music_beeper_free(music_beeper); + + return 0; +} diff --git a/applications/plugins/music_beeper/music_beeper_cli.c b/applications/plugins/music_beeper/music_beeper_cli.c new file mode 100644 index 000000000..26299fa64 --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper_cli.c @@ -0,0 +1,48 @@ +#include +#include +#include +#include "music_beeper_worker.h" + +static void music_beeper_cli(Cli* cli, FuriString* args, void* context) { + UNUSED(context); + MusicBeeperWorker* music_beeper_worker = music_beeper_worker_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + + do { + if(storage_common_stat(storage, furi_string_get_cstr(args), NULL) == FSE_OK) { + if(!music_beeper_worker_load(music_beeper_worker, furi_string_get_cstr(args))) { + printf("Failed to open file %s\r\n", furi_string_get_cstr(args)); + break; + } + } else { + if(!music_beeper_worker_load_rtttl_from_string( + music_beeper_worker, furi_string_get_cstr(args))) { + printf("Argument is not a file or RTTTL\r\n"); + break; + } + } + + printf("Press CTRL+C to stop\r\n"); + music_beeper_worker_set_volume(music_beeper_worker, 1.0f); + music_beeper_worker_start(music_beeper_worker); + while(!cli_cmd_interrupt_received(cli)) { + furi_delay_ms(50); + } + music_beeper_worker_stop(music_beeper_worker); + } while(0); + + furi_record_close(RECORD_STORAGE); + music_beeper_worker_free(music_beeper_worker); +} + +void music_beeper_on_system_start() { +#ifdef SRV_CLI + Cli* cli = furi_record_open(RECORD_CLI); + + cli_add_command(cli, "music_beeper", CliCommandFlagDefault, music_beeper_cli, NULL); + + furi_record_close(RECORD_CLI); +#else + UNUSED(music_beeper_cli); +#endif +} diff --git a/applications/plugins/music_beeper/music_beeper_worker.c b/applications/plugins/music_beeper/music_beeper_worker.c new file mode 100644 index 000000000..711ac1b2e --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper_worker.c @@ -0,0 +1,506 @@ +#include "music_beeper_worker.h" + +#include +#include + +#include +#include + +#include + +#define TAG "MusicBeeperWorker" + +#define MUSIC_BEEPER_FILETYPE "Flipper Music Format" +#define MUSIC_BEEPER_VERSION 0 + +#define SEMITONE_PAUSE 0xFF + +#define NOTE_C4 261.63f +#define NOTE_C4_SEMITONE (4.0f * 12.0f) +#define TWO_POW_TWELTH_ROOT 1.059463094359f + +typedef struct { + uint8_t semitone; + uint8_t duration; + uint8_t dots; +} NoteBlock; + +ARRAY_DEF(NoteBlockArray, NoteBlock, M_POD_OPLIST); + +struct MusicBeeperWorker { + FuriThread* thread; + bool should_work; + + MusicBeeperWorkerCallback callback; + void* callback_context; + + float volume; + uint32_t bpm; + uint32_t duration; + uint32_t octave; + NoteBlockArray_t notes; +}; + +static int32_t music_beeper_worker_thread_callback(void* context) { + furi_assert(context); + MusicBeeperWorker* instance = context; + + NoteBlockArray_it_t it; + NoteBlockArray_it(it, instance->notes); + + while(instance->should_work) { + if(NoteBlockArray_end_p(it)) { + NoteBlockArray_it(it, instance->notes); + furi_delay_ms(10); + } else { + NoteBlock* note_block = NoteBlockArray_ref(it); + + float note_from_a4 = (float)note_block->semitone - NOTE_C4_SEMITONE; + float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4); + float duration = + 60.0 * furi_kernel_get_tick_frequency() * 4 / instance->bpm / note_block->duration; + uint32_t dots = note_block->dots; + while(dots > 0) { + duration += duration / 2; + dots--; + } + uint32_t next_tick = furi_get_tick() + duration; + float volume = instance->volume; + + if(instance->callback) { + instance->callback( + note_block->semitone, + note_block->dots, + note_block->duration, + 0.0, + instance->callback_context); + } + + furi_hal_speaker_stop(); + furi_hal_speaker_start(frequency, volume); + while(instance->should_work && furi_get_tick() < next_tick) { + volume *= 1; + furi_hal_speaker_set_volume(volume); + furi_delay_ms(2); + } + NoteBlockArray_next(it); + } + } + + furi_hal_speaker_stop(); + + return 0; +} + +MusicBeeperWorker* music_beeper_worker_alloc() { + MusicBeeperWorker* instance = malloc(sizeof(MusicBeeperWorker)); + + NoteBlockArray_init(instance->notes); + + instance->thread = furi_thread_alloc(); + furi_thread_set_name(instance->thread, "MusicBeeperWorker"); + furi_thread_set_stack_size(instance->thread, 1024); + furi_thread_set_context(instance->thread, instance); + furi_thread_set_callback(instance->thread, music_beeper_worker_thread_callback); + + instance->volume = 1.0f; + + return instance; +} + +void music_beeper_worker_clear(MusicBeeperWorker* instance) { + NoteBlockArray_reset(instance->notes); +} + +void music_beeper_worker_free(MusicBeeperWorker* instance) { + furi_assert(instance); + furi_thread_free(instance->thread); + NoteBlockArray_clear(instance->notes); + free(instance); +} + +static bool is_digit(const char c) { + return isdigit(c) != 0; +} + +static bool is_letter(const char c) { + return islower(c) != 0 || isupper(c) != 0; +} + +static bool is_space(const char c) { + return c == ' ' || c == '\t'; +} + +static size_t extract_number(const char* string, uint32_t* number) { + size_t ret = 0; + *number = 0; + while(is_digit(*string)) { + *number *= 10; + *number += (*string - '0'); + string++; + ret++; + } + return ret; +} + +static size_t extract_dots(const char* string, uint32_t* number) { + size_t ret = 0; + *number = 0; + while(*string == '.') { + *number += 1; + string++; + ret++; + } + return ret; +} + +static size_t extract_char(const char* string, char* symbol) { + if(is_letter(*string)) { + *symbol = *string; + return 1; + } else { + return 0; + } +} + +static size_t extract_sharp(const char* string, char* symbol) { + if(*string == '#' || *string == '_') { + *symbol = '#'; + return 1; + } else { + return 0; + } +} + +static size_t skip_till(const char* string, const char symbol) { + size_t ret = 0; + while(*string != '\0' && *string != symbol) { + string++; + ret++; + } + if(*string != symbol) { + ret = 0; + } + return ret; +} + +static bool music_beeper_worker_add_note( + MusicBeeperWorker* instance, + uint8_t semitone, + uint8_t duration, + uint8_t dots) { + NoteBlock note_block; + + note_block.semitone = semitone; + note_block.duration = duration; + note_block.dots = dots; + + NoteBlockArray_push_back(instance->notes, note_block); + + return true; +} + +static int8_t note_to_semitone(const char note) { + switch(note) { + case 'C': + return 0; + // C# + case 'D': + return 2; + // D# + case 'E': + return 4; + case 'F': + return 5; + // F# + case 'G': + return 7; + // G# + case 'A': + return 9; + // A# + case 'B': + return 11; + default: + return 0; + } +} + +static bool music_beeper_worker_parse_notes(MusicBeeperWorker* instance, const char* string) { + const char* cursor = string; + bool result = true; + + while(*cursor != '\0') { + if(!is_space(*cursor)) { + uint32_t duration = 0; + char note_char = '\0'; + char sharp_char = '\0'; + uint32_t octave = 0; + uint32_t dots = 0; + + // Parsing + cursor += extract_number(cursor, &duration); + cursor += extract_char(cursor, ¬e_char); + cursor += extract_sharp(cursor, &sharp_char); + cursor += extract_number(cursor, &octave); + cursor += extract_dots(cursor, &dots); + + // Post processing + note_char = toupper(note_char); + if(!duration) { + duration = instance->duration; + } + if(!octave) { + octave = instance->octave; + } + + // Validation + bool is_valid = true; + is_valid &= (duration >= 1 && duration <= 128); + is_valid &= ((note_char >= 'A' && note_char <= 'G') || note_char == 'P'); + is_valid &= (sharp_char == '#' || sharp_char == '\0'); + is_valid &= (octave <= 16); + is_valid &= (dots <= 16); + if(!is_valid) { + FURI_LOG_E( + TAG, + "Invalid note: %lu%c%c%lu.%lu", + duration, + note_char == '\0' ? '_' : note_char, + sharp_char == '\0' ? '_' : sharp_char, + octave, + dots); + result = false; + break; + } + + // Note to semitones + uint8_t semitone = 0; + if(note_char == 'P') { + semitone = SEMITONE_PAUSE; + } else { + semitone += octave * 12; + semitone += note_to_semitone(note_char); + semitone += sharp_char == '#' ? 1 : 0; + } + + if(music_beeper_worker_add_note(instance, semitone, duration, dots)) { + FURI_LOG_D( + TAG, + "Added note: %c%c%lu.%lu = %u %lu", + note_char == '\0' ? '_' : note_char, + sharp_char == '\0' ? '_' : sharp_char, + octave, + dots, + semitone, + duration); + } else { + FURI_LOG_E( + TAG, + "Invalid note: %c%c%lu.%lu = %u %lu", + note_char == '\0' ? '_' : note_char, + sharp_char == '\0' ? '_' : sharp_char, + octave, + dots, + semitone, + duration); + } + cursor += skip_till(cursor, ','); + } + + if(*cursor != '\0') cursor++; + } + + return result; +} + +bool music_beeper_worker_load(MusicBeeperWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool ret = false; + if(strcasestr(file_path, ".fmf")) { + ret = music_beeper_worker_load_fmf_from_file(instance, file_path); + } else { + ret = music_beeper_worker_load_rtttl_from_file(instance, file_path); + } + return ret; +} + +bool music_beeper_worker_load_fmf_from_file(MusicBeeperWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool result = false; + FuriString* temp_str; + temp_str = furi_string_alloc(); + + Storage* storage = furi_record_open(RECORD_STORAGE); + FlipperFormat* file = flipper_format_file_alloc(storage); + + do { + if(!flipper_format_file_open_existing(file, file_path)) break; + + uint32_t version = 0; + if(!flipper_format_read_header(file, temp_str, &version)) break; + if(furi_string_cmp_str(temp_str, MUSIC_BEEPER_FILETYPE) || + (version != MUSIC_BEEPER_VERSION)) { + FURI_LOG_E(TAG, "Incorrect file format or version"); + break; + } + + if(!flipper_format_read_uint32(file, "BPM", &instance->bpm, 1)) { + FURI_LOG_E(TAG, "BPM is missing"); + break; + } + if(!flipper_format_read_uint32(file, "Duration", &instance->duration, 1)) { + FURI_LOG_E(TAG, "Duration is missing"); + break; + } + if(!flipper_format_read_uint32(file, "Octave", &instance->octave, 1)) { + FURI_LOG_E(TAG, "Octave is missing"); + break; + } + + if(!flipper_format_read_string(file, "Notes", temp_str)) { + FURI_LOG_E(TAG, "Notes is missing"); + break; + } + + if(!music_beeper_worker_parse_notes(instance, furi_string_get_cstr(temp_str))) { + break; + } + + result = true; + } while(false); + + furi_record_close(RECORD_STORAGE); + flipper_format_free(file); + furi_string_free(temp_str); + + return result; +} + +bool music_beeper_worker_load_rtttl_from_file(MusicBeeperWorker* instance, const char* file_path) { + furi_assert(instance); + furi_assert(file_path); + + bool result = false; + FuriString* content; + content = furi_string_alloc(); + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + + do { + if(!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_E(TAG, "Unable to open file"); + break; + }; + + uint16_t ret = 0; + do { + uint8_t buffer[65] = {0}; + ret = storage_file_read(file, buffer, sizeof(buffer) - 1); + for(size_t i = 0; i < ret; i++) { + furi_string_push_back(content, buffer[i]); + } + } while(ret > 0); + + furi_string_trim(content); + if(!furi_string_size(content)) { + FURI_LOG_E(TAG, "Empty file"); + break; + } + + if(!music_beeper_worker_load_rtttl_from_string(instance, furi_string_get_cstr(content))) { + FURI_LOG_E(TAG, "Invalid file content"); + break; + } + + result = true; + } while(0); + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + furi_string_free(content); + + return result; +} + +bool music_beeper_worker_load_rtttl_from_string(MusicBeeperWorker* instance, const char* string) { + furi_assert(instance); + + const char* cursor = string; + + // Skip name + cursor += skip_till(cursor, ':'); + if(*cursor != ':') { + return false; + } + + // Duration + cursor += skip_till(cursor, '='); + if(*cursor != '=') { + return false; + } + cursor++; + cursor += extract_number(cursor, &instance->duration); + + // Octave + cursor += skip_till(cursor, '='); + if(*cursor != '=') { + return false; + } + cursor++; + cursor += extract_number(cursor, &instance->octave); + + // BPM + cursor += skip_till(cursor, '='); + if(*cursor != '=') { + return false; + } + cursor++; + cursor += extract_number(cursor, &instance->bpm); + + // Notes + cursor += skip_till(cursor, ':'); + if(*cursor != ':') { + return false; + } + cursor++; + if(!music_beeper_worker_parse_notes(instance, cursor)) { + return false; + } + + return true; +} + +void music_beeper_worker_set_callback( + MusicBeeperWorker* instance, + MusicBeeperWorkerCallback callback, + void* context) { + furi_assert(instance); + instance->callback = callback; + instance->callback_context = context; +} + +void music_beeper_worker_set_volume(MusicBeeperWorker* instance, float volume) { + furi_assert(instance); + instance->volume = volume; +} + +void music_beeper_worker_start(MusicBeeperWorker* instance) { + furi_assert(instance); + furi_assert(instance->should_work == false); + + instance->should_work = true; + furi_thread_start(instance->thread); +} + +void music_beeper_worker_stop(MusicBeeperWorker* instance) { + furi_assert(instance); + furi_assert(instance->should_work == true); + + instance->should_work = false; + furi_thread_join(instance->thread); +} diff --git a/applications/plugins/music_beeper/music_beeper_worker.h b/applications/plugins/music_beeper/music_beeper_worker.h new file mode 100644 index 000000000..bc30abf81 --- /dev/null +++ b/applications/plugins/music_beeper/music_beeper_worker.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*MusicBeeperWorkerCallback)( + uint8_t semitone, + uint8_t dots, + uint8_t duration, + float position, + void* context); + +typedef struct MusicBeeperWorker MusicBeeperWorker; + +MusicBeeperWorker* music_beeper_worker_alloc(); + +void music_beeper_worker_clear(MusicBeeperWorker* instance); + +void music_beeper_worker_free(MusicBeeperWorker* instance); + +bool music_beeper_worker_load(MusicBeeperWorker* instance, const char* file_path); + +bool music_beeper_worker_load_fmf_from_file(MusicBeeperWorker* instance, const char* file_path); + +bool music_beeper_worker_load_rtttl_from_file(MusicBeeperWorker* instance, const char* file_path); + +bool music_beeper_worker_load_rtttl_from_string(MusicBeeperWorker* instance, const char* string); + +void music_beeper_worker_set_callback( + MusicBeeperWorker* instance, + MusicBeeperWorkerCallback callback, + void* context); + +void music_beeper_worker_set_volume(MusicBeeperWorker* instance, float volume); + +void music_beeper_worker_start(MusicBeeperWorker* instance); + +void music_beeper_worker_stop(MusicBeeperWorker* instance); + +#ifdef __cplusplus +} +#endif From 607e46e757535a75489b4ca78acdd10cbb5f7fe0 Mon Sep 17 00:00:00 2001 From: Haseo Date: Thu, 13 Oct 2022 21:17:26 +0200 Subject: [PATCH 2/2] Add Blackjack --- .../plugins/blackjack/application.fam | 12 + applications/plugins/blackjack/blackjack.c | 372 ++++++++++++++++++ .../plugins/blackjack/blackjack_10px.png | Bin 0 -> 119 bytes applications/plugins/blackjack/card.c | 239 +++++++++++ applications/plugins/blackjack/card.h | 40 ++ applications/plugins/blackjack/defines.h | 61 +++ applications/plugins/blackjack/ui.c | 127 ++++++ applications/plugins/blackjack/ui.h | 20 + applications/plugins/blackjack/util.c | 65 +++ applications/plugins/blackjack/util.h | 18 + 10 files changed, 954 insertions(+) create mode 100644 applications/plugins/blackjack/application.fam create mode 100644 applications/plugins/blackjack/blackjack.c create mode 100644 applications/plugins/blackjack/blackjack_10px.png create mode 100644 applications/plugins/blackjack/card.c create mode 100644 applications/plugins/blackjack/card.h create mode 100644 applications/plugins/blackjack/defines.h create mode 100644 applications/plugins/blackjack/ui.c create mode 100644 applications/plugins/blackjack/ui.h create mode 100644 applications/plugins/blackjack/util.c create mode 100644 applications/plugins/blackjack/util.h diff --git a/applications/plugins/blackjack/application.fam b/applications/plugins/blackjack/application.fam new file mode 100644 index 000000000..798e240e3 --- /dev/null +++ b/applications/plugins/blackjack/application.fam @@ -0,0 +1,12 @@ +App( + appid="blackjack", + name="Blackjack", + apptype=FlipperAppType.EXTERNAL, + entry_point="blackjack_app", + cdefines=["APP_BLACKJACK"], + requires=["gui"], + stack_size=1 * 1024, + order=30, + fap_icon="blackjack_10px.png", + fap_category="Games", +) \ No newline at end of file diff --git a/applications/plugins/blackjack/blackjack.c b/applications/plugins/blackjack/blackjack.c new file mode 100644 index 000000000..798fa6e5a --- /dev/null +++ b/applications/plugins/blackjack/blackjack.c @@ -0,0 +1,372 @@ + +#include +#include +#include +#include + +#include "defines.h" +#include "card.h" +#include "util.h" +#include "ui.h" + +#define APP_NAME "Blackjack" +#define STARTING_MONEY 200 +#define DEALER_MAX 17 + + +void start_round(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, handCount(game_state->player_cards, game_state->player_card_count)); + + if (!game_state->animating && game_state->state == GameStatePlay) { + draw_play_menu(canvas, game_state); + } +} + + +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); + + switch (game_state->state) { + case GameStateStart: + case GameStateGameOver: + draw_message_scene(canvas, game_state); + break; + case GameStatePlay: + draw_player_scene(canvas, game_state); + break; + case GameStateDealer: + draw_dealer_scene(canvas, game_state); + break; + } + if (game_state->state != GameStateStart && game_state->state != GameStateGameOver) { + animateQueue(game_state, canvas); + draw_ui(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; +} + +char *letters[4] = {"spade", "hearth", "diamond", "club"}; + +void drawPlayerCard(GameState *game_state) { + Card c = draw_card(game_state); + game_state->player_cards[game_state->player_card_count] = c; + game_state->player_card_count++; +} + +void drawDealerCard(GameState *game_state) { + Card c = draw_card(game_state); + game_state->dealer_cards[game_state->dealer_card_count] = c; + game_state->dealer_card_count++; + FURI_LOG_D(APP_NAME, "drawing dealer %s %i", letters[c.pip], c.character + 2); +} +//endregion + +//region queue callbacks +void to_lose_state(const GameState *game_state, Canvas *const canvas) { + UNUSED(game_state); + popupFrame(canvas); + elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "You lost"); +} + +void to_bust_state(const GameState *game_state, Canvas *const canvas) { + UNUSED(game_state); + popupFrame(canvas); + elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Busted!"); +} + +void to_draw_state(const GameState *game_state, Canvas *const canvas) { + UNUSED(game_state); + popupFrame(canvas); + elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Draw"); +} + +void to_dealer_turn(const GameState *game_state, Canvas *const canvas) { + UNUSED(game_state); + popupFrame(canvas); + elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Dealers turn"); +} + +void to_win_state(const GameState *game_state, Canvas *const canvas) { + UNUSED(game_state); + popupFrame(canvas); + elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "You win"); +} + +void to_start(const GameState *game_state, Canvas *const canvas) { + UNUSED(game_state); + popupFrame(canvas); + elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Round started"); +} + +void before_start(GameState *gameState) { + gameState->dealer_card_count = 0; + gameState->player_card_count = 0; +} + + +void start(GameState *game_state) { + start_round(game_state); +} + +void draw(GameState *game_state) { + game_state->player_score += game_state->bet; + game_state->bet = 0; + queue(game_state, start, before_start, to_start); +} + +void game_over(GameState *game_state) { + game_state->state = GameStateGameOver; +} + +void lose(GameState *game_state) { + game_state->state = GameStatePlay; + game_state->bet = 0; + if (game_state->player_score >= ROUND_PRICE) + queue(game_state, start, before_start, to_start); + else + queue(game_state, game_over, NULL, NULL); +} + +void win(GameState *game_state) { + game_state->state = GameStatePlay; + game_state->player_score += game_state->bet * 2; + game_state->bet = 0; + queue(game_state, start, before_start, to_start); +} + + +void dealerTurn(GameState *game_state) { + game_state->state = GameStateDealer; +} +//endregion + +void player_tick(GameState *game_state) { + uint8_t score = handCount(game_state->player_cards, game_state->player_card_count); + if ((game_state->doubled && score <= 21) || score == 21) { + queue(game_state, dealerTurn, NULL, NULL); + } else if (score > 21) { + queue(game_state, lose, NULL, to_bust_state); + } else { + if (game_state->selectDirection == DirectionUp && game_state->selectedMenu > 0) { + game_state->selectedMenu--; + } + if (game_state->selectDirection == DirectionDown && game_state->selectedMenu < 2) { + game_state->selectedMenu++; + } + if (game_state->selectDirection == Select) { + //double + if (!game_state->doubled && game_state->selectedMenu == 0 && + game_state->player_score >= ROUND_PRICE) { + + game_state->player_score -= ROUND_PRICE; + game_state->bet += ROUND_PRICE; + game_state->doubled = true; + game_state->selectedMenu = 1; + queue(game_state, drawPlayerCard, NULL, draw_card_animation); + game_state->player_cards[game_state->player_card_count] = game_state->deck.cards[game_state->deck.index]; + score = handCount(game_state->player_cards, game_state->player_card_count + 1); + if (score > 21) + queue(game_state, lose, NULL, to_bust_state); + else + queue(game_state, dealerTurn, NULL, to_dealer_turn); + + } //hit + else if (game_state->selectedMenu == 1) { + queue(game_state, drawPlayerCard, NULL, draw_card_animation); + } //stay + else if (game_state->selectedMenu == 2) { + queue(game_state, dealerTurn, NULL, to_dealer_turn); + } + } + } +} + + +void dealer_tick(GameState *game_state) { + uint8_t dealer_score = handCount(game_state->dealer_cards, game_state->dealer_card_count); + uint8_t player_score = handCount(game_state->player_cards, game_state->player_card_count); + + if (dealer_score >= DEALER_MAX) { + if (dealer_score > 21 || dealer_score < player_score) + queue(game_state, win, NULL, to_win_state); + else if (dealer_score > player_score) + queue(game_state, lose, NULL, to_lose_state); + else if (dealer_score == player_score) + queue(game_state, draw, NULL, to_draw_state); + } else { + queue(game_state, drawDealerCard, NULL, draw_card_animation); + } +} + +void tick(GameState *game_state) { + game_state->last_tick = furi_get_tick(); + + if (!game_state->started && game_state->state == GameStatePlay) { + game_state->started = true; + drawDealerCard(game_state); + queue(game_state, drawPlayerCard, NULL, draw_card_animation); + queue(game_state, drawDealerCard, NULL, draw_card_animation); + queue(game_state, drawPlayerCard, NULL, draw_card_animation); + } + + if (!run_queue(game_state)) { + if (game_state->state == GameStatePlay) { + player_tick(game_state); + } else if (game_state->state == GameStateDealer) { + dealer_tick(game_state); + } + } + + game_state->selectDirection = None; + +} + +void start_round(GameState *game_state) { + game_state->player_card_count = 0; + game_state->dealer_card_count = 0; + game_state->selectedMenu = 0; + game_state->started = false; + game_state->doubled = false; + game_state->animating = true; + game_state->animationStart = 0; + shuffleDeck(&(game_state->deck)); + game_state->doubled = false; + game_state->bet = ROUND_PRICE; + if (game_state->player_score < ROUND_PRICE) { + game_state->state = GameStateGameOver; + } else { + game_state->player_score -= ROUND_PRICE; + } + game_state->state = GameStatePlay; +} + +void init(GameState *game_state) { + game_state->last_tick = 0; + game_state->player_score = STARTING_MONEY; + generateDeck(&(game_state->deck)); + 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); +} + +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)); + init(game_state); + 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 *game_state = (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: + game_state->selectDirection = DirectionUp; + break; + case InputKeyDown: + game_state->selectDirection = DirectionDown; + break; + case InputKeyRight: + game_state->selectDirection = DirectionRight; + break; + case InputKeyLeft: + game_state->selectDirection = DirectionLeft; + break; + case InputKeyBack: + processing = false; + break; + + case InputKeyOk: + if (game_state->state == GameStateGameOver || game_state->state == GameStateStart) { + init(game_state); + } else { + game_state->selectDirection = Select; + } + break; + } + } + } else if (event.type == EventTypeTick) { + tick(game_state); + } + } else { + FURI_LOG_D(APP_NAME, "osMessageQueue: event timeout"); + // event timeout + } + view_port_update(view_port); + release_mutex(&state_mutex, game_state); + } + + + 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: + queue_clear(); + 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 0000000000000000000000000000000000000000..7382d6358bde65d970575fb40995145d5d3e26d0 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2VGmzZ%#=aj&F%}28J29*~C-V}>VGHmHasB`Q zKad%E=yDy9lJ|6R4B?oWoZ!IP)59UwbM^#Bva!*jn+{A(9T(Wx892Nd)DK8X1_G5b Nc)I$ztaD0e0sxqvA7ual literal 0 HcmV?d00001 diff --git a/applications/plugins/blackjack/card.c b/applications/plugins/blackjack/card.c new file mode 100644 index 000000000..2f43a33f7 --- /dev/null +++ b/applications/plugins/blackjack/card.c @@ -0,0 +1,239 @@ +#include "card.h" +#include + +//region CardDesign +bool pips[4][49] = + { + { + //spades + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 1, 0, 1, 1, + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 1, 1, 1, 0, 0 + }, + { + //hearts + 0, 1, 0, 0, 0, 1, 0, + 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 1, 1, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 0, 1, 0, 0, 0, + }, + { + //diamonds + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, + 0, 1, 1, 1, 1, 1, 0, + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 0, 1, 0, 0, 0 + }, + { + //clubs + 0, 0, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 0, 0, + 1, 1, 0, 1, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, + 1, 1, 0, 1, 0, 1, 1, + 0, 0, 0, 1, 0, 0, 0, + 0, 0, 1, 1, 1, 0, 0 + } + }; + +bool backDesign[4] = { + 0, 1, + 1, 0 +}; +//endregion + +uint8_t characters[13] = + { + 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K', 'A' + }; + + +//region Player card positions +uint8_t playerCardPositions[22][4] = { + //first row + {108, 38, 0, 0}, + {98, 38, 0, 1}, + {88, 38, 0, 1}, + {78, 38, 0, 1}, + {68, 38, 0, 1}, + {58, 38, 0, 1}, + {48, 38, 0, 1}, + {38, 38, 0, 1}, + //second row + {104, 26, 1, 0}, + {94, 26, 1, 1}, + {84, 26, 1, 1}, + {74, 26, 1, 1}, + {64, 26, 1, 1}, + {54, 26, 1, 1}, + {44, 26, 1, 1}, + //third row + {99, 14, 1, 0}, + {89, 14, 1, 1}, + {79, 14, 1, 1}, + {69, 14, 1, 1}, + {59, 14, 1, 1}, + {49, 14, 1, 1}, +}; +//endregion + + +void drawPlayerDeck(const Card cards[21], uint8_t count, Canvas *const canvas) { + for (uint8_t i = 0; i < count; i++) { + CardState state = Normal; + if (playerCardPositions[i][2] == 1 && playerCardPositions[i][3] == 1) + state = BottomAndRightCut; + else if (playerCardPositions[i][3] == 1) + state = RightCut; + else if (playerCardPositions[i][2] == 1) + state = BottomCut; + drawCardAt(playerCardPositions[i][0], playerCardPositions[i][1], cards[i].pip, cards[i].character, state, + canvas); + } +} + +void drawCardAt(uint8_t pos_x, uint8_t pos_y, uint8_t pip, uint8_t character, CardState state, Canvas *const canvas) { + if (state == Normal) { + canvas_set_color(canvas, ColorWhite); + canvas_draw_box(canvas, pos_x, pos_y, CARD_WIDHT, CARD_HEIGHT); + + canvas_set_color(canvas, ColorBlack); + canvas_draw_frame(canvas, pos_x, pos_y, CARD_WIDHT, CARD_HEIGHT); + } else { + if (state == BottomCut || state == BottomAndRightCut) + canvas_draw_line(canvas, pos_x, pos_y, pos_x, pos_y + CARD_HALF_HEIGHT - 1); //half height line + + if (state == BottomCut) { + canvas_draw_line(canvas, pos_x, pos_y, pos_x + CARD_WIDHT - 1, pos_y); //full width line + canvas_draw_line(canvas, pos_x + CARD_WIDHT - 1, pos_y, pos_x + CARD_WIDHT - 1, + pos_y + CARD_HALF_HEIGHT - 1); //half height line + } + + if (state == BottomAndRightCut) { + canvas_draw_line(canvas, pos_x, pos_y, pos_x + CARD_HALF_WIDHT - 1, pos_y); //half width + } + + if (state == RightCut) { + canvas_draw_line(canvas, pos_x, pos_y, pos_x + CARD_HALF_WIDHT - 1, pos_y); //half width + canvas_draw_line(canvas, pos_x, pos_y, pos_x, pos_y + CARD_HEIGHT - 1); //full height line + canvas_draw_line(canvas, pos_x, pos_y + CARD_HEIGHT - 1, pos_x + CARD_HALF_WIDHT - 1, + pos_y + CARD_HEIGHT - 1); //full height line + } + + } + + uint8_t left = pos_x + CORNER_MARGIN; + uint8_t right = (pos_x + CARD_WIDHT - CORNER_MARGIN - 7); + uint8_t top = pos_y + CORNER_MARGIN; + uint8_t bottom = (pos_y + CARD_HEIGHT - CORNER_MARGIN - 7); + + for (uint8_t x = 0; x < 7; x++) { + for (uint8_t y = 0; y < 7; y++) { + if (pips[pip][x + y * 7]) { + if (state == Normal || state == BottomCut) + canvas_draw_dot(canvas, right + x + 1, top + y); + if (state == Normal || state == RightCut) + canvas_draw_dot(canvas, left + x - 1, bottom + y); + } + } + } + + canvas_set_font(canvas, FontSecondary); + char drawChar[3]; + if (character < 9) + snprintf(drawChar, sizeof(drawChar), "%i", character + 2); + else { + snprintf(drawChar, sizeof(drawChar), "%c", characters[character]); + } + + canvas_set_font_direction(canvas, CanvasDirectionLeftToRight); + canvas_draw_str_aligned(canvas, left + 2, top + 3, AlignCenter, AlignCenter, drawChar); + + canvas_set_font_direction(canvas, CanvasDirectionRightToLeft); + if (state == Normal) { //flipper crashes on non center aligned text when upside down + uint8_t margin = 9; + if (character == 8) //10 needs bigger margin + margin = 12; + canvas_draw_str_aligned(canvas, right + margin, bottom - 3, AlignCenter, AlignCenter, drawChar); + } + + canvas_set_font_direction(canvas, CanvasDirectionLeftToRight); + //canvas_draw_str(canvas, left, top, drawChar ); +} + +void drawCardBackAt(uint8_t pos_x, uint8_t pos_y, Canvas *const canvas) { + canvas_set_color(canvas, ColorWhite); + canvas_draw_box(canvas, pos_x, pos_y, CARD_WIDHT, CARD_HEIGHT); + + canvas_set_color(canvas, ColorBlack); + canvas_draw_frame(canvas, pos_x, pos_y, CARD_WIDHT, CARD_HEIGHT); + for (uint8_t x = 0; x < CARD_WIDHT - 2; x++) { + for (uint8_t y = 0; y < CARD_HEIGHT - 2; y++) { + uint8_t _x = x; + uint8_t _y = y * 2; + if (backDesign[(_x + _y) % 4]) { + canvas_draw_dot(canvas, pos_x + x + 1, pos_y + y + 1); + } + } + } +} + +void generateDeck(Deck *deck_ptr) { + uint16_t counter = 0; + 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 + }; + counter++; + } + } + } +} + +void shuffleDeck(Deck *deck_ptr) { + srand(DWT->CYCCNT); + deck_ptr->index = 0; + int max = 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 handCount(const Card cards[21], 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; +} \ No newline at end of file diff --git a/applications/plugins/blackjack/card.h b/applications/plugins/blackjack/card.h new file mode 100644 index 000000000..aa99784bb --- /dev/null +++ b/applications/plugins/blackjack/card.h @@ -0,0 +1,40 @@ +#ifndef _card_h +#define _card_h + +#include + +#define DECK_COUNT 6 +#define CARD_HEIGHT 24 +#define CARD_HALF_HEIGHT CARD_HEIGHT/2 +#define CARD_WIDHT 18 +#define CARD_HALF_WIDHT CARD_WIDHT/2 +#define CORNER_MARGIN 3 +#define LEGEND_SIZE 10 + +typedef enum { + Normal, BottomCut, RightCut, BottomAndRightCut, TopCut, LeftCut, TopAndLeftCut +} CardState; + +typedef struct { + uint8_t pip; + uint8_t character; +} Card; + +typedef struct { + Card cards[52 * DECK_COUNT]; + int index; +} Deck; + +void drawPlayerDeck(const Card cards[21], uint8_t count, Canvas *const canvas); + +void drawCardAt(uint8_t pos_x, uint8_t pos_y, uint8_t pip, uint8_t character, CardState state, Canvas *const canvas); + +void drawCardBackAt(uint8_t pos_x, uint8_t pos_y, Canvas *const canvas); + +void generateDeck(Deck *deck_ptr); + +void shuffleDeck(Deck *deck_ptr); + +uint8_t handCount(const Card cards[21], uint8_t count); + +#endif \ 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..36e547eb3 --- /dev/null +++ b/applications/plugins/blackjack/defines.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include "card.h" + +#define ANIMATION_TIME furi_ms_to_ticks(1500) +#define ANIMATION_END_MARGIN furi_ms_to_ticks(200) +#define ROUND_PRICE 10 + +typedef enum { + EventTypeTick, + EventTypeKey, +} EventType; + +typedef struct { + EventType type; + InputEvent input; +} AppEvent; + +typedef enum { + GameStateGameOver, + GameStateStart, + GameStatePlay, + GameStateDealer, +} PlayState; + +typedef enum { + DirectionUp, + DirectionRight, + DirectionDown, + DirectionLeft, + Select, + None +} Direction; + +typedef struct { + Card player_cards[21]; + Card dealer_cards[21]; + uint8_t player_card_count; + uint8_t dealer_card_count; + + Direction selectDirection; + + uint32_t player_score; + uint8_t multiplier; + uint32_t bet; + uint8_t player_time; + bool doubled; + bool animating; + bool started; + uint8_t selectedMenu; + Deck deck; + PlayState state; + unsigned int last_tick; + unsigned int animationStart; + bool dealer_animating; + unsigned int delay_tick; +} GameState; + diff --git a/applications/plugins/blackjack/ui.c b/applications/plugins/blackjack/ui.c new file mode 100644 index 000000000..ab6462473 --- /dev/null +++ b/applications/plugins/blackjack/ui.c @@ -0,0 +1,127 @@ +#include "ui.h" +#include "card.h" +#include +#include "util.h" + +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) + drawPlayerDeck((game_state->player_cards), max_card, canvas); + + drawCardBackAt(13, 5, canvas); + + max_card = game_state->dealer_card_count; + if (max_card > 1) { + drawCardAt(2, 2, game_state->dealer_cards[1].pip, game_state->dealer_cards[1].character, Normal, + canvas); + } +} + +void draw_dealer_scene(Canvas *const canvas, const GameState *game_state) { + uint8_t max_card = game_state->dealer_card_count; + drawPlayerDeck((game_state->dealer_cards), max_card, canvas); +} + +void draw_card_animation(const GameState *game_state, Canvas *const canvas) { + float t = (float) (furi_get_tick() - game_state->animationStart) / (ANIMATION_TIME - ANIMATION_END_MARGIN); + t *= 2; + Card animatingCard = game_state->deck.cards[game_state->deck.index]; + if (t > 1) { + int cardY = round(lerp(-CARD_HEIGHT, 10, 1)); + drawCardAt(64 - CARD_HALF_WIDHT, cardY, animatingCard.pip, + animatingCard.character, Normal, canvas); + } else { + int cardY = round(lerp(-CARD_HEIGHT, 10, t)); + drawCardAt(64 - CARD_HALF_WIDHT, cardY, animatingCard.pip, + animatingCard.character, Normal, canvas); +// drawCardBackAt(64 - CARD_HALF_WIDHT, cardY, canvas); + } +} + +void popupFrame(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_message_scene(Canvas *const canvas, const GameState *game_state) { + switch (game_state->state) { + case GameStateStart: + canvas_set_font(canvas, FontPrimary); + elements_multiline_text_aligned(canvas, 64, 5, AlignCenter, AlignTop, "Blackjack"); + canvas_set_font(canvas, FontSecondary); + elements_multiline_text_aligned(canvas, 64, 24, AlignCenter, AlignTop, "Made by Doofy"); + elements_multiline_text_aligned(canvas, 64, 38, AlignCenter, AlignTop, "Press center button\nto start"); + break; + case GameStateGameOver: + canvas_set_font(canvas, FontPrimary); + elements_multiline_text_aligned(canvas, 64, 5, AlignCenter, AlignTop, "Game Over"); + canvas_set_font(canvas, FontSecondary); + elements_multiline_text_aligned(canvas, 64, 24, AlignCenter, AlignTop, "Press center button\nto start"); + break; + default: + break; + } +} + +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 < 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_score(Canvas *const canvas, bool top, uint8_t amount) { + char drawChar[20]; + snprintf(drawChar, sizeof(drawChar), "Player score: %i", amount); + if (top) + canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, drawChar); + else + canvas_draw_str_aligned(canvas, 64, 62, AlignCenter, AlignBottom, drawChar); +} + +void draw_money(Canvas *const canvas, uint32_t score) { + canvas_set_font(canvas, FontSecondary); + char drawChar[10]; + uint32_t currAmount = score; + if (currAmount < 1000) { + snprintf(drawChar, sizeof(drawChar), "$%lu", currAmount); + } else { + char c = 'K'; + for (uint8_t i = 0; i < 4; i++) { + currAmount = currAmount / 1000; + if (currAmount < 1000) { + c = MoneyMul[i]; + break; + } + } + + snprintf(drawChar, sizeof(drawChar), "$%lu %c", currAmount, c); + } + canvas_draw_str_aligned(canvas, 126, 2, AlignRight, AlignTop, drawChar); +} \ 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..7a9499f99 --- /dev/null +++ b/applications/plugins/blackjack/ui.h @@ -0,0 +1,20 @@ +#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_message_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 draw_card_animation(const GameState *game_state, Canvas *const canvas); + +void popupFrame(Canvas *const canvas); \ No newline at end of file diff --git a/applications/plugins/blackjack/util.c b/applications/plugins/blackjack/util.c new file mode 100644 index 000000000..bc260eb54 --- /dev/null +++ b/applications/plugins/blackjack/util.c @@ -0,0 +1,65 @@ +#include "util.h" + +static List *afterDelay; + +float lerp(float v0, float v1, float t) { + return (1 - t) * v0 + t * v1; +} + +void queue(GameState *game_state, + void (*callback)(GameState *game_state), + void (*start)(GameState *game_state), + void (*processing)(const GameState *gameState, Canvas *const canvas) +) { + if (afterDelay == NULL) { + game_state->animationStart = game_state->last_tick; + afterDelay = malloc(sizeof(List)); + afterDelay->callback = callback; + afterDelay->processing = processing; + afterDelay->start = start; + afterDelay->next = NULL; + } else { + List *next = afterDelay; + while (next->next != NULL) { next = (List *) (next->next); } + next->next = malloc(sizeof(List)); + ((List *) next->next)->callback = callback; + ((List *) next->next)->processing = processing; + ((List *) next->next)->start = start; + ((List *) next->next)->next = NULL; + } +} + +void queue_clear() { + while (afterDelay != NULL) { + List *f = afterDelay; + free(f); + afterDelay = f->next; + } +} + +void dequeue(GameState *game_state) { + ((List *) afterDelay)->callback(game_state); + List *f = afterDelay; + afterDelay = (List *) afterDelay->next; + free(f); + if (afterDelay != NULL && afterDelay->start != NULL)afterDelay->start(game_state); + game_state->animationStart = game_state->last_tick; +} + +void animateQueue(const GameState *game_state, Canvas *const canvas) { + if (afterDelay != NULL && ((List *) afterDelay)->processing != NULL) { + ((List *) afterDelay)->processing(game_state, canvas); + } +} + +bool run_queue(GameState *game_state) { + if (afterDelay != NULL) { + game_state->animating = true; + if ((game_state->last_tick - game_state->animationStart) > ANIMATION_TIME) { + dequeue(game_state); + } + return true; + } + game_state->animating = false; + return false; +} diff --git a/applications/plugins/blackjack/util.h b/applications/plugins/blackjack/util.h new file mode 100644 index 000000000..9bd966740 --- /dev/null +++ b/applications/plugins/blackjack/util.h @@ -0,0 +1,18 @@ +#pragma once +#include "defines.h" + +typedef struct{ + void (*callback)(GameState *game_state); + void (*processing)(const GameState *game_state, Canvas *const canvas); + void (*start)(GameState *game_state); + void *next; +} List; + +float lerp(float v0, float v1, float t); +void queue(GameState *game_state, + void (*callback)(GameState *game_state), + void (*start)(GameState *game_state), + void (*processing)(const GameState *gameState, Canvas *const canvas)); +bool run_queue(GameState *gameState); +void animateQueue(const GameState *gameState, Canvas *const canvas); +void queue_clear(); \ No newline at end of file