diff --git a/applications/external/bomberduck/LICENSE b/applications/external/bomberduck/LICENSE new file mode 100644 index 000000000..bce361a99 --- /dev/null +++ b/applications/external/bomberduck/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/applications/external/bomberduck/README.md b/applications/external/bomberduck/README.md new file mode 100644 index 000000000..2d133145a --- /dev/null +++ b/applications/external/bomberduck/README.md @@ -0,0 +1,2 @@ +# flipperzero-bomberduck +Bomberman clone on flipper zero! diff --git a/applications/external/bomberduck/application.fam b/applications/external/bomberduck/application.fam new file mode 100644 index 000000000..afcd5a6ee --- /dev/null +++ b/applications/external/bomberduck/application.fam @@ -0,0 +1,15 @@ +App( + appid="bomberduck", + name="Bomberduck", + apptype=FlipperAppType.EXTERNAL, + entry_point="bomberduck_app", + cdefines=["BOMBERDUCK"], + requires=[ + "gui", + ], + stack_size=1 * 1024, + order=90, + fap_icon="bomb.png", + fap_category="Games", + fap_icon_assets="assets", +) diff --git a/applications/external/bomberduck/assets/bomb0.png b/applications/external/bomberduck/assets/bomb0.png new file mode 100644 index 000000000..3fdc3a3c1 Binary files /dev/null and b/applications/external/bomberduck/assets/bomb0.png differ diff --git a/applications/external/bomberduck/assets/bomb1.png b/applications/external/bomberduck/assets/bomb1.png new file mode 100644 index 000000000..11d05b9b7 Binary files /dev/null and b/applications/external/bomberduck/assets/bomb1.png differ diff --git a/applications/external/bomberduck/assets/bomb2.png b/applications/external/bomberduck/assets/bomb2.png new file mode 100644 index 000000000..38ce7c732 Binary files /dev/null and b/applications/external/bomberduck/assets/bomb2.png differ diff --git a/applications/external/bomberduck/assets/box.png b/applications/external/bomberduck/assets/box.png new file mode 100644 index 000000000..bbd352b6f Binary files /dev/null and b/applications/external/bomberduck/assets/box.png differ diff --git a/applications/external/bomberduck/assets/end.png b/applications/external/bomberduck/assets/end.png new file mode 100644 index 000000000..d634933b7 Binary files /dev/null and b/applications/external/bomberduck/assets/end.png differ diff --git a/applications/external/bomberduck/assets/enemy1.png b/applications/external/bomberduck/assets/enemy1.png new file mode 100644 index 000000000..7ee7cb27f Binary files /dev/null and b/applications/external/bomberduck/assets/enemy1.png differ diff --git a/applications/external/bomberduck/assets/enemyleft.png b/applications/external/bomberduck/assets/enemyleft.png new file mode 100644 index 000000000..bb85dfbb2 Binary files /dev/null and b/applications/external/bomberduck/assets/enemyleft.png differ diff --git a/applications/external/bomberduck/assets/enemyright.png b/applications/external/bomberduck/assets/enemyright.png new file mode 100644 index 000000000..45e6a861a Binary files /dev/null and b/applications/external/bomberduck/assets/enemyright.png differ diff --git a/applications/external/bomberduck/assets/explore.png b/applications/external/bomberduck/assets/explore.png new file mode 100644 index 000000000..5eb50b669 Binary files /dev/null and b/applications/external/bomberduck/assets/explore.png differ diff --git a/applications/external/bomberduck/assets/playerleft.png b/applications/external/bomberduck/assets/playerleft.png new file mode 100644 index 000000000..86997a985 Binary files /dev/null and b/applications/external/bomberduck/assets/playerleft.png differ diff --git a/applications/external/bomberduck/assets/playerright.png b/applications/external/bomberduck/assets/playerright.png new file mode 100644 index 000000000..1a6283d9c Binary files /dev/null and b/applications/external/bomberduck/assets/playerright.png differ diff --git a/applications/external/bomberduck/assets/unbreakbox.png b/applications/external/bomberduck/assets/unbreakbox.png new file mode 100644 index 000000000..5e65912d5 Binary files /dev/null and b/applications/external/bomberduck/assets/unbreakbox.png differ diff --git a/applications/external/bomberduck/bomb.png b/applications/external/bomberduck/bomb.png new file mode 100644 index 000000000..44b9bfdea Binary files /dev/null and b/applications/external/bomberduck/bomb.png differ diff --git a/applications/external/bomberduck/bomberduck.c b/applications/external/bomberduck/bomberduck.c new file mode 100644 index 000000000..7b8b5f14a --- /dev/null +++ b/applications/external/bomberduck/bomberduck.c @@ -0,0 +1,645 @@ +#include +#include + +#include +#include +#include +#include +#include +#include "bomberduck_icons.h" +#include + +int max(int a, int b) { + return (a > b) ? a : b; +} + +int min(int a, int b) { + return (a < b) ? a : b; +} + +#define WorldSizeX 12 +#define WorldSizeY 6 +#define BombRange 1 + + +typedef struct { + FuriMutex* mutex; +} BomberState; + +typedef struct { + int row; + int col; +} Cell; + +typedef struct { + Cell cells[WorldSizeY * WorldSizeX]; + int front; + int rear; +} Queue; + +void enqueue(Queue* q, Cell c) { + q->cells[q->rear] = c; + q->rear++; +} + +Cell dequeue(Queue* q) { + Cell c = q->cells[q->front]; + q->front++; + + return c; +} + +bool is_empty(Queue* q) { + return q->front == q->rear; +} + +typedef struct { + int x; + int y; + int planted; +} Bomb; + +typedef struct { + int x; + int y; + bool side; +} Player; + +typedef struct { + int x; + int y; + int last; + bool side; + int level; +} Enemy; + +typedef struct { + int matrix[WorldSizeY][WorldSizeX]; + Player* player; + bool running; + int level; + + Enemy enemies[10]; + int enemies_count; + + Bomb bombs[100]; + int bombs_count; + + int endx; + int endy; +} World; + +Player player = {0, 0, 1}; +World world = {{{0}}, &player, 1, 0, {}, 0, {}, 0, 0, 0}; +bool vibration = false; + +void init() { + player.x = 1; + player.y = 1; + + world.endx = 4 + rand() % 8; + world.endy = rand() % 6; + for(int i = 0; i < WorldSizeY; i++) { + for(int j = 0; j < WorldSizeX; j++) { + world.matrix[i][j] = rand() % 3; + } + } + world.running = 1; + world.bombs_count =0; + vibration = false; + for(int j = max(0, player.y - BombRange); j < min(WorldSizeY, player.y + BombRange + 1); j++) { + world.matrix[j][player.x] = 0; + } + + for(int j = max(0, player.x - BombRange); j < min(WorldSizeX, player.x + BombRange + 1); j++) { + world.matrix[player.y][j] = 0; + } + + world.enemies_count = 0; + for(int j = 0; j < rand() % 4 + world.level / 5; j++) { + Enemy enemy; + enemy.x = 4 + rand() % 7; + enemy.y = rand() % 6; + enemy.last = 0; + enemy.side = 1; + enemy.level = 0; + + world.enemies[j] = enemy; + world.enemies_count++; + + for(int m = max(0, world.enemies[j].y - BombRange); + m < min(WorldSizeY, world.enemies[j].y + BombRange + 1); + m++) { + world.matrix[m][world.enemies[j].x] = 0; + } + + for(int m = max(0, world.enemies[j].x - BombRange); + m < min(WorldSizeX, world.enemies[j].x + BombRange + 1); + m++) { + world.matrix[world.enemies[j].y][m] = 0; + } + } + world.matrix[world.endy][world.endx] = 1; +} + +const NotificationSequence end = { + &message_vibro_on, + + &message_note_ds4, + &message_delay_10, + &message_sound_off, + &message_delay_10, + + &message_note_ds4, + &message_delay_10, + &message_sound_off, + &message_delay_10, + + &message_note_ds4, + &message_delay_10, + &message_sound_off, + &message_delay_10, + + &message_vibro_off, + NULL, +}; + +static const NotificationSequence bomb2 = { + &message_vibro_on, + &message_delay_25, + &message_vibro_off, + NULL, +}; + +static const NotificationSequence bomb_explore = { + &message_vibro_on, + &message_delay_50, + &message_vibro_off, + NULL, +}; + +static const NotificationSequence vibr1 = { + &message_vibro_on, + &message_delay_10, + &message_vibro_off, + &message_delay_10, + &message_vibro_on, + &message_delay_10, + &message_vibro_off, + &message_delay_10, + + NULL, +}; + + +void intToStr(int num, char* str) { + int i = 0, sign = 0; + + if(num < 0) { + num = -num; + sign = 1; + } + + do { + str[i++] = num % 10 + '0'; + num /= 10; + } while(num > 0); + + if(sign) { + str[i++] = '-'; + } + + str[i] = '\0'; + + // Reverse the string + int j, len = i; + char temp; + for(j = 0; j < len / 2; j++) { + temp = str[j]; + str[j] = str[len - j - 1]; + str[len - j - 1] = temp; + } +} + +bool BFS() { + // Initialize visited array and queue + int visited[WorldSizeY][WorldSizeX] = {0}; + Queue q = {.front = 0, .rear = 0}; + // Mark the starting cell as visited and enqueue it + visited[world.player->y][world.player->x] = 1; + Cell startCell = {.row = world.player->y, .col = world.player->x}; + enqueue(&q, startCell); + // Traverse the field + while(!is_empty(&q)) { + // Dequeue a cell from the queue + Cell currentCell = dequeue(&q); + // Check if the current cell is the destination cell + if(currentCell.row == world.endy && currentCell.col == world.endx) { + return true; + } + // Check the neighboring cells + for(int rowOffset = -1; rowOffset <= 1; rowOffset++) { + for(int colOffset = -1; colOffset <= 1; colOffset++) { + // Skip diagonals and the current cell + if(rowOffset == 0 && colOffset == 0) { + continue; + } + if(rowOffset != 0 && colOffset != 0) { + continue; + } + // Calculate the row and column of the neighboring cell + int neighborRow = currentCell.row + rowOffset; + int neighborCol = currentCell.col + colOffset; + // Skip out-of-bounds cells and already visited cells + if(neighborRow < 0 || neighborRow >= WorldSizeY || neighborCol < 0 || + neighborCol >= WorldSizeX) { + continue; + } + if(visited[neighborRow][neighborCol]) { + continue; + } + // Mark the neighboring cell as visited and enqueue it + if(world.matrix[neighborRow][neighborCol] != 2) { + visited[neighborRow][neighborCol] = 1; + Cell neighborCell = {.row = neighborRow, .col = neighborCol}; + enqueue(&q, neighborCell); + } + } + } + } + return false; +} + +static void draw_callback(Canvas* canvas, void* ctx) { + furi_assert(ctx); + const BomberState* bomber_state = ctx; + + furi_mutex_acquire(bomber_state->mutex, FuriWaitForever); + if(!BFS()) { + init(); + } + canvas_clear(canvas); + + canvas_draw_icon(canvas, world.endx * 10 + 4, world.endy * 10 + 2, &I_end); + + if(world.running) { + for(size_t i = 0; i < WorldSizeY; i++) { + for(size_t j = 0; j < WorldSizeX; j++) { + switch(world.matrix[i][j]) { + case 0: + break; + case 1: + canvas_draw_icon(canvas, j * 10 + 4, i * 10 + 2, &I_box); + break; + case 2: + canvas_draw_icon(canvas, j * 10 + 4, i * 10 + 2, &I_unbreakbox); + break; + case 3: + canvas_draw_icon(canvas, j * 10 + 4, i * 10 + 2, &I_bomb0); + break; + case 4: + canvas_draw_icon(canvas, j * 10 + 4, i * 10 + 2, &I_bomb1); + break; + case 5: + canvas_draw_icon(canvas, j * 10 + 4, i * 10 + 2, &I_bomb2); + break; + case 6: + canvas_draw_icon(canvas, j * 10 + 4, i * 10 + 2, &I_explore); + world.matrix[i][j] = 0; + break; + } + } + } + + if(world.player->side) { + canvas_draw_icon( + canvas, world.player->x * 10 + 4, world.player->y * 10 + 2, &I_playerright); + } else { + canvas_draw_icon( + canvas, world.player->x * 10 + 4, world.player->y * 10 + 2, &I_playerleft); + } + + for(int i = 0; i < world.enemies_count; i++) { + if(world.enemies[i].level > 0) { + canvas_draw_icon( + canvas, world.enemies[i].x * 10 + 4, world.enemies[i].y * 10 + 2, &I_enemy1); + } else { + if(world.enemies[i].side) { + canvas_draw_icon( + canvas, + world.enemies[i].x * 10 + 4, + world.enemies[i].y * 10 + 2, + &I_enemyright); + } else { + canvas_draw_icon( + canvas, + world.enemies[i].x * 10 + 4, + world.enemies[i].y * 10 + 2, + &I_enemyleft); + } + } + } + } else { + canvas_set_font(canvas, FontPrimary); + if(world.player->x == world.endx && world.player->y == world.endy) { + if(world.level == 20) { + canvas_draw_str(canvas, 30, 35, "You win!"); + }else{ + canvas_draw_str(canvas, 30, 35, "Next level!"); + char str[20]; + intToStr(world.level, str); + canvas_draw_str(canvas, 90, 35, str); + } + + } else { + canvas_draw_str(canvas, 30, 35, "You died :("); + } + } + + furi_mutex_release(bomber_state->mutex); +} + +static void input_callback(InputEvent* input_event, void* ctx) { + // Проверяем, что контекст не нулевой + furi_assert(ctx); + FuriMessageQueue* event_queue = ctx; + + furi_message_queue_put(event_queue, input_event, FuriWaitForever); +} + +int32_t bomberduck_app(void* p) { + UNUSED(p); + + // Текущее событие типа InputEvent + InputEvent event; + // Очередь событий на 8 элементов размера InputEvent + FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + + BomberState* bomber_state = malloc(sizeof(BomberState)); + + bomber_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); // Alloc Mutex + if(!bomber_state->mutex) { + FURI_LOG_E("BomberDuck", "cannot create mutex\r\n"); + furi_message_queue_free(event_queue); + free(bomber_state); + return 255; + } + + DOLPHIN_DEED(DolphinDeedPluginGameStart); + // Создаем новый view port + ViewPort* view_port = view_port_alloc(); + // Создаем callback отрисовки, без контекста + view_port_draw_callback_set(view_port, draw_callback, bomber_state); + // Создаем callback нажатий на клавиши, в качестве контекста передаем + // нашу очередь сообщений, чтоб запихивать в неё эти события + view_port_input_callback_set(view_port, input_callback, event_queue); + + // Создаем GUI приложения + Gui* gui = furi_record_open(RECORD_GUI); + // Подключаем view port к GUI в полноэкранном режиме + gui_add_view_port(gui, view_port, GuiLayerFullscreen); + NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION); + notification_message_block(notification, &sequence_display_backlight_enforce_on); + + init(); + + // Бесконечный цикл обработки очереди событий + while(1) { + if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) { + furi_mutex_acquire(bomber_state->mutex, FuriWaitForever); + // Если нажата кнопка "назад", то выходим из цикла, а следовательно и из приложения + + if(event.type == InputTypePress) { + if(event.key == InputKeyOk) { + if(world.running) { + if(world.matrix[world.player->y][world.player->x] == 0 && + world.bombs_count < 2) { + notification_message(notification, &bomb2); + world.matrix[world.player->y][world.player->x] = 3; + Bomb bomb = {world.player->x, world.player->y, furi_get_tick()}; + world.bombs[world.bombs_count] = bomb; + world.bombs_count++; + } + } else { + init(); + } + } + if(world.running) { + if(event.key == InputKeyUp) { + if(world.player->y > 0 && + world.matrix[world.player->y - 1][world.player->x] == 0) + world.player->y--; + } + if(event.key == InputKeyDown) { + if(world.player->y < WorldSizeY - 1 && + world.matrix[world.player->y + 1][world.player->x] == 0) + world.player->y++; + } + if(event.key == InputKeyLeft) { + world.player->side = 0; + if(world.player->x > 0 && + world.matrix[world.player->y][world.player->x - 1] == 0) + world.player->x--; + } + if(event.key == InputKeyRight) { + world.player->side = 1; + if(world.player->x < WorldSizeX - 1 && + world.matrix[world.player->y][world.player->x + 1] == 0) + world.player->x++; + } + } + } else if(event.type == InputTypeLong) { + if(event.key == InputKeyBack) { + break; + } + } + } + if(world.running) { + if(world.player->x == world.endx && world.player->y == world.endy) { + notification_message(notification, &end); + world.running = 0; + world.level += 1; + if(world.level%5==0){ + DOLPHIN_DEED(DolphinDeedPluginGameWin); + } + } + for(int i = 0; i < world.bombs_count; i++) { + if(furi_get_tick() - world.bombs[i].planted > + (unsigned long)max((3000 - world.level * 150), 1000)) { + vibration = false; + world.matrix[world.bombs[i].y][world.bombs[i].x] = 6; + notification_message(notification, &bomb_explore); + + for(int j = max(0, world.bombs[i].y - BombRange); + j < min(WorldSizeY, world.bombs[i].y + BombRange + 1); + j++) { + if(world.matrix[j][world.bombs[i].x] != 2) { + world.matrix[j][world.bombs[i].x] = 6; + if(j == world.player->y && world.bombs[i].x == world.player->x) { + notification_message(notification, &end); + world.running = 0; + } + for(int e = 0; e < world.enemies_count; e++) { + if(j == world.enemies[e].y && + world.bombs[i].x == world.enemies[e].x) { + if(world.enemies[e].level > 0) { + world.enemies[e].level--; + } else { + for(int l = e; l < world.enemies_count - 1; l++) { + world.enemies[l] = world.enemies[l + 1]; + } + world.enemies_count--; + } + } + } + } + } + + for(int j = max(0, world.bombs[i].x - BombRange); + j < min(WorldSizeX, world.bombs[i].x + BombRange + 1); + j++) { + if(world.matrix[world.bombs[i].y][j] != 2) { + world.matrix[world.bombs[i].y][j] = 6; + if(world.bombs[i].y == world.player->y && j == world.player->x) { + notification_message(notification, &end); + world.running = 0; + } + for(int e = 0; e < world.enemies_count; e++) { + if(world.bombs[i].y == world.enemies[e].y && + j == world.enemies[e].x) { + if(world.enemies[e].level > 0) { + world.enemies[e].level--; + } else { + for(int l = e; l < world.enemies_count - 1; l++) { + world.enemies[l] = world.enemies[l + 1]; + } + world.enemies_count--; + } + } + } + } + } + + for(int j = i; j < world.bombs_count - 1; j++) { + world.bombs[j] = world.bombs[j + 1]; + } + world.bombs_count--; + } else if(furi_get_tick() - world.bombs[i].planted > (unsigned long)max((3000 - world.level * 150)*2/3, 666)&&world.matrix[world.bombs[i].y][world.bombs[i].x]!=5) { + world.matrix[world.bombs[i].y][world.bombs[i].x] = 5; + vibration=true; + + } else if(furi_get_tick() - world.bombs[i].planted > (unsigned long)max((3000 - world.level * 150)/3, 333)&& world.matrix[world.bombs[i].y][world.bombs[i].x]!=4) { + world.matrix[world.bombs[i].y][world.bombs[i].x] = 4; + + } + } + for(int e = 0; e < world.enemies_count; e++) { + if(world.player->y == world.enemies[e].y && + world.player->x == world.enemies[e].x) { + notification_message(notification, &end); + world.running = 0; + } + } + + for(int e = 0; e < world.enemies_count; e++) { + if(world.enemies[e].level > 0) { + if(furi_get_tick() - world.enemies[e].last > + (unsigned long)max((2000 - world.level * 100), 1000)) { + world.enemies[e].last = furi_get_tick(); + int move = rand() % 4; + switch(move) { + case 0: + if(world.enemies[e].y > 0 && + world.matrix[world.enemies[e].y - 1][world.enemies[e].x] != 2) + world.enemies[e].y--; + break; + case 1: + if(world.enemies[e].y < WorldSizeY - 1 && + world.matrix[world.enemies[e].y + 1][world.enemies[e].x] != 2) + world.enemies[e].y++; + break; + case 2: + world.enemies[e].side = 0; + if(world.enemies[e].x > 0 && + world.matrix[world.enemies[e].y][world.enemies[e].x - 1] != 2) + world.enemies[e].x--; + break; + case 3: + world.enemies[e].side = 1; + if(world.enemies[e].x < WorldSizeX - 1 && + world.matrix[world.enemies[e].y][world.enemies[e].x + 1] != 2) + world.enemies[e].x++; + default: + break; + } + } + } else { + if(furi_get_tick() - world.enemies[e].last > + (unsigned long)max((1000 - world.level * 50), 500)) { + world.enemies[e].last = furi_get_tick(); + int move = rand() % 4; + switch(move) { + case 0: + if(world.enemies[e].y > 0 && + world.matrix[world.enemies[e].y - 1][world.enemies[e].x] == 0) + world.enemies[e].y--; + break; + case 1: + if(world.enemies[e].y < WorldSizeY - 1 && + world.matrix[world.enemies[e].y + 1][world.enemies[e].x] == 0) + world.enemies[e].y++; + break; + case 2: + world.enemies[e].side = 0; + if(world.enemies[e].x > 0 && + world.matrix[world.enemies[e].y][world.enemies[e].x - 1] == 0) + world.enemies[e].x--; + break; + case 3: + world.enemies[e].side = 1; + if(world.enemies[e].x < WorldSizeX - 1 && + world.matrix[world.enemies[e].y][world.enemies[e].x + 1] == 0) + world.enemies[e].x++; + default: + break; + } + } + } + } + for(int e = 0; e < world.enemies_count; e++) { + for(int h = e + 1; h < world.enemies_count; h++) { + if(world.enemies[e].y == world.enemies[h].y && + world.enemies[e].x == world.enemies[h].x) { + world.enemies[h].level++; + for(int l = e; l < world.enemies_count - 1; l++) { + world.enemies[l] = world.enemies[l + 1]; + } + world.enemies_count--; + } + } + } + if(vibration){ + notification_message(notification, &vibr1); + } + } + + view_port_update(view_port); + furi_mutex_release(bomber_state->mutex); + } + + // Return to normal backlight settings + notification_message_block(notification, &sequence_display_backlight_enforce_auto); + furi_record_close(RECORD_NOTIFICATION); + // Специальная очистка памяти, занимаемой очередью + furi_message_queue_free(event_queue); + + // Чистим созданные объекты, связанные с интерфейсом + gui_remove_view_port(gui, view_port); + view_port_free(view_port); + + furi_mutex_free(bomber_state->mutex); + furi_record_close(RECORD_GUI); + free(bomber_state); + + return 0; +}