File Coverage

blib/lib/Games/ScottAdams/Parse.pm
Criterion Covered Total %
statement 173 202 85.6
branch 70 96 72.9
condition 1 3 33.3
subroutine 23 24 95.8
pod 0 1 0.0
total 267 326 81.9


line stmt bran cond sub pod time code
1             # $Id: Parse.pm,v 1.3 2006/11/03 21:00:13 mike Exp $
2              
3             # Parse.pm - parsing functions for Scott Adams game files.
4              
5             package Games::ScottAdams::Game;
6 1     1   8 use strict;
  1         3  
  1         48  
7              
8 1     1   1113 use Games::ScottAdams::File;
  1         4  
  1         3609  
9              
10              
11             sub parse {
12 5     5 0 80 my $this = shift();
13 5         8 my($filename) = @_;
14              
15             #warn "parsing '$filename'";
16 5 50       29 my $fh = new Games::ScottAdams::File($filename)
17             or die "can't open '$filename': $!";
18              
19 5         17 while (defined ($_ = $fh->getline(1))) {
20 252         393 s/^\s+%/%/; # Skip any whitespace before leading %
21 252 100       2903 if (/^%room (.*)/i) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    0          
22 19         55 $this->_parse_room($fh, $1);
23             } elsif (/^%exit\s+(.*)\s+(.*)/i) {
24 30         84 $this->_parse_exit($fh, $1, $2);
25             } elsif (/^%item\s+(.*)/i) {
26 33         80 $this->_parse_item($fh, $1);
27             } elsif (/^%getdrop\s+(.*)/i) {
28 14         35 $this->_parse_getdrop($fh, $1);
29             } elsif (/^%(at|nowhere)\s*(.*)/i) {
30 11         29 $this->_parse_at($fh, $2);
31             } elsif (/^%start\s+(.*)/i) {
32 1         4 $this->_parse_start($fh, $1);
33             } elsif (/^%treasury\s+(.*)/i) {
34 1         5 $this->_parse_treasury($fh, $1);
35             } elsif (/^%maxload\s+(.*)/i) {
36 1         5 $this->_parse_maxload($fh, $1);
37             } elsif (/^%lighttime\s+(.*)/i) {
38 1         4 $this->_parse_lighttime($fh, $1);
39             } elsif (/^%ident\s+(.*)/i) {
40 1         5 $this->_parse_ident($fh, $1);
41             } elsif (/^%version\s+(.*)/i) {
42 1         4 $this->_parse_version($fh, $1);
43             } elsif (/^%wordlen\s+(.*)/i) {
44 1         5 $this->_parse_wordlen($fh, $1);
45             } elsif (/^%lightsource\s+(.*)/i) {
46 0         0 $this->_parse_lightsource($fh, $1);
47             } elsif (/^%action\s+(.*)\s+(.*)/i) {
48 39         92 $this->_parse_action($fh, $1, $2);
49             } elsif (/^%action\s+(.*)/i) {
50 8         24 $this->_parse_action($fh, $1, undef);
51             } elsif (/^%occur\s*(.*)/i) {
52 18         44 $this->_parse_action($fh, undef, $1);
53             } elsif (/^%result/i) {
54 65         150 $this->_parse_result($fh);
55             } elsif (/^%comment\s+(.*)/i) {
56 4         25 $this->_parse_comment($fh, $1);
57             } elsif (/^%([nv])alias\s+(.*)\s+(.*)/i) {
58 0         0 $this->_parse_alias($fh, lc($1), $2, $3);
59             } elsif (/^%include\s+(.*)/i) {
60 4         9 my $newfile = $1;
61             #warn "%include '$newfile'";
62             # Interpret filenames relative to directory of current file
63 4 50       14 if ($filename =~ m@/@) {
64 4         6 my $prefix = $filename;
65 4         17 $prefix =~ s@(.*)/.*@$1@;
66 4         10 $newfile = "$prefix/$newfile";
67             }
68 4         27 $this->parse($newfile);
69             } elsif (!/^%/) {
70 0         0 $fh->warn("expected directive, got '$_' (ignored)");
71             } else {
72 0         0 $fh->warn("unrecognised directive (ignored): '$_'");
73             }
74             }
75              
76 5         14 $this->_coalesce_aliases();
77 5         108 return 1;
78             }
79              
80              
81             # PRIVATE to the parse() method
82             sub _parse_room {
83 19     19   21 my $this = shift();
84 19         38 my($fh, $name) = @_;
85              
86 19         28 my $desc = '';
87 19         51 while (defined (my $line = $fh->getline(1))) {
88 43 100       131 if ($line =~ /^\s*%/) {
89 19         51 $fh->ungetline($line);
90 19         25 last;
91             }
92              
93 24         142 $desc .= "$line\n";
94             }
95              
96 19         25 my $num = @{ $this->{rooms} }; # 0-based index of room to be added
  19         40  
97 19         107 my $room = new Games::ScottAdams::Room($name, $desc, $num);
98 19         29 push @{ $this->{rooms} }, $room;
  19         43  
99 19 50       55 if (defined $this->{roomname}->{$name}) {
100 0         0 $fh->warn("discarding old room '$name'");
101             }
102              
103 19         48 $this->{roomname}->{$name} = $room;
104 19         27 $this->{_room} = $room;
105 19 100       99 $this->{_roomname1} = $name
106             if !defined $this->{_roomname1};
107             }
108              
109              
110             # PRIVATE to the parse() method
111             sub _parse_exit {
112 30     30   41 my $this = shift();
113 30         73 my($fh, $dir, $dest) = @_;
114              
115 30         45 my $room = $this->{_room};
116 30 50       66 if (!defined $room) {
117 0         0 $fh->warn("ignoring %exit '$dir'->'$dest' before first room");
118 0         0 return;
119             }
120              
121 30         80 my $roomname = $room->name();
122 30         60 $dir = lc(substr($dir, 0, 1));
123 30 50       96 if ($dir !~ /^[nsewud]$/) {
124 0         0 $fh->warn("ignoring %exit '$dir'->'$dest' at '$roomname'");
125 0         0 return;
126             }
127              
128 30         82 my $old = $room->exit($dir);
129 30 50       62 if (defined $old) {
130 0         0 $fh->warn("discarding old exit '$dir'->'$old' at '$roomname'");
131             }
132              
133 30         82 $room->exit($dir, $dest);
134             }
135              
136              
137             # PRIVATE to the parse() method
138             sub _parse_item {
139 33     33   44 my $this = shift();
140 33         61 my($fh, $name) = @_;
141              
142 33         38 my $where = undef; # item is initially nowhere
143 33         55 my $room = $this->{_room};
144 33 50       66 if (defined $room) {
145 33         95 $where = $room->name();
146             }
147              
148 33         92 my $desc = $fh->getline(1);
149 33         90 while (defined (my $line = $fh->getline(1))) {
150 33 50       127 if ($line =~ /^\s*%/) {
151 33         85 $fh->ungetline($line);
152 33         53 last;
153             }
154              
155 0         0 $desc .= "\n$line";
156             }
157              
158 33         38 my $num = @{ $this->{items} }; # 0-based index of item to be added
  33         59  
159 33         39 if (0) {
160             ### No need to do this, is there?
161             if ($num == 9) {
162             # Leave slot 9 free for the light-source
163             my $nothing = new Games::ScottAdams::Item('', '', $num++);
164             push @{ $this->{items} }, $nothing;
165             }
166             }
167              
168 33         127 my $item = new Games::ScottAdams::Item($name, $desc, $num, $where);
169 33         48 push @{ $this->{items} }, $item;
  33         67  
170 33 50       97 if (defined $this->{itemname}->{$name}) {
171 0         0 $fh->warn("discarding old item '$name'");
172             }
173              
174 33         86 $this->{itemname}->{$name} = $item;
175 33         49 $this->{_item} = $item;
176 33 100       158 if ($desc =~ /^\*/) {
177 3         14 $this->{ntreasures}++;
178             }
179             }
180              
181              
182             # PRIVATE to the parse() method
183             sub _parse_getdrop {
184 14     14   22 my $this = shift();
185 14         22 my($fh, $name) = @_;
186              
187 14         25 my $item = $this->{_item};
188 14 50       30 if (!defined $item) {
189 0         0 $fh->warn("ignoring %getdrop '$name' before first item");
190 0         0 return;
191             }
192              
193 14         40 my $itemname = $item->name();
194 14         41 my $old = $item->getdrop();
195 14 50       30 if (defined $old) {
196 0         0 $fh->warn("discarding old getdrop '$old' for '$itemname'");
197             }
198              
199 14         36 $item->getdrop($name);
200 14         34 $this->_parse_alias($fh, 'n', $name);
201             }
202              
203              
204             # PRIVATE to the parse() method
205             sub _parse_at {
206 11     11   16 my $this = shift();
207 11         22 my($fh, $where) = @_;
208              
209 11         18 my $item = $this->{_item};
210 11 50       23 if (!defined $item) {
211 0         0 $fh->warn("ignoring %at '$where' before first item");
212 0         0 return;
213             }
214              
215 11         32 my $itemname = $item->name();
216 11         31 my $old = $item->where();
217 11 50 33     61 if (defined $old && $where ne '') {
218             #$fh->warn("replacing location '$old' with '$where' for '$itemname'");
219             }
220              
221 11         31 $item->where($where);
222             }
223              
224              
225             # All the following wrappers are PRIVATE to the parse() method
226             sub _parse_start {
227 1     1   12 return _parse_param(@_, 'start', 'start room'); }
228             sub _parse_treasury {
229 1     1   3 return _parse_param(@_, 'treasury', 'treasury room'); }
230             sub _parse_maxload {
231 1     1   3 return _parse_param(@_, 'maxload', 'maximum load'); }
232             sub _parse_lighttime {
233 1     1   4 return _parse_param(@_, 'lighttime', 'light duration'); }
234             sub _parse_ident {
235 1     1   6 return _parse_param(@_, 'ident', 'adventure identifier'); }
236             sub _parse_version {
237 1     1   5 return _parse_param(@_, 'version', 'version number'); }
238             sub _parse_wordlen {
239 1     1   4 return _parse_param(@_, 'wordlen', 'word length'); }
240             sub _parse_lightsource {
241 0     0   0 return _parse_param(@_, 'lightsource', 'light source'); }
242              
243              
244             # PRIVATE to the _parse_{start...wordlen}() methods
245             sub _parse_param {
246 7     7   10 my $this = shift();
247 7         15 my($fh, $value, $param, $caption) = @_;
248              
249 7 50       19 if (defined $this->{$param}) {
250 0         0 $fh->warn("discarding old $caption '", $this->{$param}, "'");
251             }
252              
253 7         32 $this->{$param} = $value;
254             }
255              
256              
257             # PRIVATE to the parse() method
258             sub _parse_action {
259 65     65   83 my $this = shift();
260 65         142 my($fh, $verb, $noun) = @_;
261              
262 65         70 my $num = @{ $this->{actions} }; # 0-based index of action to be added
  65         124  
263 65         222 my $action = new Games::ScottAdams::Action($verb, $noun, $num);
264 65         91 push @{ $this->{actions} }, $action;
  65         144  
265 65         113 $this->{_action} = $action;
266              
267             # Register noun and verb
268 65 100       169 $this->_parse_alias($fh, 'v', $verb) if $verb;
269 65 100       183 $this->_parse_alias($fh, 'n', $noun) if $noun;
270              
271 65         196 while (defined (my $line = $fh->getline(1))) {
272 124 100       384 if ($line =~ /^\s*%/) {
273 65         173 $fh->ungetline($line);
274 65         104 last;
275             }
276 59         160 $action->add_cond($line);
277             }
278              
279 65         212 return;
280             }
281              
282              
283             # PRIVATE to the parse() method
284             sub _parse_result {
285 65     65   83 my $this = shift();
286 65         67 my($fh) = @_;
287              
288 65         110 my $action = $this->{_action};
289 65 50       133 if (!defined $action) {
290 0         0 $fh->warn("ignoring %result before first action");
291 0         0 return;
292             }
293              
294 65         163 while (defined (my $line = $fh->getline(1))) {
295 185 100       503 if ($line =~ /^\s*%/) {
296 64         183 $fh->ungetline($line);
297 64         246 last;
298             }
299 121         308 $action->add_result($line);
300             }
301             }
302              
303              
304             # PRIVATE to the parse() method
305             sub _parse_comment {
306 4     4   8 my $this = shift();
307 4         8 my($fh, $comment) = @_;
308              
309 4         8 my $action = $this->{_action};
310 4 50       10 if (!defined $action) {
311 0         0 $fh->warn("ignoring %comment before first %action");
312 0         0 return;
313             }
314              
315 4         13 my $old = $action->comment();
316 4 50       11 if (defined $old) {
317 0         0 $fh->warn("discarding old comment '$old'");
318             }
319              
320 4         12 $action->comment($comment);
321             }
322              
323              
324             # PRIVATE to the parse() method
325             sub _parse_alias {
326 100     100   119 my $this = shift();
327 100         109 my $fh = shift();
328 100         134 my $type = lc(shift());
329 100         158 my @words = map { uc() } @_;
  100         298  
330              
331 100         197 my $href = $this->{$type . 'vocab'};
332 100         156 for my $word (@words) {
333 100 50       184 $fh->fatal("empty word") if !$word;
334 100         96 push @{ $href->{$word} }, grep { $_ ne $word } @words;
  100         271  
  100         436  
335             }
336             }
337              
338              
339             # PRIVATE to the parse() method
340             #
341             # At this point, we have a bunch of equivalence classes for each
342             # vocabulary set, we we need to coalesce them. For example, if we
343             # have the following in the source --
344             # %valias enter go
345             # %valias run go
346             # %valias walk go
347             # We'll have a {vvocab} hash that looks like this --
348             # RUN -> GO
349             # GO -> ENTER,RUN,WALK
350             # ENTER -> GO
351             # WALK -> GO
352             # But we want a single list of all four words
353              
354             sub _coalesce_aliases {
355 5     5   6 my $this = shift();
356              
357 5         12 $this->{vvocab} = _extend_lists($this->{vvocab});
358 5         20 $this->{nvocab} = _extend_lists($this->{nvocab});
359             }
360              
361             # PRIVATE to the _coalesce_aliases() method
362             sub _extend_lists {
363 10     10   13 my($vocab) = @_;
364              
365 10         47 my @keys = keys %$vocab; # we're going to change this
366 10         20 foreach my $key (@keys) {
367             #warn "considering aliases for '$key'";
368 89 50       196 next if !exists $vocab->{$key};
369             #warn "thinking about '$key'";
370 89         196 my @list = _equivalents($vocab, $key, { $key => 1});
371             #warn "\t" . join(' ', @list);
372 89         159 foreach my $used (@list) {
373 0         0 delete $vocab->{$used};
374             #warn "deleted '$used'";
375             }
376 89         205 $vocab->{$key} = [ @list ];
377             }
378              
379 10         30 return $vocab;
380             }
381              
382             # PRIVATE to the _extend_lists method()
383             sub _equivalents {
384 89     89   113 my($vocab, $word, $seen) = @_;
385              
386 89         88 my @equivalents;
387 89         113 my $cref = $vocab->{$word};
388 89         137 foreach my $candidate (@$cref) {
389 0 0       0 next if exists $seen->{$candidate};
390 0         0 $seen->{$candidate} = 1;
391 0         0 my @sub = _equivalents($vocab, $candidate, $seen);
392 0         0 push @equivalents, $candidate, @sub;
393             }
394              
395 89         155 return @equivalents;
396             }
397              
398              
399             1;