Thursday, 15 December 2022

Listen to the sea

 

Rockstar is an esoteric programming language that creates code that looks like song lyrics.

I tried to use it to solve this code puzzle: https://adventofcode.com/2022/day/1.

This wasn't that hard, and after puzzling a bit, I ended up with this code that works.

my ship is everything
waves are everything
let my life be waves
let my dreams be waves
Listen to the sea

A pirate wants gold and freedom
If gold is greater than freedom
send gold

give freedom back

While the sea isn't mysterious or the mind isn't mysterious
If the sea is silent
let my life be a pirate taking my ship, and my life
let my ship be waves
Put the sea into the mind
Listen to the sea

If the sea isn't silent
let my love be the sea without waves 
let my ship be with my love
Put the sea into the mind
Listen to the sea


let the sea be my life
shout her

(Full explanation of all the lines follows further below in this blogpost)

You can try it online, and it returns the correct answer if you append two newlines to the bottom of the input. But this could be nicer.

According to the specifications, the expression:
the sea is mysterious

It should return true if "the sea" is null (meaning end of file) and false if "the sea" is an empty string. 

In addition, the expression:  
If the sea is silent

should return true if the sea is an empty string.

But in practice, due to a bug in the interpreter, both mysterious and silent act exactly the same and match both an empty string and a null value. This means we cannot distinguish between the two and need two newlines at the end to ensure the program can detect it.

If we write a program according to spec, we get these lines.

my ship is everything
waves are everything
let my life be waves
let my dreams be waves
Listen to the sea

A pirate wants gold and freedom
If gold is greater than freedom
send gold

give freedom back

While the sea isn't mysterious
If the sea is silent
let my life be a pirate taking my ship, and my life
let my ship be waves
Listen to the sea

If the sea isn't silent
let my love be the sea without waves 
let my ship be with my love
Listen to the sea


let the sea be my life
shout her


When I first made this edit, I accidentally also removed the line "let my ship be waves"  and I actually found that the song flows better without that line, so I decided the keep the "wrong" version. (If you wanna have a singable fixed version, you can move the last "Listen to the sea" one line down, and replace the second "listen to the sea" with "let my ship be waves".)

I also realigned the verses, so each verse has 4 lines. I also broke down long lines into two, and combined some shorter lines.

Both these changes completely break the program as a valid rockstar program, but make it possible to actually sing it.

So then I end up with verses that are close to the working program but not quite.

my ship is everything
waves are everything
let my life be waves; let my dreams be waves
Listen to the sea

A pirate wants gold and freedom
If gold is greater than freedom
send gold, give freedom back
While the sea isn't mysterious

If the sea is silent
let my life be a pirate 
taking my ship, and my life
Listen to the sea

If the sea isn't silent
let my love be the sea without waves 
let my ship be with my love
Listen to the sea

let the sea be my life
shout her


This can actually be sung, so I hired Liliia K  to sing it for me. The result can be seen below




The full song (before I applied a bunch of artistic licences) is equivalent to the following python code. 


myShip = 0
waves = 0
myLife = waves
myDreams = waves
theSea = input()

def aPirate(gold, freedom):
    if gold > freedom:
        return gold
    return freedom

while theSea is not None:
    if theSea == '':
        myLife = aPirate(
            myShip, myLife
        )
        myShip = 0
        theSea = input()
    if theSea != '':
        myLove = theSea - waves
        myShip += myLove
        theSea = input()

theSea = myLife
print(theSea)

There are only two caveats. 

  1. the input function in python blocks when there are no more lines to read, I modified it to return None instead. This matches the behaviour in rockstar
  2. Rockstar uses javascript internally, so a string minus an integer is an integer. In python, this gives an error.
You can add these lines above your script to mitigate both problems.

with open("input1.txt") as file:
    lines = list(reversed(file.read().split("\n")))


def input():
    if lines:
        l = lines.pop()
        try:
            return int(l)
        except ValueError:
            return l

Here follows a line-by-line explanation of all the code:

myShip = 0       | my ship is everything
waves = 0        | waves are everything
myLife = waves   | let my life be waves
myDreams = waves | let my dreams be waves

We initialise a bunch of variables. "everything" has ten letters, so the value of the poetic literal is 0. 
The variable "myDreams" is never used, but it was left over after a refactor, and the song flows better using the variable, so I decided to leave it.

theSea = input() | Listen to the sea

Store the current line from the input into the sea. Rockstar does not have "foreach" nor "do while" loops, so the only way to read the input line by line is to initialize "theSea" to the first line before the while loop. More about that later.

def aPirate(gold, freedom): | A pirate wants gold and freedom
    if gold > freedom:      | If gold is greater than freedom
        return gold         | Send gold
    return freedom          | Give freedom back

Define a function "aPirate" that takes in two arguments ("gold" and "freedom") and returns the larger one. Needs a newline after "Send gold" to close the if block and after "return freedom" to close the function call.

while theSea is not None:  | While the sea isn't mysterious

A while loop. while the input file has unread lines. 

if theSea == '': | If the sea is silent

Checks if we have read an empty line. In the context of the riddle, we start to count the calories of a new elf.

myLife = aPirate(myShip, myLife) | let my life be a pirate taking my ship, and my life

"myLife" is a variable that stores the elf with the highest amount of calories. "myShip" stores the calories of the current elf. This line of code stores the maximum of the calories of the current elf and the already found maximum into the maximum. In python terms, this would be equivalent to myLife = max(myShip, myLife). This changes the variable "myLife" from "the highest number of calories excluding the current elf" to "the highest number of calories including the current elf."

myShip = 0 | let my ship be waves

Resets the "myShip", meaning the "current calory counter" back to zero. This line is missing in the sung version.

theSea = input() | Listen to the sea

Reads the next line of the input

if theSea != '': | If the sea isn't silent

Checks if we have read a non-empty line. This is the alternative (else) branch of the previous if. Although in practice, it can be shortcutted cause the variable "theSea" might have changed since the previous if. This makes no difference to the actual execution of the code.

myLove = theSea - waves | let my love be the sea without waves 


Converts "theSea" to an integer. This is a hack. In javascript -and rockstar by extension- subtracting zero from a string converts it into an integer. So "myLove" ends up with a number of calories as an integer. 


myShip += myLove | let my ship be with my love

Adds the number of calories on the current line to the number of calories of the current elf. 

theSea = input() | Listen to the sea


Reads the next line of the input

theSea = myLife | let the sea be my life
print(theSea) | shout her

Just a poetic version of printing the current maximum stored in "myLife."




Monday, 25 April 2022

Lunar Lockout

 I made a small web applet in which you can play Lunar Lockout. The first 40 puzzles are the official puzzles by the makers of the game, and the following 80 are puzzles generated by an AI. 

The applet can be found here: https://elmosfire.gitlab.io/lockoutpuzzles/

In addition, all solutions to the official puzzles can be found here, nicely animated: https://www.youtube.com/watch?v=35rG8Cl0Hac

The Rules:

The characters can only move up, down, left and right.
You can only move a character on the board if its movement can be blocked by another character.
The red character will pass over the centre square unless a proper block has been made for normal movement there.

Movement:

In order to move the different characters, you must first click the character you would like to move.
This character will stay selected until you click another one or you beat the match.
Upon starting a new level the red character will become selected
Use the number pad or arrow keys to move your character.

Tuesday, 29 March 2022

Light switch puzzle

Thomas has a row of lamps, of odd length. Each lamp has two possible states: on or off. Next to each lamp, there is a switch. If a lamp is on, then pressing the switch next to it will change the state of the two neighbours of the lamp (only one if the lamp is at one of the ends of the row), but the lamp itself will remain on. The switches next to lamps that are off do nothing. At the start, only one lamp was on. At what positions in the row could this lamp have been, given that Thomas managed to turn all lamps on?

The source of this riddle can be found here

This problem can obviously be solved with an invariant. But strangely enough, finding this invariant by trial and error is harder than it seems. And even if you find it, this solution does not give you inside into how the problem works and how you could solve similar problems, so while they work, they are less than helpful.

For example, for the above riddle, I could state:

If we define

A = \left(\begin{matrix}1&0\\0&-1\end{matrix}\right)

and

B = \left(\begin{matrix}1&1\\0&1\end{matrix}\right)

replace each on-lamp with A, and each off-lamp with B, and multiply all matrixes, the end result forms an invariant, and as AnBAm=B is only valid if m=n, the lamp has to be in the centre.

This solution works but is not helpful.

So instead of trying to find a solution, we will describe a semi-structural way to find a solution.

Structural solution.

First, we notice that switches work differently when they are on the first or last lamp. This can be fixed by adding virtual lamps at the start and end of the line. These lamps have a state but not a switch.

We work with a system that is discrete, local, one-dimensional and finite, so it might be a good idea to replace each lamp with a group element, and then multiply the group elements in order.

Let us use the following notation for this group:

a: lamp turned off

x: lamp turned on

Then our invariant has to satisfy the following equations:

\begin{align*}xxx&=axa\\xxa&=axx\end{align*}

Note that every group satisfying this will be invariant, but not every group will be useful. For example, if we take the group

\inline (\mathbb{Z},+)

and define a=1 and x=1, our invariant will be the total number of lamps.

Although, even as "the number of lamps in our final state has to be the same as our initial state" is obvious, the fact that a statement we know is true follows from our math, is a good check to verify if are statements are correct.

The difficult part here is finding a group that is simple enough to actually work with it, but complex enough so it still contains the information we need. This might cause some trial and error, and is very case dependent. Luckily, for this riddle, we can make the statement that we are interested in the position of our lamps, so elements of our group that are central, so that can be moved freely without changing the invariant, are useless. Therefore it might be useful to consider a centerless group.

In addition, we will assume that are group is generated by a and x, there is no reason to complicate our group with other generators.

So first, we notice that x^2 commutes with both x (by definition) and a, (given). Therefore,

x^2 = 1

Using this, we can see that:

\begin{align*}xxx&=axa\\x&=axa\\xx&=axax\\1&=(ax)^2\end{align*}

Now we know that is group contains 2 elements that do not commute and square to the unit element. This means our group is the infinite dihedral group. But if you don’t know the group names from the top of your head, you can just go to Wikipedia: Symmetry groups and pick the one that has the correct properties.

To be fully rigorous, we have to check that x and b=ax do generate the group, but as a=bx, this is trivially true.

Given this group, The initial state can be represented as anxam and the final state as x. Putting both statements to be equal gives us am-n=1 which can only be true if m-n=0. Therefore the final state can only be reached if the centre lamp was on.

If you want to be fancier, you can prove that every state matching the invariant (and the invariant of the number of lamps) can be actually reached, but that is outside of the scope of this blogpost.

Friday, 28 January 2022

Wordle strategy

Wordle is a popular word game similar to mastermind.
If you do not know how to play, the linked website explains it better than I can.

The naive tactic is to guess a random word that you think might be the solution. This tactic might work, and as this game is very luck-based, an optimal tactic will not necessarily do better.

But in practice, this tactic does not bode very well. In essence, the program has the information. You, as a player, are trying to get the information. There is no point in confirming the information you already know to be true.

For example, imagine you picked as the first word “pound”, and the game returned that “o”, “u”, “n”, and “d” are correct. Furthermore, “p” is absent. There are multiple different words you might pick next, like “found”, “sound”, “round”, or “wound”. But if we are unlucky, we will need four guesses. A better guess would be “safer”. We can immediately distinguish between the different words depending on the letter that lights up.

To refine this tactic, we will need to quantify our notion of information. One can easily see that if only a single possible word could be the solution, information is very high. Similarly, if the solution can be any word, information is low. Therefore a good measure for the entropy would be the number of possible words.

This means that we will search the word to guess such that the worst-case entropy is minimal. This means that we partition the set of possible words into subsets for each word depending on the feedback we would get if this word were the solution. We take the size of the most significant subset, and we minimise that. We can keep doing this in a loop.

All the possible guesses can be precalculated, and the solution you can find here:

or in fullscreen view: here