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

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

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.1748290646.txt.gz · Last modified: 2025/05/26 23:17 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