ifmapper 1.0.0 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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