92655e5e354d4060a9f5982dfd28be3014d27804
[open-adventure.git] / make_graph.py
1 #!/usr/bin/env python3
2 """\
3 usage: make-graph.py [-a] -d] [-m] [-s]
4
5 Make a DOT graph of Colossal Cave.
6
7 -a = emit graph of entire dungeon
8 -d = emit graoh of mazw all different
9 -f = emit graph of forest locations
10 -m = emit graph of maze all alike
11 -s = emit graph of non-forest surface locations
12 -v = include internal sy,no;s in room labels
13 """
14 # Copyright (c) 2017 by Eric S. Raymond
15 # SPDX-License-Identifier: BSD-2-clause
16
17 import sys, getopt, yaml
18
19 def allalike(loc):
20     "Select out loci related to the Maze All Alike"
21     return location_lookup[loc]["conditions"].get("ALLALIKE")
22
23 def alldifferent(loc):
24     "Select out loci related to the Maze All Alike"
25     return location_lookup[loc]["conditions"].get("ALLDIFFERENT")
26
27 def surface(loc):
28     "Select out surface locations"
29     return location_lookup[loc]["conditions"].get("ABOVE")
30
31 def forest(loc):
32     return location_lookup[loc]["conditions"].get("FOREST")
33
34 def abbreviate(d):
35     m = {"NORTH":"N", "EAST":"E", "SOUTH":"S", "WEST":"W", "UPWAR":"U", "DOWN":"D"}
36     return m.get(d, d)
37
38 def roomlabel(loc):
39     "Generate a room label from the description, if possible"
40     loc_descriptions = location_lookup[loc]['description']
41     description = ""
42     if debug:
43         description = loc[4:]
44     longd = loc_descriptions["long"]
45     short = loc_descriptions["maptag"] or loc_descriptions["short"]
46     if short is None and longd is not None and len(longd) < 20:
47         short = loc_descriptions["long"]
48     if short is not None:
49         if short.startswith("You're "):
50             short = short[7:]
51         if short.startswith("You are "):
52             short = short[8 :]
53         if short.startswith("in ") or short.startswith("at ") or short.startswith("on "):
54             short = short[3:]
55         if short.startswith("the "):
56             short = short[4:]
57         if short[:3] in {"n/s", "e/w"}:
58             short = short[:3].upper() + short[3:]
59         elif short[:2] in {"ne", "sw", "se", "nw"}:
60             short = short[:2].upper() + short[2:]
61         else:
62             short = short[0].upper() + short[1:]
63         if debug:
64             description += "\\n"
65         description += short
66         if loc in startlocs:
67             description += "\\n(" + ",".join(startlocs[loc]).lower() + ")"
68     return description
69
70 # A forwarder is a location that you can't actually stop in - when you go there
71 # it ships some message (which is the point) then shifts you to a nexr location.
72 # A forwarder has a zero-length array of notion verbs in its travel section.
73 #
74 # Here is an example forwarder declaration:
75 #
76 # - LOC_GRUESOME:
77 #    description:
78 #      long: 'There is now one more gruesome aspect to the spectacular vista.'
79 #      short: !!null
80 #      maptag: !!null
81 #    conditions: {DEEP: true}
82 #    travel: [
83 #      {verbs: [], action: [goto, LOC_NOWHERE]},
84 #    ]
85
86 def is_forwarder(loc):
87     "Is a location a forwarder?"
88     travel = location_lookup[loc]['travel']
89     return len(travel) == 1 and len(travel[0]['verbs']) == 0
90
91 def forward(loc):
92     "Chase a location through forwarding links."
93     while is_forwarder(loc):
94         loc = location_lookup[loc]["travel"][0]["action"][1]
95     return loc
96
97 def reveal(objname):
98     "Should this object be revealed when mappinmg?"
99     if "OBJ_" in objname:
100         return False
101     if objname == "VEND":
102         return True
103     obj = object_lookup[objname]
104     return not obj.get("immovable")
105
106 if __name__ == "__main__":
107     with open("adventure.yaml", "r") as f:
108         db = yaml.safe_load(f)
109
110     location_lookup = dict(db["locations"])
111     object_lookup = dict(db["objects"])
112
113     try:
114         (options, arguments) = getopt.getopt(sys.argv[1:], "adfmsv")
115     except getopt.GetoptError as e:
116         print(e)
117         sys.exit(1)
118
119     subset = allalike
120     debug = False
121     for (switch, val) in options:
122         if switch == '-a':
123             subset = lambda loc: True
124         elif switch == '-d':
125             subset = alldifferent
126         elif switch == '-f':
127             subset = forest
128         elif switch == '-m':
129             subset = allalike
130         elif switch == '-s':
131             subset = surface
132         elif switch == '-v':
133             debug = True
134         else:
135             sys.stderr.write(__doc__)
136             raise SystemExit(1)
137
138     startlocs = {}
139     for obj in db["objects"]:
140         objname = obj[0]
141         location = obj[1].get("locations")
142         if location != "LOC_NOWHERE" and reveal(objname):
143             if location in startlocs:
144                 startlocs[location].append(objname)
145             else:
146                 startlocs[location] = [objname]
147
148     # Compute reachability, using forwards.
149     # Dictionary ke6y is (from, to) iff its a valid link,
150     # value is correspoinding motion verbs.
151     links = {}
152     nodes = []
153     for (loc, attrs) in db["locations"]:
154         nodes.append(loc)
155         travel = attrs["travel"]
156         if len(travel) > 0:
157             for dest in travel:
158                 verbs = [abbreviate(x) for x in dest["verbs"]]
159                 if len(verbs) == 0:
160                     continue
161                 action = dest["action"]
162                 if action[0] == "goto":
163                     dest = forward(action[1])
164                     if not (subset(loc) or subset(dest)):
165                         continue
166                     links[(loc, dest)] = verbs
167
168     neighbors = set()
169     for loc in nodes:
170         for (f, t) in links:
171             if f == 'LOC_NOWHERE' or t == 'LOC_NOWHERE':
172                 continue
173             if (f == loc and subset(t)) or (t == loc and subset(f)):
174                 if loc not in neighbors:
175                     neighbors.add(loc)
176
177     print("digraph G {")
178
179     for loc in nodes:
180         if not is_forwarder(loc):
181             node_label = roomlabel(loc)
182             if subset(loc):
183                 print('    %s [shape=box,label="%s"]' % (loc[4:], node_label))
184             elif loc in neighbors:
185                 print('    %s [label="%s"]' % (loc[4:], node_label))
186
187     # Draw arcs
188     for (f, t) in links:
189         arc = "%s -> %s" % (f[4:], t[4:])
190         label=",".join(links[(f, t)]).lower()
191         if len(label) > 0:
192             arc += ' [label="%s"]' % label
193         print("    " + arc)
194     print("}")
195
196 # end