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.

One of the most common frustrations when working with rust is the speed of the compiler. Well actually, the lack of it. At work we currently have a 15k line project. From a clean slate, compilation takes 92 seconds. If all of the dependencies are compiled, cargo build still takes over 30 seconds to run on a two year old 15 inch MacBook Pro. Anecdotally, this is more than enough time to get distracted with Slack or Reddit. Time to dive into the less common features of cargo.

By design the usual collection of cargo commands do not expose the underlying details of the compiler. This choice removes a large swath of potential build system issues. But sometimes users need to fiddle with the lower levels of the toolchain. cargo provides a subcommand specifically for this purpose - cargo rustc.

λ> cargo rustc –help
Compile a package and all of its dependencies

In our project we have three targets: a library which contains all of the business logic, an executable which wraps the library and provides the command line parsing, and a second executable for managing our rfc process.1 With the normal subcommands - build, test, doc - cargo automatically handles creating all three build artifacts. This is one of the advantages of severely limiting users ability to customize the build process. cargo rustc does not provide this affordance, instead requiring you to specify the desired build artifact with --lib or --bin. The documentation quoted below explains exactly how to use these flags.

This command requires that only one target is being compiled. If more than one
target is available for the current package the filters of –lib, –bin, etc,
must be used to select which target is compiled. To pass flags to all compiler
processes spawned by Cargo, use the $RUSTFLAGS environment variable or the
`build.rustflags` configuration option.

Reading through github issues, reddit, and the internals forum there is oft mention of the -Z flag to rustc. According to the help page -Z is the catch-all flag to”Print internal options for debugging rustc”. Let’s see what is available.

λ> cargo rustc –lib -Zhelp
warning: the option `Z` is unstable and should only be used on the nightly compiler, but it is currently accepted for backwards compatibility; this will soon change, see issue #31847 for more details

Oops. rustup to the rescue. Adding the +nightly flag to our command will tell cargo to use the nightly toolchain and thus remove the warning. The command to run is now cargo +nightly rustc --lib -Zhelp

Three of the debug options have reduced the wait for feedback: parse-only, no-trans, and incremental.2 The first two options skip compilation phases while incremental caches intermediate compilation results and only reruns the phases invalidated by subsequent changes.

parse-only does exactly what it says on the tin - only runs the parse phase of the compiler. Only running parsing catches typos and some type errors and completes in under a second. This behavior serves as the basis for many editor plugins such as flycheck.

no-trans runs all of the compilation phases up until llvm. This this covers linting and analysis which produce the bulk of the valuable feedback provided by the compiler. Including unimplemented methods for traits, missing match branches, move semantics, and lifetime checks. I have noticed myself running this command most frequently. Looking through my shell history confirms that notion.

Incremental compilation is a very new and experimental feature in rustc. There are many caveats with using incremental compilation, not the least of which is no guarantees on correctness. The goal of this exercise is to document some options for gaining information quickly about your rust program. Not to reduce overall compilation times. Incremental compilation is enabled by the incremental parameter to the -Z flag. incremental also requires a path to store the incremental build index. So the final command for experimenting with incremental compilation is cargo +nightly rustc --lib -Zincremental=incremental_state. With incremental compilation we have seen build times reduce to 4 seconds for minor changes to a single module.

tl;dr cargo +nightly rustc --lib -Zno-trans is awesome.3


  1. We follow a similar rfc process to rust for proposing changes to the project. So far we have found the separation of design and implementation to significantly reduce change review time (even when adding together rfc and implementation reviews). This probably deserves its own post.
  2. When reading through the help listing I was reminded of no-analysis which is invaluable in debugging macros but completely unrelated to this post.
  3. You can automatically run this command with cargo +nightly watch -- "rustc --lib -- -Zno-trans". The watch subcommand can be found on github.

For the past few months I have been working with a team of puzzlers to write a puzzle hunt for the DC area called DCPHR1. A puzzle hunt is an event where teams of people compete to solve puzzles of all types at different locations around the city. Our first hunt was run last weekend in Clarendon. This post describes my experience writing one of the puzzles Lycanthropic Love.

Brief tangent: DCPHR is an annual event. We are always looking for more people to help write, test, and run the hunt. Email me if you are interested in participating in developing next year’s hunt.

Take a moment now to go look at the final product hosted at lycanthropic.love. The rest of this post will dive into the details of both the puzzle mechanics and their implementation in the browser with Elm. So, beware of spoilers.

I chose to work on this puzzle specifically to experiment with new browser technologies. So I couldn’t go with plain JavaScript. I’m drawn to languages over frameworks, which leaves me with ClojureScript, CoffeeScript, TypeScript, or Elm. Past experience with Clojure2 and ClojureScript3 left me wanting more useful error messages and less Java dependencies. CoffeeScript and TypeScript are supersets of JavaScript which forces them to maintain / paper over some of the JavaScript ugliness (the exact thing I was trying to avoid). Which leaves Elm.

Elm provides a runtime which handles the interaction with the DOM and JavaScript and thus allows me to focus on my business logic. Immutability, friendly errors, and strong typing also match closely with my recent rust experience. So Elm it is.

Lycanthropic Love is a satirical dating app for Werewolves. Inspired by Tinder, you are presented with matches. For each match you are expected to swipe right for yes and left for no. In this puzzle you are presented with 7 different profiles. Your job is to read each profile, determine a pattern for what they like, and then select the appropriate matches.

Each profile contains a list of likes. All of the likes have a common relationship. The matches which should be swiped right also have this relationship. For example, the Bare Wolf lists his likes as “Pandemonium”, “discount sales”, “education”, and “Pabst Blue Ribbon”. Each of these items contain each vowel exactly once, also known as supervocalics4.

Once all of the matches are swiped, the results are listed on the main page as a series of paws up or down. These five swipes can then be converted to a letter using a binary table (common encoding technique provided in the intro packet). The answer to the puzzle is a seven letter word.

Now for the fun part: writing web page. Let’s start with the wolf data structure (called Models in Elm).

type alias Wolf =
  { name            : String
  , imgLink         : String
  , likes           : List String
  , matches         : List Match
  }

type alias Match =
  { name            : String
  , imgLink         : String
  , shouldLike      : Bool
  , swipe           : Maybe SwipeDirection
  }

type SwipeDirection
  = Left
  | Right

I chose to store the matches inside of the Wolf type and the SwipeDirection inside of the Match. This choice allowed rendering to be a straightforward tree walk, while storing a swipe requires two list lookups, both for the wolf and the match I later found out (while watching an elm-conf presentation) that a Ziplist5 would have been the correct compromise.

To provide a taste of the rendering section, let’s look at the process for rendering the profile screen for a single wolf. The function takes a Maybe Wolf since it is possible that a wolf hasn’t been selected yet (likely due to the refresh or back button, one of the many areas that could be improved) and returns a request to the Elm runtime (and virtual dom) to add nodes to the DOM tree. This indirection is one of the key reasons Elm is both safe and fast. Developers only write requests and must deal with both the success and failure of that request. The Elm runtime then optimizes DOM insertions with one of the fastest virtual DOM implementations.

renderCurrent : Maybe Wolf -> Html Msg
renderCurrent wolf =
  case wolf of
    Just wolf ->
      let dislikes =
        case List.length(wolf.dislikes) of
          0 -> div [] []
          _ -> div []
               [ h3 [] [ text "Dislikes" ]
               , div [] [ text (String.join ", " wolf.dislikes) ]
               ]
      in
      div [ class "wolf-profile" ]
      [ wolfIcon (onClick NoOp) wolf
      , h1 [] [ text wolf.name ]
      -- , h2 [] [ text wolf.epitat ]
      , h3 [] [ text "Likes" ]
      , div [] [ text ( String.join ", " wolf.likes ) ]
      , dislikes
      , a [ class "button",  onClick ShowMatches ] [ text "View Matches" ]
      , a [ class "button", onClick Logout ] [ text "Back" ]
      ]
    Nothing -> div [ class "error" ] [ text "Render Current triggered without a wolf" ]

To demonstrate both the consequences of the Wolf type structure and my lack of experience with Elm, here is the code which stores the swipes. Maybe chaining and ziplists would drastically reduce the size of this code snippet.

Swipe direction ->
  case state.mode of
    All -> (state, Cmd.none)
    Current -> (state, Cmd.none)
    _ ->
      case lookupWolf state.currentWolf state.wolves of
        Just wolf ->
          let state =
            storeSwipe direction state
          in
          case state.currentMatch of
            Just matchIndex ->
              case List.head (List.drop matchIndex wolf.matches) of
                Just matchedWolf ->
                  case optionallyIncrement state.currentMatch of
                    Just n ->
                      case n >= List.length(wolf.matches) of
                      True ->
                        ( { state
                          | currentMatch = Just 0
                          , mode = All
                          }
                          , Navigation.newUrl (toPath All)
                        )
                      False ->
                        ( { state
                          | currentMatch = Just n
                          }
                          , Cmd.none
                        )
                    Nothing ->
                      ( { state
                        | currentMatch = Nothing
                        , mode = All
                        }
                        , Navigation.newUrl (toPath All)
                      )
                Nothing -> (
                  { state
                  | mode = All
                  , currentMatch = Nothing
                  }
                  , Navigation.newUrl (toPath All))
            Nothing ->(state, Cmd.none)
        Nothing -> ( -- Landing page was swiped away
          { state
          | mode = All
          }
          , Navigation.newUrl (toPath All))

In order to capture the swipe I had to fall back to JavaScript and the Hammer.js library. Elm uses ports and subscriptions to communicate with JavaScript. The code below is slightly bloated due to dynamically attaching event listeners tonodes (which represent the swipeable object) as they are added to the DOM. The virtual dom asynchronously updates the actual dom nodes, hence the setInterval to periodically test whether the node is ready or not.

var app = Elm.Main.fullscreen()

app.ports.match.subscribe(function(id) {
  var interval = setInterval(function() {
    var el = document.getElementById(id)
    if(el) { window.clearInterval(interval) }
    else { return }
    var hammertime = new Hammer(el, null)
    el.style.display = "block"
    var element_width = el.getBoundingClientRect().width / 2
    el.style.left = window.innerWidth / 2 - element_width + "px"

    // ignore pan event which comes after swipe due to javascript event ordering
    var swiped = false

    if(registered) { return }
    hammertime.on("pan", function (event) {
      if(swiped) { swiped = false; return }
      el.style.left = event.center.x - element_width + "px"
    })

    hammertime.on('swipe', function(ev) {
      swiped = true

      if(ev.direction == Hammer.DIRECTION_RIGHT) {
        app.ports.swipe.send("right")
      } else if(ev.direction == Hammer.DIRECTION_LEFT) {
        app.ports.swipe.send("left")
      } else {
        return
      }

      element_width = el.getBoundingClientRect().width / 2
      el.style.left = (window.innerWidth / 2) - element_width + "px"
    })

    registered = true
  })
}, 10)

Despite some less than ideal choices, Elm was easy to get started with. By forcing me to think about my data model up front I ended up with a cleaner application. I look forward to using Elm on my next project.


  1. DCPHR is pronunced “Decipher” and stands for DC Puzzle Hunt. We never came up with a useable acronym which used the “R”.
  2. Clojure projects: neighborhoods and nuc cluster
  3. ClojureScript project: corewar
  4. Clever readers will notice that “supervocalics” is also supervocalic.
  5. A ziplist stores a list as three components Before, Current, After; where Before and After are lists themselves.

Warning: The rest of this post will reveal many of the twists introduced in the first six months. Come back after you play through June.

Rose and I received Pandemic Legacy: Season 1 as a Christmas gift from one of our board gaming friends. We dove in and played the first six months in a single sitting; ended by a universal desire for sleep. The purpose of this post is to document our progress, our plan going forward, and tactics we have employed before continuing to the second half of the game.

Current Status

  • We are undefeated through June1.
  • Our medic and operations expert each have one scar.
  • All other characters are unharmed.
  • All red territories are now faded2.
  • All faded territories are isolated with road blocks from the rest of the world.
  • Panic Level 1: Chicago, Lima, Madrid, Algiers, Lagos, Khartoum, Moscow, Tehran, Delhi, Shanghai, Hong Kong, Osaka, and Sydney
  • Panic Level 2: St. Petersburg and Ho Chi Minh City
  • Panic Level 3: Kinsasha
  • No collapsing or fallen cities.
  • Three positive mutations on black3.
  • Permanent Research Stations: Atlanta, Karachi, and Taipei
  • Permanent Military Bases: Lagos and Taipei

Tactics

We have heavily invested in infrastructure, with five permanent buildings, to reduce travel time and contain the disease4. April-June we stuck with the same character load out: Medic with Veteran upgrade, Operations Expert, and Quarantine Specialist. We have no intention of changing this unless a new character or mechanic appears.

Summary

I’m feeling optimistic about our chances in the second half of the year. And this is a fantastic Christmas present.


  1. June was a nail-biter ending with five neighboring cities in Faded territories with three Faded figures each and seven outbreaks. The main cause of this tension was the player deck. Eight of the last ten cards were black cities which made it nearly impossible to cure black despite our positive mutations from previously eradicating black.
  2. In a stroke of luck, the red cubes mutated into COdA-403. Lucky because red has the least connections to other colors which limits the ability of COdA-403 to spread.
  3. One accidental eradication since we never drew a black city card in March.
  4. Heavily influenced by my real-life career building computing infrastructure.