Hardware components | ||||||
| × | 1 | ||||
| × | 64 | ||||
| × | 8 | ||||
| × | 2 | ||||
| × | 2 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
|
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.
//
// 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;
}
}
/*************************************************
* Public Constants
*
* This note table was originally written by Brett
* Hagman, on whose work the tone() command was
* based.
*
*************************************************/
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978
const int OFF[8][8] = {
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0}
};
const int ON[8][8] = {
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1}
};
const int LETTER_O[8][8] = {
{0, 1, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 0, 0, 1, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 1, 0, 0, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 1, 0}
};
const int LETTER_K[8][8] = {
{0, 1, 1, 0, 0, 0, 1, 1},
{0, 1, 1, 0, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0, 0},
{0, 1, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 0, 1, 1, 0, 0},
{0, 1, 1, 0, 0, 1, 1, 0},
{0, 1, 1, 0, 0, 0, 1, 1},
{0, 1, 1, 0, 0, 0, 0, 1}
};
const int TEST[8][8] = {
{0, 1, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 0, 0, 1, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 1, 1},
{1, 1, 1, 0, 0, 1, 1, 1},
{0, 1, 1, 1, 1, 1, 1, 0}
};
const int ARROW_LEFT[8][8] = {
{0, 0, 0, 1, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0, 0},
{0, 1, 0, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{0, 1, 0, 1, 1, 1, 1, 1},
{0, 0, 1, 1, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 0}
};
const int ARROW_RIGHT[8][8] = {
{0, 0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 1, 1, 0, 0},
{1, 1, 1, 1, 1, 0, 1, 0},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 0, 1, 0},
{0, 0, 0, 0, 1, 1, 0, 0},
{0, 0, 0, 0, 1, 0, 0, 0}
};
const int PLAYER_1[8][8] = {
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 1, 1, 1, 0, 0}
};
const int PLAYER_2[8][8] = {
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0, 0},
{0, 0, 1, 1, 1, 1, 0, 0}
};
Comments