2021-11-23 16:07:46 +00:00
|
|
|
#include <furi.h>
|
|
|
|
#include <gui/gui.h>
|
|
|
|
#include <input/input.h>
|
|
|
|
#include <stdlib.h>
|
2022-05-27 19:18:35 +00:00
|
|
|
#include <notification/notification.h>
|
|
|
|
#include <notification/notification_messages.h>
|
2021-11-23 16:07:46 +00:00
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
// +-----x
|
|
|
|
// |
|
|
|
|
// |
|
|
|
|
// y
|
|
|
|
uint8_t x;
|
|
|
|
uint8_t y;
|
|
|
|
} Point;
|
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
GameStateLife,
|
|
|
|
|
|
|
|
// https://melmagazine.com/en-us/story/snake-nokia-6110-oral-history-taneli-armanto
|
|
|
|
// Armanto: While testing the early versions of the game, I noticed it was hard
|
|
|
|
// to control the snake upon getting close to and edge but not crashing — especially
|
|
|
|
// in the highest speed levels. I wanted the highest level to be as fast as I could
|
|
|
|
// possibly make the device "run," but on the other hand, I wanted to be friendly
|
|
|
|
// and help the player manage that level. Otherwise it might not be fun to play. So
|
|
|
|
// I implemented a little delay. A few milliseconds of extra time right before
|
|
|
|
// the player crashes, during which she can still change the directions. And if
|
|
|
|
// she does, the game continues.
|
|
|
|
GameStateLastChance,
|
|
|
|
|
|
|
|
GameStateGameOver,
|
|
|
|
} GameState;
|
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
DirectionUp,
|
|
|
|
DirectionRight,
|
|
|
|
DirectionDown,
|
|
|
|
DirectionLeft,
|
|
|
|
} Direction;
|
|
|
|
|
|
|
|
#define MAX_SNAKE_LEN 253
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
Point points[MAX_SNAKE_LEN];
|
|
|
|
uint16_t len;
|
|
|
|
Direction currentMovement;
|
|
|
|
Direction nextMovement; // if backward of currentMovement, ignore
|
|
|
|
Point fruit;
|
|
|
|
GameState state;
|
|
|
|
} SnakeState;
|
|
|
|
|
|
|
|
typedef enum {
|
|
|
|
EventTypeTick,
|
|
|
|
EventTypeKey,
|
|
|
|
} EventType;
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
EventType type;
|
|
|
|
InputEvent input;
|
|
|
|
} SnakeEvent;
|
|
|
|
|
|
|
|
static void snake_game_render_callback(Canvas* const canvas, void* ctx) {
|
|
|
|
const SnakeState* snake_state = acquire_mutex((ValueMutex*)ctx, 25);
|
|
|
|
if(snake_state == NULL) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Before the function is called, the state is set with the canvas_reset(canvas)
|
|
|
|
|
|
|
|
// Frame
|
|
|
|
canvas_draw_frame(canvas, 0, 0, 128, 64);
|
|
|
|
|
|
|
|
// Fruit
|
|
|
|
Point f = snake_state->fruit;
|
|
|
|
f.x = f.x * 4 + 1;
|
|
|
|
f.y = f.y * 4 + 1;
|
|
|
|
canvas_draw_rframe(canvas, f.x, f.y, 6, 6, 2);
|
|
|
|
|
|
|
|
// Snake
|
|
|
|
for(uint16_t i = 0; i < snake_state->len; i++) {
|
|
|
|
Point p = snake_state->points[i];
|
|
|
|
p.x = p.x * 4 + 2;
|
|
|
|
p.y = p.y * 4 + 2;
|
|
|
|
canvas_draw_box(canvas, p.x, p.y, 4, 4);
|
|
|
|
}
|
|
|
|
|
2022-06-21 02:50:34 +00:00
|
|
|
// Show score on the game field
|
|
|
|
char buffer2[6];
|
|
|
|
snprintf(buffer2, sizeof(buffer2), "%u", snake_state->len - 7);
|
|
|
|
canvas_draw_str_aligned(canvas, 126, 8, AlignRight, AlignBottom, buffer2);
|
|
|
|
|
2021-11-23 16:07:46 +00:00
|
|
|
// Game Over banner
|
|
|
|
if(snake_state->state == GameStateGameOver) {
|
|
|
|
// Screen is 128x64 px
|
|
|
|
canvas_set_color(canvas, ColorWhite);
|
|
|
|
canvas_draw_box(canvas, 34, 20, 62, 24);
|
|
|
|
|
|
|
|
canvas_set_color(canvas, ColorBlack);
|
|
|
|
canvas_draw_frame(canvas, 34, 20, 62, 24);
|
|
|
|
|
|
|
|
canvas_set_font(canvas, FontPrimary);
|
|
|
|
canvas_draw_str(canvas, 37, 31, "Game Over");
|
|
|
|
|
|
|
|
canvas_set_font(canvas, FontSecondary);
|
|
|
|
char buffer[12];
|
|
|
|
snprintf(buffer, sizeof(buffer), "Score: %u", snake_state->len - 7);
|
|
|
|
canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignBottom, buffer);
|
|
|
|
}
|
|
|
|
|
|
|
|
release_mutex((ValueMutex*)ctx, snake_state);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snake_game_input_callback(InputEvent* input_event, osMessageQueueId_t event_queue) {
|
|
|
|
furi_assert(event_queue);
|
|
|
|
|
|
|
|
SnakeEvent event = {.type = EventTypeKey, .input = *input_event};
|
|
|
|
osMessageQueuePut(event_queue, &event, 0, osWaitForever);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snake_game_update_timer_callback(osMessageQueueId_t event_queue) {
|
|
|
|
furi_assert(event_queue);
|
|
|
|
|
|
|
|
SnakeEvent event = {.type = EventTypeTick};
|
|
|
|
osMessageQueuePut(event_queue, &event, 0, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snake_game_init_game(SnakeState* const snake_state) {
|
|
|
|
Point p[] = {{8, 6}, {7, 6}, {6, 6}, {5, 6}, {4, 6}, {3, 6}, {2, 6}};
|
|
|
|
memcpy(snake_state->points, p, sizeof(p));
|
|
|
|
|
|
|
|
snake_state->len = 7;
|
|
|
|
|
|
|
|
snake_state->currentMovement = DirectionRight;
|
|
|
|
|
|
|
|
snake_state->nextMovement = DirectionRight;
|
|
|
|
|
|
|
|
Point f = {18, 6};
|
|
|
|
snake_state->fruit = f;
|
|
|
|
|
|
|
|
snake_state->state = GameStateLife;
|
|
|
|
}
|
|
|
|
|
|
|
|
static Point snake_game_get_new_fruit(SnakeState const* const snake_state) {
|
|
|
|
// 1 bit for each point on the playing field where the snake can turn
|
|
|
|
// and where the fruit can appear
|
|
|
|
uint16_t buffer[8];
|
|
|
|
memset(buffer, 0, sizeof(buffer));
|
|
|
|
uint8_t empty = 8 * 16;
|
|
|
|
|
|
|
|
for(uint16_t i = 0; i < snake_state->len; i++) {
|
|
|
|
Point p = snake_state->points[i];
|
|
|
|
|
|
|
|
if(p.x % 2 != 0 || p.y % 2 != 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
p.x /= 2;
|
|
|
|
p.y /= 2;
|
|
|
|
|
|
|
|
buffer[p.y] |= 1 << p.x;
|
|
|
|
empty--;
|
|
|
|
}
|
|
|
|
// Bit set if snake use that playing field
|
|
|
|
|
|
|
|
uint16_t newFruit = rand() % empty;
|
|
|
|
|
|
|
|
// Skip random number of _empty_ fields
|
|
|
|
for(uint8_t y = 0; y < 8; y++) {
|
|
|
|
for(uint16_t x = 0, mask = 1; x < 16; x += 1, mask <<= 1) {
|
|
|
|
if((buffer[y] & mask) == 0) {
|
|
|
|
if(newFruit == 0) {
|
|
|
|
Point p = {
|
|
|
|
.x = x * 2,
|
|
|
|
.y = y * 2,
|
|
|
|
};
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
newFruit--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// We will never be here
|
|
|
|
Point p = {0, 0};
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool snake_game_collision_with_frame(Point const next_step) {
|
|
|
|
// if x == 0 && currentMovement == left then x - 1 == 255 ,
|
|
|
|
// so check only x > right border
|
|
|
|
return next_step.x > 30 || next_step.y > 14;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
snake_game_collision_with_tail(SnakeState const* const snake_state, Point const next_step) {
|
|
|
|
for(uint16_t i = 0; i < snake_state->len; i++) {
|
|
|
|
Point p = snake_state->points[i];
|
|
|
|
if(p.x == next_step.x && p.y == next_step.y) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
static Direction snake_game_get_turn_snake(SnakeState const* const snake_state) {
|
|
|
|
switch(snake_state->currentMovement) {
|
|
|
|
case DirectionUp:
|
|
|
|
switch(snake_state->nextMovement) {
|
|
|
|
case DirectionRight:
|
|
|
|
return DirectionRight;
|
|
|
|
case DirectionLeft:
|
|
|
|
return DirectionLeft;
|
|
|
|
default:
|
|
|
|
return snake_state->currentMovement;
|
|
|
|
}
|
|
|
|
case DirectionRight:
|
|
|
|
switch(snake_state->nextMovement) {
|
|
|
|
case DirectionUp:
|
|
|
|
return DirectionUp;
|
|
|
|
case DirectionDown:
|
|
|
|
return DirectionDown;
|
|
|
|
default:
|
|
|
|
return snake_state->currentMovement;
|
|
|
|
}
|
|
|
|
case DirectionDown:
|
|
|
|
switch(snake_state->nextMovement) {
|
|
|
|
case DirectionRight:
|
|
|
|
return DirectionRight;
|
|
|
|
case DirectionLeft:
|
|
|
|
return DirectionLeft;
|
|
|
|
default:
|
|
|
|
return snake_state->currentMovement;
|
|
|
|
}
|
|
|
|
default: // case DirectionLeft:
|
|
|
|
switch(snake_state->nextMovement) {
|
|
|
|
case DirectionUp:
|
|
|
|
return DirectionUp;
|
|
|
|
case DirectionDown:
|
|
|
|
return DirectionDown;
|
|
|
|
default:
|
|
|
|
return snake_state->currentMovement;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static Point snake_game_get_next_step(SnakeState const* const snake_state) {
|
|
|
|
Point next_step = snake_state->points[0];
|
|
|
|
switch(snake_state->currentMovement) {
|
|
|
|
// +-----x
|
|
|
|
// |
|
|
|
|
// |
|
|
|
|
// y
|
|
|
|
case DirectionUp:
|
|
|
|
next_step.y--;
|
|
|
|
break;
|
|
|
|
case DirectionRight:
|
|
|
|
next_step.x++;
|
|
|
|
break;
|
|
|
|
case DirectionDown:
|
|
|
|
next_step.y++;
|
|
|
|
break;
|
|
|
|
case DirectionLeft:
|
|
|
|
next_step.x--;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return next_step;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snake_game_move_snake(SnakeState* const snake_state, Point const next_step) {
|
|
|
|
memmove(snake_state->points + 1, snake_state->points, snake_state->len * sizeof(Point));
|
|
|
|
snake_state->points[0] = next_step;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void snake_game_process_game_step(SnakeState* const snake_state) {
|
|
|
|
if(snake_state->state == GameStateGameOver) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool can_turn = (snake_state->points[0].x % 2 == 0) && (snake_state->points[0].y % 2 == 0);
|
|
|
|
if(can_turn) {
|
|
|
|
snake_state->currentMovement = snake_game_get_turn_snake(snake_state);
|
|
|
|
}
|
|
|
|
|
|
|
|
Point next_step = snake_game_get_next_step(snake_state);
|
|
|
|
|
|
|
|
bool crush = snake_game_collision_with_frame(next_step);
|
|
|
|
if(crush) {
|
|
|
|
if(snake_state->state == GameStateLife) {
|
|
|
|
snake_state->state = GameStateLastChance;
|
|
|
|
return;
|
|
|
|
} else if(snake_state->state == GameStateLastChance) {
|
|
|
|
snake_state->state = GameStateGameOver;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if(snake_state->state == GameStateLastChance) {
|
|
|
|
snake_state->state = GameStateLife;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
crush = snake_game_collision_with_tail(snake_state, next_step);
|
|
|
|
if(crush) {
|
|
|
|
snake_state->state = GameStateGameOver;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool eatFruit = (next_step.x == snake_state->fruit.x) && (next_step.y == snake_state->fruit.y);
|
|
|
|
if(eatFruit) {
|
2022-05-27 19:18:35 +00:00
|
|
|
NotificationApp* notification = furi_record_open("notification");
|
|
|
|
notification_message(notification, &sequence_single_vibro);
|
|
|
|
notification_message(notification, &sequence_blink_white_100);
|
|
|
|
furi_record_close("notification");
|
|
|
|
|
2021-11-23 16:07:46 +00:00
|
|
|
snake_state->len++;
|
|
|
|
if(snake_state->len >= MAX_SNAKE_LEN) {
|
|
|
|
snake_state->state = GameStateGameOver;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
snake_game_move_snake(snake_state, next_step);
|
|
|
|
|
|
|
|
if(eatFruit) {
|
|
|
|
snake_state->fruit = snake_game_get_new_fruit(snake_state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int32_t snake_game_app(void* p) {
|
2022-05-06 13:37:10 +00:00
|
|
|
UNUSED(p);
|
2021-11-23 16:07:46 +00:00
|
|
|
srand(DWT->CYCCNT);
|
|
|
|
|
|
|
|
osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(SnakeEvent), NULL);
|
|
|
|
|
2022-02-18 19:53:46 +00:00
|
|
|
SnakeState* snake_state = malloc(sizeof(SnakeState));
|
2021-11-23 16:07:46 +00:00
|
|
|
snake_game_init_game(snake_state);
|
|
|
|
|
|
|
|
ValueMutex state_mutex;
|
|
|
|
if(!init_mutex(&state_mutex, snake_state, sizeof(SnakeState))) {
|
2021-12-14 22:39:59 +00:00
|
|
|
FURI_LOG_E("SnakeGame", "cannot create mutex\r\n");
|
2021-11-23 16:07:46 +00:00
|
|
|
free(snake_state);
|
|
|
|
return 255;
|
|
|
|
}
|
|
|
|
|
|
|
|
ViewPort* view_port = view_port_alloc();
|
|
|
|
view_port_draw_callback_set(view_port, snake_game_render_callback, &state_mutex);
|
|
|
|
view_port_input_callback_set(view_port, snake_game_input_callback, event_queue);
|
|
|
|
|
|
|
|
osTimerId_t timer =
|
|
|
|
osTimerNew(snake_game_update_timer_callback, osTimerPeriodic, event_queue, NULL);
|
|
|
|
osTimerStart(timer, osKernelGetTickFreq() / 4);
|
|
|
|
|
|
|
|
// Open GUI and register view_port
|
|
|
|
Gui* gui = furi_record_open("gui");
|
|
|
|
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
|
|
|
|
|
|
|
SnakeEvent event;
|
|
|
|
for(bool processing = true; processing;) {
|
|
|
|
osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 100);
|
|
|
|
|
|
|
|
SnakeState* snake_state = (SnakeState*)acquire_mutex_block(&state_mutex);
|
|
|
|
|
|
|
|
if(event_status == osOK) {
|
|
|
|
// press events
|
|
|
|
if(event.type == EventTypeKey) {
|
|
|
|
if(event.input.type == InputTypePress) {
|
|
|
|
switch(event.input.key) {
|
|
|
|
case InputKeyUp:
|
|
|
|
snake_state->nextMovement = DirectionUp;
|
|
|
|
break;
|
|
|
|
case InputKeyDown:
|
|
|
|
snake_state->nextMovement = DirectionDown;
|
|
|
|
break;
|
|
|
|
case InputKeyRight:
|
|
|
|
snake_state->nextMovement = DirectionRight;
|
|
|
|
break;
|
|
|
|
case InputKeyLeft:
|
|
|
|
snake_state->nextMovement = DirectionLeft;
|
|
|
|
break;
|
|
|
|
case InputKeyOk:
|
|
|
|
if(snake_state->state == GameStateGameOver) {
|
|
|
|
snake_game_init_game(snake_state);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case InputKeyBack:
|
|
|
|
processing = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if(event.type == EventTypeTick) {
|
|
|
|
snake_game_process_game_step(snake_state);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// event timeout
|
|
|
|
}
|
|
|
|
|
|
|
|
view_port_update(view_port);
|
|
|
|
release_mutex(&state_mutex, snake_state);
|
|
|
|
}
|
|
|
|
|
|
|
|
osTimerDelete(timer);
|
|
|
|
view_port_enabled_set(view_port, false);
|
|
|
|
gui_remove_view_port(gui, view_port);
|
|
|
|
furi_record_close("gui");
|
|
|
|
view_port_free(view_port);
|
|
|
|
osMessageQueueDelete(event_queue);
|
|
|
|
delete_mutex(&state_mutex);
|
|
|
|
free(snake_state);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Screen is 128x64 px
|
|
|
|
// (4 + 4) * 16 - 4 + 2 + 2border == 128
|
|
|
|
// (4 + 4) * 8 - 4 + 2 + 2border == 64
|
|
|
|
// Game field from point{x: 0, y: 0} to point{x: 30, y: 14}.
|
|
|
|
// The snake turns only in even cells - intersections.
|
|
|
|
// ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
|
|
|
// └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘
|