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.

Last fall at my local board game meetup I was introduced to A Game of Thrones The Board Game. Originally based on Diplomacy, A Game of Thrones places three to six players in charge of the great houses of Westeros to vie for control of the continent. With a minimal element of luck1 players spread influence with diplomacy, card selection, and strategic planning. I am fascinated by this game and have played a few dozen times in person and one hundred games online2.

Now that we have dispensed with the pleasantries we can get on to the purpose of this post: expanding the game to support up to nine players. There are a few variants available online - “A Feast For Crows” Expansion3, “The Winds of Winter” Expansion, and Essos4. I went for the “Essos” expansion since it was the first one I found. I intend to try the other two when my regular playing group gets tired of the Essos version.

The Map

In order to make room for three more players/houses the territories in the central portion of Westeros have been divided and rearranged and the western portion of Essos has been introduced. There have also been some other balance adjustments (e.g. rearranging resources, borders, and strongholds) as fallout from introducing many new territories. The wildlings track has changed to increments of 3 to account for the new players5. The supply track has also been expanded up to 8 barrels since there are potentially more territories to cover on the path to seven castles. Click on the map for a full resolution version.

the map

Essos

The most obvious change when looking at the new map is the addition of a new continent on the eastern portion of the map: Essos. Hence the name of this expansion. Essos consists of eight territories with two strongholds and two castles. Braavos starts with a neutral force of 8 to eliminate a quick rush but is a valuable position late in the game for many houses.

The more interesting change accompanying the new continent is the additional sea zones. Now there are four sea zones off the east coast of Westeros which allow for three houses to participate in combat. In the official six player map it is common for sea zones to fall into a stalemate where players continually place support and raid tokens in alternating sea zones which completely eliminates sea zones from play6. With three players the dynamic changes to incentivize diplomacy and/or strategy.

The Riverlands

Move over Lannister time to make room for another house. Divided into eight territories and covered with castles, strongholds, and resources - the Westerlands now serve as the home for two houses. This change requires drastic changes to Lannister, Greyjoy, and Tyrell strategies while bringing more of the story to life by turning the Riverlands into a war-torn countryside.

The Vale

The Vale has been littered with resources and is no longer the stomping grounds for House Stark. There are now two strongholds and two castles east of the river along with a new sea zone, The Bay of Crabs. The impassable borders and rivers along with no single central region make navigating and holding the Vale a significant challenge. If you can muster enough strength there are plenty of resources and easy access to Essos.

Impassable Borders

The official map contains rivers denoted as blue borders between regions. Rivers break the adjacency between otherwise neighboring spaces. So units cannot move between, raid, or support spaces across a river. Unless there is a bridge. The Essos map introduces the concept of impassable borders which are black lines between spaces. These are functionally equivalent to rivers but lack the thematic motivation. The impassable borders were introduced since the central region of the board is now crowded with two additional houses.

Balance Adjustments

There are less drastic, but equally important, changes throughout the rest of Westeros in order to keep the game balanced. Starting at the top and working our way down through each of the remaining houses, first we have House Stark. Since the Vale is now occupied by a new house the resources that the Starks’ traditionally rely on have been redistributed across a newly partitioned north.

The Shivering Sea now extends to Braavos and due to the newly added sea zone The Byte loses the ability act as unraidable support7. That honor has moved to the west coast with the addition of “Bear Island” and the division of the Bay of Ice into two sea zones. No longer touching either ocean, Winterfell is dramatically smaller. Instead Deepwood is added to the west coast with a castle and a port.

Continuing down the western coast we reach House Greyjoy. The castle at Flint’s Finger and stronghold at Seagard have swapped places. No other changes beyond the modifications for the Tullys and Lannisters.

Continuing the counterclockwise tour of Westeros, House Tyrell. The castle previously The Reach is now further from King’s Landing making it easier to hold, while the extra supply in from Blackwater is now further from Highgarden forcing House Tyrell to look elsewhere for supply.

Next up: the Martells. Storm’s End is no longer adjacent to the Sea of Dorne but the Rainwood has been added and includes a supply. Lys is a nearby island and Essos is only one sea zone away.

Finally House Baratheon whose seat has been moved from Dragonstone to Storm’s End. This is to allow for separation between all three new neighbors. Tarth is a new island added to the east of Storm’s End and provides the opportunity for a quick supply grab. The rest of the changes that affect House Baratheon have been discused previously.

Printing the Map

By replacing the tracks with Essos the expanded map is nearly identical in size to the original. Extra printouts can be placed anywhere around the table to track the influence and victory status for each house. I went to the FedEx Office store across the street from my apartment to print the map. Two hours and $50 later I walked out with a freshly minted map.

The New Houses

The three new houses are House Arryn, House Targaryen, and House Tully. Each house needs cards, power tokens, track markers, and a shield (to hide which orders have yet to be used). The original designer produced a few pdfs containing all of the required images. My wife and I spent two nights at the FedEx Office store printing, cutting, and laminating all of the pieces. Lamination is not necessarily the best production method, but it was the cheapest that provided any level of durability. I plan to try a different method next time.

House Arryn

The House Arryn cards place a strong emphasis on power tokens. In fact, every card with text deals with power tokens. This provides an interesting twist to bidding since tokens are easier to come by but are also valuable in battle. house arryn cards

House Targaryen

The House Targaryen cards either increase strength of units in combat or allow you to place/move orders after resolving combat. Daenerys is the only card in the game (aside from the awful Tides of Battle cards) with a skull symbol. The skull kills one unit even in the presence of towers. house targaryen cards

House Tully

Since the designers could not settle on seven cards House Tully is the only house to have eight. Staying true to the theme, many of the cards provide tactical advantages if you are able to predict what card your opponent will play. house tully cards

The Units

I have recently obtained a Printrbot Simple Metal 3D printer for projects just like this one. To create the pieces I found and printed these pieces by Srokap for each house at 30% infill to match the weight of the official pieces. Purple for House Targaryen, sky blue for House Arryn, and orange for House Tully. Purple and blue work thematically, but orange was the only other color of plastic I had on hand. the pieces, 3d printed

References and Files

I have uploaded the resources required to make the game to Dropbox (tar.gz or zip). If you try the game or have any suggestions for improving it let me know.


  1. The game has only one element of randomness - the ordering of events in the Westeros decks.
  2. Thronemaster
  3. Not to be confused with the official “A Feast For Crows” expansion with House Arryn and scenarios.
  4. Video overview from the designer
  5. The wildling track still follows the 1 power token per three players per space ratio. This leaves bidding strategies relatively untouched.
  6. The Raid/Support stalemate would be a great topic for a future blog post.
  7. Support placed in an area which cannot be raided since the player controls all neighboring zones.