diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b19b0029..0c511eae0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,11 @@
### New Update
-* Fix KeeLoq Uknown behavior, patch StarLine same way
-* Fix incorrect var in protocol Scher-Khan (by @Skorpionm)
-* Came Atomo working emulation algorytm!
+* Games: Snake & Tetis now shows score
+* Volume patch in music player (testing needed)
+* Two new games: Arkanoid & Tic Tac Toe
#### Previous changes
+* Fixed KeeLoq Uknown behavior, patched StarLine same way
+* Fixed incorrect var in protocol Scher-Khan (by @Skorpionm)
+* Came Atomo working emulation algorithm!
* Updated UniRF Remix app, .txt file support
* Fixed macOS Brewfile, so compiling on macOS now works
* Updated CAME Atomo
diff --git a/ReadMe.md b/ReadMe.md
index 7e4953982..596fb3273 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -5,7 +5,7 @@
Welcome to Flipper Zero's Custom Firmware repo!
Our goal is to make any features possible in this device without any limitations!
-Please help us realize emulation for all dynamic (rolling codes) protocols and brute-force app!
+Please help us implement emulation for all dynamic (rolling codes) protocols and brute-force app!
@@ -177,6 +177,8 @@ Use **`flipper-z-{target}-full-{suffix}.dfu`** to flash your device.
- [WAV Player (By DrZlo13)](https://github.com/flipperdevices/flipperzero-firmware/tree/zlo/wav-player) With Fix From [Atmanos](https://github.com/at-manos)
- [Tetris (By jeffplang)](https://github.com/jeffplang/flipperzero-firmware/tree/tetris_game/applications/tetris_game)
- [Spectrum Analyzer (By jolcese)](https://github.com/jolcese/flipperzero-firmware/tree/spectrum/applications/spectrum_analyzer)
+- [Arkanoid (By gotnull)](https://github.com/gotnull/flipperzero-firmware-wPlugins)
+- [Tic Tac Toe (By gotnull)](https://github.com/gotnull/flipperzero-firmware-wPlugins)
# Links
diff --git a/applications/applications.c b/applications/applications.c
index 247c601c5..d06e48aa8 100644
--- a/applications/applications.c
+++ b/applications/applications.c
@@ -56,6 +56,8 @@ extern int32_t spectrum_analyzer_app(void* p);
// Games
extern int32_t snake_game_app(void* p);
extern int32_t tetris_game_app(void *p);
+extern int32_t tictactoe_game_app(void* p);
+extern int32_t arkanoid_game_app(void* p);
// On system start hooks declaration
extern void bt_on_system_start();
@@ -405,6 +407,23 @@ const FlipperApplication FLIPPER_GAMES[] = {
.flags = FlipperApplicationFlagDefault},
#endif
+
+#ifdef APP_ARKANOID_GAME
+ {.app = arkanoid_game_app,
+ .name = "Arkanoid",
+ .stack_size = 1024,
+ .icon = &A_Plugins_14,
+ .flags = FlipperApplicationFlagDefault},
+#endif
+
+#ifdef APP_TICTACTOE_GAME
+ {.app = tictactoe_game_app,
+ .name = "Tic Tac Toe",
+ .stack_size = 1024,
+ .icon = &A_Plugins_14,
+ .flags = FlipperApplicationFlagDefault},
+#endif
+
};
const size_t FLIPPER_GAMES_COUNT = COUNT_OF(FLIPPER_GAMES);
diff --git a/applications/applications.mk b/applications/applications.mk
index 60a339bdf..d176e5ac4 100644
--- a/applications/applications.mk
+++ b/applications/applications.mk
@@ -51,9 +51,13 @@ APP_SPECTRUM_ANALYZER = 1
# Plugins
APP_MUSIC_PLAYER = 1
-APP_SNAKE_GAME = 1
APP_WAV_PLAYER = 1
+
+# Games
+APP_SNAKE_GAME = 1
APP_TETRIS_GAME = 1
+APP_TICTACTOE_GAME = 1
+APP_ARKANOID_GAME = 1
# Debug
APP_ACCESSOR = 1
@@ -278,6 +282,18 @@ CFLAGS += -DAPP_SPECTRUM_ANALYZER
SRV_GUI = 1
endif
+APP_TICTACTOE_GAME ?= 0
+ifeq ($(APP_TICTACTOE_GAME),1)
+CFLAGS += -DAPP_TICTACTOE_GAME
+SRV_GUI = 1
+endif
+
+APP_ARKANOID_GAME ?= 0
+ifeq ($(APP_ARKANOID_GAME),1)
+CFLAGS += -DAPP_ARKANOID_GAME
+SRV_GUI = 1
+endif
+
APP_IBUTTON ?= 0
ifeq ($(APP_IBUTTON), 1)
CFLAGS += -DAPP_IBUTTON
diff --git a/applications/arkanoid/arkanoid_game.c b/applications/arkanoid/arkanoid_game.c
new file mode 100644
index 000000000..676b13a55
--- /dev/null
+++ b/applications/arkanoid/arkanoid_game.c
@@ -0,0 +1,410 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "Arkanoid"
+
+unsigned int COLUMNS = 13; //Columns of bricks
+unsigned int ROWS = 4; //Rows of bricks
+int dx = -1; //Initial movement of ball
+int dy = -1; //Initial movement of ball
+int xb; //Balls starting possition
+int yb; //Balls starting possition
+bool released; //If the ball has been released by the player
+bool paused = false; //If the game has been paused
+int xPaddle; //X position of paddle
+bool isHit[4][13]; //Array of if bricks are hit or not
+bool bounced = false; //Used to fix double bounce glitch
+int lives = 3; //Amount of lives
+int level = 1; //Current level
+unsigned int score = 0; //Score for the game
+unsigned int brickCount; //Amount of bricks hit
+int pad, pad2, pad3; //Button press buffer used to stop pause repeating
+int oldpad, oldpad2, oldpad3;
+char text[16]; //General string buffer
+bool start = false; //If in menu or in game
+bool initialDraw = false; //If the inital draw has happened
+char initials[3]; //Initials used in high score
+
+//Ball Bounds used in collision detection
+int leftBall;
+int rightBall;
+int topBall;
+int bottomBall;
+
+//Brick Bounds used in collision detection
+int leftBrick;
+int rightBrick;
+int topBrick;
+int bottomBrick;
+
+int tick;
+
+#define FLIPPER_LCD_WIDTH 128
+#define FLIPPER_LCD_HEIGHT 64
+
+typedef enum { EventTypeTick, EventTypeKey } EventType;
+
+typedef enum { DirectionUp, DirectionRight, DirectionDown, DirectionLeft } Direction;
+
+typedef enum { GameStatePlaying, GameStateGameOver } GameState;
+
+typedef struct {
+ int x;
+ int y;
+} Point;
+
+typedef struct {
+ GameState game_state;
+} ArkanoidState;
+
+typedef struct {
+ EventType type;
+ InputEvent input;
+} GameEvent;
+
+// generate number in range [min,max)
+int rand_range(int min, int max) {
+ int number = min + rand() % (max - min);
+ return number;
+}
+
+void intro(Canvas* canvas) {
+ canvas_set_font(canvas, FontPrimary);
+ canvas_draw_str(canvas, 46, 0, "Arkanoid");
+
+ //arduboy.tunes.tone(987, 160);
+ //delay(160);
+ //arduboy.tunes.tone(1318, 400);
+ //delay(2000);
+}
+
+void move_ball(Canvas* canvas) {
+ tick++;
+ if(released) {
+ //Move ball
+ if(abs(dx) == 2) {
+ xb += dx / 2;
+ // 2x speed is really 1.5 speed
+ if(tick % 2 == 0) xb += dx / 2;
+ } else {
+ xb += dx;
+ }
+ yb = yb + dy;
+
+ //Set bounds
+ leftBall = xb;
+ rightBall = xb + 2;
+ topBall = yb;
+ bottomBall = yb + 2;
+
+ //Bounce off top edge
+ if(yb <= 0) {
+ yb = 2;
+ dy = -dy;
+ // arduboy.tunes.tone(523, 250);
+ }
+
+ //Lose a life if bottom edge hit
+ if(yb >= FLIPPER_LCD_HEIGHT) {
+ canvas_draw_frame(canvas, xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
+ xPaddle = 54;
+ yb = 60;
+ released = false;
+ lives--;
+
+ sprintf(text, "LIVES:%u", lives);
+ canvas_draw_str(canvas, 0, 90, text);
+
+ // arduboy.tunes.tone(175, 250);
+ if(rand_range(0, 2) == 0) {
+ dx = 1;
+ } else {
+ dx = -1;
+ }
+ }
+
+ //Bounce off left side
+ if(xb <= 0) {
+ xb = 2;
+ dx = -dx;
+ // arduboy.tunes.tone(523, 250);
+ }
+
+ //Bounce off right side
+ if(xb >= FLIPPER_LCD_WIDTH - 2) {
+ xb = FLIPPER_LCD_WIDTH - 4;
+ dx = -dx;
+ // arduboy.tunes.tone(523, 250);
+ }
+
+ //Bounce off paddle
+ if(xb + 1 >= xPaddle && xb <= xPaddle + 12 && yb + 2 >= FLIPPER_LCD_HEIGHT - 1 &&
+ yb <= FLIPPER_LCD_HEIGHT) {
+ dy = -dy;
+ dx = ((xb - (xPaddle + 6)) / 3); //Applies spin on the ball
+ // prevent straight bounce
+ if(dx == 0) {
+ dx = (rand_range(0, 2) == 1) ? 1 : -1;
+ }
+ // arduboy.tunes.tone(200, 250);
+ }
+
+ //Bounce off Bricks
+ for(int row = 0; row < ROWS; row++) {
+ for(int column = 0; column < COLUMNS; column++) {
+ if(!isHit[row][column]) {
+ //Sets Brick bounds
+ leftBrick = 10 * column;
+ rightBrick = 10 * column + 10;
+ topBrick = 6 * row + 1;
+ bottomBrick = 6 * row + 7;
+
+ //If A collison has occured
+ if(topBall <= bottomBrick && bottomBall >= topBrick &&
+ leftBall <= rightBrick && rightBall >= leftBrick) {
+ // Draw score
+ score += (level * 10);
+ sprintf(text, "SCORE:%u", score);
+ canvas_draw_str(canvas, 80, 90, text);
+
+ brickCount++;
+ isHit[row][column] = true;
+ canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4);
+
+ //Vertical collision
+ if(bottomBall > bottomBrick || topBall < topBrick) {
+ //Only bounce once each ball move
+ if(!bounced) {
+ dy = -dy;
+ yb += dy;
+ bounced = true;
+ // arduboy.tunes.tone(261, 250);
+ }
+ }
+
+ //Hoizontal collision
+ if(leftBall < leftBrick || rightBall > rightBrick) {
+ //Only bounce once brick each ball move
+ if(!bounced) {
+ dx = -dx;
+ xb += dx;
+ bounced = true;
+ // arduboy.tunes.tone(261, 250);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ //Reset Bounce
+ bounced = false;
+ } else {
+ //Ball follows paddle
+ xb = xPaddle + 5;
+ }
+}
+
+void draw_lives(Canvas* canvas) {
+ sprintf(text, "LIVES:%u", lives);
+ canvas_draw_str(canvas, 0, 90, text);
+}
+
+void draw_ball(Canvas* canvas) {
+ canvas_draw_dot(canvas, xb, yb);
+ canvas_draw_dot(canvas, xb + 1, yb);
+ canvas_draw_dot(canvas, xb, yb + 1);
+ canvas_draw_dot(canvas, xb + 1, yb + 1);
+
+ move_ball(canvas);
+}
+
+void draw_paddle(Canvas* canvas) {
+ canvas_draw_frame(canvas, xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
+}
+
+void reset_level(Canvas* canvas) {
+ //Undraw paddle
+ canvas_draw_frame(canvas, xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
+
+ //Undraw ball
+ canvas_draw_dot(canvas, xb, yb);
+ canvas_draw_dot(canvas, xb + 1, yb);
+ canvas_draw_dot(canvas, xb, yb + 1);
+ canvas_draw_dot(canvas, xb + 1, yb + 1);
+
+ //Alter various variables to reset the game
+ xPaddle = 54;
+ yb = 60;
+ brickCount = 0;
+ released = false;
+
+ // Reset all brick hit states
+ for(int row = 0; row < ROWS; row++) {
+ for(int column = 0; column < COLUMNS; column++) {
+ isHit[row][column] = false;
+ }
+ }
+}
+
+static void arkanoid_state_init(ArkanoidState* const arkanoid_state) {
+ // Set the initial game state
+ arkanoid_state->game_state = GameStatePlaying;
+
+ // Reset initial state
+ initialDraw = false;
+}
+
+static void arkanoid_draw_callback(Canvas* const canvas, void* ctx) {
+ const ArkanoidState* arkanoid_state = acquire_mutex((ValueMutex*)ctx, 25);
+ if(arkanoid_state == NULL) {
+ return;
+ }
+
+ //Initial level draw
+ if(!initialDraw) {
+ initialDraw = true;
+
+ // Set default font for text
+ canvas_set_font(canvas, FontPrimary);
+
+ //Draws the new level
+ reset_level(canvas);
+ }
+
+ //Draws new bricks and resets their values
+ for(int row = 0; row < ROWS; row++) {
+ for(int column = 0; column < COLUMNS; column++) {
+ if(!isHit[row][column]) {
+ canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4);
+ }
+ }
+ }
+
+ if(lives > 0) {
+ draw_paddle(canvas);
+
+ draw_ball(canvas);
+
+ if(brickCount == ROWS * COLUMNS) {
+ level++;
+ reset_level(canvas);
+ }
+ } else {
+ reset_level(canvas);
+ initialDraw = false;
+ start = false;
+ lives = 3;
+ score = 0;
+ }
+
+ release_mutex((ValueMutex*)ctx, arkanoid_state);
+}
+
+static void arkanoid_input_callback(InputEvent* input_event, osMessageQueueId_t event_queue) {
+ furi_assert(event_queue);
+
+ GameEvent event = {.type = EventTypeKey, .input = *input_event};
+ osMessageQueuePut(event_queue, &event, 0, osWaitForever);
+}
+
+static void arkanoid_update_timer_callback(osMessageQueueId_t event_queue) {
+ furi_assert(event_queue);
+
+ GameEvent event = {.type = EventTypeTick};
+ osMessageQueuePut(event_queue, &event, 0, 0);
+}
+
+int32_t arkanoid_game_app(void* p) {
+ osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(GameEvent), NULL);
+
+ ArkanoidState* arkanoid_state = malloc(sizeof(ArkanoidState));
+ arkanoid_state_init(arkanoid_state);
+
+ ValueMutex state_mutex;
+ if(!init_mutex(&state_mutex, arkanoid_state, sizeof(ArkanoidState))) {
+ FURI_LOG_E(TAG, "Cannot create mutex\r\n");
+ free(arkanoid_state);
+ return 255;
+ }
+
+ // Set system callbacks
+ ViewPort* view_port = view_port_alloc();
+ view_port_draw_callback_set(view_port, arkanoid_draw_callback, &state_mutex);
+ view_port_input_callback_set(view_port, arkanoid_input_callback, event_queue);
+
+ osTimerId_t timer =
+ osTimerNew(arkanoid_update_timer_callback, osTimerPeriodic, event_queue, NULL);
+ osTimerStart(timer, osKernelGetTickFreq() / 22);
+
+ // Open GUI and register view_port
+ Gui* gui = furi_record_open("gui");
+ gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+ GameEvent event;
+ for(bool processing = true; processing;) {
+ osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 100);
+ ArkanoidState* arkanoid_state = (ArkanoidState*)acquire_mutex_block(&state_mutex);
+
+ if(event_status == osOK) {
+ // Key events
+ if(event.type == EventTypeKey) {
+ if(event.input.type == InputTypePress || event.input.type == InputTypeLong ||
+ event.input.type == InputTypeRepeat) {
+ switch(event.input.key) {
+ case InputKeyBack:
+ processing = false;
+ break;
+ case InputKeyRight:
+ if(xPaddle < FLIPPER_LCD_WIDTH - 12) {
+ xPaddle += 8;
+ }
+ break;
+ case InputKeyLeft:
+ if(xPaddle > 0) {
+ xPaddle -= 8;
+ }
+ break;
+ case InputKeyUp:
+ break;
+ case InputKeyDown:
+ break;
+ case InputKeyOk:
+ //Release ball if FIRE pressed
+ released = true;
+
+ //Apply random direction to ball on release
+ if(rand_range(0, 2) == 0) {
+ dx = 1;
+ } else {
+ dx = -1;
+ }
+
+ //Makes sure the ball heads upwards
+ dy = -1;
+ break;
+ }
+ }
+ }
+ } else {
+ // Event timeout
+ FURI_LOG_D(TAG, "osMessageQueue: Event timeout");
+ }
+
+ view_port_update(view_port);
+ release_mutex(&state_mutex, arkanoid_state);
+ }
+
+ 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);
+ furi_record_close("notification");
+
+ return 0;
+}
\ No newline at end of file
diff --git a/applications/music_player/music_player.c b/applications/music_player/music_player.c
index 9b5dda0fa..06c7140d1 100644
--- a/applications/music_player/music_player.c
+++ b/applications/music_player/music_player.c
@@ -253,7 +253,7 @@ MusicPlayer* music_player_alloc() {
instance->model = malloc(sizeof(MusicPlayerModel));
memset(instance->model->duration_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE);
memset(instance->model->semitone_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE);
- instance->model->volume = 3;
+ instance->model->volume = 1;
instance->model_mutex = osMutexNew(NULL);
diff --git a/applications/music_player/music_player_worker.c b/applications/music_player/music_player_worker.c
index f43aa7be8..949477e11 100644
--- a/applications/music_player/music_player_worker.c
+++ b/applications/music_player/music_player_worker.c
@@ -79,7 +79,7 @@ static int32_t music_player_worker_thread_callback(void* context) {
furi_hal_speaker_stop();
furi_hal_speaker_start(frequency, volume);
while(instance->should_work && furi_hal_get_tick() < next_tick) {
- volume *= 0.9945679;
+ volume *= 1.0000000;
furi_hal_speaker_set_volume(volume);
furi_hal_delay_ms(2);
}
diff --git a/applications/snake_game/snake_game.c b/applications/snake_game/snake_game.c
index b07210ee5..1f33d414b 100644
--- a/applications/snake_game/snake_game.c
+++ b/applications/snake_game/snake_game.c
@@ -84,6 +84,11 @@ static void snake_game_render_callback(Canvas* const canvas, void* ctx) {
canvas_draw_box(canvas, p.x, p.y, 4, 4);
}
+ // 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);
+
// Game Over banner
if(snake_state->state == GameStateGameOver) {
// Screen is 128x64 px
diff --git a/applications/tetris_game/tetris_game.c b/applications/tetris_game/tetris_game.c
index 18c221b89..04b621226 100644
--- a/applications/tetris_game/tetris_game.c
+++ b/applications/tetris_game/tetris_game.c
@@ -150,6 +150,11 @@ static void tetris_game_render_callback(Canvas* const canvas, void* ctx) {
tetris_game_draw_border(canvas);
tetris_game_draw_playfield(canvas, tetris_state);
+ // Show score on the game field
+ char buffer2[6];
+ snprintf(buffer2, sizeof(buffer2), "%u", tetris_state->numLines);
+ canvas_draw_str_aligned(canvas, 61, 8, AlignRight, AlignBottom, buffer2);
+
if(tetris_state->gameState == GameStateGameOver) {
// 128 x 64
canvas_set_color(canvas, ColorWhite);
@@ -476,4 +481,4 @@ int32_t tetris_game_app() {
free(tetris_state);
return 0;
-}
+}
\ No newline at end of file
diff --git a/applications/tictactoe_game/tictactoe_game.c b/applications/tictactoe_game/tictactoe_game.c
new file mode 100644
index 000000000..d69684c1c
--- /dev/null
+++ b/applications/tictactoe_game/tictactoe_game.c
@@ -0,0 +1,349 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define TAG "TicTacToe"
+
+uint8_t selBoxX;
+uint8_t selBoxY;
+
+uint8_t selX = 2;
+uint8_t selY = 2;
+
+uint16_t scoreX;
+uint16_t scoreO;
+
+char player = 'X';
+
+char field[3][3];
+bool fieldx[3][3];
+
+const uint8_t coords[3] = {6, 27, 48};
+
+bool button_state = false;
+
+typedef enum { EventTypeTick, EventTypeKey } EventType;
+
+typedef enum { DirectionUp, DirectionRight, DirectionDown, DirectionLeft } Direction;
+
+typedef enum { GameStatePlaying, GameStateGameOver } GameState;
+
+typedef struct {
+ GameState game_state;
+ osTimerId_t timer;
+} TicTacToeState;
+
+typedef struct {
+ EventType type;
+ InputEvent input;
+} GameEvent;
+
+void drawCross(Canvas* const canvas, uint8_t x, uint8_t y) {
+ canvas_draw_line(canvas, x, y, x + 9, y + 9); // top left - bottom right slash
+ canvas_draw_line(canvas, x + 9, y, x, y + 9); // down left - top right slash
+}
+
+void drawCircle(Canvas* const canvas, uint8_t x, uint8_t y) {
+ canvas_draw_circle(canvas, x + 4, y + 5, 5);
+}
+
+void player_switch() {
+ if(player == 'O') {
+ player = 'X';
+ } else if(player == 'X') {
+ player = 'O';
+ }
+}
+
+void tictactoe_draw(Canvas* canvas) {
+ // Draws the game field
+ canvas_draw_frame(canvas, 0, 0, 64, 64); // frame
+ canvas_draw_line(canvas, 0, 21, 63, 21); // horizontal line
+ canvas_draw_line(canvas, 0, 42, 63, 42); // horizontal line
+ canvas_draw_line(canvas, 21, 0, 21, 63); // vertical line
+ canvas_draw_line(canvas, 42, 0, 42, 63); // vertical line
+
+ // Draws the game field elements (X or O)
+ for(uint8_t i = 0; i <= 2; i++) {
+ for(uint8_t j = 0; j <= 2; j++) {
+ if(field[i][j] == 'O') {
+ drawCircle(canvas, coords[i], coords[j]);
+ } else if(field[i][j] == 'X') {
+ drawCross(canvas, coords[i], coords[j]);
+ }
+ }
+ }
+
+ // Draws the selection box
+ if(selX == 1) {
+ selBoxX = 1;
+ } else if(selX == 2) {
+ selBoxX = 22;
+ } else if(selX == 3) {
+ selBoxX = 43;
+ }
+
+ if(selY == 1) {
+ selBoxY = 1;
+ } else if(selY == 2) {
+ selBoxY = 22;
+ } else if(selY == 3) {
+ selBoxY = 43;
+ }
+
+ canvas_set_color(canvas, ColorBlack);
+ canvas_draw_frame(canvas, selBoxX, selBoxY, 20, 20);
+ canvas_draw_frame(canvas, selBoxX + 1, selBoxY + 1, 18, 18);
+
+ // Draws the sidebar
+ canvas_set_font(canvas, FontPrimary);
+ canvas_draw_str(canvas, 81, 10, "SCORE");
+ canvas_draw_str(canvas, 75, 24, "X:");
+
+ char scoreXBuffer[10];
+ sprintf(scoreXBuffer, "%d", scoreX);
+ canvas_draw_str(canvas, 88, 24, scoreXBuffer);
+ canvas_draw_str(canvas, 75, 35, "O:");
+
+ char scoreOBuffer[10];
+ sprintf(scoreOBuffer, "%d", scoreO);
+ canvas_draw_str(canvas, 88, 35, scoreOBuffer);
+
+ canvas_set_font(canvas, FontSecondary);
+ canvas_draw_str(canvas, 75, 46, "Player:");
+
+ if(player == 'X') {
+ drawCross(canvas, 93, 50);
+ } else if(player == 'O') {
+ drawCircle(canvas, 93, 50);
+ }
+}
+
+void clear_game_field() {
+ // Clears the game field arrays
+ for(uint8_t i = 0; i <= 2; i++) {
+ for(uint8_t j = 0; j <= 2; j++) {
+ field[i][j] = ' ';
+ fieldx[i][j] = false;
+ }
+ }
+
+ selX = 2; // Centers the selection box on X axis
+ selY = 2; // Centers the selection box on Y axis
+}
+
+void reset_game_data() {
+ scoreO = 0;
+ scoreX = 0;
+ player = 'X';
+}
+
+void draw_win(Canvas* canvas, char player) {
+ // Handles the score table
+ if(player == 'X') {
+ scoreX++;
+ } else if(player == 'O') {
+ scoreO++;
+ }
+
+ // Switches the players
+ player_switch();
+
+ // Draws the board with players switched
+ tictactoe_draw(canvas);
+
+ // Clear the game field
+ clear_game_field();
+
+ // Draw the new board
+ tictactoe_draw(canvas);
+}
+
+static void tictactoe_state_init(TicTacToeState* const tictactoe_state) {
+ // Set the initial game state
+ tictactoe_state->game_state = GameStatePlaying;
+
+ clear_game_field();
+
+ reset_game_data();
+}
+
+static void tictactoe_draw_callback(Canvas* const canvas, void* ctx) {
+ const TicTacToeState* tictactoe_state = acquire_mutex((ValueMutex*)ctx, 25);
+ if(tictactoe_state == NULL) {
+ return;
+ }
+
+ if(selX > 3) {
+ selX = 3;
+ } else if(selX < 1) {
+ selX = 1;
+ }
+
+ if(selY > 3) {
+ selY = 3;
+ } else if(selY < 1) {
+ selY = 1;
+ }
+
+ // Assigns the game field elements their value (X or O) when the OK button is pressed
+ if(button_state) {
+ button_state = false;
+ for(uint8_t i = 0; i <= 2; i++) {
+ for(uint8_t j = 0; j <= 2; j++) {
+ if((selX == i + 1) && (selY == j + 1) && (fieldx[i][j] == false)) {
+ if(player == 'X') {
+ field[i][j] = 'X';
+ fieldx[i][j] = true;
+ player_switch();
+ } else if(player == 'O') {
+ field[i][j] = 'O';
+ fieldx[i][j] = true;
+ player_switch();
+ }
+ }
+ }
+ }
+ }
+
+ // Checks the game field for winning combinations
+ if((field[0][0] == 'X') && (field[1][0] == 'X') && (field[2][0] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[0][1] == 'X') && (field[1][1] == 'X') && (field[2][1] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[0][2] == 'X') && (field[1][2] == 'X') && (field[2][2] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[0][0] == 'X') && (field[0][1] == 'X') && (field[0][2] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[1][0] == 'X') && (field[1][1] == 'X') && (field[1][2] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[2][0] == 'X') && (field[2][1] == 'X') && (field[2][2] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[0][0] == 'X') && (field[1][1] == 'X') && (field[2][2] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[2][0] == 'X') && (field[1][1] == 'X') && (field[0][2] == 'X')) {
+ draw_win(canvas, 'X');
+ } else if((field[0][0] == 'O') && (field[1][0] == 'O') && (field[2][0] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if((field[0][1] == 'O') && (field[1][1] == 'O') && (field[2][1] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if((field[0][2] == 'O') && (field[1][2] == 'O') && (field[2][2] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if((field[0][0] == 'O') && (field[0][1] == 'O') && (field[0][2] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if((field[1][0] == 'O') && (field[1][1] == 'O') && (field[1][2] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if((field[2][0] == 'O') && (field[2][1] == 'O') && (field[2][2] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if((field[0][0] == 'O') && (field[1][1] == 'O') && (field[2][2] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if((field[2][0] == 'O') && (field[1][1] == 'O') && (field[0][2] == 'O')) {
+ draw_win(canvas, 'O');
+ } else if(
+ (fieldx[0][0] == true) && (fieldx[0][1] == true) && (fieldx[0][2] == true) &&
+ (fieldx[1][0] == true) && (fieldx[1][1] == true) && (fieldx[1][2] == true) &&
+ (fieldx[2][0] == true) && (fieldx[2][1] == true) && (fieldx[2][2] == true)) {
+ draw_win(canvas, 'T');
+ }
+
+ tictactoe_draw(canvas);
+
+ release_mutex((ValueMutex*)ctx, tictactoe_state);
+}
+
+static void tictactoe_input_callback(InputEvent* input_event, osMessageQueueId_t event_queue) {
+ furi_assert(event_queue);
+
+ GameEvent event = {.type = EventTypeKey, .input = *input_event};
+ osMessageQueuePut(event_queue, &event, 0, osWaitForever);
+}
+
+static void tictactoe_update_timer_callback(osMessageQueueId_t event_queue) {
+ furi_assert(event_queue);
+
+ GameEvent event = {.type = EventTypeTick};
+ osMessageQueuePut(event_queue, &event, 0, 0);
+}
+
+int32_t tictactoe_game_app(void* p) {
+ osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(GameEvent), NULL);
+
+ TicTacToeState* tictactoe_state = malloc(sizeof(TicTacToeState));
+
+ ValueMutex state_mutex;
+ if(!init_mutex(&state_mutex, tictactoe_state, sizeof(TicTacToeState))) {
+ FURI_LOG_E(TAG, "Cannot create mutex\r\n");
+ free(tictactoe_state);
+ return 255;
+ }
+
+ // Set system callbacks
+ ViewPort* view_port = view_port_alloc();
+ view_port_draw_callback_set(view_port, tictactoe_draw_callback, &state_mutex);
+ view_port_input_callback_set(view_port, tictactoe_input_callback, event_queue);
+
+ tictactoe_state->timer =
+ osTimerNew(tictactoe_update_timer_callback, osTimerPeriodic, event_queue, NULL);
+ osTimerStart(tictactoe_state->timer, osKernelGetTickFreq() / 22);
+
+ tictactoe_state_init(tictactoe_state);
+
+ // Open GUI and register view_port
+ Gui* gui = furi_record_open("gui");
+ gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+ GameEvent event;
+ for(bool processing = true; processing;) {
+ osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 100);
+ TicTacToeState* tictactoe_state = (TicTacToeState*)acquire_mutex_block(&state_mutex);
+
+ if(event_status == osOK) {
+ // Key events
+ if(event.type == EventTypeKey) {
+ if(event.input.type == InputTypePress) {
+ switch(event.input.key) {
+ case InputKeyBack:
+ processing = false;
+ break;
+ case InputKeyRight:
+ selX++;
+ break;
+ case InputKeyLeft:
+ selX--;
+ break;
+ case InputKeyUp:
+ selY--;
+ break;
+ case InputKeyDown:
+ selY++;
+ break;
+ case InputKeyOk:
+ button_state = true;
+ break;
+ }
+ }
+ }
+ } else {
+ // Event timeout
+ FURI_LOG_D(TAG, "osMessageQueue: Event timeout");
+ }
+
+ view_port_update(view_port);
+ release_mutex(&state_mutex, tictactoe_state);
+ }
+
+ osTimerDelete(tictactoe_state->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);
+ furi_record_close("notification");
+ delete_mutex(&state_mutex);
+ free(tictactoe_state);
+
+ return 0;
+}
\ No newline at end of file