An ASCII puzzle for an escape room challenge

Jail Break

© Lead Image © besjunior; 123rf.com

© Lead Image © besjunior; 123rf.com

Article from Issue 243/2021
Author(s):

A digital puzzle presents a challenge for young people in an escape room.

A teacher recently asked me to help create a couple of puzzles for an escape room she was designing for her classes. Escape rooms have a number of interpretations, themes, and implementations but ultimately comprise a series of puzzles designed around a theme. Solving one puzzle provides a clue to something else. Sometimes a puzzle is just an off-the-shelf combination lock, and as you play other parts of the game, you'll discover the combination (or three numbers that you can try as the combination).

One puzzle I designed starts with a number on a seven-segment display (see the "Seven-Segment Display" box), a bundle of leads clipped onto two rows of electrical connections, and an unfinished set of notes. A previous adventurer has started to decode the puzzle and left notes for whoever might follow. The top set of connections is numbered 128, 64, 32, 16 , 8, 4, 2, and 1. The bottom set of connections isn't labeled, but each post has an associated LED, only a few of which are lit.

Seven-Segment Displays

Seven-segment displays are one of the most prevalent types of displays found in electronics projects. They are easy to control and very readable, even from large distances or in small physical sizes. The downside is that the value 7 is not displayed by seven discreet LEDs but by three LEDs arranged in the shape of a 7.

A number of driver chips can take care of the translation to an LED display. The MAX7219 used in this project can drive up to eight seven-segment displays (or 64 individual LEDs). If you are only displaying numbers, you can pass them directly to the 7219 in binary form, and it will display the appropriate segments to show the corresponding decimal number.

Because I want to show letters, as well, a special "no decode" mode tells the driver to just display the segments requested rather than decoding the value into a number. In this case, I pass an 8-bit binary number, where each bit represents one of the seven segments. The last bit represents the decimal point (not used in this project).

A seven-segment display by its nature has a "font" that's quite blocky. Numbers are no problem to display, and even most of the alphabet can be represented without too much trouble. However, the letters K, M, V, W, and X don't line up well with seven segments. If you use your imagination, though, these exceptions don't hinder things too much (Figure 1).

Figure 1: Several letters don't translate directly to seven-segment displays. Here's how I chose to represent them.

Theory of Operation

Built into the code of the display box, the secret word to be revealed is part of an Arduino program. The number displayed is the ASCII representation of the character currently being sought. Players must use clip leads to connect the numbers on the top to the active (lit) connections on the bottom so that the connected posts add up to the displayed number. A "check" post is tapped with an extra clip lead, and the display will either say CORRECT or NO.

After each check, the active posts change so the clip leads will always have to be rearranged; however, the number being sought remains the same until it is discovered. Once the number is discovered, the letter is revealed on the left of the display, and the players can start working on the next number.

Construction

The 3D-printed faceplate (Figure 2) has places for the electrical connections. Each 1-inch #8 bolt connects directly to an Arduino pin (Figure 3); the bottom row also connects to an LED to show whether that post is active. Some 1x3 lumber cut at 45° angles form the rest of the box, with scrap plywood used for the back. Because this is a short-term project, I didn't worry about battery access, but I did put a power switch on the outside.

Figure 2: The puzzle faceplate after I installed the LEDs. The empty rows of holes will house bolts as electrical contacts. The holes around the outside are to mount the panel on top of the project box.
Figure 3: The wiring for the puzzle box. Note that most connections just lead from the GPIO to more robust hardware on the panel. The top set of connections use PWM pins 2 through 9, with the check post on pin 10. The bottom set of connections use analog pins A2 through A9. The red line is the 5V power connection.

Code

In Listing 1 [1], the include in line 1 brings in an external library. In this case, that is LedControl.h, which controls the display driver attached to the seven-segment display.

Listing 1

cryptexCode.c

001 #include <LedControl.h>
002
003 String puzzle = "PASSWORD";
004 int iLetterIndex = 0;
005 char cLastLetter = 0;
006 LedControl lc=LedControl(14,15,16,1);
007
008 void displayTarget()
009 {
010   int i;
011   String sNumber = String ( puzzle [ iLetterIndex ] , DEC );
012
013   Serial.println ( puzzle [ iLetterIndex ] , DEC );
014
015   for ( i = 0 ; i < sNumber.length() ; i ++ )
016   {
017     showDigit ( 2-i , puzzle [ iLetterIndex ] );
018   }
019 }
020
021 unsigned int countBits(unsigned int n)
022 {
023     unsigned int iCount = 0;
024     while (n) {
025         iCount += n & 1;
026         n >>= 1;
027     }
028     return iCount;
029 }
030
031 void rotateOutputs()
032 {
033   int iNeededBits = 0;
034   int iCurrentBits = 0;
035   int iRandom = 0;
036
037   iNeededBits = countBits ( puzzle [ iLetterIndex ] );
038   while ( iNeededBits != countBits ( iRandom ) )
039   {
040     iRandom = random ( 0 , 255 );
041   }
042
043   Serial.println ( iRandom, BIN );
044   if ( ( iRandom & 1 ) != 0 ) digitalWrite ( 2 , HIGH );
045   else digitalWrite ( 2 , LOW );
046
047   if ( ( iRandom & 2 ) != 0 ) digitalWrite ( 3 , HIGH );
048   else digitalWrite ( 3 , LOW );
049
050   if ( ( iRandom & 4 ) != 0 ) digitalWrite ( 4 , HIGH );
051   else digitalWrite ( 4 , LOW );
052
053   if ( ( iRandom & 8 ) != 0 ) digitalWrite ( 5 , HIGH );
054   else digitalWrite ( 5 , LOW );
055
056   if ( ( iRandom & 16 ) != 0 ) digitalWrite ( 6 , HIGH );
057   else digitalWrite ( 6 , LOW );
058
059   if ( ( iRandom & 32 ) != 0 ) digitalWrite ( 7 , HIGH );
060   else digitalWrite ( 7 , LOW );
061
062   if ( ( iRandom & 64 ) != 0 ) digitalWrite ( 8 , HIGH );
063   else digitalWrite ( 8 , LOW );
064
065   if ( ( iRandom & 128 ) != 0 ) digitalWrite ( 9 , HIGH );
066   else digitalWrite ( 9 , LOW );
067 }
068
069 void ledYES()
070 {
071    showDigit ( 2 , 'Y' );
072    showDigit ( 1 , 'E' );
073    showDigit ( 0 , 'S' );
074 }
075
076 void ledNO()
077 {
078    showDigit ( 1 , 'N' );
079    showDigit ( 0 , 'O' );
080 }
081
082 void showDigit ( int iPosition , char cLetter )
083 {
084   int iPattern = 0;
085
086   switch ( cLetter )
087   {
088     case 'A': iPattern = 0b01110111;break;
089     case 'B': iPattern = 0b00011111;break;
090     case 'C': iPattern = 0b00001101;break;
091     case 'D': iPattern = 0b00111101;break;
092     case 'E': iPattern = 0b01001111;break;
093     case 'F': iPattern = 0b01000111;break;
094     case 'G': iPattern = 0b01011110;break;
095     case 'H': iPattern = 0b00110111;break;
096     case 'I': iPattern = 0b00010000;break;
097     case 'J': iPattern = 0b00111100;break;
098     case 'K': iPattern = 0b00000111;break;
099     case 'L': iPattern = 0b00001110;break;
100     case 'M': iPattern = 0b01010101;break;
101     case 'N': iPattern = 0b00010101;break;
102     case 'O': iPattern = 0b00011101;break;
103     case 'P': iPattern = 0b01100111;break;
104     case 'Q': iPattern = 0b01110011;break;
105     case 'R': iPattern = 0b00000101;break;
106     case 'S': iPattern = 0b01011011;break;
107     case 'T': iPattern = 0b00001111;break;
108     case 'U': iPattern = 0b00011100;break;
109     case 'V': iPattern = 0b00011000;break;
110     case 'W': iPattern = 0b01011000;break;
111     case 'X': iPattern = 0b00010010;break;
112     case 'Y': iPattern = 0b00111011;break;
113     case 'Z': iPattern = 0b01101100;break;
114     case '0': iPattern = 0b01111110;break;
115     case '1': iPattern = 0b00110000;break;
116     case '2': iPattern = 0b01101101;break;
117     case '3': iPattern = 0b01111001;break;
118     case '4': iPattern = 0b00110011;break;
119     case '5': iPattern = 0b01011011;break;
120     case '6': iPattern = 0b01011111;break;
121     case '7': iPattern = 0b01110000;break;
122     case '8': iPattern = 0b01111111;break;
123     case '9': iPattern = 0b01111011;break;
124   }
125   lc.setRow ( 0 , iPosition , iPattern );
126 }
127
128 void setup() {
129   // put your setup code here, to run once:
130   Serial.begin ( 9600 );
131
132   pinMode ( A0 , INPUT_PULLUP );
133   pinMode ( A1 , INPUT_PULLUP );
134   pinMode ( A2 , INPUT_PULLUP );
135   pinMode ( A3 , INPUT_PULLUP );
136   pinMode ( A4 , INPUT_PULLUP );
137   pinMode ( A5 , INPUT_PULLUP );
138   pinMode ( A6 , INPUT_PULLUP );
139   pinMode ( A7 , INPUT_PULLUP );
140
141   pinMode ( 10 , INPUT_PULLUP );
142
143   pinMode ( 14 , OUTPUT );  // display DATA
144   pinMode ( 15 , OUTPUT );  // display CLK
145   pinMode ( 16 , OUTPUT );  // display CS
146
147   digitalWrite ( 16 , HIGH );
148
149   pinMode ( 2 , OUTPUT );
150   pinMode ( 3 , OUTPUT );
151   pinMode ( 4 , OUTPUT );
152   pinMode ( 5 , OUTPUT );
153   pinMode ( 6 , OUTPUT );
154   pinMode ( 7 , OUTPUT );
155   pinMode ( 8 , OUTPUT );
156   pinMode ( 9 , OUTPUT );
157
158   randomSeed ( analogRead ( 12 ) );
159   rotateOutputs();
160
161   lc.shutdown(0,false);
162   lc.setIntensity ( 0 , 8 );
163   lc.clearDisplay ( 0 );
164 }
165
166 void loop() {
167   // put your main code here, to run repeatedly:
168   if ( cLastLetter != puzzle [ iLetterIndex ] )
169   {
170     displayTarget();
171     cLastLetter = puzzle [ iLetterIndex ];
172   }
173
174   if ( digitalRead ( 10 ) == LOW )
175   {
176     int iPort = 0;
177
178     if ( digitalRead ( A7 ) == 0 ) iPort += 1;
179     if ( digitalRead ( A6 ) == 0 ) iPort += 2;
180     if ( digitalRead ( A5 ) == 0 ) iPort += 4;
181     if ( digitalRead ( A4 ) == 0 ) iPort += 8;
182     if ( digitalRead ( A3 ) == 0 ) iPort += 16;
183     if ( digitalRead ( A2 ) == 0 ) iPort += 32;
184     if ( digitalRead ( A1 ) == 0 ) iPort += 64;
185     if ( digitalRead ( A0 ) == 0 ) iPort += 128;
186
187     if ( cLastLetter == iPort )
188     {
189       iLetterIndex += 1;
190       Serial.println ( "CORRECT" );
191       ledYES();
192       delay ( 1000 );
193       for ( i = 0 , i < iLetterIndex - 1 , i ++ )
194       {
195          showDigit ( 7 - i , puzzle [ i ] );
196       }
197       Serial.println ( puzzle.substring ( 0 , iLetterIndex ) );
198       rotateOutputs();
199     }
200     else
201     {
202       Serial.println ( "INCORRECT" );
203       ledNO();
204       delay ( 1000 );
205       displayTarget();
206       rotateOutputs();
207     }
208     while ( digitalRead ( 10 ) == LOW ) delay ( 100 );
209   }
210 }

The next several lines define the string puzzle, which I've set as PASSWORD, as the word that will be revealed as the game is played; the iLetterIndex variable, which is the letter position currently being discovered in the puzzle; and cLastLetter, which is the ASCII value of the character currently being discovered. Finally, lc is an instance of LedControl included on line 1. Its four arguments are the pin numbers for data, clock, chip select, and the number of 7219 chips in the series (in this case, just 1).

The displayTarget function (lines 8-19) sends to the display the number that the players are trying to find. The int i line is defined inside the function, so it is local to the enclosing function. In defining the sNumber string, the character position currently being sought is iLetterIndex, and puzzle is the secret word. This single-character string is what the players are currently seeking. The argument DEC specifies a decimal value of the character, or its value in ASCII, rather than the character itself and will be shown on the LED display.

Serial.println prints the same value to the serial monitor (purely for debugging). Because the serial monitor is inaccessible and the physical connection is sealed in a box, I didn't worry about removing it for the final version of the code.

The for loop iterates over each character in the string to be displayed, and line 17 sends it to the display. I'll talk about showDigit and how it works a little later.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • New Tech Retrofit

    An electronic project at a local science center was showing its age, calling for a refresh: in this case, rebuilding it almost from scratch with an Arduino instead of relays.

  • Perl: Arduino Project

    With a few lines of self-written firmware and a simple Perl script, Perlmeister Mike Schilli trains one of his Linux computers with a plugged in Arduino board to switch electrical equipment on and off using a relay. Enchanting.

  • Serial Communications

    We explore serial communications, from the electrical specs to protocols and libraries, with an example of serial communication with an Arduino.

  • Sudoku

    Some regard Sudoku as the 21st

    century Rubik’s Cube. We’ll

    show you how to get started

    with Sudoku in Linux.

  • Knight's Tour

    If you're looking for a head start on solving the classic Knight's Tour chess challenge, try this homegrown Python script.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News