How Good Is Your Memory Geocache (Simon Says) - GC9GNKE

by feiticeir0 in Circuits > Arduino

4056 Views, 56 Favorites, 0 Comments

How Good Is Your Memory Geocache (Simon Says) - GC9GNKE

IMG_20211009_184454-1.jpg

This is another cache of so called "Smart Cache" or "Gadge Cache".

I've always wanted to create this kind of cache - like my previous instructables - and it's was also on a need to use the Arduinos and all the electronic "gizmos" I have at home.

On this type of caches is necessary to solve a "puzzle" to get to the logbook.

In this case, this memory game, it's necessary to press the right buttons, on the right sequence, for some levels.


Supplies

Geocaching

"Join the world's largest treasure hunt."

I've been doing Geocaching for a long time now.

I just love discovering new places, the Nature and solving crazy puzzles. The essence of Geocaching is, by solving puzzles, reading clues or by just going straight to the place, to discover a geocache.

A geocache is something (most of the times, a Tupperware) that another geocacher hid (sometimes in plain sight) that coins a logbook, some times some swagg, and, on the rare occasions, a geocoin or a trackable.

The logbook, a piece of paper, is for you to sign, as a testimony that you've been there (you must also log your visit on the geocache page at Geocaching.com) .

The intencion of the Geocacher with that geocache is for you to have fun, solve a crazy puzzle, take your family to walk a rail on a sunny day or just for you to discover a wonderful place.

There are some crazy geocaches out there - some you must solve at night, others require a UV light, some water, for you to use a bycicle pump and others just for you to crack your head thinking of a solution... Or just to spend hours searching for a geocache and never to be found. There's even one Geocache on the International Space Station. Someday, it could be the moon or mars. Imagine when traveling amonst the stars is just like taking a bus to another town (a spaceship to another planet) - Geocaching would be crazy hard ! :)

But, there's another side to it: instead solving other one's caches, create your own, give them a theme, a twist and watch (read) the Geocachers reactions.

Since I was little, I love treasure hunts. I'm from the 80s, and movies like The Goonies just filled one's imagination. Who didn't want to find a pirate's treasure ? Or just like Indiana Jones. Not for the whip, but for the clues, the puzzles, the stories.

The Cache

IMG_20211009_184531.jpg
Tens boa memória ?

This cache is composed of 4 buttons - Blue, Green, White and Yellow. Nothing special about the colors - just the button colors I had at home in that moment. You can use the ones that take your fancy.

We have 5 levels (you can set that in the code) that we need to overcome. In each level is necessary to press a number of buttons (again, you choose that in the code) that will increase with each level.

The buttons that are needed to be pressed are shown before each level starts. By advancing the level, the time between each button turning on and off gets shorter, meaning you have less time to memorize the sequence.

Don't bother to memorize the sequence between failures - each time a new game starts, it's a new sequence. :)

The speaker plays tones with each button. That can also help. The tones are always the same for each button. When showing the sequence to repeat, you'll ear a tone. Play the video to show the game.

If you get all the sequences right, a music is played and the servo releases the gate latch that opens the door for the logbook.

Electronics

simon_says_layout_bb.png
IMG_20211009_232638.jpg
IMG_20211009_232735.jpg
IMG_20211009_232742.jpg
WhatsApp Image 2021-11-04 at 11.27.59 AM.jpeg
IMG_20220503_232124.jpg
IMG_20220503_232132.jpg

The wiring for this cache is simple.

The follow the schematics to know where to wire each button and each button LED, as well the middle LED (this one signals when to start entering the sequence), the buzzer and the servo.

The Arduino will be powered from the jack connector, from the 9v battery connector. This will power the entire cache.

Container

IMG_20211009_184522.jpg
WhatsApp Image 2021-11-04 at 11.27.27 AM.jpeg
IMG_20220501_150720.jpg
IMG_20220501_150717.jpg
IMG_20220502_172304.jpg
ezgif.com-gif-maker.gif

For the container, like all my other caches, use wood to create a big rectangle.

I've separated the electronics compartment above, and a small compartment, with a door, bellow for the logbook.

This compartment is held locked by the gate latch, that opens when the game is won.


I've attached the 3D printed files for:

  • Securing the servo motor to the wood
  • Pin on the door that locks in the gate latch.


Code

For the cache, two files are needed. The code itself and the header pitches.h . This file is available from Arduino tutorials.

Because the cache buttons can take any color, in the code, the buttons are referred by its position on the cache - Top, Left, Right, Bottom

--------------

| T |

| L R |

| B |

--------------

We also use the Servo.h to control the opening of the door.

We first include all the libraries needed.

Start by defining some PINs - buzzer and servo

We then define the sounds for the buttons - each time a button is pressed (or the cache shows the sequence), alongside the light, a sound is emited.

An array is created with the defined sounds .

For easier code, all the arrays will have the same order of the button positions - top[0], left[1], right[2], bottom[3]

We then define the buttons and buttons LED pins and create an array with the definitions.

#include <Servo.h>
#include "pitches.h"


//0 to deactivate debugging
//1 for debugging
#define DEBUG 0

#define BUZZER 12

#define SERVO 3
Servo myservo;

#define soundButtonTop 262
#define soundButtonLeft 330
#define soundButtonBottom 392 
#define soundButtonRight 494

int soundsArray[4] = {soundButtonTop,soundButtonLeft, soundButtonBottom,soundButtonRight};
/*
 * Definitions for buttons and leds
 */
#define ledTop 11
#define ledLeft 10
#define ledBottom 9
#define ledRight 8


int ledsArray[4] = {ledTop, ledLeft, ledBottom, ledRight};

//buttons definitions
#define buttonTop 7
#define buttonLeft 6
#define buttonBottom 5
#define buttonRight 4

#define NO_BUTTON 0

int buttonsArray[4] = {buttonTop, buttonLeft, buttonBottom, buttonRight};


Next, we have some variables that control the game

NUM_LEVELS - how many levels the player must win to open the logbook

BUTTON_TIMEOUT - the time the player as to press the button.

#define NUM_LEVELS 5

//tempo (milisegundos - 3 segundos) que o geocacher tem para pressionar o botao
#define BUTTON_TIMEOUT 3000

Next, a new PIN definition, the middile LED that signals when they can start pressing buttons

#define ledReady 13


The current level indicator, starting at 0

//current level
int curLevel = 0;


Next, we define the time (in milliseconds) that the game will show the next sequence to replicate. This is the interval between each button display.

For the first level, the game takes 1s between each button in the sequence - until 600ms for the final sequence


Next, we define how many buttons a player must press in each level. Starting with 4, until 7 . For it not to be complicated, we have two levels with the same number of buttons. We want people to have fun.

/**
 * definicao de tempos para os LEDs acenderem e desligaram
 * Nivel mais altos, menos tempo para mostrar
 * 2s, 1,5s, 900ms, 700ms, 600ms, 500ms - 
 */
//Para 6 niveis
//int timeLevels[NUM_LEVELS] = {1000, 900, 750, 650, 600, 500};

//Para 5 niveis
int timeLevels[NUM_LEVELS] = {1000, 900, 750, 650, 600};

/**
 * Tamanho das sequencias para memorizar e repetir
 * Mais alto o nivel, maior a sequencia a memorizar
 * Primeiro nivel acende 4 botoes
 * Segundo nivel, acende 5 botoes e assim sucessivamente. 
 * 4, 5, 6, 7, 8, 10
 */
//Para 6 niveis
//int sequences[NUM_LEVELS] = {4, 5, 5, 6, 7, 8};

// Para 5 niveis
int sequences[NUM_LEVELS] = {4, 5, 5, 6, 7};


Next the bonce delay for the button - not to register several clicks with one touch of the button.

long debounceDelay = 200;   // tempo para "deixar de saltar". aumentar se botao regista mais que uma vez


setup function


We set the LEDs as output, the buttons as input. Initialize the servo. Initialize the LEDs and wait 3 seconds

void setup() {
 // put your setup code here, to run once:
 //LED PINS
 for (int i = 0; i < 4; i++)
   pinMode (ledsArray[i], OUTPUT);

 //led Ready
 pinMode (ledReady, OUTPUT);

 //Button PINS
 for (int i = 0; i < 4; i++)
   pinMode (buttonsArray[i], INPUT_PULLUP);

 // Servo
 myservo.attach(SERVO);

 //Serial port
 Serial.begin(115200);
 initializeLeds();
 delay(3000);
}


initializeLeds

Just shows an animation

/*
 * Just show off a animation for startup
 */
void initializeLeds() {
 int delay_timer = 100;
 for (int i = 0; i < 4; i++) {
   digitalWrite (ledsArray[i], HIGH);
   delay(delay_timer);
   digitalWrite (ledsArray[i], LOW);
 }
}


Loop function


In the loop function, the game unfolds.

We start by calling a function flashLevelWaiting. Is just to show an animation while we're waiting for the players to get ready to play.

Next, we call the function playMemoryGame(). This is the main game function. If it returns 1, meaning the player was able to play all levels and it won.

Winning

If it wins, we play a tune by calling playVencedor() - wait 1s and then open the logbook door.

After that, wait 5s - enough time to open it - and reset the servo position, to lower the door latch. After that, just wait forever, until a button is pressed. If pressed, a new game starts.

Losing

If it loses - returning 0 - we play a tune for the gameOver, wait 1s and start again.

void loop() {

 //play something a do something
 //until user presses a key to start the game. 
 flashLevelWaiting();
 delay(2000); //esperar 2s ate comecar o jogo

 //when ready, if return 0 is not ok
 //return 1, win and play sound
 if (playMemoryGame()) {
   //play vitory
   if (DEBUG) Serial.println("A tocar vitoria...");
   playVencedor();
   delay(1000);
   //open door
   if (DEBUG) Serial.println ("Opening logbook door");
   myservo.write(170);
   //indicar que ja podem abrir a porta
   showOpenLogBook();
   //wait 5s
   delay(5000);
   //put in normal position
   if (DEBUG) Serial.println ("Closing logbook door");
   myservo.write(90);
   //wait until a button is pressed
   do {} while (!getButton());
   delay(100);
 }
 else {
   //play gameover
   if (DEBUG) Serial.println("Perdeu....");
   playGameOver();
   delay (1000);
 } 
 //reset game levels
 curLevel = 0;
}


PlayMemoryGame


This is the main function. Here's where the game is controlled.

We start in a for loop - running for each level the games is configured - starting at 0 until NUM_LEVELS.

We start by creating a vector - flashArray (it's a vector :) ) - that will be the size of the number of buttons to memorize. In the current level - 0, that will be 4 - that is determined by the array sequences defined on the top.

We now fill each position of the vector with a led position (top, left, right, bottom) - randomized . This will be the sequence displayed, that the player will have to replicate. We use a function called randomizeGame that will fill the flashArray vector with the random sequence.

We then show the sequence to the user by passing the vector to the function showLevel.

After showing the sequence to repeat, we signal that we're ready to process the input - readyLevel()

Next, if parseUserLevel returns anything than 1, we lost the game. If not, we increase the current level - curLevel +=1 and the for loop goes to the next iteration.

If the for loop goes until the end, we return 1 and exit the function

boolean playMemoryGame () {
 //lets randomize 
  for (int z = 0; z < NUM_LEVELS; z++) {
   Serial.println("Current Level: " + String(curLevel));

   /*
    * create array with current level sequences
    * Just create an array -
    * Sequences is the number of buttons to press from memory
    * curLevel is current level - current level will
    * be the index in the sequences array - the more high in the level
    * the more button presses will have to remember
    */
   int flashArray[sequences[curLevel]];

   randomizeGame (flashArray, sequences[curLevel]);

   //call the function that will light up the leds
   showLevel (flashArray);
   readyLevel(); //mostrar ao jogador que pode pressionar botoes
   if (DEBUG) Serial.println ("Waiting for user");

   //now, let's match the pressed buttons with the game
   if (!parseUserLevel(flashArray)) {
     //User missed something, ending game
     if (DEBUG) Serial.println(" Gameover");
     return 0;
   }
   //Estamos aqui, o utilizador acertou todos os botoes
   //esperar 1s ate ao proximo nivel
   delay(1000);
   curLevel += 1;
   if (DEBUG) Serial.println("Next Level: " + String(curLevel));
  }

 //if we got here, winner
  return 1;
}


randomizeGame

This function just fills the array with LEDs PIN (from ledsArray) in random order. It initializes the randomSeed by reading something from the analog PINs.

void randomizeGame (int flashArray[], int arraySize) {
 //make sure this is really random
 randomSeed(analogRead(A0));
 //fill the array with random Leds
 for (int y = 0; y < arraySize; y++)
   flashArray[y] = ledsArray[random(0,4)];
}


showLevel

This function shows the user the sequence it must repeat.

In a for loop, from 0 to the number of buttons it must press in the current level, plays the tone for that button - tonner (this function also lights up the LED) - wait's the time defined for that level and goes for the next button.

/**
 * Function show the random LEDs according with the Arrays
 * playLevel - show what buttons user must press
 * 
 */
void showLevel (int flashArray[]) {
 //lets light up some leds
 for (int x = 0; x < sequences[curLevel]; x ++) {
   Serial.print(String(flashArray[x]) + " ");
   //digitalWrite (flashArray[x], HIGH);
   tonner (flashArray[x]);
   delay(timeLevels[curLevel]); 
   //digitalWrite (flashArray[x], LOW);
   delay(timeLevels[curLevel]);
 }

NOTE:

I'm realizing while wrinting this that I have the delay twice - instead of waiting the time defined, it was waiting twice. For the first leve, the player was waiting 2s between each button - but for the upper levels, it was twice. I was thinking that the time waited was perfect - if this wasn't here, probably the time was just to fast... Will do some testing and ajust this acoordingly.

getButton

This function will read the button pressed (the PIN number) or return 0 if the BUTTON_TIMEOUT time is reached.

int getButton () {
 long startTime = millis();
 //get what button was pressed and return it
 // we return the pin number - it's the same on the array
 while ( (millis() - startTime) < BUTTON_TIMEOUT) {
   if (digitalRead(buttonTop) == 0) return (ledTop);
   else if (digitalRead(buttonLeft) == 0) return (ledLeft);
   else if (digitalRead(buttonRight) == 0) return (ledRight);
   else if (digitalRead(buttonBottom) == 0) return (ledBottom);
 }
 return 0;
}


parseUserLevel

This function will parse the input for the current level.

while the number of pressed buttons is less than the needed for the current level - the number of buttons for each sequence - we check for user input

Next, we get the pressed button, by calling the getButton() function - described above

if we have a button pressed (not equal to 0), we play the tone of that button. We wait the debounceDelay time - to prevement to register multiple presses when the player just pressed one time - and check if the button pressed is the same that was supposed to be pressed - the order matters - stored in the flashArray array. We increment the numPressbuttons variable, turn the leds off and go to the next iteration, by using the continue keyworkd.

if the pressed button was the wrong one, we exit the function returning 0. We now it's the wrong one because we didn't went to the next iteration.

if the pressed button was 0 (timed out), we also exit the function.

If the get this far, we pressed all the buttons, in the correct order. Let's exit with 1 - success.

/*
 * parse user input for current level
 * Every button in presses, we will check if is the same on the array
 * If it fails, return 0
 */
int parseUserLevel (int flashArray[]) {
 int numPressButtons = 0;

 while (numPressButtons < sequences[curLevel]) {
   long startTime = millis();
   int button = getButton();
   if (DEBUG) Serial.println("Button pressed: " + String(button));
   if (button != NO_BUTTON) {
     //play music
     tonner (button);
     if (DEBUG) Serial.println("Button pressed inside: " + String(button));
     delay (debounceDelay); //debounce to prevent multiple presses
     //check if button pressed is the same as in the array
     if (button == flashArray[numPressButtons]) {
       if (DEBUG) Serial.println ("corret button");
       //next button
       numPressButtons++;
       if (DEBUG) Serial.println("numPress: " + String(numPressButtons));
       setLedsPressed (NO_BUTTON);
       continue; //advance next
     }
     if (DEBUG) Serial.println("Error button");
     return 0;
   } //end if button
   if (DEBUG) Serial.println("TIMEOUT");
   return 0;
 }
 //we got here, user got them right
 return 1;
}


setLedsPressed

This function will turn the LEDs on of the pressed button or off if not the pressed button. This way, we loop though all the buttons.

//light up the LED of the button pressed
void setLedsPressed(int buttonPressed) {
 if (buttonPressed == ledTop)
   digitalWrite (ledTop, HIGH);
 else 
   digitalWrite (ledTop, LOW);

 if (buttonPressed == ledLeft)
   digitalWrite (ledLeft, HIGH);
 else 
   digitalWrite (ledLeft, LOW);

 if (buttonPressed == ledBottom)
   digitalWrite (ledBottom, HIGH);
 else 
   digitalWrite (ledBottom, LOW);

 if (buttonPressed == ledRight)
   digitalWrite (ledRight, HIGH);
 else 
   digitalWrite (ledRight, LOW);
}


play

This function will play a note for a specified duration - or 150ms if not specified.

Tone is a function from Arduino that generates a tone. More info on the function help page. We just need to specify the speaker PIN, the note to play and the duration.

void play (long note, int duration = 150) {
 tone (BUZZER,note,duration);
 delay (1+duration);
}


Tonner

This function will play the defined tone for the button pressed and turn the leds on or off.

void tonner (int buttonPressed) {
 //light leds
 setLedsPressed (buttonPressed);

 switch (buttonPressed) {
   case ledTop:
     play (soundButtonTop);
     break;
   case ledLeft:
     play (soundButtonLeft);
     break;
   case ledBottom:
     play (soundButtonBottom);
     break;
   case ledRight:
     play (soundButtonRight);
     break;   
 }
 setLedsPressed(0);
}


playGameOver

This function plays the losing sound ! If you lose, this is the song you'll hear.

//GameOver
void playGameOver() {
 int pressedButton;
 //turn all the LEDs on
 for (int j = 0; j <= 3; j++) {
   digitalWrite(ledsArray[j], HIGH);
 }
 tone(BUZZER, 130, 250);  //E6
 delay(275);
 tone(BUZZER, 73, 250);  //G6
 delay(275);
 tone(BUZZER, 65, 150);  //E7
 delay(175);
 tone(BUZZER, 98, 500);  //C7
 delay(500);

 delay(200);
 setLedsPressed(0);
}


playVencedor

This function plays the wining sound ! If you win, this is the song you'll hear.

//
void playVencedor() {

 //turn all the LEDs on
 for (int j = 0; j <= 3; j++) {
   digitalWrite(ledsArray[j], HIGH);
 }

 for (int d = 0; d < 3; d++) {
   //play the 1Up noise
   tone(BUZZER, 1318, 150);  //E6
   delay(175);
   tone(BUZZER, 1567, 150);  //G6
   delay(175);
   tone(BUZZER, 2637, 150);  //E7
   delay(175);
   tone(BUZZER, 2093, 150);  //C7
   delay(175);
   tone(BUZZER, 2349, 150);  //D7
   delay(175);
   tone(BUZZER, 3135, 500);  //G7
   delay(500);
 }

 delay(100);
}


flashLevelWaithing

This function indicates the player we're ready to start the game. It will turn all the leds on and play a music.

/*
 * Esta funcao vai acender todos os leds
 * para indicar que esta a espera
 */
void flashLevelWaiting () {
 while (1) {
 //flash all of the LEDs when the game starts
   for (int i = 0; i <= 3; i++) {
     tone(BUZZER, soundsArray[i], 200); //play one of the 4 tones
     //turn all of the leds on
     digitalWrite(ledsArray[0], HIGH);
     digitalWrite(ledsArray[1], HIGH);
     digitalWrite(ledsArray[2], HIGH);
     digitalWrite(ledsArray[3], HIGH);
     delay(100);        //wait for a moment
     //turn all of the leds off
     digitalWrite(ledsArray[0], LOW);
     digitalWrite(ledsArray[1], LOW);
     digitalWrite(ledsArray[2], LOW);
     digitalWrite(ledsArray[3], LOW);
     delay(100);  //wait for a moment
   }//this will repeat 4 times
 if (getButton() != NO_BUTTON) return;
 }
}


readyLevel

This function will turn the middle LED on and off, play a sound, indicating the player that we're ready to read it's inputs. This plays after the game shows the combination to be repeated.


/*
 * Funcao para mostrar ao utilizador
 * que estamos prontos para aceitar os inputs
 */
void readyLevel () {
 digitalWrite (ledReady, HIGH);
 tone (BUZZER, NOTE_CS6, 150);
 delay(100);
 digitalWrite (ledReady, LOW);
}


showOpenLogBook

This functions display an animation on the center LED and plays a sound, indicating that the logbook door is openning.

/*
 * Funcao que mostra que podemos abrir o logbook
 */
void showOpenLogBook() {
 for (int z=0; z < 10; z++) {
   digitalWrite (ledReady, HIGH);
   tone (BUZZER, NOTE_DS5, 100);
   delay(50);
   digitalWrite (ledReady, LOW);
   delay(50);
 }
}


I would like to thank to Sparkfun for their excelent tutorial on an experiment they have called Simon Says.

You can check it here. It's awesome.

Conclusion

Creating this cache was a challenge. Not only for the code, bu the container and trying to figure out how to open the logbook door - a gate latch, a padlock with code, etc..

The code was also a bit challenging, mainly because the Arduino would register a lot of button presses instead of just one. That was crazy to figure out and to get the timings right.


This cache is already published and the reactions have been great - GC9GNKE

I'm always hopping people will have fun doing it just like I have creating them.

Happy caching.