Add tool for graphing rooms and items

This commit is contained in:
RunasSudo 2017-02-04 23:01:28 +10:30
parent ecd9ae56dd
commit 6c4b12435b
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
3 changed files with 201 additions and 1 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
/dumps
/finish.htm
/tools/ackermann.o
/tools/venv
/tools/*.pdf

View File

@ -136,7 +136,7 @@ So far so good, but what's this??
Aah, so it looks like each room is stored as a block of 5 words, the first four pointers to lengths of words: a string (the title), a string (the text), a list of pointers to strings (the exit names) and a list of pointers to more rooms (the exits), followed by 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.
Further analysis suggests that this particular call relates to keeping track of which rooms in the maze have been visited, which is used to determine whether the code is shown when arriving at the designated room 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 ¯\\\_(ツ)\_/¯

198
tools/graph.py Executable file
View File

@ -0,0 +1,198 @@
#!/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 struct
import textwrap
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
def __str__(self):
return """<
{:04x}: {}
<br/>{}
{}
>""".format(
self.location,
self.name,
'<br/>'.join(textwrap.wrap(textwrap.shorten(self.description, 300), 50)),
'<br/><font point-size="10">Callback: {:04x}</font>'.format(self.callback) if self.callback != 0 else '')
items = {}
class Item:
def __init__(self):
self.location = None
self.name = None
self.description = None
self.room = None
self.callback = None
def __str__(self):
return """<
{:04x}: {}
<br/>{}
{}
>""".format(
self.location,
self.name,
'<br/>'.join(textwrap.wrap(textwrap.shorten(self.description, 300), 50)),
'<br/><font point-size="10">Callback: {:04x}</font>'.format(self.callback) if self.callback != 0 else '')
# 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
rooms[location] = room
# Name
len_name = SYN_MEM[ptr_name]
room.name = bytes(SYN_MEM[ptr_name+1:ptr_name+1+len_name]).decode('ascii')
# Description
if ptr_description == 0x6952:
# o_O
ptr_description = 0x1f4f
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)
for door in room.doors:
traverse_room(door.destination)
def traverse_item(location):
if location in items:
return
# Read data from initial struct
ptr_name = SYN_MEM[location]
ptr_description = SYN_MEM[location + 1]
ptr_room = SYN_MEM[location + 2]
ptr_callback = SYN_MEM[location + 3]
item = Item()
item.location = location
items[location] = item
# Name
len_name = SYN_MEM[ptr_name]
item.name = bytes(SYN_MEM[ptr_name+1:ptr_name+1+len_name]).decode('ascii')
# Description
len_description = SYN_MEM[ptr_description]
item.description = bytes(SYN_MEM[ptr_description+1:ptr_description+1+len_description]).decode('ascii')
# Callback
item.callback = ptr_callback
# Room
if ptr_room != 0x7fff:
item.room = ptr_room
if ptr_room not in rooms:
traverse_room(ptr_room)
# Seed rooms
traverse_room(0x090d) # Foothills
traverse_room(0x09b8) # Synacor Headquarters
traverse_room(0x09cc) # Beachside
# Exhaustive check
for i in range(0x090d, 0x099e, 5):
if i not in rooms:
traverse_room(i)
for i in range(0x099f, 0x0a6c, 5):
if i not in rooms:
traverse_room(i)
for i in range(0x0a6c, 0x0aac, 4):
if i not in items:
traverse_item(i)
import graphviz
dot = graphviz.Digraph()
for code, room in rooms.items():
dot.node('{:04x}'.format(code), str(room))
for door in room.doors:
dot.edge('{:04x}'.format(code), '{:04x}'.format(door.destination), door.name)
for code, item in items.items():
dot.node('{:04x}'.format(code), str(item), style='dashed')
if item.room is not None:
dot.edge('{:04x}'.format(item.room), '{:04x}'.format(code), style='dashed')
print(dot.source)