Maker Pro
Arduino

DIY Arduino Tug of War Game on Homemade 8x8 Led Matrix

January 22, 2022 by Mirko Pavleski
Share
banner

Tug of War is an old game where two players pull on a rope, one player winning by pulling the rope far enough to their own side.

Hardware

Software

1 Arduino IDE

Tools

1 Soldering iron (generic)
1 Solder Wire, Lead Free

Tug of War is an old game where two players pull on a rope, one player winning by pulling the rope far enough to their own side. Now this game is also known to the younger generations thanks to the mega-popular Korean TV series "Squid Game".

Get PCB Prototype Free Trial Order: https://www.pcbgogo.com/promo/from_MirkoPavleskiMK

Today I will show you how to make an electronic version of this game that is played on a homemade 8 on 8 Led Matrix. In this version, the two players press a button as fast as they can to pull the virtual rope to their side. The code is taken from the Andrew R McHugh GitHub page, and the hardware part differs in that the matrix is homemade and consists of 64 5mm LEDs. I also made two robust large Arcade buttons for a better gaming feel. I decided to make these custom buttons because I plan to use them later in my other projects, considering that they are really solid and great to play. In this case, I use the box from one of my previous projects, so the code is minimally modified.

The rules of the game are as follows:

Two players mash their buttons as fast as possible. When one player gets a point, the board flashes, and points to them with an arrow. When one player wins (first to three points), the board flashes and shows the number of the winning player (left = 1, right = 2). Music plays in the background, randomly generated from the E minor. Music speed increases as either player get closer to winning a point.

So, the device is relatively simple to make and contains only a few components:

- Arduino Nano microcontroller

- 8x8 Homemade Led matrix

- buzzer

- and Two buttons

On the link above you can order a PCB so the device would get a professional look.

First, we turn on the game and wait for two vertical lines to appear. At that point, each player has to push their button as fast as they can. The winner of the party is the player who will be the first to draw this line on his side. The winner of the party is the player who will be the first to draw this line on his side. Finally, the device is built into a suitable box made of PVC board with thicknesses of 3mm and 5mm and coated with self-adhesive colored wallpaper.

5.JPG
DSCN4582.JPG
IMG_20211228_210727.jpg
Untitled Sketch_schem.jpg
//
// Project 1 - Tug-Of-War
//
// Code by Andrew R McHugh
// Gadgets, Carnegie Mellon Univeristy, Hudson


/*
   Game Mechanics
   --------------

   Two players mash their buttons as fast as possible
   This creates a tug-of-war with the LED "rope"
   When one player gets a point, the board flashes and points to them with an arrow
   When one player wins (first to three points), the board flashes and shows the number of the winning player (left = 1, right = 2)
   Music plays in the background, randomly generated from the E minor
   Music speed increases as either player gets closer to winning a point

*/

// Include libraries
// Letters for the displayed patterns
// Pitches for the music notes
#include "letters.h"
#include "pitches.h"

// Set global variables
// Devmode turns on Serial.print() comments
bool DEVMODE = false;
// Mute mutes the speaker
bool MUTE = false;

// STATE is a variable that controls the global state of the game
enum States {
  startup,
  play,
  playerWin,
  win,
  postGame,
  score
};
States STATE;

// Setup the pins, mostly in arrays
const int ledPinsRows[] = {4, 3, 0, A0, A1, A2, A3, A4};
const int ledPinsCols[] = {12, 11, 10, 9, 8, 7, 6, 5};//0
const int btnPins[] = {A5, 2};
const int musicPin = 13;

// Define the musical scale to randomly pull from
int music[] {
  // E Minor
  NOTE_E3, NOTE_G3, NOTE_A3, NOTE_B4, NOTE_D4, NOTE_E4, NOTE_G4, NOTE_A4
};
// Number of notes in array
const int noteCount = 8;
// Initializing the position of the current note
int notePosition = 0;

// Durration of notes before speed adjustments, 4 = a quarter note
int noteDurations[] = {
  4, 4, 4, 4, 4, 4, 4, 4
};
// Initialize other music variables
int noteDuration; //defined later in the code
int musicSpeed = 1; //1 is normal, 2 is kinda-fast, three is fast

// Defined button labels for ease of use and increased readability
const int LEFT = 0;
const int RIGHT = 1;

// Set the initial states and last states
int btnStates[] = {HIGH, HIGH}; //{left, right}
int btnStatesLast[] = {LOW, LOW};

// Set the initial player scores
int playerScore[] = {1, 1}; // round scores, to be updated per round, based on the math, they start at 1
int playerScoreInit[] = {1, 1}; // defined initial round scores
int playerScoreMaster[] = {0, 0}; // the master score that can change the overall game state

// Initialize the who won last and who the game winner is
int playerWinLast;
int playerWinner;

// Initialize the total score and win location
int totalScore;
int winLocation; // defines the location of the "rope" on the led matrix

// Initialize time variables, definitions follow in code
unsigned long timeStart;
unsigned long timeLimit;
unsigned long timeMeter;
unsigned long masterClock;
unsigned long lastNotePlayedClock;

// Initialize the playArray which gets drawn on the board
int playArray[8][8];

//
// Updates button states. Sets current-stored to
// last then reads current states
//
void updateBtnStates() {
  btnStatesLast[LEFT] = btnStates[LEFT];
  btnStatesLast[RIGHT] = btnStates[RIGHT];

  btnStates[LEFT] = digitalRead(btnPins[LEFT]);
  btnStates[RIGHT] = digitalRead(btnPins[RIGHT]);
}

// Draws a given array onto the dot matrix. If
// passed an optional variable "inverse", the
// function draws the inverse of the array.
void draw(const int dotArray[8][8], bool inverse = false) {
  
  // Initialize a this variable
  int thisLED = 0;

  // Loop that draws the given array onto the matrix
  for (int thisRow = 0; thisRow < 8; thisRow++) {
    digitalWrite(ledPinsRows[thisRow], LOW);
    for (int thisCol = 0; thisCol < 8; thisCol++) {

      thisLED = dotArray[thisRow][thisCol];

      // checks inverse variable to either make 1s or 0s the drawn dot
      if (inverse == false) {
        if (thisLED == 1) {
          digitalWrite(ledPinsCols[thisCol], HIGH);
        }
      } else if (inverse == true) {
        if (thisLED == 0) {
          digitalWrite(ledPinsCols[thisCol], HIGH);
        }
      }

      // Magic sauce
      digitalWrite(ledPinsCols[thisCol], LOW);
    }
    digitalWrite(ledPinsRows[thisRow], HIGH);
  }

}

// draw() for a specified amount of time (without interupt)
void drawFor(const int dotArray[8][8], int mil, bool inverse = false) {

  // Set time and given duration
  timeStart = millis();
  timeLimit = timeStart + mil;
  timeMeter = timeStart;

  // While the counted time is less than the desired limit, keep drawing the array
  while (timeMeter < timeLimit) {
    // Draw the array
    draw(dotArray, inverse);
    // Update the counted time
    timeMeter = millis();
  }
}

// Specific "draw" for setting the matrix on
void setMatrixOn() {
  for (int thisCol = 0; thisCol < 8; thisCol++) {
    digitalWrite(ledPinsCols[thisCol], HIGH);
  }
  for (int thisRow = 0; thisRow < 8; thisRow++) {
    digitalWrite(ledPinsRows[thisRow], LOW);
  }
}

// Specific "draw" for setting the matrix off
void setMatrixOff() {
  for (int thisCol = 0; thisCol < 8; thisCol++) {
    digitalWrite(ledPinsCols[thisCol], LOW);
  }
  for (int thisRow = 0; thisRow < 8; thisRow++) {
    digitalWrite(ledPinsRows[thisRow], HIGH);
  }
}

// A set of development tests, controlled by case switch
// Not used for production
void testMatrix(int test) {
  switch (test) {
    case 1:
      for (int thisCol = 0; thisCol < 8; thisCol++) {
        for (int thisRow = 0; thisRow < 8; thisRow++) {
          digitalWrite(ledPinsRows[thisRow], LOW);
          digitalWrite(ledPinsCols[thisCol], HIGH);
          delay(20);
          digitalWrite(ledPinsRows[thisRow], HIGH);
          digitalWrite(ledPinsCols[thisCol], LOW);
        }
      }
      break;
    case 2:
      draw(TEST);
      break;
    default:
      setMatrixOn();
      delay(100);
      setMatrixOff();
  }
}

// Turn just one LED on
void ledOn(int row, int col, bool setMatrixOffVar = false) {
  // Check to see if we should start by turning the whole matrix off
  if (setMatrixOffVar == true) {
    setMatrixOff();
  }
  digitalWrite(ledPinsCols[col], HIGH);
  digitalWrite(ledPinsRows[row], LOW);
}

// Turn just one LED off
void ledOff(int row, int col, bool setMatrixOnVar = false) {
  // Check to see if we should start by turning the whole matrix on
  if (setMatrixOnVar == true) {
    setMatrixOn();
  }
  digitalWrite(ledPinsCols[col], LOW);
  digitalWrite(ledPinsRows[row], HIGH);
}
// Update the scores and redraw matrix accordingly
void updateScores() {
  // See who is pressing their button
  // Only adds one point per press, holding doesn't work here
  for (int side = 0; side < 2; side++) {
    if ((btnStates[side] != btnStatesLast[side]) && (btnStates[side] == HIGH)) {
      playerScore[side]++;
      if (DEVMODE == true) {
        Serial.print("Player Score ");
        Serial.print(side);
        Serial.print(playerScore[side]);
        Serial.println();
        delay(10); //edit
      }
    }
  }
  // Can update the buttons states now
  updateBtnStates();

  // Read scores
  // Draw scores based on "winningness"
  // If score is high enough, change state to win

  // The numbers in this if statement are "magic numbers" I played with them and they
  // seem to give an enjoyable experience with variability.
  if ((playerScore[LEFT] > 15) || (playerScore[RIGHT] > 15)) {
    // Because their score gets mapped to a location on the display, winning requires
    // a higher percentage, not pure number. I thus decrement the score (which changes
    // the mapping behavior and constrain this adjustment to keep some stability.
    playerScore[LEFT]  = constrain(playerScore[LEFT]  - 4, 0, playerScore[LEFT] - 4);
    playerScore[RIGHT] = constrain(playerScore[RIGHT] - 4, 0, playerScore[RIGHT] - 4);
  }
  totalScore = playerScore[LEFT] + playerScore[RIGHT];
  // Maps the scores to a usable location. -1 and 9 are the win states
  winLocation = map(playerScore[RIGHT], 0, totalScore, -1, 9);
  
  // Update the music speed based on their "winningness" location on the matrix
  updateMusicSpeed(winLocation);

  // Dev function
  if (DEVMODE == true) {
    Serial.println(winLocation);
  }
  // Win state
  if (winLocation == -1) {
    playerScoreMaster[LEFT] += 1;
    playerWinLast = LEFT;
    STATE = playerWin;
  }
  // Win state
  else if (winLocation == 9) {
    playerScoreMaster[RIGHT] += 1;
    playerWinLast = RIGHT;
    STATE = playerWin;
  }
  // If you don't win, draw the matrix again
  else {
    for (int j = 0; j < 8; j++) {
      for (int i = 0; i < 8; i++) {
        // Selectively draw two lines for the pure center of the matrix
        if ((winLocation == 4) && (j == winLocation)) {
          playArray[i][j] = 1;
          playArray[i][j - 1] = 1;
        }
        // Otherwise draw a single line on either side of center
        else if ((winLocation < 4) && (j == winLocation)) {
          playArray[i][j] = 1;
        }
        else if ((winLocation > 4) && (j == winLocation - 1)) {
          playArray[i][j] = 1;
        }
        else {
          playArray[i][j] = 0;
        }
      }
    }
    // Now that you've prepared the array, draw it
    draw(playArray);
  }
}

// When a player wins a round...
void playerWinSingle() {
  // Flashes the board and points to who won the round
  drawFor(OFF, 300);
  drawFor(ON, 300);
  drawFor(OFF, 300);
  drawFor(ON, 300);
  if (playerWinLast == LEFT) {
    drawFor(ARROW_LEFT, 1000);
  }
  else if (playerWinLast == RIGHT) {
    drawFor(ARROW_RIGHT, 1000);
  }

  // Dev function
  if(DEVMODE == true){
    Serial.print("Player win last ");
    Serial.println(playerWinLast);
    Serial.print("Player winner ");
    Serial.println(playerWinner);
    Serial.print("Player score master left ");
    Serial.println(playerScoreMaster[LEFT]);
    Serial.print("Player score master right ");
    Serial.println(playerScoreMaster[RIGHT]);
  }

  // If one of the players has reached three points, set the overall game state to win
  if ((playerScoreMaster[LEFT] == 3) || (playerScoreMaster[RIGHT] == 3)) {
    if (playerScoreMaster[LEFT] == 3) {
      playerWinner = LEFT;
    }
    else if ((playerScoreMaster[RIGHT] == 3)) {
      playerWinner = RIGHT;
    }
    STATE = win;
  }
  // But if no one wins, set the game state back to play and reset player round scores
  else {
    STATE = play;
    playerScore[LEFT] = playerScoreInit[LEFT];
    playerScore[RIGHT] = playerScoreInit[RIGHT];
  }
}

// If a player wins the entire game...
void playerWinGame() {

  // Flash the board
  drawFor(OFF, 300);
  drawFor(ON, 300);
  drawFor(OFF, 300);
  drawFor(ON, 300);

  // Draw their number if left
  if (playerWinner == LEFT) {
    drawFor(PLAYER_1, 1000);
    drawFor(PLAYER_1,  500, true);
    drawFor(PLAYER_1, 1000);
    drawFor(PLAYER_1,  500, true);
    drawFor(PLAYER_1, 1000);
    drawFor(PLAYER_1,  500, true);
    drawFor(PLAYER_1, 1000);
  }
  // or if right
  else if (playerWinner == RIGHT) {
    drawFor(PLAYER_2, 1000);
    drawFor(PLAYER_2,  500, true);
    drawFor(PLAYER_2, 1000);
    drawFor(PLAYER_2,  500, true);
    drawFor(PLAYER_2, 1000);
    drawFor(PLAYER_2,  500, true);
    drawFor(PLAYER_2, 1000);
  }
}

// Sometimes you have to stop the love
// Shorthand to stop playing the music
void stopMusic() {
  noTone(musicPin);
}

// A small function that updates the music speed based on the
// level of "winningness". More win equals more fast.
void updateMusicSpeed(int winLocation) {
  int absWinLocation = abs(winLocation - 4);
  if (absWinLocation < 2) {
    musicSpeed = 1; // normal speed
  }
  else if (absWinLocation < 3) {
    musicSpeed = 2; // kinda-fast
  }
  else {
    musicSpeed = 3; // real-fast
  }
}

// Function that plays the music
// Optionally, it can run "asyncronously"
// Syncronous function adapted from Melody example
void playMusic(int toneSpeed, bool async = false) {
  // No music if you've muted
  if (MUTE == true) {
    stopMusic();
  }
  else {
    // If you're doing asyncronous playing
    if (async == true) {
      /*
         Get masterclock time
         If masterclock time > lastnoteplayed + noteduration,
           play next note
         Otherwise keep playing the current note
      */

      masterClock = millis();
      noteDuration = (1000 / noteDurations[notePosition]) / toneSpeed;

      if (masterClock > lastNotePlayedClock + noteDuration) {
        if (music[notePosition] == 0) {
          noTone(musicPin);
        }
        else {
          tone(musicPin, music[notePosition]);
        }
        // Choose a random note to play
        notePosition = random(0, 7) % noteCount;
        lastNotePlayedClock = millis();
      }

    }
    // this is basically exactly what was in the Melody example
    else if (async == false) {
      // iterate over the notes of the melody:
      for (int thisNote = 0; thisNote < noteCount; thisNote++) {

        // to calculate the note duration, take one second
        // divided by the note type.
        //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
        noteDuration = (1000 / noteDurations[thisNote]) / toneSpeed;
        tone(musicPin, music[thisNote], noteDuration);

        // to distinguish the notes, set a minimum time between them.
        // the note's duration + 30% seems to work well:
        double pauseBetweenNotes = noteDuration * 1;
        delay(pauseBetweenNotes);
        // stop the tone playing:
        noTone(musicPin);
      }
    }
  }
}

// To restart the game, you can hold both buttons down after someone wins
void restart() {
  updateBtnStates();
  if ((btnStates[LEFT] == HIGH) && (btnStates[LEFT] == HIGH)) {
    playerScore[LEFT] = playerScoreInit[LEFT];
    playerScore[RIGHT] = playerScoreInit[RIGHT];
    playerScoreMaster[LEFT] = 0;
    playerScoreMaster[RIGHT] = 0;
    STATE = startup;
  }

}
// The initial draw function, "loading"
void drawStartup() {
  for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 8; j++) {
      ledOn(i, j, true);
      delay(25);
    }
  }

  playMusic(1);
  playMusic(2);
  playMusic(1);


  STATE = play;
}

// The setup function. It sets up the environment on load.
void setup() {
  masterClock = millis();

  if (DEVMODE == true) {
    Serial.begin(9600);
  }

  // Set LEDs to output
  for (int thisLED = 0; thisLED < 8; thisLED++) {
    pinMode(ledPinsCols[thisLED], OUTPUT);
  };
  for (int thisLED = 0; thisLED < 8; thisLED++) {
    pinMode(ledPinsRows[thisLED], OUTPUT);
  };

  pinMode(musicPin, OUTPUT);

  // Set btns to input
  pinMode(btnPins[LEFT], INPUT);
  pinMode(btnPins[RIGHT], INPUT);

  if (DEVMODE == true) {
    // flash the board to make sure everything is plugged in
    setMatrixOn();
    delay(300);
    setMatrixOff();
    Serial.println("Setup complete");
  }

  // Set game state
  STATE = startup;
}



void loop() {
  // Master switch that controls what happens in which game state
  switch (STATE) {
    case startup:
      drawStartup();
      break;
    case play:
      updateScores();
      playMusic(musicSpeed, true);
      break;
    case playerWin:
      stopMusic();
      playerWinSingle();
      break;
    case win:
      stopMusic();
      playerWinGame();
      restart();
      break;
  }

}

Author

Avatar
Mirko Pavleski

Electronics , Arduino , Physics

Related Content

Categories

Comments


You May Also Like