File: data.py

package info (click to toggle)
python-adventure 1.6-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, trixie
  • size: 784 kB
  • sloc: python: 2,001; sh: 35; makefile: 19
file content (401 lines) | stat: -rw-r--r-- 14,770 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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# -*- coding: utf-8 -*-

"""Parse the original PDP ``advent.dat`` file.

Copyright 2010-2015 Brandon Rhodes.  Licensed as free software under the
Apache License, Version 2.0 as detailed in the accompanying README.txt.

"""
from operator import attrgetter
from .model import Hint, Message, Move, Object, Room, Word

# The Adventure data file knows only the first five characters of each
# word in the game, so we have to know the full verion of each word.

long_words = { w[:5]: w for w in """upstream downstream forest
forward continue onward return retreat valley staircase outside building stream
cobble inward inside surface nowhere passage tunnel canyon awkward
upward ascend downward descend outdoors barren across debris broken
examine describe slabroom depression entrance secret bedquilt plover
oriental cavern reservoir office headlamp lantern pillow velvet fissure tablet
oyster magazine spelunker dwarves knives rations bottle mirror beanstalk
stalactite shadow figure drawings pirate dragon message volcano geyser
machine vending batteries carpet nuggets diamonds silver jewelry treasure
trident shards pottery emerald platinum pyramid pearl persian spices capture
release discard mumble unlock nothing extinguish placate travel proceed
continue explore follow attack strike devour inventory detonate ignite
blowup peruse shatter disturb suspend sesame opensesame abracadabra
shazam excavate information""".split() }

class Data(object):
    def __init__(self):
        self.rooms = {}
        self.vocabulary = {}
        self.objects = {}
        self.messages = {}
        self.class_messages = []
        self.hints = {}
        self.magic_messages = {}

    def referent(self, word):
        if word.kind == 'noun':
            return self.objects[word.n % 1000]

# Helper functions.

def make_object(dictionary, klass, n):
    if n not in dictionary:
        dictionary[n] = obj = klass()
        obj.n = n
    return dictionary[n]

def expand_tabs(segments):
    it = iter(segments)
    line = next(it)
    for segment in it:
        spaces = 8 - len(line) % 8
        line += ' ' * spaces + segment
    return line

def accumulate_message(dictionary, n, line):
    dictionary[n] = dictionary.get(n, '') + line + '\n'

# Knowledge of what each section contains.

def section1(data, n, *etc):
    """Handle record from “Section 1: long form descriptions”.

    Section 1: long form descriptions. Each line contains a location
    number, a TAB, and a line of text. The set of (necessarily adjacent)
    lines whose numbers are X form the long description of location X.

    """
    room = make_object(data.rooms, Room, n)
    if not etc[0].startswith('>$<'):
        room.long_description += expand_tabs(etc) + '\n'

def section2(data, n, line):
    """Handle record from “Section 2: short form descriptions”.

    Section 2: short form descriptions. Same format as long form.  Not
    all places have short descriptions.

    """
    make_object(data.rooms, Room, n).short_description += line + '\n'

def section3(data, x, y, *verbs):
    """Handle record from “Section 3: travel table”.

    Section 3: travel table. Each line contains a location number (X), a
    second location number (Y), and a list of motion numbers (see
    section 4).

    Each motion represents a verb which will go to Y if currently at
    X. Y, in turn, is interpreted as follows. Let M=Y/1000, N=Y MOD
    1000.

        If N<=300: it is the location to go to.

        If 300<N<=500: N-300 is used in a computed GOTO to a
        section of special code.

        If N>500: message N-500 from section 6 is printed, and he
        stays wherever he is.

    Meanwhile, M specifies the conditions on the motion.

        If M=0: it's unconditional.
        If 0<M<100: it is done with M% probability.
        If M=100: unconditional, but forbidden to dwarves.
        If 100<M<=200: he must be carrying object M-100.
        If 200<M<=300: must be carrying or in same room as M-200.
        If 300<M<=400: PROP(M MOD 100) must *not* be 0.
        If 400<M<=500: PROP(M MOD 100) must *not* be 1.
        If 500<M<=600: PROP(M MOD 100) must *not* be 2, etc.

    If the condition (if any) is not met, then the next *different*
    "destination" value is used (unless it fails to meet *its*
    conditions, in which case the next is found, etc.).  Typically, the
    next dest will be for one of the same verbs, so that its only use is
    as the alternate destination for those verbs. For instance:

        15	110022	29	31	34	35	23	43
        15	14	29

    This says that, from LOC 15, any of the verbs 29, 31, etc., will
    take him to 22 if he's carrying object 10, and otherwise will go to
    14.

        11	303008	49
        11	9	50

    This says that, from 11, 49 takes him to 8 unless PROP(3)=0, in
    which case he goes to 9. Verb 50 takes him to 9 regardless of
    PROP(3).

    """
    last_travel = data._last_travel
    if last_travel[0] == x and last_travel[1][0] == verbs[0]:
        verbs = last_travel[1]  # same first verb implies use whole list
    else:
        data._last_travel = [x, verbs]

    m, n = divmod(y, 1000)
    mh, mm = divmod(m, 100)

    if m == 0:
        condition = (None,)
    elif 0 < m < 100:
        condition = ('%', m)
    elif m == 100:
        condition = ('not_dwarf',)
    elif 100 < m <= 200:
        condition = ('carrying', mm)
    elif 200 < m <= 300:
        condition = ('carrying_or_in_room_with', mm)
    elif 300 < m:
        condition = ('prop!=', mm, mh - 3)

    if n <= 300:
        action = make_object(data.rooms, Room, n)
    elif 300 < n <= 500:
        action = n  # special computed goto
    else:
        action = make_object(data.messages, Message, n - 500)

    move = Move()
    if len(verbs) == 1 and verbs[0] == 1:
        move.is_forced = True
    else:
        move.verbs = [ make_object(data.vocabulary, Word, verb_n)
                       for verb_n in verbs if verb_n < 100 ] # skip bad "109"
    move.condition = condition
    move.action = action
    data.rooms[x].travel_table.append(move)

def section4(data, n, text, *etc):
    """Handle record from “Section 4: vocabulary”.

    Section 4: vocabulary. Each line contains a number (N), a TAB, and a
    five-letter word. Call M=N/1000. If M=0, then the word is a motion
    verb for use in travelling (see section 3). Else, if M=1, the word
    is an object. Else, if M=2, the word is an action verb (such as
    "CARRY" or "ATTACK"). Else, if M=3, the word is a special case verb
    (such as "DIG") and N MOD 1000 is an index into section 6. Objects
    from 50 to (currently, anyway) 79 are considered treasures (for
    pirate, closeout).

    """
    text = text.lower()
    text = long_words.get(text, text)
    word = make_object(data.vocabulary, Word, n)
    if word.text is None:  # this is the first word with index "n"
        word.text = text
    else:  # there is already a word sitting at "n", so create a synonym
        original = word
        word = Word()
        word.n = n
        word.text = text
        original.add_synonym(word)
    word.kind = ['travel', 'noun', 'verb', 'snappy_comeback'][n // 1000]
    if word.kind == 'noun':
        n %= 1000
        obj = make_object(data.objects, Object, n)
        obj.names.append(text)
        obj.is_treasure = (n >= 50)
        data.objects[text] = obj
    if text not in data.vocabulary:  # since duplicate names exist
        data.vocabulary[text] = word

def section5(data, n, *etc):
    """Handle record from “Section 5: object descriptions”.

    Section 5: object descriptions. Each line contains a number (N), a
    TAB, and a message. If N is from 1 to 100, the message is the
    "inventory" message for object N. Otherwise, N should be 000, 100,
    200, etc., and the message should be the description of the
    preceding object when its PROP value is N/100. The N/100 is used
    only to distinguish multiple messages from multi-line messages; the
    PROP info actually requires all messages for an object to be present
    and consecutive.  Properties which produce no message should be
    given the message ">$<".

    """
    if 1 <= n <= 99:
        data._object = make_object(data.objects, Object, n)
        data._object.inventory_message = expand_tabs(etc)
    else:
        n //= 100
        messages = data._object.messages
        if etc[0].startswith('>$<'):
            more = ''
        else:
            more = expand_tabs(etc) + '\n'
        messages[n] = messages.get(n, '') + more

def section6(data, n, *etc):
    """Handle record from “Section 6: arbitrary messages”.

    Section 6: arbitrary messages. Same format as sections 1, 2, and 5,
    except the numbers bear no relation to anything (except for special
    verbs in section 4).

    """
    message = make_object(data.messages, Message, n)
    message.text += expand_tabs(etc) + '\n'

def section7(data, n, room_n, fixed=None):
    """Handle record from “Section 7: object locations”.

    Section 7: object locations. Each line contains an object number and
    its initial location (zero (or omitted) if none).  If the object is
    immovable, the location is followed by a "-1". If it has two
    locations (e.g. the grate) the first location is followed with the
    second, and the object is assumed to be immovable.

    """
    obj = make_object(data.objects, Object, n)
    if room_n:
        room = make_object(data.rooms, Room, room_n)
        obj.drop(room)
    if fixed is not None:
        if fixed == -1:
            obj.is_fixed = True
        else:
            room2 = make_object(data.rooms, Room, fixed)
            obj.rooms.append(room2)  # exists two places, like grate
    obj.starting_rooms = list(obj.rooms)  # remember where things started

def section8(data, word_n, message_n):
    """Handle record from “Section 8: action defaults”.

    Section 8: action defaults. Each line contains an "action-verb"
    number and the index (in section 6) of the default message for the
    verb.

    """
    if not message_n:
        return
    word = make_object(data.vocabulary, Word, word_n + 2000)
    message = make_object(data.messages, Message, message_n)
    for word2 in word.synonyms:
        word2.default_message = message

def section9(data, bit, *nlist):
    """Handle record from “Section 9: liquid assets, etc.”.

    Section 9: liquid assets, etc. Each line contains a number (N) and
    up to 20 location numbers. Bit N (where 0 is the units bit) is set
    in COND(LOC) for each LOC given. The COND bits currently assigned
    are:

        0: light
        1: if bit 2 is on: on for oil, off for water
        2: liquid asset, see bit 1
        3: pirate doesn't go here unless following player

    Other bits are used to indicate areas of interest to "hint"
    routines:

        4: trying to get into cave
        5: trying to catch bird
        6: trying to deal with snake
        7: lost in maze
        8: pondering dark room
        9: at Witt's End

    COND(LOC) is set to 2, overriding all other bits, if LOC has forced
    motion.

    """
    for n in nlist:
        room = make_object(data.rooms, Room, n)
        if bit == 0:
            room.is_light = True
        elif bit == 1:
            room.liquid = make_object(data.objects, Object, 22) #oil
        elif bit == 2:
            room.liquid = make_object(data.objects, Object, 21) #water
        elif bit == 3:
            room.is_forbidden_to_pirate = True
        else:
            hint = make_object(data.hints, Hint, bit)
            hint.rooms.append(room)

def section10(data, score, line, *etc):
    """Handle record from “Section 10: class messages”.

    Section 10: class messages. Each line contains a number (N), a TAB,
    and a message describing a classification of player. The scoring
    section selects the appropriate message, where each message is
    considered to apply to players whose scores are higher than the
    previous N but not higher than this N. Note that these scores
    probably change with every modification (and particularly expansion)
    of the program.

    """
    data.class_messages.append((score, line))

def section11(data, n, turns_needed, penalty, question_n, message_n):
    """Handle record from “Section 11: hints”.

    Section 11: hints. Each line contains a hint number (corresponding
    to a COND bit, see section 9), the number of turns he must be at the
    right LOC(s) before triggering the hint, the points deducted for
    taking the hint, the message number (section 6) of the question, and
    the message number of the hint. These values are stashed in the
    "HINTS" array.

    HNTMAX is set to the max hint number (<= HNTSIZ). Numbers 1-3 are
    unusable since COND bits are otherwise assigned, so 2 is used to
    remember if he's read the clue in the repository, and 3 is used to
    remember whether he asked for instructions (gets more turns, but
    loses points).

    """
    hint = make_object(data.hints, Hint, n)
    hint.turns_needed = turns_needed
    hint.penalty = penalty
    hint.question = make_object(data.messages, Message, question_n)
    hint.message = make_object(data.messages, Message, message_n)

def section12(data, n, line):
    """Handle record from “Section 12: magic messages”.

    Section 12: magic messages. Identical to section 6 except put in a
    separate section for easier reference. Magic messages are used by
    the startup, maintenance mode, and related routines.

    """
    accumulate_message(data.magic_messages, n, line)

# Process every section of the file in turn.

def parse(data, datafile):
    """Read the Adventure data file and return a ``Data`` object."""
    data._last_travel = [0, [0]]  # x and verbs used by section 3

    while True:
        section_number = int(datafile.readline())
        if not section_number:  # no further sections
            break
        store = globals().get('section%d' % section_number)
        while True:
            fields = [ (int(field) if field.lstrip('-').isdigit() else field)
                       for field in datafile.readline().strip().split('\t') ]
            if fields[0] == -1:  # end-of-section marker
                break
            store(data, *fields)

    del data._last_travel  # state used by section 3
    del data._object       # state used by section 5

    data.object_list = sorted(set(data.objects.values()), key=attrgetter('n'))
    #data.room_list = sorted(set(data.rooms.values()), key=attrgetter('n'))
    for obj in data.object_list:
        name = obj.names[0]
        if hasattr(data, name):
            name = name + '2'  # create identifiers like ROD2, PLANT2
        setattr(data, name, obj)

    return data