#include <Arduino_FreeRTOS.h>
#include <queue.h>
#include <semphr.h>
#include <LiquidCrystal.h>
// ======================
// LCD
// ======================
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
// ======================
// PINS
// ======================
#define BTN_UP 2
#define BTN_DOWN 3
#define BTN_LEFT 4
#define BTN_RIGHT 5
#define BTN_SELECT 6
#define BUZZER 13
// ======================
// RTOS
// ======================
QueueHandle_t inputQueue;
SemaphoreHandle_t boardMutex;
// ======================
// GAME STATE
// ======================
char board[3][3];
int cursorX = 0;
int cursorY = 0;
char currentPlayer = 'X';
int scoreX = 0;
int scoreO = 0;
bool soundBusy = false;
bool soundLock = false;
enum GameState {
START_SCREEN,
RUNNING
};
GameState state = START_SCREEN;
// ======================
// WIN LINE STORAGE
// ======================
int winX[3];
int winY[3];
bool hasWinLine = false;
// ======================
// INPUT
// ======================
enum ButtonType {
NONE,
UP,
DOWN,
LEFT,
RIGHT,
SELECT
};
// ======================
// SETUP
// ======================
void setup() {
lcd.begin(20, 4);
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
pinMode(BTN_SELECT, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
inputQueue = xQueueCreate(10, sizeof(ButtonType));
boardMutex = xSemaphoreCreateMutex();
initBoard();
xTaskCreate(TaskInput, "Input", 128, NULL, 2, NULL);
xTaskCreate(TaskGame, "Game", 256, NULL, 2, NULL);
xTaskCreate(TaskDisplay, "Display", 256, NULL, 1, NULL);
vTaskStartScheduler();
}
void loop() {}
// ======================
// INIT
// ======================
void initBoard() {
for (int y = 0; y < 3; y++)
for (int x = 0; x < 3; x++)
board[y][x] = ' ';
}
void playTone(int freq, int dur) {
if (soundBusy || soundLock) return;
soundBusy = true;
soundLock = true;
tone(BUZZER, freq, dur);
vTaskDelay(dur / portTICK_PERIOD_MS);
soundBusy = false;
vTaskDelay(50 / portTICK_PERIOD_MS);
soundLock = false;
}
// ======================
// INPUT TASK
// ======================
void TaskInput(void *pv) {
ButtonType lastBtn = NONE;
while (1) {
ButtonType btn = NONE;
if (!digitalRead(BTN_UP)) btn = UP;
else if (!digitalRead(BTN_DOWN)) btn = DOWN;
else if (!digitalRead(BTN_LEFT)) btn = LEFT;
else if (!digitalRead(BTN_RIGHT)) btn = RIGHT;
else if (!digitalRead(BTN_SELECT)) btn = SELECT;
if (btn != NONE && btn != lastBtn) {
xQueueSend(inputQueue, &btn, 0);
lastBtn = btn;
}
if (btn == NONE) lastBtn = NONE;
vTaskDelay(40 / portTICK_PERIOD_MS);
}
}
// ======================
// WIN CHECK
// ======================
bool checkWin(char p) {
// rows
for (int i = 0; i < 3; i++) {
if (board[i][0] == p && board[i][1] == p && board[i][2] == p) {
winX[0]=i; winY[0]=0;
winX[1]=i; winY[1]=1;
winX[2]=i; winY[2]=2;
hasWinLine = true;
return true;
}
}
// cols
for (int i = 0; i < 3; i++) {
if (board[0][i] == p && board[1][i] == p && board[2][i] == p) {
winX[0]=0; winY[0]=i;
winX[1]=1; winY[1]=i;
winX[2]=2; winY[2]=i;
hasWinLine = true;
return true;
}
}
// diag
if (board[0][0] == p && board[1][1] == p && board[2][2] == p) {
winX[0]=0; winY[0]=0;
winX[1]=1; winY[1]=1;
winX[2]=2; winY[2]=2;
hasWinLine = true;
return true;
}
if (board[0][2] == p && board[1][1] == p && board[2][0] == p) {
winX[0]=0; winY[0]=2;
winX[1]=1; winY[1]=1;
winX[2]=2; winY[2]=0;
hasWinLine = true;
return true;
}
return false;
}
bool isFull() {
for (int y = 0; y < 3; y++)
for (int x = 0; x < 3; x++)
if (board[y][x] == ' ') return false;
return true;
}
// ======================
// GAME TASK
// ======================
void TaskGame(void *pv) {
ButtonType btn;
while (1) {
if (xQueueReceive(inputQueue, &btn, portMAX_DELAY) == pdTRUE) {
if (state == START_SCREEN) {
if (btn == SELECT) {
state = RUNNING;
lcd.clear();
}
continue;
}
if (state != RUNNING) continue;
if (btn == UP && cursorY > 0) cursorY--;
if (btn == DOWN && cursorY < 2) cursorY++;
if (btn == LEFT && cursorX > 0) cursorX--;
if (btn == RIGHT && cursorX < 2) cursorX++;
if (btn == SELECT) {
xSemaphoreTake(boardMutex, portMAX_DELAY);
if (board[cursorY][cursorX] == ' ') {
board[cursorY][cursorX] = currentPlayer;
bool win = checkWin(currentPlayer);
bool draw = isFull();
if (win) {
if (currentPlayer == 'X') scoreX++;
else scoreO++;
animateWin();
resetGame();
}
else if (draw) {
showMessage("DRAW!");
resetGame();
}
else {
currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
}
}
xSemaphoreGive(boardMutex);
}
}
}
}
// ======================
// WIN ANIMATION
// ======================
void animateWin() {
for (int i = 0; i < 3; i++) {
lcd.clear();
bool show = (i % 2 == 0);
for (int y = 0; y < 3; y++) {
lcd.setCursor(0, y);
for (int x = 0; x < 3; x++) {
bool isWinCell = false;
for (int k = 0; k < 3; k++) {
if (winX[k] == y && winY[k] == x) {
isWinCell = true;
}
}
char c = board[y][x];
if (c == ' ') c = '.';
if (isWinCell && show) {
lcd.print('['); lcd.print(c); lcd.print(']');
} else {
lcd.print(' '); lcd.print(c); lcd.print(' ');
}
}
}
playTone(1200 + i * 200, 120);
vTaskDelay(250 / portTICK_PERIOD_MS);
}
hasWinLine = false;
}
// ======================
// DISPLAY TASK
// ======================
void TaskDisplay(void *pv) {
while (1) {
if (state == START_SCREEN) {
lcd.clear();
lcd.setCursor(4, 1);
lcd.print("TIC TAC TOE");
lcd.setCursor(3, 2);
lcd.print("PRESS SELECT");
vTaskDelay(300 / portTICK_PERIOD_MS);
continue;
}
if (state == RUNNING) {
xSemaphoreTake(boardMutex, portMAX_DELAY);
lcd.setCursor(0, 0); lcd.print(" ");
lcd.setCursor(0, 1); lcd.print(" ");
lcd.setCursor(0, 2); lcd.print(" ");
lcd.setCursor(0, 3); lcd.print(" ");
for (int y = 0; y < 3; y++) {
lcd.setCursor(0, y);
for (int x = 0; x < 3; x++) {
char c = board[y][x];
if (c == ' ') c = '.';
if (cursorX == x && cursorY == y) {
lcd.print('['); lcd.print(c); lcd.print(']');
} else {
lcd.print(' '); lcd.print(c); lcd.print(' ');
}
}
}
lcd.setCursor(16, 0);
lcd.print("X-");
lcd.print(scoreX);
lcd.setCursor(16, 1);
lcd.print("O-");
lcd.print(scoreO);
lcd.setCursor(12, 3);
lcd.print("Turn:");
lcd.print(currentPlayer);
xSemaphoreGive(boardMutex);
}
vTaskDelay(150 / portTICK_PERIOD_MS);
}
}
// ======================
// RESET
// ======================
void resetGame() {
initBoard();
cursorX = 0;
cursorY = 0;
currentPlayer = 'X';
}
// ======================
// MESSAGE
// ======================
void showMessage(String msg) {
lcd.clear();
lcd.setCursor(5, 1);
lcd.print(msg);
tone(BUZZER, 1000, 150);
vTaskDelay(150 / portTICK_PERIOD_MS);
tone(BUZZER, 1500, 150);
vTaskDelay(1200 / portTICK_PERIOD_MS);
lcd.clear();
}