Add notes to code 7
This commit is contained in:
parent
f6ac40234c
commit
d8d2d51eab
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/challenge.bin
|
||||
/dumps
|
||||
/tools/ackermann.o
|
||||
|
157
notes.md
157
notes.md
@ -2,13 +2,15 @@
|
||||
|
||||
Note: Codes seem to be unique for every user.
|
||||
|
||||
## Code 1
|
||||
## The programming codes
|
||||
|
||||
### Code 1
|
||||
Found in `arch-spec`.
|
||||
|
||||
== hints ==
|
||||
- Here's a code for the challenge website: ............
|
||||
|
||||
## Code 2
|
||||
### Code 2
|
||||
Obtained on initial execution of code. (Or just be lazy like me and extract it from the binary.)
|
||||
|
||||
Welcome to the Synacor Challenge!
|
||||
@ -17,7 +19,7 @@ Obtained on initial execution of code. (Or just be lazy like me and extract it f
|
||||
|
||||
Required opcodes: `nop` and `out`
|
||||
|
||||
## Code 3
|
||||
### Code 3
|
||||
Obtained at the completion of the self-test.
|
||||
|
||||
Executing self-test...
|
||||
@ -27,7 +29,9 @@ Obtained at the completion of the self-test.
|
||||
|
||||
Required opcodes: All except `halt` and `in`
|
||||
|
||||
## Code 4
|
||||
## The RPG codes
|
||||
|
||||
### Code 4 (Tablet)
|
||||
At the foothills, the first area of the RPG:
|
||||
|
||||
== Foothills ==
|
||||
@ -41,7 +45,7 @@ At the foothills, the first area of the RPG:
|
||||
|
||||
Required opcodes: All except `halt`
|
||||
|
||||
## Code 5 (Twisty passages)
|
||||
### Code 5 (Twisty passages)
|
||||
After falling down the bridge, there is an `empty lantern` to the `east`, which should be `take`n. To the `west`, there is a passage, where one can take the `ladder` down or venture into the `darkness`. Attempting to venture into the `darkness` at this point will result in being eaten by Grues.
|
||||
|
||||
Taking the `ladder` down, then traversing `west`, `south`, `north`, a code is obtained:
|
||||
@ -54,12 +58,12 @@ Taking the `ladder` down, then traversing `west`, `south`, `north`, a code is ob
|
||||
|
||||
There is also a can, which can be `take`n and `use`d to fill the lantern, which can then be `use`d to become lit, and which will keep away Grues.
|
||||
|
||||
## Code 6 (Dark passage, ruins and teleporter)
|
||||
### Code 6 (Dark passage, ruins and teleporter)
|
||||
Returning to the fork at the passage and venturing to the `darkness`, we now `continue` `west`ward to the ruins. The `north` door is locked, but dotted elsewhere around the ruins are a `red coin` (2 dots), `corroded coin` (triangle = 3 sides), `shiny coin` (pentagon = 5 sides), `concave coin` (7 dots) and `blue coin` (9 dots) which should be `take`n and `look`ed at, and the equation:
|
||||
|
||||
_ + _ * _^2 + _^3 - _ = 399
|
||||
|
||||
### Mathematical!
|
||||
#### Mathematical!
|
||||
In other words, we seek a solution to the equation *a* + *bc*<sup>2</sup> + *d*<sup>3</sup> - e = 399, where {*a*, *b*, *c*, *d*, *e*} = {2, 3, 5, 7, 9}.
|
||||
|
||||
Synacor being, of course, a programming-orientated exercise, the usual response to this problem is to code up a quick program to loop through all 5! = 120 permutations of the coins to find which one satisfies the equation. Being a mathematician, however – could you tell from the italics? – I will solve this the [*proper* way](https://xkcd.com/435/): with a scientific calculator and some thinking (\*insert insufferably smug expression here\*).
|
||||
@ -90,3 +94,142 @@ Proceed to the `north` door and `use` the `teleporter` to obtain the code:
|
||||
............
|
||||
|
||||
After a few moments, you find yourself back on solid ground and a little disoriented.
|
||||
|
||||
## The true-believers-only codes
|
||||
At this point, you will almost certainly need to delve into the code of the challenge, if you haven't already. The code in `challenge.bin` past the self-test is encrypted, so disassembling and analysing the code is most easily done based off a memory dump from a running copy.
|
||||
|
||||
### The guts
|
||||
(Note to self: `pop` takes an operand, *duh*. No wonder everything looked funny…)
|
||||
|
||||
Note that at `1808` there is the following data:
|
||||
|
||||
1808 data 00b7
|
||||
1809 data "You find yourself standing at the base of an enormous mountain. ...
|
||||
|
||||
(Accompanied by some other familiar-sounding but somewhat garbled strings.) Searching the code for references to `1809` yields nothing, but searching for `1808` yields the following as the only result:
|
||||
|
||||
090d data 17fe 1808 6914 6917
|
||||
0911 halt # (i.e. data 0000)
|
||||
|
||||
The first address referred to in the `090d` line has value `0009`, which looks suspiciously like a length. Indeed, this is the exact length of the following string `Foothills`. Continuing in this manner,
|
||||
|
||||
17fe data 0009 "Foothills"
|
||||
1808 data 00b7 "You find yourself standing at the base of an enormous mountain. ...
|
||||
6914 data 0002 18c0 18c8
|
||||
6917 data 0002 0917 0912
|
||||
|
||||
Following the rabbit hole,
|
||||
|
||||
18c0 data 0007 "doorway"
|
||||
18c8 data 0005 "south"
|
||||
0917 data 1929 1933 691e 6921 0000
|
||||
0912 data 18ce 18d8 691a 691c 0000
|
||||
|
||||
And unsurprisingly, the `0912` line leads to the data for the screen reached when venturing `south` from the beginning.
|
||||
|
||||
Scrolling through the list of rooms beginning `090d`, we notice a peculiarly long line, `0949`:
|
||||
|
||||
0949 data
|
||||
1fe3 # -> string "Twisty passages"
|
||||
1ff3 # -> string "You are in a maze ...
|
||||
695b # -> data 0005 206f 2076 207c 2082 2087
|
||||
6961 # -> data 0005 093f 094e 0953 0958 095d
|
||||
|
||||
So far so good, but what's this??
|
||||
|
||||
0e9e # -> wmem 0e8e 0000; ret
|
||||
208c # -> string "Twisty passages"
|
||||
209c # -> string "You are in a twisty maze ...
|
||||
...
|
||||
|
||||
Aah, so it looks like each room is stored as a block of 5 words, each a pointer to a length of words: a string (the title), a string (the text), a list of pointers to strings (the exit names), a list of pointers to more rooms (the exits), and a memory location to `call` (or `0000`).
|
||||
|
||||
Further analysis suggests that this particular call relates to the step counter for the Grues in the maze.
|
||||
|
||||
We probably could have reached these same conclusions by analysing the suspicious-looking block of code following the room definitions, but assembly makes my head spin so ¯\_(ツ)_/¯
|
||||
|
||||
Now what about items? Looking at a more familiar item, the tablet:
|
||||
|
||||
0a6c data 468e 4695 090d 1270
|
||||
468e data 0006 "tablet"
|
||||
4695 data 0088 "The tablet seems appropriate for use as a writing surface but is unfortunately blank. Perhaps you should USE it as a writing surface..."
|
||||
090d ... # the foothills from earlier
|
||||
1270 ... # a subroutine that presumably prints code 4
|
||||
|
||||
### Code 7 (Synacor Headquarters and Teleporter 2: Electric Boogaloo)
|
||||
|
||||
Examining the data for the teleporter:
|
||||
|
||||
0a94 data 4a55 4a60 099f 1545
|
||||
4a55 data 000a "teleporter"
|
||||
4a60 data 0048 "This small device has a button on it and reads \"teleporter\" on the side."
|
||||
099f ... # the north room in the ruins
|
||||
1545 ... # aha!
|
||||
|
||||
Now, let's see what this `1545` does. C-style, because assembly makes my brain spin.
|
||||
|
||||
```c
|
||||
1545() {
|
||||
if (R8 == 0)
|
||||
return 15e5(); // Ground state teleport
|
||||
05b2(70ac, 05fb, 1807 + 585a); // Secure text print
|
||||
for(i = 0; i < 5; i++); // Speed up loop, because why not?
|
||||
|
||||
if (178b(0004, 0001) != 0006) // The check!
|
||||
return 15cb();
|
||||
|
||||
05b2(7156, 05fb, 1ed6 + 0992);
|
||||
|
||||
0731(R8, 650a, 7fff, 7239);
|
||||
|
||||
05b2(723d, 05fb, 7c1f + 0146);
|
||||
|
||||
mem[0aac] = 09c2;
|
||||
mem[0aad] = 0000;
|
||||
|
||||
mem[0a94 + 0002] = 7fff;
|
||||
|
||||
return 1652();
|
||||
}
|
||||
|
||||
178b(R1, R2) {
|
||||
if (R1 != 0) {
|
||||
return 1793(R1, R2);
|
||||
}
|
||||
return R2 + 0001;
|
||||
}
|
||||
|
||||
1793(R1, R2) {
|
||||
if (R2 != 0) {
|
||||
return 17a0(R1, R2);
|
||||
}
|
||||
R1 = R1 + 7fff;
|
||||
R2 = R8;
|
||||
R1 = 178b(R1, R2);
|
||||
return R1;
|
||||
}
|
||||
|
||||
17a0(R1, R2) {
|
||||
R2 = R2 + 7fff;
|
||||
R2 = 178b(R1, R2);
|
||||
R1 = R1 + 7fff;
|
||||
R1 = 178b(R1, R2);
|
||||
return R1;
|
||||
}
|
||||
```
|
||||
|
||||
Phew, so in other words, we seek an `R8` such that `178b(4, 1, R8)` equals 6. Let's see if we can't rewrite that function. Note that adding 0x7fff = 32767 to a number modulo 32768 is equivalent to subtracting 1. Thinking through the code, then,
|
||||
|
||||
A(R1, R2) = R2 + 1 , if R1 = 0
|
||||
A(R1 - 1, R8) , if R1 ≠ 0 and R2 = 0
|
||||
A(R1 - 1, A(R1, R2 - 1)) , if R1 ≠ 0 and R2 ≠ 0
|
||||
|
||||
Recognise anything? Well, neither did I the first time, and I'd [already seen the video](https://www.youtube.com/watch?v=i7sm9dzFtEI). It's the [Ackermann function](https://en.wikipedia.org/wiki/Ackermann_function)! With the slight twist that instead of the second line being `A(R1 - 1, 1)`, it's `A(R1 - 1, R8)`.
|
||||
|
||||
No mathematical wizardry here, just implementing this and run a brute-force on all possible values of `R8`. And as much as it pains me to admit this, this is a tool for the raw processing efficiency of C, which I am not very proficient in, so I based my solution on [this wonderful code](https://github.com/glguy/synacor-vm/blob/master/teleport.c) by [glguy](https://github.com/glguy). My only contribution is the parallelisation of the computation. (500% speed-up! Whoo!)
|
||||
|
||||
gcc ackermann.c -o ackermann -lpthread -O3 && ./ackermann
|
||||
|
||||
Running the algorithm, the correct value is revealed to be `0x6486`.
|
||||
|
||||
### Code 8 (Beach and vault)
|
||||
|
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import struct # for bytes<-->word handling
|
||||
import sys # for stdin
|
||||
import sys # for args, stdin
|
||||
|
||||
SYN_PTR = 0
|
||||
SYN_MEM = [0] * 32768
|
||||
|
74
tools/ackermann.c
Normal file
74
tools/ackermann.c
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
synacor.py - An implementation of the Synacor Challenge
|
||||
Copyright © 2016 RunasSudo
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define NUM_THREADS 8
|
||||
|
||||
static uint16_t A(uint16_t R1, uint16_t R2, uint16_t R8, uint16_t cache[5][0x8000]) {
|
||||
if (cache[R1][R2] != 0xffff) {
|
||||
return cache[R1][R2];
|
||||
}
|
||||
|
||||
if (R1 == 0)
|
||||
return (R2 + 1) & 0x7fff;
|
||||
if (R2 == 0)
|
||||
return A(R1 - 1, R8, R8, cache);
|
||||
|
||||
return A(R1 - 1, cache[R1][R2 - 1] = A(R1, R2 - 1, R8, cache), R8, cache);
|
||||
}
|
||||
|
||||
static void* bruteThread(void* rmdrVoid) {
|
||||
uint16_t rmdr;
|
||||
uint16_t cache[5][0x8000];
|
||||
uint16_t R8;
|
||||
|
||||
rmdr = *((int *) rmdrVoid);
|
||||
|
||||
for (R8 = rmdr; R8 <= 0x7fff; R8 += NUM_THREADS) {
|
||||
memset(cache, 0xff, sizeof(cache)); /* Clear the cache */
|
||||
|
||||
if (!(R8 & 0xff))
|
||||
printf("%04x...\n", R8);
|
||||
|
||||
if (A(4, 1, R8, cache) == 6)
|
||||
printf("Found a solution! %04x...\n", R8);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
pthread_t threads[NUM_THREADS];
|
||||
int thread_args[NUM_THREADS];
|
||||
int i;
|
||||
|
||||
for (i = 0; i < NUM_THREADS; i++) {
|
||||
printf("Spawning thread %d...\n", i);
|
||||
thread_args[i] = i; /* rmdr */
|
||||
pthread_create(&threads[i], NULL, bruteThread, (void*) &thread_args[i]);
|
||||
}
|
||||
|
||||
/* wait */
|
||||
for (i = 0; i < NUM_THREADS; i++) {
|
||||
pthread_join(threads[i], NULL);
|
||||
}
|
||||
|
||||
printf("Search complete\n");
|
||||
}
|
154
tools/disasm.py
Executable file
154
tools/disasm.py
Executable file
@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
# synacor.py - An implementation of the Synacor Challenge
|
||||
# Copyright © 2016 RunasSudo
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import struct # for bytes<-->word handling
|
||||
import sys # for args
|
||||
|
||||
class OpLiteral:
|
||||
def __init__(self, value):
|
||||
self.value = value;
|
||||
def get(self):
|
||||
return '{:04x}'.format(self.value);
|
||||
def set(self):
|
||||
return '{:04x}'.format(self.value);
|
||||
|
||||
class OpRegister:
|
||||
def __init__(self, register):
|
||||
self.register = register;
|
||||
def get(self):
|
||||
return 'R{}'.format(self.register + 1);
|
||||
def set(self):
|
||||
return 'R{}'.format(self.register + 1);
|
||||
|
||||
def readWord():
|
||||
byteData = data.read(2)
|
||||
if len(byteData) < 2:
|
||||
return None
|
||||
return struct.unpack('<H', byteData)[0]
|
||||
|
||||
def readOp():
|
||||
word = readWord()
|
||||
if 0 <= word <= 32767:
|
||||
return OpLiteral(word)
|
||||
if 32768 <= word <= 32775:
|
||||
return OpRegister(word - 32768)
|
||||
raise Exception('Invalid word {} at {}'.format(word, SYN_PTR))
|
||||
|
||||
def escapeChar(char):
|
||||
return char.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
|
||||
|
||||
MODE_OUT = False
|
||||
MODE_DAT = False #False, 1 (data), 2 (text)
|
||||
|
||||
with open(sys.argv[1], 'rb') as data:
|
||||
while True:
|
||||
pos = int(data.tell() / 2)
|
||||
|
||||
word = readWord()
|
||||
if word == None:
|
||||
break
|
||||
|
||||
if MODE_OUT and word != 19:
|
||||
print('"')
|
||||
MODE_OUT = False
|
||||
if MODE_DAT and 0 <= word <= 21:
|
||||
if MODE_DAT == 1:
|
||||
print()
|
||||
if MODE_DAT == 2:
|
||||
print('"')
|
||||
MODE_DAT = False
|
||||
|
||||
if word == 21: #NOP
|
||||
print('{:04x} nop'.format(pos))
|
||||
elif word == 0: #HALT
|
||||
print('{:04x} halt'.format(pos))
|
||||
elif word == 1: #SET
|
||||
print('{:04x} set {} {}'.format(pos, readOp().set(), readOp().get()))
|
||||
elif word == 2: #PUSH
|
||||
print('{:04x} push {}'.format(pos, readOp().get()))
|
||||
elif word == 3: #POP
|
||||
print('{:04x} pop {}'.format(pos, readOp().set()))
|
||||
elif word == 4: #EQ
|
||||
print('{:04x} eq {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
|
||||
elif word == 5: #GT
|
||||
print('{:04x} gt {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
|
||||
elif word == 6: #JMP
|
||||
print('{:04x} jmp {}'.format(pos, readOp().get()))
|
||||
elif word == 7: #JT (jump if not zero)
|
||||
print('{:04x} jt {} {}'.format(pos, readOp().get(), readOp().get()))
|
||||
elif word == 8: #JF (jump if zero)
|
||||
print('{:04x} jf {} {}'.format(pos, readOp().get(), readOp().get()))
|
||||
elif word == 9: #ADD
|
||||
print('{:04x} add {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
|
||||
elif word == 10: #MULT
|
||||
print('{:04x} mult {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
|
||||
elif word == 11: #MOD
|
||||
print('{:04x} mod {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
|
||||
elif word == 12: #AND
|
||||
print('{:04x} and {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
|
||||
elif word == 13: #OR
|
||||
print('{:04x} or {} {} {}'.format(pos, readOp().set(), readOp().get(), readOp().get()))
|
||||
elif word == 14: #NOT
|
||||
print('{:04x} not {} {}'.format(pos, readOp().set(), readOp().get()))
|
||||
elif word == 15: #RMEM
|
||||
print('{:04x} rmem {} {}'.format(pos, readOp().set(), readOp().get()))
|
||||
elif word == 16: #WMEM
|
||||
print('{:04x} wmem {} {}'.format(pos, readOp().set(), readOp().get()))
|
||||
elif word == 17: #CALL
|
||||
print('{:04x} call {}'.format(pos, readOp().get()))
|
||||
elif word == 18: #RET
|
||||
print('{:04x} ret'.format(pos))
|
||||
elif word == 19: #OUT
|
||||
op = readOp()
|
||||
|
||||
if isinstance(op, OpLiteral):
|
||||
if MODE_OUT:
|
||||
print(escapeChar(chr(op.value)), end='')
|
||||
else:
|
||||
print('{:04x} out "{}'.format(pos, escapeChar(chr(op.value))), end='')
|
||||
MODE_OUT = True
|
||||
if op.value == 0x0a:
|
||||
print('"')
|
||||
MODE_OUT = False # break on newlines
|
||||
else:
|
||||
if MODE_OUT:
|
||||
print('"')
|
||||
MODE_OUT = False
|
||||
print('{:04x} out {}'.format(pos, op.get(), end=''))
|
||||
elif word == 20: #IN
|
||||
print('{:04x} in {}'.format(pos, readOp().set()))
|
||||
else:
|
||||
if 32 <= word <= 126:
|
||||
# looks like letters - unfortunately "\n" looks like MULT
|
||||
if MODE_DAT == 2:
|
||||
print(escapeChar(chr(word)), end='')
|
||||
else:
|
||||
if MODE_DAT == 1:
|
||||
print()
|
||||
print('{:04x} data "{}'.format(pos, escapeChar(chr(word))), end='')
|
||||
MODE_DAT = 2
|
||||
if word == 0x0a:
|
||||
print('"')
|
||||
MODE_DAT = False # break on newlines
|
||||
else:
|
||||
if MODE_DAT == 1:
|
||||
print(' {:04x}'.format(word), end='')
|
||||
else:
|
||||
if MODE_DAT == 2:
|
||||
print('"')
|
||||
print('{:04x} data {:04x}'.format(pos, word), end='')
|
||||
MODE_DAT = 1
|
Reference in New Issue
Block a user