Add Snake game (#829)
* Add snake game * Applications: Added a classic game https://en.wikipedia.org/wiki/Snake_(video_game_genre) * Snake Game: Making it impossible to lose button presses * Use more native press button event * Snake Game: use low level InputTypePress event instead of InputTypeShort high level unpredictable event Co-authored-by: LionZXY <nikita@kulikof.ru> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
		
							parent
							
								
									e54e4a6d77
								
							
						
					
					
						commit
						68274b6c27
					
				| @ -42,6 +42,7 @@ extern int32_t vibro_test_app(void* p); | |||||||
| 
 | 
 | ||||||
| // Plugins
 | // Plugins
 | ||||||
| extern int32_t music_player_app(void* p); | extern int32_t music_player_app(void* p); | ||||||
|  | extern int32_t snake_game_app(void* p); | ||||||
| 
 | 
 | ||||||
| // On system start hooks declaration
 | // On system start hooks declaration
 | ||||||
| extern void bt_cli_init(); | extern void bt_cli_init(); | ||||||
| @ -203,6 +204,10 @@ const FlipperApplication FLIPPER_PLUGINS[] = { | |||||||
| #ifdef APP_MUSIC_PLAYER | #ifdef APP_MUSIC_PLAYER | ||||||
|     {.app = music_player_app, .name = "Music Player", .stack_size = 1024, .icon = &A_Plugins_14}, |     {.app = music_player_app, .name = "Music Player", .stack_size = 1024, .icon = &A_Plugins_14}, | ||||||
| #endif | #endif | ||||||
|  | 
 | ||||||
|  | #ifdef APP_SNAKE_GAME | ||||||
|  |     {.app = snake_game_app, .name = "Snake Game", .stack_size = 1024, .icon = &A_Plugins_14}, | ||||||
|  | #endif | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const size_t FLIPPER_PLUGINS_COUNT = sizeof(FLIPPER_PLUGINS) / sizeof(FlipperApplication); | const size_t FLIPPER_PLUGINS_COUNT = sizeof(FLIPPER_PLUGINS) / sizeof(FlipperApplication); | ||||||
|  | |||||||
| @ -35,6 +35,7 @@ APP_ABOUT	= 1 | |||||||
| 
 | 
 | ||||||
| # Plugins
 | # Plugins
 | ||||||
| APP_MUSIC_PLAYER = 1 | APP_MUSIC_PLAYER = 1 | ||||||
|  | APP_SNAKE_GAME = 1 | ||||||
| 
 | 
 | ||||||
| # Debug
 | # Debug
 | ||||||
| APP_ACCESSOR = 1 | APP_ACCESSOR = 1 | ||||||
| @ -185,6 +186,11 @@ CFLAGS		+= -DAPP_MUSIC_PLAYER | |||||||
| SRV_GUI		= 1 | SRV_GUI		= 1 | ||||||
| endif | endif | ||||||
| 
 | 
 | ||||||
|  | APP_SNAKE_GAME ?= 0 | ||||||
|  | ifeq ($(APP_SNAKE_GAME), 1) | ||||||
|  | CFLAGS		+= -DAPP_SNAKE_GAME | ||||||
|  | SRV_GUI		= 1 | ||||||
|  | endif | ||||||
| 
 | 
 | ||||||
| APP_IBUTTON ?= 0 | APP_IBUTTON ?= 0 | ||||||
| ifeq ($(APP_IBUTTON), 1) | ifeq ($(APP_IBUTTON), 1) | ||||||
|  | |||||||
							
								
								
									
										419
									
								
								applications/snake-game/snake-game.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										419
									
								
								applications/snake-game/snake-game.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,419 @@ | |||||||
|  | #include <furi.h> | ||||||
|  | #include <gui/gui.h> | ||||||
|  | #include <input/input.h> | ||||||
|  | #include <stdlib.h> | ||||||
|  | 
 | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 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) { | ||||||
|  |         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) { | ||||||
|  |     srand(DWT->CYCCNT); | ||||||
|  | 
 | ||||||
|  |     osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(SnakeEvent), NULL); | ||||||
|  | 
 | ||||||
|  |     SnakeState* snake_state = furi_alloc(sizeof(SnakeState)); | ||||||
|  |     snake_game_init_game(snake_state); | ||||||
|  | 
 | ||||||
|  |     ValueMutex state_mutex; | ||||||
|  |     if(!init_mutex(&state_mutex, snake_state, sizeof(SnakeState))) { | ||||||
|  |         furi_log_print(FURI_LOG_ERROR, "cannot create mutex\r\n"); | ||||||
|  |         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.
 | ||||||
|  | // ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||||
|  | // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||||
|  | // └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘
 | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Oleg Schwann
						Oleg Schwann