Project tutorial
Tacoyaki (Lights Out) Game

Tacoyaki (Lights Out) Game © GPL3+

Play three variants of the game Tacoyaki (also known as Lights Out). Full instructions and code included.

  • 525 views
  • 0 comments
  • 2 respects

Components and supplies

Necessary tools and machines

3drag
3D Printer (generic)

Apps and online services

About this project

This make is a games console with 16 light up push switches, a 2 digit 7 segment display and a piezo-electric speaker. It is all driven by an Arduino Pro Mini.

The sketch included plays 3 variants of the game Tacoyaki (also known as Lights Out).

Video

Demo

Level 1 - Tacoyaki+

In this variant, by pressing a button, its light and those of the (non-diagonally) adjacent buttons will change (switch on if it was off, and vice versa). The goal is to switch off all the LEDs in under 25 moves.

Solution:

  • For each light on row 1, press the button beneath it on row 2 to turn the light off. This way row 1 is completely unlit.
  • Repeat step a for rows 2-3, This is usually called 'chasing the lights'.

Level 2 - Tacoyaki+ with wrapround

The main difference is that the board has no edges - the left and right columns are considered to be adjacent, as are the top and bottom rows. Every light therefore has exactly 4 neighbours, and so every move changes exactly 5 lights.

Solution:

This is an easy puzzle once you know the following two facts:

  • To change an individual light, press it and its four neighbours.
  • To know whether you need to press a button or not, check its own light and the neighbouring lights. If an odd number of these 5 lights are switched on, then the button needs to be pressed, otherwise it does not.

The following solution then suggests itself:

  • Use fact 2 above on all the buttons in the middle two rows.
  • For each light that is on in row 2, press the button above it in row 1
  • For each light that is on in row 3, press the button below it in row 4.

Level 3 - Tacoyaki

In this variant, pressing a button will change the state of all LEDs diagonally from it. So all lights in a NW, NE, SE, SW direction from the pressed button (including the button itself) will change state.

Solution:

Number the rows 1-4, the columns A-D.

Press a combination of buttons on row 2 such that the lights on row 1 are switched off. This seems tricky, but is quite easy once you know how. First switch off lights A1, C1, from left to right as follows:

  • If A1 is on then press B2.
  • If C1 is on then press D2. Next switch off lights A2, A4, A6 from right to left as follows:
  • If D1 is on then press C2.
  • If B1 is on then press A2.

Now row 1 is completely unlit.

  • Repeat the first step for rows 2-3, so that now you only have lights on row 4.
  • If the light at A4 is on, press D1.
  • If the light at B4 is on, press C1.
  • If the light at C4 is on, press B1.
  • If the light at D4 is on, press A1.
  • Note how the button pattern to press on the top row is the mirror image of the light pattern on the bottom row.
  • Repeat the first two steps, chasing the lights down and it will be magically solved.

Other games you might want to add

  • Whack-a-Mole
  • Super Simple Simon

Building the case

Print the top and bottom case files on your printer. I used 0.2 mm layer height and a 20% infill. Drill out the mounting posts with a 2.5 mm drill and make a thread with a 3 mm tap.

PCB board

The PCB was made using the Toner method. I have included the Eagle files so you can get them manufactured if you wish. The Arduino Pro Mini and MAX7219 ic are mounted on the back of the board. The switches with LEDs (See instructions below on how to make these), Buzzer and 2 Digit Common Cathode 0.56" 7-Segment display are mounted on the front of the board. Use 6 mm M3 screws to hold the board in place. I suggest you insert the bezel in the front panel, place the 7 segment display in the hole, mount the board and solder the display once it has been pushed down to sit flat with the front panel.

Software

Sketch is included. You should find it easy to add more games. There is plenty of flash program space available.

Adding the LEDs to the buttons

I used cheap 12 x 12 buttons and insert a 1210 white LED under the button cap.

Solder thin wires to the LED. Try and ensure that the lead is mounted under the LED and not on the side of the LED otherwise it makes it hard to insert. Also bring the leads out in opposite corners:

Once you have soldered it, test the LED by using a resistor connected to a battery or similar. Cut the positive lead shorter that the other lead (this is the anode). I found that if the leads are soldered on the bottom of the LED as shown above, you can insert the wires and position the LED on top of the switch. The button cap should just go straight on. Pull the wires taught and solder them to the PCB.

Code

TacoyakiV2.inoC/C++
#include "Display.h"
#include "Buttons.h"
#include "Music.h"

// Define Sound pins
#define SPEAKER 10

// LED display
#define DATA 11
#define CLOCK 13
#define LOAD 12

// Switches
#define COL0 2
#define COL1 3
#define COL2 4
#define COL3 5
#define ROW0 6
#define ROW1 7
#define ROW2 8
#define ROW3 9

// Noise pin
#define RANDOM_SEED_PIN A0

#define TOTAL_LEVELS 3
#define MAX_TRIES 25
enum LevelEnum { NOT_USED, TOYAKI_PLUS, TOYAKI_PLUS_WRAP, TOYAKI };
LevelEnum currentLevel = TOYAKI_PLUS;
int totalTries = 0;

void setup() 
{
  Serial.begin(115200);
  

  randomSeed(analogRead(RANDOM_SEED_PIN));
  
  //Setup display
  initDisplay(DATA, CLOCK, LOAD);
  //Setup switches
  initButtons(ROW0, ROW1, ROW2, ROW3, COL0, COL1, COL2, COL3);
  //Setup buzzer
  initMusic(SPEAKER);
}

void loop()
{
  //Display current level
  showLevel((int)currentLevel);

  //Light up LEDs the user can select for the level  
  uint16_t flashMask = 0;
  for (int i = 0; i < TOTAL_LEVELS; i++)
  {
    flashMask = flashMask | (0x0001 << i);
  }
  uint16_t buttonOnStates = flashMask;
  showLedStates(buttonOnStates);
  
  //Wait until button 0, 1, 2, 3 is pressed.
  #define FLASH_PERIOD 200
  unsigned long flashTime = millis() + FLASH_PERIOD;
  uint16_t button = NO_BUTTON;
  while (button == NO_BUTTON || button >= TOTAL_LEVELS)
  {
    button = buttonPressed();
    delay(10);
    if (millis() > flashTime)
    {
      buttonOnStates = buttonOnStates ^ flashMask;
      showLedStates(buttonOnStates);
      flashTime = millis() + FLASH_PERIOD;
    }
  }
  currentLevel = (LevelEnum)(button + 1);
  showLevel((int)currentLevel);
  delay(500);
  
  //Start by creating a random board.
  buttonOnStates = 0;
  uint16_t nextMove = 0;
  for (int i = 0; i < 16; i++)
  {
    buttonOnStates = buttonOnStates ^ getChangesForNextMove(1 << (random(16)));
  }
  Serial.println("buttonOnStates: " + String(buttonOnStates, 16));
  showLedStates(buttonOnStates);

  //Play game until solved or tries is MAX_TRIES
  totalTries = 0;
  do
  {
    uint16_t buttonMask = 0;
    while (buttonMask == 0)
    {
      buttonMask = maskPressed();
      delay(10);
    }
    Serial.println("buttonMask: " + String(buttonMask, 16));
    buttonOnStates = buttonOnStates ^ getChangesForNextMove(buttonMask);
    Serial.println("buttonOnStates: " + String(buttonOnStates, 16));
    showLedStates(buttonOnStates);
    totalTries++;
    showValue(totalTries, true);
    playTurnTone();
    delay(100);
  }
  while (buttonOnStates != 0 && totalTries < MAX_TRIES);

  if (totalTries == MAX_TRIES)
  {
    //Lose game
    flashValue(totalTries, 10, 200);
    playLoseMusic();
  }
  else
  {
    //Display win
    buttonOnStates = 0x5A5A;
    showLedStates(buttonOnStates);
    playWinMusic();
    for (int i = 0; i < 10; i++) 
    {
      showValue(totalTries, (i & 1) == 0);
      buttonOnStates = buttonOnStates ^ 0xFFFF;
      showLedStates(buttonOnStates);
      delay(300);
    }
  }
  clearLeds();
}

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

//Return a mask for the LEDS that need to change from the given button mask
uint16_t getChangesForNextMove(uint16_t button)
{
  uint16_t changes = 0;  

  switch(currentLevel)
  {
    case TOYAKI_PLUS: changes = moveToyakiPlus(button); break;
    case TOYAKI_PLUS_WRAP: changes = moveToyakiPlusWrap(button); break;
    case TOYAKI: changes = moveToyaki(button); break;
  }
  return changes;
}

//------------------------------------------------------------------------------------------------------------------
// Calculate next ToyakiPlus position
// button - mask of button pressed
// returns - mask of leds to change
//
//Switches and LED masks
//0001 0002 0004 0008
//0010 0020 0040 0080
//0100 0200 0400 0800
//1000 2000 4000 8000
//UP - X >> 4
//DN - X << 4
//<- - X >> 1 & 0x7777; 
//-> - X << 1 & 0xEEEE;
uint16_t moveToyakiPlus(uint16_t button)
{
  uint16_t up = button >> 4;
  uint16_t dn = button << 4;
  uint16_t le = (button >> 1) & 0x7777;
  uint16_t rg = (button << 1) & 0xEEEE;
  return (button | up | dn | le | rg);
}

//------------------------------------------------------------------------------------------------------------------
// Calculate next ToyakiPlusWrap position
// button - mask of button pressed
// returns - mask of leds to change
//
//Switches and LED masks
//0001 0002 0004 0008
//0010 0020 0040 0080
//0100 0200 0400 0800
//1000 2000 4000 8000
uint16_t moveToyakiPlusWrap(uint16_t button)
{
  uint16_t up = (button < 0x0010) ? button << 12 : button >> 4;
  uint16_t dn = (button >= 0x1000) ? button >> 12 : button << 4;
  uint16_t le = (button & 0x1111) ? button << 3 : (button >> 1) & 0x7777;
  uint16_t rg = (button & 0x8888) ? button >> 3 : (button << 1) & 0xEEEE;
  return (button | up | dn | le | rg);
}

//------------------------------------------------------------------------------------------------------------------
// Calculate next Toyaki position
// button - mask of button pressed
// returns - mask of leds to change
//
//Switches and LED masks
//0001 0002 0004 0008
//0010 0020 0040 0080
//0100 0200 0400 0800
//1000 2000 4000 8000
uint16_t moveToyaki(uint16_t button)
{
  uint16_t mask = button;
  uint16_t nw = button, ne = button, se = button, sw = button;
  for (int i = 0; i < 3; i++) 
  {
    nw = (nw & 0xEEEE) >> 5; mask |= nw;
    ne = (ne & 0x7777) >> 3; mask |= ne;
    se = (se & 0x7777) << 5; mask |= se;
    sw = (sw & 0xEEEE) << 3; mask |= sw;
  }
  return mask;
}
Buttons.hC/C++
#pragma once

#define NO_BUTTON -1

uint16_t _switchStates = 0;
int _nextColToScan = 0;
int _nextRowToScan = 0;
uint8_t _colPins[4];
uint8_t _rowPins[4];

//Initialse button routines
//Buttons numbered top to bottom, left to right
//row0, col0 is button 0
//row3, col3 is button 15
void initButtons(uint8_t row0,uint8_t row1,uint8_t row2,uint8_t row3,uint8_t col0,uint8_t col1,uint8_t col2,uint8_t col3);

//Tests if any of the buttons have been pressed and released
//  returns the button that was pressed or -1
int buttonPressed();

//Tests if any of the buttons have been pressed and released
//  returns the mask of button that was pressed or 0
uint16_t maskPressed();

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

//Initialse button routines
//Buttons numbered top to bottom, left to right
//row0, col0 is button 0
//row3, col3 is button 15
void initButtons(uint8_t row0,uint8_t row1,uint8_t row2,uint8_t row3,uint8_t col0,uint8_t col1,uint8_t col2,uint8_t col3)
{
  _colPins[0] = col0;
  _colPins[1] = col1;
  _colPins[2] = col2;
  _colPins[3] = col3;
  _rowPins[0] = row0;
  _rowPins[1] = row1;
  _rowPins[2] = row2;
  _rowPins[3] = row3;

  //Setup switches
  for (int i = 0; i < 4; i++)
  {
    pinMode(_colPins[i], INPUT);
    pinMode(_rowPins[i], OUTPUT);
    digitalWrite(_rowPins[i], LOW);
  }

  // Switches
  // Delay between each interrupt - eg: 1 sec ==> (16*10^6) / (1*1024) - 1 (must be <65536) = 15640
  // 1mS - 15.640
  // 5mS - 78.2
  #define SCAN_SPEED 78

  // Set up timer for background switch scanning
  cli();  //stop interrupts
  //set timer1 interrupt for flash
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0
  // set compare match register for 1hz increments
  OCR1A = SCAN_SPEED;
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS10 and CS12 bits for 1024 prescaler
  TCCR1B |= (1 << CS12) | (1 << CS10);  
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);    
  // allow interrupts
  sei();
  
}

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

//timer1 interrupt for switch scanning 5mS
//Each iteration of the timer tests a single row and column. The state
//of all switches are held in switchStates.
ISR(TIMER1_COMPA_vect)
{
  //Work out which bit in switchStates reflect this row and column
  uint16_t mask = (0x0001 << _nextColToScan) << (_nextRowToScan << 2);

  //Set the row pin high
  digitalWrite(_rowPins[_nextRowToScan], HIGH);
  //Read the column pin
  if (digitalRead(_colPins[_nextColToScan]) == HIGH)
  {
    //Pressed - Set the bit that matches this button
    _switchStates = _switchStates | mask;
  }
  else
  {
    //Not pressed - Clear the bit that matches this button
    _switchStates = _switchStates & ~mask;
  }
  //Set the row pin low again
  digitalWrite(_rowPins[_nextRowToScan], LOW);

  //Update the row and column counters for next timer interrupt
  _nextColToScan = (_nextColToScan + 1) & 0x03;
  if (_nextColToScan == 0)
  {
    _nextRowToScan = (_nextRowToScan + 1) & 0x03;
  }
}

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

//Tests if any of the buttons have been pressed and released
//  returns the button that was pressed or -1
int buttonPressed()
{
  int button = NO_BUTTON;   //Result store
  uint16_t mask = 0x0001;   //Mask for switchStates
  int btn = 0;              //Button/LED button number
  //Cycle through all 16 buttons
  while (btn < 16 && button == NO_BUTTON)
  {
    //Test if pressed
    if (_switchStates & mask)
    {
      //Delay for debouncing
      _delay_ms(10);
      //Test if it is still pressed otherwise it is a bounce
      if (_switchStates & mask)
      {
        //Wait until button is released
        while (_switchStates & mask)
        {
          _delay_ms(50);
        }
        //We have a valid button press
        button = btn;
      }
    }
    //Set up for next button to test
    mask = mask << 1;
    btn++;
  }
  return button;
}

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

//Tests if any of the buttons have been pressed and released
//  returns mask of button that was pressed or 0
uint16_t maskPressed()
{
  uint16_t button = 0;      //Result store
  uint16_t mask = 0x0001;   //Mask for switchStates
  int btn = 0;              //Button/LED button number
  //Cycle through all 16 buttons
  while (btn < 16 && button == 0)
  {
    //Test if pressed
    if (_switchStates & mask)
    {
      //Delay for debouncing
      _delay_ms(10);
      //Test if it is still pressed otherwise it is a bounce
      if (_switchStates & mask)
      {
        //Wait until button is released
        while (_switchStates & mask)
        {
          _delay_ms(50);
        }
        //We have a valid button press
        button = mask;
      }
    }
    //Set up for next button to test
    mask = mask << 1;
    btn++;
  }
  return button;
}
Music.hC/C++
#pragma once

uint8_t _speakerPin;

// Initialise Music player
void initMusic(uint8_t speakerPin);

//Play a sound of a frequency in Hz for a duration in mS
void playSound(double freqHz, int durationMs);

//Play turn tone
void playTurnTone();

//Play wah wah wah wahwahwahwahwahwah
void playLoseMusic();

//Play winning music
void playWinMusic();

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

// Initialise Music player
void initMusic(uint8_t speakerPin)
{
  _speakerPin = speakerPin;
  pinMode(_speakerPin, OUTPUT);
  
}

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

//Play a sound of a frequency in Hz for a duration in mS
void playSound(double freqHz, int durationMs)
{
  //Calculate the period in microseconds
  int periodMicro = int((1/freqHz)*1000000);
  int halfPeriod = periodMicro/2;
   
  //store start time
  long startTime = millis();
   
  //(millis() - startTime) is elapsed play time
  while((millis() - startTime) < durationMs)
  {
    digitalWrite(_speakerPin, HIGH);
    delayMicroseconds(halfPeriod);
    digitalWrite(_speakerPin, LOW);
    delayMicroseconds(halfPeriod);
  }
}

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

//Play turn tone
void playTurnTone()
{
  playSound(300,50); 
}

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

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

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

//Play winning music
void playWinMusic()
{
  playSound(880,100); //A5
  playSound(988,100); //B5
  playSound(523,100); //C5
  playSound(988,100); //B5
  playSound(523,100); //C5
  playSound(587,100); //D5
  playSound(523,100); //C5
  playSound(587,100); //D5
  playSound(659,100); //E5
  playSound(587,100); //D5
  playSound(659,100); //E5
  playSound(659,100); //E5
  playSound(880,100); //A5
  playSound(988,100); //B5
  playSound(523,100); //C5
  playSound(988,100); //B5
  playSound(523,100); //C5
  playSound(587,100); //D5
  playSound(523,100); //C5
  playSound(587,100); //D5
  playSound(659,100); //E5
  playSound(587,100); //D5
  playSound(659,100); //E5
  playSound(659,100); //E5
  delay(250);
}
Display.hC/C++
#pragma once
#include <LedControl.h>

//Because there is no default constructor for he LedControl, we can't create an instance of it
//without passing some parameters. This instance will be replaced in initDisplay and the garbage
//collector can clean up this instance.
LedControl _lc = LedControl(11, 13, 12);
 
//Initilaise MAX7219. This drives the two digit display on Digit 0 and Digit 1 and also the switch LEDs. Row 0 is
//Digit 3, Row 1 is Digit 4, Row 2 is Digit 5 and Row 3 is Digit 6. The Switch LEDs using segments d thru g
//representing columns 0 to 3.
void initDisplay(uint8_t dataPin, uint8_t clockPin, uint8_t loadPin);

//Turn off all button LEDs
void clearLeds();

//Turn on or off one of the button LEDs
// MAX2819 segment order is dp a b c d e f g
// Columns are d e f g
// Rows are Digit 3 4 5 6
// led - LED to switch on or off (0..15)
// on - True to switch on, False to switch off
void showLed(int led, bool on);

//Turn on or off the button LEDs based on ledStates
void showLedStates(uint16_t ledStates);

//Flashes a value on the display
// value - number to flash
// repeat - number of times to flash
// delta - time in mS between each state
void flashValue(int value, int repeat, int delta);

//Display a numeric value
// value - 0 to 99
// on - true to show value, false to turn off display
void showValue(int value, bool on);

//Display a numeric value
// level - Level number (0-9)
void showLevel(int level);

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

//Initilaise MAX7219. This drives the two digit display on Digit 0 and Digit 1 and also the switch LEDs. Row 0 is
//Digit 3, Row 1 is Digit 4, Row 2 is Digit 5 and Row 3 is Digit 6. The Switch LEDs using segments d thru g
//representing columns 0 to 3.
void initDisplay(uint8_t dataPin, uint8_t clockPin, uint8_t loadPin)
{
  _lc = LedControl(dataPin,clockPin,loadPin,1);
  
  //Setup LEDs
  _lc.shutdown(0,false);     //Wakeup call
  _lc.setScanLimit(0, 7);    //Number of digits
  _lc.setIntensity(0, 15);    //Brightness
  _lc.clearDisplay(0);

}

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

//Turn off all button LEDs
void clearLeds()
{
  showLedStates(0);
}

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

//Turn on or off one of the button LEDs
// MAX2819 segment order is dp a b c d e f g
// Columns are d e f g
// Rows are Digit 3 4 5 6
// led - LED to switch on or off (0..15)
// on - True to switch on, False to switch off
void showLed(int led, bool on)
{
  _lc.setLed(0, (led >> 2) + 3, (led & 3) + 4, on);
}

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

//Turn on or off the button LEDs based on ledStates
void showLedStates(uint16_t ledStates)
{
  uint16_t mask = 0x0001;
  for (int i = 0; i < 16; i++)
  {
    showLed(i, ((ledStates & mask) != 0));
    mask = mask << 1;
  }
}

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

//Flashes a value on the display
// value - number to flash
// repeat - number of times to flash
// delta - time in mS between each state
void flashValue(int value, int repeat, int delta)
{
  for (int x=0; x < repeat;x++)
  {
    showValue(value, (x & 1) != 0);
    delay(delta);
  }
  showValue(value, true);
}

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

//Display a numeric value
// value - 0 to 99
// on - true to show value, false to turn off display
void showValue(int value, bool on)
{
  value = min(value, 99);
  int tens = value / 10;
  int units = value % 10;
  if (on)
  {
    _lc.setDigit(0,1,units,false);
  }
  else 
  {
    _lc.setChar(0,1,' ', false);
  }
  if (on && tens != 0)
  {
    _lc.setDigit(0,0,tens,false);
  }
  else
  {
    _lc.setChar(0,0,' ', false);
  }
}

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

//Display a numeric value
// level - Level number (0-9)
void showLevel(int level)
{
  _lc.setDigit(0,1,min(level, 9),false);
  _lc.setChar(0,0,'L', false);
}

Custom parts and enclosures

Case - Top
Case - Bottom

Schematics

Schematic
Schematic qwu2sbxhn0
Eagle Files
Schematic and PCB layout in Eagle Format
eagle_files_vtqk30DQvZ.zip

Comments

Similar projects you might like

Arduino Pocket Game Console + A-Maze - Maze Game

Project tutorial by Alojz Jakob

  • 11,010 views
  • 8 comments
  • 28 respects

Simon Mini Game

Project tutorial by John Bradnam

  • 885 views
  • 0 comments
  • 3 respects

LED Roulette Game

Project tutorial by Arduino “having11” Guy

  • 13,157 views
  • 5 comments
  • 23 respects

Snake LED Matrix Game

Project tutorial by Team Arduino bro

  • 14,657 views
  • 10 comments
  • 14 respects

Breadboard to PCB Part 1 - Making the Simon Says Game

Project tutorial by Monica Houston and Katie Kristoff

  • 9,019 views
  • 11 comments
  • 32 respects

Arduino Game By LCD

Project tutorial by Mohammed Magdy

  • 61,885 views
  • 57 comments
  • 194 respects
Add projectSign up / Login