Crack The Code Game, Built Into A DIY Safe Puzzle Box

In this project, I’m going to be showing you how to build your own Arduino based crack the code game. You use a dial to guess the randomly generated code to the safe and 8 LEDs on the front of the safe then tell you how many of the digits you’ve guessed are correct and how many are in the right place as well. Using this feedback you can work your way towards cracking the code to the safe, and once you do, the game will then tell you how many attempts it took you to crack.

Here’s a video of the build. Read on for the full step by step instructions.

This project assumes you know the basics of Arduino programming, otherwise read our article on getting started with Arduino.

There have been a number of different forms of this game over the years, from physical pegboards in the 70s to video games, and more recently you may have seen these types of puzzles as single images on social media platforms.

Crack The Code Lock Code Puzzle

The safe is initially open, allowing you to put something into the inside compartment. You then push the dial to lock the safe using a servo on the inside of the door and a random code is generated.

The Box Opens To Put Something Inside

You then need to input the code by turning the dial to select the digits and pushing the dial to confirm each digit.

Entering A Code

After your fourth digit is chosen, the safe displays how many of your digits are correct and how many of them are in the correct place using the red and green LEDs on the door. A red LED indicates a correct digit and a green LED indicate that it’s also in the correct place. So you’re looking to light up all four red and green LEDs in order to crack the code and open the safe.

The LEDs Light Up To Show Correct Digits And Positions

The safe keeps track of how many guesses you’ve made in order to crack the code. It may sound complicated at first but it’s actually not that difficult. You just need to remember and build upon your previous guesses. Most of the time you should be able to crack the code in 5 to 10 guesses, depending on how lucky your initial guesses are.

What You Need To Build Your Own Crack The Code Puzzle Box

In addition to these, you’ll also need some basic tools, a soldering iron, glue gun and some wood glue. You’ll also need to have access to a laser cutter to cut the box. If you don’t have access to one, consider using an online laser cutting service. There are quite a few options available and they’re quite affordable, they’ll even deliver to your door.

If you’re interested in a laser cutter, this is the one I’ve used for this project.

K40 Laser Cutter – Buy Here

How To Build Your Own Crack The Code Safe Box

Cutting & Assembling The Safe Box

I started off by designing the safe box in Inkscape. It had to be large enough to house an Arduino Uno, have a compartment to put something into and look like a traditional safe. The pieces are designed to be cut from 3mm MDF, you could also cut the parts from 3mm acrylic or plywood. If you use a different thickness material then you’ll need to modify the edges of the box pieces so that the slots fit together.

Box Components Were Designed In Inkscape

Download The Laser Cutting File – Box Cutting Files

The download above includes svg, dxf and A3 printable pdf files to give you a couple of options for cutting or printing.

There are 6 main panels which make up the box. The front panel has a cutout in it for the door and the back has one for the Arduino and battery compartment cover. The 6 main panels are labelled in the cutting file so that you’re able to identify them.

The dial is also made up from laser-cut pieces which are then glued together.

There are three decorative panels which are stuck onto the top and two sides of the box to make it look more like a safe. There are also two panels which make up the door and a divider panel which goes into the middle of the box to separate the safe compartment from the electronics compartment.

The pieces fit onto a single piece of MDF 400 x 500mm. They can be divided up into smaller pieces, as I’ve done, if your laser cutter isn’t big enough to cut them all in one go. I cut out two sections at a time.

Let’s cut out the pieces.

Box Components Were Laser Cut

I used some masking tape to prevent the smoke from the laser from marking the wood. I had to take this off before gluing the box together, which is a little tedious but produces a much better end product.

Laser Cut Components

I started out gluing the decorative panels onto the top and sides first using some PVA wood glue and small plastic clamps. Make sure that you’ve got the pieces in the correct order so that you know which are which. There are three different pieces; the top and bottom are the same, the sides are the same and the front and back are the same. I’ve put text labels under each in the laser cutting file. Then glue the two door panels together.

Glue The Box Components Together

Once the panels are dry, you can assemble the box.

Gluing The Box Sides Together

Make sure the cutouts for the centre divider are on the sides. These are to run any wires from the front of the box to the back of the box where the Arduino and battery sit.

The hinges are also laser cut and just glue into place once you’ve lined up the door. Make sure that they’re parallel to the door or you’ll have difficulty opening it. Also, make sure that the door is positioned over the centre of the cutout in the box so that there are no gaps visible.

Box Hinges Glued Into Place

You may also need to sand a little bit off of the inside hinged edge of the door once the hinges are dry so that it doesn’t rub on the inside edge of the box cutout as it moves past.

Sand Some Of The Inside Edge Of The Box Hinge

Glue the four square pieces into the corners behind the back panel to hold the screws for the back cover in place.

Pieces To Hold The Back Screws In Place

Then drill holes for the screws. I used a couple of spare screws I had lying around which came with the micro servos.

Drill Holes For The Back Screws

You can then mount the screen onto the door with two screws, the Arduino into the back compartment and lastly the encoder through the front door. The nut for the encoder is hidden by the recess provided by the front panel.

Install The Electronic Components

Connecting The Electronics

Now that the box is complete, we can start connecting the electronic components together.

Here is the schematic:

Crack The Code Schematic

We’ve got 8 LEDs connected to the digital IO pins 6 to 13. The locking servo connected to pin 5. The encoder connected to pins 2, 3 and 4 and the OLED display connected to the Arduino’s I2C interface.

I used a 220 ohm resistor for each LED, which I soldered directly onto the cathode (negative pin) of the LED before gluing them into place with a glue gun.

Soldering Components Together

Glue Components To The Box

I connected the components together using coloured ribbon cable to keep the wiring neat and to help keep track of which wire needed to go to each Arduino pin.

Wiring Once Complete

I pushed the ribbon cables through to the back compartment and the soldered sections of header strips onto the ends to plug directly into the Arduino.

Pin Headers To Arduino Uno

I also mounted a power switch onto the back cover and connected this to a battery plug to connect to a rechargeable battery to power the game. I used a 3 cell, 800mAh LiPo battery which I had from some RC aircraft. You could also use a 9V battery or a battery holder which houses 4 AA batteries.

Lastly, you’ll need to mount the locking servo on the door. Position it towards the edge of the door so that it passes over the lip in the box and the arm is able to push up against the inside of the lip to lock the box. This isn’t the strongest locking mechanism but it is really simple and it works for the game’s purpose.

Servo Door Lock

The safe box is now complete.

Crack The Code Safe Puzzle Box

Safe Dial

Coding The Game

The code for this game isn’t that complicated but is quite lengthy. I’ve tried to make the code as readable as possible by splitting it up into functions and putting in a lot of comments.

The code makes use of an interrupt routine to take the inputs from the rotary encoder, this is based on Simon Merrett’s example for a rotary encoder input which I found online. It was the most simple I could find and didn’t require any additional libraries to function.

Here is the code:

//Code Breaker
//Michael Klements
//The DIY Life
//15 May 2020

//Encoder interrupt routine adapted from Simon Merrett's example code

#include <SPI.h>                          //Import libraries to control the OLED display
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Servo.h>                        //Import library to control the servo

Servo lockServo;                          //Create a servo object for the lock servo

#define SCREEN_WIDTH 128                  // OLED display width, in pixels
#define SCREEN_HEIGHT 32                  // OLED display height, in pixels

#define OLED_RESET -1                     // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);   // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)

static int pinA = 2;                      //Hardware interrupt digital pin 2
static int pinB = 3;                      //Hardware interrupt digital pin 3
volatile byte aFlag = 0;                  //Rising edge on pinA to signal that the encoder has arrived at a detent
volatile byte bFlag = 0;                  //Rising edge on pinB to signal that the encoder has arrived at a detent (opposite direction to when aFlag is set)
volatile byte encoderPos = 0;             //Current value of encoder position, digit being input form 0 to 9
volatile byte prevEncoderPos = 0;         //To track whether the encoder has been turned and the display needs to update
volatile byte reading = 0;                //Stores direct value from interrupt pin

const byte buttonPin = 4;                 //Pin number for encoder push button
byte oldButtonState = HIGH;               //First button state is open because of pull-up resistor
const unsigned long debounceTime = 10;    //Debounce delay time
unsigned long buttonPressTime;            //Time button has been pressed for debounce

byte correctNumLEDs[4] = {9,12,7,11};      //Pin numbers for correct number LEDs (Indicate a correct digit)
byte correctPlaceLEDs[4] = {6,10,8,13};    //Pin numbers for correct place LEDs (Indicate a correct digit in the correct place)

byte code[4] = {0,0,0,0};                  //Create an array to store the code digits
byte codeGuess[4] = {0,0,0,0};             //Create an array to store the guessed code digits
byte guessingDigit = 0;                    //Tracks the current digit being guessed
byte numGuesses = 0;                       //Tracks how many guesses it takes to crack the code
boolean correctGuess = true;               //Variable to check whether the code has been guessed correctly, true initially to generate a new code on startup

void setup()
{
  Serial.begin(9600);                                 //Starts the Serial monitor for debugging
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C))      //Connect to the OLED display
  {
    Serial.println(F("SSD1306 allocation failed"));   //If connection fails
    for(;;);                                          //Don't proceed, loop forever
  }
  display.clearDisplay();                             //Clear display
  lockServo.attach(5);                                //Assign the lock servo to pin 5
  for(int i=0 ; i<=3 ; i++)                           //Define pin modes for the LEDs
  {
    pinMode(correctNumLEDs[i], OUTPUT);
    pinMode(correctPlaceLEDs[i], OUTPUT);
  }
  pinMode(pinA, INPUT_PULLUP);                        //Set pinA as an input, pulled HIGH to the logic voltage
  pinMode(pinB, INPUT_PULLUP);                        //Set pinB as an input, pulled HIGH to the logic voltage
  attachInterrupt(0,PinA,RISING);                     //Set an interrupt on PinA
  attachInterrupt(1,PinB,RISING);                     //Set an interrupt on PinB
  pinMode (buttonPin, INPUT_PULLUP);                  //Set the encoder button as an input, pulled HIGH to the logic voltage
  randomSeed(analogRead(0));                          //Randomly choose a starting point for the random function, otherwise code pattern is predictable
  display.setTextColor(SSD1306_WHITE);                //Set the text colour to white
  startupAni();                                       //Display the startup animation
}

void loop() 
{
  if(correctGuess)                                            //Code between games to reset if the guess is correct, initially true to open safe and then generate new code
  {
    lockServo.write(140);                                     //Unlock the safe
    delay(300);
    updateLEDs (0,4);                                         //Flashing LED sequence
    delay(300);
    updateLEDs (4,0);
    delay(300);
    updateLEDs (0,4);
    delay(300);
    updateLEDs (4,0);
    delay(300);
    updateLEDs (4,4);                                         //Turn all LEDs on
    if(numGuesses >= 1)                                       //Check that its not the start of the game
    {
      display.clearDisplay();                                 //Clear the display
      display.setTextSize(1);                                 //Set the display text size to small
      display.setCursor(35,10);                               //Set the display cursor position
      display.print(F("In "));                                //Set the display text
      display.print(numGuesses);                              //Set the display text
      display.setCursor(35,20);                               //Set the display cursor position
      display.print(F("Attempts"));                           //Set the display text
      display.display();                                      //Output the display text
      delay(5000);
    }
    display.clearDisplay();                                   //Clear the display
    display.setTextSize(1);                                   //Set the display text size to small
    display.setCursor(35,10);                                 //Set the display cursor position
    display.print(F("Push To"));                              //Set the display text
    display.setCursor(35,20);                                 //Set the display cursor position
    display.print(F("Lock Safe"));                            //Set the display text
    display.display();                                        //Output the display text
    display.setTextSize(2);                                   //Set the display text size back to large
    boolean lock = false;                                     //Safe is initially not locked
    boolean pressed = false;                                  //Keeps track of button press
    while(!lock)                                              //While button is not pressed, wait for it to be pressed
    {
      byte buttonState = digitalRead (buttonPin); 
      if (buttonState != oldButtonState)
      {
        if (millis () - buttonPressTime >= debounceTime)      //Debounce button
        {
          buttonPressTime = millis ();                        //Time when button is pressed
          oldButtonState =  buttonState;                      //Remember button state
          if (buttonState == LOW)
          {
            pressed = true;                                   //Records button has been pressed
          }
          else 
          {
            if (pressed == true)                              //Makes sure that button is pressed and then released before continuing in the code
            {
              lockServo.write(45);                            //Lock the safe
              display.clearDisplay();                         //Clear the display
              display.setCursor(30,10);                       //Set the display cursor position
              display.print(F("Locked"));                     //Set the display text
              display.display();                              //Output the display text
              lock = true;
            }
          }  
        }
      }
    }
    generateNewCode();                                        //Calls function to generate a new random code
    updateLEDs (0,0);
    correctGuess = false;                                     //The code guess is initially set to incorrect
    numGuesses = 0;                                           //Reset the number of guesses counter
  }
  inputCodeGuess();                                           //Calls function to allow the user to input a guess
  numGuesses++;                                               //Increment the guess counter
  checkCodeGuess();                                           //Calls function to check the input guess
  encoderPos = 0;                                             //Reset the encoder position
  guessingDigit = 0;                                          //Reset the digit being guessed
  codeGuess[0] = 0;                                           //Reset the first digit of the code
  updateDisplayCode();                                        //Update the displayed code
}

void updateDisplayCode()                                      //Function to update the display with the input code
{
  String temp = "";                                           //Temporary variable to concatenate the code string
  if(!correctGuess)                                           //If the guess is not correct then update the display
  {
    for (int i=0 ; i<guessingDigit ; i++)                     //Loops through the four digits to display them
    {
      temp = temp + codeGuess[i];
    }
    temp = temp + encoderPos;
    for (int i=guessingDigit+1 ; i<=3 ; i++)
    {
      temp = temp + "0";
    }
    Serial.println(temp);                                     //Output to Serial monitor for debugging
    display.setTextSize(2);                                   //Set the display text size
    display.clearDisplay();                                   //Clear the display
    display.setCursor(40,10);                                 //Set the display cursor position
    display.println(temp);                                    //Set the display text
    display.display();                                        //Update the display
  }
}

void generateNewCode()                                        //Function to generate a new random code
{
  Serial.print("Code: ");
  for (int i=0 ; i<= 3 ; i++)                                 //Loops through the four digits and assigns a random number to each
  {
    code[i] = random(0,9);                                    //Generate a random number for each digit
    Serial.print(code[i]);                                    //Display the code on Serial monitor for debugging
  }
  Serial.println();
}

void inputCodeGuess()                                         //Function to allow the user to input a guess
{
  for(int i=0 ; i<=3 ; i++)                                   //User must guess all four digits
  {
    guessingDigit = i;
    boolean confirmed = false;                                //Both used to confirm button push to assign a digit to the guess code
    boolean pressed = false;
    encoderPos = 0;                                           //Encoder starts from 0 for each digit
    while(!confirmed)                                         //While the user has not confirmed the digit input
    {
      byte buttonState = digitalRead (buttonPin); 
      if (buttonState != oldButtonState)
      {
        if (millis () - buttonPressTime >= debounceTime)      //Debounce button
        {
          buttonPressTime = millis ();                        //Time when button was pushed
          oldButtonState =  buttonState;                      //Remember button state for next time
          if (buttonState == LOW)
          {
            codeGuess[i] = encoderPos;                        //If the button is pressed, accept the current digit into the guessed code
            pressed = true;
          }
          else 
          {
            if (pressed == true)                              //Confirm the input once the button is released again
            {
              updateDisplayCode();                            //Update the code being displayed
              confirmed = true;
            }
          }  
        }
      }
      if(encoderPos!=prevEncoderPos)                          //Update the displayed code if the encoder position has changed
      {
        updateDisplayCode();
        prevEncoderPos=encoderPos;
      }
    }
  }
}

void checkCodeGuess()                                         //Function to check the users guess against the generated code
{
  int correctNum = 0;                                         //Variable for the number of correct digits in the wrong place
  int correctPlace = 0;                                       //Variable for the number of correct digits in the correct place
  int usedDigits[4] = {0,0,0,0};                              //Mark off digits which have been already identified in the wrong place, avoids counting repeated digits twice
  for (int i=0 ; i<= 3 ; i++)                                 //Loop through the four digits in the guessed code
  {
    for (int j=0 ; j<=3 ; j++)                                //Loop through the four digits in the generated code
    {
      if (codeGuess[i]==code[j])                              //If a number is found to match
      {
        if(usedDigits[j]!=1)                                  //Check that it hasn't been previously identified
        {
          correctNum++;                                       //Increment the correct digits in the wrong place counter
          usedDigits[j] = 1;                                  //Mark off the digit as been identified
          break;                                              //Stop looking once the digit is found
        }
      }
    }
  }
  for (int i=0 ; i<= 3 ; i++)                                 //Compares the guess digits to the code digits for correct digits in correct place
  {
    if (codeGuess[i]==code[i])                                //If a correct digit in the correct place is found
      correctPlace++;                                         //Increment the correct place counter
  }
  updateLEDs(correctNum, correctPlace);                        //Calls a function to update the LEDs to reflect the guess
  if(correctPlace==4)                                          //If all 4 digits are correct then the code has been cracked
  {
    display.clearDisplay();                                    //Clear the display
    display.setCursor(20,10);                                  //Set the display cursor position
    display.print(F("Cracked"));                               //Set the display text
    display.display();                                         //Output the display text
    correctGuess = true;
  }
  else
    correctGuess = false;
}

void updateLEDs (int corNum, int corPla)                        //Function to update the LEDs to reflect the guess
{
  for(int i=0 ; i<=3 ; i++)                                     //First turn all LEDs off
  {
    digitalWrite(correctNumLEDs[i], LOW);
    digitalWrite(correctPlaceLEDs[i], LOW);
  }
  for(int j=0 ; j<=corNum-1 ; j++)                              //Turn on the number of correct digits in wrong place LEDs
  {
    digitalWrite(correctNumLEDs[j], HIGH);
  }
  for(int k=0 ; k<=corPla-1 ; k++)                              //Turn on the number of correct digits in the correct place LEDs
  {
    digitalWrite(correctPlaceLEDs[k], HIGH);
  }
}

void startupAni ()
{
  display.setTextSize(2);                     //Set the display text size
  display.setCursor(35,10);                   //Set the display cursor position
  display.println(F("Crack"));                //Set the display text
  display.display();                          //Output the display text
  delay(500);
  display.clearDisplay();                     //Clear the display
  display.setCursor(45,10);
  display.println(F("The"));
  display.display();
  delay(500);
  display.clearDisplay();
  display.setCursor(40,10);
  display.println(F("Code"));
  display.display();
  delay(500);
  display.clearDisplay();
}

void PinA()                               //Rotary encoder interrupt service routine for one encoder pin
{
  cli();                                  //Stop interrupts happening before we read pin values
  reading = PIND & 0xC;                   //Read all eight pin values then strip away all but pinA and pinB's values
  if(reading == B00001100 && aFlag)       //Check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
  {     
    if(encoderPos>0)
      encoderPos --;                      //Decrement the encoder's position count
    else
      encoderPos = 9;                     //Go back to 9 after 0
    bFlag = 0;                            //Reset flags for the next turn
    aFlag = 0;                            //Reset flags for the next turn
  }
  else if (reading == B00000100)          //Signal that we're expecting pinB to signal the transition to detent from free rotation
    bFlag = 1;
  sei();                                  //Restart interrupts
}

void PinB()                               //Rotary encoder interrupt service routine for the other encoder pin
{
  cli();                                  //Stop interrupts happening before we read pin values
  reading = PIND & 0xC;                   //Read all eight pin values then strip away all but pinA and pinB's values
  if (reading == B00001100 && bFlag)      //Check that we have both pins at detent (HIGH) and that we are expecting detent on this pin's rising edge
  {
    if(encoderPos<9)
      encoderPos ++;                      //Increment the encoder's position count
    else
      encoderPos = 0;                     //Go back to 0 after 9
    bFlag = 0;                            //Reset flags for the next turn
    aFlag = 0;                            //Reset flags for the next turn
  }
  else if (reading == B00001000)          //Signal that we're expecting pinA to signal the transition to detent from free rotation
    aFlag = 1;
  sei();                                  //Restart interrupts
}

Download The Code – CrackTheCode

We start by importing libraries to control the OLED display and the servo. The libraries are used to drive the OLED display, you can find more information on driving these OLED displays on Adafruit’s page on using I2C OLED displays.

We then set the parameters for the display and create all of our variables.

I’ve separated the variables into sections and given each a comment to identify their function. There are quite a few variables dedicated to tracking the encoder turns as these are done through rising edge interrupts on pins 2 and 3. The LED pins are assigned through to arrays which make updating them a bit easier. There are two code arrays created, on to store the randomly generated code and one to store the user’s current guess. There are also a few variables to track whether the uses code is correct, which digit is currently being guessed and how many guesses the user has made so far.

In the setup function, we start the display, which won’t progress unless the connection to the display is successful. Then attach the servo, set the LED and encoder IO pin modes. We then make use of the randomSeed function which reads in from an open analog input in order to randomise the starting point for the random function. If we don’t do this then the randomly generated codes will follow a predictable pattern which we’ll soon be able to identify. Lastly, we display the Crack The Code text animation on the display.

The loop function flashes the LEDs and displays the message “push to lock safe” which then waits until the user pushes the dial to start the game. The same code is run at the end of a game which then displays the number of attempts and waits for a dial press to start a new game.

There is some debouncing code on the encoder pushbutton and once pushed, the servo locks the safe and a random code is generated. The code then calls a function to ask the user to input their guess and then another to check the guess, this is repeated until the user guesses the code correctly. After each code guess, we increment the number of guesses counter and reset the input code so that the user can select a code from 0000 again. You could also change this to carry on from the previous code again if you’d prefer.

There is a function to update the code being displayed, which is called every time the encoder is turned and the displayed code needs to change. This just loops through the code guess array and displays the four digits.

The function to generate a new code simply assigns a random digit to each of the four elements in the code array.

The function to input a code guess allows the user to select a digit using the encoder and then confirm each digit input by pushing the encoder down. The displayed code is updated after each button push and any time that the encoder position has been changed. So the encoder updates the position variable using the interrupt function and this portion of the code checks if the variable has been changed and then updates the code accordingly. The interrupt doesn’t directly drive any portions of the code, it only updates the encoder position variable.

The check code guess function then looks through the guessed code and decides how many digits are correct and how many are in the correct place. There is more code to the logic behind the digits in the incorrect place so that duplicates in the user’s input and in the generated code are managed. So if the user guesses 5555 and the code contains one 5 then only one of the red LEDs will light up. The same goes for the user guessing a single 5 in their code while the generated code contains two 5s, only one red LED will light up. The correct digit in the correct place check is a simple comparison of the digits in each position of the two arrays.

The update LEDs function switches the correct number of red and green LEDs on based on the output from the check code guess function.

The startup ani function displays the three-word “Crack The Code” animation on startup.

Lastly, two interrupt functions manage the input from the encoder, one incrementing the digit upwards when turned clockwise and one downwards when turned anticlockwise. These are both rising edge interrupts available on pins 2 and 3 of an Arduino Uno. These could also be set as falling edge or low level interrupts. You can read more on the Arduino’s available interrupts if they interest you.

Now let’s upload the code and try it out.

Edit: Modify Code To Start With Locked Box

I’ve had quite a few people ask about how to start the box off locked rather than with the current push to start the game and lock the box. This is quite easy to change, you just need to modify three lines:

Add lockServo.write(45) into the setup function (at the end)  to lock the safe when the box is turned on.

Move line 73 – lockServo.write(140) into the if statement below it (at the beginning). The statement starting on line 84, so insert it into line 86. This will then only unlock the box again once the guess is correct.

Change the displayed text in line 101 to read display.print(F(“Start”)). As the box will already be locked on startup.

This will then start the box off locked. You’ll still need to press the encoder to start the game but this won’t do anything to the lock in the first round as it will already be locked. In subsequent games, starting the game will lock the box again.

Playing The Game and Trying To Crack The Code

The guessed code is input using the dial to increment the digit and a push on the dial to go to the next digit or to confirm the code once all four digits are selected.

Crack The Code Safe Puzzle Box

The LEDs on the front then light up to tell us what was correct in our guess. The red LEDs indicate that you’ve got a correct digit but not necessarily in the correct place and the green LEDs indicate that the digit is also in the correct place. The position or order of the LEDs is meaningless, they don’t give you any further information. Remember that you’ll need to be careful with double digits as the LEDs don’t light up multiples times for repeated digits unless you’ve repeated the digit in your guessed code.

The LEDs Light Up To Show Correct Digits And Positions

The safe is initially unlocked, allowing you to put something inside it.

The Box Opens To Put Something Inside

We then push the dial to lock the safe and generate a new code. You then have to try and figure out the code in order to open the safe again.

Box Locked

Once you’ve cracked the code, the box will unlock and display the number of attempts it took you to unlock it.

Code Cracked

Showing Number Of Attempts

The easiest way to see how the game is played is to watch the video at the beginning which has two demonstrations near the end.

Enjoy building your own crack the code safe box. Let me know how it goes for you or what changes you’ve made to it in the comments section.

Community Builds

Asaf Matan has made some fantastic modifications to the puzzle box to make the storage area a little larger, separate the electronics and to run on an ESP32 instead of an Arduino. Take a look at his blog post for the code and some additional build images:

Asaf Matan Build 1

Asaf Matan Build 2

Share This Guide

Crack The Code Social

Michael Klements
Michael Klements
Hi, my name is Michael and I started this blog in 2016 to share my DIY journey with you. I love tinkering with electronics, making, fixing, and building - I'm always looking for new projects and exciting DIY ideas. If you do too, grab a cup of coffee and settle in, I'm happy to have you here.

37 COMMENTS

  1. Dear Michael
    thanks for conceiving and sharing the crack-the-code-game and the vault.
    I build it together with my son, and he liked it — well, actually, I did most of the building part and he did most of the cracking part 🙂

    We did some changes to the original design though:
    a) We used an ESP32 instead of an arduino and adapted the software. If you wish, I can share with you, so you can make it available for others.
    b) The hinges glued to the frame are too weak and frequently become loose. I strengthened them using a lug. This however does not lock too well. Here I would suggest to change the geometry of the hinges, make them longer and add 2 rectangular openings to the front panel of the vault. The extended hinges could then be stuck through the holes. If each of the hinges have an additional hole it would be possible to fix them from the inside of the vault with an additional (wooden) pin.
    c) My son want to have an integrated charger for the battery. So we added one:
    https://www.ebay.de/itm/5V-18650-Lithium-Battery-Boost-UPS-Uninterrupted-Protection-Charging-Power-Board/173875727531
    To mount it, we brought some leftover plywood in form, glued it to the back panel and mounted the UPS on it using small screws.
    d) I widened one opening on the separator panel in the inside of the vault, so that all cables from coming from the door fit easily through and can slide a little forth and back, if the door is opened or closed
    e) From some left over plywood I build a strain relieve for the cables connected to some device on the front door.
    If you sent me your email address, I can sent you photos in return — that makes it maybe easier to understand the changes.

    Best regards
    Tilman

    • Hi Tilman,
      Thanks for the great feedback. Yes I’d be happy to share your ESP32 code, and also any pictures if you’ve got available? You can send them to admin(at)the-diy-life(dot)com.
      I also like your suggestions on the hinges, they are a bit fragile as currently designed.
      Well done on the build!

      • Dear, I also wanted to cut out the parts with the laser. My laser can handle a maximum of A4 format. As a beginner, I cannot transfer your A3 format to an A4 format. May I ask you to do that for me please. I am 82 years old and it is sometimes a bit more difficult for me. Thanks in advance. Kind regards, Guido.

  2. Hello Michael,
    Thanks a lot for sharing this project it is amazing. The first time I saw it I knew I had to build it. Since I don’t have access to a laser cutter I decided to port it to a 3d printer. So I took the svg files and manage to convert them into stl files so I can print them. I redesigned the button to my liking. Also, instead of using an Arduino Uno, I went for the Nano and because of that I was able to fit all the electronics on the front door, only 2 wires for the battery going to the back compartment.

    Thanks again for sharing.

    Best regards,
    Jean

    • Hi Jean,
      Thats great! Congratulations on your build and thanks for the feedback! Would you be happy to share some pictures of your build? I’d like to include a photo or two of a 3D printed one on the post as well.
      Thank you!

  3. Hello,
    I am a beginner in Arduino coding and I want to do this project. It is really cool!
    I am using an Arduino mega. I have changed the pins for the OLED pins accordingly. However, I am facing a problem with the rotary encoder: when I turn the button it doesn’t change the digits on the display (the rotary seems to work when I press it though, ie to start the game). I am not sure what the problem is. I have used the code file and did not change a thing. I would really appreciate some help as it is very frustrating 🙁

    Thanks!
    Best

    • The Arduino Mega has different hardware interrupt pins to the Uno, which the rotating of the encoder relies on. You’ll need to get these mapped correctly to get the encoder working. A quick look at the Mega’s pinout and I assume that you just need to change the following from 0 and 1:
      attachInterrupt(0,PinA,RISING);
      attachInterrupt(1,PinB,RISING);
      to 4 and 5:
      attachInterrupt(4,PinA,RISING);
      attachInterrupt(5,PinB,RISING);
      0 and 1 are for pins 20 and 21 on the Mega, which your encoder is not connected to.

      • Hoping this is still an active topic. I am trying to make this but extreme size, 10 digit code if possible. I know, seems kind of over the top, but hey, why not. I too am trying to use an Arduino Mega and for the time being I am laying it out on the Wokwi sandbox to prove it out first. I have successfully added the extra leds, increased the code up to 10 digits, but I cannot get the rotary to work. The switch works great, but the rotary doesn’t. I tried making the simulator with the 4 leds and 4 digit code, but I still cannot get the rotary to work. I tried the above interrupt, but the simulator still does not work.

        https://wokwi.com/projects/359366933824183297

  4. Hi Michael,
    I am a beginner and only started recently with Arduino and coding. I love your “crack the code ” safe.

    I copied your code and used the schematic to connect everything up. The encoder, the LED’s and the servo works perfectly. When I press the encoder to “start” the servo moves and the LED’s start to flash.

    I have an issue with the OLED screen. At first I thought I’d damaged the screen somehow, because it had a fuzzy display. Then I did some research and it turns out Adafruit screens are not that common here and the screen I have bought, was a generic screen. It’s a OLED Display 128×64, 1.3inch, 4 PIN. I found a forum where the one person suggested including the “u8lid” library. I covers a wide range of displays. I tested the theory using a test code and adding the “u8glib”, and the display worked!

    My problem now is changing the code so the screen works with the rest of the code. I have no idea how to do that. Is there any way you could maybe help changing, or advise how to change, just the display part of the code? I tried, but failed. I know too little about coding.

    Kind Regards,
    Lana

    • Hi Lana,
      This is the first time I’ve heard of this issue, thanks for letting me know.
      I’ll have a look at the u8lid library, I’ve never used it. You’ll probably need to go and change every line of code that is used to display content, it would be a fairly significant change to the code.

  5. Hello,
    can anyone write me how LEDs are paired? Whether according to the connection 1. green with 1. red or according to how they are numbered in the program (9 + 6) etc.?

    Thank you for your help.

    • I’m not really sure what you are asking. The LEDs are connected as shown in the breadboard layout. The cathode (negative) legs LEDs are all connected together after the resistors to ground or 0V.

      • I’m sorry, I guess I wrote it wrong. I know electrically how to connect them. I wanted to know how they were paired together. For the first number, the second number, etc. Or does it matter which two LEDs are together and in what place?
        Excuse my English, it’s just a Google translator.
        Thank you for your answer and time.

  6. Took a few attempts but finally managed to get the electronics working. I wired the LED’s the wrong way round to start with!! My bad! A brilliant tutorial, and lots of fun to construct. Thanks for taking your time to make this available.

  7. Would you know if this will work on a Nano board? I would like to make a permanent soldered version. My UNO version works great, but I want to reuse the UNO for other projects. Thanks.
    Steve

  8. bonjour super project que je vient de terminé et qui fonctionne . ce pendant quand on et trop rapide sur l encodeur rotatif sa beug !!! ou sa fait un reset !! . auriez vous une idée du souci merci par avance .

    • Good to hear you’ve built your own puzzle box. I’m not really sure what you mean by your description of the encoder error you’re getting, could you explain it a bit further?

  9. This is a brilliant project that begs to be made into a gadget geocache. Thanks for your hard work. I’ll post my modifications when I finish.

  10. Best
    I have a problem with the sketch for Arduino.
    I keep getting the error message: CrackTheCode:52:3: error: ‘display’ was not declared in this scope
    How can I fix that?

  11. hi michael, I am making the project to donate it, but I can’t make the numbers change, only the code 0000 remains and I don’t know what I can do, do you have any idea how to solve it?

  12. hi michael, I am making the project to donate it, but I can’t make the numbers change, only the code 0000 remains and I don’t know what I can do, do you have any idea how to solve it?

  13. Hi Michael,

    I am making this safe for my school IT project. Thanks for making this available. I have two questions?
    1. Can I simply use arduino nano without changing any code?
    2. I am a confused about what needs to be connected to the the 5V pin wire (the right most side, where it is going across) in the circuit diagram. I can see the black wire (GND) is connected to the encoder. What about the 5V red pin – it seems to be not connected to anything on the top part of the diagram.

    Eagerly waiting to your reply so I can finish my project.

    regards,
    Aaryan

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest posts

Which NVMe Hat Is The Best For A Raspberry Pi 5

If you don’t know already, I’ve been selling these 3D printed cases for Raspberry Pi’s online for a few years now. With the launch...

Track Aircraft In Real-time With Your Raspberry Pi Using The FlightAware Pro

Have you ever seen a flight overhead and wondered where it is going? Or seen a unique-looking aircraft and wondered what type or model...

Pi 5 Desktop Case For NVMe Base or HatDrive! Bottom

Today we're going to be assembling a 3D-printed case for the Raspberry Pi 5 and Pimoroni's NVMe Base or Pineberry's HatDrive! This is an...

Related posts