Whack a Mole
Initially started this project with a view to replicate an arcade Whack A Mole machine. However the mole mechanism proved too costly and buying spares from suppliers were around £80 a mole. I therefore opted for a version that used illuminated buttons to hit instead of moles.
In addition, I also wanted a machine that I could use on a table top or on a stand and the stand needed to be easily disassembled so it would take up less room for storage.
The game last 30 seconds and you get 10 points for hitting lit light and -5 points for hitting unlit light. Game initially starts with just one light at a time but this increases as score goes up. Time buttons are lit decreases as score goes up so game gets more difficult as score increases.
If you score 500 or more then you get an additional 15 seconds.
When game is started, there is a five second countdown and during last 5 seconds of game score flashes to warn time is running out.
Video footage of actual game can be found on Youtube
Supplies
Electronics:
1 x Arduino for controlling the game
8 x 50mm illuminated green LED arcade buttons
1 x arcade button for starting game
1 x 12V power supply (green button lights needed 12V supply)
8 x Relays (SRD-05VDC-SL-C)
7 segment LED display
Electrical connector Strips
Breadboard for Relays
Wiring
Miscellaneous:
8 Roofing Bolts and 8 Pronged Tee Nuts
Wood for frame
12mm plywood for top of Whack A Mole
3mm Hardboard for sides of game
Paint, Black Permanent Marker
Stand for Whack a Mole
There are two lengths of wood (front and back panels) to support Whack A Mole unit when it is placed inside the stand. Pronged Tee nuts are used on the frame so when it is dismantled that the bolts are not lost.
Outside is stand is painted with gloss. Black outlines and black details were done using Black permanent marker.
Underside of Whack a Mole
This shows the underside of the desktop unit. Arduino and relays are mounted on wooden rail at rear. The underside of the arcade buttons can be seen on the above photo
Arduino Wiring
The photos above tend to indicate a bit of chaotic wiring but this is because I have wired everything through connector blocks to simplify any fault finding.
From the Arduino there are 9 inputs (eight for light switches and on for start button) and 8 outputs.
Input pins in this photo are pins 22, 24,26, 28, 30, 32, 34, 36 & 38
Output pins are 39, 41, 43, 45, 47, 49, 51, 53
The inputs are wired to the first connector block and a 10k Ohm resistor sits between that and the next connector block. There is a yellow wire from 2nd connector block for each of the connectors (after the resistor) connected back the GND connector.
Outputs are connected to the lower connector block that is then connected to the relays.
Circuit diagram is included on attached PDF
Downloads
Arduino Sketch Coding
This is the Arduino code that I have written for the game.
On powering up the first thing it does is rotate through the LED's on the 7 digit display to check that all is ok. Next it then switches on each LED one at a time so there is a visual check everything is okay on startup.
#include "Adafruit_LEDBackpack.h"
#include "Adafruit_GFX.h"
Adafruit_7segment matrix = Adafruit_7segment();
const int startLitTime = 3000; // How long Moles Stay Lit at Start of Games
const int Level2000ms = 50; // Score when Moles Lit Time changes to 2 seconds
const int Level1000ms = 200; // Score when Moles Lit Time changes to 1 second
const int Level500ms = 400; // Score when Moles Lit Time changes to half a second
const int MoleLevel2 = 100; // Score when number of Moles at a time changes from 1 to 2
const int MoleLevel3 = 300; // Score when number of Moles at a time changes from 2 to 3
int buttonPin[8] = {22, 24, 26, 28, 30, 32, 34, 36}; // Button Switches will be wired back to these pins
int buttonLight[8] = {39, 41, 43, 45, 47, 49, 51, 53}; // Pins to trigger relays for Button Lights
int buttonState[8] = {0, 0, 0, 0, 0, 0, 0, 0}; // State of Input Switches to detect whe button is pressed
int lastButtonState[8] = {0, 0, 0, 0, 0, 0, 0, 0}; // State of button last time looped.
int startButton = 40;
int startButtonState = 0;
boolean moleActive[8] = {false, false, false, false, false, false, false, false}; // Indicator if Mole should be active
long lastPressed[8]; // Time of when key was last pressed. Used to eliminate stuttering when switch contact is made
long moleStart[8]; // Start of mole lit time
long moleEnd[8]; // Expiry timeo mole's lit time
boolean Started = false;
boolean Finished;
int molesLit = 0; // Number of moles lit
int score = 0;
int hiScore = 100;
int newMole;
long startTimer;
int timeToPlay = 30;
long idleStart; // Used to calculate how long since idle on start up
boolean bonusUsed = false;
void setup() {
// initialize serial communication:
Serial.begin(9600);
//Adafruit 7-segment backpack
#ifndef __AVR_ATtiny85__
// Serial.begin(9600);
// Serial.println("7 Segment Backpack Test");
#endif
matrix.begin(0x70);
MatrixTest();
matrix.drawColon(false);
matrix.clear();
delay(1000);
// initialize the button pin as a input:
for (int i=0; i <= 7; i++){
Serial.println((String)"Initializing i="+i+". INPUT Pin="+buttonPin[5]);
pinMode (buttonPin[5], INPUT);
}
// Initialize Start button
pinMode (startButton, INPUT);
// Initialize the button pin as a input:
for (int i=0; i <= 7; i++){
Serial.print ("Initializing OUTPUT Pin ");
Serial.println (i);
pinMode (buttonLight[i], OUTPUT);
Serial.println ((String)"i="+i+" Pin:"+buttonLight[i]+" LOW");
digitalWrite(buttonLight[i], LOW);
delay (100);
Serial.println ((String)"i="+i+" Pin:"+buttonLight[i]+" HIGH");
digitalWrite(buttonLight[i], HIGH);
delay (510);
digitalWrite(buttonLight[i], LOW);
}
delay(3000);
idleStart = millis();
}
void loop() {
int secs;
int i8;
int i600;
secs = int((millis()-idleStart)/1000);
i8 = secs-(8*(int(secs/8))); // 0 to 7 depending on time since idle
if (i8<=3) {
// 0 to 3
matrix.clear();
matrix.print(hiScore, DEC);
matrix.writeDisplay();
matrix.blinkRate(2);
}
else {
// 4 to 7
UpdateScore();
matrix.blinkRate(0);
}
if (Started) {
StartGame();
Started = false;
}
startButtonState = digitalRead(startButton);
// Serial.println(startButtonState);
if (startButtonState == 1) {
Started = true;
}
i600 = secs-(600*(int(secs/600))); //every 10 minutes
//Serial.println(i600);
if (i600 == 599) {
// then idle for 10 minutes
// so make a noise to attract attention
//DUM 1
digitalWrite(buttonLight[0], HIGH);
delay (10);
digitalWrite(buttonLight[0], LOW);
delay (150);
//DIDDY 2 3
digitalWrite(buttonLight[2], HIGH);
delay (10);
digitalWrite(buttonLight[2], LOW);
delay (350);
digitalWrite(buttonLight[1], HIGH);
delay (10);
digitalWrite(buttonLight[1], LOW);
delay (250);
//DUMDUM 4 5
digitalWrite(buttonLight[5], HIGH);
delay (10);
digitalWrite(buttonLight[5], LOW);
delay (250);
digitalWrite(buttonLight[7], HIGH);
delay (10);
digitalWrite(buttonLight[7], LOW);
delay (650);
//DUM DUM 6 7
digitalWrite(buttonLight[6], HIGH);
delay (10);
digitalWrite(buttonLight[6], LOW);
delay (400);
digitalWrite(buttonLight[6], HIGH);
delay (10);
digitalWrite(buttonLight[6], LOW);
delay (3000);
}
}
void StartGame(){
int i;
int timeLeft;
long nowtime;
long keytime;
long startTimeLeft;
randomSeed(analogRead(0));
matrix.blinkRate(1);
matrix.print(5555, DEC);
matrix.writeDisplay();
delay(1000);
matrix.print(4444, DEC);
matrix.writeDisplay();
delay(1000);
matrix.print(3333, DEC);
matrix.writeDisplay();
delay(1000);
matrix.print(2222, DEC);
matrix.writeDisplay();
delay(1000);
matrix.print(1111, DEC);
matrix.writeDisplay();
delay(1000);
matrix.clear();
delay (1000);
matrix.blinkRate(0);
molesLit=0;
score=0;
timeToPlay = 30;
bonusUsed = false;
UpdateScore();
Finished = false;
Serial.println ("Starting Game");
for (int i=0; i <= 7; i++){
buttonState[i]=0;
lastButtonState[i]=0;
moleActive[i]=false;
}
startTimer=millis();
AddMole();
do
{
for (int i=0; i <= 7; i++){
buttonState[i] = digitalRead(buttonPin[i]);
if (buttonState[i]==1 && lastButtonState[i] == 0) { // then key has been pressed as state changed from 0 to 1
keytime=millis()-lastPressed[i];
if (keytime <= 400) { // then ignore as likely jitter\stutter
// do nothing
}
else {
lastPressed[i] = millis();
Serial.print ((String)"Button "+i+" pressed: "+buttonState[i]);
if (moleActive[i]==true) {
score=score+10;
UpdateScore();
// Set new mole first so current active one not selected;
AddMole();
molesLit = molesLit - 1;
moleActive[i]=0;
// const MoleLevel2 = 100;
// const MoleLevel3 = 300;
digitalWrite(buttonLight[i], LOW);
if (score>=MoleLevel2 && molesLit<=1) {
AddMole();
}
if (score>=MoleLevel3 && molesLit<=2) {
AddMole();
}
// Now check if score over 500 then add 15 seconds
if (bonusUsed == false && score>=500) {
bonusUsed=true;
timeToPlay = 45;
}
}
else {
// Penalty as Mole is not Active when button pressed
score=score-5;
if (score < 0) {
score=0;
}
UpdateScore();
} // End Else
}
}
// if (buttonState[i]==0 && lastButtonState[i] == 1)
// then button released and not need to deal with that event
lastButtonState[i]=buttonState[i];
} // end For
//Check Status of Moles
for (int i=0; i<=7; i++){
if (moleActive[i]) {
nowtime=millis();
if (moleEnd[i] < nowtime) {
Serial.println((String)"Mole "+i+" timed out.");
// Set new mole first so current active one not selected;
AddMole();
molesLit = molesLit - 1;
moleActive[i]=0;
digitalWrite(buttonLight[i], LOW);
}
}
} // End For
timeLeft=timeToPlay-int((millis()-startTimer)/1000);
//Serial.println(timeLeft);
if (timeLeft >=10) {
matrix.blinkRate(0);
}
else
if (timeLeft>=6) {
matrix.blinkRate(1);
}
else {
matrix.blinkRate(2);
}
if (timeLeft <= 0) {
Finished = true;
}
// When game ends, Finished is set to TRUE
} while (Finished == false);
// turn off all lights at end of game
for (int i=0; i <= 7; i++){
digitalWrite(buttonLight[i],LOW);
}
// Wait five seconds to show score
matrix.blinkRate(0);
UpdateScore();
delay(5000);
matrix.blinkRate(0);
UpdateScore();
if (score >= hiScore) {
matrix.blinkRate(2);
hiScore = score;
delay(5000);
}
}
void AddMole(){
int timetolive;
timetolive=startLitTime;
Serial.println((String)"Choosing new mole. Score= "+score+". Moles lit="+molesLit);
Serial.println("Choosing new Mole...");
ChooseMole();
Serial.print("Mole Active: ");
Serial.println(newMole);
moleActive[newMole]=1;
digitalWrite(buttonLight[newMole], HIGH);
// const Level2000ms = 50;
// const Level1000ms = 200;
// const Level500ms = 400;
if (score >= Level2000ms) {
timetolive = 2000;
if (score>= Level1000ms) {
timetolive = 1000;
if (score>=Level500ms) {
timetolive= 500;
}
}
}
moleStart[newMole]=millis();
moleEnd[newMole]=millis()+timetolive;
molesLit=molesLit+1;
//buttonLight[newMole]=HIGH;
}
void ChooseMole(){
int findMole;
boolean finished;
int state;
do {
finished=false;
findMole=random(8);
if (moleActive[findMole]==1) {
//Serial.println("Is Zero");
finished=false;
}
else {
//Serial.println("Is One");
finished=true;
}
state = digitalRead(buttonPin[findMole]);
if (state==1) {
finished=false;
}
} while (finished == false);
newMole=findMole;
}
void UpdateScore(){
matrix.clear();
matrix.print(score, DEC);
matrix.writeDisplay();
}
void MatrixTest(){
// A quick test that the 7-segment LED is working.
uint8_t counter = 0;
for (uint8_t d=0; d<25; d++) {
// paint one LED per row. The HT16K33 internal memory looks like
// a 8x16 bit matrix (8 rows, 16 columns)
for (uint8_t i=0; i<8; i++) {
// draw a diagonal row of pixels
matrix.displaybuffer[i] = _BV((counter+i) % 16) | _BV((counter+i+8) % 16) ;
}
// write the changes we just made to the display
matrix.writeDisplay();
delay(100);
counter++;
if (counter >= 16) counter = 0;
delay(1);
}
// Must execute .writeDisplay() after .clear()
matrix.clear();
matrix.writeDisplay();
}