File Coverage

blib/lib/Chess/PGN/Parse.pm
Criterion Covered Total %
statement 174 502 34.6
branch 53 226 23.4
condition 6 65 9.2
subroutine 15 58 25.8
pod 42 42 100.0
total 290 893 32.4


line stmt bran cond sub pod time code
1             ############################################################
2             #
3             # Chess:PGN::Parse - a parser for PGN games
4             #
5             # Copyright (c) 2002 by Giuseppe Maxia
6             # Produced under the GPL (Golden Perl Laziness)
7             # Distributed under the GPL (GNU General Public License)
8             #
9             ############################################################
10              
11             # StringHandle
12             # Utility package to read input from string, imitating
13             # a file handle.
14             package StringHandle;
15 1     1   1035 use strict;
  1         2  
  1         39  
16 1     1   5 use warnings;
  1         1  
  1         61  
17             use overload
18             q{<>} => sub {
19 0     0   0 return shift @{$_[0]};
  0         0  
20 1     1   2240 };
  1         2039  
  1         9  
21            
22             sub new {
23 0     0   0 my $class = shift;
24 0         0 return bless [split /^/xm, $_[0]], $class;
25             }
26 0     0   0 sub close { } ## no critic
27            
28             package Chess::PGN::Parse; ## no critic
29 1     1   1130 use English qw( -no_match_vars ) ;
  1         13576  
  1         8  
30              
31             require 5.006;
32 1     1   2209 use IO::File;
  1         18202  
  1         9623  
33              
34             require Exporter;
35              
36             our @ISA = qw(Exporter);
37             our @EXPORT = qw(shrink_epd expand_epd STR NAG);
38             our @EXPORT_OK = qw();
39              
40             our $VERSION = '0.19'; # 10-jan-2006
41              
42             =head1 NAME
43              
44             Chess::PGN::Parse - reads and parses PGN (Portable Game Notation) Chess files
45              
46             =head1 SYNOPSIS
47              
48             use Chess::PGN::Parse;
49             use English qw( -no_match_vars );
50             my $pgnfile = "kk_2001.pgn";
51             my $pgn = new Chess::PGN::Parse $pgnfile
52             or die "can't open $pgnfile\n";
53             while ($pgn->read_game()) {
54             print $pgn->white, ", " , $pgn->black, ", ",
55             $pgn->result, ", ",
56             $pgn->game, "\n";
57             }
58              
59              
60             use Chess::PGN::Parse;
61             my $text ="";
62             {
63             local $INPUT_RECORD_SEPARATOR = undef;
64             open PGN "< $pgnfile" or die;
65             $text = ;
66             close $text;
67             }
68             # reads from string instead of a file
69             my $pgn = new Chess::PGN::Parse undef, $text;
70             while ($pgn->read_game()) {
71             print $pgn->white, ", " , $pgn->black, ", ",
72             $pgn->result, ", ",
73             $pgn->game, "\n";
74             }
75              
76             use Chess::PGN::Parse;
77             my $pgnfile = "kk_2001.pgn";
78             my $pgn = new Chess::PGN::Parse $pgnfile
79             or die "can't open $pgnfile\n";
80             my @games = $pgn->smart_read_all();
81              
82              
83             =head1 DESCRIPTION
84              
85             Chess::PGN::Parse offers a range of methods to read and manipulate Portable Game Notation files.
86             PGN files contain chess games produced by chess programs following a standard format (http://www.schachprobleme.de/chessml/faq/pgn/). It is among the preferred means of chess games distribution. Being a public, well established standard, PGN is understood by many chess archive programs.
87             Parsing simple PGN files is not difficult. However, dealing with some of the intricacies of the Standard is less than trivial. This module offers a clean handle toward reading and parsing complex PGN files.
88              
89             A PGN file has several B, which are key/values pairs at the header of each game, in the format
90             [key "value"]
91              
92             After the header, the game follows. A string of numbered chess moves, optionally interrupted by braced comments and recursive parenthesized variants and comments. While dealing with simple braced comments is straightforward, parsing nested comments can give you more than a headache.
93              
94             Chess::PGN::Parse most immediate methods are:
95             read_game() reads one game, separating the tags and the game text.
96              
97             parse_game() parse the current game, and stores the moves into an
98             array and optionally saves the comments into an array of hashes
99             for furter usage. It can deal with nested comments and recursive
100             variations.
101              
102             quick_parse_game() Same as the above, but doesn't save the comments,
103             which are just stripped from the text. It can't deal with nested
104             comments. Should be the preferred method when we know that we are
105             dealing with simple PGNs.
106              
107             smart_parse_game() Best of the above methods. A preliminary check
108             will call parse_game() or quick_parse_game(), depending on the
109             presence of nested comments in the game.
110              
111             read_all(), quick_read_all(), smart_read_all() will read all the records
112             in the current PGN file and return an array of hashes with all the
113             parsed details from the games.
114              
115             =head2 Parsing games
116              
117             Parsing PGN games is actually two actions: reading and parsing.
118             The reading will only identify the two components of a game, i.e.
119             the tags and the moves text. During this phase, the tags are
120             decomposed and stored into an internal hash for future use,
121             while the game text is left untouched.
122              
123             Reading a game is accomplished through the read_game() method,
124             which will identify not only the standard game format but also
125             some unorthodox cases, such as games with no separating blank line
126             between tags and moves, games with no blank lines at the end of
127             the moves, leading blank lines, tags spanning over several lines
128             and some minor quibbles.
129             If you know that your games don't have any of these problems,
130             you might choose the read_standard_game() method, which is a
131             bit faster.
132              
133             After the reading, you can either use the game text as it is,
134             or you can ask for parsing. What is it? Parsing is the process
135             of identifying and isolating the moves from the rest of the game
136             text, such as comments and recursive variations. This process
137             can be accomplished in two ways: using quick_parse_game(), the
138             non moves elements are just stripped off and discarded, leaving
139             an array of bare moves. If the comments and the recursive
140             variations (RAV) are valuable to you, you can use the parse_game()
141             method, which will strip the excess text, but it can store it
142             into an appropriate data structure. Passing the option
143             {save_comments =>'yes'} to parse_game(), game comments will
144             be stored into a hash, having as key the move number + color.
145             Multiple comments for the same move are appended to the previous
146             one. If this structure doesn't provide enough details, a further
147             option {comments_struct => 'array'} will store an array of
148             comments for each move. Even more details are available using
149             {comments_struct => 'hol'}, which will trigger the creation of
150             a hash of lists (hol), where the key is the comment type (RAV,
151             NAG, brace, semicolon, escaped) and the value is a list of
152             homogeneous comments belonging to the same move.
153              
154             A further option {log_errors => 'yes'} will save the errors
155             into a structure similar to the comments (no options on the
156             format, though. All errors for one given move are just a
157             string). What are errors? Just anything that is not recognized
158             as any of the previous elements. Not a move, or a move number,
159             or a comment, either text or recursive. Anything that the
160             parser cannot actively classify as 'known' will be stored
161             as error.
162              
163             =head2 Getting the parsed values
164              
165             At the end of the exercise, you can access the components
166             through some standard methods.
167             The standard tags have their direct access method (white,
168             black, site, event, date, result, round). More methods give
169             access to some commonly used elements:
170             game() is the unparsed text, moves() returns an array of parsed
171             moves, without move numbers, comments() and errors() return
172             the relative structures after parsing.
173             About game(), it's worth mentioning that, using quick_parse_game(),
174             the game text is stripped of all non moves elements. This is
175             an intended feature, to privilege speed. If you need to preserve
176             the original game text after parsing, either copy it before
177             calling quick_parse_game() or use parse_game() instead.
178              
179             =head2 Recursive Parsing
180              
181             PGN games may include RAV (Recursive Annotated Variations) which
182             is just game text inside parentheses.
183             This module can recognize RAV sequences and store them as comments.
184             One of the things you can do with these sequences is to parse
185             them again and get bare moves that you can feed to a chess engine
186             or a move analyzer (Chess::PGN::EPD by H.S.Myers is one of them).
187             Chess::PGN::Parse does not directly support recursive parsing of
188             games, but it makes it possible.
189             Parse a game, saving the comments as hash of list (see above),
190             and then check for comments that are of 'RAV' type. For each
191             entry in the comments array, strip the surrounding parentheses
192             and create a new Chess::PGN::Parse object with that text.
193             Easier to do than to describe, actually. For an example of this
194             technique, check the file F.
195              
196             =head2 EXPORT
197              
198             new, STR, read_game, tags, event, site, white, black, round, date, result, game , NAG, moves
199              
200             =head2 DEPENDENCIES
201              
202             IO::File
203              
204             =head1 Class methods
205              
206             =over 4
207              
208             =item new()
209              
210             Create a new Chess::PGN::Parse object (requires file name)
211             my $pgn = Chess::PGN::Parse->new "filename.pgn"
212             or die "no such file \n";
213              
214             =cut
215              
216             my @seven_tags_roster = qw(Event Site Date Round White Black Result);
217              
218             sub new {
219 2     2 1 158 my $class = shift;
220 2         4 my $filename = shift;
221 2         4 my $fh = undef;
222 2 50       10 if (defined $filename) {
223 2   50     32 $fh = new IO::File "< $filename"
224             || return ;
225             }
226             else {
227 0         0 my $text = shift;
228 0         0 $fh = new StringHandle $text;
229             }
230 2         14657 my $self = bless {
231             GameMoves =>[], # game moves
232             GameComments =>{}, # comments with reference to the move
233             gamedescr => {}, # will contain the PGN tags
234             GameErrors => {}, # will contain the parsing errors
235             fh => \$fh # filehandle to the PGN file
236             }, $class;
237 2         11 return $self;
238             }
239              
240             =for internal use
241             the object destroyer cleans possible hanging references
242              
243             =cut
244              
245             sub DESTROY {
246 2     2   79 my $self = shift;
247 2         5 undef $self->{GameComments};
248 2         4 undef $self->{GameErrors};
249 2         7 undef $self->{gamedescr};
250 2         18 undef $self->{GameMoves};
251 2         4 eval {
252             #if (defined ${$self->{fh}}) {
253 2         3 ${$self->{fh}}->close();
  2         22  
254             #}
255             };
256 2         70 undef $self->{fh};
257 2         128 return;
258             }
259             my %symbolic_annotation_glyph = (
260             q{$1} => q{!},
261             q{$2} => q{?},
262             q{$3} => q{!!},
263             q{$4} => q{??},
264             q{$5} => q{!?},
265             q{$6} => q{?!},
266             );
267              
268             my %numeric_annotation_glyph = ();
269              
270             =item NAG()
271             returns the corresponding Numeric Annotation Glyph
272              
273             =cut
274              
275             sub NAG {
276 0     0 1 0 my $item = shift;
277 0 0       0 return unless $item =~ /\$?(\d+)/x;
278 0 0 0     0 return if ($1 > 139) or ($1 < 0);
279 0 0       0 unless (scalar keys %numeric_annotation_glyph) {
280 0         0 local $INPUT_RECORD_SEPARATOR = undef;
281 0         0 eval ; ## no critic
282             }
283 0         0 my $nag_ref = \%numeric_annotation_glyph;
284 0 0 0     0 if (($1 > 0) and ($1 <=6)) {
285 0         0 $nag_ref = \%symbolic_annotation_glyph
286             }
287 0 0       0 if ($item =~ /^\$/x) {
288 0         0 return $nag_ref->{$item}
289             }
290             else {
291 0         0 return $nag_ref->{q{$}.$item}
292             }
293             }
294              
295             =item STR()
296              
297             returns the Seven Tags Roster array
298              
299             @array = $pgn->STR();
300             @array = PGNParser::STR();
301              
302             =cut
303              
304             sub STR {
305 0     0 1 0 return @seven_tags_roster;
306             }
307              
308             =item event()
309              
310             returns the Event tag
311              
312             =item site()
313              
314             returns the Site tag
315              
316             =item date()
317              
318             returns the Date tag
319              
320             =item white()
321              
322             returns the White tag
323              
324             =item black()
325              
326             returns the Black tag
327              
328             =item result()
329              
330             returns the result tag
331              
332             =item round()
333              
334             returns the Round tag
335              
336             =item game()
337              
338             returns the unparsed game moves
339              
340             =item time()
341              
342             returns the Time tag
343              
344             =item eco()
345              
346             returns the ECO tag
347              
348             =item eventdate()
349              
350             returns the EventDate tag
351              
352             =item moves()
353              
354             returns an array reference to the game moves (no numbers)
355             =cut
356              
357             =item comments()
358              
359             returns a hash reference to the game comments (the key is the move number and the value are the comments for such move)
360              
361             =cut
362              
363             =item errors()
364              
365             returns a hash reference to the game errors (the key is the move number and the value are the errors for such move)
366              
367             =item set_event()
368              
369             returns or modifies the Event tag
370              
371             =item set_site()
372              
373             returns or modifies the Site tag
374              
375             =item set_date()
376              
377             returns or modifies the Date tag
378              
379             =item set_white()
380              
381             returns or modifies the White tag
382              
383             =item set_black()
384              
385             returns or modifies the Black tag
386              
387             =item set_result()
388              
389             returns or modifies the result tag
390              
391             =item set_round()
392              
393             returns or modifies the Round tag
394              
395             =item set_game()
396              
397             returns or modifies the unparsed game moves
398              
399             =item set_time()
400              
401             returns or modifies the Time tag
402              
403             =item set_eco()
404              
405             returns or modifies the ECO tag
406              
407             =item set_eventdate()
408              
409             returns or modifies the EventDate tag
410              
411              
412             =item set_moves()
413              
414             returns or modifies an array reference to the game moves (no numbers)
415              
416             =cut
417              
418             sub event {
419 0     0 1 0 my $self = shift;
420 0         0 return $self->{gamedescr}{Event}
421             }
422              
423             sub site {
424 0     0 1 0 my $self = shift;
425 0         0 return $self->{gamedescr}{Site}
426             }
427              
428             sub date {
429 0     0 1 0 my $self = shift;
430 0         0 return $self->{gamedescr}{Date}
431             }
432              
433             sub white {
434 0     0 1 0 my $self = shift;
435 0         0 return $self->{gamedescr}{White}
436             }
437              
438             sub black {
439 0     0 1 0 my $self = shift;
440 0         0 return $self->{gamedescr}{Black}
441             }
442              
443             sub result {
444 0     0 1 0 my $self = shift;
445 0         0 return $self->{gamedescr}{Result}
446             }
447              
448             sub round {
449 0     0 1 0 my $self = shift;
450 0         0 return $self->{gamedescr}{Round}
451             }
452              
453             ## no critic
454             sub time {
455 0     0 1 0 my $self = shift;
456 0         0 return $self->{gamedescr}{Time}
457             }
458             ## use critic
459              
460             sub eventdate {
461 0     0 1 0 my $self = shift;
462 0         0 return $self->{gamedescr}{EventDate}
463             }
464              
465             sub eco {
466 0     0 1 0 my $self = shift;
467 0         0 return $self->{gamedescr}{ECO}
468             }
469              
470             sub game {
471 0     0 1 0 my $self = shift;
472 0         0 return $self->{gamedescr}{Game}
473             }
474              
475             sub moves {
476 0     0 1 0 my $self = shift;
477 0         0 return $self->{GameMoves};
478             }
479              
480              
481             sub set_event {
482 0     0 1 0 my $self = shift;
483 0 0       0 $self->{gamedescr}{Event} = $_[0] if @_;
484 0         0 return $self->{gamedescr}{Event}
485             }
486              
487             sub set_site {
488 0     0 1 0 my $self = shift;
489 0 0       0 $self->{gamedescr}{Site} = shift if @_;
490 0         0 return $self->{gamedescr}{Site}
491             }
492              
493             sub set_date {
494 0     0 1 0 my $self = shift;
495 0 0       0 $self->{gamedescr}{Date} = shift if @_;
496 0         0 return $self->{gamedescr}{Date}
497             }
498              
499             sub set_white {
500 0     0 1 0 my $self = shift;
501 0 0       0 $self->{gamedescr}{White} = shift if @_;
502 0         0 return $self->{gamedescr}{White}
503             }
504              
505             sub set_black {
506 0     0 1 0 my $self = shift;
507 0 0       0 $self->{gamedescr}{Black} = shift if @_;
508 0         0 return $self->{gamedescr}{Black}
509             }
510              
511             sub set_result {
512 0     0 1 0 my $self = shift;
513 0 0       0 $self->{gamedescr}{Result} = shift if @_;
514 0         0 return $self->{gamedescr}{Result}
515             }
516              
517             sub set_round {
518 0     0 1 0 my $self = shift;
519 0 0       0 $self->{gamedescr}{Round} = shift if @_;
520 0         0 return $self->{gamedescr}{Round}
521             }
522              
523             sub set_time {
524 0     0 1 0 my $self = shift;
525 0 0       0 $self->{gamedescr}{Time} = shift if @_;
526 0         0 return $self->{gamedescr}{Time}
527             }
528              
529             sub set_eventdate {
530 0     0 1 0 my $self = shift;
531 0 0       0 $self->{gamedescr}{EventDate} = shift if @_;
532 0         0 return $self->{gamedescr}{EventDate}
533             }
534              
535             sub set_eco {
536 0     0 1 0 my $self = shift;
537 0 0       0 $self->{gamedescr}{ECO} = shift if @_;
538 0         0 return $self->{gamedescr}{ECO}
539             }
540              
541             sub set_game {
542 0     0 1 0 my $self = shift;
543 0 0       0 $self->{gamedescr}{Game} = shift if @_;
544 0         0 return $self->{gamedescr}{Game}
545             }
546              
547             sub set_moves {
548 0     0 1 0 my $self = shift;
549 0 0 0     0 $self->{GameMoves} = shift if (@_ && (ref $_[0] eq 'ARRAY')) ;
550 0         0 return $self->{GameMoves};
551             }
552              
553             sub errors {
554 0     0 1 0 my $self = shift;
555 0         0 return $self->{GameErrors};
556             }
557              
558             sub comments {
559 0     0 1 0 my $self = shift;
560 0         0 return $self->{GameComments};
561             }
562              
563             =for internal use
564             initialize the pgn object fields.
565              
566             =cut
567              
568             sub _init {
569 23     23   25 my $self = shift;
570 23         27 for (keys %{$self->{gamedescr}}) {
  23         98  
571 273         524 $self->{gamedescr}{$_} = q{};
572             }
573 23 50       86 delete $self->{gamedescr}{FirstMove}
574             if exists $self->{gamedescr}{FirstMove};
575 23         42 undef $self->{GameMoves};
576 23         31 undef $self->{GameComments};
577 23         33 undef $self->{GameErrors}; # 0.07
578 23         41 return;
579             }
580              
581             =item tags()
582            
583             returns a hash reference to all the parsed tags
584              
585             $hash_ref = $pgn->tags();
586              
587             =cut
588              
589             sub tags {
590 0     0 1 0 my $self = shift;
591 0         0 return \%{$self->{gamedescr}};
  0         0  
592             }
593              
594             =item read_all()
595              
596             Will read and parse all the games in the current file and return a reference to an array of hashes.
597             Each hash item contains both the raw data and the parsed moves and comments
598              
599             Same parameters as for parse_game(). Default : discard comments
600              
601             my $games_ref = $pgn->read_all();
602              
603             =cut
604              
605             sub read_all {
606 1     1 1 43 my $self=shift;
607 1         2 my $params = shift;
608 1         3 my @games =();
609 1         11 while ($self->read_game()) {
610 1         6 $self->parse_game($params);
611 1         1 my %gd = %{$self->{gamedescr}};
  1         17  
612 1         4 $gd{GameComments} = $self->{GameComments};
613 1         3 $gd{GameErrors} = $self->{GameErrors};
614 1         5 $gd{GameMoves} = $self->{GameMoves};
615 1         5 push @games, \%gd;
616             }
617 1         1349 return \@games;
618             }
619              
620             =item quick_read_all()
621              
622             Will read and quick parse all the games in the current file and return a reference to an array of hashes.
623             Each hash item contains both the raw data and the parsed moves
624             Comments are discarded. Same parameters as for quick_parse_game().
625              
626             my $games_ref = $pgn->quick_read_all();
627              
628             =cut
629              
630             sub quick_read_all {
631 1     1 1 86 my $self=shift;
632 1         4 my $params = shift;
633 1         4 my @games =();
634 1         6 while ($self->read_game()) {
635 20         50 $self->quick_parse_game($params);
636 20         27 my %gd = %{$self->{gamedescr}};
  20         346  
637 20         58 $gd{GameMoves} = $self->{GameMoves};
638 20         73 push @games, \%gd;
639             }
640 1         8 return \@games;
641             }
642              
643             =item smart_read_all()
644              
645             Will read and quick parse all the games in the current file and return a reference to an array of hashes.
646             Each hash item contains both the raw data and the parsed moves
647             Comments are discarded. Calls smart_read_game() to decide which method is best to parse each given game.
648              
649             my $games_ref = $pgn->smart_read_all();
650              
651             =cut
652              
653             sub smart_read_all {
654 0     0 1 0 my $self=shift;
655 0         0 my $params = shift;
656 0         0 my @games =();
657 0         0 while ($self->read_game()) {
658 0         0 $self->smart_parse_game($params);
659 0         0 my %gd = %{$self->{gamedescr}};
  0         0  
660 0         0 $gd{GameMoves} = $self->{GameMoves};
661 0         0 push @games, \%gd;
662             }
663 0         0 return \@games;
664             }
665              
666              
667             =item read_game()
668              
669             reads the next game from the given PGN file.
670             Returns TRUE (1) if successful (= a game was read)
671             or FALSE (0) if no more games are available or
672             an unexpected EOF occurred before the end of parsing
673            
674             while ($pgn->read_game()) {
675             do_something_smart;
676             }
677            
678             It can read standard and in some cases even non-standard PGN
679             games. The following deviance from the standard are handled:
680            
681             1. no blank line between tags and moves;
682             2. no blank line between games
683             3. blank line(s) before a game (start of file)
684             4. multiple tags in the same line
685             5. tags spanning over more lines
686             (can't cumulate with rule 4)
687             6. No tags (only moves).
688             (can't cumulate with rule 2)
689             7. comments (starting with ";") outside the game text
690            
691             =cut
692              
693             #
694             # read_game() introduced in 0.07
695             #
696             { #closure for read_game
697             # this is the memory between loops. The
698             # reading engine recognizes some elements
699             # one line after.
700             # For example, game text ends when we
701             # read tags from the input. At this moment,
702             # we have to return from the method, but
703             # we must keep in memory what we have last read.
704             # This structure will also take care of the
705             # tags spanning over several lines.
706             my %memory = (
707             tag => q{},
708             utag => 0, # = unfinished tag
709             game => q{},
710             tag_printed => 0,
711             game_printed => 0,
712             );
713              
714             sub _process_game {
715 21     21   32 my $self = shift;
716 21 50       42 return 0 unless $memory{game};
717 21 50       58 $self->{gamedescr}{missing} .= 'tags' unless $memory{tag_printed};
718 21         30 $memory{tag_printed} = 0;
719 21         82 $self->{gamedescr}{Game} .= $memory{game};
720 21         28 $memory{game} = q{};
721 21         23 $memory{game_printed} =1;
722 21         72 return 1;
723             }
724              
725             sub _process_tag {
726 232     232   361 my $self = shift;
727 232 50       493 if ($memory{game}) {
728 0         0 $self->_process_game;
729             }
730 232 50       511 return 0 if $memory{utag};
731 232 50       788 if ($memory{tag} =~ tr/]// > 1) {
732             # deals with multiple tags in one line
733 0         0 $memory{tag} =~ s/\]\s?/\]\n/g;
734             }
735 232         1553 while ($memory{tag} =~ /\[(\w+)\s+"(.*)"\]\s*/g) {
736 232         3279 $self->{gamedescr}{$1} = $2;
737             }
738 232         282 $memory{tag_printed} =1;
739 232         614 $memory{tag} = q{};
740 232         383 $memory{game_printed} = 0;
741 232         869 return;
742             }
743              
744             sub read_game {
745 23     23 1 35 my $self = shift;
746 23         26 my $fh = ${$self->{fh}};
  23         76  
747 23         55 $self->_init();
748 23 50       58 $self->_process_tag if $memory{tag};
749 23 50       164 return $self->_process_game if $memory{game};
750 23         216 while (<$fh>) {
751             # handle semicolon comments
752 416 50       856 if (/^;/) {
753 0 0 0     0 if ($memory{game_printed} or (! $memory{game})) { # between games
    0          
754 0         0 chomp;
755 0         0 $self->{gamedescr}{Comment} .= $_ ;
756             # comments between games are saved as tags
757             }
758             elsif ($memory{game}){
759 0         0 $memory{game} .= $_;
760             }
761 0         0 next; # anything else is discarded.
762             }
763             # normalize tagless games
764 416 100       1496 if (/^\s*$/) {
765 41 100       85 if ($memory{game}) {
766             # handles comments with embedded newlines.
767 20 50       85 if (($memory{game} =~ tr/\{//) < ($memory{game} =~ tr/\}//) ) {
768 0         0 next;
769             }
770 20         114 return $self->_process_game;
771             }
772 21         235 next;
773             }
774             # deals with multi-line tags
775 375 50 66     3469 if ($memory{utag}) {
    100          
776 0         0 chomp;
777 0         0 $memory{tag} .= $_;
778 0         0 my $left_brackets = ($memory{tag} =~ tr/\[//);
779 0         0 my $right_brackets = ($memory{tag} =~ tr/\]//);
780 0 0       0 if ( $left_brackets == $right_brackets ) {
781 0         0 $memory{utag} = 0;
782 0         0 $memory{tag_printed} = 0;
783 0         0 $memory{tag} .= "\n";
784             }
785             }
786             elsif (/^\[/ && (! $memory{game})) {
787 232         305 my $left_brackets = tr/\[//;
788 232         300 my $right_brackets = tr/\]//;
789 232 50       376 if ($left_brackets == $right_brackets) {
    0          
790 232         440 $memory{tag} = $_;
791             }
792             elsif ($right_brackets > $left_brackets) {
793 0         0 warn "Parsing error at line $.\n";
794             }
795             else {
796 0         0 $memory{utag} = 1;
797 0         0 chomp;
798 0         0 $memory{tag} = $_;
799 0         0 $memory{tag_printed} =0;
800             }
801             }
802             else {
803 143         632 s/^\s*//;
804 143         263 $memory{game} .= $_;
805             }
806 375 100       1132 if ($memory{tag}) {
807 232 50       616 return $self->_process_game if $memory{game};
808 232         451 $self->_process_tag;
809             }
810             }
811 3 50       13 if ($memory{tag}) {
812 0         0 $self->_process_tag;
813             }
814 3 100       9 if ($memory{game}) {
815 1         5 return $self->_process_game;
816             }
817 2         9 return 0;
818             }
819             } # end read_game() closure
820              
821             =item read_standard_game()
822              
823             reads the next game from the given PGN file.
824             Returns TRUE (1) if successful (= a game was read)
825             or FALSE (0) if no more games are available or
826             an unexpected EOF occurred before the end of parsing
827            
828             while ($pgn->read_standard_game()) {
829             do_something_smart;
830             }
831              
832             This method deals only with well formed PGN games. Use
833             the more forgiving read_game() for PGN files that don't
834             fully respect the PGN standard.
835            
836             =cut
837              
838             sub read_standard_game {
839 0     0 1 0 my $self = shift;
840 0         0 my $fh = ${$self->{fh}};
  0         0  
841 0         0 $self->_init();
842 0         0 my $block = 1;
843             #return 0 if eof($fh); # changed in version 0.06
844 0         0 while (<$fh>) {
845 0 0       0 return 0 unless defined $_; # 0.06
846 0         0 chomp;
847 0 0       0 $block = 0 if /^\s*$/;
848 0 0       0 last unless $block;
849 0 0       0 last unless /\[(\w+)/;
850 0         0 my $tag = $1;
851 0 0       0 last unless /\"(.*)\"/;
852 0         0 my $value = $1;
853 0         0 $self->{gamedescr}{$tag} = $value;
854             }
855 0         0 $block = 1;
856             #return 0 if eof($fh); # changed in version 0.06
857 0 0       0 return 0 unless defined $_; # 0.06
858 0         0 while (<$fh>) {
859 0 0       0 return 0 unless defined $_; # 0.06
860 0 0       0 $block = 0 if /^\s*$/;
861 0 0       0 last unless $block;
862 0         0 $self->{gamedescr}{Game} .= $_;
863             }
864 0         0 return 1;
865             }
866              
867             =for internal use
868              
869             _get_tags() returns a list of tags depending on the parameters
870              
871             _get_format() returns a format to be used when printing tags
872              
873             _get_formatted_tag() returns a tag formatted according to the
874             given template.
875              
876             =cut
877              
878             sub _get_tags {
879 0     0   0 my $self = shift;
880 0         0 my $params = shift;
881 0         0 my @newtags=();
882 0         0 my %seen = (Game =>1);
883 0 0 0     0 if (exists $params->{all_tags}
    0          
884             and ($params->{all_tags} =~ /^(?:[Yy][Ee][Ss]|1)$/))
885             {
886 0         0 for (@seven_tags_roster) {
887 0         0 push @newtags, $_;
888 0         0 $seen{$_}++;
889             }
890 0         0 for (sort {lc $a cmp lc $b} keys %{$self->{gamedescr}}) {
  0         0  
  0         0  
891 0 0       0 push @newtags, $_ unless $seen{$_};
892             }
893             }
894             elsif (exists $params->{tags}) {
895 0         0 for (@{$params->{tags}}) {
  0         0  
896 0         0 push @newtags, $_;
897             }
898             }
899             else {
900 0         0 @newtags = @seven_tags_roster;
901             }
902 0         0 return @newtags;
903             }
904              
905              
906             sub _get_left_right {
907 0     0   0 my $pattern = shift;
908 0         0 my $format = shift;
909 0         0 my $left_delimiter = shift;
910 0         0 my $right_delimiter = shift;
911 0 0       0 if (defined $pattern) {
912 0 0       0 if (length($pattern) == 1) {
    0          
    0          
913 0         0 $format = $pattern . $format .$pattern;
914             }
915             elsif (length($pattern) == 2) {
916 0         0 my @chars = split //, $pattern;
917 0         0 $left_delimiter = $chars[0];
918 0         0 $right_delimiter= $chars[1];
919             }
920             elsif ($pattern =~ /^(.*)\|(.*)$/) {
921 0         0 $left_delimiter = $1;
922 0         0 $right_delimiter = $2;
923             }
924             }
925 0         0 $format = $left_delimiter . $format . $right_delimiter;
926 0         0 return $format;
927             }
928              
929             sub _get_format {
930 0     0   0 my $params = shift;
931 0         0 my $format = _get_left_right($params->{quotes}, q{#value#},q{"},q{"});
932 0         0 $format = _get_left_right($params->{brackets},q{#tag# }.$format,q{[},q{]});
933 0         0 return $format;
934             }
935              
936             sub _formatted_tag {
937 0     0   0 my ($format, $tag, $value) = @_;
938 0         0 my $subst = $format;
939 0         0 $subst =~ s/#tag#/$tag/;
940 0         0 $subst =~ s/#value#/$value/;
941 0         0 return $subst;
942             }
943              
944             =item standard_PGN()
945              
946             returns a string containing all current PGN tags, including
947             the game.
948             Parameters are passed through a hash reference. None is
949             required.
950              
951             tags => [tag list], # default is the Seven Tags Roster.
952             # You may specify only the tags you want to
953             # print
954             # tags => [qw(White Black Result)]
955            
956             all_tags => 'no', # default 'no'. If yes (or 1), it outputs all the tags
957             # if 'tags' and 'all_tags' are used, 'all_tags'
958             # prevails
959              
960             nl => q{\n}, # default '\n'. Tag separator. Can be changed
961             # according to your needs.
962             # nl => '
\n' is a good candidate for HTML
963             # output.
964            
965             brackets => q{[]}, # default '[]'. Output tags within brackets.
966             # Bracketing can be as creative as you want.
967             # If the left and rigth bracketing sequence are
968             # longer than one character, they must be separated
969             # by a pipe (|) symbol.
970             # '()', '(|)\t,'{|}\n' and '{}' are valid
971             # sequences.
972             #
973             # '

|

' will output HTML header 1
974             # '{|}\n' will enclose each tag
975             # between bold braces.
976            
977             quotes => q{"}, # default '"'. Quote tags values.
978             # As for brackets, quotes can be specified in
979             # pairs: '<>' and '<|>' are equivalent.
980             # If the quoting sequence is more than one char,
981             # the pipe symbol is needed to separate the left
982             # quote from the right one.
983             # '|' will produce HTML italicized text.
984            
985             game => 'yes', # default 'yes'. Output the game text
986             # If the game was parsed, returns a clean list
987             # of moves, else the unparsed text
988              
989             comments => 'no' # Default 'no'. Output the game comments.
990             # Requires the 'game' option
991            
992             =cut
993              
994             my %switchcolor = ('w' => 'b', 'b' => 'w');
995             sub standard_PGN {
996 0     0 1 0 my $self = shift;
997 0         0 my $params = shift;
998 0         0 my %seen =(Game =>1);
999 0         0 my @tags = $self->_get_tags($params);
1000 0         0 my $out = q{};
1001 0         0 my $nl ="\n";
1002 0         0 my $out_game = 'yes';
1003 0 0 0     0 $out_game = 0 if # 0.11
1004             exists $params->{game}
1005             and (lc($params->{game}) ne 'yes');
1006            
1007 0         0 my $out_comments = 0; # 0.11
1008 0 0 0     0 $out_comments = 'yes' if $out_game # 0.11
      0        
1009             and (exists $params->{comments}
1010             and (lc($params->{comments}) eq 'yes'));
1011            
1012 0 0       0 $nl = $params->{nl} if exists $params->{nl};
1013 0         0 my $format = _get_format($params);
1014 0         0 for (@tags) {
1015 0 0       0 $self->{gamedescr}{$_}=q{?} unless exists $self->{gamedescr}{$_};
1016             #$out .= qq/[$_ "$self->{gamedescr}{$_}"]\n/;
1017 0         0 $out .= _formatted_tag($format, $_, $self->{gamedescr}{$_});
1018 0         0 $out .= $nl;
1019             }
1020 0 0       0 if (@tags) {
1021 0         0 $out .= $nl;
1022             }
1023 0 0       0 return $out unless $out_game;
1024 0 0       0 if (defined $self->{GameMoves}) { # if parsed
1025 0         0 my $count = 0;
1026 0         0 my $color = 'w';
1027 0 0 0     0 if ((defined $self->{gamedescr}{FirstMove}) # 0.07
1028             and ($self->{gamedescr}{FirstMove} =~ m/(\d+)([bw])/)) # 0.07
1029             {
1030 0         0 $count = $1; # 0.07
1031 0         0 $color = $2; # 0.07
1032 0 0       0 $out .= "$count\.\.\." if $color eq 'b'; # 0.07
1033             }
1034 0         0 my $len = 0;
1035 0         0 for (@{$self->moves}) { #
  0         0  
1036 0 0       0 if ($color eq 'w') {
1037 0         0 $count++;
1038 0 0 0     0 $out .= q{ } and $len++ if $len and ($count > 1);
      0        
1039 0         0 $out .= $count . q{ };
1040 0         0 $len += length($count) +2;
1041             }
1042             else {
1043 0         0 $out .= q{ };
1044 0         0 $len++;
1045             }
1046 0         0 $out .= $_;
1047 0         0 $len += length($_);
1048 0 0 0     0 if ($out_comments # 0.11
1049             && exists $self->comments->{($count-1)."${color}"}) { # 0.12
1050 0         0 my $comment = $self->comments->{($count-1)."${color}"}; # 0.12
1051 0         0 my $needs_nl = $comment =~ /^\s*;/;
1052             #
1053             # deal with comment length here
1054             #
1055 0 0       0 if ($len >= 75) {
1056 0         0 $len = 0;
1057 0         0 $out .= $nl;
1058             }
1059 0         0 while ($len + length($comment) > 75) {
1060 0         0 my $delta = 75 - $len;
1061 0 0       0 $delta = 0 if $delta < 0;
1062 0         0 my ($portion) = $comment =~ /^(.{1,$delta})\W/;
1063 0         0 $out .= $portion;
1064 0         0 $len = 0;
1065 0         0 $out .= $nl;
1066 0         0 $comment = substr($comment, length($portion) +1);
1067             }
1068 0         0 $out .= $comment;
1069 0 0       0 $out .= $nl if $needs_nl;
1070 0         0 $len += length($comment);
1071             }
1072 0         0 $color = $switchcolor{$color};
1073 0 0       0 if ($len >= 75) {
1074 0         0 $len = 0;
1075 0         0 $out .= $nl;
1076             }
1077             }
1078 0         0 $out .=" $self->{gamedescr}{Result}$nl";
1079             }
1080             else { # not parsed - returns game text
1081 0         0 $out .= $self->{gamedescr}{Game};
1082             }
1083 0         0 return $out;
1084             }
1085              
1086             =item smart_parse_game()
1087              
1088             Parses the current game, returning the moves only.
1089             Uses by default quick_parse_game(), unless recursive comments are found in the source game.
1090              
1091             =cut
1092              
1093             sub smart_parse_game {
1094 0     0 1 0 my $self = shift;
1095 0         0 my $params = shift;
1096 0 0       0 if ($self->{gamedescr}{Game} =~ /\(/) {
1097 0         0 $self->parse_game($params)
1098             }
1099             else {
1100 0         0 $self->quick_parse_game($params)
1101             }
1102 0         0 return;
1103             }
1104              
1105             =item quick_parse_game()
1106              
1107             Parses the current game, returning the moves only.
1108             Comments are discarded.
1109             This function does FAIL on Recursive Annotated Variation or nested comments.
1110             Parameters (passed as a hash reference): check_moves = 'yes'|'no'. Default : no. If requested, each move is checked against a RegEx, to filter off possible unbraced comments.
1111              
1112             =cut
1113              
1114             # ==============================================
1115             # These two regular expressions were produced by
1116             # Damian Conway's module Regexp::Common
1117             # ----------------------------------------------
1118             # On the author's suggestion, these lines
1119             #
1120             # use Regexp::Common;
1121             # print "$RE{balanced}{-parens=>'()'}\n";
1122             # print "$RE{balanced}{-parens=>'{}'}\n";
1123             #
1124             # produced the RegEx code, which was edited
1125             # and inserted here for efficiency reasons.
1126             # ==============================================
1127              
1128             our $re_parens; ## no critic
1129             $re_parens = qr/
1130             (?:(?:(?:[(](?:(?>[^)(]+)
1131             |(??{$re_parens}))*[)]))
1132             |(?:(?!)))
1133             /x;
1134              
1135             our $re_brace; ## no critic
1136             $re_brace = qr/
1137             (?:(?:(?:[{](?:(?>[^}{]+)
1138             |(??{$re_brace}))*[}]))
1139             |(?:(?!)))
1140             /x;
1141              
1142             # ==============================================
1143              
1144             # regular expressions for game parsing
1145             my $re_result = qr{(?:1\-0|0\-1|1\/2\-1\/2|\*)};
1146             my $re_move = qr{[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](?:\=?[QRBN])?};
1147             # piece ^^^^^
1148             # unambiguous column or line ^^^ ^^^
1149             # capture ^
1150             # destination square ^^^ ^^^
1151             # promotion ^ ^^^^^
1152             my $re_castling = qr/O\-O(?:\-O)?/;
1153             my $re_check = qr/(?:(?:\#|\+(\+)?))?/;
1154             my $re_any_move = qr/(?:$re_move|$re_castling)$re_check/;
1155             my $re_nag = qr/\$\d+/;
1156             my $re_number = qr/\d+\.(?:\.\.)?/;
1157             my $re_escape = qr/^\%[^\n]*\n/;
1158             my $re_eol_comment= qr/;.*$/;
1159             my $re_rav = $re_parens;
1160             my $re_comment = $re_brace;
1161              
1162             sub quick_parse_game {
1163 20     20 1 23 my $self = shift;
1164 20         23 my $params = shift; # hash reference to parameters
1165 20         158 $self->{gamedescr}{Game} =~ s/$re_eol_comment//mg; # rm EOL comments
1166 20         88 $self->{gamedescr}{Game} =~ s/$re_escape//mgo; # rm escaped lines
1167 20         1585 $self->{gamedescr}{Game} =~
1168             s/$re_comment//g; # remove comments
1169 20         1917 $self->{gamedescr}{Game} =~
1170             s/$re_rav//g; # remove RAV
1171 20 50       81 return 0
1172             if $self->{gamedescr}{Game} =~
1173             /\(/; # the game still contains RAV
1174 20 50       70 return 0
1175             if $self->{gamedescr}{Game} =~
1176             /\{/; # undetected nested comments
1177 20         172 $self->{gamedescr}{Game} =~ s/\n/ /g; # remove newlines
1178 20         58 $self->{gamedescr}{Game} =~
1179             s/\r/ /g; # remove return chars (DOS)
1180 20         49 $self->{gamedescr}{Game} =~ s/$re_nag//go; # remove NAG
1181 20         1993 $self->{gamedescr}{Game} =~ s/\d+\.//g; # remove numbers
1182 20         56 $self->{gamedescr}{Game} =~ s/\.\.(?:\.)?//g; # remove "..."
1183 20         270 $self->{gamedescr}{Game} =~ s/$re_result\s*\Z//o;
1184 20         65 my $re_filter = qr/\S/;
1185 20 50 33     71 if (exists $params->{check_moves}
1186             and ($params->{check_moves} =~ /^(?:yes|1)$/))
1187             {
1188 0         0 $re_filter = $re_any_move;
1189             }
1190 20 50       57 return unless $self->{gamedescr}{Game}; # discards empty games
1191 1620         4926 $self->{GameMoves} =
1192 20         541 [grep { m/$re_filter/o } split /\s+/, $self->{gamedescr}{Game}];
1193 20         127 return;
1194             }
1195              
1196             =item parse_game()
1197              
1198             Parses the current game (after read_game() was called).
1199             Accepts parameters as hash reference.
1200              
1201             $pgn->parse_game(); # default save_comments => 'no'
1202              
1203             $pgn->parse_game({
1204             save_comments => 'yes',
1205             comments_struct => 'string'});
1206            
1207             {comments_struct => 'string'} is the default value
1208             When 'comments_struct' is 'string', multiple comments
1209             for the same move are concatenated to one string
1210              
1211             {comments_struct => 'array'}
1212             If 'array', comments are stored as an anonymous array,
1213             one comment per element
1214              
1215             {comments_struct => 'hol'}
1216             If 'hol', comments are stored as a hash of lists, where
1217             there is a list of comments for each comment type
1218             (NAG, RAV, braced, semicolon, escaped)
1219            
1220             $pgn->parse_game({save_comments => 'yes',
1221             log_errors => 'yes'});
1222              
1223             parse_game() implements a finite state machine on two assumptions:
1224              
1225             1. No moves or move numbers are truncated at the end of a line;
1226             2. the possible states in a PGN game are:
1227              
1228             a. move number
1229             b. move
1230             c. braced comment
1231             d. EOL comment
1232             e. Numeric Annotation Glyph
1233             f. Recursive Annotated Variation
1234             g. Result
1235             h. unbraced comments (barewords, "!?+-=")
1236              
1237             Items from "a" to "g" are actively parsed and recognized.
1238             Anything unrecognized goes into the "h" state and discarded
1239             (or stored, if log_errors was requested)
1240              
1241             =cut
1242              
1243             { # start closure for parse_game
1244             my %comment_types = (
1245             q{$} => 'NAG',
1246             q{(} => 'RAV',
1247             q[{] => 'brace',
1248             q{%} => 'escaped',
1249             q{;} => 'semicolon',
1250             );
1251              
1252             sub parse_game {
1253 1     1 1 2 my $self = shift;
1254 1         2 my $params = shift;
1255 1   33     25 my $save_comments = ((exists $params->{save_comments})
1256             and ($params->{save_comments} =~ /^(?:yes|1)$/));
1257 1 50       19 my $log_errors = (exists $params->{log_errors})
1258             and ($params->{log_errors} =~ /^(?:yes|1)$/);
1259 1 50       5 return unless $self->{gamedescr}{Game};
1260 1         3 my $movecount = 0;
1261 1         2 my $color = 'b';
1262 1         4 $self->{gamedescr}{Game} =~ s/0\-0\-0/O-O-O/g;
1263 1         4 $self->{gamedescr}{Game} =~ s/0\-0/O-O/g;
1264 1         74 $self->{gamedescr}{Game} =~ s/$re_result\s*\Z//o;
1265              
1266 1         4 my $comments_struct = 'string';
1267 1 50 33     14 $comments_struct = $params->{comments_struct}
1268             if ($save_comments
1269             and exists $params->{comments_struct});
1270 1 50       7 $comments_struct = 'string'
1271             unless $comments_struct =~ /^(?:array|hol)$/;
1272 1         3 my $plycount = 0;
1273 1         2 my $countless =0;
1274 1         90 $self->{gamedescr}{Game} =~ s/\s*\Z//;
1275 1         6 $self->{gamedescr}{Game} =~ s/^\s*//;
1276 1 50       11 if ($self->{gamedescr}{Game} !~ /\d\./) {
1277 0         0 $countless = 1;
1278 0         0 $movecount = 1;
1279             }
1280            
1281 1         3 $self->{GameMoves} = [];
1282            
1283 1         8 for ($self->{gamedescr}{Game}) {
1284 1         5 while (! /\G \s* \z/xgc ) {
1285 92 100       3026 if ( m/\G($re_number)\s*/mgc) {
    100          
    100          
    50          
1286 22         35 my $num=$1;
1287 22 100       50 if (( $num =~ tr/\.//d) > 1) {
1288 3         51 $color = 'w';
1289             }
1290 22 100       74 if ($movecount == 0) {
    100          
    50          
1291 1         3 $movecount = $num;
1292 1 50       8 $self->{gamedescr}{FirstMove} =
1293             $num.$switchcolor{$color} # fixed 0.07
1294             unless $num.$switchcolor{$color} eq '1w';
1295             }
1296             elsif ($movecount == ($num -1)) {
1297 18         59 $movecount++;
1298             }
1299             elsif ($movecount != $num) {
1300 0         0 $self->{GameErrors}->{$movecount.$color}
1301             .= " invalid move sequence ($num <=> $movecount)";
1302 0         0 $movecount++;
1303             }
1304             }
1305             elsif ( m/\G($re_any_move)\s*/mgc ) {
1306 37         39 push @{$self->{GameMoves}}, $1;
  37         104  
1307 37         61 $color = $switchcolor{$color};
1308 37 50       156 if ($countless) {
1309 0         0 $plycount++;
1310 0 0       0 if ($plycount == 2) {
1311 0         0 $plycount =0;
1312 0         0 $movecount++;
1313             }
1314             }
1315             }
1316             elsif (
1317             m/\G($re_comment
1318             |$re_eol_comment
1319             |$re_rav
1320             |$re_nag|$re_escape)\s*/mgcx
1321             )
1322             {
1323 9 50       18 if ($save_comments) {
1324 9         14 my $tempcomment = $1;
1325 9         15 $tempcomment =~ tr/\r//d;
1326 9         21 $tempcomment =~ s/\n/ /g;
1327 9         18 $tempcomment =~ s/^\s+//;
1328 9         27 $tempcomment =~ s/\s+$//;
1329 9 50       18 if ($comments_struct eq 'string') {
    0          
1330 9         88 $self->{GameComments}->{$movecount.$color} .=
1331             q{ } . $tempcomment;
1332             }
1333             elsif ($comments_struct eq 'array') {
1334 0         0 push @{$self->{GameComments}->{$movecount.$color}},
  0         0  
1335             $tempcomment;
1336             }
1337             else { # hol
1338 0         0 $tempcomment =~ m/^(.)/;
1339 0         0 my $comment_type ='unknown';
1340 0 0 0     0 $comment_type = $comment_types{$1}
1341             if ($1 and exists $comment_types{$1});
1342 0         0 push @{$self->{GameComments}->{$movecount.$color}->{$comment_type}} ,
  0         0  
1343             $tempcomment;
1344             }
1345             }
1346             }
1347             elsif ( m/\G(\S+\s*)/mgc ) {
1348 24 50       46 if ($log_errors) {
1349 24         77 $self->{GameErrors}->{$movecount.$color} .= q{ } . $1;
1350 24         53 $self->{GameErrors}->{$movecount.$color} =~ tr/\r//d;
1351 24         3256 $self->{GameErrors}->{$movecount.$color} =~ s/\n/ /g;
1352             }
1353             }
1354             }
1355             }
1356 1         4 return 1;
1357             }
1358              
1359             =item add_comments()
1360              
1361             Allows inserting comments for an already parsed game;
1362             it accepts comments passed as an anonymous hash.
1363             An optional second parameter sets the storage type.
1364             They are the same as for parse_game();
1365             'string' (default) all comments for a given move are
1366             concatenated together
1367             'array' each comment for a given move is stored as
1368             an array element
1369             'hol' Comments are stored in a hash of lists
1370             different for each comment type.
1371              
1372             =cut
1373              
1374             sub add_comments {
1375 0     0 1   my $self = shift;
1376 0           my $comments = shift;
1377 0           my $comment_struct = shift;
1378 0 0 0       $comment_struct = 'string'
1379             unless ($comment_struct && ($comment_struct =~ /^hol|array$/));
1380 0 0 0       if ($self->moves && $comments && (ref $comments eq 'HASH')) {
      0        
1381 0           for (keys %{ $comments } ) {
  0            
1382 0 0         next unless /^\d+(?:w|b)$/;
1383 0 0         if ($comment_struct eq 'string') {
    0          
1384 0           $self->{GameComments}->{$_} .=
1385             q{ } . $comments->{$_};
1386             }
1387             elsif ($comment_struct eq 'array') {
1388 0           push @{$self->{GameComments}->{$_}},
  0            
1389             $comments->{$_};
1390             }
1391             else { # hol
1392 0           $comments->{$_} =~ m/^(.)/;
1393 0           my $comment_type ='unknown';
1394 0 0 0       $comment_type = $comment_types{$1}
1395             if ($1 and exists $comment_types{$1});
1396 0           push @{$self->{GameComments}->{$_}->{$comment_type}} ,
  0            
1397             $comments->{$_};
1398             }
1399             }
1400             }
1401 0           return $self->{GameComments};
1402             }
1403              
1404             } # end closure for parse_game()
1405              
1406             =item shrink_epd()
1407              
1408             Given a EPD (Extended Position Description) string, shrink_epd() will convert it into a bit string, which reduces the original by about 50%.
1409             It can be restored to the original string by expand_epd()
1410              
1411             =cut
1412              
1413             # K k 0001 1001 001
1414             # Q q 0010 1010 010
1415             # R r 0011 1011 011
1416             # B b 0100 1100 100
1417             # N n 0101 1101 101
1418             # P p 0110 1110 110
1419             # E 0000 0000 000
1420             # 111
1421             # rnbqkbnr/pppppppp/8/8/3P4/8/PPP1PPPP/RNBQKBNR b KQkq d3 (38 bytes)
1422             # 1011 1101 1100 1010 1001 1100 1101 1011 4
1423             # 1110 1110 1110 1110 1110 1110 1110 1110 4
1424             # 11111000 1
1425             # 11111000 1
1426             # 11110011 0110 11110100 2.5
1427             # 11111000 1
1428             # 0110 0110 0110 11110001 0110 0110 0110 0110 4.5
1429             # 0011 0101 0100 0010 0001 0100 0101 0011 4
1430             # 22
1431              
1432             { #start EPD closure
1433             my %pieces2bits = (
1434             K => 1, # 0001
1435             k => 9, # 1001
1436             Q => 2, # 0010
1437             q => 10, # 1010
1438             R => 3, # 0011
1439             r => 11, # 1011
1440             B => 4, # 0100
1441             b => 12, # 1100
1442             N => 5, # 0101
1443             n => 13, # 1101
1444             P => 6, # 0110
1445             p => 14, # 1110
1446             1 => 0, # 0000
1447             2 => 7, # 0111
1448             3 => 8, # 1000
1449             4 => 0xF4, # 1111 0100
1450             5 => 0xF5, # 1111 0101
1451             6 => 0xF6, # 1111 0110
1452             7 => 0xF7, # 1111 0111
1453             8 => 0xF8, # 1111 1000
1454             );
1455              
1456             my %castling2bits = (
1457             'KQkq' => 15, # 1111 F KQkq
1458             'KQk' => 14, # 1110 E KQk-
1459             'KQq' => 13, # 1101 D KQ-q
1460             'KQ' => 12, # 1100 C KQ--
1461             'Kkq' => 11, # 1011 B K-kq
1462             'Kk' => 10, # 1010 A K-k-
1463             'Kq' => 9, # 1001 9 K--q
1464             'K' => 8, # 1000 8 K---
1465             'Qkq' => 7, # 0111 7 -Qkq
1466             'Qk' => 6, # 0110 6 -Qk-
1467             'Qq' => 5, # 0101 5 -Q-q
1468             'Q' => 4, # 0100 4 -Q--
1469             'kq' => 3, # 0011 3 --kq
1470             'k' => 2, # 0010 2 --k-
1471             'q' => 1, # 0001 1 ---q
1472             q{-} => 0, # 0111 0 ----
1473             );
1474              
1475             my %ep2bits = (
1476             q{-} => 0,
1477             'a' => 1,
1478             'b' => 2,
1479             'c' => 3,
1480             'd' => 4,
1481             'e' => 5,
1482             'f' => 6,
1483             'g' => 7,
1484             'h' => 8,
1485             );
1486             my %color2bits = ('w' => 0, 'b' => 1 );
1487             my %bits2color = ( 0 => 'w', 1 => 'b');
1488              
1489             my %bits2pieces = map { $pieces2bits{$_}, $_ } keys %pieces2bits;
1490             my %bits2castling = map { $castling2bits{$_}, $_ } keys %castling2bits;
1491             my %bits2ep = map { $ep2bits{$_}, $_ } keys %ep2bits;
1492              
1493             sub shrink_epd {
1494 0     0 1   my $source = shift;
1495 0           my $piece = q{};
1496 0           my $vecstring = q{};
1497 0           my $offset = 0;
1498 0           my ($fen, $color, $castling, $ep) = split / /, $source;
1499 0           while ($fen =~ /(.)/g) {
1500 0 0         next if $1 eq q{/};
1501 0           $piece = $pieces2bits{$1};
1502 0 0         if ($piece < 0x0F) {
1503 0           vec($vecstring, $offset++, 4) = $piece;
1504             }
1505             else {
1506 0           vec($vecstring, $offset++, 4) = 0x0F;
1507 0           vec($vecstring, $offset++, 4) = $1;
1508             }
1509             }
1510 0           vec($vecstring, $offset++, 4) = $color2bits{$color};
1511 0           vec($vecstring, $offset++, 4) = $castling2bits{$castling};
1512 0           vec($vecstring, $offset++, 4) = $ep2bits{substr($ep,0,1)};
1513 0           return $vecstring;
1514             }
1515              
1516             =item expand_epd()
1517              
1518             given a EPD bitstring created by shrink_epd(), expand_epd() will restore the original text.
1519              
1520             =cut
1521              
1522             sub expand_epd {
1523 0     0 1   my $vecstring = shift;
1524 0           my $piece = -1;
1525 0           my $asciistr=q{};
1526 0           my $offset =0;
1527 0           my $rowsum =0;
1528 0           my $overall_sum =0;
1529 0           while ($offset < length($vecstring)*2) {
1530 0           $piece = vec($vecstring, $offset++, 4);
1531 0 0         if ($piece == 0x0F) {
1532 0           $piece = hex('F' . vec($vecstring,$offset++,4));
1533             }
1534 0           $piece = $bits2pieces{$piece};
1535 0           $asciistr .= $piece;
1536 0 0         if ($piece =~ /[1-8]/) {
1537 0           $rowsum += $piece
1538             }
1539             else {
1540 0           $rowsum++;
1541             }
1542 0 0         if ($rowsum == 8) {
1543 0           $overall_sum += $rowsum;
1544 0           $rowsum =0;
1545 0 0         last if ($overall_sum >= 64);
1546 0           $asciistr .=q{/};
1547             }
1548             }
1549 0           my $color = $bits2color{vec($vecstring,$offset++,4)};
1550 0           $asciistr .= q{ } . $color;
1551 0           $asciistr .= q{ } . $bits2castling{vec($vecstring,$offset++,4)};
1552 0           my $ep = $bits2ep{vec($vecstring,$offset++,4)};
1553 0 0         if ($ep ne q{-}) {
1554 0 0         $ep .= $color eq 'w' ? '6' : '3';
1555             }
1556 0           $asciistr .= q{ } . $ep;
1557 0           return $asciistr;
1558             }
1559             } # end EPD closure
1560             =back
1561              
1562             =head1 AUTHOR
1563              
1564             Giuseppe Maxia, gmax@cpan.org
1565              
1566             =head1 THANKS
1567              
1568             Thanks to
1569             - Hugh S. Myers for advice, support, testing and brainstorming;
1570             - Damian Conway for the recursive Regular Expressions used to parse comments;
1571             - all people at PerlMonks (www.perlmonks.org) for advice and good developing environment.
1572             - Nathan Neff for pointing out an insidious, hard-to-spot bug in my RegExes.
1573              
1574             =head1 COPYRIGHT
1575              
1576             The Chess::PGN::Parse module is Copyright (c) 2002 Giuseppe Maxia,
1577             Sardinia, Italy. All rights reserved.
1578            
1579             You may distribute this software under the terms of either the GNU
1580             General Public License version 2 or the Artistic License, as
1581             specified in the Perl README file.
1582             The embedded and encosed documentation is released under
1583             the GNU FDL Free Documentation License 1.1
1584              
1585             =cut
1586              
1587             1;
1588             __DATA__