This shows you the differences between two versions of the page.
pm:prj2025:avaduva:andrei_iulian.manea [2025/05/26 23:17] andrei_iulian.manea [Software Design] |
pm:prj2025:avaduva:andrei_iulian.manea [2025/05/27 13:18] (current) andrei_iulian.manea [Download] |
||
---|---|---|---|
Line 30: | Line 30: | ||
1 x speaker\\ | 1 x speaker\\ | ||
1 X buzzer\\ | 1 X buzzer\\ | ||
- | 1 X LED strip\\ | ||
- | 1 X LED\\ | ||
- | 3 X MOSFET N-MOS IRF540N transistor\\ | ||
1 X 220Ohm resistor\\ | 1 X 220Ohm resistor\\ | ||
1 x XPT8871 mono audio amplifier\\ | 1 x XPT8871 mono audio amplifier\\ | ||
Line 38: | Line 35: | ||
**Circuit Layout**\\ | **Circuit Layout**\\ | ||
- | {{ :pm:prj2025:avaduva:pm_schema_cablaj_andrei_manea_2.jpg?300 |}} | + | {{ :pm:prj2025:avaduva:pm_schema_electrica_andrei_manea_final.png?300 |}} |
**Circuit Design**\\ | **Circuit Design**\\ | ||
- | {{ :pm:prj2025:avaduva:pm_schema_electrica_andrei_manea.jpg?300 |}} | + | {{ :pm:prj2025:avaduva:pm_schema_electrica_andrei_manea_final2.png?300 |}} |
**Circuit Implementation** | **Circuit Implementation** | ||
Line 115: | Line 112: | ||
</note> | </note> | ||
- | <code> | + | 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. |
- | #include <Adafruit_GFX.h> | + | |
- | #include <Adafruit_ST7735.h> | + | |
- | #include <SPI.h> | + | |
+ | 1. Tic-Tac-Toe Game | ||
+ | * setup() | ||
+ | * Initializes the TFT display via SPI (tft.initR()), clears the screen. | ||
+ | * Configures buttons (GPIO inputs with pull-ups), the buzzer pin (GPIO output), and seeds the random number generator using an ADC read. | ||
+ | * Calculates the grid’s starting coordinates, resets the game board, and starts the first turn timer. | ||
+ | * loop() | ||
+ | * End-of-Game Handling: If gameover is true, it shows either “Player X wins!”, “Player O wins!”, or “Draw!”; then waits resetDelay ms before clearing and restarting. | ||
+ | * Turn Timer | ||
+ | * Between warningThreshold and moveTimeout: sounds the buzzer via tone() (PWM) and displays a countdown once per second. | ||
+ | * After moveTimeout: automatically picks a random free cell for the current player, draws the mark, and checks for win/draw. If the game continues, it swaps players and resets the turn timer. | ||
+ | * Otherwise: silences the buzzer and clears any warning message. | ||
+ | * Input & Rendering: Reads button presses (debounced in software) to move the cursor or place a mark, then highlights the current cell and redraws any X/O. | ||
+ | * Supporting Functions | ||
+ | * Board management: resetBoard(), drawGrid(), highlightCell(), drawMark() | ||
+ | * Game logic: moveCursor(), placeMark(), checkWin(), isBoardFull() | ||
+ | * Turn management: startTurn(), clearWarningArea(), displayTimerWarning(), displayWinner(), displayDraw() | ||
+ | * Random fallback: randomMove() for timeouts | ||
- | #define GRID_SIZE 3 | + | 2. Speaker |
- | #define CELL_SIZE 30 | + | * setup() |
- | #define Random A0 | + | * Initializes the SD card over SPI (chip-select on pin 10). |
- | #define KEYBOARD_PIN4 A4 | + | * Configures the TMRpcm library’s CS and speaker pins and sets playback volume. |
- | #define KEYBOARD_PIN1 A1 | + | * loop() |
- | #define KEYBOARD_PIN2 A2 | + | * If no audio is currently playing, calls audio.play("powerup.wav"). |
- | #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); | + | Here is the full code: [[https://github.com/LilAndy2/PM_Project]] |
- | char board[GRID_SIZE][GRID_SIZE]; | + | ===== Results ===== |
- | 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; | + | |
+ | Here is a short demo of the game in action. | ||
- | void setup() { | + | [[https://youtube.com/shorts/_YSpUl_ECTo?feature=share]] |
- | 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); | + | |
- | } | + | |
- | </code> | + | |
- | + | ||
- | ===== Rezultate Obţinute ===== | + | |
- | + | ||
- | <note tip> | + | |
- | Care au fost rezultatele obţinute în urma realizării proiectului vostru. | + | |
- | </note> | + | |
- | + | ||
- | ===== Concluzii ===== | + | |
+ | ===== Conclusions ===== | ||
+ | * It was a cute project which took me a lot of time to make. | ||
+ | * I had to learn the basics of Arduino from scratch and try to make something functional. | ||
+ | * While the aesthetic can be improved by adding colors and decorations, it is the best I could do in the time given. | ||
+ | * While the experience was very frustrating at times, it was also fun to learn something new. | ||
===== Download ===== | ===== Download ===== | ||
- | <note warning> | + | Here is an archive with all the files of the project. |
- | O arhivă (sau mai multe dacă este cazul) cu fişierele obţinute în urma realizării proiectului: surse, scheme, etc. Un fişier README, un ChangeLog, un script de compilare şi copiere automată pe uC crează întotdeauna o impresie bună ;-). | + | |
- | 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**. | + | {{:pm:prj2025:avaduva:pm_project_andrei_manea.zip|}} |
- | </note> | + | |
===== Journal ===== | ===== Journal ===== | ||
Line 430: | Line 164: | ||
19.05.2025 - uploaded circuit design and circuit layout\\ | 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\\ | 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 | + | 26.05.2025 - implemented the software\\ |
- | ===== Bibliografie/Resurse ===== | + | 27.05.2025 - filmed the demo of the game |
+ | ===== Bibliography / Resources ===== | ||
- | <note> | + | __Hardware Resources__ |
- | Listă cu documente, datasheet-uri, resurse Internet folosite, eventual grupate pe **Resurse Software** şi **Resurse Hardware**. | + | * https://docs.arduino.cc/resources/datasheets/A000066-datasheet.pdf |
- | </note> | + | * https://shorturl.at/ccQdk |
+ | * https://www.electrokit.com/upload/product/41015/41015739/41015739_-_SD_Card_Module.pdf | ||
+ | |||
+ | __Software Resources__ | ||
+ | * https://cdn-learn.adafruit.com/downloads/pdf/adafruit-gfx-graphics-library.pdf | ||
+ | * https://www.tinkercad.com/projects/Tic-Tac-Toe-Game-on-Arduino-for-Beginners | ||
<html><a class="media mediafile mf_pdf" href="?do=export_pdf">Export to PDF</a></html> | <html><a class="media mediafile mf_pdf" href="?do=export_pdf">Export to PDF</a></html> | ||