scottkit 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/GPL-2 +339 -0
- data/Makefile +5 -0
- data/README +75 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bin/scottkit +96 -0
- data/bin/scottkit.rb +96 -0
- data/data/.gitignore +1 -0
- data/data/adams/.gitignore +2 -0
- data/data/adams/AdamsGames.zip +0 -0
- data/data/adams/Makefile +18 -0
- data/data/crystal/crystal.map +112 -0
- data/data/crystal/crystal.sck +598 -0
- data/data/crystal/crystal.solution +82 -0
- data/data/dan-and-matt.sck +180 -0
- data/data/dan-and-matt.solution +32 -0
- data/data/howarth/.gitignore +2 -0
- data/data/howarth/Makefile +14 -0
- data/data/howarth/mysterious.tar.gz +0 -0
- data/data/test/Makefile +18 -0
- data/data/test/adams/Makefile +13 -0
- data/data/test/adams/adv01.solution +186 -0
- data/data/test/adams/adv01.transcript +869 -0
- data/data/test/adams/adv01.transcript.md5 +1 -0
- data/data/test/adams/adv02.solution +225 -0
- data/data/test/adams/adv02.transcript +970 -0
- data/data/test/adams/adv02.transcript.md5 +1 -0
- data/data/test/adams/adv04.solution +187 -0
- data/data/test/adams/adv04.transcript +876 -0
- data/data/test/adams/adv04.transcript.md5 +1 -0
- data/data/test/crystal.decompile +628 -0
- data/data/test/crystal.sao +373 -0
- data/data/test/crystal.save-file +62 -0
- data/data/test/crystal.save-script +41 -0
- data/data/test/crystal.sck +598 -0
- data/data/test/crystal.solution +82 -0
- data/data/test/crystal.transcript +413 -0
- data/data/test/t6.pretty-print +225 -0
- data/data/test/t7.sao +110 -0
- data/data/test/t7.solution +28 -0
- data/data/test/t7.transcript +147 -0
- data/data/tutorial/t1.sck +5 -0
- data/data/tutorial/t2.sck +14 -0
- data/data/tutorial/t3.sck +38 -0
- data/data/tutorial/t4.sck +62 -0
- data/data/tutorial/t5.sck +87 -0
- data/data/tutorial/t6.sck +119 -0
- data/data/tutorial/t7.sck +135 -0
- data/lib/scottkit/compile.rb +661 -0
- data/lib/scottkit/decompile.rb +175 -0
- data/lib/scottkit/game.rb +409 -0
- data/lib/scottkit/play.rb +474 -0
- data/notes/Definition +147 -0
- data/notes/Definition.saved-game +9 -0
- data/notes/Definition.scottfree-1.14 +142 -0
- data/notes/adventureland-maze +20 -0
- data/notes/continue-action +51 -0
- data/test/test_canonicalise.rb +94 -0
- data/test/test_compile.rb +54 -0
- data/test/test_decompile.rb +13 -0
- data/test/test_play.rb +20 -0
- data/test/test_playadams.rb +36 -0
- data/test/test_save.rb +31 -0
- data/test/withio.rb +15 -0
- data/test/withio_test.rb +31 -0
- metadata +118 -0
@@ -0,0 +1,661 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# Test with: cd /usr/local/src/mike/scott/scottkit && ruby1.9 -I lib bin/scottkit -c data/tutorial/t7.sck
|
3
|
+
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
module ScottKit
|
7
|
+
class Game
|
8
|
+
class Compiler
|
9
|
+
private
|
10
|
+
|
11
|
+
# Creates a new compiler for the specified game, set up ready to
|
12
|
+
# compile the game in the specified file.
|
13
|
+
#
|
14
|
+
# The input file may be specified either as a filename or a
|
15
|
+
# filehandle, or both. If both are given, then the filename is
|
16
|
+
# used only in reporting to help locate errors. _Some_ value
|
17
|
+
# must be given for the filename: an empty string is OK.
|
18
|
+
#
|
19
|
+
# (In case you're wondering, the main reason this has to be
|
20
|
+
# passed a Game object is that the behaviour of compile is
|
21
|
+
# influenced by the game's options.)
|
22
|
+
#
|
23
|
+
def initialize(game, filename, fh = nil)
|
24
|
+
@game = game
|
25
|
+
@lexer = Lexer.new(game, filename, fh)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Compiles the specified game-source file, writing the resulting
|
29
|
+
# object file to stdout, whence it should be redirected into a
|
30
|
+
# file so that it can be played. Yes, this API is sucky: it
|
31
|
+
# would be better if we had a simple compile method that builds
|
32
|
+
# the game in memory in a form that can by played, and which can
|
33
|
+
# then also be saved as an object file by some other method --
|
34
|
+
# but that would have been more work for little gain.
|
35
|
+
#
|
36
|
+
def compile_to_stdout
|
37
|
+
begin
|
38
|
+
tree = parse
|
39
|
+
generate_code(tree)
|
40
|
+
true
|
41
|
+
rescue
|
42
|
+
return false if String($!) == "syntax error"
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse
|
48
|
+
ident, version, unknown1, unknown2, start, treasury, maxload,
|
49
|
+
wordlen, lighttime, lightsource =
|
50
|
+
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil
|
51
|
+
rooms = [ CRoom.new(nil, nil, {}) ]
|
52
|
+
items, actions, verbgroups, noungroups = [], [], [], []
|
53
|
+
|
54
|
+
while peek != :eof
|
55
|
+
case peek
|
56
|
+
when :ident then skip; ident = match :symbol
|
57
|
+
when :version then skip; version = match :symbol
|
58
|
+
when :unknown1 then skip; unknown1 = match :symbol
|
59
|
+
when :unknown2 then skip; unknown2 = match :symbol
|
60
|
+
when :start then skip; start = match :symbol
|
61
|
+
when :treasury then skip; treasury = match :symbol
|
62
|
+
when :maxload then skip; maxload = match :symbol
|
63
|
+
when :wordlen then skip; wordlen = match :symbol
|
64
|
+
when :lighttime then skip; lighttime = match :symbol
|
65
|
+
when :lightsource then skip; lightsource = match :symbol
|
66
|
+
when :room then rooms << parse_room
|
67
|
+
when :item then items << parse_item(rooms.size-1)
|
68
|
+
when :action then actions << parse_action
|
69
|
+
when :occur then actions << parse_occur
|
70
|
+
when :verbgroup then verbgroups << parse_wordgroup
|
71
|
+
when :noungroup then noungroups << parse_wordgroup
|
72
|
+
else match nil, "new directive"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if peek != :eof
|
77
|
+
error "additional text remains after parsing"
|
78
|
+
end
|
79
|
+
|
80
|
+
CGame.new(ident, version, unknown1, unknown2, start, treasury,
|
81
|
+
maxload, wordlen, lighttime, lightsource, rooms,
|
82
|
+
items, actions, verbgroups, noungroups)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_room
|
86
|
+
match :room
|
87
|
+
name = match :symbol
|
88
|
+
desc = match :symbol
|
89
|
+
exits = {}
|
90
|
+
while peek == :exit
|
91
|
+
match :exit
|
92
|
+
direction = match :direction
|
93
|
+
dest = match :symbol
|
94
|
+
exits[direction] = dest
|
95
|
+
end
|
96
|
+
CRoom.new(name, desc, exits)
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_item(last_room)
|
100
|
+
match :item
|
101
|
+
name = match :symbol
|
102
|
+
desc = match :symbol
|
103
|
+
called, where = nil, nil
|
104
|
+
while true
|
105
|
+
case peek
|
106
|
+
when :called then match :called; called = match :symbol
|
107
|
+
when :at then match :at; where = match :symbol
|
108
|
+
when :nowhere then match :nowhere; where = ROOM_NOWHERE
|
109
|
+
when :carried then match :carried; where = ROOM_CARRIED
|
110
|
+
else break
|
111
|
+
end
|
112
|
+
end
|
113
|
+
CItem.new(name, desc, called, where ? where : last_room)
|
114
|
+
end
|
115
|
+
|
116
|
+
def parse_action
|
117
|
+
match :action
|
118
|
+
verb = match :symbol
|
119
|
+
if peek == :when
|
120
|
+
noun = nil
|
121
|
+
elsif peek == ":" # to terminate single-word actions if no conditions
|
122
|
+
skip
|
123
|
+
noun = nil
|
124
|
+
else
|
125
|
+
noun = match :symbol
|
126
|
+
skip if peek == ":" # optional
|
127
|
+
end
|
128
|
+
|
129
|
+
conds, instructions, comment = parse_actionbody
|
130
|
+
CAction.new(verb, noun, conds, instructions, comment)
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_occur
|
134
|
+
match :occur
|
135
|
+
if peek == :percent
|
136
|
+
chance = match :percent
|
137
|
+
skip if peek == ":" # optional
|
138
|
+
else
|
139
|
+
chance = nil
|
140
|
+
end
|
141
|
+
|
142
|
+
conds, instructions, comment = parse_actionbody
|
143
|
+
CAction.new(nil, chance, conds, instructions, comment)
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_actionbody
|
147
|
+
conds = []
|
148
|
+
while peek == :when || peek == :and
|
149
|
+
skip
|
150
|
+
op = match :symbol
|
151
|
+
type = Condition::OPStotype[op] or
|
152
|
+
error "unknown condition op '#{op}'"
|
153
|
+
case type
|
154
|
+
when :NONE then val = 0 # Any numeric value is fine here
|
155
|
+
when :number then val = match :symbol
|
156
|
+
when :room then val = match :symbol
|
157
|
+
when :item then val = match :symbol
|
158
|
+
else error "condition op '#{op}' has unknown type '#{type}'"
|
159
|
+
end
|
160
|
+
conds << [ op, val ]
|
161
|
+
end
|
162
|
+
|
163
|
+
instructions = []
|
164
|
+
while peek == :symbol
|
165
|
+
op = match :symbol
|
166
|
+
if op == "print"
|
167
|
+
val = [ match(:symbol) ]
|
168
|
+
else
|
169
|
+
type = Instruction::OPStotype[op] or
|
170
|
+
error "unknown instruction op '#{op}'"
|
171
|
+
case type
|
172
|
+
when :NONE then val = []
|
173
|
+
when :number then val = [ match(:symbol) ]
|
174
|
+
when :room then val = [ match(:symbol) ]
|
175
|
+
when :item then val = [ match(:symbol) ]
|
176
|
+
when :item_item then val = [ match(:symbol), match(:symbol) ]
|
177
|
+
when :item_room then val = [ match(:symbol), match(:symbol) ]
|
178
|
+
else error "instruction op '#{op}' has unknown type '#{type}'"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
instructions << [ op, val ]
|
182
|
+
end
|
183
|
+
|
184
|
+
comment = nil
|
185
|
+
if peek == :comment
|
186
|
+
skip
|
187
|
+
comment = match :symbol
|
188
|
+
end
|
189
|
+
|
190
|
+
[ conds, instructions, comment ]
|
191
|
+
end
|
192
|
+
|
193
|
+
def parse_wordgroup
|
194
|
+
skip
|
195
|
+
words = []
|
196
|
+
while peek == :symbol
|
197
|
+
words << match(:symbol)
|
198
|
+
end
|
199
|
+
words
|
200
|
+
end
|
201
|
+
|
202
|
+
# Skip over the current token
|
203
|
+
def skip; match peek; end
|
204
|
+
|
205
|
+
# Delegators through to the lexer class: just to keep parser source terse
|
206
|
+
def peek; @lexer.peek; end
|
207
|
+
def match(token, estr = nil); @lexer.match token, estr; end
|
208
|
+
def error(str); @lexer.error str; end
|
209
|
+
|
210
|
+
|
211
|
+
def generate_code(tree)
|
212
|
+
@had_errors = false
|
213
|
+
rooms = tree.rooms
|
214
|
+
items = tree.items
|
215
|
+
|
216
|
+
if tree.lightsource then
|
217
|
+
# The light-source is always item #9, so swap as necessary
|
218
|
+
lindex = items.index { |x| x.name == tree.lightsource } or
|
219
|
+
gerror "lightsource '#{tree.lightsource}' does not exist"
|
220
|
+
if (lindex != ITEM_LAMP)
|
221
|
+
items << CItem.new(nil, "", nil, 0) while items.size <= ITEM_LAMP
|
222
|
+
items[lindex], items[ITEM_LAMP] = items[ITEM_LAMP], items[lindex]
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Make name->index maps for rooms and items
|
227
|
+
roommap = { "_ROOM0" => 0 }
|
228
|
+
itemmap = {}
|
229
|
+
rooms.each.with_index { |room, index| roommap[room.name] = index }
|
230
|
+
items.each.with_index { |item, index| itemmap[item.name] = index }
|
231
|
+
|
232
|
+
startindex, treasuryindex = [ [ tree.start, "start" ],
|
233
|
+
[ tree.treasury, "treasury" ]
|
234
|
+
].map {
|
235
|
+
|ref| loc, caption = *ref
|
236
|
+
!loc ? 1 : roommap[loc] or
|
237
|
+
gerror "#{caption} room '#{loc}' does not exist"
|
238
|
+
}
|
239
|
+
|
240
|
+
# Resolve room names in exits
|
241
|
+
rooms.each do |room|
|
242
|
+
room.exits.each do |dir, dest|
|
243
|
+
room.exits[dir] = roommap[dest] or
|
244
|
+
gerror "'#{dest}' (#{dir} from #{room.name}) does not exist"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Resolve room names in item locations
|
249
|
+
items.each do |item|
|
250
|
+
if item.where.class == String
|
251
|
+
num = room_by_name(item.where, roommap) or
|
252
|
+
gerror "location '#{item.where}' for #{item.desc}) does not exist"
|
253
|
+
item.where = num
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Map each verb and noun to group of all its synonyms
|
258
|
+
@wordlen = Integer(tree.wordlen ||= 3)
|
259
|
+
verbtogroup, nountogroup = [ tree.verbgroups,
|
260
|
+
tree.noungroups ].map { |groups|
|
261
|
+
groups = groups.map { |list| list.map { |word| word.upcase[0, @wordlen] } }
|
262
|
+
res = {}
|
263
|
+
groups.each do |list|
|
264
|
+
list.each { |word| res[word] = list }
|
265
|
+
end
|
266
|
+
res
|
267
|
+
}
|
268
|
+
|
269
|
+
# Compile vocabulary, including synonyms.
|
270
|
+
verbs = [ "AUT" ]
|
271
|
+
nouns = [ "ANY" ]
|
272
|
+
verbmap, nounmap = {}, {}
|
273
|
+
|
274
|
+
# Verb 1 is GO, verb 10 is GET, verb 18 is DROP (always).
|
275
|
+
[ ["go", 1], ["get", 10], ["drop", 18] ]. each do |pair|
|
276
|
+
insert_word(verbtogroup, verbs, verbmap, *pair)
|
277
|
+
end
|
278
|
+
# Nouns 1-6 are directions: no synonyms possible
|
279
|
+
0.upto(5).each do |i|
|
280
|
+
insert_word(nountogroup, nouns, nounmap, @game.dirname(i), i+1)
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
# Messages from actions will be accumulated here
|
285
|
+
messages = [ "" ] # Maps message-number to message
|
286
|
+
messagemap = {} # Maps message to message-number
|
287
|
+
|
288
|
+
# Instructions must not exceed four per batch
|
289
|
+
actions = []
|
290
|
+
tree.actions.each do |action|
|
291
|
+
ins, acc = action.instructions, []
|
292
|
+
while ins.size > 4
|
293
|
+
acc.concat ins.shift(3)
|
294
|
+
acc.push [ "continue" ]
|
295
|
+
end
|
296
|
+
acc.concat ins
|
297
|
+
acc.push [0] while acc.size % 4 != 0
|
298
|
+
|
299
|
+
# We now have batches of four instructions; each but the
|
300
|
+
# first must be placed in a new action.
|
301
|
+
action.instructions = acc.shift(4)
|
302
|
+
actions << action
|
303
|
+
actions << CAction.new(nil, 0, [], acc.shift(4), "cont", []) while
|
304
|
+
acc.count != 0
|
305
|
+
end
|
306
|
+
|
307
|
+
# Resolve room and item names in actions and occurrences
|
308
|
+
actions.each do |action|
|
309
|
+
if action.verb
|
310
|
+
# Actual actions
|
311
|
+
action.verb = insert_word(verbtogroup, verbs, verbmap, action.verb)
|
312
|
+
if action.noun
|
313
|
+
action.noun = insert_word(nountogroup, nouns, nounmap,
|
314
|
+
action.noun)
|
315
|
+
else
|
316
|
+
action.noun = 0
|
317
|
+
end
|
318
|
+
else
|
319
|
+
# Occurrences
|
320
|
+
action.verb = 0
|
321
|
+
action.noun = Integer(action.noun || 100)
|
322
|
+
end
|
323
|
+
|
324
|
+
action.conds.each do |cond|
|
325
|
+
op, arg = cond[0], cond[1]
|
326
|
+
opcode = Condition::OPStoindex[op]
|
327
|
+
type = Condition::OPStotype[op]
|
328
|
+
raise "impossible unknown condition op '#{op}'" if
|
329
|
+
!opcode || !type
|
330
|
+
cond[0] = opcode
|
331
|
+
case type
|
332
|
+
when :NONE then # nothing to do
|
333
|
+
when :number then cond[1] = Integer(cond[1])
|
334
|
+
when :room then cond[1] = roommap[arg] or
|
335
|
+
gerror "unknown room in condition '#{arg}'"
|
336
|
+
when :item then cond[1] = itemmap[arg] or
|
337
|
+
gerror "unknown item in condition '#{arg}'"
|
338
|
+
else gerror "condition op '#{op}' has unknown type '#{type}'"
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
gathered_args = []
|
343
|
+
action.instructions.each do |ins|
|
344
|
+
op, args = ins[0], ins[1]
|
345
|
+
arg0, arg1 = *args
|
346
|
+
if op == 0
|
347
|
+
next
|
348
|
+
elsif op == "print"
|
349
|
+
if !(msgno = messagemap[arg0])
|
350
|
+
messages << arg0
|
351
|
+
msgno = messagemap[arg0] = messages.size-1
|
352
|
+
end
|
353
|
+
ins[0] = msgno <= 51 ? msgno : msgno+50
|
354
|
+
next
|
355
|
+
end
|
356
|
+
opcode = Instruction::OPStoindex[op]
|
357
|
+
type = Instruction::OPStotype[op]
|
358
|
+
raise "impossible unknown instruction op '#{op}'" if
|
359
|
+
!opcode || !type
|
360
|
+
ins[0] = opcode
|
361
|
+
case type
|
362
|
+
when :NONE then # nothing to do
|
363
|
+
when :number then gathered_args << Integer(arg0)
|
364
|
+
when :room then gathered_args << (roommap[arg0] or
|
365
|
+
gerror "unknown room in instruction '#{arg0}'")
|
366
|
+
when :item then gathered_args << (itemmap[arg0] or
|
367
|
+
gerror "unknown item in instruction '#{arg0}'")
|
368
|
+
when :item_item then gathered_args << (itemmap[arg0] or
|
369
|
+
gerror "unknown item in instruction '#{arg0}'")
|
370
|
+
gathered_args << (itemmap[arg1] or
|
371
|
+
gerror "unknown item in instruction '#{arg1}'")
|
372
|
+
when :item_room then gathered_args << (itemmap[arg0] or
|
373
|
+
gerror "unknown item in instruction '#{arg0}'")
|
374
|
+
gathered_args << (roommap[arg1] or
|
375
|
+
gerror "unknown room in instruction '#{arg1}'")
|
376
|
+
else gerror "instruction op '#{op}' has unknown type '#{type}'"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
action.gathered_args = gathered_args
|
380
|
+
end
|
381
|
+
|
382
|
+
# Add auto-get names of items to vocabulary
|
383
|
+
items.each do |item|
|
384
|
+
insert_word(nountogroup, nouns, nounmap, item.called) if item.called
|
385
|
+
end
|
386
|
+
|
387
|
+
1.upto([ verbs.size-1, nouns.size-1 ].max) do |i|
|
388
|
+
verbs[i] = "" if !verbs[i]
|
389
|
+
nouns[i] = "" if !nouns[i]
|
390
|
+
end
|
391
|
+
|
392
|
+
return if @had_errors
|
393
|
+
|
394
|
+
# Write header
|
395
|
+
puts tree.unknown1 || 0
|
396
|
+
puts items.size-1
|
397
|
+
puts actions.size-1
|
398
|
+
puts verbs.size-1
|
399
|
+
puts rooms.size-1
|
400
|
+
puts tree.maxload || -1
|
401
|
+
puts startindex
|
402
|
+
puts items.select { |x| x.desc[0] == "*" }.count
|
403
|
+
puts tree.wordlen
|
404
|
+
puts tree.lighttime || -1
|
405
|
+
puts messages.size-1
|
406
|
+
puts treasuryindex
|
407
|
+
puts # Blank line
|
408
|
+
|
409
|
+
# Actions
|
410
|
+
actions.each do |action|
|
411
|
+
print 150*action.verb + action.noun, " "
|
412
|
+
|
413
|
+
print action.conds.map { |x| String(x[0] + 20 * x[1]) + " " }.join
|
414
|
+
print action.gathered_args.map { |x| String(20*x) + " " }.join
|
415
|
+
nconds = action.conds.size + action.gathered_args.size
|
416
|
+
raise "condition has #{nconds} conditions" if nconds > 5
|
417
|
+
(5-nconds).times { print "0 " }
|
418
|
+
|
419
|
+
ins = action.instructions.map { |x| x[0] }
|
420
|
+
(4-ins.count).times { ins << 0 }
|
421
|
+
puts "#{150*ins[0] + ins[1]} #{150*ins[2] + ins[3]}\n"
|
422
|
+
end
|
423
|
+
puts # Blank line
|
424
|
+
|
425
|
+
# Vocab
|
426
|
+
verbs.each.with_index do |verb, i|
|
427
|
+
puts "\"#{verb}\" \"#{nouns[i]}\""
|
428
|
+
end
|
429
|
+
puts # Blank line
|
430
|
+
|
431
|
+
# Rooms
|
432
|
+
rooms.each do |room|
|
433
|
+
0.upto(5).each do |i|
|
434
|
+
exit = room.exits[@game.dirname(i)]
|
435
|
+
print(exit ? exit : 0, " ")
|
436
|
+
end
|
437
|
+
print "\"#{room.desc}\"\n"
|
438
|
+
end
|
439
|
+
puts # Blank line
|
440
|
+
|
441
|
+
# Messages
|
442
|
+
messages.each do |message|
|
443
|
+
puts "\"#{message}\"\n"
|
444
|
+
end
|
445
|
+
puts # Blank line
|
446
|
+
|
447
|
+
# Items
|
448
|
+
items.each do |item|
|
449
|
+
desc = item.desc
|
450
|
+
desc += "/" + item.called.upcase[0, @wordlen] + "/" if item.called
|
451
|
+
puts "\"#{desc}\" #{item.where}"
|
452
|
+
end
|
453
|
+
puts # Blank line
|
454
|
+
|
455
|
+
# Action comments
|
456
|
+
actions.each do |action|
|
457
|
+
puts "\"#{action.comment || ""}\"\n"
|
458
|
+
end
|
459
|
+
puts # Blank line
|
460
|
+
|
461
|
+
# Trailer
|
462
|
+
puts tree.version || 0
|
463
|
+
puts tree.ident || 0
|
464
|
+
puts tree.unknown2 || 0
|
465
|
+
end
|
466
|
+
|
467
|
+
def room_by_name(loc, roommap)
|
468
|
+
if @game.options[:bug_tolerant] && loc[0,5] == "_ROOM"
|
469
|
+
Integer(loc[5,999])
|
470
|
+
else
|
471
|
+
roommap[loc]
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# Complex API here, sorry. If word, or an equivalent word
|
476
|
+
# according to synmap, is not already in list and map, inserts
|
477
|
+
# it and its synonyms into both, with map hashing words to the
|
478
|
+
# index they appear at. Word is inserted at index if specified
|
479
|
+
# and otherwise at the first free slot, or off the end if there
|
480
|
+
# are no free slots. Synonyms, if any, follow thereafter in a
|
481
|
+
# block. Returns the index of the word in list
|
482
|
+
def insert_word(synmap, list, map, word, index = nil)
|
483
|
+
word = word.upcase[0, @wordlen]
|
484
|
+
syn = synmap[word] || [word]
|
485
|
+
canonical = syn[0]
|
486
|
+
return map[canonical] if map[canonical]
|
487
|
+
if !index
|
488
|
+
index = 1
|
489
|
+
index += 1 while syn.each_index.any? { |i| list[index+i] != nil }
|
490
|
+
end
|
491
|
+
|
492
|
+
firstindex = index
|
493
|
+
syn.each do |thisword|
|
494
|
+
if list[index]
|
495
|
+
return(gerror "can't insert word '#{thisword}' " +
|
496
|
+
"@at position #{index} -- got '#{list[index]}'")
|
497
|
+
end
|
498
|
+
list[index] = index == firstindex ? thisword : "*"+thisword
|
499
|
+
map[thisword] = firstindex
|
500
|
+
index += 1
|
501
|
+
end
|
502
|
+
firstindex
|
503
|
+
end
|
504
|
+
|
505
|
+
def gerror(str)
|
506
|
+
$stderr.puts "error: #{str}"
|
507
|
+
@had_errors = true
|
508
|
+
0
|
509
|
+
end
|
510
|
+
|
511
|
+
|
512
|
+
public :compile_to_stdout # Must be visible to Game.compile()
|
513
|
+
public :parse # Used by test_compile.rb
|
514
|
+
|
515
|
+
|
516
|
+
CGame = Struct.new(:ident, :version, :unknown1, :unknown2,
|
517
|
+
:start, :treasury, :maxload, :wordlen,
|
518
|
+
:lighttime, :lightsource, :rooms, :items,
|
519
|
+
:actions, :verbgroups, :noungroups) #:nodoc:
|
520
|
+
CRoom = Struct.new(:name, :desc, :exits) #:nodoc:
|
521
|
+
CItem = Struct.new(:name, :desc, :called, :where) #:nodoc:
|
522
|
+
CAction = Struct.new(:verb, :noun, :conds, :instructions,
|
523
|
+
:comment, :gathered_args) #:nodoc:
|
524
|
+
|
525
|
+
|
526
|
+
class Lexer #:nodoc:
|
527
|
+
attr_reader :lexeme
|
528
|
+
|
529
|
+
TOKENMAP = {
|
530
|
+
"start" => :start,
|
531
|
+
"treasury" => :treasury,
|
532
|
+
"ident" => :ident,
|
533
|
+
"version" => :version,
|
534
|
+
"unknown1" => :unknown1,
|
535
|
+
"unknown2" => :unknown2,
|
536
|
+
"maxload" => :maxload,
|
537
|
+
"wordlen" => :wordlen,
|
538
|
+
"lighttime" => :lighttime,
|
539
|
+
"lightsource" => :lightsource,
|
540
|
+
"room" => :room,
|
541
|
+
"exit" => :exit,
|
542
|
+
"north" => :direction, "south" => :direction, "east" => :direction,
|
543
|
+
"west" => :direction, "up" => :direction, "down" => :direction,
|
544
|
+
"item" => :item,
|
545
|
+
"called" => :called,
|
546
|
+
"at" => :at,
|
547
|
+
"nowhere" => :nowhere,
|
548
|
+
"carried" => :carried,
|
549
|
+
"action" => :action,
|
550
|
+
"occur" => :occur,
|
551
|
+
"when" => :when,
|
552
|
+
"and" => :and,
|
553
|
+
"comment" => :comment,
|
554
|
+
"verbgroup" => :verbgroup,
|
555
|
+
"noungroup" => :noungroup,
|
556
|
+
}
|
557
|
+
|
558
|
+
def initialize(game, filename, fh = nil)
|
559
|
+
if !fh
|
560
|
+
fh = File.new(filename)
|
561
|
+
end
|
562
|
+
@game, @filename, @fh = game, filename, fh
|
563
|
+
@linenumber = 0
|
564
|
+
@buffer = ""
|
565
|
+
@lookahead = nil
|
566
|
+
end
|
567
|
+
|
568
|
+
def error(str)
|
569
|
+
filename = (defined? @filename) ? @filename : "<UNKNOWN>"
|
570
|
+
$stderr.puts "#{filename}:#{@linenumber}:#{str}"
|
571
|
+
raise "syntax error"
|
572
|
+
end
|
573
|
+
|
574
|
+
def lex
|
575
|
+
token = _lex
|
576
|
+
@game.dputs :show_tokens, "token: #{render(token)}"
|
577
|
+
token
|
578
|
+
end
|
579
|
+
|
580
|
+
def _lex
|
581
|
+
@buffer.lstrip!
|
582
|
+
while @buffer == "" do
|
583
|
+
if !(@buffer = @fh.gets)
|
584
|
+
return :eof
|
585
|
+
end
|
586
|
+
@linenumber += 1
|
587
|
+
@buffer.chomp!
|
588
|
+
@buffer.rstrip!
|
589
|
+
@buffer.lstrip!
|
590
|
+
end
|
591
|
+
|
592
|
+
if @buffer[0] == "#"
|
593
|
+
# Comment runs to end of line
|
594
|
+
@buffer = ""
|
595
|
+
return _lex # Be honest, a GOTO would be better here
|
596
|
+
elsif match = @buffer.match(/^"(.*?)"/)
|
597
|
+
@lexeme, @buffer = match[1], match.post_match
|
598
|
+
:symbol
|
599
|
+
elsif match = @buffer.match(/^"(.*)/)
|
600
|
+
# Multi-line string -- can include hashes and indents
|
601
|
+
s = match[1]
|
602
|
+
while @buffer = @fh.gets
|
603
|
+
@linenumber += 1
|
604
|
+
@buffer.chomp!
|
605
|
+
if match = @buffer.match(/(.*?)"/)
|
606
|
+
@lexeme = s + "\n" + match[1]
|
607
|
+
@buffer = match.post_match
|
608
|
+
break
|
609
|
+
else
|
610
|
+
s += "\n" + @buffer
|
611
|
+
end
|
612
|
+
end
|
613
|
+
:symbol
|
614
|
+
elsif match = @buffer.match(/^(\d+)%/)
|
615
|
+
@lexeme, @buffer = match[1], match.post_match
|
616
|
+
:percent
|
617
|
+
elsif match = @buffer.match(/^([!a-z_0-9-]+)/i)
|
618
|
+
@lexeme, @buffer = match[1], match.post_match
|
619
|
+
TOKENMAP[@lexeme] || :symbol
|
620
|
+
else
|
621
|
+
# Must be a single character
|
622
|
+
@lexeme = @buffer[0]
|
623
|
+
@buffer[0] = ""
|
624
|
+
@lexeme
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
def peek
|
629
|
+
@lookahead ||= lex
|
630
|
+
end
|
631
|
+
|
632
|
+
def match(expected, estr = nil)
|
633
|
+
token = peek
|
634
|
+
@lookahead = nil
|
635
|
+
if token != expected
|
636
|
+
error("expected #{estr || expected}, got #{render(token)}" +
|
637
|
+
" (before `#{@buffer.lstrip}')")
|
638
|
+
end
|
639
|
+
@lexeme
|
640
|
+
end
|
641
|
+
|
642
|
+
def render(token)
|
643
|
+
extra = render_lexeme(token, @lexeme)
|
644
|
+
extra ? "#{token} #{extra}" : token
|
645
|
+
end
|
646
|
+
|
647
|
+
def render_lexeme(token, lexeme)
|
648
|
+
if token == :direction
|
649
|
+
lexeme
|
650
|
+
elsif token == :symbol
|
651
|
+
"\"#{lexeme}\""
|
652
|
+
elsif token == :percent
|
653
|
+
"'#{lexeme}%'"
|
654
|
+
else
|
655
|
+
nil
|
656
|
+
end
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
661
|
+
end
|