ifmapper 0.7 → 0.8
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/HISTORY.txt +31 -0
- data/IFMapper.gemspec +3 -3
- data/{IFMapper.rb → IFMapper.rbw} +0 -0
- data/TODO.txt +7 -0
- data/lib/IFMapper/Connection.rb +5 -0
- data/lib/IFMapper/FXMap.rb +283 -128
- data/lib/IFMapper/FXMapperWindow.rb +31 -32
- data/lib/IFMapper/IFMReader.rb +5 -44
- data/lib/IFMapper/Map.rb +57 -0
- data/lib/IFMapper/PDFMapExporter.rb +70 -28
- data/lib/IFMapper/Room.rb +3 -0
- data/lib/IFMapper/Section.rb +30 -0
- data/lib/IFMapper/TranscriptReader.rb +748 -0
- metadata +13 -10
@@ -0,0 +1,748 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
#
|
4
|
+
# Class used to parse an Infocom-style transcript and automatically
|
5
|
+
# generate a map. This can be used to automatically map a game as
|
6
|
+
# you play it.
|
7
|
+
#
|
8
|
+
# This code is based on Perl code by Dave Chapeskie, used in IFM,
|
9
|
+
# albeit it has been enhanced to handle multi-line commands, room stub
|
10
|
+
# exits, objects parsing and the ability to parse transcripts as
|
11
|
+
# they are being spit out.
|
12
|
+
#
|
13
|
+
class TranscriptReader
|
14
|
+
|
15
|
+
PROMPT = /^>\s*/
|
16
|
+
LOOK = /^l(ook)?/i
|
17
|
+
UNDO = /^undo$/i
|
18
|
+
TELEPORT = /^(restart|restore)$/i
|
19
|
+
IGNORE = /transcript/i
|
20
|
+
OTHERS = /^(read|inventory$|i$)/i
|
21
|
+
UNSCRIPT = /^unscript$/i
|
22
|
+
BLANK = /^\s*$/
|
23
|
+
MOVE = /^(walk|run|go)\s+/i
|
24
|
+
TAKE = /^(take|get)\s+(a\s+|the\s+)?(.*)/i
|
25
|
+
DROP = /^(drop|leave)\s+()/i
|
26
|
+
STARTUP = /(^[A-Z]+$|Copyright|\([cC]\)\s*\d|Trademark|Release|Version|[Ss]erial [Nn]umber|Written by)/
|
27
|
+
DARKNESS = /dark/i
|
28
|
+
DEAD = /(You die|You have died|You are dead)/i
|
29
|
+
|
30
|
+
# Compass direction command -> direction mapping.
|
31
|
+
DIRMAP = {
|
32
|
+
"n" => 0, "north" => 0, "ne" => 1, "northeast" => 1,
|
33
|
+
"e" => 2, "east" => 2, "southeast" => 3, "se" => 3,
|
34
|
+
"south" => 4, "s" => 4, "southwest" => 5, "sw" => 5,
|
35
|
+
"west" => 6, "w" => 6, "northwest" => 7, "nw" => 7
|
36
|
+
}
|
37
|
+
|
38
|
+
ODIRMAP = {"up" => 1, "u" => 1, "down" => 2, "d" => 2,
|
39
|
+
"in" => 3, "out" => 4, 'enter' => 3, 'exit' => 4 }
|
40
|
+
|
41
|
+
# Direction list in order of positioning preference.
|
42
|
+
DIRLIST = [ 0, 4, 2, 6, 1, 3, 5, 7 ]
|
43
|
+
|
44
|
+
NAME_REMOVE = /(\s+\(.+\)|,\s+[io]n\s+.+)/ # remove things like (on the bed)
|
45
|
+
NAME_INVALID = /[\[\]\.!\?]/
|
46
|
+
NAME_MAXWORDS = 20
|
47
|
+
NAME_MAXUNCAP = 4 # so that lowercase room/end will be accepted
|
48
|
+
|
49
|
+
# Default room description recognition parameters.
|
50
|
+
DESC_MINWORDS = 10
|
51
|
+
|
52
|
+
|
53
|
+
## Change this to non-dil to print out debugging info
|
54
|
+
@@debug = nil
|
55
|
+
|
56
|
+
|
57
|
+
def debug(*msg)
|
58
|
+
if @@debug
|
59
|
+
$stdout.puts msg
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
## Possible nessages indicating get/take succeeded
|
64
|
+
TAKE_OK = [
|
65
|
+
/taken/i,
|
66
|
+
/you\s+now\s+have\s+(got\s+)?the/i,
|
67
|
+
]
|
68
|
+
|
69
|
+
IT = /^(it|them)$/
|
70
|
+
|
71
|
+
#
|
72
|
+
# Handle a take action
|
73
|
+
#
|
74
|
+
def take(move, objs)
|
75
|
+
return unless @here
|
76
|
+
objs.each { |cmd, dummy, obj|
|
77
|
+
next if not obj
|
78
|
+
|
79
|
+
objlist = obj.split(',')
|
80
|
+
o = objlist[0]
|
81
|
+
if objlist.size == 1 and o != 'all'
|
82
|
+
# ignore 'get up'
|
83
|
+
next if cmd == 'get' and o == 'up'
|
84
|
+
o = @last_obj if o =~ IT
|
85
|
+
next if not o
|
86
|
+
status = move[:reply].to_s
|
87
|
+
TAKE_OK.each { |re|
|
88
|
+
if status =~ re and not @objects.has_key?(o)
|
89
|
+
@here.objects << o << "\n"
|
90
|
+
@objects[o] = 1
|
91
|
+
@last_obj = o
|
92
|
+
break
|
93
|
+
end
|
94
|
+
}
|
95
|
+
else
|
96
|
+
# Handle multiple objects
|
97
|
+
move[:reply].each { |reply|
|
98
|
+
o, status = reply.split(':')
|
99
|
+
next if not status
|
100
|
+
o = @last_obj if o =~ IT
|
101
|
+
if o and not @objects.has_key?(o)
|
102
|
+
@here.objects << o << "\n"
|
103
|
+
@objects[o] = 1
|
104
|
+
@last_obj = o
|
105
|
+
end
|
106
|
+
}
|
107
|
+
end
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
#
|
112
|
+
# Add a new room to the automapper
|
113
|
+
#
|
114
|
+
def add(room)
|
115
|
+
@rooms[room] = {:section => @map.section, :desc => nil }
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# Remove some rooms from the automap knowledge (user removed these manually)
|
120
|
+
#
|
121
|
+
def remove(rooms)
|
122
|
+
rooms.each { |r| @rooms.delete(r) }
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def parse
|
127
|
+
# Find first command prompt
|
128
|
+
while line = @f.gets
|
129
|
+
break if PROMPT =~ line
|
130
|
+
end
|
131
|
+
parse_line(line)
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Given a room description, parse it to try to find out all exits
|
136
|
+
# from room.
|
137
|
+
#
|
138
|
+
|
139
|
+
DIRS = {
|
140
|
+
'north' => 0, 'northeast' => 1, 'east' => 2, 'southeast' => 3, 'south' => 4,
|
141
|
+
'southwest' => 5, 'west' => 6, 'northwest' => 7
|
142
|
+
}
|
143
|
+
|
144
|
+
DIR = '(' + DIRS.keys.join('|') + ')'
|
145
|
+
OR = '(and|or)'
|
146
|
+
|
147
|
+
EXITS_REGEX =
|
148
|
+
[
|
149
|
+
# You can go south or east
|
150
|
+
/you\s+can\s+go\s+#{DIR}\s+#{OR}\s+#{DIR}[,\.\s+]/i,
|
151
|
+
# You can go south
|
152
|
+
/you\s+can\s+go\s+#{DIR}[,\.\s+]/i,
|
153
|
+
# to the east or west
|
154
|
+
/to\s+the\s+#{DIR}\s+#{OR}\s+#{DIR}[,\.\s+]/i,
|
155
|
+
# to the east
|
156
|
+
/to\s+the\s+#{DIR}[,\.\s+]/i,
|
157
|
+
# paths lead west and north
|
158
|
+
/(run|lead|wander|winds)\s+#{DIR}\s+#{OR}\s+#{DIR}[,\.\s+]/i,
|
159
|
+
# east-west corridor
|
160
|
+
/[\.\s+]#{DIR}[\/-]#{DIR}[,\.\s+]/i,
|
161
|
+
# East is the postoffice
|
162
|
+
/[,\.\s+]#{DIR}\s+is/i,
|
163
|
+
# continues|lies|etc... east
|
164
|
+
/(runs|leads|heads|opens|winds|continues|branches|lies|wanders|bends|curves)\s+#{DIR}[,\.\s+]/i,
|
165
|
+
/(running|leading|heading|opening|branching|lying|wandering|looking|bending)\s+#{DIR}[,\.\s+]/i,
|
166
|
+
]
|
167
|
+
|
168
|
+
EXITS_SPECIAL = {
|
169
|
+
/four\s+directions/i => [0, 2, 4, 6],
|
170
|
+
/four\s+compass\s+points/i => [0, 2, 4, 6],
|
171
|
+
/all\s+directions/i => [0, 1, 2, 3, 4, 5, 6, 7],
|
172
|
+
}
|
173
|
+
|
174
|
+
def parse_exits(r, desc)
|
175
|
+
return if not desc
|
176
|
+
exits = []
|
177
|
+
|
178
|
+
# Remove \n from descriptions
|
179
|
+
desc.gsub(/\n/, ' ')
|
180
|
+
|
181
|
+
# Now, start searching for stuff
|
182
|
+
|
183
|
+
# First try the special directions...
|
184
|
+
EXITS_SPECIAL.each { |re, dirs|
|
185
|
+
if desc =~ re
|
186
|
+
exits += dirs
|
187
|
+
break
|
188
|
+
end
|
189
|
+
}
|
190
|
+
|
191
|
+
# If that failed, start searching for exits
|
192
|
+
if exits.empty?
|
193
|
+
EXITS_REGEX.each { |re|
|
194
|
+
matches = desc.scan(re)
|
195
|
+
next if matches.empty?
|
196
|
+
matches.each { |arr|
|
197
|
+
arr.each { |m|
|
198
|
+
next unless DIRS[m]
|
199
|
+
exits << DIRS[m]
|
200
|
+
}
|
201
|
+
}
|
202
|
+
}
|
203
|
+
end
|
204
|
+
|
205
|
+
exits.uniq!
|
206
|
+
|
207
|
+
# Add a 'stub' for the new connection
|
208
|
+
exits.each { |exit|
|
209
|
+
next if r[exit]
|
210
|
+
c = @map.new_connection( r, exit, nil )
|
211
|
+
c.dir = Connection::AtoB
|
212
|
+
debug "\tADDED STUB #{c}"
|
213
|
+
}
|
214
|
+
end
|
215
|
+
|
216
|
+
def parse_line(line)
|
217
|
+
return unless line
|
218
|
+
|
219
|
+
@moves.clear
|
220
|
+
|
221
|
+
#
|
222
|
+
# Read all commands
|
223
|
+
#
|
224
|
+
loop do
|
225
|
+
line.sub!(PROMPT, '')
|
226
|
+
line.chop!
|
227
|
+
line.sub!(/\s+$/, '')
|
228
|
+
cmd = line
|
229
|
+
|
230
|
+
# Read reply
|
231
|
+
reply = []
|
232
|
+
while line = @f.gets
|
233
|
+
break if line =~ PROMPT
|
234
|
+
line.chop!
|
235
|
+
line.sub!(/\s+$/,'')
|
236
|
+
reply << line
|
237
|
+
end
|
238
|
+
|
239
|
+
break if not line
|
240
|
+
|
241
|
+
if cmd =~ UNDO
|
242
|
+
@moves.pop
|
243
|
+
else
|
244
|
+
move = { }
|
245
|
+
move[:cmd] = cmd
|
246
|
+
move[:reply] = reply
|
247
|
+
@moves << move
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# Step 2
|
254
|
+
tele = nil
|
255
|
+
@moves.each { |move|
|
256
|
+
cmd = move[:cmd]
|
257
|
+
next if cmd =~ IGNORE or cmd =~ OTHERS
|
258
|
+
if cmd =~ UNSCRIPT
|
259
|
+
tele = 1
|
260
|
+
next
|
261
|
+
end
|
262
|
+
|
263
|
+
tele = 1 if cmd =~ TELEPORT
|
264
|
+
|
265
|
+
name = nil
|
266
|
+
desc = ''
|
267
|
+
roomflag = false
|
268
|
+
desc_gap = false
|
269
|
+
startup = false
|
270
|
+
rooms = []
|
271
|
+
move[:reply].each { |r|
|
272
|
+
tele = 1 if r =~ DEAD
|
273
|
+
|
274
|
+
if r =~ STARTUP
|
275
|
+
# Dealing with a startup message, such as:
|
276
|
+
# MY GAME
|
277
|
+
# Copyright (C) 1984 ComputerQuest
|
278
|
+
# We skip the whole thing until the next blank line
|
279
|
+
debug "#{r} skipped due to startup"
|
280
|
+
startup = true
|
281
|
+
desc = ''
|
282
|
+
name = nil
|
283
|
+
roomflag = false
|
284
|
+
desc_gap = false
|
285
|
+
next
|
286
|
+
end
|
287
|
+
next if startup and r !~ BLANK
|
288
|
+
startup = false
|
289
|
+
|
290
|
+
if not roomflag and r !~ BLANK and roomname(r)
|
291
|
+
debug "set roomflag with #{r}"
|
292
|
+
roomflag = true
|
293
|
+
name = r
|
294
|
+
next
|
295
|
+
end
|
296
|
+
|
297
|
+
break if roomflag and desc_gap and r =~ BLANK
|
298
|
+
|
299
|
+
desc_gap = true if roomflag and r =~ BLANK
|
300
|
+
|
301
|
+
|
302
|
+
if roomflag and r !~ BLANK
|
303
|
+
desc << r << "\n"
|
304
|
+
end
|
305
|
+
|
306
|
+
if r =~ BLANK and roomflag # and desc != ''
|
307
|
+
|
308
|
+
if desc.count("\n") == 1 and desc =~ /\?$/
|
309
|
+
# A "What next?" type of prompt, not a room description
|
310
|
+
desc = ''
|
311
|
+
end
|
312
|
+
desc = nil if desc == ''
|
313
|
+
|
314
|
+
rooms << {
|
315
|
+
:name => name,
|
316
|
+
:desc => desc,
|
317
|
+
:tele => tele
|
318
|
+
}
|
319
|
+
roomflag = false
|
320
|
+
desc_gap = false
|
321
|
+
name = nil
|
322
|
+
desc = ''
|
323
|
+
tele = nil
|
324
|
+
end
|
325
|
+
}
|
326
|
+
|
327
|
+
if not rooms.empty?
|
328
|
+
move[:rooms] = rooms
|
329
|
+
move[:look] = true if cmd =~ LOOK
|
330
|
+
move[:reply] = nil
|
331
|
+
end
|
332
|
+
}
|
333
|
+
|
334
|
+
|
335
|
+
@moves.each { |move|
|
336
|
+
cmd = move[:cmd]
|
337
|
+
if objs = cmd.scan(TAKE)
|
338
|
+
take(move, objs)
|
339
|
+
end
|
340
|
+
|
341
|
+
next unless move[:rooms]
|
342
|
+
move[:rooms].each { |room|
|
343
|
+
name = room[:name]
|
344
|
+
debug "SECTION: #{@map.section}"
|
345
|
+
debug "HERE IS: #{@here}"
|
346
|
+
debug "CMD: #{cmd}"
|
347
|
+
debug "ENDS AT: #{name}"
|
348
|
+
|
349
|
+
desc = room[:desc]
|
350
|
+
line = move[:line]
|
351
|
+
cmd = move[:cmd]
|
352
|
+
|
353
|
+
# If we teleported, try to find room
|
354
|
+
if room[:tele]
|
355
|
+
debug "\t ****TELEPORT TO #{name}****"
|
356
|
+
@here = find_room(name, desc)
|
357
|
+
debug "\t TO: #{@here}"
|
358
|
+
end
|
359
|
+
|
360
|
+
# If it is a look command or we don't know where we are yet,
|
361
|
+
# set current room.
|
362
|
+
if move[:look] or not @here
|
363
|
+
@here = new_room(move, name, desc) unless @here
|
364
|
+
@here.selected = true
|
365
|
+
next
|
366
|
+
end
|
367
|
+
|
368
|
+
# Otherwise, assume we moved in some way. Try to find the new room.
|
369
|
+
there = find_room(name, desc)
|
370
|
+
next if there == @here
|
371
|
+
|
372
|
+
cmd.sub!(MOVE, '')
|
373
|
+
if DIRMAP[cmd]
|
374
|
+
dir = DIRMAP[cmd]
|
375
|
+
go = nil
|
376
|
+
cmd = nil
|
377
|
+
elsif ODIRMAP[cmd]
|
378
|
+
go = ODIRMAP[cmd]
|
379
|
+
dir = choose_dir(@here, there, go)
|
380
|
+
cmd = nil
|
381
|
+
else
|
382
|
+
# special move ---
|
383
|
+
dir = choose_dir(@here, there)
|
384
|
+
go = nil
|
385
|
+
end
|
386
|
+
|
387
|
+
@here.selected = false
|
388
|
+
if not there
|
389
|
+
# Unvisited -- new room
|
390
|
+
@here = new_room(move, name, desc, dir, @here, go, cmd)
|
391
|
+
else
|
392
|
+
# Visited before -- new link
|
393
|
+
new_link(move, @here, there, dir, go, cmd)
|
394
|
+
@here = there
|
395
|
+
end
|
396
|
+
}
|
397
|
+
}
|
398
|
+
|
399
|
+
@moves.clear
|
400
|
+
@map.fit
|
401
|
+
if @map.kind_of?(FXMap)
|
402
|
+
@here.selected = true if @here
|
403
|
+
@map.zoom = @map.zoom
|
404
|
+
@map.center_view_on_room(@here) if @here
|
405
|
+
@map.verify_integrity
|
406
|
+
@map.draw
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def find_room(name, desc)
|
411
|
+
bestscore = 0
|
412
|
+
best = nil
|
413
|
+
@rooms.each { |r, room|
|
414
|
+
score = 0
|
415
|
+
|
416
|
+
|
417
|
+
if desc and room[:desc]
|
418
|
+
# We have a description...
|
419
|
+
# Try exact description match first
|
420
|
+
score += 10 if room[:desc] == desc
|
421
|
+
|
422
|
+
# Try substring match
|
423
|
+
score += 5 if room[:desc].index(desc)
|
424
|
+
|
425
|
+
# If still no luck, try first N words
|
426
|
+
if score == 0
|
427
|
+
dwords = room[:desc].split(' ')
|
428
|
+
words = desc.split(' ')
|
429
|
+
match = true
|
430
|
+
0.upto(DESC_MINWORDS) { |i|
|
431
|
+
if words[i] != dwords[i]
|
432
|
+
match = false
|
433
|
+
break
|
434
|
+
end
|
435
|
+
}
|
436
|
+
|
437
|
+
score += 2 if match
|
438
|
+
end
|
439
|
+
else
|
440
|
+
# Just the name, not so good
|
441
|
+
score += 1 if r.name == name
|
442
|
+
end
|
443
|
+
next if score <= bestscore
|
444
|
+
bestscore = score
|
445
|
+
best = [r, room[:section]]
|
446
|
+
}
|
447
|
+
return nil if not best
|
448
|
+
|
449
|
+
if best[1] != @map.section
|
450
|
+
debug "\t ------> #{best[0]} in section #{best[1]}"
|
451
|
+
@map.section = best[1]
|
452
|
+
end
|
453
|
+
return best[0]
|
454
|
+
end
|
455
|
+
|
456
|
+
#
|
457
|
+
# Determine if line corresponds to a room name
|
458
|
+
#
|
459
|
+
def roomname(line)
|
460
|
+
# Remove unwanted stuff
|
461
|
+
line.sub!(NAME_REMOVE, '')
|
462
|
+
|
463
|
+
# quick check for invalid format
|
464
|
+
return false if line =~ NAME_INVALID
|
465
|
+
return false unless line =~ /\w/
|
466
|
+
|
467
|
+
# Check word count
|
468
|
+
words = line.split(' ')
|
469
|
+
return false if words.size > NAME_MAXWORDS
|
470
|
+
|
471
|
+
# Check uncapitalized words
|
472
|
+
return false if line =~ /^[ a-z\/\\\-\(\)]/
|
473
|
+
|
474
|
+
words.each { |w|
|
475
|
+
return false if w =~ /^[a-z]/ and w.size > NAME_MAXUNCAP
|
476
|
+
}
|
477
|
+
return true
|
478
|
+
end
|
479
|
+
|
480
|
+
#
|
481
|
+
# Create a new room
|
482
|
+
#
|
483
|
+
def new_room( move, name, desc, dir = nil, from = nil, go = nil, cmd = nil )
|
484
|
+
if not from
|
485
|
+
debug "FROM undefined. Increase section #{name}"
|
486
|
+
@section += 1
|
487
|
+
@map.new_section if @section >= @map.sections.size
|
488
|
+
r = @map.new_room(0, 0)
|
489
|
+
r.name = name
|
490
|
+
else
|
491
|
+
x = from.x
|
492
|
+
y = from.y
|
493
|
+
dx, dy = Room::DIR_TO_VECTOR[dir]
|
494
|
+
x += dx
|
495
|
+
y += dy
|
496
|
+
@map.shift(x, y, dx, dy) if not @map.free?(x, y)
|
497
|
+
|
498
|
+
if not @map.free?(x, y)
|
499
|
+
raise "Error when shifting rooms"
|
500
|
+
end
|
501
|
+
|
502
|
+
debug "+++ New Room #{name} from #{from}"
|
503
|
+
r = @map.new_room(x, y)
|
504
|
+
r.name = name
|
505
|
+
c = nil
|
506
|
+
if from[dir]
|
507
|
+
# oops, we had a connection there.
|
508
|
+
c = from[dir]
|
509
|
+
b = c.roomB
|
510
|
+
if not b
|
511
|
+
# Stub connection. Update it.
|
512
|
+
debug "\tUPDATE #{c}"
|
513
|
+
odir = (dir + 4) % 8
|
514
|
+
c.roomB = r
|
515
|
+
r[odir] = c
|
516
|
+
debug "\tNOW IT IS #{c}"
|
517
|
+
elsif b.name =~ DARKNESS
|
518
|
+
# Was it a dark room that is now lit?
|
519
|
+
@map.delete_connection(c)
|
520
|
+
@map.delete_room(b)
|
521
|
+
r.darkness = true
|
522
|
+
c = nil
|
523
|
+
else
|
524
|
+
# Probably a link that is complex
|
525
|
+
# or oneway. Shift it to some other location
|
526
|
+
exitB = b.exits.rindex(c)
|
527
|
+
shift_link(from, dir)
|
528
|
+
c = nil
|
529
|
+
end
|
530
|
+
end
|
531
|
+
if c == nil
|
532
|
+
c = @map.new_connection( from, dir, r )
|
533
|
+
c.exitAtext = go if go
|
534
|
+
c.dir = Connection::AtoB
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
parse_exits(r, desc)
|
539
|
+
|
540
|
+
# Update room description
|
541
|
+
@rooms[r][:desc] = desc
|
542
|
+
return r
|
543
|
+
end
|
544
|
+
|
545
|
+
def shift_link(room, dir)
|
546
|
+
idx = dir + 1
|
547
|
+
idx = 0 if idx > 7
|
548
|
+
while idx != dir
|
549
|
+
break if not room[idx]
|
550
|
+
idx += 1
|
551
|
+
idx = 0 if idx > 7
|
552
|
+
end
|
553
|
+
if idx != dir
|
554
|
+
room[idx] = room[dir]
|
555
|
+
room[dir] = nil
|
556
|
+
# get position of other room
|
557
|
+
ox, oy = Room::DIR_TO_VECTOR[dir]
|
558
|
+
c = room[idx]
|
559
|
+
if c.roomA == room
|
560
|
+
b = c.roomB
|
561
|
+
else
|
562
|
+
b = c.roomA
|
563
|
+
end
|
564
|
+
x, y = [b.x, b.y]
|
565
|
+
x -= ox
|
566
|
+
y -= oy
|
567
|
+
dx, dy = Room::DIR_TO_VECTOR[idx]
|
568
|
+
@map.shift(x, y, -dx, -dy)
|
569
|
+
else
|
570
|
+
raise "Warning. Cannot shift connection."
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
def new_link(move, from, to, dir, go, cmd)
|
575
|
+
odir = (dir + 4) % 8
|
576
|
+
c = nil
|
577
|
+
if from[dir]
|
578
|
+
c = from[dir]
|
579
|
+
debug "\tMOVE #{c} DIR: #{dir}"
|
580
|
+
if not c.roomB
|
581
|
+
# Stub connection, fill it
|
582
|
+
c.roomB = to
|
583
|
+
debug "\tREMOVE #{to[odir]}"
|
584
|
+
@map.delete_connection(to[odir]) if to[odir] and to[odir].stub?
|
585
|
+
to[odir] = c
|
586
|
+
debug "\tREPLACED WITH #{c}"
|
587
|
+
elsif c.roomB == to
|
588
|
+
# We already went this way. Nothing to do.
|
589
|
+
debug "\tWE ALREADY PASSED THRU HERE"
|
590
|
+
elsif c.roomA == to
|
591
|
+
# We verified we can travel thru this connection in both
|
592
|
+
# directions. Change its status to both.
|
593
|
+
c.dir = Connection::BOTH
|
594
|
+
debug "\tSECTION: #{@map.section}"
|
595
|
+
debug "\tVERIFIED EXIT BOTH WAYS"
|
596
|
+
else
|
597
|
+
if c.roomA == from
|
598
|
+
b = c.roomB
|
599
|
+
if b.name =~ DARKNESS
|
600
|
+
@map.delete_connection(c)
|
601
|
+
@map.delete_room(b)
|
602
|
+
to.darkness = true
|
603
|
+
c = nil
|
604
|
+
else
|
605
|
+
dir = Room::DIRECTIONS[dir]
|
606
|
+
@map.cannot_automap "Maze detected.\n'#{from}' #{dir} leads to '#{c.roomB}',\nnot to this '#{to}'."
|
607
|
+
return nil
|
608
|
+
end
|
609
|
+
else
|
610
|
+
# We have a connection that turns. Move the link around
|
611
|
+
shift_link(from, dir)
|
612
|
+
c = nil
|
613
|
+
end
|
614
|
+
end
|
615
|
+
elsif to[odir]
|
616
|
+
c = to[odir]
|
617
|
+
if not c.roomB
|
618
|
+
debug "\tREMOVE #{to[odir]} and REPLACE with #{c}"
|
619
|
+
# Stub connection, fill it
|
620
|
+
c.roomB = from
|
621
|
+
# @map.delete_connection(from[dir]) if from[dir].stub?
|
622
|
+
from[dir] = c
|
623
|
+
c.dir = Connection::BtoA
|
624
|
+
c.exitBtext = go if go
|
625
|
+
elsif c.roomB == from
|
626
|
+
c.exitBtext = go if go
|
627
|
+
else
|
628
|
+
# We need to change odir to something else
|
629
|
+
# shift_link(to, odir)
|
630
|
+
odir = choose_dir(to, from, go)
|
631
|
+
c = nil
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
if not c
|
636
|
+
# First, check all from exits that are AtoB to see if we have one
|
637
|
+
# that goes to the room we want.
|
638
|
+
from.exits.each_with_index { |e, idx|
|
639
|
+
next unless e
|
640
|
+
if e.roomA == to and e.dir == Connection::AtoB
|
641
|
+
c = e
|
642
|
+
from[idx] = nil
|
643
|
+
end
|
644
|
+
}
|
645
|
+
if c
|
646
|
+
# If so, make that connection go both ways and attach it to
|
647
|
+
# current direction.
|
648
|
+
from[dir] = c
|
649
|
+
c.dir = Connection::BOTH
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
if not c
|
654
|
+
# No link exists -- create new one
|
655
|
+
begin
|
656
|
+
c = @map.new_connection( from, dir, to, odir )
|
657
|
+
c.exitAtext = go if go
|
658
|
+
c.dir = Connection::AtoB
|
659
|
+
rescue
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
return c
|
664
|
+
end
|
665
|
+
|
666
|
+
|
667
|
+
# Choose a direction to represent up/down/in/out.
|
668
|
+
def choose_dir(a, b, go = nil)
|
669
|
+
best = nil
|
670
|
+
bestscore = 0
|
671
|
+
if go
|
672
|
+
rgo = go % 2 == 0? go - 1 : go + 1
|
673
|
+
debug "#{Connection::EXIT_TEXT[go]} <=> #{Connection::EXIT_TEXT[rgo]}"
|
674
|
+
# First, check if room already has exit moving towards other room
|
675
|
+
a.exits.each_with_index { |e, idx|
|
676
|
+
next if not e
|
677
|
+
roomA = e.roomA
|
678
|
+
roomB = e.roomB
|
679
|
+
if roomA == a and roomB == b and e.exitBtext == rgo
|
680
|
+
e.exitAtext = go
|
681
|
+
return idx
|
682
|
+
elsif roomB == a and roomA == b and e.exitAtext == rgo
|
683
|
+
e.exitBtext = go
|
684
|
+
return idx
|
685
|
+
end
|
686
|
+
}
|
687
|
+
end
|
688
|
+
|
689
|
+
# No such luck... Pick a direction.
|
690
|
+
DIRLIST.each { |dir|
|
691
|
+
# We prefer straight directions to diagonal ones
|
692
|
+
inc = dir % 2 == 1 ? 1 : 3
|
693
|
+
rdir = (dir + 4) % 8
|
694
|
+
score = 0
|
695
|
+
# We prefer directions where both that dir and the opposite side
|
696
|
+
# are empty.
|
697
|
+
score += inc unless a[dir]
|
698
|
+
score += 1 unless a[rdir]
|
699
|
+
next if score <= bestscore
|
700
|
+
bestscore = score
|
701
|
+
best = dir
|
702
|
+
}
|
703
|
+
|
704
|
+
if bestscore == 0
|
705
|
+
$stderr.puts "No free exit for up/down"
|
706
|
+
end
|
707
|
+
|
708
|
+
return best
|
709
|
+
end
|
710
|
+
|
711
|
+
def stop
|
712
|
+
@t.stop
|
713
|
+
end
|
714
|
+
|
715
|
+
def destroy
|
716
|
+
@t.kill
|
717
|
+
@f.close
|
718
|
+
GC.start
|
719
|
+
end
|
720
|
+
|
721
|
+
def initialize(map, file)
|
722
|
+
@file = file
|
723
|
+
@map = map
|
724
|
+
@objects = {}
|
725
|
+
@moves = []
|
726
|
+
@rooms = {}
|
727
|
+
@here = nil
|
728
|
+
@section = -1
|
729
|
+
@last_obj = nil
|
730
|
+
end
|
731
|
+
|
732
|
+
def start
|
733
|
+
@f = File.open(@file, 'r')
|
734
|
+
parse
|
735
|
+
@t = Thread.new {
|
736
|
+
loop do
|
737
|
+
begin
|
738
|
+
parse_line(@f.gets)
|
739
|
+
rescue => e
|
740
|
+
puts e
|
741
|
+
puts e.backtrace
|
742
|
+
end
|
743
|
+
Thread.pass
|
744
|
+
end
|
745
|
+
}
|
746
|
+
@t.run
|
747
|
+
end
|
748
|
+
end
|