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
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
To create this cache, we're going to need:
- An Arduino UNO (although some others will do)
- 4x Big Dome push buttons - 100mm or 60mm - 4 different colors
- 1x 10mm LED - any color
- 1x Speaker
- 1x 9v battery connector
- 1x Metal DC Motor Servo
- 1x Self Locking Gate Latch
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
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
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
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.
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.