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
#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); }
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