ifmapper 1.0.0 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (218) hide show
  1. data/HISTORY.txt +648 -627
  2. data/IFMapper.gemspec +29 -28
  3. data/IFMapper.rbw +31 -31
  4. data/TODO.txt +8 -7
  5. data/bin/IFMapper +31 -31
  6. data/docs/en/index.html +0 -0
  7. data/docs/en/start.html +3 -2
  8. data/docs/en/start.html~ +516 -0
  9. data/docs/es/index.html +0 -0
  10. data/docs/es/start.html +13 -14
  11. data/docs/es/start.html~ +1280 -0
  12. data/docs/images/IFMapper_main.gif +0 -0
  13. data/docs/images/automap.gif +0 -0
  14. data/docs/images/complex_connection.gif +0 -0
  15. data/docs/images/connection.gif +0 -0
  16. data/docs/images/connection_menu.gif +0 -0
  17. data/docs/images/room_description.gif +0 -0
  18. data/docs/images/room_small.gif +0 -0
  19. data/icons/copy.png +0 -0
  20. data/icons/cut.png +0 -0
  21. data/icons/filenew.png +0 -0
  22. data/icons/fileopen.png +0 -0
  23. data/icons/filesave.png +0 -0
  24. data/icons/filesaveas.png +0 -0
  25. data/icons/help.png +0 -0
  26. data/icons/kill.png +0 -0
  27. data/icons/nextpage.png +0 -0
  28. data/icons/paste.png +0 -0
  29. data/icons/prevpage.png +0 -0
  30. data/icons/printicon.png +0 -0
  31. data/icons/redo.png +0 -0
  32. data/icons/room_e.gif +0 -0
  33. data/icons/room_e.xpm +0 -0
  34. data/icons/room_n.gif +0 -0
  35. data/icons/room_n.xpm +0 -0
  36. data/icons/room_ne.gif +0 -0
  37. data/icons/room_ne.xpm +0 -0
  38. data/icons/room_nw.gif +0 -0
  39. data/icons/room_nw.xpm +0 -0
  40. data/icons/room_s.gif +0 -0
  41. data/icons/room_s.xpm +0 -0
  42. data/icons/room_se.gif +0 -0
  43. data/icons/room_se.xpm +0 -0
  44. data/icons/room_sw.gif +0 -0
  45. data/icons/room_sw.xpm +0 -0
  46. data/icons/room_w.gif +0 -0
  47. data/icons/room_w.xpm +0 -0
  48. data/icons/saveas.png +0 -0
  49. data/icons/undo.png +0 -0
  50. data/icons/winapp.png +0 -0
  51. data/icons/zoom.png +0 -0
  52. data/lib/IFMapper/AStar.rb +250 -250
  53. data/lib/IFMapper/Connection.rb +202 -202
  54. data/lib/IFMapper/FXAboutDialogBox.rb +32 -32
  55. data/lib/IFMapper/FXConnection.rb +364 -364
  56. data/lib/IFMapper/FXConnectionDialogBox.rb +124 -124
  57. data/lib/IFMapper/FXDCPostscript.rb +404 -404
  58. data/lib/IFMapper/FXDCPrint.rb +15 -15
  59. data/lib/IFMapper/FXItemList.rb +108 -0
  60. data/lib/IFMapper/FXMap.rb +2147 -2116
  61. data/lib/IFMapper/FXMapColorBox.rb +88 -88
  62. data/lib/IFMapper/FXMapDialogBox.rb +127 -127
  63. data/lib/IFMapper/FXMapFileDialog.rb +34 -34
  64. data/lib/IFMapper/FXMapperSettings.rb +206 -205
  65. data/lib/IFMapper/FXMapperWindow.rb +1592 -1571
  66. data/lib/IFMapper/FXPDFMapExporterOptionsDialogBox.rb +46 -0
  67. data/lib/IFMapper/FXRoom.rb +263 -263
  68. data/lib/IFMapper/FXRoomDialogBox.rb +159 -159
  69. data/lib/IFMapper/FXRoomList.rb +95 -95
  70. data/lib/IFMapper/FXSearchDialogBox.rb +51 -51
  71. data/lib/IFMapper/FXSection.rb +33 -33
  72. data/lib/IFMapper/FXSectionDialogBox.rb +38 -38
  73. data/lib/IFMapper/FXSpline.rb +52 -52
  74. data/lib/IFMapper/FXWarningBox.rb +51 -50
  75. data/lib/IFMapper/GUEReader.rb +445 -445
  76. data/lib/IFMapper/IFMReader.rb +584 -584
  77. data/lib/IFMapper/IFMWriter.rb +245 -227
  78. data/lib/IFMapper/Inform7Writer.rb +579 -573
  79. data/lib/IFMapper/InformReader.rb +478 -478
  80. data/lib/IFMapper/InformWriter.rb +364 -359
  81. data/lib/IFMapper/Map.rb +202 -200
  82. data/lib/IFMapper/MapPrinting.rb +162 -162
  83. data/lib/IFMapper/MapReader.rb +900 -900
  84. data/lib/IFMapper/PDFMapExporter.rb +526 -483
  85. data/lib/IFMapper/Room.rb +153 -151
  86. data/lib/IFMapper/Section.rb +234 -234
  87. data/lib/IFMapper/TADSReader.rb +474 -471
  88. data/lib/IFMapper/TADSWriter.rb +375 -370
  89. data/lib/IFMapper/TranscriptDialogBox.rb +0 -0
  90. data/lib/IFMapper/TranscriptReader.rb +1361 -1359
  91. data/lib/IFMapper/locales/en/Messages.rb +446 -435
  92. data/lib/IFMapper/locales/es/Messages.rb +451 -440
  93. data/lib/IFMapper/locales/es/Messages_iso-8859-1.rb +455 -440
  94. data/lib/IFMapper/locales/es/runme.sh +3 -3
  95. data/maps/A New Life.map b/data/maps/A New → Life.map +0 -0
  96. data/maps/AMFV.map +0 -0
  97. data/maps/AllRoads.map +0 -0
  98. data/maps/Aotearoa.map +0 -0
  99. data/maps/Bronze.map +0 -0
  100. data/maps/Bureaucracy.ifm +0 -0
  101. data/maps/Bureaucracy.map +0 -0
  102. data/maps/CityOfSecrets.map +0 -0
  103. data/maps/DDIV.map +0 -0
  104. data/maps/Following_A_Star.map +0 -0
  105. data/maps/Heated.map +0 -0
  106. data/maps/Heroine.map +0 -0
  107. data/maps/History Repeating.map b/data/maps/History → Repeating.map +0 -0
  108. data/maps/Hollywood_Hijinx.ifm +0 -0
  109. data/maps/Janitor.map +0 -0
  110. data/maps/Jigsaw.ifm +0 -0
  111. data/maps/Jigsaw.map +0 -0
  112. data/maps/LGOP.ifm +0 -0
  113. data/maps/Mercy.ifm +0 -0
  114. data/maps/Ninjas_Fate.map +0 -0
  115. data/maps/Pen_and_Paint.map +0 -0
  116. data/maps/Planetfall.ifm +0 -0
  117. data/maps/Planetfall.map +0 -0
  118. data/maps/Plundered_Hearts.ifm +0 -0
  119. data/maps/QuietEvening.map +0 -0
  120. data/maps/Ralph.ifm +0 -0
  121. data/maps/Reliques_of_Tolti_Alph.map +0 -0
  122. data/maps/Revolution.map +0 -0
  123. data/maps/Robots_of_Dawn.ifm +0 -0
  124. data/maps/SavoirFare.map +0 -0
  125. data/maps/Seastalker.ifm +0 -0
  126. data/maps/Seastalker.map +0 -0
  127. data/maps/Sherlock.ifm +0 -0
  128. data/maps/SoFar.ifm +0 -0
  129. data/maps/Starcross.ifm +0 -0
  130. data/maps/Suspended.ifm +0 -0
  131. data/maps/Tangle.map +0 -0
  132. data/maps/The_Lost_Sheep.map +0 -0
  133. data/maps/Unforgotten.map +0 -0
  134. data/maps/Warbler's Nest.map +0 -0
  135. data/maps/Warbler's_Nest.map +0 -0
  136. data/maps/Westminster_Abbey.map +0 -0
  137. data/maps/WinterWonderland.map +0 -0
  138. data/maps/Wishbringer.ifm +0 -0
  139. data/maps/Wishbringer2.ifm +0 -0
  140. data/maps/Zork1.ifm +0 -0
  141. data/maps/Zork2.ifm +0 -0
  142. data/maps/Zork3.ifm +0 -0
  143. data/maps/Zork_Zero.ifm +0 -0
  144. data/maps/anchor.ifm +0 -0
  145. data/maps/anchor.map +0 -0
  146. data/maps/atrox.ifm +0 -0
  147. data/maps/awaken.ifm +0 -0
  148. data/maps/babel.ifm +0 -0
  149. data/maps/balances.map +0 -0
  150. data/maps/ballerina.map +0 -0
  151. data/maps/bear.map +0 -0
  152. data/maps/bluechairs.map +0 -0
  153. data/maps/break_in.map +0 -0
  154. data/maps/bse.ifm +0 -0
  155. data/maps/building.map +0 -0
  156. data/maps/change.ifm +0 -0
  157. data/maps/christminster.map +0 -0
  158. data/maps/curses.ifm +0 -0
  159. data/maps/curves.ifm +0 -0
  160. data/maps/deadline.map +0 -0
  161. data/maps/delusions.map +0 -0
  162. data/maps/devours.map +0 -0
  163. data/maps/distress.map +0 -0
  164. data/maps/djinni.map +0 -0
  165. data/maps/dreamhold.map +0 -0
  166. data/maps/drift3.map +0 -0
  167. data/maps/eas.map +0 -0
  168. data/maps/eas2.map +0 -0
  169. data/maps/eas3.map +0 -0
  170. data/maps/edifice.ifm +0 -0
  171. data/maps/fallacy.map +0 -0
  172. data/maps/frozen.ifm +0 -0
  173. data/maps/gamlet.map +0 -0
  174. data/maps/glow.ifm +0 -0
  175. data/maps/guilty_bastards.map +0 -0
  176. data/maps/heist.map +0 -0
  177. data/maps/heroes.map +0 -0
  178. data/maps/inhumane.map +0 -0
  179. data/maps/kaged.map +0 -0
  180. data/maps/library.ifm +0 -0
  181. data/maps/lurkinghorror.map +0 -0
  182. data/maps/metamorphoses.map +0 -0
  183. data/maps/mindelec.ifm +0 -0
  184. data/maps/minster.ifm +0 -0
  185. data/maps/mite.map +0 -0
  186. data/maps/moonmist.map +0 -0
  187. data/maps/muldoon_legacy.map +0 -0
  188. data/maps/muse.ifm +0 -0
  189. data/maps/paperchase.ifm +0 -0
  190. data/maps/party.map +0 -0
  191. data/maps/pawn.map +0 -0
  192. data/maps/photograph.map +0 -0
  193. data/maps/pkgirl.map +0 -0
  194. data/maps/pytho.map +0 -0
  195. data/maps/risorgimento.map +0 -0
  196. data/maps/sherbet.map +0 -0
  197. data/maps/simple.map +0 -0
  198. data/maps/slouch.map +0 -0
  199. data/maps/space_st.ifm +0 -0
  200. data/maps/splashdown.map +0 -0
  201. data/maps/spring.map +0 -0
  202. data/maps/squarecircle.map +0 -0
  203. data/maps/stationfall.ifm +0 -0
  204. data/maps/theatre.ifm +0 -0
  205. data/maps/toonesia.ifm +0 -0
  206. data/maps/tortoise.ifm +0 -0
  207. data/maps/trinity.map +0 -0
  208. data/maps/vespers.map +0 -0
  209. data/maps/vgame.ifm +0 -0
  210. data/maps/wasp.map +0 -0
  211. data/maps/weather.ifm +0 -0
  212. data/maps/windhall.ifm +0 -0
  213. data/maps/worlds.map +0 -0
  214. data/maps/xtcontest.map +0 -0
  215. data/maps/zdungeon.map +0 -0
  216. data/maps/zebulon.ifm +0 -0
  217. data/maps/zerosum.map +0 -0
  218. metadata +226 -183
File without changes
@@ -1,1359 +1,1361 @@
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 and oneway exits, objects parsing and the ability to parse
11
- # transcripts as they are being spit out.
12
- #
13
- class TranscriptReader
14
-
15
- TRANSCRIPT = /^(?:Start of a transcript of|Here begins a transcript of interaction with (.*)|Here begins a transcript of interaction with)/
16
-
17
- # PROMPT = /^>\s*/
18
- LOOK = /^l(ook)?/i
19
- UNDO = /^undo$/i
20
- RESTART = /^restart$/i
21
- RESTORE = /^restore$/i
22
- IGNORE = /transcript/i
23
- OTHERS = /^(read|inventory$|i$)/i
24
- UNSCRIPT = /^unscript$/i
25
- BLANK = /^\s*$/
26
- TAKE = /^(take|get)\s+(a\s+|the\s+)?(.*)/i
27
- DROP = /^(drop|leave)\s+()/i
28
- STARTUP = /(^[A-Z]+$|Copyright|\([cC]\)\s*\d|Trademark|Release|Version|[Ss]erial [Nn]umber|Written by)/
29
- DARKNESS = /^dark(ness)?$|^in the dark$|^a dark place$|^It is pitch black|^It is too dark to see anything|^You stumble around in the dark/i
30
- DEAD = /(You die|You have died|You are dead)/i
31
- YES = /^y(es)/i
32
-
33
- THEN = /\bthen\b/i
34
-
35
- # Compass direction command -> direction mapping.
36
- DIRMAP = {
37
- "n" => 0, "north" => 0, "ne" => 1, "northeast" => 1,
38
- "e" => 2, "east" => 2, "southeast" => 3, "se" => 3,
39
- "south" => 4, "s" => 4, "southwest" => 5, "sw" => 5,
40
- "west" => 6, "w" => 6, "northwest" => 7, "nw" => 7
41
- }
42
-
43
- ODIRMAP = {"up" => 1, "u" => 1, "down" => 2, "d" => 2,
44
- "in" => 3, "out" => 4, 'enter' => 3, 'exit' => 4 }
45
-
46
- DIR_REX = '(' + DIRMAP.keys.join('|') + '|' + ODIRMAP.keys.join('|') + ')'
47
- GO = /^((walk|run|go|drive|jump)\s+)?#{DIR_REX}[.,\s]*\b/i
48
-
49
- # Direction list in order of positioning preference.
50
- DIRLIST = [ 0, 4, 2, 6, 1, 3, 5, 7 ]
51
-
52
- # No exit replies
53
- NO_EXIT = [
54
- /\byou\scan't\sgo\sthat\sway\b/i,
55
- /\byou\scan't\sgo\sin\sthat\sdirection\b/i,
56
- /\bdoorways\slead\s/i,
57
- /\bthat's\sa\swall\b/i,
58
- /\bno\sexit\b/i,
59
- /\bthere's\s+nothing\sin\sthat\sdirection\b/i,
60
- /\bblock\sthe\sway/i,
61
- /\byou\scan\sonly\sgo\s/i,
62
- /\byou\scan\sgo\sonly\s/i,
63
- /\bimpenetrable\b/i,
64
- /\bbars\syour\sway/i,
65
- /\bthere's\s.*in\s(?:your|the)\sway/i
66
- ]
67
-
68
- # Closed exit replies
69
- CLOSED_EXIT = [
70
- /door\s+is\s+closed/i,
71
- ]
72
-
73
- # remove things like (on the bed) from short room name
74
- PREPOSITION = "(?:in|on|under|behind|inside|\w+ing)"
75
- NAME_REMOVE = /(\s+\(#{PREPOSITION}\s+.+\)|,\s+#{PREPOSITION}\s+[^\,\.]+)/
76
-
77
- # not a room name if it has any of this.
78
- NAME_INVALID = /--|[:;\*\[\]\|\+\=!\.\?\000<>]/
79
-
80
- # Salutations and other words with periods that can appear as name of room
81
- SALUT = '(Mr|Mr?s|Miss|Jr|Sr|St|Dr|Ave|Inc)'
82
- SALUTATIONS = /\b#{SALUT}\./
83
-
84
- # Maximum length of room name. Otherwise, a sentence we ignore.
85
- NAME_MAXWORDS = 20
86
-
87
- # word list that may be uncapitalized in room name
88
- NAME_UNCAP = /^(?:of|on|to|with|by|a|in|the|at|under|through|near)$/i
89
-
90
- # Default room description recognition parameters.
91
- DESC_MINWORDS = 20
92
-
93
- # Ignore these words when matching words in description
94
- # To avoid cases, like: An open gate vs. A closed gate
95
- DESC_IGNORE = /(?:an|a|open(ed)?|closed?)/i
96
-
97
-
98
- RESTORE_OK = /\b(Okay|Ok|completed?|Restored)\b/
99
-
100
- ##
101
- THE = '(?:the|an|a|your|some of the|some)\s+'
102
-
103
- ## Regex to eliminate articles from get/take replies.
104
- ARTICLE = /^#{THE}/i
105
-
106
- ## Possible nessages indicating get/take succeeded
107
- TAKE_OBJ = '([\w\d\-\'\s]+)'
108
- TAKE_FROM = '(?:\s+(?:up|from|out)\s+.*)$'
109
- TAKE_ALIAS = '(?:grab|pick\s+up|pick|pilfer|take(?:\s+up)?)'
110
- TAKE_OK = [
111
- /\btaken\b/i,
112
- /you\s+have\s+now\s+(?:got\s+)?(?:#{THE})?#{TAKE_OBJ}#{TAKE_FROM}/i,
113
- /you\s+have\s+now\s+(?:got\s+)?(?:#{THE})?#{TAKE_OBJ}/i,
114
- /you\s+#{TAKE_ALIAS}\s+(?:#{THE})?#{TAKE_OBJ}#{TAKE_FROM}/i,
115
- /you\s+#{TAKE_ALIAS}\s+(?:#{THE})?#{TAKE_OBJ}/i,
116
- /you\s+now\s+have\s+(?:got\s+)?(?:#{THE})?#{TAKE_OBJ}/i,
117
- /you\s+pick\s+up\s+(?:#{THE})?#{TAKE_OBJ}/i,
118
- /you\s+are\s+now\s+holding\s+(?:#{THE})?#{TAKE_OBJ}/i,
119
- ]
120
-
121
- IT = /^(it|them)$/i
122
-
123
- @@win = nil
124
-
125
- ## Change this to non-nil to print out debugging info
126
- @@debug = nil
127
-
128
- def debug(*msg)
129
- if @@debug
130
- $stdout.puts msg
131
- end
132
- end
133
-
134
- IDENTIFY_BY_DESCRIPTION = 0
135
- IDENTIFY_BY_SHORTNAME = 1
136
-
137
- SHORTNAME_CLASSIC = 0
138
- SHORTNAME_CAPITALIZED = 1
139
- SHORTNAME_MOONMIST = 2
140
- SHORTNAME_WITNESS = 3
141
- SHORTNAME_ADRIFT = 4
142
-
143
- attr_accessor :identify, :shortName, :map
144
-
145
- def shortName=(x)
146
- @shortName = x
147
- if x == SHORTNAME_ADRIFT
148
- @prompt = /^[a-z]/
149
- else
150
- @prompt = /^>\s*/
151
- end
152
- end
153
-
154
-
155
- #
156
- # Handle a take action
157
- #
158
- def take(move, objs)
159
- return unless @here
160
- if @here.objects != '' and @here.objects[-1,1] != "\n"
161
- @here.objects << "\n"
162
- end
163
-
164
- objs.each { |cmd, dummy, obj|
165
- next if not obj or not move[:reply]
166
-
167
- objlist = obj.split(',')
168
- o = objlist[0]
169
- if objlist.size == 1 and o != 'all'
170
- # ignore 'get up'
171
- next if cmd == 'get' and (o == 'up' or o == 'off')
172
- o = @last_obj if o =~ IT
173
- next if not o
174
- if move[:reply][0] =~ /^\((.*)\)$/
175
- nobj = $1
176
- words = nobj.split(/\s+/)
177
- if words.size < 7
178
- # Acclaration, like:
179
- # > get mask
180
- # (the brown mask)
181
- # Taken.
182
- o = nobj
183
- else
184
- # too big of an aclaration... probably something like
185
- # (putting the xxx in your bag to make room)
186
- end
187
- end
188
- o.sub!(ARTICLE, '')
189
- status = move[:reply].to_s
190
- TAKE_OK.each { |re|
191
- if status =~ re then
192
- obj = $1 || o
193
- obj = o if obj =~ /\b(it|them)\b/
194
- @last_obj = obj
195
- # Take succeeded, change object o
196
- if not @objects.has_key?(obj) and
197
- not @here.objects =~ /\b#{obj}\b/
198
- @here.objects << obj << "\n"
199
- @objects[obj] = 1
200
- end
201
- break
202
- end
203
- }
204
- else
205
- # Handle multiple objects
206
- move[:reply].each { |reply|
207
- o, status = reply.split(':')
208
- next if not status
209
- next if o.split(' ').size > 6 # ignore if too many words
210
- o.sub!(ARTICLE, '')
211
- TAKE_OK.each { |re|
212
- if status =~ re then
213
- if o and not @objects.has_key?(o) and
214
- not @here.objects =~ /\b#{o}\b/
215
- @here.objects << o << "\n"
216
- @objects[o] = 1
217
- end
218
- @last_obj = o
219
- break
220
- end
221
- }
222
- }
223
- end
224
- }
225
- @here.objects.squeeze("\n")
226
- end
227
-
228
- #
229
- # Add a new room to the automapper
230
- #
231
- def add(room)
232
- @rooms << room
233
- end
234
-
235
- #
236
- # Remove some rooms from the automap knowledge (user removed these manually)
237
- #
238
- def remove(rooms)
239
- rooms.each { |r| @rooms.delete(r) }
240
- end
241
-
242
-
243
- #
244
- # Given a room description, parse it to try to find out all exits
245
- # from room.
246
- #
247
-
248
- DIRS = {
249
- # SWD added variants used in e.g. YAGWAD
250
- 'north-east' => 1, 'south-east' => 3, 'south-west' => 5, 'north-west' => 7,
251
- # Usual directions
252
- 'north' => 0, 'northeast' => 1, 'east' => 2, 'southeast' => 3,
253
- 'south' => 4, 'southwest' => 5, 'west' => 6, 'northwest' => 7,
254
- }
255
-
256
- # SWD: hack: the variants with dashes must come first
257
- # (so that regexes matches 'north-east' in preference to 'north'),
258
- # so we manually prepend their names. The duplication is harmless,
259
- # it just makes the regexes slightly slower in the non-match case.
260
- DIR = '(' + DIRS.keys.join('\b|') + ')'
261
- OR = '(?:and|or)'
262
-
263
- EXITS_REGEX =
264
- [
265
- # paths lead west and north
266
- /(run|bears|lead|wander|winds|go|continues)\s+#{DIR}\s+#{OR}\s+#{DIR}\b/i,
267
- # to the east or west
268
- /to\s+the#{DIR}\s+#{OR}\s+(to\s+the\s+)?#{DIR}\b/i,
269
- # You can go south
270
- /you\s+can\s+go#{DIR}\b/i,
271
- # to the east
272
- /to\s+the\s+#{DIR}\b/i,
273
- # east-west corridor
274
- /[\.\s+]#{DIR}[\/-]#{DIR}\s+(passage|corridor)/i,
275
- # East is the postoffice
276
- /\b#{DIR}\s+is/i,
277
- # Directly north
278
- /\bDirectly\s+#{DIR}/i,
279
- # continues|lies|etc... east
280
- /(runs|bears|leads|heads|opens|winds|continues\s+on|continues|branches|lies|wanders|bends|curves)\s+#{DIR}\b/i,
281
- /(running|leading|heading|opening|branching|lying|wandering|looking|bending)\s+#{DIR}\b/i,
282
- ]
283
-
284
- # passage westwards
285
- # gap in the northeast corner
286
-
287
-
288
- EXITS_SPECIAL = {
289
- /four\s+directions/i => [0, 2, 4, 6],
290
- /four\s+compass\s+points/i => [0, 2, 4, 6],
291
- /all\s+directions/i => [0, 1, 2, 3, 4, 5, 6, 7],
292
- }
293
-
294
- def parse_exits(r, desc)
295
- return if not desc
296
- exits = []
297
-
298
- # Now, start searching for stuff
299
-
300
- # First try the special directions...
301
- EXITS_SPECIAL.each { |re, dirs|
302
- if desc =~ re
303
- exits += dirs
304
- break
305
- end
306
- }
307
-
308
- # If that failed, start searching for exits
309
- if exits.empty?
310
- EXITS_REGEX.each { |re|
311
- matches = desc.scan(re)
312
- next if matches.empty?
313
- matches.each { |arr|
314
- arr.each { |m|
315
- next unless DIRS[m]
316
- exits << DIRS[m]
317
- }
318
- }
319
- }
320
- end
321
-
322
- exits.uniq!
323
-
324
- # Add a 'stub' for the new connection
325
- exits.each { |exit|
326
- next if r[exit]
327
- begin
328
- c = @map.new_connection( r, exit, nil )
329
- c.dir = Connection::AtoB
330
- debug "\tADDED STUB #{c}"
331
- rescue ConnectionError
332
- end
333
- }
334
- end
335
-
336
-
337
- def parse_line(line)
338
- return unless line
339
- @map.mutex.synchronize do
340
- _parse_line(line)
341
- end
342
-
343
- if @map.kind_of?(FXMap)
344
- @map.clear_selection
345
- @here.selected = true if @here
346
- @map.create_pathmap
347
- @map.zoom = @map.zoom
348
- @map.center_view_on_room(@here) if @here
349
- @map.draw
350
- end
351
- end
352
-
353
- def _parse_line(line)
354
- @moves.clear
355
-
356
- #
357
- # Read all commands
358
- #
359
- loop do
360
- line.sub!(@prompt, '') if @prompt.to_s =~ />/
361
- line.chop!
362
- line.sub!(/\s+$/, '')
363
- cmd = line
364
-
365
- # Read reply
366
- reply = []
367
- while line = @f.gets
368
- break if line =~ @prompt
369
- line.chop!
370
- line.sub!(/\s+$/,'')
371
- reply << line
372
- end
373
-
374
- if cmd =~ UNDO
375
- if @moves.size < 2
376
- @here = nil # out of moves, no clue where we are
377
- else
378
- @moves.pop
379
- end
380
- else
381
- move = { }
382
-
383
- # Replace all 'THEN' in command for periods
384
- cmd.sub!(THEN, '.')
385
- # and all ANDs for commas
386
- cmd.sub!(/\band\b/i,',')
387
- # and multiple periods for just one
388
- cmd.sub!(/\s*\.+\s*/, '.')
389
-
390
- move[:cmd] = cmd
391
- move[:reply] = reply
392
- @moves << move
393
- end
394
-
395
- break if not line
396
-
397
- end
398
-
399
-
400
- # Step 2
401
- @restart = false
402
- tele = nil
403
- @moves.each { |move|
404
- cmd = move[:cmd]
405
- next if cmd =~ IGNORE or cmd =~ OTHERS
406
- if cmd =~ UNSCRIPT
407
- tele = 1
408
- @here = nil
409
- next
410
- end
411
-
412
- if @restart and cmd =~ YES
413
- @here = nil
414
- else
415
- @restart = false
416
- tele = nil
417
- end
418
-
419
- if cmd =~ RESTART
420
- @restart = true
421
- tele = 1
422
- end
423
-
424
- if cmd =~ RESTORE
425
- if move[:reply].join() =~ RESTORE_OK
426
- @here = nil
427
- tele = 1
428
- else
429
- next
430
- end
431
- end
432
-
433
- name = nil
434
- desc = ''
435
- roomflag = false
436
- desc_gap = false
437
- startup = false
438
- rooms = []
439
- move[:reply].each { |r|
440
- tele = 1 if r =~ DEAD
441
-
442
- if r =~ STARTUP
443
- # Dealing with a startup message, such as:
444
- # MY GAME
445
- # Copyright (C) 1984 ComputerQuest
446
- # We skip the whole thing until the next blank line
447
- debug "#{r} skipped due to startup"
448
- startup = true
449
- desc = ''
450
- name = nil
451
- roomflag = false
452
- desc_gap = false
453
- @here = nil
454
- tele = 1
455
- next
456
- end
457
- next if startup and r !~ BLANK
458
- startup = false
459
-
460
- if not roomflag and r !~ BLANK and n = room_name(r)
461
- debug "Found room #{n}"
462
- roomflag = true
463
- desc_gap = false
464
- name = n
465
- desc = ''
466
- next
467
- end
468
-
469
- if not desc_gap and roomflag and r =~ BLANK and desc == ''
470
- desc_gap = true
471
- next
472
- end
473
-
474
-
475
- if roomflag and r !~ BLANK
476
- desc << r << "\n"
477
- next
478
- end
479
-
480
- if r =~ BLANK and roomflag
481
- if desc.count("\n") == 1 and desc =~ /\?$/
482
- # A "What next?" type of prompt, not a room description
483
- desc = ''
484
- end
485
- if desc == ''
486
- desc = nil
487
- else
488
- desc.gsub!(/\n/, ' ')
489
- desc.strip!
490
- end
491
-
492
- rooms << {
493
- :name => name,
494
- :desc => desc,
495
- :tele => tele
496
- }
497
- roomflag = false
498
- desc_gap = false
499
- name = nil
500
- desc = ''
501
- tele = nil
502
- end
503
- }
504
-
505
- # Oops, there was no newline between room description and prompt and
506
- # we missed the last room.
507
- # This happens for example with Magnetic Scrolls transcripts.
508
- if name
509
- rooms << {
510
- :name => name,
511
- :desc => desc,
512
- :tele => tele
513
- }
514
- end
515
-
516
- if not rooms.empty?
517
- move[:rooms] = rooms
518
- move[:look] = true if cmd =~ LOOK
519
- move[:reply] = nil
520
- end
521
- }
522
-
523
-
524
- @moves.each { |move|
525
- cmd = move[:cmd]
526
- if objs = cmd.scan(TAKE)
527
- take(move, objs)
528
- end
529
-
530
- if not move[:rooms]
531
- # Check if a moving command failed
532
- if cmd =~ GO
533
- dir = $3
534
- # See if a stub direction exits in that direction
535
- if @here and DIRMAP[dir] then
536
- dir = DIRMAP[dir]
537
- next unless @here[dir]
538
-
539
- if @here[dir].stub?
540
- # Check if reply was that there's no exit there.
541
- NO_EXIT.each { |re|
542
- if move[:reply][0] =~ re
543
- # If so, remove it... automapper got it wrong. Not an exit.
544
- @map.delete_connection(@here[dir])
545
- break
546
- end
547
- }
548
- end
549
-
550
- # Check if there is a closed door
551
- if @here[dir] and @here[dir].type == Connection::FREE
552
- CLOSED_EXIT.each { |re|
553
- if move[:reply][0] =~ re
554
- # If so, flag it
555
- @here[dir].type = Connection::CLOSED_DOOR
556
- break
557
- end
558
- }
559
- end
560
- end
561
- end
562
- next
563
- end
564
-
565
- move[:rooms].each { |room|
566
- name = room[:name]
567
- debug "SECTION: #{@map.section}"
568
- debug "HERE IS: #{@here}"
569
- debug "CMD: #{cmd}"
570
- debug "ENDS AT: #{name}"
571
-
572
- desc = room[:desc]
573
- desc.gsub!(/(\w)\s*\n/, '\1 ')
574
-
575
- line = move[:line]
576
-
577
- # If we teleported, try to find room
578
- if room[:tele]
579
- debug "\t ****TELEPORT TO #{name}****"
580
- @here = find_room(name, desc)
581
- debug "\t TO: #{@here}"
582
- end
583
-
584
- # Make sure the user has not deleted the current room from map
585
- # and that we are in the proper section.
586
- if @here
587
- found = false
588
- @map.sections.each_with_index { |sect, idx|
589
- if sect.rooms.include?(@here)
590
- found = true
591
- if idx != @map.section
592
- @map.fit
593
- @map.section = idx
594
- end
595
- break
596
- end
597
- }
598
- @here = nil if not found
599
- end
600
-
601
- # If it is a look command or we don't know where we are yet,
602
- # set current room.
603
- if move[:look] or not @here
604
- if move[:look] and name =~ DARKNESS
605
- @here = find_room(name, desc) if @here and not @here.darkness
606
- else
607
- @here = find_room(name, desc)
608
- end
609
- @here = new_room(move, name, desc) unless @here
610
- @here.selected = true
611
- next
612
- end
613
-
614
- cmd.sub!(GO, '')
615
- dir = $3
616
-
617
- # Swallow everything until next command (separated by .)
618
- cmd.sub!(/.*\./, '')
619
-
620
- debug "MOVED IN DIRECTION: #{dir} CMD LEFT:#{cmd}"
621
- if not cmd
622
- debug "Oops... run out of commands."
623
- break
624
- end
625
-
626
- sect = @map.section
627
-
628
- # Otherwise, assume we moved in some way. Try to find the new room.
629
- if name =~ DARKNESS
630
- idx = DIRMAP[dir] || ODIRMAP[dir]
631
- # Moving to a dark room is special. We verify the exit to see
632
- # if it was already moving to a dark room (in which case it
633
- # becomes "there") or if it was moving to some other room (in
634
- # which case we flag it as dark and make it there)
635
- if idx and @here[idx]
636
- c = @here[idx]
637
-
638
- if c.roomA == @here
639
- b = c.roomB
640
- else
641
- b = c.roomA
642
- end
643
-
644
- if not b
645
- there = nil
646
- else
647
- if b.name !~ DARKNESS
648
- b.darkness = true
649
- end
650
- there = b
651
- end
652
- else
653
- # No connection yet, create dark room
654
- there = nil
655
- end
656
- else
657
- there = find_room(name, desc)
658
- end
659
-
660
- next if there == @here
661
-
662
-
663
- go = nil
664
- if DIRMAP[dir]
665
- dir = DIRMAP[dir]
666
- elsif ODIRMAP[dir]
667
- go = ODIRMAP[dir]
668
- debug "#{dir} move #{go}"
669
- dir = choose_dir(@here, there, go)
670
- else
671
- # special move --- teleport/death/etc.
672
- go = 0
673
- dir = choose_dir(@here, there, go)
674
- end
675
- debug "MOVED IN DIR INDEX: #{dir}"
676
-
677
- @here.selected = false
678
- if not there
679
- # Unvisited -- new room
680
- @here = new_room(move, name, desc, dir, @here, go)
681
- else
682
- # Visited before -- new link
683
- if sect == @map.section # don't conn
684
- new_link(move, @here, there, dir, go)
685
- end
686
- @here = there
687
- end
688
- }
689
- }
690
-
691
- @moves.clear
692
- @map.fit
693
- if @map.kind_of?(FXMap)
694
- end
695
- end
696
-
697
- def find_room(name, desc)
698
- case @identify
699
- when IDENTIFY_BY_DESCRIPTION
700
- return find_room_by_desc(name, desc)
701
- when IDENTIFY_BY_SHORTNAME
702
- return find_room_by_name(name, desc)
703
- end
704
- end
705
-
706
- def find_room_by_name(name, desc)
707
- bestscore = 0
708
- best = nil
709
-
710
- @map.sections.each_with_index { |sect, idx|
711
- sect.rooms.each { |room|
712
- score = 0
713
- score += 1 if room.name == name
714
-
715
- if score == 1 and desc and room.desc
716
- # We have a description...
717
- # Try exact description match first
718
- score += 100 if room.desc == desc
719
-
720
- # Try substring match
721
- score += 50 if room.desc.index(desc)
722
-
723
- # If we have a room where both name and desc match,
724
- # we get a better score than just description only.
725
- # This is to help, for example, Trinity, where two rooms have
726
- # the exact description but different name.
727
- score += 1 if room.name == name and score > 0
728
-
729
- # If still no luck, try first N words
730
- if score == 1
731
- dwords = room.desc.split(' ')
732
- words = desc.split(' ')
733
- match = true
734
- count = 0
735
- 0.upto(desc.size) { |i|
736
- # Ignore some words (like open/close, which may just mean
737
- # some doors changed state)
738
- next if words[i] =~ DESC_IGNORE
739
-
740
- if words[i] != dwords[i]
741
- if count < DESC_MINWORDS
742
- match = false
743
- break
744
- else
745
- next
746
- end
747
- end
748
- count += 1
749
- }
750
-
751
- score += count if match
752
- end
753
- end
754
- next if score <= bestscore
755
- bestscore = score
756
- best = [room, idx]
757
- }
758
- }
759
-
760
- return nil if not best
761
-
762
- # Make sure we are in the right section
763
- if best[1] != @map.section
764
- @map.fit
765
- @map.section = best[1]
766
- end
767
- if desc and (not best[0].desc or best[0].desc == '')
768
- best[0].desc = desc
769
- end
770
- return best[0]
771
- end
772
-
773
- def find_room_by_desc(name, desc)
774
- bestscore = 0
775
- best = nil
776
-
777
- @map.sections.each_with_index { |sect, idx|
778
- sect.rooms.each { |room|
779
- score = 0
780
-
781
- if desc and room.desc
782
- # We have a description...
783
- # Try exact description match first
784
- score += 100 if room.desc == desc
785
-
786
- # Try substring match
787
- score += 50 if room.desc.index(desc)
788
-
789
- # If we have a room where both name and desc match,
790
- # we get a better score than just description only.
791
- # This is to help, for example, Trinity, where two rooms have
792
- # the exact description but different name.
793
- score += 1 if room.name == name and score > 0
794
-
795
- # If still no luck, try first N words
796
- if score == 0
797
- dwords = room.desc.split(' ')
798
- words = desc.split(' ')
799
- match = true
800
- count = 0
801
- 0.upto(desc.size) { |i|
802
- # Ignore some words (like open/close, which may just mean
803
- # some doors changed state)
804
- next if words[i] =~ DESC_IGNORE
805
-
806
- if words[i] != dwords[i]
807
- if count < DESC_MINWORDS
808
- match = false
809
- break
810
- else
811
- next
812
- end
813
- end
814
- count += 1
815
- }
816
-
817
- score += count if match and room.name == name
818
- end
819
- else
820
- # Just the name, not so good
821
- score += 1 if room.name == name
822
- end
823
- next if score <= bestscore
824
- bestscore = score
825
- best = [room, idx]
826
- }
827
- }
828
-
829
- return nil if not best
830
-
831
- # Make sure we are in the right section
832
- if best[1] != @map.section
833
- @map.fit
834
- @map.section = best[1]
835
- end
836
- if desc and (not best[0].desc or best[0].desc == '')
837
- best[0].desc = desc
838
- end
839
- return best[0]
840
- end
841
-
842
- def room_name(line)
843
- case @shortName
844
- when SHORTNAME_CAPITALIZED
845
- return room_name_classic(line, false)
846
- when SHORTNAME_MOONMIST
847
- return room_name_moonmist(line)
848
- when SHORTNAME_WITNESS
849
- return room_name_witness(line)
850
- else
851
- return room_name_classic(line, true)
852
- end
853
- end
854
-
855
- def capitalize_room(line)
856
- words = line.split(' ')
857
- words.each_with_index { |w, idx|
858
- if idx > 0 and w =~ NAME_UNCAP
859
- w.downcase!
860
- else
861
- w.capitalize!
862
- end
863
- }
864
- return words.join(' ')
865
- end
866
-
867
- def room_name_alan(line)
868
- return false if line !~ /^[^.]+\.$/
869
- line.sub!(/\.$/, '')
870
- return room_name_classic(line, true)
871
- end
872
-
873
- def room_name_witness(line)
874
- if line =~ /^You\sare\s(?:now\s)?(?:[io]n\s)?(?:#{THE})?([\w'\d\s\-_]+)\.$/
875
- return false if $1 =~ /own feet/
876
- return $1
877
- elsif line =~ /^\(([\w\d\s_]+)\)$/
878
- return $1
879
- else
880
- return false
881
- end
882
- end
883
-
884
- def room_name_moonmist(line)
885
- return false if line =~ /^\(You are not holding it.\)$/
886
- if line =~ /^\(You\sare\s(?:now\s)?(?:[io]n\s)?(?:#{THE})?([\w'\d\s\-_]+)\.\)$/
887
- return capitalize_room( $1 )
888
- else
889
- return false
890
- end
891
- end
892
-
893
- #
894
- # Determine if line corresponds to a room name
895
- #
896
- def room_name_classic(line, all_capitals = true)
897
- # Check if user/game has created a room with that name already
898
- return line if find_room(line, nil)
899
-
900
- # We have a room if we match darkness
901
- return line if line =~ DARKNESS
902
-
903
- # Remove unwanted stuff line (on the bed)
904
- line.sub!(NAME_REMOVE, '')
905
-
906
- # Check if user/game has created a room with that name already
907
- return line if find_room(line, nil)
908
-
909
- # Remove periods from salutations
910
- line.sub!(SALUTATIONS, '\1')
911
-
912
- # quick check for invalid format
913
- return false if line =~ NAME_INVALID
914
-
915
- # Qucik check for word characters
916
- return false unless line =~ /\w/
917
-
918
- # Check if we start line with uncapitalized words or symbols
919
- return false if line =~ /^[ a-z\/\\\-\(\)']/
920
-
921
- # Check if line holds only capitalized words or several spaces together
922
- # or a quote or a 1) line. If so, not a room.
923
- return false if line =~ /^[A-Z\d,\.\/\-"'\s]+$/ or line =~ /\s\s/ or
924
- line =~ /^".*"$/ or line =~ /^"[^"]+$/ or line =~ /^\d+\)/
925
-
926
- # Check word count (if too many, not a room)
927
- words = line.split(' ')
928
- return false if words.size > NAME_MAXWORDS
929
-
930
- return false if not all_capitals and words.size > 6
931
-
932
- # If not, check all words of 4 chars or more are capitalized
933
- # and that there are no 3 or more short letter words together
934
- # (which means a diagram)
935
- num = 0
936
- words.each { |w|
937
- return false if all_capitals and w =~ /^[a-z]/ and w !~ NAME_UNCAP
938
- if w.size <= 2
939
- num += 1
940
- return false if num > 2
941
- else
942
- num = 0
943
- end
944
- }
945
-
946
- # Restore period to salutations
947
- line.sub!(/\b#{SALUT}\b/, '\1.')
948
-
949
- # Okay, it is a room.
950
- return line
951
- end
952
-
953
- #
954
- # Create a new room
955
- #
956
- def new_room( move, name, desc, dir = nil, from = nil, go = nil )
957
- b = nil
958
- dark = false
959
-
960
- if not from
961
- debug "FROM undefined. Increase section #{name}"
962
- @section += 1
963
- @map.new_section if @section >= @map.sections.size
964
- r = @map.new_room(0, 0)
965
- r.name = name
966
- else
967
-
968
- darkexits = []
969
- if name =~ DARKNESS
970
- dark = true
971
- else
972
- dark = false
973
- if from[dir]
974
- # oops, we had a connection there.
975
- c = from[dir]
976
- b = c.opposite(from)
977
- if b and b.name =~ DARKNESS
978
- # Was it a dark room that is now lit?
979
- # if so, set dark flag and delete dark room
980
- @map.delete_connection(c)
981
- @map.delete_room_only(b)
982
- dark = true
983
- c = nil
984
- end
985
- end
986
- end
987
-
988
- x = from.x
989
- y = from.y
990
- dx, dy = Room::DIR_TO_VECTOR[dir]
991
- x += dx
992
- y += dy
993
- @map.shift(x, y, dx, dy) if not @map.free?(x, y)
994
-
995
-
996
- debug "+++ New Room #{name} from #{from}"
997
- r = @map.new_room(x, y)
998
- r.name = name
999
- r.darkness = dark
1000
- if b and dark
1001
- b.exits.each_with_index { |de, didx|
1002
- next unless de
1003
- @map.sections[@map.section].connections << de
1004
- r[didx] = de
1005
- if de.roomA == b
1006
- de.roomA = r
1007
- else
1008
- de.roomB = r
1009
- end
1010
- }
1011
- end
1012
- c = nil
1013
- if from[dir]
1014
- # oops, we had a connection there.
1015
- c = from[dir]
1016
- b = c.roomB
1017
- if c.stub?
1018
- # Stub connection. Update it.
1019
- debug "\tUPDATE #{c}"
1020
- odir = (dir + 4) % 8
1021
- c.exitAtext = go if go
1022
- c.type = Connection::SPECIAL if go == 0
1023
- c.roomB = r
1024
- r[odir] = c
1025
- debug "\tNOW IT IS #{c}"
1026
- else
1027
- # Probably a link that is complex
1028
- # or oneway. Shift it to some other location
1029
- shift_link(from, dir)
1030
- c = nil
1031
- end
1032
- end
1033
- if c == nil
1034
- begin
1035
- c = @map.new_connection( from, dir, r )
1036
- c.exitAtext = go if go
1037
- c.type = Connection::SPECIAL if go == 0
1038
- c.dir = Connection::AtoB
1039
- rescue Section::ConnectionError
1040
- end
1041
- end
1042
- end
1043
-
1044
- parse_exits(r, desc)
1045
-
1046
- # Update room description
1047
- r.desc = desc
1048
- return r
1049
- end
1050
-
1051
- def shift_link(room, dir)
1052
- idx = dir + 1
1053
- idx = 0 if idx > 7
1054
- while idx != dir
1055
- break if not room[idx]
1056
- idx += 1
1057
- idx = 0 if idx > 7
1058
- end
1059
- if idx != dir
1060
- room[idx] = room[dir]
1061
- room[dir] = nil
1062
- # get position of other room
1063
- ox, oy = Room::DIR_TO_VECTOR[dir]
1064
- c = room[idx]
1065
- if c.roomA == room
1066
- b = c.roomB
1067
- else
1068
- b = c.roomA
1069
- end
1070
- x, y = [b.x, b.y]
1071
- x -= ox
1072
- y -= oy
1073
- dx, dy = Room::DIR_TO_VECTOR[idx]
1074
- @map.shift(x, y, -dx, -dy)
1075
- else
1076
- debug "Warning. Cannot shift connection."
1077
- end
1078
- end
1079
-
1080
-
1081
- def new_link(move, from, to, dir, go)
1082
- odir = (dir + 4) % 8
1083
- c = nil
1084
- # If we have something in the from direction
1085
- if from[dir]
1086
- c = from[dir]
1087
- debug "\tMOVE #{c} DIR: #{dir}"
1088
- if c.stub?
1089
- # Stub connection, fill it
1090
- c.roomB = to
1091
- c.exitAtext = go if go
1092
- c.type = Connection::SPECIAL if go == 0
1093
- # we still need to check the destination to[odir]...
1094
- if not to[odir]
1095
- # nothing to do
1096
- elsif to[odir].stub?
1097
- @map.delete_connection(to[odir])
1098
- debug "\tREMOVE #{to[odir]}"
1099
- else
1100
- # this end cannot be deleted. we need to shift odir
1101
- debug "\tCHOOSE NEW DIR for #{odir}"
1102
- rgo = nil
1103
- if go and go > 0
1104
- rgo = rgo % 2 == 0? go - 1 : go + 1
1105
- end
1106
- odir = choose_dir(to, from, rgo, dir)
1107
- @map.delete_connection(to[odir]) if to[odir]
1108
- debug "\tSHIFTED DESTINATION TO EXIT #{odir}"
1109
- end
1110
- to[odir] = c
1111
- elsif c.roomB == to
1112
- # We already went this way. Nothing to do.
1113
- debug "\tWE ALREADY PASSED THRU HERE"
1114
- elsif c.roomA == to
1115
- # We verified we can travel thru this connection in both
1116
- # directions. Change its status to both.
1117
- c.dir = Connection::BOTH
1118
- debug "\tSECTION: #{@map.section}"
1119
- debug "\tVERIFIED EXIT BOTH WAYS"
1120
- else
1121
- debug "\tOTHER"
1122
- if c.roomA == from
1123
- b = c.roomB
1124
- if b.name =~ DARKNESS
1125
- debug "*** REPLACING DARK ROOM ***"
1126
- @map.delete_connection(c)
1127
- to.darkness = true
1128
- @map.delete_room(b)
1129
- c = nil
1130
- else
1131
- if c.exitAtext != 0
1132
- # if it was an up/down/in/out dir, we shift it
1133
- shift_link(from, dir)
1134
- c = nil
1135
- else
1136
- # else, we really have a problem in the map
1137
- debug "*** CANNOT AUTOMAP --- MAZE ***"
1138
- dir = Room::DIRECTIONS[dir]
1139
- @map.cannot_automap "Maze detected.\n'#{from}' #{dir} leads to '#{c.roomB}',\nnot to this '#{to}'."
1140
- # self.stop
1141
- return nil
1142
- end
1143
- end
1144
- else
1145
- debug "SHIFT LINK #{from} #{dir}"
1146
- # We have a connection that turns. Move the link around
1147
- shift_link(from, dir)
1148
- c = nil
1149
- end
1150
- end
1151
- end
1152
-
1153
- if not c and to[odir]
1154
- c = to[odir]
1155
- if c.stub?
1156
- debug "\tREMOVE #{to[odir]} and REPLACE with #{c}"
1157
- # Stub connection, fill it
1158
- c.roomB = from
1159
- # @map.delete_connection(from[dir]) if from[dir].stub?
1160
- from[dir] = c
1161
- c.dir = Connection::BtoA
1162
- c.exitBtext = go if go
1163
- c.type = Connection::SPECIAL if go == 0
1164
- elsif c.roomB == from
1165
- c.exitBtext = go if go
1166
- c.type = Connection::SPECIAL if go == 0
1167
- else
1168
- # We need to change odir to something else
1169
- rgo = nil
1170
- if go and go > 0
1171
- rgo = go % 2 == 0? go - 1 : go + 1
1172
- end
1173
- odir = choose_dir(to, from, rgo, dir)
1174
- @map.delete_connection(to[odir]) if to[odir] and to[odir].stub?
1175
- c = nil
1176
- end
1177
- end
1178
-
1179
- if not c
1180
- # First, check all from exits that are AtoB to see if we have one
1181
- # that goes to the room we want.
1182
- from.exits.each_with_index { |e, idx|
1183
- next unless e
1184
- if e.roomA == to and e.dir == Connection::AtoB
1185
- c = e
1186
- from[idx] = nil
1187
- end
1188
- }
1189
- if c
1190
- # If so, make that connection go both ways and attach it to
1191
- # current direction.
1192
- from[dir] = c
1193
- c.dir = Connection::BOTH
1194
- c.exitAtext = go if go
1195
- c.type = Connection::SPECIAL if go == 0
1196
- end
1197
- end
1198
-
1199
- if not c
1200
- # No link exists -- create new one.
1201
- begin
1202
- c = @map.new_connection( from, dir, to, odir )
1203
- c.exitAtext = go if go
1204
- c.dir = Connection::AtoB
1205
- rescue Section::ConnectionError
1206
- end
1207
- end
1208
-
1209
- return c
1210
- end
1211
-
1212
-
1213
- # Choose a direction to represent up/down/in/out.
1214
- def choose_dir(a, b, go = nil, exitB = nil)
1215
- # Don't add a new connection if we already have a normal connection
1216
- # to the room and we are moving up/down/etc.
1217
- if go
1218
- rgo = go % 2 == 0? go - 1 : go + 1
1219
- debug "#{Connection::EXIT_TEXT[go]} <=> #{Connection::EXIT_TEXT[rgo]}"
1220
- # First, check if room already has exit moving towards other room
1221
- a.exits.each_with_index { |e, idx|
1222
- next if not e
1223
- roomA = e.roomA
1224
- roomB = e.roomB
1225
- if roomA == a and roomB == b
1226
- e.exitAtext = go if e.exitBtext == rgo
1227
- return idx
1228
- elsif roomB == a and roomA == b
1229
- e.exitBtext = go if e.exitAtext == rgo
1230
- return idx
1231
- end
1232
- }
1233
- end
1234
-
1235
- # We prefer directions that travel less... so we need to figure
1236
- # out where we start from...
1237
- if b
1238
- x = b.x
1239
- y = b.y
1240
- else
1241
- x = a.x
1242
- y = a.y
1243
- end
1244
- if exitB
1245
- dx, dy = Room::DIR_TO_VECTOR[exitB]
1246
- x += dx
1247
- y += dy
1248
- end
1249
-
1250
- # No such luck... Pick a direction.
1251
- best = nil
1252
- bestscore = nil
1253
-
1254
- DIRLIST.each { |dir|
1255
- # We prefer straight directions to diagonal ones
1256
- inc = dir % 2 == 1 ? 10 : 14
1257
- score = 1000
1258
- # We prefer directions where both that dir and the opposite side
1259
- # are empty.
1260
- if (not a[dir]) or a[dir].stub?
1261
- score += inc
1262
- score += 4 if a[dir] #attaching to stubs is better
1263
- end
1264
- # rdir = (dir + 4) % 8
1265
- # score += 1 unless a[rdir]
1266
-
1267
- # Measure distance for that exit, we prefer shorter
1268
- # paths
1269
- dx, dy = Room::DIR_TO_VECTOR[dir]
1270
- dx = (a.x + dx) - x
1271
- dy = (a.y + dy) - y
1272
- d = dx * dx + dy * dy
1273
- score -= d
1274
- next if bestscore and score <= bestscore
1275
- bestscore = score
1276
- best = dir
1277
- }
1278
-
1279
- if not bestscore
1280
- raise "No free exit for choose_dir"
1281
- end
1282
-
1283
- return best
1284
- end
1285
-
1286
- def stop
1287
- @t.kill if @t
1288
- end
1289
-
1290
- def destroy
1291
- @t.kill if @t
1292
- @f.close if @f
1293
- GC.start
1294
- end
1295
-
1296
-
1297
- def properties(modal = false)
1298
- require 'IFMapper/TranscriptDialogBox'
1299
- if not @@win
1300
- @@win = TranscriptDialogBox.new(self)
1301
- else
1302
- @@win.copy_from(self)
1303
- end
1304
- if modal
1305
- @@win.execute
1306
- end
1307
- end
1308
-
1309
- def initialize(map, file)
1310
- @shortName = 0
1311
- @prompt = /^>\s*/
1312
- @identify = 0
1313
-
1314
- @file = file
1315
- @map = map
1316
- @objects = {}
1317
- @moves = []
1318
- @here = nil
1319
- @section = -1
1320
- @last_obj = nil
1321
- end
1322
-
1323
- # Step one user command at a time
1324
- def step
1325
- begin
1326
- parse_line(@f.gets)
1327
- rescue => e
1328
- $stderr.puts e
1329
- $stderr.puts e.backtrace
1330
- end
1331
- end
1332
-
1333
- def start
1334
- if not @f
1335
- @f = File.open(@file, 'r')
1336
- while line = @f.gets
1337
- if @map.name =~ /^Empty Map/ and line =~ TRANSCRIPT
1338
- if $1
1339
- @map.name = $1
1340
- else
1341
- @map.name = @f.gets.strip
1342
- end
1343
- @map.name = capitalize_room(@map.name)
1344
- end
1345
- break if @prompt =~ line
1346
- end
1347
- parse_line(line)
1348
- end
1349
-
1350
- @t = Thread.new {
1351
- loop do
1352
- self.step
1353
- Thread.pass
1354
- sleep 3
1355
- end
1356
- }
1357
- @t.run
1358
- end
1359
- end
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 and oneway exits, objects parsing and the ability to parse
11
+ # transcripts as they are being spit out.
12
+ #
13
+ class TranscriptReader
14
+
15
+ TRANSCRIPT = /^(?:Start of a transcript of|Here begins a transcript of interaction with (.*)|Here begins a transcript of interaction with)/
16
+
17
+ # PROMPT = /^>\s*/
18
+ LOOK = /^l(ook)?/i
19
+ UNDO = /^undo$/i
20
+ RESTART = /^restart$/i
21
+ RESTORE = /^restore$/i
22
+ IGNORE = /transcript/i
23
+ OTHERS = /^(read|inventory$|i$)/i
24
+ UNSCRIPT = /^unscript$/i
25
+ BLANK = /^\s*$/
26
+ TAKE = /^(take|get)\s+(a\s+|the\s+)?(.*)/i
27
+ DROP = /^(drop|leave)\s+()/i
28
+ STARTUP = /(^[A-Z]+$|Copyright|\([cC]\)\s*\d|Trademark|Release|Version|[Ss]erial [Nn]umber|Written by)/
29
+ DARKNESS = /^dark(ness)?$|^in the dark$|^a dark place$|^It is pitch black|^It is too dark to see anything|^You stumble around in the dark/i
30
+ DEAD = /(You die|You have died|You are dead)/i
31
+ YES = /^y(es)/i
32
+
33
+ THEN = /\bthen\b/i
34
+
35
+ # Compass direction command -> direction mapping.
36
+ DIRMAP = {
37
+ "n" => 0, "north" => 0, "ne" => 1, "northeast" => 1,
38
+ "e" => 2, "east" => 2, "southeast" => 3, "se" => 3,
39
+ "south" => 4, "s" => 4, "southwest" => 5, "sw" => 5,
40
+ "west" => 6, "w" => 6, "northwest" => 7, "nw" => 7
41
+ }
42
+
43
+ ODIRMAP = {"up" => 1, "u" => 1, "down" => 2, "d" => 2,
44
+ "in" => 3, "out" => 4, 'enter' => 3, 'exit' => 4 }
45
+
46
+ DIR_REX = '(' + DIRMAP.keys.join('|') + '|' + ODIRMAP.keys.join('|') + ')'
47
+ GO = /^((walk|run|go|drive|jump)\s+)?#{DIR_REX}[.,\s]*\b/i
48
+
49
+ # Direction list in order of positioning preference.
50
+ DIRLIST = [ 0, 4, 2, 6, 1, 3, 5, 7 ]
51
+
52
+ # No exit replies
53
+ NO_EXIT = [
54
+ /\byou\scan't\sgo\sthat\sway\b/i,
55
+ /\byou\scan't\sgo\sin\sthat\sdirection\b/i,
56
+ /\bdoorways\slead\s/i,
57
+ /\bthat's\sa\swall\b/i,
58
+ /\bno\sexit\b/i,
59
+ /\bthere's\s+nothing\sin\sthat\sdirection\b/i,
60
+ /\bblock\sthe\sway/i,
61
+ /\byou\scan\sonly\sgo\s/i,
62
+ /\byou\scan\sgo\sonly\s/i,
63
+ /\bimpenetrable\b/i,
64
+ /\bbars\syour\sway/i,
65
+ /\bthere's\s.*in\s(?:your|the)\sway/i
66
+ ]
67
+
68
+ # Closed exit replies
69
+ CLOSED_EXIT = [
70
+ /door\s+is\s+closed/i,
71
+ ]
72
+
73
+ # remove things like (on the bed) from short room name
74
+ PREPOSITION = "(?:in|on|under|behind|inside|along|\w+ing)"
75
+ NAME_REMOVE = /(\s+\(#{PREPOSITION}\s+.+\)|,\s+#{PREPOSITION}\s+[^\,\.]+)/
76
+
77
+ # not a room name if it has any of this.
78
+ NAME_INVALID = /--|[:;\*\[\]\|\+\=!\.\?\000<>]/
79
+
80
+ # Salutations and other words with periods that can appear as name of room
81
+ SALUT = '(Mr|Mr?s|Miss|Jr|Sr|St|Dr|Ave|Inc)'
82
+ SALUTATIONS = /\b#{SALUT}\./
83
+
84
+ # Maximum length of room name. Otherwise, a sentence we ignore.
85
+ NAME_MAXWORDS = 20
86
+
87
+ # word list that may be uncapitalized in room name
88
+ NAME_UNCAP = /^(?:of|on|to|with|by|a|in|the|at|under|through|near)$/i
89
+
90
+ # Default room description recognition parameters.
91
+ DESC_MINWORDS = 20
92
+
93
+ # Ignore these words when matching words in description
94
+ # To avoid cases, like: An open gate vs. A closed gate
95
+ DESC_IGNORE = /(?:an|a|open(ed)?|closed?)/i
96
+
97
+
98
+ RESTORE_OK = /\b(Okay|Ok|completed?|Restored)\b/
99
+
100
+ ##
101
+ THE = '(?:the|an|a|your|some of the|some)\s+'
102
+
103
+ ## Regex to eliminate articles from get/take replies.
104
+ ARTICLE = /^#{THE}/i
105
+
106
+ ## Possible nessages indicating get/take succeeded
107
+ TAKE_OBJ = '([\w\d\-\'\s]+)'
108
+ TAKE_FROM = '(?:\s+(?:up|from|out)\s+.*)$'
109
+ TAKE_ALIAS = '(?:grab|pick\s+up|pick|pilfer|take(?:\s+up)?)'
110
+ TAKE_OK = [
111
+ /\btaken\b/i,
112
+ /you\s+have\s+now\s+(?:got\s+)?(?:#{THE})?#{TAKE_OBJ}#{TAKE_FROM}/i,
113
+ /you\s+have\s+now\s+(?:got\s+)?(?:#{THE})?#{TAKE_OBJ}/i,
114
+ /you\s+#{TAKE_ALIAS}\s+(?:#{THE})?#{TAKE_OBJ}#{TAKE_FROM}/i,
115
+ /you\s+#{TAKE_ALIAS}\s+(?:#{THE})?#{TAKE_OBJ}/i,
116
+ /you\s+now\s+have\s+(?:got\s+)?(?:#{THE})?#{TAKE_OBJ}/i,
117
+ /you\s+pick\s+up\s+(?:#{THE})?#{TAKE_OBJ}/i,
118
+ /you\s+are\s+now\s+holding\s+(?:#{THE})?#{TAKE_OBJ}/i,
119
+ ]
120
+
121
+ IT = /^(it|them)$/i
122
+
123
+ @@win = nil
124
+
125
+ ## Change this to non-nil to print out debugging info
126
+ @@debug = nil
127
+
128
+ def debug(*msg)
129
+ if @@debug
130
+ $stdout.puts msg
131
+ end
132
+ end
133
+
134
+ IDENTIFY_BY_DESCRIPTION = 0
135
+ IDENTIFY_BY_SHORTNAME = 1
136
+
137
+ SHORTNAME_CLASSIC = 0
138
+ SHORTNAME_CAPITALIZED = 1
139
+ SHORTNAME_MOONMIST = 2
140
+ SHORTNAME_WITNESS = 3
141
+ SHORTNAME_ADRIFT = 4
142
+
143
+ attr_reader :shortName
144
+ attr_accessor :identify, :map
145
+
146
+ def shortName=(x)
147
+ @shortName = x
148
+ if x == SHORTNAME_ADRIFT
149
+ @prompt = /^[a-z]/
150
+ else
151
+ @prompt = /^>\s*/
152
+ end
153
+ end
154
+
155
+
156
+ #
157
+ # Handle a take action
158
+ #
159
+ def take(move, objs)
160
+ return unless @here
161
+ if @here.objects != '' and @here.objects[-1,1] != "\n"
162
+ @here.objects << "\n"
163
+ end
164
+
165
+ objs.each { |cmd, dummy, obj|
166
+ next if not obj or not move[:reply]
167
+
168
+ objlist = obj.split(',')
169
+ o = objlist[0]
170
+ if objlist.size == 1 and o != 'all'
171
+ # ignore 'get up'
172
+ next if cmd == 'get' and (o == 'up' or o == 'off')
173
+ o = @last_obj if o =~ IT
174
+ next if not o
175
+ if move[:reply][0] =~ /^\((.*)\)$/
176
+ nobj = $1
177
+ words = nobj.split(/\s+/)
178
+ if words.size < 7
179
+ # Acclaration, like:
180
+ # > get mask
181
+ # (the brown mask)
182
+ # Taken.
183
+ o = nobj
184
+ else
185
+ # too big of an aclaration... probably something like
186
+ # (putting the xxx in your bag to make room)
187
+ end
188
+ end
189
+ o.sub!(ARTICLE, '')
190
+ status = move[:reply].to_s
191
+ TAKE_OK.each { |re|
192
+ if status =~ re then
193
+ obj = $1 || o
194
+ obj = o if obj =~ /\b(it|them)\b/
195
+ @last_obj = obj
196
+ # Take succeeded, change object o
197
+ if not @objects.has_key?(obj) and
198
+ not @here.objects =~ /\b#{obj}\b/
199
+ @here.objects << obj << "\n"
200
+ @objects[obj] = 1
201
+ end
202
+ break
203
+ end
204
+ }
205
+ else
206
+ # Handle multiple objects
207
+ move[:reply].each { |reply|
208
+ o, status = reply.split(':')
209
+ next if not status
210
+ next if o.split(' ').size > 6 # ignore if too many words
211
+ o.sub!(ARTICLE, '')
212
+ TAKE_OK.each { |re|
213
+ if status =~ re then
214
+ if o and not @objects.has_key?(o) and
215
+ not @here.objects =~ /\b#{o}\b/
216
+ @here.objects << o << "\n"
217
+ @objects[o] = 1
218
+ end
219
+ @last_obj = o
220
+ break
221
+ end
222
+ }
223
+ }
224
+ end
225
+ }
226
+ @here.objects.squeeze("\n")
227
+ end
228
+
229
+ #
230
+ # Add a new room to the automapper
231
+ #
232
+ def add(room)
233
+ @rooms << room
234
+ end
235
+
236
+ #
237
+ # Remove some rooms from the automap knowledge (user removed these manually)
238
+ #
239
+ def remove(rooms)
240
+ rooms.each { |r| @rooms.delete(r) }
241
+ end
242
+
243
+
244
+ #
245
+ # Given a room description, parse it to try to find out all exits
246
+ # from room.
247
+ #
248
+
249
+ DIRS = {
250
+ # SWD added variants used in e.g. YAGWAD
251
+ 'north-east' => 1, 'south-east' => 3, 'south-west' => 5, 'north-west' => 7,
252
+ # Usual directions
253
+ 'north' => 0, 'northeast' => 1, 'east' => 2, 'southeast' => 3,
254
+ 'south' => 4, 'southwest' => 5, 'west' => 6, 'northwest' => 7,
255
+ }
256
+
257
+ # SWD: hack: the variants with dashes must come first
258
+ # (so that regexes matches 'north-east' in preference to 'north'),
259
+ # so we manually prepend their names. The duplication is harmless,
260
+ # it just makes the regexes slightly slower in the non-match case.
261
+ DIR = '(' + DIRS.keys.join('\b|') + ')'
262
+ OR = '(?:and|or)'
263
+
264
+ EXITS_REGEX =
265
+ [
266
+ # paths lead west and north
267
+ /(run|bears|lead|wander|winds|go|continues)\s+#{DIR}\s+#{OR}\s+#{DIR}\b/i,
268
+ # to the east or west
269
+ /to\s+the#{DIR}\s+#{OR}\s+(to\s+the\s+)?#{DIR}\b/i,
270
+ # You can go south
271
+ /you\s+can\s+go#{DIR}\b/i,
272
+ # to the east
273
+ /to\s+the\s+#{DIR}\b/i,
274
+ # east-west corridor
275
+ /[\.\s+]#{DIR}[\/-]#{DIR}\s+(passage|corridor)/i,
276
+ # East is the postoffice
277
+ /\b#{DIR}\s+is/i,
278
+ # Directly north
279
+ /\bDirectly\s+#{DIR}/i,
280
+ # continues|lies|etc... east
281
+ /(runs|bears|leads|heads|opens|winds|continues\s+on|continues|branches|lies|wanders|bends|curves)\s+#{DIR}\b/i,
282
+ /(running|leading|heading|opening|branching|lying|wandering|looking|bending)\s+#{DIR}\b/i,
283
+ ]
284
+
285
+ # passage westwards
286
+ # gap in the northeast corner
287
+
288
+
289
+ EXITS_SPECIAL = {
290
+ /four\s+directions/i => [0, 2, 4, 6],
291
+ /four\s+compass\s+points/i => [0, 2, 4, 6],
292
+ /all\s+directions/i => [0, 1, 2, 3, 4, 5, 6, 7],
293
+ }
294
+
295
+ def parse_exits(r, desc)
296
+ return if not desc
297
+ exits = []
298
+
299
+ # Now, start searching for stuff
300
+
301
+ # First try the special directions...
302
+ EXITS_SPECIAL.each { |re, dirs|
303
+ if desc =~ re
304
+ exits += dirs
305
+ break
306
+ end
307
+ }
308
+
309
+ # If that failed, start searching for exits
310
+ if exits.empty?
311
+ EXITS_REGEX.each { |re|
312
+ matches = desc.scan(re)
313
+ next if matches.empty?
314
+ matches.each { |arr|
315
+ arr.each { |m|
316
+ next unless DIRS[m]
317
+ exits << DIRS[m]
318
+ }
319
+ }
320
+ }
321
+ end
322
+
323
+ exits.uniq!
324
+
325
+ # Add a 'stub' for the new connection
326
+ exits.each { |exit|
327
+ next if r[exit]
328
+ begin
329
+ c = @map.new_connection( r, exit, nil )
330
+ c.dir = Connection::AtoB
331
+ debug "\tADDED STUB #{c}"
332
+ rescue ConnectionError
333
+ end
334
+ }
335
+ end
336
+
337
+
338
+ def parse_line(line)
339
+ return unless line
340
+ @map.mutex.synchronize do
341
+ _parse_line(line)
342
+ end
343
+
344
+ if @map.kind_of?(FXMap)
345
+ @map.clear_selection
346
+ @here.selected = true if @here
347
+ @map.create_pathmap
348
+ @map.zoom = @map.zoom
349
+ @map.center_view_on_room(@here) if @here
350
+ @map.draw
351
+ end
352
+ end
353
+
354
+ def _parse_line(line)
355
+ @moves.clear
356
+
357
+ #
358
+ # Read all commands
359
+ #
360
+ loop do
361
+ line.sub!(@prompt, '') if @prompt.to_s =~ />/
362
+ line.chop!
363
+ line.sub!(/\s+$/, '')
364
+ cmd = line
365
+
366
+ # Read reply
367
+ reply = []
368
+ while line = @f.gets
369
+ break if line =~ @prompt
370
+ line.chop!
371
+ line.sub!(/\s+$/,'')
372
+ reply << line
373
+ end
374
+
375
+ if cmd =~ UNDO
376
+ if @moves.size < 2
377
+ @here = nil # out of moves, no clue where we are
378
+ else
379
+ @moves.pop
380
+ end
381
+ else
382
+ move = { }
383
+
384
+ # Replace all 'THEN' in command for periods
385
+ cmd.sub!(THEN, '.')
386
+ # and all ANDs for commas
387
+ cmd.sub!(/\band\b/i,',')
388
+ # and multiple periods for just one
389
+ cmd.sub!(/\s*\.+\s*/, '.')
390
+
391
+ move[:cmd] = cmd
392
+ move[:reply] = reply
393
+ @moves << move
394
+ end
395
+
396
+ break if not line
397
+
398
+ end
399
+
400
+
401
+ # Step 2
402
+ @restart = false
403
+ tele = nil
404
+ @moves.each { |move|
405
+ cmd = move[:cmd]
406
+ next if cmd =~ IGNORE or cmd =~ OTHERS
407
+ if cmd =~ UNSCRIPT
408
+ tele = 1
409
+ @here = nil
410
+ next
411
+ end
412
+
413
+ if @restart and cmd =~ YES
414
+ @here = nil
415
+ else
416
+ @restart = false
417
+ tele = nil
418
+ end
419
+
420
+ if cmd =~ RESTART
421
+ @restart = true
422
+ tele = 1
423
+ end
424
+
425
+ if cmd =~ RESTORE
426
+ if move[:reply].join() =~ RESTORE_OK
427
+ @here = nil
428
+ tele = 1
429
+ else
430
+ next
431
+ end
432
+ end
433
+
434
+ name = nil
435
+ desc = ''
436
+ roomflag = false
437
+ desc_gap = false
438
+ startup = false
439
+ rooms = []
440
+ move[:reply].each { |r|
441
+ tele = 1 if r =~ DEAD
442
+
443
+ if r =~ STARTUP
444
+ # Dealing with a startup message, such as:
445
+ # MY GAME
446
+ # Copyright (C) 1984 ComputerQuest
447
+ # We skip the whole thing until the next blank line
448
+ debug "#{r} skipped due to startup"
449
+ startup = true
450
+ desc = ''
451
+ name = nil
452
+ roomflag = false
453
+ desc_gap = false
454
+ @here = nil
455
+ tele = 1
456
+ next
457
+ end
458
+ next if startup and r !~ BLANK
459
+ startup = false
460
+
461
+ if not roomflag and r !~ BLANK and n = room_name(r)
462
+ debug "Found room #{n}"
463
+ roomflag = true
464
+ desc_gap = false
465
+ name = n
466
+ desc = ''
467
+ next
468
+ end
469
+
470
+ if not desc_gap and roomflag and r =~ BLANK and desc == ''
471
+ desc_gap = true
472
+ next
473
+ end
474
+
475
+
476
+ if roomflag and r !~ BLANK
477
+ desc << r << "\n"
478
+ next
479
+ end
480
+
481
+ if r =~ BLANK and roomflag
482
+ if desc.count("\n") == 1 and desc =~ /\?$/
483
+ # A "What next?" type of prompt, not a room description
484
+ desc = ''
485
+ end
486
+ if desc == ''
487
+ desc = nil
488
+ else
489
+ desc.gsub!(/\n/, ' ')
490
+ desc.strip!
491
+ end
492
+
493
+ rooms << {
494
+ :name => name,
495
+ :desc => desc,
496
+ :tele => tele
497
+ }
498
+ roomflag = false
499
+ desc_gap = false
500
+ name = nil
501
+ desc = ''
502
+ tele = nil
503
+ end
504
+ }
505
+
506
+ # Oops, there was no newline between room description and prompt and
507
+ # we missed the last room.
508
+ # This happens for example with Magnetic Scrolls transcripts.
509
+ if name
510
+ rooms << {
511
+ :name => name,
512
+ :desc => desc,
513
+ :tele => tele
514
+ }
515
+ end
516
+
517
+ if not rooms.empty?
518
+ move[:rooms] = rooms
519
+ move[:look] = true if cmd =~ LOOK
520
+ move[:reply] = nil
521
+ end
522
+ }
523
+
524
+
525
+ @moves.each { |move|
526
+ cmd = move[:cmd]
527
+ if objs = cmd.scan(TAKE)
528
+ take(move, objs)
529
+ end
530
+
531
+ if not move[:rooms]
532
+ # Check if a moving command failed
533
+ if cmd =~ GO
534
+ dir = $3
535
+ # See if a stub direction exits in that direction
536
+ if @here and DIRMAP[dir] then
537
+ dir = DIRMAP[dir]
538
+ next unless @here[dir]
539
+
540
+ if @here[dir].stub?
541
+ # Check if reply was that there's no exit there.
542
+ NO_EXIT.each { |re|
543
+ if move[:reply][0] =~ re
544
+ # If so, remove it... automapper got it wrong. Not an exit.
545
+ @map.delete_connection(@here[dir])
546
+ break
547
+ end
548
+ }
549
+ end
550
+
551
+ # Check if there is a closed door
552
+ if @here[dir] and @here[dir].type == Connection::FREE
553
+ CLOSED_EXIT.each { |re|
554
+ if move[:reply][0] =~ re
555
+ # If so, flag it
556
+ @here[dir].type = Connection::CLOSED_DOOR
557
+ break
558
+ end
559
+ }
560
+ end
561
+ end
562
+ end
563
+ next
564
+ end
565
+
566
+ move[:rooms].each { |room|
567
+ name = room[:name]
568
+ debug "SECTION: #{@map.section}"
569
+ debug "HERE IS: #{@here}"
570
+ debug "CMD: #{cmd}"
571
+ debug "ENDS AT: #{name}"
572
+
573
+ desc = room[:desc]
574
+ desc.gsub!(/(\w)\s*\n/, '\1 ')
575
+
576
+ line = move[:line]
577
+
578
+ # If we teleported, try to find room
579
+ if room[:tele]
580
+ debug "\t ****TELEPORT TO #{name}****"
581
+ @here = find_room(name, desc)
582
+ debug "\t TO: #{@here}"
583
+ end
584
+
585
+ # Make sure the user has not deleted the current room from map
586
+ # and that we are in the proper section.
587
+ if @here
588
+ found = false
589
+ @map.sections.each_with_index { |sect, idx|
590
+ if sect.rooms.include?(@here)
591
+ found = true
592
+ if idx != @map.section
593
+ @map.fit
594
+ @map.section = idx
595
+ end
596
+ break
597
+ end
598
+ }
599
+ @here = nil if not found
600
+ end
601
+
602
+ # If it is a look command or we don't know where we are yet,
603
+ # set current room.
604
+ if move[:look] or not @here
605
+ if move[:look] and name =~ DARKNESS
606
+ @here = find_room(name, desc) if @here and not @here.darkness
607
+ else
608
+ @here = find_room(name, desc)
609
+ end
610
+ @here = new_room(move, name, desc) unless @here
611
+ @here.selected = true
612
+ next
613
+ end
614
+
615
+ cmd.sub!(GO, '')
616
+ dir = $3
617
+
618
+ # Swallow everything until next command (separated by .)
619
+ cmd.sub!(/.*\./, '')
620
+
621
+ debug "MOVED IN DIRECTION: #{dir} CMD LEFT:#{cmd}"
622
+ if not cmd
623
+ debug "Oops... run out of commands."
624
+ break
625
+ end
626
+
627
+ sect = @map.section
628
+
629
+ # Otherwise, assume we moved in some way. Try to find the new room.
630
+ if name =~ DARKNESS
631
+ idx = DIRMAP[dir] || ODIRMAP[dir]
632
+ # Moving to a dark room is special. We verify the exit to see
633
+ # if it was already moving to a dark room (in which case it
634
+ # becomes "there") or if it was moving to some other room (in
635
+ # which case we flag it as dark and make it there)
636
+ if idx and @here[idx]
637
+ c = @here[idx]
638
+
639
+ if c.roomA == @here
640
+ b = c.roomB
641
+ else
642
+ b = c.roomA
643
+ end
644
+
645
+ if not b
646
+ there = nil
647
+ else
648
+ if b.name !~ DARKNESS
649
+ b.darkness = true
650
+ end
651
+ there = b
652
+ end
653
+ else
654
+ # No connection yet, create dark room
655
+ there = nil
656
+ end
657
+ else
658
+ there = find_room(name, desc)
659
+ end
660
+
661
+ next if there == @here
662
+
663
+
664
+ go = nil
665
+ if DIRMAP[dir]
666
+ dir = DIRMAP[dir]
667
+ elsif ODIRMAP[dir]
668
+ go = ODIRMAP[dir]
669
+ debug "#{dir} move #{go}"
670
+ dir = choose_dir(@here, there, go)
671
+ else
672
+ # special move --- teleport/death/etc.
673
+ go = 0
674
+ dir = choose_dir(@here, there, go)
675
+ end
676
+ debug "MOVED IN DIR INDEX: #{dir}"
677
+
678
+ @here.selected = false
679
+ if not there
680
+ # Unvisited -- new room
681
+ @here = new_room(move, name, desc, dir, @here, go)
682
+ else
683
+ # Visited before -- new link
684
+ if sect == @map.section # don't conn
685
+ new_link(move, @here, there, dir, go)
686
+ end
687
+ @here = there
688
+ end
689
+ }
690
+ }
691
+
692
+ @moves.clear
693
+ @map.fit
694
+ if @map.kind_of?(FXMap)
695
+ end
696
+ end
697
+
698
+ def find_room(name, desc)
699
+ case @identify
700
+ when IDENTIFY_BY_DESCRIPTION
701
+ return find_room_by_desc(name, desc)
702
+ when IDENTIFY_BY_SHORTNAME
703
+ return find_room_by_name(name, desc)
704
+ end
705
+ end
706
+
707
+ def find_room_by_name(name, desc)
708
+ bestscore = 0
709
+ best = nil
710
+
711
+ @map.sections.each_with_index { |sect, idx|
712
+ sect.rooms.each { |room|
713
+ score = 0
714
+ score += 1 if room.name == name
715
+
716
+ if score == 1 and desc and room.desc
717
+ # We have a description...
718
+ # Try exact description match first
719
+ score += 100 if room.desc == desc
720
+
721
+ # Try substring match
722
+ score += 50 if room.desc.index(desc)
723
+
724
+ # If we have a room where both name and desc match,
725
+ # we get a better score than just description only.
726
+ # This is to help, for example, Trinity, where two rooms have
727
+ # the exact description but different name.
728
+ score += 1 if room.name == name and score > 0
729
+
730
+ # If still no luck, try first N words
731
+ if score == 1
732
+ dwords = room.desc.split(' ')
733
+ words = desc.split(' ')
734
+ match = true
735
+ count = 0
736
+ 0.upto(desc.size) { |i|
737
+ # Ignore some words (like open/close, which may just mean
738
+ # some doors changed state)
739
+ next if words[i] =~ DESC_IGNORE
740
+
741
+ if words[i] != dwords[i]
742
+ if count < DESC_MINWORDS
743
+ match = false
744
+ break
745
+ else
746
+ next
747
+ end
748
+ end
749
+ count += 1
750
+ }
751
+
752
+ score += count if match
753
+ end
754
+ end
755
+ next if score <= bestscore
756
+ bestscore = score
757
+ best = [room, idx]
758
+ }
759
+ }
760
+
761
+ return nil if not best
762
+
763
+ # Make sure we are in the right section
764
+ if best[1] != @map.section
765
+ @map.fit
766
+ @map.section = best[1]
767
+ end
768
+ if desc and (not best[0].desc or best[0].desc == '')
769
+ best[0].desc = desc
770
+ end
771
+ return best[0]
772
+ end
773
+
774
+ def find_room_by_desc(name, desc)
775
+ bestscore = 0
776
+ best = nil
777
+
778
+ @map.sections.each_with_index { |sect, idx|
779
+ sect.rooms.each { |room|
780
+ score = 0
781
+
782
+ if desc and room.desc
783
+ # We have a description...
784
+ # Try exact description match first
785
+ score += 100 if room.desc == desc
786
+
787
+ # Try substring match
788
+ score += 50 if room.desc.index(desc)
789
+
790
+ # If we have a room where both name and desc match,
791
+ # we get a better score than just description only.
792
+ # This is to help, for example, Trinity, where two rooms have
793
+ # the exact description but different name.
794
+ score += 1 if room.name == name and score > 0
795
+
796
+ # If still no luck, try first N words
797
+ if score == 0
798
+ dwords = room.desc.split(' ')
799
+ words = desc.split(' ')
800
+ match = true
801
+ count = 0
802
+ 0.upto(desc.size) { |i|
803
+ # Ignore some words (like open/close, which may just mean
804
+ # some doors changed state)
805
+ next if words[i] =~ DESC_IGNORE
806
+
807
+ if words[i] != dwords[i]
808
+ if count < DESC_MINWORDS
809
+ match = false
810
+ break
811
+ else
812
+ next
813
+ end
814
+ end
815
+ count += 1
816
+ }
817
+
818
+ score += count if match and room.name == name
819
+ end
820
+ else
821
+ # Just the name, not so good
822
+ score += 1 if room.name == name
823
+ end
824
+ next if score <= bestscore
825
+ bestscore = score
826
+ best = [room, idx]
827
+ }
828
+ }
829
+
830
+ return nil if not best
831
+
832
+ # Make sure we are in the right section
833
+ if best[1] != @map.section
834
+ @map.fit
835
+ @map.section = best[1]
836
+ end
837
+ if desc and (not best[0].desc or best[0].desc == '')
838
+ best[0].desc = desc
839
+ end
840
+ return best[0]
841
+ end
842
+
843
+ def room_name(line)
844
+ case @shortName
845
+ when SHORTNAME_CAPITALIZED
846
+ return room_name_classic(line, false)
847
+ when SHORTNAME_MOONMIST
848
+ return room_name_moonmist(line)
849
+ when SHORTNAME_WITNESS
850
+ return room_name_witness(line)
851
+ else
852
+ return room_name_classic(line, true)
853
+ end
854
+ end
855
+
856
+ def capitalize_room(line)
857
+ words = line.split(' ')
858
+ words.each_with_index { |w, idx|
859
+ if idx > 0 and w =~ NAME_UNCAP
860
+ w.downcase!
861
+ else
862
+ w.capitalize!
863
+ end
864
+ }
865
+ return words.join(' ')
866
+ end
867
+
868
+ def room_name_alan(line)
869
+ return false if line !~ /^[^.]+\.$/
870
+ line.sub!(/\.$/, '')
871
+ return room_name_classic(line, true)
872
+ end
873
+
874
+ def room_name_witness(line)
875
+ if line =~ /^You\sare\s(?:now\s)?(?:[io]n\s)?(?:#{THE})?([\w'\d\s\-_]+)\.$/
876
+ return false if $1 =~ /own feet/
877
+ return $1
878
+ elsif line =~ /^\(([\w\d\s_]+)\)$/
879
+ return $1
880
+ else
881
+ return false
882
+ end
883
+ end
884
+
885
+ def room_name_moonmist(line)
886
+ return false if line =~ /^\(You are not holding it.\)$/
887
+ if line =~ /^\(You\sare\s(?:now\s)?(?:[io]n\s)?(?:#{THE})?([\w'\d\s\-_]+)\.\)$/
888
+ return capitalize_room( $1 )
889
+ else
890
+ return false
891
+ end
892
+ end
893
+
894
+ #
895
+ # Determine if line corresponds to a room name
896
+ #
897
+ def room_name_classic(line, all_capitals = true)
898
+ # Check if user/game has created a room with that name already
899
+ return line if find_room(line, nil)
900
+
901
+ # We have a room if we match darkness
902
+ return line if line =~ DARKNESS
903
+
904
+ # Remove unwanted stuff line (on the bed)
905
+ line.sub!(NAME_REMOVE, '')
906
+
907
+ # Check if user/game has created a room with that name already
908
+ return line if find_room(line, nil)
909
+
910
+ # Remove periods from salutations
911
+ line.sub!(SALUTATIONS, '\1')
912
+
913
+ # quick check for invalid format
914
+ return false if line =~ NAME_INVALID
915
+
916
+ # Qucik check for word characters
917
+ return false unless line =~ /\w/
918
+
919
+ # Check if we start line with uncapitalized words or symbols
920
+ return false if line =~ /^[ a-z\/\\\-\(\)']/
921
+
922
+ # Check if line holds only capitalized words or several spaces together
923
+ # or a quote or a 1) line. If so, not a room.
924
+ return false if line =~ /^[A-Z\d,\.\/\-"'\s]+$/ or line =~ /\s\s/ or
925
+ line =~ /^".*"$/ or line =~ /^"[^"]+$/ or line =~ /^\d+\)/
926
+
927
+ # Check word count (if too many, not a room)
928
+ words = line.split(' ')
929
+ return false if words.size > NAME_MAXWORDS
930
+
931
+ return false if not all_capitals and words.size > 6
932
+
933
+ # If not, check all words of 4 chars or more are capitalized
934
+ # and that there are no 3 or more short letter words together
935
+ # (which means a diagram)
936
+ num = 0
937
+ words.each { |w|
938
+ return false if all_capitals and w =~ /^[a-z]/ and w !~ NAME_UNCAP
939
+ if w.size <= 2
940
+ num += 1
941
+ return false if num > 2
942
+ else
943
+ num = 0
944
+ end
945
+ }
946
+
947
+ # Restore period to salutations
948
+ line.sub!(/\b#{SALUT}\b/, '\1.')
949
+
950
+ # Okay, it is a room.
951
+ return line
952
+ end
953
+
954
+ #
955
+ # Create a new room
956
+ #
957
+ def new_room( move, name, desc, dir = nil, from = nil, go = nil )
958
+ b = nil
959
+ dark = false
960
+
961
+ if not from
962
+ debug "FROM undefined. Increase section #{name}"
963
+ @section += 1
964
+ @map.new_section if @section >= @map.sections.size
965
+ r = @map.new_room(0, 0)
966
+ r.name = name
967
+ else
968
+
969
+ darkexits = []
970
+ if name =~ DARKNESS
971
+ dark = true
972
+ else
973
+ dark = false
974
+ if from[dir]
975
+ # oops, we had a connection there.
976
+ c = from[dir]
977
+ b = c.opposite(from)
978
+ if b and b.name =~ DARKNESS
979
+ # Was it a dark room that is now lit?
980
+ # if so, set dark flag and delete dark room
981
+ @map.delete_connection(c)
982
+ @map.delete_room_only(b)
983
+ dark = true
984
+ c = nil
985
+ end
986
+ end
987
+ end
988
+
989
+ x = from.x
990
+ y = from.y
991
+ dx, dy = Room::DIR_TO_VECTOR[dir]
992
+ x += dx
993
+ y += dy
994
+ @map.shift(x, y, dx, dy) if not @map.free?(x, y)
995
+
996
+
997
+ debug "+++ New Room #{name} from #{from}"
998
+ r = @map.new_room(x, y)
999
+ r.name = name
1000
+ r.darkness = dark
1001
+ if b and dark
1002
+ b.exits.each_with_index { |de, didx|
1003
+ next unless de
1004
+ @map.sections[@map.section].connections << de
1005
+ r[didx] = de
1006
+ if de.roomA == b
1007
+ de.roomA = r
1008
+ else
1009
+ de.roomB = r
1010
+ end
1011
+ }
1012
+ end
1013
+ c = nil
1014
+ if from[dir]
1015
+ # oops, we had a connection there.
1016
+ c = from[dir]
1017
+ b = c.roomB
1018
+ if c.stub?
1019
+ # Stub connection. Update it.
1020
+ debug "\tUPDATE #{c}"
1021
+ odir = (dir + 4) % 8
1022
+ c.exitAtext = go if go
1023
+ c.type = Connection::SPECIAL if go == 0
1024
+ c.roomB = r
1025
+ r[odir] = c
1026
+ debug "\tNOW IT IS #{c}"
1027
+ else
1028
+ # Probably a link that is complex
1029
+ # or oneway. Shift it to some other location
1030
+ shift_link(from, dir)
1031
+ c = nil
1032
+ end
1033
+ end
1034
+ if c == nil
1035
+ begin
1036
+ c = @map.new_connection( from, dir, r )
1037
+ c.exitAtext = go if go
1038
+ c.type = Connection::SPECIAL if go == 0
1039
+ c.dir = Connection::AtoB
1040
+ rescue Section::ConnectionError
1041
+ end
1042
+ end
1043
+ end
1044
+
1045
+ parse_exits(r, desc)
1046
+
1047
+ # Update room description
1048
+ r.desc = desc
1049
+ return r
1050
+ end
1051
+
1052
+ def shift_link(room, dir)
1053
+ idx = dir + 1
1054
+ idx = 0 if idx > 7
1055
+ while idx != dir
1056
+ break if not room[idx]
1057
+ idx += 1
1058
+ idx = 0 if idx > 7
1059
+ end
1060
+ if idx != dir
1061
+ room[idx] = room[dir]
1062
+ room[dir] = nil
1063
+ # get position of other room
1064
+ ox, oy = Room::DIR_TO_VECTOR[dir]
1065
+ c = room[idx]
1066
+ if c.roomA == room
1067
+ b = c.roomB
1068
+ else
1069
+ b = c.roomA
1070
+ end
1071
+ x, y = [b.x, b.y]
1072
+ x -= ox
1073
+ y -= oy
1074
+ dx, dy = Room::DIR_TO_VECTOR[idx]
1075
+ @map.shift(x, y, -dx, -dy)
1076
+ else
1077
+ debug "Warning. Cannot shift connection."
1078
+ end
1079
+ end
1080
+
1081
+
1082
+ def new_link(move, from, to, dir, go)
1083
+ odir = (dir + 4) % 8
1084
+ c = nil
1085
+ # If we have something in the from direction
1086
+ if from[dir]
1087
+ c = from[dir]
1088
+ debug "\tMOVE #{c} DIR: #{dir}"
1089
+ if c.stub?
1090
+ # Stub connection, fill it
1091
+ c.roomB = to
1092
+ c.exitAtext = go if go
1093
+ c.type = Connection::SPECIAL if go == 0
1094
+ # we still need to check the destination to[odir]...
1095
+ if not to[odir]
1096
+ # nothing to do
1097
+ elsif to[odir].stub?
1098
+ @map.delete_connection(to[odir])
1099
+ debug "\tREMOVE #{to[odir]}"
1100
+ else
1101
+ # this end cannot be deleted. we need to shift odir
1102
+ debug "\tCHOOSE NEW DIR for #{odir}"
1103
+ rgo = nil
1104
+ if go and go > 0
1105
+ rgo = rgo % 2 == 0? go - 1 : go + 1
1106
+ end
1107
+ odir = choose_dir(to, from, rgo, dir)
1108
+ @map.delete_connection(to[odir]) if to[odir]
1109
+ debug "\tSHIFTED DESTINATION TO EXIT #{odir}"
1110
+ end
1111
+ to[odir] = c
1112
+ elsif c.roomB == to
1113
+ # We already went this way. Nothing to do.
1114
+ debug "\tWE ALREADY PASSED THRU HERE"
1115
+ elsif c.roomA == to
1116
+ # We verified we can travel thru this connection in both
1117
+ # directions. Change its status to both.
1118
+ c.dir = Connection::BOTH
1119
+ debug "\tSECTION: #{@map.section}"
1120
+ debug "\tVERIFIED EXIT BOTH WAYS"
1121
+ else
1122
+ debug "\tOTHER"
1123
+ if c.roomA == from
1124
+ b = c.roomB
1125
+ if b.name =~ DARKNESS
1126
+ debug "*** REPLACING DARK ROOM ***"
1127
+ @map.delete_connection(c)
1128
+ to.darkness = true
1129
+ @map.delete_room(b)
1130
+ c = nil
1131
+ else
1132
+ if c.exitAtext != 0
1133
+ # if it was an up/down/in/out dir, we shift it
1134
+ shift_link(from, dir)
1135
+ c = nil
1136
+ else
1137
+ # else, we really have a problem in the map
1138
+ debug "*** CANNOT AUTOMAP --- MAZE ***"
1139
+ dir = Room::DIRECTIONS[dir]
1140
+ @map.cannot_automap "Maze detected.\n'#{from}' #{dir} leads to '#{c.roomB}',\nnot to this '#{to}'."
1141
+ # self.stop
1142
+ return nil
1143
+ end
1144
+ end
1145
+ else
1146
+ debug "SHIFT LINK #{from} #{dir}"
1147
+ # We have a connection that turns. Move the link around
1148
+ shift_link(from, dir)
1149
+ c = nil
1150
+ end
1151
+ end
1152
+ end
1153
+
1154
+ if not c and to[odir]
1155
+ c = to[odir]
1156
+ if c.stub?
1157
+ debug "\tREMOVE #{to[odir]} and REPLACE with #{c}"
1158
+ # Stub connection, fill it
1159
+ c.roomB = from
1160
+ # @map.delete_connection(from[dir]) if from[dir].stub?
1161
+ from[dir] = c
1162
+ c.dir = Connection::BtoA
1163
+ c.exitBtext = go if go
1164
+ c.type = Connection::SPECIAL if go == 0
1165
+ elsif c.roomB == from
1166
+ c.exitBtext = go if go
1167
+ c.type = Connection::SPECIAL if go == 0
1168
+ else
1169
+ # We need to change odir to something else
1170
+ rgo = nil
1171
+ if go and go > 0
1172
+ rgo = go % 2 == 0? go - 1 : go + 1
1173
+ end
1174
+ odir = choose_dir(to, from, rgo, dir)
1175
+ @map.delete_connection(to[odir]) if to[odir] and to[odir].stub?
1176
+ c = nil
1177
+ end
1178
+ end
1179
+
1180
+ if not c
1181
+ # First, check all from exits that are AtoB to see if we have one
1182
+ # that goes to the room we want.
1183
+ from.exits.each_with_index { |e, idx|
1184
+ next unless e
1185
+ if e.roomA == to and e.dir == Connection::AtoB
1186
+ c = e
1187
+ from[idx] = nil
1188
+ end
1189
+ }
1190
+ if c
1191
+ # If so, make that connection go both ways and attach it to
1192
+ # current direction.
1193
+ from[dir] = c
1194
+ c.dir = Connection::BOTH
1195
+ c.exitAtext = go if go
1196
+ c.type = Connection::SPECIAL if go == 0
1197
+ end
1198
+ end
1199
+
1200
+ if not c
1201
+ # No link exists -- create new one.
1202
+ begin
1203
+ c = @map.new_connection( from, dir, to, odir )
1204
+ c.exitAtext = go if go
1205
+ c.dir = Connection::AtoB
1206
+ rescue Section::ConnectionError
1207
+ end
1208
+ end
1209
+
1210
+ return c
1211
+ end
1212
+
1213
+
1214
+ # Choose a direction to represent up/down/in/out.
1215
+ def choose_dir(a, b, go = nil, exitB = nil)
1216
+ # Don't add a new connection if we already have a normal connection
1217
+ # to the room and we are moving up/down/etc.
1218
+ if go
1219
+ rgo = go % 2 == 0? go - 1 : go + 1
1220
+ debug "#{Connection::EXIT_TEXT[go]} <=> #{Connection::EXIT_TEXT[rgo]}"
1221
+ # First, check if room already has exit moving towards other room
1222
+ a.exits.each_with_index { |e, idx|
1223
+ next if not e
1224
+ roomA = e.roomA
1225
+ roomB = e.roomB
1226
+ if roomA == a and roomB == b
1227
+ e.exitAtext = go if e.exitBtext == rgo
1228
+ return idx
1229
+ elsif roomB == a and roomA == b
1230
+ e.exitBtext = go if e.exitAtext == rgo
1231
+ return idx
1232
+ end
1233
+ }
1234
+ end
1235
+
1236
+ # We prefer directions that travel less... so we need to figure
1237
+ # out where we start from...
1238
+ if b
1239
+ x = b.x
1240
+ y = b.y
1241
+ else
1242
+ x = a.x
1243
+ y = a.y
1244
+ end
1245
+ if exitB
1246
+ dx, dy = Room::DIR_TO_VECTOR[exitB]
1247
+ x += dx
1248
+ y += dy
1249
+ end
1250
+
1251
+ # No such luck... Pick a direction.
1252
+ best = nil
1253
+ bestscore = nil
1254
+
1255
+ DIRLIST.each { |dir|
1256
+ # We prefer straight directions to diagonal ones
1257
+ inc = dir % 2 == 1 ? 10 : 14
1258
+ score = 1000
1259
+ # We prefer directions where both that dir and the opposite side
1260
+ # are empty.
1261
+ if (not a[dir]) or a[dir].stub?
1262
+ score += inc
1263
+ score += 4 if a[dir] #attaching to stubs is better
1264
+ end
1265
+ # rdir = (dir + 4) % 8
1266
+ # score += 1 unless a[rdir]
1267
+
1268
+ # Measure distance for that exit, we prefer shorter
1269
+ # paths
1270
+ dx, dy = Room::DIR_TO_VECTOR[dir]
1271
+ dx = (a.x + dx) - x
1272
+ dy = (a.y + dy) - y
1273
+ d = dx * dx + dy * dy
1274
+ score -= d
1275
+ next if bestscore and score <= bestscore
1276
+ bestscore = score
1277
+ best = dir
1278
+ }
1279
+
1280
+ if not bestscore
1281
+ raise "No free exit for choose_dir"
1282
+ end
1283
+
1284
+ return best
1285
+ end
1286
+
1287
+ def stop
1288
+ @t.kill if @t
1289
+ end
1290
+
1291
+ def destroy
1292
+ @t.kill if @t
1293
+ @f.close if @f
1294
+ GC.start
1295
+ end
1296
+
1297
+
1298
+ def properties(modal = false)
1299
+ require 'IFMapper/TranscriptDialogBox'
1300
+ if not @@win
1301
+ @@win = TranscriptDialogBox.new(self)
1302
+ else
1303
+ @@win.copy_from(self)
1304
+ end
1305
+ if modal
1306
+ @@win.execute
1307
+ end
1308
+ end
1309
+
1310
+ def initialize(map, file)
1311
+ @shortName = 0
1312
+ @prompt = /^>\s*/
1313
+ @identify = 0
1314
+
1315
+ @f = nil
1316
+ @file = file
1317
+ @map = map
1318
+ @objects = {}
1319
+ @moves = []
1320
+ @here = nil
1321
+ @section = -1
1322
+ @last_obj = nil
1323
+ end
1324
+
1325
+ # Step one user command at a time
1326
+ def step
1327
+ begin
1328
+ parse_line(@f.gets)
1329
+ rescue => e
1330
+ $stderr.puts e
1331
+ $stderr.puts e.backtrace
1332
+ end
1333
+ end
1334
+
1335
+ def start
1336
+ if not @f
1337
+ @f = File.open(@file, 'r')
1338
+ while line = @f.gets
1339
+ if @map.name =~ /^Empty Map/ and line =~ TRANSCRIPT
1340
+ if $1
1341
+ @map.name = $1
1342
+ else
1343
+ @map.name = @f.gets.strip
1344
+ end
1345
+ @map.name = capitalize_room(@map.name)
1346
+ end
1347
+ break if @prompt =~ line
1348
+ end
1349
+ parse_line(line)
1350
+ end
1351
+
1352
+ @t = Thread.new {
1353
+ loop do
1354
+ self.step
1355
+ Thread.pass
1356
+ sleep 3
1357
+ end
1358
+ }
1359
+ @t.run
1360
+ end
1361
+ end