John Bradnam
Published © GPL3+

Touch-A-Mole Game

A "Whack-A-Mole" game on a 4 x 4 touchpad powered by a ATtiny1614 microprocessor.

IntermediateFull instructions provided8 hours226
Touch-A-Mole Game

Things used in this project

Hardware components

Microchip ATtiny1614 Microprocessor
×1
XC4602 Touch Key Module (4 x 4)
×1
Blue 3mm top-hat LED
Flat-top (see image in description)
×16
Resistor 100 ohm
Resistor 100 ohm
0805 SMD variant
×4
Capacitor 10 µF
Capacitor 10 µF
0805 non-polarized ceramic variant
×1
USB mini through-hole socket
×1
Buzzer
Buzzer
Low profile (try search for 17mm Piezo)
×1

Story

Read more

Custom parts and enclosures

STL Files

STL file for 3D printing. Use a 0.2mm layer height and no supports

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

Whack-A-Mole_V1.ino

C/C++
/**
 * TOUCH WHACK-A-MOLE
 * Copyright to John Bradnam (jbrad2089@gmail.com)
 * 
 * ATTiny1614 Pins mapped to Ardunio Pins
 *
 *             +--------+
 *         VCC + 1   14 + GND
 * (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
 *       1 PA5 + 3   12 + PA2 9  (MISO)
 * (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
 *       3 PA7 + 5   10 + PA0 11 (UPDI)
 * (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
 * (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
 *             +--------+
 *             
 * BOARD: ATtiny1614/1604/814/804/414/404/214/204
 * Chip: ATtiny1614
 * Clock Speed: 20MHz
 * Programmer: jtag2updi (megaTinyCore)
*/
#include <EEPROM.h>
#include <TimerFreeTone.h>  // https://bitbucket.org/teckel12/arduino-timer-free-tone/wiki/Home

#define DEBUG   //Comment out when ready to release

//My touch switch has a bad 4 key so I am going to ensure that isn't selected as a mole
//Comment out the next line if yours i OK
#define BAD_KEY_MASK 0x0010

#define COL_1 0 //PA4 - Column 1
#define COL_2 1 //PA5 - Column 2
#define COL_4 2 //PA6 - Column 4
#define COL_3 3 //PA7 - Column 3
#define ROW_2 4 //PB3 - Row 2
#define ROW_1 5 //PB2 - Row 1
#define ROW_4 6 //PB1 - Row 4
#define ROW_3 7 //PB0 - Row 3
#define SPEAKER 8 //PA1 - Speaker
#define SDO 9 //PA2 - Touch Switch SDO
#define SCL 10 //PA3 - Touch Switch SCL

#define BUZZER_PORT PORTA.OUT
#define BUZZER_MASK PIN1_bm

struct LED
{
  uint8_t row;
  uint8_t col;
}; 

LED leds[] = {
  {ROW_1,COL_1},{ROW_1,COL_2},{ROW_1,COL_3},{ROW_1,COL_4},
  {ROW_2,COL_1},{ROW_2,COL_2},{ROW_2,COL_3},{ROW_2,COL_4},
  {ROW_3,COL_1},{ROW_3,COL_2},{ROW_3,COL_3},{ROW_3,COL_4},
  {ROW_4,COL_1},{ROW_4,COL_2},{ROW_4,COL_3},{ROW_4,COL_4}
};

//Refresh timer for LEDs
//ATtiny1614's default clock is 20MHz(RC)
//CLK_PER is 3.333MHz ( default prescaler rate is 6, then 20/6 == 3.3 )
//interrupt interval = 4ms ( 6,667 / 3.333MHz = 0.002sec )
//2ms * 16 LEDs = 32mS or a refresh rate of 31 times / sec
#define TCB_COMPARE 6667
uint16_t ledBuffer = 0;
uint8_t nextLed = 0;
LED lastLed = {0,0};

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
  uint32_t magic;
  uint32_t seed;
  uint8_t level;        //Last level reached
} EEPROM_DATA;

EEPROM_DATA EepromData;       //Current EEPROM settings

//Game data
long levelTimeout;
int levelHitCount;
uint16_t lastMoleLitMask;
uint16_t lastKeyMask;


//-------------------------------------------------------------------------
//Initialise Hardware
void setup() 
{
  pinMode(SPEAKER, OUTPUT);
  pinMode(COL_1, OUTPUT);
  pinMode(COL_2, OUTPUT);
  pinMode(COL_3, OUTPUT);
  pinMode(COL_4, OUTPUT);
  pinMode(ROW_1, OUTPUT);
  pinMode(ROW_2, OUTPUT);
  pinMode(ROW_3, OUTPUT);
  pinMode(ROW_4, OUTPUT);
  digitalWrite(COL_1, LOW);
  digitalWrite(COL_2, LOW);
  digitalWrite(COL_3, LOW);
  digitalWrite(COL_4, LOW);
  digitalWrite(ROW_1, HIGH);
  digitalWrite(ROW_2, HIGH);
  digitalWrite(ROW_3, HIGH);
  digitalWrite(ROW_4, HIGH);
 
  //Set up display refresh timer
  TCB0.CCMP = TCB_COMPARE;
  TCB0.INTCTRL = TCB_CAPT_bm;
  TCB0.CTRLA = TCB_ENABLE_bm;

  //Enable interrupts
  sei();

  //To ensure every game from startup is different, store a new random seed for the
  //next time the unit starts
  readEepromData();
  randomSeed(EepromData.seed);
  EepromData.seed = EepromData.seed + random(0xffffffff);
  writeEepromData();
}

//-------------------------------------------------------------------------
//Timer B Interrupt handler interrupt each 2mS - output leds
ISR(TCB0_INT_vect)
{

  //Turn off last LED displayed
  if (lastLed.row != 0)
  {
    digitalWrite(lastLed.row,HIGH);
    digitalWrite(lastLed.col,LOW);
    lastLed.row = 0;
  }

  //Get bit mask of next LED to show
  uint16_t mask = 1 << nextLed;

  //Show LED if on
  if (ledBuffer & mask)
  {
    lastLed.row = leds[nextLed].row;    
    lastLed.col = leds[nextLed].col;
    digitalWrite(lastLed.row,LOW);
    digitalWrite(lastLed.col,HIGH);
  }

  //Increase nextLed for next interrupt period
  nextLed = (nextLed + 1) & 0x0F;

  //Clear interrupt flag
  TCB0.INTFLAGS |= TCB_CAPT_bm; //clear the interrupt flag(to reset TCB0.CNT)
}

//-----------------------------------------------------------------------------------
// Main loop
void loop()
{
  ledBuffer = 0; //Clear all leds
  levelHitCount = 0;
  lastMoleLitMask = 0;
  lastKeyMask = 1;
  playStartRound(); //Show user game has started
  
  //Calculate how long player has to hit moles
  levelTimeout = millis() + (16 - EepromData.level) * 1000;
  while (millis() < levelTimeout && levelHitCount < 16)
  {
    //Only change mole location if user pressed a button
    if (lastKeyMask != 0)
    {
      //Clear last led lit
      ledBuffer &= ~lastMoleLitMask;
      //get a random mole location that doesn't match the last one
      lastMoleLitMask = getRandomMoleMask(lastMoleLitMask);
      //light up new location
      ledBuffer |= lastMoleLitMask;
    }
    //Get user response
    lastKeyMask = getKeyMask();
    if (lastMoleLitMask != 0 && lastKeyMask == lastMoleLitMask)
    {
      playHitTone();
      levelHitCount++;
    }
    else if (lastKeyMask != 0)
    {
      playMissTone();
      levelHitCount = max(levelHitCount - 1, 0);
    }
  }
  
  playEndRound();
  
  //Calculate result (0 to 4 - decrease level, 5 to 12 - no level change, 13 to 16 - increase level
  if (levelHitCount > 12)
  {
    //increase level to a maximum of level 15
    EepromData.level = min(EepromData.level + 1, 15);
    writeEepromData();
  }
  else if (levelHitCount <= 4)
  {
    EepromData.level = max(EepromData.level - 1, 0);
    writeEepromData();
  }
    
  showResults(levelHitCount);
  delay(500);
}

//-------------------------------------------------------------------------------------
//returns key number or 0 if no key pressed
int getKey()
{
  int key = 0;                         //default to no keys pressed
  pinMode(SCL, OUTPUT);
  digitalWrite(SCL, HIGH);
  pinMode(SDO, INPUT);
  delay(2);                            //ensure data is reset to first key
  for(int i = 1;i < 17;i++)
  {
    digitalWrite(SCL, LOW);             //toggle clock
    digitalWrite(SCL, HIGH);
    if (!digitalRead(SDO)) 
    {
      key = i;                         //valid data found
      break;
    }
  }
  return key;
}

//-------------------------------------------------------------------------------------
//returns key mask or 0 if no key pressed
uint16_t getKeyMask()
{
  uint16_t key = 0;                    //default to no keys pressed
  pinMode(SCL, OUTPUT);
  digitalWrite(SCL, HIGH);
  pinMode(SDO, INPUT);
  delay(2);                            //ensure data is reset to first key
  uint16_t mask = 0x0001;
  for(int i = 0;i < 16;i++)
  {
    digitalWrite(SCL, LOW);            //toggle clock
    digitalWrite(SCL, HIGH);
    if (!digitalRead(SDO)) 
    {
      key = mask;                      //valid data found
      break;
    }
    mask = mask << 1;
  }
  return key;
}

//-----------------------------------------------------------------------------------
//Returns a different mole square mask from the last one
// last - moles that shouldn't be selected
// returns new mole to light up
uint16_t getRandomMoleMask(uint16_t last)
{
  uint16_t mask = 1 << random(16);
#ifdef BAD_KEY_MASK
  while (mask == last || mask == BAD_KEY_MASK)
#else
  while (mask == last)
#endif  
  {
    mask = 1 << random(16);
  }
  return mask;
}

//-----------------------------------------------------------------------------------
//Show the results
//Lights up the LEDs with the number of hits.
//Plays losing sound for no hits and winning sound for 16 hits
void showResults(int hits) 
{
  //Clear all leds
  ledBuffer = 0;

  //Light up leds reflecting the number of hits
  uint32_t mask = 0x0001;
  int note = 100;
  for (int i = 1; i <= 16; i++)
  {
    if (i <= hits)
    {
      //Show LED
      ledBuffer |= mask;
    }
    else
    {
      //We are done lighting up the LEDs
      break;
    }
    //Get mask for next LED to light up
    mask = mask << 1;
    //Play an ascending note for each led that lights up
    TimerFreeTone(SPEAKER, note, 100);
    note += 10;
    TimerFreeTone(SPEAKER, note, 100);
    note += 10;
  }

  //Play the win or lose sound
  if (hits == 0)
  {
    playLoseSound();
  }
  else if (hits == 16)
  {
    playWinSound();
  }

  //Flash the LEDS
  if (hits > 0)
  {
    uint16_t temp = ledBuffer;
    ledBuffer = 0;
    for (int i = 0; i < 10; i++)
    {
      ledBuffer = ledBuffer ^ temp;
      delay(100);
    }
    ledBuffer = temp;
  }
}

//-----------------------------------------------------------------------------------
//Play the sound for the end of a round
void playEndRound() 
{
  #define MAX_NOTE 4978               // Maximum high tone in hertz. Used for siren.
  #define MIN_NOTE 31                 // Minimum low tone in hertz. Used for siren.
  
  for (int note = MAX_NOTE; note >= MIN_NOTE; note -= 5)
  {                       
    TimerFreeTone(SPEAKER, note, 1);
  }
}

//-----------------------------------------------------------------------------------
//Play the sound for the start of a round
void playStartRound() 
{
  #define MAX_NOTE 4978               // Maximum high tone in hertz. Used for siren.
  #define MIN_NOTE 31                 // Minimum low tone in hertz. Used for siren.
  
  for (int note = MIN_NOTE; note <= MAX_NOTE; note += 5)
  {                       
    TimerFreeTone(SPEAKER, note, 1);
  }
}

//-----------------------------------------------------------------------------------
//Play a decaying sound
void playNoteDecay(int start) 
{
  #define DECAY_NOTE 100                // Minimum delta time.
  
  for (int note = start; note >= DECAY_NOTE; note -= 10)
  {                       
    TimerFreeTone(SPEAKER, note, 100);
  }
}

//-----------------------------------------------------------------------------------
//Play a attack sound
void playNoteAttack(int start) 
{
  #define DECAY_NOTE 100                // Minimum delta time.
  
  for (int note = DECAY_NOTE; note <= start; note += 10)
  {                       
    TimerFreeTone(SPEAKER, note, 100);
  }
}


//------------------------------------------------------------------

void playHitTone()
{
  TimerFreeTone(SPEAKER, 300, 150); 
}

//------------------------------------------------------------------

void playMissTone()
{
  TimerFreeTone(SPEAKER, 50, 150); 
}

//------------------------------------------------------------------

//Play a high note as a sign you lost
void playWinSound()
{
  //TimerFreeTone(SPEAKER,880,300);
  TimerFreeTone(SPEAKER,880,100); //A5
  TimerFreeTone(SPEAKER,988,100); //B5
  TimerFreeTone(SPEAKER,523,100); //C5
  TimerFreeTone(SPEAKER,988,100); //B5
  TimerFreeTone(SPEAKER,523,100); //C5
  TimerFreeTone(SPEAKER,587,100); //D5
  TimerFreeTone(SPEAKER,523,100); //C5
  TimerFreeTone(SPEAKER,587,100); //D5
  TimerFreeTone(SPEAKER,659,100); //E5
  TimerFreeTone(SPEAKER,587,100); //D5
  TimerFreeTone(SPEAKER,659,100); //E5
  TimerFreeTone(SPEAKER,659,100); //E5
  delay(250);
}

//------------------------------------------------------------------------------------------------------------------

//Play wah wah wah wahwahwahwahwahwah
void playLoseSound()
{
  delay(400);
  //wah wah wah wahwahwahwahwahwah
  for(double wah=0; wah<4; wah+=6.541)
  {
    TimerFreeTone(SPEAKER, 440+wah, 50);
  }
  TimerFreeTone(SPEAKER, 466.164, 100);
  delay(80);
  for(double wah=0; wah<5; wah+=4.939)
  {
    TimerFreeTone(SPEAKER, 415.305+wah, 50);
  }
  TimerFreeTone(SPEAKER, 440.000, 100);
  delay(80);
  for(double wah=0; wah<5; wah+=4.662)
  {
    TimerFreeTone(SPEAKER, 391.995+wah, 50);
  }
  TimerFreeTone(SPEAKER, 415.305, 100);
  delay(80);
  for(int j=0; j<7; j++)
  {
    TimerFreeTone(SPEAKER, 391.995, 70);
    TimerFreeTone(SPEAKER, 415.305, 70);
  }
  delay(400);
}

//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData()
{
  //This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
  EEPROM.put(EEPROM_ADDRESS,EepromData);
}

//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
  //Eprom
  EEPROM.get(EEPROM_ADDRESS,EepromData);
  if (EepromData.magic != EEPROM_MAGIC)
  {
    Serial.println("Initialising EEPROM ...");
    EepromData.magic = EEPROM_MAGIC;
    EepromData.seed = millis();
    EepromData.level = 0;
    writeEepromData();
  }
}

Credits

John Bradnam

John Bradnam

141 projects • 167 followers

Comments