Dice to See You A blog

Alchemists Code

A board game called Alchemists came out in the US in early 2015. One of the most interesting things about this game is that it depends on a companion application to store some secret information as four letter codes. The game doesn't give us an algorithm for interpreting these codes, so that leaves it up to us to reverse engineer one.

Update: the algorithm was changed in April 2015. See the new post for details.

Summary

Before I go into way too much detail on this, let me just summarize the key findings.

The first letter of the code tells you the alchemical for the Fern ingredient:

1st Letter Alchemical
A I Q Y R+
B J R Z G+
C K S B+
D L T R-
E M U G-
F N V B-
G O W ++
H P X --

The Feather ingredient has about a 35% chance of being either the all plus or all minus alchemical, a 40% chance of being a normal negative alchemical, and less than a 25% of being a normal positive alchemical.

Update: the above odds are not correct unless you manually enter your own random code. If you let the app generate the code for you, then the mappings for all of the ingredients should be equally probable.

There, done. That's the important stuff. You can stop reading unless you want to know why.

Background

The board game has eight ingredients and eight alchemicals. Your goal in the deduction part of the game is to figure out which alchemical corresponds to each ingredient. You can get information to help you do that by combining two ingredients into potions and testing them. See the official explanation for more details.

The mapping between ingredients and alchemicals is different for every game. So at the start of the game, you use the companion app to generate a random four letter code. The companion app can compute a mapping from this code, and you can run your tests through the app to get information that helps you deduce the mapping.

The explanation of the code algorithm in the manual is not particularly helpful:

If the battery on your smartphone dies and you want to use the gamemaster triangle to finish your game, just convert the four-letter game code to an ordered list of 8 ingredients using this simple algorithm: ... Actually, on second thought, maybe we should keep that information to ourselves.

But it does tell us that the algorithm is simple, which is a challenge if I've ever heard one. So let's see if we can figure it out!

Naming Conventions

The game instructions mostly use pictures to refer to the ingredients and alchemicals. I'm way too lazy to do that, so we'll need to agree on some names for them. And for the purposes of our algorithm, we'll also need to specify a default order for them.

Ingredients

ID Name
0 Fern
1 Claw
2 Mushroom
3 Flower
4 Root
5 Scorpion
6 Toad
7 Feather

The manual gives a name for every ingredient at some point except the flower. For our purposes, I'm using the shortest name for each one ("Claw" instead of "Bird Claw", "Root" instead of "Mandrake Root", "Feather" instead of "Raven Feather").

The order of the ingredients is based on the order they appear in the official companion app. The filenames of the images in the app are "i0.png" through "i7.png", so that's the order we have them in. Easy enough, right?

Alchemicals

ID Image Code
0 s_red_plus R+
1 s_green_plus G+
2 s_blue_plus B+
3 s_red_minus R-
4 s_green_minus G-
5 s_blue_minus B-
6 u_plus ++
7 u_minus --

Unlike the ingredients, I haven't found any officials names for the alchemicals. But the image filenames in the companion app come to our rescue again, giving us some hints about how we can name them:

  • Six alchemicals have exactly one large aspect, and are named after it: s_red_plus, s_green_minus, etc.
  • Two alchemicals have three large aspects of the same sign: u_plus and u_minus

The order of the alchemicals is from the companion app's result for code AAAA. That's the first possible code, so it's the best ordering to use for the purposes of our algorithm.

Solutions

We'll also need a concise way to describe the solution for a code. For that, I'm going to use an ordered list of the alchemicals.

Here's an example representation for the code DEMO:

DEMO = R- R+ G+ -- B- B+ G- ++

The first listed alchemical goes to our first ingredient (Fern), the second alchemical goes to our second ingredient (Claw), etc.

This means that for the code DEMO, the mapping is:

  • Fern = R-
  • Claw = R+
  • Mushroom = G+
  • Flower = --
  • Root = B-
  • Scorpion = B+
  • Toad = G-
  • Feather = ++

Algorithm

With all of that garbage out of the way, we can finally get to the good part. The basic steps in our version of the algorithm are:

  1. Convert the four letters of the code into numbers: A = 0, B = 1, C = 2, etc.
  2. Use a custom algorithm to translate those numbers into an eight digit factoradic number
  3. Convert the factoradic number to a permutation using the Lehmer Code encoding

That permutation will be of the numbers 0-7, which we can map back to our alchemicals by ID (from our alchemical naming conventions).

Here's some pseudocode for the algorithm:

function codeToPermutation(code) {
    var c1 = charToInt(code[0])
    var c2 = charToInt(code[1])
    var c3 = charToInt(code[2])
    var c4 = charToInt(code[3])

    var f1 = c1 % 8
    var f2 = c2 % 7
    var f3 = c3 % 6
    var f4 = c4 % 5
    var f5 = floor(c3 / 6) % 4
    var f6 = floor(c2 / 7) % 3
    var f7 = floor(c1 / 8) % 2
    var f8 = 0

    var factoradic = [f1, f2, f3, f4, f5, f6, f7, f8]
    return lehmerDecode(factoradic)
}

Note: % is the modulo operator and floor is the floor function

And I guess we can make this pseudocode into bad javascript if we add these functions:

function lehmerDecode(factoradic) {
    var remaining = [0, 1, 2, 3, 4, 5, 6, 7]
    var permutation = []
    for (var i in [0, 1, 2, 3, 4, 5, 6, 7]) {
        var factoradicElement = factoradic[i]
        permutation[i] = remaining[factoradicElement]
        remaining.splice(factoradicElement, 1)
    }
    return permutation
}

function charToInt(c) {
    var i = c.toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0)
    if (i < 0 || i >= 26) {
        i = 0
    }
    return i
}

function floor(i) {
    return Math.floor(i)
}

Here are the results from a few example codes:

  • DEMO
    • => (3 4 0 4 2 0 0 0)!
    • => (3 5 0 7 4 1 2 6)
    • => R- B- R+ -- G- G+ B+ ++
  • AAAA
    • => (0 0 0 0 0 0 0 0)!
    • => (0 1 2 3 4 5 6 7)
    • => R+ G+ B+ R- G- B- ++ --
  • ZZZZ
    • => (1 4 1 0 0 0 1 0)!
    • => (1 5 2 0 3 4 7 6)
    • => G+ B- B+ R+ R- G- -- ++
  • PUXE
    • => (7 6 5 4 3 2 1 0)!
    • => (7 6 5 4 3 2 1 0)
    • => -- ++ B- G- R- B+ G+ R+

Ramifications

So at first glace, the algorithm seems reasonable. It obscures the solution, and it's complicated enough that I can't run the whole thing in my head. But on closer inspection, there are a couple flaws.

The Fern

The algorithm works by building a factoradic number. That's great because when we convert that to a permutation, each digit in the permutation depends on the digits that came before it. So even though there is only one code letter that determines each digit in the factoradic number, when we convert that to the permutation we add in some dependencies on other letters. With one exception...

The first ingredient is the Fern, so the only code letter that affects its alchemical mapping is the first one. That means if you know the first letter of the code, you can calculate the alchemical for the Fern:

Letter Code
A I Q Y R+
B J R Z G+
C K S B+
D L T R-
E M U G-
F N V B-
G O W ++
H P X --

This is probably the most serious issue with the algorithm. That's a small enough table to memorize, or a simple enough calculation to perform in your head. So anyone could figure out what the alchemical mapping for the Fern is just by seeing the code (which is prominently displayed in the companion app).

Permutations

There are only 456,976 possible codes (264), so we can pretty easily generate the permutations for all of them: alchemists_codes.7z.

Here's a summary of the results:

Probability Chart

Fern Claw Mushroom Flower Root Scorpion Toad Feather
R+ 15.4% 13.0% 13.8% 13.3% 12.7% 14.4% 11.0% 6.3%
G+ 15.4% 13.0% 13.8% 12.2% 13.0% 13.4% 11.5% 7.7%
B+ 11.5% 13.6% 13.2% 12.3% 13.4% 13.5% 12.4% 10.1%
R- 11.5% 13.6% 12.0% 12.2% 12.8% 13.1% 13.1% 11.8%
G- 11.5% 13.6% 11.5% 12.2% 12.1% 12.5% 13.3% 13.3%
B- 11.5% 12.7% 11.7% 12.3% 11.8% 11.6% 13.4% 15.0%
++ 11.5% 10.2% 12.0% 12.7% 12.2% 10.8% 13.4% 17.2%
-- 11.5% 10.2% 12.0% 12.7% 12.2% 10.8% 12.0% 18.6%

What does this tell us? If you take a random code, then there is some bias in which alchemical will be mapped to an ingredient. For most ingredients, this is only a small bias. For example, the Flower and Root are actually pretty close to the ideal of 12.5% for each alchemical.

But the Feather has a weird distribution. It has low odds of being either R+ or G+, and it has high odds of being ++ or --. This probably has something to do with it being the last ingredient? It just gets whatever is left after all of the other ingredients have gotten their assignments.

This is only really a problem if you select a code at random. I don't know for sure how the companion app chooses the code for a new game, but I have to assume that it does randomly generate a code since that would be the easiest method with this algorithm.

The app could avoid any bias by randomly selecting the permutation first, and then computing a code from that. Each permutation will have more than one possible code, so it would probably be best if it then randomly selected from all possible codes for the permutation.

I doubt it does all of that now, though. So it's likely are the above odds are what you can expect to see if you use the companion app to start a new game.

Update: I've been informed that the official companion app actually does select a random permutation when starting a new game, and then generates a code from the permutation. So you should not see these biased probabilities if you generate a new code through the official app. But you would run into these biased probability problems if you manually entered a random code.


Alternative Algorithm

I initially ended this post after pointing out some problems in the algorithm. But I don't feel right about leaving it without looking into how an alternative algorithm could avoid these problems. So in the unlikely event that you want to read more, you can continue to part two.