Overview of Puzzle

Last weekend I wanted to continue learning about arduino and 3D printing, so I created a physical logic puzzle. Inspired by the XBox puzzle from the Famine Games, the Rainbow Box is a logic puzzle where the goal is to enter the word “rainbow” into the box. There is not much else I can say about the puzzle without spoiling it.

Spoiler Warning


The rest of this post will provide the solution as well as explore the implementation of the puzzle. If you want to solve the puzzle yourself stop here and come back once you are done.

Solution

Note


Since the seven segment display has only two columns of vertical lines it is impossible to render the entire alphabet, including "w". Which is necessary for entering the word "rainbow". Thus it is required to determine the function of each button before submitting an answer. I guess one could brute force it by trying all 32 combinations after entering "rainbo", but that defeats the goal of a puzzle.

To begin, flip the switch and wait for the screen to finish displaying the spiral. Now the box is ready to receive input. There are six buttons, one on each side of the cube. The first step in solving is to observe what each button does when pressed individually.

The blue button displays a p. The green button displays an h. The orange button displays a b. The red button displays an a. The white button displays the phrase nope. The yellow button displays d.

The white button is an obvious outlier, displaying a phrase instead of a single letter. From this fact we deduce that the white button is used to submit the current letter. Now we have to look at the colored buttons. The table below sorts the buttons alphabetically based on the letter produced when pressed.

Button ColorLetter Displayed
reda
orangeb
yellowd
greenh
bluep
Fig1. Mapping from button to letter

Alphabetizing the button characters has revealed an interesting pattern: the buttons are now ordered based on the colors of the rainbow.1 The next step is to try pairs of buttons. For each pair of buttons predict what be displayed and record the output in a five-by-five table.

redorangeyellowgreenblue
red ceiq
orangec fjr
yellowef lt
green ijl ?
blue qrt?
Fig2. Pairs of buttons

Now that all of the pairs of buttons have been observed there is enough information to make a hypothesis about the role of each button. Start by looking at the list of single button outputs: b, d, h, p. Compare that list with the list of pairs which include the red button: c, e, i, and q. Notice that the characters in the red button list are the very next character in the alphabet. From this, the hypothesis is that the red button shifts the output by one letter. This hypothesis can be tested by pressing the red button along with one of the non-red pairs. Pressing the orange, yellow, and red buttons simulataneously reveals a g. Thereby confirming our hypothesis.

Using the same process shows that the orange button (c, f, j, r) shifts by two letters and the yellow button(e, f, l, t) shifts by four letters. Green (i, j, l, ?) and blue (q, r, t, ?) appear to shift by eight and sixteen but have an unknown letter. Based on our theory we can resolve that ambiguity by adding the red button and then shifting backwards by one letter. green-blue-red reveals a y which means that green-blue is x.

The buttons shift the display character by 1, 2, 4, 8, or 16. All of which are powers of two. This means that the box is using each button to represent a single bit in the the letter that is displayed. The following figure shows the conversion between bits and letters.

LetterIndexBinary
A 100001
B 200010
C 300011
D 400100
E 500101
F 600110
G 700111
H 801000
I 901001
J1001010
K1101011
L1201100
M1301101
N1401110
O1501111
P1610000
Q1710001
R1810010
S1910011
T2010100
U2110101
V2210110
W2310111
X2411000
Y2511001
Z2611010
Fig3. letters, decimal, and binary representations of numbers

The table above shows the buttons needed to enter each letter.

  1. r = 10010 = blue + orange
  2. a = 00001 = red
  3. i = 01001 = green + red
  4. n = 01110 = green + yellow + orange
  5. b = 00010 = orange
  6. o = 01111 = green + yellow + orange + red
  7. w = 10111 = blue + yellow + orange + red

Entering each of the button combinations above produces the final message: “congrats you have found the pot of gold” which is the end of the puzzle.

Typeface

Each letter is rendered on the seven segment display. The rendering process uses the integer computed from the current button configuration as an index into the typeface table. Each row of the table contains eight integers representing the signal to send to each segment (HIGH or LOW).

The following figure is a JavaScript implementation of the rendering process. The text input accepts a single character and displays it on the adjacent display. Some of the characters are not renderable and are represented by a single dot.

Fig4. Typeface Explorer

Code

The code centers around one main data strucure, state. state represents the current state of each button (pressed or not) and the current stage of the puzzle (which letters have been correctly entered). There are then a series of functions which construct the state from the buttons, render the state on the display, and submit a letter. The numberEntered function is particularly interesting as it succintly summarizes the entire puzzle.

const int LED_A = 2;
const int LED_B = 3;
const int LED_C = 4;
const int LED_D = 5;
const int LED_E = 6;
const int LED_F = 7;
const int LED_G = 8;
const int LED_H = 9;
const int RED_PIN = 10;
const int ORANGE_PIN = 11;
const int YELLOW_PIN = 12;
const int GREEN_PIN = 13;
const int BLUE_PIN = 14;
const int SUBMIT_PIN = 15;

static const int typeface[32][8] =
  { {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW }
  , {HIGH, HIGH, HIGH, LOW,  HIGH, HIGH, HIGH, LOW }
  , {LOW,  LOW,  HIGH, HIGH, HIGH, HIGH, HIGH, LOW }
  , {HIGH, LOW,  LOW,  HIGH, HIGH, HIGH, LOW,  LOW }
  , {LOW,  HIGH, HIGH, HIGH, HIGH, LOW,  HIGH, LOW }
  , {HIGH, LOW,  LOW,  HIGH, HIGH, HIGH, HIGH, LOW }
  , {HIGH, LOW,  LOW,  LOW,  HIGH, HIGH, HIGH, LOW }
  , {HIGH, HIGH, HIGH, HIGH, LOW,  HIGH, HIGH, LOW }
  , {LOW,  LOW,  HIGH, LOW,  HIGH, HIGH, HIGH, LOW }
  , {LOW,  LOW,  LOW,  LOW,  HIGH, HIGH, LOW,  LOW }
  , {LOW,  HIGH, HIGH, HIGH, HIGH, LOW,  LOW,  LOW }
  , {LOW,  HIGH, HIGH, LOW,  HIGH, HIGH, HIGH, HIGH}
  , {LOW,  LOW,  LOW,  HIGH, HIGH, HIGH, LOW,  LOW }
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  HIGH}
  , {LOW,  LOW,  HIGH, LOW,  HIGH, LOW,  HIGH, LOW }
  , {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW,  LOW }
  , {HIGH, HIGH, LOW,  LOW,  HIGH, HIGH, HIGH, LOW }
  , {HIGH, HIGH, HIGH, LOW,  LOW,  HIGH, HIGH, LOW }
  , {LOW,  LOW,  LOW,  LOW,  HIGH, LOW,  HIGH, LOW }
  , {HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, HIGH, LOW }
  , {LOW,  LOW,  LOW,  HIGH, HIGH, HIGH, HIGH, LOW }
  , {LOW,  LOW,  HIGH, HIGH, HIGH, LOW,  LOW,  LOW }
  , {LOW,  HIGH, HIGH, HIGH, HIGH, HIGH, LOW,  LOW }
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  HIGH}
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  HIGH}
  , {LOW,  HIGH, HIGH, HIGH, LOW,  HIGH, HIGH, LOW }
  , {HIGH, HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, LOW }
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW }
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW }
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW }
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW }
  , {LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW } };

void setup() {
  pinMode(LED_A, OUTPUT);
  pinMode(LED_B, OUTPUT);
  pinMode(LED_C, OUTPUT);
  pinMode(LED_D, OUTPUT);
  pinMode(LED_E, OUTPUT);
  pinMode(LED_F, OUTPUT);
  pinMode(LED_G, OUTPUT);
  pinMode(LED_H, OUTPUT);
  pinMode(RED_PIN,    INPUT);
  pinMode(ORANGE_PIN, INPUT);
  pinMode(YELLOW_PIN, INPUT);
  pinMode(GREEN_PIN,  INPUT);
  pinMode(BLUE_PIN,   INPUT);
  pinMode(SUBMIT_PIN, INPUT);

  spiral();
}

enum stage {
  START,
  FAIL,
  R,
  RA,
  RAI,
  RAIN,
  RAINB,
  RAINBO,
  WIN,
};

struct state {
  enum stage progress;
  bool redPressed;
  bool orangePressed;
  bool yellowPressed;
  bool greenPressed;
  bool bluePressed;
  bool submitPressed;
};

void loop() {
  static struct state s = {START, false, false, false, false, false, false};
  readButtons(&s);
  renderState(&s);
  if(s.submitPressed) { submit(&s); }
}

void spiral() {
  spiralOn();
  spiralOff();
}

void spiralOn() {
  digitalWrite(LED_A, HIGH);
  delay(100);
  digitalWrite(LED_B, HIGH);
  delay(100);
  digitalWrite(LED_C, HIGH);
  delay(100);
  digitalWrite(LED_D, HIGH);
  delay(100);
  digitalWrite(LED_E, HIGH);
  delay(100);
  digitalWrite(LED_F, HIGH);
  delay(100);
}

void spiralOff() {
  digitalWrite(LED_A, LOW);
  delay(100);
  digitalWrite(LED_B, LOW);
  delay(100);
  digitalWrite(LED_C, LOW);
  delay(100);
  digitalWrite(LED_D, LOW);
  delay(100);
  digitalWrite(LED_E, LOW);
  delay(100);
  digitalWrite(LED_F, LOW);
  delay(100);
}

void readButtons(struct state* s) {
  if (digitalRead(RED_PIN) == HIGH) { s->redPressed = true; } else { s->redPressed = false; }
  if (digitalRead(ORANGE_PIN) == HIGH) { s->orangePressed = true; } else { s->orangePressed = false; }
  if (digitalRead(YELLOW_PIN) == HIGH) { s->yellowPressed = true; } else { s->yellowPressed = false; }
  if (digitalRead(GREEN_PIN) == HIGH) { s->greenPressed = true; } else { s->greenPressed = false; }
  if (digitalRead(BLUE_PIN) == HIGH) { s->bluePressed = true; } else { s->bluePressed = false; }
  if (digitalRead(SUBMIT_PIN) == HIGH) { s->submitPressed = true; } else { s->submitPressed = false; }
}

void renderIndex(unsigned int index) {
  if(index > 31) { index = 0; }
  const int* pattern = *(typeface+index);
  digitalWrite(LED_A, *pattern++);
  digitalWrite(LED_B, *pattern++);
  digitalWrite(LED_C, *pattern++);
  digitalWrite(LED_D, *pattern++);
  digitalWrite(LED_E, *pattern++);
  digitalWrite(LED_F, *pattern++);
  digitalWrite(LED_G, *pattern++);
  digitalWrite(LED_H, *pattern++);
}

void renderCharacter(char c) {
  if(c >= 'A' && c <= 'Z') {
    renderIndex(c - 'A' + 1);
  } else if(c >= 'a' && c <= 'z') {
    renderIndex(c - 'a' + 1);
  } else {
    renderIndex(0);
  }
}

void renderString(char* s) {
  while (*s) {
    renderCharacter(*s++);
    delay(400);
  }
}

void renderState(const struct state* s) {
  renderIndex(numberEntered(s));
}

void submit(struct state* s) {
  switch(s->progress) {
    case START:
      if(letterEntered(s) == 'R') { s->progress = R; renderString((char*) "r"); } else { s->progress = FAIL; }
      break;
    case R:
      if(letterEntered(s) == 'A') { s->progress = RA; renderString((char*) "a"); } else { s->progress = FAIL; }
      break;
    case RA:
      if(letterEntered(s) == 'I') { s->progress = RAI; renderString((char*) "i"); } else { s->progress = FAIL; }
      break;
    case RAI:
      if(letterEntered(s) == 'N') { s->progress = RAIN; renderString((char*) "n"); } else { s->progress = FAIL; }
      break;
    case RAIN:
      if(letterEntered(s) == 'B') { s->progress = RAINB; renderString((char*) "b"); } else { s->progress = FAIL; }
      break;
    case RAINB:
      if(letterEntered(s) == 'O') { s->progress = RAINBO; renderString((char*) "o"); } else { s->progress = FAIL; }
      break;
    case RAINBO:
      if(letterEntered(s) == 'W') {
        s->progress = WIN;
        renderString((char*) "congrats you have found the pot of gold");
      } else {
        s->progress = FAIL;
      }
      break;
    default:
      break;
  }

  if(s->progress == FAIL) {
    renderString((char*) "nope");
    s->progress = START;
  }
}

unsigned int numberEntered(const struct state* s) {
  return (s->redPressed    << 0)
       | (s->orangePressed << 1)
       | (s->yellowPressed << 2)
       | (s->greenPressed  << 3)
       | (s->bluePressed   << 4);
}

char letterEntered(const struct state* s) {
  return numberEntered(s) + 'A' - 1;
}

Circuit Diagram2

Fig5. Circuit Diagram

3D Models

I created the panels for the box with 123D design. I started by measuring the battery holder since it is the largest component. Then I added some buffer and ended up with each panel being 70mm x 70mm x 5mm. Next I worked out the joints. This was accomplished by adding 5mm x 5mm x 5mm cubes along each edge of each panel, forming a complete 80mm x 80mm x 80mm box. With all of the pieces in place I started merging the cubes along each edge to one of the two adjacent panels, alternating between each panel to form a finger joint. When I reached the corners I arbitrarily chose one of the three panels to join the final 8 cubes to.

With the box completed I measured each of the physical components (switches and display). Then I placed corresponding holes around the box where I wanted the components to be attached. After the model was finished it was just a matter of printing.

Fig6. Screenshot of box parts in 123D Design

The model file shown in Fig6 can be downloaded here.

Assembly

Physically assembling the box was the most challenging aspect of this project. While I have spent many years learning how to think in three dimensions and how to model the world in software, I have spent zero time soldering outside of assembling my ergodox3. A few burnt fingers and ugly solder joints later I managed to build a working prototype.

Once completing the box I realized that I spent zero time considering the mechanical properties of the wires and solder joints. This oversight makes the box fragile and replacing the batteries almost always requires pulling out the soldering iron and reattaching wires. I have already purchased screw terminals, zipties, and a hot glue gun to avoid the same error when building the next puzzle.

Transportation

In order to store and transport the cube I took the assembled puzzle to the container store and tried fitting it into a bunch of different boxes. I ended up using one of the cylindrical gift boxes. Then I created spacers for the top and bottom to prevent the puzzle from bouncing around. I also attached some foam (left over from one of my pelican cases) to the top spacer to reduce damage from any impacts or vibrations.


  1. This ordering mechanic was intentionally created to reduce the amount of memorization required for entering letters.
  2. Created with Fritzing which was surprisingly intuitive. Highly recommended. NixOS even has a package for it.
  3. Speaking of mechanical keyboards, I should really get around to writing that post.