Implement breadth-first search to solve vault

This commit is contained in:
RunasSudo 2017-02-05 12:28:30 +10:30
parent 6c4b12435b
commit b4e9e1295c
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 159 additions and 4 deletions

View File

@ -47,7 +47,7 @@ At the foothills, the first area of the RPG:
Required opcodes: All except `halt`
### 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.
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:
@ -60,7 +60,7 @@ 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)
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:
Returning to the fork at the passage and venturing to the `darkness`, we now `continue` `west`ward several times, then `north`ward several times 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
@ -248,7 +248,7 @@ I've implemented this as a debug function to prepare the teleporter:
### Code 8 (Beach and vault)
Arriving at the beach, traverse `north`ward until you reach a fork. To the `east` lies a `journal` containing clues as to the upcoming puzzle.
With the ability to map the puzzle (possibly with the help of our knowledge of the map data format), and (unlike the adventurers, it seems) a grasp of basic arithmetic, this puzzle shouldn't be too difficult to solve. I couldn't do better than [paiv](https://github.com/paiv)'s [solution](https://paiv.github.io/blog/2016/04/24/synacor-challenge.html) ([code](https://github.com/paiv/synacor-challenge/tree/master/code/src/vault), [map](https://github.com/paiv/synacor-challenge/blob/master/notes/vault-locks.svg)), and it doesn't look like there's much parallelisation to do, so I'll leave you with those links.
With the ability to map the puzzle (possibly with the help of our knowledge of the map data format), and (unlike the adventurers, it seems) a grasp of basic arithmetic, this puzzle shouldn't be too difficult to solve. Armed with our map data, we can now do a [breadth-first search to find the shortest solution](https://github.com/RunasSudo/synacor.py/blob/master/tools/bfs.py).
The solution is:

155
tools/bfs.py Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
# synacor.py - An implementation of the Synacor Challenge
# Copyright © 2017 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 collections
import re
import struct
import sys
rooms = {}
class Door:
def __init__(self):
self.name = None
self.destination = None
class Room:
def __init__(self):
self.location = None
self.name = None
self.description = None
self.doors = []
self.callback = None
self.is_operator = False
self.mutate_value = None
# Read code into memory
SYN_MEM = [0] * 32768
with open('../dumps/init.raw', 'rb') as data:
i = 0
while True:
byteData = data.read(2)
if len(byteData) < 2:
break
SYN_MEM[i] = struct.unpack('<H', byteData)[0]
i += 1
def traverse_room(location):
if location in rooms:
return
# Read data from initial struct
ptr_name = SYN_MEM[location]
ptr_description = SYN_MEM[location + 1]
ptr_door_names = SYN_MEM[location + 2]
ptr_door_dests = SYN_MEM[location + 3]
ptr_callback = SYN_MEM[location + 4]
room = Room()
room.location = location
# Name
len_name = SYN_MEM[ptr_name]
room.name = bytes(SYN_MEM[ptr_name+1:ptr_name+1+len_name]).decode('ascii')
if room.name.startswith('Vault '):
rooms[location] = room
else:
return
# Description
len_description = SYN_MEM[ptr_description]
room.description = bytes(SYN_MEM[ptr_description+1:ptr_description+1+len_description]).decode('ascii')
# Callback
room.callback = ptr_callback
# Door names
door_name_ptrs = []
len_door_name_ptrs = SYN_MEM[ptr_door_names]
door_name_ptrs = SYN_MEM[ptr_door_names+1:ptr_door_names+1+len_door_name_ptrs]
# Door dests
door_dests = []
len_door_dest_ptrs = SYN_MEM[ptr_door_dests]
door_dest_ptrs = SYN_MEM[ptr_door_dests+1:ptr_door_dests+1+len_door_dest_ptrs]
assert len(door_name_ptrs) == len(door_dest_ptrs)
# Process doors
for i in range(len(door_name_ptrs)):
door = Door()
len_door_name = SYN_MEM[door_name_ptrs[i]]
door.name = bytes(SYN_MEM[door_name_ptrs[i]+1:door_name_ptrs[i]+1+len_door_name]).decode('ascii')
door.destination = door_dest_ptrs[i]
room.doors.append(door)
# Parse description
if room.name == 'Vault Antechamber':
room.mutate_value = lambda val: '22'
else:
match = re.search(r"mosaic depicting the number '(.*)'", room.description)
if match:
room_value = match.group(1)
room.mutate_value = lambda val: val + room_value + ')'
else:
match = re.search(r"mosaic depicting a '(.*)' symbol", room.description)
if match:
room.is_operator = True
room_operator = match.group(1)
room.mutate_value = lambda val: '(' + val + room_operator
else:
raise Exception('Unable to parse description: {}'.format(room.description))
for door in room.doors:
traverse_room(door.destination)
# Seed rooms
traverse_room(0x0a3f)
# Breadth-first search
queue = collections.deque()
initial_status = ([(None, 0x0a3f)], None) # current path, previous value
queue.append(initial_status)
while True:
current_status = queue.popleft()
current_room = current_status[0][-1][1]
current_value = rooms[current_room].mutate_value(current_status[1])
print(current_status)
print(current_value)
if current_room == 0x0a12: # Vault Door
if eval(current_value) == 30:
# We have reached the goal
sys.exit(0)
else:
# We must not enter the vault door unless we have the right number, else the orb will disappear
# Do not continue this line of searching
continue
for edge in rooms[current_room].doors:
if edge.destination in rooms:
new_status = (current_status[0] + [(edge.name, edge.destination)], current_value)
queue.append(new_status)

View File

@ -168,7 +168,7 @@ def traverse_item(location):
# Seed rooms
traverse_room(0x090d) # Foothills
traverse_room(0x09b8) # Synacor Headquarters
traverse_room(0x09cc) # Beachside
traverse_room(0x09c2) # Beachside
# Exhaustive check
for i in range(0x090d, 0x099e, 5):