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.
Files changed (67) hide show
  1. data/.gitignore +4 -0
  2. data/GPL-2 +339 -0
  3. data/Makefile +5 -0
  4. data/README +75 -0
  5. data/Rakefile +58 -0
  6. data/VERSION +1 -0
  7. data/bin/scottkit +96 -0
  8. data/bin/scottkit.rb +96 -0
  9. data/data/.gitignore +1 -0
  10. data/data/adams/.gitignore +2 -0
  11. data/data/adams/AdamsGames.zip +0 -0
  12. data/data/adams/Makefile +18 -0
  13. data/data/crystal/crystal.map +112 -0
  14. data/data/crystal/crystal.sck +598 -0
  15. data/data/crystal/crystal.solution +82 -0
  16. data/data/dan-and-matt.sck +180 -0
  17. data/data/dan-and-matt.solution +32 -0
  18. data/data/howarth/.gitignore +2 -0
  19. data/data/howarth/Makefile +14 -0
  20. data/data/howarth/mysterious.tar.gz +0 -0
  21. data/data/test/Makefile +18 -0
  22. data/data/test/adams/Makefile +13 -0
  23. data/data/test/adams/adv01.solution +186 -0
  24. data/data/test/adams/adv01.transcript +869 -0
  25. data/data/test/adams/adv01.transcript.md5 +1 -0
  26. data/data/test/adams/adv02.solution +225 -0
  27. data/data/test/adams/adv02.transcript +970 -0
  28. data/data/test/adams/adv02.transcript.md5 +1 -0
  29. data/data/test/adams/adv04.solution +187 -0
  30. data/data/test/adams/adv04.transcript +876 -0
  31. data/data/test/adams/adv04.transcript.md5 +1 -0
  32. data/data/test/crystal.decompile +628 -0
  33. data/data/test/crystal.sao +373 -0
  34. data/data/test/crystal.save-file +62 -0
  35. data/data/test/crystal.save-script +41 -0
  36. data/data/test/crystal.sck +598 -0
  37. data/data/test/crystal.solution +82 -0
  38. data/data/test/crystal.transcript +413 -0
  39. data/data/test/t6.pretty-print +225 -0
  40. data/data/test/t7.sao +110 -0
  41. data/data/test/t7.solution +28 -0
  42. data/data/test/t7.transcript +147 -0
  43. data/data/tutorial/t1.sck +5 -0
  44. data/data/tutorial/t2.sck +14 -0
  45. data/data/tutorial/t3.sck +38 -0
  46. data/data/tutorial/t4.sck +62 -0
  47. data/data/tutorial/t5.sck +87 -0
  48. data/data/tutorial/t6.sck +119 -0
  49. data/data/tutorial/t7.sck +135 -0
  50. data/lib/scottkit/compile.rb +661 -0
  51. data/lib/scottkit/decompile.rb +175 -0
  52. data/lib/scottkit/game.rb +409 -0
  53. data/lib/scottkit/play.rb +474 -0
  54. data/notes/Definition +147 -0
  55. data/notes/Definition.saved-game +9 -0
  56. data/notes/Definition.scottfree-1.14 +142 -0
  57. data/notes/adventureland-maze +20 -0
  58. data/notes/continue-action +51 -0
  59. data/test/test_canonicalise.rb +94 -0
  60. data/test/test_compile.rb +54 -0
  61. data/test/test_decompile.rb +13 -0
  62. data/test/test_play.rb +20 -0
  63. data/test/test_playadams.rb +36 -0
  64. data/test/test_save.rb +31 -0
  65. data/test/withio.rb +15 -0
  66. data/test/withio_test.rb +31 -0
  67. 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