Project tutorial

# Tacoyaki (Lights Out) Game © GPL3+

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

• 1,578 views
• 4 respects

## Components and supplies

 SparkFun Arduino Pro Mini 328 - 5V/16MHz
×1
 MAX7219 24 Pin DIL IC
×1
 LED White 1210 SMD
×16
 12mm x 12mm Tactile switch with button cap
×16
 2 Digit 7 Segment 0.56in Common Cathode Display
×1
 Buzzer
×1
 10uF Ceramic capacitor 1206 SMD
×1
 12 pin machined header for IC socket
×2

## Necessary tools and machines

 3D Printer (generic)

## Apps and online services

 Arduino IDE

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).

### 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 inserted a 1210 white LED under each of their button caps.

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 add the button cap. 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

// 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);

//Setup display
//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
for (int i = 0; i < TOTAL_LEVELS; i++)
{
}
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)
{
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
{
{
delay(10);
}
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
//
//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
//
//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
//
//0001 0002 0004 0008
//0010 0020 0040 0080
//0100 0200 0400 0800
//1000 2000 4000 8000
uint16_t moveToyaki(uint16_t 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;
}
}
```
##### 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

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

//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);
{
//Pressed - Set the bit that matches this button
}
else
{
//Not pressed - Clear the bit that matches this button
}
//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
int btn = 0;              //Button/LED button number
//Cycle through all 16 buttons
while (btn < 16 && button == NO_BUTTON)
{
//Test if pressed
{
//Delay for debouncing
_delay_ms(10);
//Test if it is still pressed otherwise it is a bounce
{
//Wait until button is released
{
_delay_ms(50);
}
//We have a valid button press
button = btn;
}
}
//Set up for next button to test
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 button = 0;      //Result store
int btn = 0;              //Button/LED button number
//Cycle through all 16 buttons
while (btn < 16 && button == 0)
{
//Test if pressed
{
//Delay for debouncing
_delay_ms(10);
//Test if it is still pressed otherwise it is a bounce
{
//Wait until button is released
{
_delay_ms(50);
}
//We have a valid button press
}
}
//Set up for next button to test
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)
{

//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)
{
for (int i = 0; i < 16; i++)
{
showLed(i, ((ledStates & mask) != 0));
}
}

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

//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);
}
```

## Schematics

Schematic and PCB layout in Eagle Format
eagle_files_vtqk30DQvZ.zip

• 16 projects
• 44 followers

January 4, 2020

#### Members who respect this project

and 2 others

See similar projects
you might like

#### Arduino Pocket Game Console + A-Maze - Maze Game

Project tutorial by Alojz Jakob

• 10,681 views
• 28 respects

• 885 views
• 3 respects

#### LED Roulette Game

Project tutorial by Arduino “having11” Guy

• 12,809 views
• 23 respects

#### Snake LED Matrix Game

Project tutorial by Team Arduino bro

• 13,714 views
• 14 respects

#### Breadboard to PCB Part 1 - Making the Simon Says Game

Project tutorial by Katie Kristoff and Monica Houston

• 8,789 views