This is an old revision of the document!
Motivation
I think this is a fun game that I always used to play as a kid, so it's an additional motivation for me to implement it. It's simple, yet effective, and the fact that I can have „a pocket version” of it would mean a lot to me.
I used two Arduino UNO R3 as µCs. For inputs, I will have a keyboard with four buttons for moving around the Tic Tac Toe grid and an additional button for selecting in which tile a symbol will go. The music will be played using microSD card reader for the input and a speaker for the output. A buzzer will also be used to signal the last five seconds until the player will have to make a move and the LED Strip will be used for visual enhancement. Finally, an LCD will be used to display the actual game.
Materials:
2 x Arduino UNO R3
2 x mini breadboard
1 x button
1 x keyboard with 4 buttons
1 x LCD 1.44” SPI and ST7735 controller
1 x microSD/SDHC card module reader
1 x SDHC card
1 x speaker
1 X buzzer
1 X LED strip
1 X LED
3 X MOSFET N-MOS IRF540N transistor
1 X 220Ohm resistor
1 x XPT8871 mono audio amplifier
plenty of male-male and male-female wires
Circuit Implementation
1. Arduino Uno (Rev3)
2. Voltage Regulators (U1 & U3)
3. TFT Display
4. MicroSD Card Module
5. Audio Amplifier Module (PAM8403) + Speaker
6. Passive Buzzer
Given the fact that I have used two Arduino UNO, I have two separate codes. The first one runs the actual game logic and handles the input from the keyboard, allowing the player to move on the board. It also handles the timer for the buzzer, which makes a sound when the player has 2 seconds left for his turn. The second one is used to handle the speaker, taking the input from the microSD card reader.
Here is the code for the actual game.
#include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #include <SPI.h> #define GRID_SIZE 3 #define CELL_SIZE 30 #define Random A0 #define KEYBOARD_PIN4 A4 #define KEYBOARD_PIN1 A1 #define KEYBOARD_PIN2 A2 #define KEYBOARD_PIN3 A3 #define BUZZER_PIN 7 #define TFT_CS 10 #define TFT_RST 8 #define TFT_DC 9 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); char board[GRID_SIZE][GRID_SIZE]; int currPlayer = 0, currRow = 0, currCol = 0, startX, startY; bool gameover = false, displayedEnd = false, lastResultWin = false; unsigned long turnStart = 0, endTime = 0; const unsigned long moveTimeout = 7000; const unsigned long warningThreshold = 5000; const unsigned long resetDelay = 10000; void setup() { Serial.begin(9600); randomSeed(analogRead(Random)); tft.initR(INITR_BLACKTAB); tft.fillScreen(ST77XX_BLACK); startX = (tft.width() - GRID_SIZE * CELL_SIZE) / 2; startY = (tft.height() - GRID_SIZE * CELL_SIZE - 40) / 2; pinMode(KEYBOARD_PIN1, INPUT_PULLUP); pinMode(KEYBOARD_PIN2, INPUT_PULLUP); pinMode(KEYBOARD_PIN3, INPUT_PULLUP); pinMode(KEYBOARD_PIN4, INPUT_PULLUP); pinMode(BUZZER_PIN, OUTPUT); resetBoard(); startTurn(); } void loop() { if (gameover) { unsigned long now = millis(); if (!displayedEnd) { tft.fillScreen(ST77XX_BLACK); if (lastResultWin) displayWinner(currPlayer == 0 ? 'X' : 'O'); else displayDraw(); displayedEnd = true; endTime = now; noTone(BUZZER_PIN); } else if (now - endTime > resetDelay) { gameover = false; displayedEnd = false; currPlayer = 0; currRow = currCol = 0; tft.fillScreen(ST77XX_BLACK); resetBoard(); startTurn(); } return; } static int lastSecs = -1; unsigned long elapsed = millis() - turnStart; if (elapsed >= warningThreshold && elapsed < moveTimeout) { tone(BUZZER_PIN, 1000); int secs = (moveTimeout - elapsed + 500) / 1000; if (secs != lastSecs) { clearWarningArea(); lastSecs = secs; } displayTimerWarning(secs); } else if (elapsed >= moveTimeout) { noTone(BUZZER_PIN); int freeCount = 0; int freeCells[GRID_SIZE * GRID_SIZE][2]; for (int r = 0; r < GRID_SIZE; r++) { for (int c = 0; c < GRID_SIZE; c++) { if (board[r][c] == ' ') { freeCells[freeCount][0] = r; freeCells[freeCount][1] = c; freeCount++; } } } if (freeCount > 0) { int idx = random(freeCount); int r = freeCells[idx][0]; int c = freeCells[idx][1]; board[r][c] = (currPlayer == 0 ? 'X' : 'O'); drawMark(r, c); if (checkWin(r, c)) { lastResultWin = true; gameover = true; } else if (isBoardFull()) { lastResultWin = false; gameover = true; } else { currPlayer = 1 - currPlayer; startTurn(); } } return; } else { noTone(BUZZER_PIN); lastSecs = -1; clearWarningArea(); } readButtons(); highlightCell(currRow, currCol); highlightCell(currRow, currCol); } void readButtons() { static unsigned long lastDebounce = 0; const unsigned long delayMs = 200; if (millis() - lastDebounce < delayMs) return; if (digitalRead(KEYBOARD_PIN1) == LOW) { lastDebounce = millis(); moveCursor(-1, 0); } else if (digitalRead(KEYBOARD_PIN2) == LOW) { lastDebounce = millis(); moveCursor(1, 0); } else if (digitalRead(KEYBOARD_PIN3) == LOW) { lastDebounce = millis(); placeMark(); } else if (digitalRead(KEYBOARD_PIN4) == LOW) { lastDebounce = millis(); moveCursor(0, 1); } } void drawGrid() { for (int i = 1; i < GRID_SIZE; i++) { tft.drawLine(startX + i * CELL_SIZE, startY,startX + i * CELL_SIZE, startY + GRID_SIZE * CELL_SIZE, ST77XX_WHITE); tft.drawLine(startX, startY + i * CELL_SIZE,startX + GRID_SIZE * CELL_SIZE, startY + i * CELL_SIZE, ST77XX_WHITE); } } void resetBoard() { for (int i = 0; i < GRID_SIZE; i++) for (int j = 0; j < GRID_SIZE; j++) board[i][j] = ' '; drawGrid(); highlightCell(currRow, currCol); } void highlightCell(int row, int col) { for (int i = 0; i < GRID_SIZE; i++) for (int j = 0; j < GRID_SIZE; j++) { int x = startX + j * CELL_SIZE; int y = startY + i * CELL_SIZE; tft.drawRect(x, y, CELL_SIZE, CELL_SIZE, ST77XX_WHITE); if (board[i][j] != ' ') drawMark(i, j); } int x = startX + col * CELL_SIZE; int y = startY + row * CELL_SIZE; tft.drawRect(x, y, CELL_SIZE, CELL_SIZE, ST77XX_BLUE); if (board[row][col] != ' ') drawMark(row, col); } void moveCursor(int dx, int dy) { currCol = (currCol + dx + GRID_SIZE) % GRID_SIZE; currRow = (currRow + dy + GRID_SIZE) % GRID_SIZE; } void placeMark() { if (board[currRow][currCol] != ' ') return; board[currRow][currCol] = currPlayer == 0 ? 'X' : 'O'; drawMark(currRow, currCol); if (checkWin(currRow, currCol)) { lastResultWin = true; gameover = true; return; } if (isBoardFull()) { lastResultWin = false; gameover = true; return; } currPlayer = 1 - currPlayer; startTurn(); } void drawMark(int row, int col) { int x = startX + col * CELL_SIZE + CELL_SIZE/2 - 5; int y = startY + row * CELL_SIZE + CELL_SIZE/2 - 8; tft.setCursor(x, y); tft.setTextColor(ST77XX_WHITE); tft.setTextSize(2); tft.print(board[row][col]); } bool checkWin(int r, int c) { char m = board[r][c]; for (int i = 0; i < GRID_SIZE; i++) if (board[r][i] != m) goto col; return true; col:; for (int i = 0; i < GRID_SIZE; i++) if (board[i][c] != m) goto diag1; return true; diag1:; if (board[0][0]==m && board[1][1]==m && board[2][2]==m) return true; if (board[0][2]==m && board[1][1]==m && board[2][0]==m) return true; return false; } bool isBoardFull() { for (int i = 0; i < GRID_SIZE; i++) for (int j = 0; j < GRID_SIZE; j++) if (board[i][j] == ' ') return false; return true; } void startTurn() { turnStart = millis(); clearWarningArea(); } void clearWarningArea() { int16_t x = startX; int16_t y = startY + GRID_SIZE * CELL_SIZE + 5; uint16_t w = GRID_SIZE * CELL_SIZE; uint16_t h = 10; tft.fillRect(x, y, w, h, ST77XX_BLACK); } void displayTimerWarning(int secs) { String txt = String((currPlayer==0?"X":"O")) + ": " + String(secs) + "s left"; tft.setTextSize(1); int16_t x1, y1; uint16_t w, h; tft.getTextBounds(txt.c_str(), 0, 0, &x1, &y1, &w, &h); int16_t cursorX = startX + (GRID_SIZE * CELL_SIZE - w) / 2; int16_t cursorY = startY + GRID_SIZE * CELL_SIZE + 5; tft.setTextColor(ST77XX_BLUE); tft.setCursor(cursorX, cursorY); tft.print(txt); } void displayWinner(char w) { uint16_t wdt, ht; int16_t x1, y1; String msg = "Player "; msg += w; msg += " wins!"; tft.setTextSize(1); tft.setTextColor(ST77XX_WHITE); tft.getTextBounds(msg.c_str(), 0, 0, &x1, &y1, &wdt, &ht); tft.setCursor((tft.width()-wdt)/2, (tft.height()-ht)/2); tft.print(msg); } void displayDraw() { int16_t x1, y1; uint16_t wdt, ht; tft.setTextSize(2); tft.setTextColor(ST77XX_WHITE); String msg = "Draw!"; tft.getTextBounds(msg.c_str(), 0, 0, &x1, &y1, &wdt, &ht); tft.setCursor((tft.width()-wdt)/2, (tft.height()-ht)/2); tft.print(msg); }
Here is the code for the speaker:
test
Fişierele se încarcă pe wiki folosind facilitatea Add Images or other files. Namespace-ul în care se încarcă fişierele este de tipul :pm:prj20??:c? sau :pm:prj20??:c?:nume_student (dacă este cazul). Exemplu: Dumitru Alin, 331CC → :pm:prj2009:cc:dumitru_alin.
06.05.2025 - decided on the project, wrote the description and the hardware materials
11.05.2025 - materials bought, starting on the hardware design
18.05.2025 - finished hardware design
19.05.2025 - uploaded circuit design and circuit layout
23.05.2025 - decided that LED strip component does not fit with the current hardware, so I removed it
26.05.2025 - implemented the software