An ASCII puzzle for an escape room challenge
Jail Break
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).
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.
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
(incl. VAT)
Buy Linux Magazine
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.
News
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.
-
Fedora 41 Released with New Features
If you're a Fedora fan or just looking for a Linux distribution to help you migrate from Windows, Fedora 41 might be just the ticket.
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.