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