This is an old revision of the document!


Tic Tac Toe

Introduction

Name: Manea Andrei Iulian
Group: 331CB
Assistant: Alex Văduva

Description
The project is a physical implementation of the game Tic Tac Toe. Additionally, a speaker will play music in the background for better ambiance, there will be a buzzer to introduce a time limit for each move; it will signal when the player has 5 seconds left. Also, there will be a LED strip for a better visual effect.

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.

General description

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.

Hardware Design

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 Layout

Circuit Design

Circuit Implementation

1. Arduino Uno (Rev3)

  • Role: Central controller. Drives SPI for the display and SD, outputs PWM for audio/buzzer and scans the keypad.
  • Power: Can use its onboard 5 V regulator (from VIN) or the two external AMS1117 regulators shown.

2. Voltage Regulators (U1 & U3)

  • U1 (AMS1117-5 V):
    • Input: 7-12 V
    • Output: 5 V rail feeding:
      • Arduino 5 V pin
      • WS2812 LEDs
      • PAM8403 audio amp
      • Buzzer (through its series resistor)
  • U3 (AMS1117-3.3 V)
    • Input: same VIN
    • Output: 3.3 V rail feeing:
      • TFT module
      • MicroSD module

3. TFT Display

  • Power & Backlight
    • VCC → 3.3 V
    • GND → common ground
    • LED + (backlight) → 5 V
  • SPI Signals (sharing the SPI bus)
    • SCK → D13 (hardware SCK)
    • MOSI → D11
    • CS → D10
    • DC → D9 (Data / Command select)
    • RST → D8

4. MicroSD Card Module

  • Power
    • VCC → 3.3 V
    • GND → common ground
  • SPI (same bus, separate CS)
    • MOSI → D11
    • MISO → D12
    • SCK → D13
    • CS → D4
  • Separate CS allows the Arduino to select either the TFT or the SD card independently.

5. Audio Amplifier Module (PAM8403) + Speaker

  • Power
    • VCC → 5 V
    • GND → common ground
  • Inputs
    • L IN / R IN → PWM-filtered audio from Arduino
  • Outputs
    • L OUT / R OUT → speaker
  • Function: Boosts the Arduino's low-level PWM audio to drive a few-watt speaker.

6. Passive Buzzer

  • Connections
    • One leg → D3 (PWM) through a 220 Ω resistor
    • Other leg → GND
  • Function: Generates tones / alerts by toggling PWM at audio frequencies.

Real-life Circuit

Software Design

  • Environment: Arduino IDE
  • Libraries: Adafruit_GFX; Adafruit_ST7735; TMRpcm

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

Rezultate Obţinute

Care au fost rezultatele obţinute în urma realizării proiectului vostru.

Concluzii

Download

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.

Journal

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

Bibliografie/Resurse

Listă cu documente, datasheet-uri, resurse Internet folosite, eventual grupate pe Resurse Software şi Resurse Hardware.

Export to PDF

pm/prj2025/avaduva/andrei_iulian.manea.1748291417.txt.gz · Last modified: 2025/05/26 23:30 by andrei_iulian.manea
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0