File Coverage

blib/lib/Games/ScottAdams/Compile.pm
Criterion Covered Total %
statement 192 227 84.5
branch 39 76 51.3
condition 5 11 45.4
subroutine 12 14 85.7
pod 0 7 0.0
total 248 335 74.0


line stmt bran cond sub pod time code
1             # $Id: Compile.pm,v 1.3 2006/11/01 23:57:42 mike Exp $
2              
3             # Compile.pm - back-end compilation functions for Scott Adams game files.
4              
5             package Games::ScottAdams::Game;
6 1     1   84 use strict;
  1         2  
  1         3247  
7              
8              
9             sub compile {
10 1     1 0 208 my $this = shift();
11 1         5 my $strict = $this->{-strict};
12              
13 1         2 if (0) {
14             print STDERR "debugging aliases:\n";
15             my $nv = $this->{nvocab};
16             foreach my $word (keys %$nv) {
17             print STDERR "$word -> ", join(',', @{ $nv->{$word} }), "\n";
18             }
19             }
20              
21 1         3 my $lightsource = $this->{lightsource};
22 1 50       5 if (defined $lightsource) {
23             # Swap nominated item with #9 (1-based)
24             # May need to pad list so we can swap with the 9th
25 0         0 while (@{ $this->{items} } <= 9) {
  0         0  
26 0         0 push @{ $this->{items} }, new Games::ScottAdams::Item('', '', 0);
  0         0  
27             }
28              
29 0         0 my $slot = $this->resolve_item($lightsource, 'light source');
30 0         0 $this->swap_items($slot, 9);
31             }
32              
33             # Compile actions before emitting header so we know the values
34             # we'll need for $nactions and $nmessages. ($nactions is NOT in
35             # general the size of $this->{actions}, since that may contain big
36             # actions that have to be broken across two or more actions in the
37             # compiled form.)
38 1         5 my $nvocab = $this->_compile_vocab();
39 1         3 my(@compiled_actions, @compiled_comments);
40 1         3 foreach my $action (@{ $this->{actions} }) {
  1         3  
41 65         168 my @tmp = $action->compile($this);
42 65         118 push @compiled_actions, @tmp;
43 65   100     168 my $comment = $action->comment() || '';
44 65         92 $comment =~ s/\"/\'/g;
45 65         178 push @compiled_comments, $comment, map { '' } 1..(@tmp-1);
  6         23  
46             }
47              
48 1         2 my $nitems = @{ $this->{items} } - 1;
  1         4  
49 1         2 my $nactions = @compiled_actions - 1;
50 1         2 my $nrooms = @{ $this->{rooms} } - 1;
  1         3  
51              
52 1         3 my $maxload = $this->{maxload};
53 1 50       5 if (!defined $maxload) {
54 0 0       0 _fatal("maximum load not defined") if $strict;
55 0         0 $maxload = 6; # default from Adventureland
56             }
57              
58 1         3 my $start = $this->{start};
59 1 0 33     4 $start = $this->{_roomname1}
60             if !defined $start && !$strict;
61 1         4 $start = $this->resolve_room($start, 'start');
62              
63 1         3 my $ntreasures = $this->{ntreasures};
64              
65 1         3 my $wordlen = $this->{wordlen};
66 1 50       3 if (!defined $wordlen) {
67 0 0       0 _fatal("word length not defined") if $strict;
68 0         0 $wordlen = 3; # default from Adventureland
69             }
70              
71 1         3 my $lighttime = $this->{lighttime};
72 1 50       4 if (!defined $lighttime) {
73 0 0       0 _fatal("light duration not defined") if $strict;
74 0         0 $lighttime = 125; # default from Adventureland
75             }
76              
77 1         2 my $nmessages = @{ $this->{messages} } - 1;
  1         3  
78              
79 1         3 my $treasury = $this->{treasury};
80 1 0 33     4 $treasury = $this->{_roomname1}
81             if !defined $treasury && !$strict;
82 1 50       15 $treasury = $treasury eq "-" ? 255 :
83             $this->resolve_room($treasury, 'treasury');
84              
85 1         3 my $ident = $this->{ident};
86 1 50       4 if (!defined $ident) {
87 0 0       0 _fatal("adventure identifier not defined") if $strict;
88 0         0 $ident = 1; # default from Adventureland
89             }
90 1         3 my $version = $this->{version};
91 1 50       5 if (!defined $version) {
92 0 0       0 _fatal("version number not defined") if $strict;
93 0         0 $version = 416; # default from Adventureland
94             }
95              
96             # Header of 16-bit values. How many of these should there be?
97             # It's hard to say -- the documentation says there are fourteen of
98             # them, and then lists only thirteen; and sample games that I've
99             # looked at (e.g _Adventureland_) only seem to have twelve. I'll
100             # go with the _Adventureland_ format. (Reading the ScottCurses.c
101             # source appears to confirm this.)
102 1         70 print ((76<<8)+84, "\n"); # unknown -- I'm using it as MT magic
103 1         13 print $nitems, "\n"; # number of items
104 1         11 print $nactions, "\n"; # number of actions
105 1         13 print $nvocab-1, "\n"; # number of nouns and verbs (same length!)
106 1         11 print $nrooms, "\n"; # number of rooms
107 1         11 print $maxload, "\n"; # maximum a player can carry
108 1         11 print $start, "\n"; # starting room
109 1         12 print $ntreasures, "\n"; # total treasures (*)
110 1         17 print $wordlen, "\n"; # word length
111 1         11 print $lighttime, "\n"; # time light source lasts
112 1         11 print $nmessages, "\n"; # number of messages
113 1         11 print $treasury, "\n"; # treasure room (leave things here to score)
114              
115             # Actions.
116 1         11 print "\n";
117 1         3 foreach my $compiled_action (@compiled_actions) {
118 71         664 print $compiled_action, "\n";
119             }
120              
121             # Vocab. Verbs and nouns interleaved (one list padded if necessary.)
122 1         9 print "\n";
123 1         6 for (my $i = 0; $i < $nvocab; $i++) {
124 33         97 my($verb, $noun) = ($this->{verbs}->[$i], $this->{nouns}->[$i]);
125 33 50       61 $verb = '' if !defined $verb;
126 33 50       55 $noun = '' if !defined $noun;
127 33         352 print qq["$verb" "$noun"\n];
128             }
129              
130             # Rooms. These are represented as a sequence of six integers (the
131             # numbers of the rooms reached by moving North, South, East, West,
132             # Up and Down respectively) followed by a description string in
133             # double quotes.
134 1         10 print "\n";
135 1         1 foreach my $room (@{ $this->{rooms} }) {
  1         4  
136 20         34 foreach my $dir (qw(n s e w u d)) {
137 120         304 my $dest = $room->exit($dir);
138 120         150 my $dnum;
139              
140 120 100       186 if (defined $dest) {
141 30         59 $dnum = $this->resolve_room($dest, 'exit');
142             } else {
143 90         105 $dnum = 0;
144             }
145              
146 120         1139 print "$dnum ";
147             }
148              
149 20         56 my $desc = $room->desc();
150 20         31 chomp($desc);
151 20         29 $desc =~ s/\"/\'/g;
152 20         195 print qq["$desc"\n];
153             }
154              
155             # Messages
156 1         10 print "\n";
157 1         3 foreach my $msg (@{ $this->{messages} }) {
  1         3  
158 46         58 $msg =~ s/\"/\'/g;
159 46         437 print qq["$msg"\n];
160             }
161              
162             # Items. Each is a quoted description and item number.
163 1         10 print "\n";
164 1         2 foreach my $item (@{ $this->{items} }) {
  1         4  
165 33         94 my $desc = $item->desc();
166 33         43 chomp($desc);
167 33         51 $desc =~ s/\"/\'/g;
168 33         77 my $roomname = $item->where();
169 33 100       89 my $roomnum = !defined $roomname ? 0 :
170             $this->resolve_room($roomname, 'position');
171 33         83 my $getdrop = $item->getdrop();
172 33 100       75 if (defined $getdrop) {
173 14 50       28 my $canonicalisedNoun = $this->resolve_noun($getdrop)
174             or die "can't canonicalise gendrop '$getdrop'";
175 14         26 $getdrop = $this->{nouns}->[$canonicalisedNoun];
176 14         29 $desc .= '/' . uc($getdrop) . '/';
177             }
178 33         367 print qq["$desc" $roomnum\n];
179             }
180              
181             # Comments. One per compiled action
182 1         11 print "\n";
183 1         3 foreach my $comment (@compiled_comments) {
184 71         717 print qq["$comment"\n];
185             }
186              
187             # Trailer.
188 1         12 print "\n";
189 1         11 print $version, "\n"; # version number
190 1         11 print $ident, "\n"; # ident number
191 1         26 print 0, "\n"; # unknown additional magic number
192             }
193              
194              
195             sub resolve_room {
196 76     76 0 97 my $this = shift();
197 76         115 my($roomname, $caption) = @_;
198              
199 76 50       136 _fatal("$caption room not defined!")
200             if !defined $roomname;
201              
202 76         146 my $room = $this->{roomname}->{$roomname};
203 76 50       142 _fatal("$caption room '$roomname' doesn't exist!")
204             if !defined $room;
205              
206 76         198 return $room->num();
207             }
208              
209              
210             sub resolve_item {
211 67     67 0 81 my $this = shift();
212 67         92 my($itemname, $caption) = @_;
213              
214 67 50       138 _fatal("$caption item not defined!")
215             if !defined $itemname;
216              
217 67         118 my $item = $this->{itemname}->{$itemname};
218 67 50       120 _fatal("$caption item '$itemname' doesn't exist!")
219             if !defined $item;
220              
221 67         172 return $item->num();
222             }
223              
224              
225             sub resolve_message {
226 55     55 0 74 my $this = shift();
227 55         71 my($msg) = @_;
228              
229 55 50       117 if (!defined $msg) {
230 0         0 warn "ignoring empty message";
231 0         0 return 0;
232             }
233              
234 55         100 my $val = $this->{msgmap}->{$msg};
235 55 100       103 if (!defined $val) {
236 46         47 $val = @{ $this->{messages} };
  46         78  
237 46         55 push @{ $this->{messages} }, $msg;
  46         84  
238 46         130 $this->{msgmap}->{$msg} = $val;
239             }
240              
241 55         127 return $val;
242             }
243              
244              
245             sub swap_items {
246 0     0 0 0 my $this = shift();
247 0         0 my($slot1, $slot2) = @_;
248              
249 0         0 my $tmpitem = $this->{items}->[$slot2];
250 0         0 $this->{items}->[$slot2] = $this->{items}->[$slot1];
251 0         0 $this->{items}->[$slot1] = $tmpitem;
252              
253 0         0 foreach my $slot ($slot1, $slot2) {
254             # Cheeky back-door patch-up of number
255 0         0 $this->{items}->[$slot]->{num} = $slot;
256              
257             # Patch up object-name resolution
258 0         0 my $name = $this->{items}->[$slot]->name();
259 0         0 $this->{itemname}->{$name} = $this->{items}->[$slot];
260             }
261             }
262              
263              
264             # PRIVATE to the _compile() method.
265             #
266             sub _compile_vocab {
267 1     1   2 my $this = shift();
268              
269 1         8 my @verbs = $this->_make_wordlist('v', ([ '', 0 ],
270             [ 'GO', 1 ],
271             [ 'GET', 10 ],
272             [ 'DROP', 18 ]));
273 1         7 $this->{verbs} = \@verbs;
274 1         10 my @nouns = $this->_make_wordlist('n', ([ '', 0 ],
275             [ 'NORTH', 1 ],
276             [ 'SOUTH', 2 ],
277             [ 'EAST', 3 ],
278             [ 'WEST', 4 ],
279             [ 'UP', 5 ],
280             [ 'DOWN', 6 ]));
281 1         8 $this->{nouns} = \@nouns;
282              
283             # Find DVN (Difference between Verbs and Nouns)
284 1         3 my $dvn = @{ $this->{verbs} } - @{ $this->{nouns} };
  1         3  
  1         3  
285 1 50       12 if ($dvn > 0) {
    50          
286 0         0 push @{ $this->{nouns} }, map '', 1..$dvn;
  0         0  
287             } elsif ($dvn < 0) {
288 1         2 push @{ $this->{verbs} }, map '', 1..-$dvn;
  1         11  
289             }
290              
291 1         4 return scalar(@ { $this->{verbs} });
  1         3  
292             }
293              
294              
295             # PRIVATE to the _compile_vocab() method.
296             sub _make_wordlist {
297 2     2   4 my $this = shift();
298 2         7 my($type, @specials) = @_;
299              
300 2         2 my @words;
301 2         5 foreach my $ref (@specials) {
302 11         17 my($word, $index) = @$ref;
303 11         23 my @list = $this->_extract_synonyms($type, $word);
304 11         32 $this->_insert_words($type, \@words, $index, @list);
305             }
306              
307             # Add non-specials. We could do a better job than this of fitting
308             # the various-sized synonym-sets into the available slots, but
309             # let's not lose sleep over it.
310 2         6 my $vocab = $this->{$type . 'vocab'};
311 2         10 foreach my $key (keys %$vocab) {
312 42         44 my @list = ($key, @{ $vocab->{$key} });
  42         99  
313              
314             # Find first area big enough to fit all the words in
315 42         55 my $index = 1;
316 42         47 while (1) {
317 668 50       1200 die "no slots lower that 1000 for '$key'" if $index == 1000;
318 668         656 my $i;
319 668         1403 for ($i = 0; $i < @list; $i++) {
320 668 100       1419 last if defined $words[$index+$i];
321             }
322 668 100       1348 last if $i == @list;
323 626         830 $index++;
324             }
325             #warn "found slot $index for $type '$key'";
326 42         103 $this->_insert_words($type, \@words, $index, @list);
327             }
328              
329 2         21 return @words;
330             }
331              
332              
333             # PRIVATE to the _make_wordlist() method.
334             sub _extract_synonyms {
335 11     11   16 my $this = shift();
336 11         13 my($type, $word) = @_;
337              
338 11         17 my $vocab = $this->{$type . 'vocab'};
339 11         18 my $listref = $vocab->{$word};
340 11 100       24 if (defined $listref) {
341             # Lucky guess: it was head of its list
342 3         6 delete $vocab->{$word};
343 3         10 return ($word, @$listref);
344             }
345              
346             # Check if its in the RHS of any of the lists. This is a slow
347             # algorithm, but that's not going to be big problem.
348 8         39 foreach my $key (keys %$vocab) {
349 201         237 my $listref = $vocab->{$key};
350 201 50       489 if (grep { $_ eq $word } @$listref) {
  0         0  
351 0         0 delete $vocab->{$key};
352 0         0 return ($key, @$listref);
353             }
354             }
355              
356             # Not found at all: must be an unreferenced special
357 8         31 return ($word);
358             }
359              
360              
361             # PRIVATE to the _make_wordlist() method.
362             sub _insert_words {
363 53     53   68 my $this = shift();
364 53         90 my ($type, $wordsref, $index, @list) = @_;
365            
366 53         134 for (my $i = 0; $i < @list; $i++) {
367 53         69 my $word = $list[$i];
368 53 50       121 die "no slot for special '$word'"
369             if defined $wordsref->[$index+$i];
370 53 50       106 $word = '*' . $word if $i > 0;
371 53         115 $wordsref->[$index+$i] = $word;
372             #warn "inserted $type '$word' at " . ($index+$i);
373 53         308 $this->{$type . 'map'}->{$word} = $index+$i;
374             }
375             }
376              
377              
378             # The next pair of methods get called back from
379             # Games::ScottAdams::Action::compile()
380             sub resolve_verb {
381 65     65 0 78 my $this = shift();
382 65         163 return $this->_resolve_word(@_, 'verb', $this->{verbs}, $this->{vmap});
383             }
384              
385             sub resolve_noun {
386 61     61 0 76 my $this = shift();
387 61         149 return $this->_resolve_word(@_, 'noun', $this->{nouns}, $this->{nmap});
388             }
389              
390             # PRIVATE to the resolve_{verb,noun}() methods
391             sub _resolve_word {
392 126     126   162 my $this = shift();
393 126         200 my($word, $caption, $aref, $href) = @_;
394              
395 126 100       291 return 0
396             if !defined $word;
397              
398 100         130 $word = uc($word);
399 100         139 my $val = $href->{$word};
400 100 50       181 if (!defined $val) {
401 0         0 $val = $href->{'*' . $word};
402             }
403              
404 100 50       177 die "impossible: $caption '$word' undefined"
405             if !defined $val;
406              
407             # If we specified a synonym, revert to the type word
408 100   33     462 while ($val > 0 && $aref->[$val] =~ /^\*/) {
409 0         0 $val--;
410             }
411              
412 100         291 return $val;
413             }
414              
415              
416             # PRIVATE to Compile.pm
417             sub _fatal {
418 0     0     return Games::ScottAdams::File::fatal(undef, @_);
419             }
420              
421              
422             1;