File: make_graph.py

package info (click to toggle)
open-adventure 1.20-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,484 kB
  • sloc: ansic: 3,792; python: 892; sh: 295; makefile: 210
file content (221 lines) | stat: -rwxr-xr-x 6,586 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#!/usr/bin/env python3
# SPDX-FileCopyrightText: (C) Eric S. Raymond <esr@thyrsus.com>
# SPDX-License-Identifier: BSD-2-Clause
"""\
usage: make_graph.py [-a] [-d] [-m] [-s] [-v]

Make a DOT graph of Colossal Cave.

-a = emit graph of entire dungeon
-d = emit graph of maze all different
-f = emit graph of forest locations
-m = emit graph of maze all alike
-s = emit graph of non-forest surface locations
-v = include internal symbols in room labels
"""

# pylint: disable=consider-using-f-string,line-too-long,invalid-name,missing-function-docstring,multiple-imports,redefined-outer-name

import sys, getopt, yaml


def allalike(loc):
    "Select out loci related to the Maze All Alike"
    return location_lookup[loc]["conditions"].get("ALLALIKE")


def alldifferent(loc):
    "Select out loci related to the Maze All Alike"
    return location_lookup[loc]["conditions"].get("ALLDIFFERENT")


def surface(loc):
    "Select out surface locations"
    return location_lookup[loc]["conditions"].get("ABOVE")


def forest(loc):
    return location_lookup[loc]["conditions"].get("FOREST")


def abbreviate(d):
    m = {
        "NORTH": "N",
        "EAST": "E",
        "SOUTH": "S",
        "WEST": "W",
        "UPWAR": "U",
        "DOWN": "D",
    }
    return m.get(d, d)


def roomlabel(loc):
    "Generate a room label from the description, if possible"
    loc_descriptions = location_lookup[loc]["description"]
    description = ""
    if debug:
        description = loc[4:]
    longd = loc_descriptions["long"]
    short = loc_descriptions["maptag"] or loc_descriptions["short"]
    if short is None and longd is not None and len(longd) < 20:
        short = loc_descriptions["long"]
    if short is not None:
        if short.startswith("You're "):
            short = short[7:]
        if short.startswith("You are "):
            short = short[8:]
        if (
            short.startswith("in ")
            or short.startswith("at ")
            or short.startswith("on ")
        ):
            short = short[3:]
        if short.startswith("the "):
            short = short[4:]
        if short[:3] in {"n/s", "e/w"}:
            short = short[:3].upper() + short[3:]
        elif short[:2] in {"ne", "sw", "se", "nw"}:
            short = short[:2].upper() + short[2:]
        else:
            short = short[0].upper() + short[1:]
        if debug:
            description += "\\n"
        description += short
        if loc in startlocs:
            description += "\\n(" + ",".join(startlocs[loc]).lower() + ")"
    return description


# A forwarder is a location that you can't actually stop in - when you go there
# it ships some message (which is the point) then shifts you to a next location.
# A forwarder has a zero-length array of notion verbs in its travel section.
#
# Here is an example forwarder declaration:
#
# - LOC_GRUESOME:
#    description:
#      long: 'There is now one more gruesome aspect to the spectacular vista.'
#      short: !!null
#      maptag: !!null
#    conditions: {DEEP: true}
#    travel: [
#      {verbs: [], action: [goto, LOC_NOWHERE]},
#    ]


def is_forwarder(loc):
    "Is a location a forwarder?"
    travel = location_lookup[loc]["travel"]
    return len(travel) == 1 and len(travel[0]["verbs"]) == 0


def forward(loc):
    "Chase a location through forwarding links."
    while is_forwarder(loc):
        loc = location_lookup[loc]["travel"][0]["action"][1]
    return loc


def reveal(objname):
    "Should this object be revealed when mapping?"
    if "OBJ_" in objname:
        return False
    if objname == "VEND":
        return True
    obj = object_lookup[objname]
    return not obj.get("immovable")


if __name__ == "__main__":
    with open("adventure.yaml", "r", encoding="ascii", errors="surrogateescape") as f:
        db = yaml.safe_load(f)

    location_lookup = dict(db["locations"])
    object_lookup = dict(db["objects"])

    try:
        (options, arguments) = getopt.getopt(sys.argv[1:], "adfmsv")
    except getopt.GetoptError as e:
        print(e)
        sys.exit(1)

    subset = allalike
    debug = False
    for (switch, val) in options:
        if switch == "-a":
            # pylint: disable=unnecessary-lambda-assignment
            subset = lambda loc: True
        elif switch == "-d":
            subset = alldifferent
        elif switch == "-f":
            subset = forest
        elif switch == "-m":
            subset = allalike
        elif switch == "-s":
            subset = surface
        elif switch == "-v":
            debug = True
        else:
            sys.stderr.write(__doc__)
            raise SystemExit(1)

    startlocs = {}
    for obj in db["objects"]:
        objname = obj[0]
        location = obj[1].get("locations")
        if location != "LOC_NOWHERE" and reveal(objname):
            if location in startlocs:
                startlocs[location].append(objname)
            else:
                startlocs[location] = [objname]

    # Compute reachability, using forwards.
    # Dictionary key is (from, to) iff its a valid link,
    # value is corresponding motion verbs.
    links = {}
    nodes = []
    for (loc, attrs) in db["locations"]:
        nodes.append(loc)
        travel = attrs["travel"]
        if len(travel) > 0:
            for dest in travel:
                verbs = [abbreviate(x) for x in dest["verbs"]]
                if len(verbs) == 0:
                    continue
                action = dest["action"]
                if action[0] == "goto":
                    dest = forward(action[1])
                    if not (subset(loc) or subset(dest)):
                        continue
                    links[(loc, dest)] = verbs

    neighbors = set()
    for loc in nodes:
        for (f, t) in links:
            if f == "LOC_NOWHERE" or t == "LOC_NOWHERE":
                continue
            if (f == loc and subset(t)) or (t == loc and subset(f)):
                if loc not in neighbors:
                    neighbors.add(loc)

    print("digraph G {")

    for loc in nodes:
        if not is_forwarder(loc):
            node_label = roomlabel(loc)
            if subset(loc):
                print('    %s [shape=box,label="%s"]' % (loc[4:], node_label))
            elif loc in neighbors:
                print('    %s [label="%s"]' % (loc[4:], node_label))

    # Draw arcs
    for (f, t) in links:
        arc = "%s -> %s" % (f[4:], t[4:])
        label = ",".join(links[(f, t)]).lower()
        if len(label) > 0:
            arc += ' [label="%s"]' % label
        print("    " + arc)
    print("}")

# end