File Coverage

blib/lib/Games/Chess.pm
Criterion Covered Total %
statement 234 325 72.0
branch 120 206 58.2
condition 21 38 55.2
subroutine 46 53 86.7
pod 7 10 70.0
total 428 632 67.7


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # Games::Chess - represent chess pieces, positions, moves and games
3             #
4             # AUTHOR
5             # Gareth Rees
6             #
7             # COPYRIGHT
8             # Copyright (c) 1999 Gareth Rees. This module is free software: you
9             # can distribute and/or modify it under the same terms as Perl itself.
10             #
11             # $Id: Chess.pm,v 1.5 1999/06/06 18:47:24 gareth Exp $
12             #------------------------------------------------------------------------------
13              
14             package Games::Chess;
15 4     4   4153 use base 'Exporter';
  4         8  
  4         494  
16 4     4   21 use strict;
  4         7  
  4         152  
17 4     4   20 use vars qw($RCSID $VERSION $ERRMSG $DEBUG @EXPORT @EXPORT_OK %EXPORT_TAGS);
  4         12  
  4         989  
18              
19             $RCSID = q$Id: Chess.pm,v 1.5 1999/06/06 18:47:24 gareth Exp $;
20             $VERSION = '0.003';
21             $ERRMSG = '';
22             $DEBUG = 0;
23             @EXPORT = ();
24             @EXPORT_OK = qw(algebraic_to_xy colour_valid debug errmsg error
25             halfmove_count_valid move_number_valid piece_valid xy_valid
26             xy_to_algebraic
27             EMPTY WHITE BLACK PAWN KNIGHT BISHOP ROOK QUEEN KING);
28             %EXPORT_TAGS =
29             ( colours => [qw(EMPTY WHITE BLACK)],
30             pieces => [qw(EMPTY PAWN KNIGHT BISHOP ROOK QUEEN KING)],
31             constants => [qw(EMPTY WHITE BLACK PAWN KNIGHT BISHOP ROOK QUEEN KING)],
32             functions => [qw(algebraic_to_xy colour_valid debug errmsg
33             halfmove_count_valid move_number_valid piece_valid
34             xy_valid xy_to_algebraic)],
35             );
36              
37 4     4   23 use constant EMPTY => 0;
  4         6  
  4         427  
38 4     4   28 use constant WHITE => 1;
  4         6  
  4         184  
39 4     4   21 use constant BLACK => 2;
  4         7  
  4         242  
40              
41 4     4   20 use constant PAWN => 1;
  4         15  
  4         197  
42 4     4   20 use constant KNIGHT => 2;
  4         8  
  4         173  
43 4     4   19 use constant BISHOP => 3;
  4         14  
  4         205  
44 4     4   54 use constant ROOK => 4;
  4         8  
  4         161  
45 4     4   19 use constant QUEEN => 5;
  4         7  
  4         173  
46 4     4   19 use constant KING => 6;
  4         7  
  4         4151  
47              
48             sub algebraic_to_xy ( $ ) {
49 114     114 1 469 my ($sq) = @_;
50 114 100       336 $sq =~ /^([a-h])([1-8])$/
51             or return error("$sq does not specify a square in algebraic notation");
52 102         371 return (ord($1) - ord('a'), $2 - 1);
53             }
54              
55             sub colour_valid ( $ ) {
56 292     292 1 317 my ($colour) = @_;
57 292 50 66     1366 return 1 if $colour == WHITE or $colour == BLACK;
58 0         0 return error("colour $colour invalid: must be @{[WHITE]} or @{[BLACK]}");
  0         0  
  0         0  
59             }
60              
61             sub debug ( $ ) {
62 4     4 1 30 $DEBUG = shift;
63             }
64              
65             sub errmsg () {
66 10     10 1 72 return $ERRMSG;
67             }
68              
69             sub error ( $ ) {
70 224     224 0 275 $ERRMSG = shift;
71 224 50       394 if ($DEBUG > 0) {
72 0         0 my ($filename,$line) = (caller(2))[1,2];
73 0         0 my $message = "$ERRMSG at $filename line $line\n";
74 0 0       0 $DEBUG >= 2 ? die($message) : warn($message);
75             }
76 224         547 return;
77             }
78              
79             sub halfmove_count_valid ( $ ) {
80 225     225 0 333 my ($halfmove) = @_;
81 225 50       1131 return 1 if $halfmove =~ /^[0-9]+$/;
82 0         0 return error("halfmove clock '$halfmove' not a non-negative integer");
83             }
84              
85             sub move_number_valid ( $ ) {
86 425     425 0 452 my ($move) = @_;
87 425 50 33     3027 return 1 if $move =~ /^[0-9]+$/ and $move > 0;
88 0         0 return error("Fullmove number '$move' not a positive integer");
89             }
90              
91             sub piece_valid ( $ ) {
92 284     284 1 382 my ($piece) = @_;
93 284 50 33     1518 return 1 if PAWN <= $piece and $piece <= KING;
94 0         0 return error("piece $piece invalid: not between @{[PAWN]} and @{[KING]}");
  0         0  
  0         0  
95             }
96              
97             sub xy_to_algebraic ($$) {
98 165     165 1 1791 my ($x,$y) = @_;
99 165 100       335 return unless xy_valid($x,$y);
100 64         194 return chr($x + ord('a')) . ($y + 1);
101             }
102              
103             sub xy_valid ($$) {
104 3454     3454 1 4154 my ($x,$y) = @_;
105 3454 100 100     28651 return 1 if 0 <= $x and $x < 8 and 0 <= $y and $y < 8;
      100        
      66        
106 202         1277 return error("($x,$y) off chessboard: not in the range (0,0) to (7,7)");
107             }
108              
109             #------------------------------------------------------------------------------
110             # Games::Chess::Piece - representation of a chess piece
111             # A piece is represented as a blessed reference to a byte.
112             #------------------------------------------------------------------------------
113              
114             package Games::Chess::Piece;
115 4     4   22 use strict;
  4         7  
  4         3459  
116             Games::Chess->import(qw(error piece_valid colour_valid));
117              
118             my @COLOUR_NAMES = qw(empty white black unknown);
119             my @PIECE_NAMES = qw(square pawn knight bishop rook queen king unknown);
120             my $pieces = 'pnbrqk';
121             my @CODE_PIECE = split '', " $pieces ";
122             my $PIECE_CODES = " \U$pieces\E$pieces";
123             my %PIECE_CODES;
124             @PIECE_CODES{split '', $PIECE_CODES} = (0, 9..14, 17..22);
125              
126             sub new {
127 2975     2975   6786 my ($class,$val) = @_;
128 2975         4707 my $self = chr(0);
129 2975 100 33     28648 if (@_ < 2) {
    50 33        
    100          
    100          
    100          
    50          
    50          
130             # Use the default (empty square).
131             } elsif (@_ > 3) {
132 0         0 return error("Piece->new called with more than 3 arguments");
133             } elsif (@_ == 3) {
134 284 50       539 return unless colour_valid($_[1]);
135 284 50       546 return unless piece_valid($_[2]);
136 284         502 $self = chr(($_[1] << 3) + $_[2]);
137             } elsif (UNIVERSAL::isa($val,'Games::Chess::Piece')) {
138 13         21 $self = $$val;
139             } elsif (exists $PIECE_CODES{$val}) {
140 49         70 $self = chr($PIECE_CODES{$val});
141             } elsif ($val !~ /^\d+$/) {
142 0         0 return error("Piece->new('$val') invalid: '$val' not a chess piece");
143             } elsif (0 <= $val and $val < 256 and $val == int $val) {
144 2628         3223 $self = chr($val);
145             } else {
146 0         0 return error("Piece->new($val) invalid: $val outside range 0 to 255");
147             }
148 2975         11787 bless \$self, $class;
149             }
150              
151             sub code ( $ ) {
152 2543     2543   5145 my ($self) = @_;
153 2543         3300 my $col = (ord($$self) & 24) >> 3;
154 2543         3698 my $code = $CODE_PIECE[ord($$self) & 7];
155 2543 100       9166 return $col == 2 ? $code : uc($code);
156             }
157              
158             sub colour ( $ ) {
159 393     393   7479 my ($self) = @_;
160 393         1015 return (ord($$self) & 24) >> 3;
161             }
162              
163             sub colour_name ( $ ) {
164 114     114   204 my ($self) = @_;
165 114         190 return $COLOUR_NAMES[$self->colour];
166             }
167              
168             sub name ( $ ) {
169 39     39   44 my ($self) = @_;
170 39         63 return join ' ', $self->colour_name, $self->piece_name;
171             }
172              
173             sub piece ( $ ) {
174 429     429   873 my ($self) = @_;
175 429         4060 return ord($$self) & 7;
176             }
177              
178             sub piece_name ( $ ) {
179 150     150   169 my ($self) = @_;
180 150         254 return $PIECE_NAMES[$self->piece];
181             }
182              
183             #------------------------------------------------------------------------------
184             # Games::Chess::Move - representation of a chess move
185             #------------------------------------------------------------------------------
186              
187             package Games::Chess::Move;
188 4     4   33 use strict;
  4         7  
  4         1917  
189             Games::Chess->import(qw(error xy_valid));
190              
191             sub new {
192 0     0   0 my ($class,$xs,$ys,$xd,$yd,@promotion) = @_;
193 0 0 0     0 return unless xy_valid($xs,$ys) and xy_valid($xd,$yd);
194 0         0 my $self = { from => [$xs,$ys], to => [$xd,$yd] };
195 0 0       0 if (@promotion) {
196 0         0 my $p = Games::Chess::Piece->new(@promotion);
197 0 0       0 return unless $p;
198 0         0 $self->{'promotion'} = $p;
199             }
200 0         0 return bless $self, $class;
201             }
202              
203             sub cmp ( $$ ) {
204 0     0   0 my ($a,$b) = @_;
205 0 0       0 UNIVERSAL::isa($b, 'Games::Chess::Move')
206             or return error("Argument to 'cmp' must be of class Games::Chess::Move");
207             return ($a->{'from'}[0] <=> $b->{'from'}[0]
208             or $a->{'from'}[1] <=> $b->{'from'}[1]
209             or $a->{'to'}[0] <=> $b->{'to'}[0]
210             or $a->{'to'}[1] <=> $b->{'to'}[1]
211 0   0     0 or do {
212             my $ap = $a->{'promotion'};
213             my $bp = $b->{'promotion'};
214             defined $ap ? (defined $bp ? $$ap <=> $$bp : -1) : 1
215             });
216             }
217              
218             sub from ( $ ) {
219 0     0   0 my ($self) = @_;
220 0         0 return @{$self->{'from'}};
  0         0  
221             }
222              
223             sub to ( $ ) {
224 0     0   0 my ($self) = @_;
225 0         0 return @{$self->{'to'}};
  0         0  
226             }
227              
228             sub promotion ( $ ) {
229 0     0   0 my ($self) = @_;
230 0         0 return @{$self->{'promotion'}};
  0         0  
231             }
232              
233             #------------------------------------------------------------------------------
234             # Games::Chess::Position - representation of a chess position
235             #------------------------------------------------------------------------------
236              
237             package Games::Chess::Position;
238 4     4   22 use strict;
  4         7  
  4         121  
239 4     4   18 use vars '%gifs';
  4         15  
  4         16161  
240             Games::Chess->import(qw(:constants :functions error));
241              
242             my $init_pos = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
243              
244             sub new {
245 25     25   3359 my ($class,$val) = @_;
246            
247             # Passed another Position object? Return a copy.
248 25 50 66     294 if (defined $val and UNIVERSAL::isa($val,'Games::Chess::Position')) {
249 0         0 return bless { %$val }, $class;
250             }
251            
252             # We've been passed a board position in Forsythe-Edwards Notation (FEN).
253 25         48 my $self = { };
254 25 100       72 $val = $init_pos unless $val;
255            
256             # Split the FEN string into fields.
257 25         143 my @fields = split ' ', $val;
258            
259             # First element is board description: split into into ranks.
260 25         155 my @ranks = split '/', $fields[0];
261 25 50       82 @ranks == 8 or
262             return error("Position '$fields[0]' does not have 8 ranks");
263            
264             # Turn each rank into an array of 8 piece codes.
265 25         59 foreach my $r (0 .. 7) {
266 200         290 my $rank = $ranks[$r];
267 200         666 $rank =~ s/(\d)/' ' x $1/eg;
  208         670  
268 200 50       442 length $rank == 8
269             or return error("Rank $r '$rank' does not have 8 squares");
270 200         516 $ranks[$r] = [ map { $PIECE_CODES{$_} } split '', $rank ];
  1600         2516  
271 200 50       378 @{$ranks[$r]} == 8
  200         646  
272             or return error("Rank $r '$rank' contains an invalid piece code");
273             }
274            
275             # Transform the 2-d array and assemble into the board.
276 25         82 $self->{'board'} = pack('C64', map { $ranks[7-$_%8][int($_/8)] } 0 .. 63);
  1600         3059  
277            
278             # Active color (defaults to white).
279 25 50       102 $fields[1] = 'w' unless defined $fields[1];
280 25 100       75 if ($fields[1] eq 'w') {
    50          
281 21         84 $self->{'player_to_move'} = &WHITE;
282             } elsif ($fields[1] eq 'b') {
283 4         17 $self->{'player_to_move'} = &BLACK;
284             } else {
285 0         0 return error("Invalid player to move: '$fields[1]'");
286             }
287            
288             # Castling availability (defaults to none).
289 25 50       66 $fields[2] = '-' unless defined $fields[2];
290 25 100       62 unless ($fields[2] eq '-') {
291 13 50       113 (join '', sort split '', $fields[2]) eq $fields[2]
292             or return error("Castling availability '$fields[2]' not sorted");
293 13         52 foreach (split '', $fields[2]) {
294 38 50       123 /^[KQkq]$/ or return error("Castling availability '$_' not KQkq");
295 38         169 $self->{'can_castle'}{$_} = 1;
296             }
297             }
298              
299             # En passant target square (default none).
300 25 50       82 $fields[3] = '-' unless defined $fields[3];
301 25 100       69 unless ($fields[3] eq '-') {
302 2         12 my @square = algebraic_to_xy($fields[3]);
303 2 50       9 return unless @square == 2;
304 2         8 $self->{'en_passant'} = [@square];
305             }
306            
307             # Half-move clock (default 0).
308 25 50       55 $fields[4] = '0' unless defined $fields[4];
309 25 50       67 return unless halfmove_count_valid($fields[4]);
310 25         74 $self->{'halfmove'} = $fields[4];
311            
312             # Fullmove number (default 1).
313 25 50       73 $fields[5] = '1' unless defined $fields[5];
314 25 50       82 return unless move_number_valid($fields[5]);
315 25         70 $self->{'move'} = $fields[5];
316              
317             # All done.
318 25         200 return bless $self, $class;
319             }
320              
321             sub at {
322 2748     2748   6550 my ($self,$x,$y,@piece) = @_;
323 2748 50       4379 return unless xy_valid($x,$y);
324 2748 100       11582 return Games::Chess::Piece->new(vec($self->{'board'}, 8 * $x + $y, 8))
325             unless @piece;
326 120         317 my $p = Games::Chess::Piece->new(@piece);
327 120 50       256 return unless defined $p;
328 120         416 vec($self->{'board'}, 8 * $x + $y, 8) = ord $$p;
329 120         406 return 1;
330             }
331              
332             sub board ( $ ) {
333 0     0   0 my ($self) = @_;
334 0         0 return $self->{'board'};
335             }
336              
337             sub can_castle {
338 116     116   609 my ($self,$colour,$piece,$can_castle) = @_;
339 116         295 my $p = Games::Chess::Piece->new($colour,$piece);
340 116 50       257 return unless defined $p;
341 116         237 my $code = $p->code;
342 116 50       379 $code =~ /^[KQkq]$/ or return
343             error("can_castle($colour,$piece) invalid: must be king or queen");
344 116 100       492 return defined $self->{'can_castle'}{$code} unless defined $can_castle;
345 32 100       57 if ($can_castle) {
346 16         35 $self->{'can_castle'}{$code} = 1;
347             } else {
348 16         38 delete $self->{'can_castle'}{$code};
349             }
350 32         79 return 1;
351             }
352              
353             sub clear ( $$$ ) {
354 120     120   1126 my ($self,$x,$y) = @_;
355 120 50       215 return unless xy_valid($x,$y);
356 120         445 vec($self->{'board'}, 8 * $x + $y, 8) = 0;
357 120         328 return 1;
358             }
359              
360             sub en_passant {
361 516     516   2719 my ($self,@en_passant) = @_;
362 516         741 my $ep = $self->{'en_passant'};
363 516 100       1788 return defined $ep ? @$ep : () unless @en_passant;
    100          
364 256 50       488 return unless xy_valid(@en_passant);
365 256         711 $self->{'en_passant'} = [@en_passant];
366 256         639 return 1;
367             }
368              
369             sub halfmove_clock {
370 404     404   1514 my ($self,$halfmove) = @_;
371 404 100       1032 return $self->{'halfmove'} unless defined $halfmove;
372 200 50       308 return unless halfmove_count_valid($halfmove);
373 200         392 $self->{'halfmove'} = $halfmove;
374 200         345 return 1;
375             }
376              
377             sub move_number {
378 804     804   2860 my ($self,$move) = @_;
379 804 100       2063 return $self->{'move'} unless defined $move;
380 400 50       595 return unless move_number_valid($move);
381 400         585 $self->{'move'} = $move;
382 400         645 return 1;
383             }
384              
385             sub player_to_move {
386 20     20   105 my ($self,$colour) = @_;
387 20 100       61 return $self->{'player_to_move'} unless defined $colour;
388 8 50       18 return unless colour_valid($colour);
389 8         13 $self->{'player_to_move'} = $colour;
390 8         17 return 1;
391             }
392              
393             my @CASTLE_TESTS =
394             (
395             [ &WHITE, &KING, { 'e1' => 'K', 'h1' => 'R' } ],
396             [ &WHITE, &QUEEN, { 'e1' => 'K', 'a1' => 'R' } ],
397             [ &BLACK, &KING, { 'e8' => 'k', 'h8' => 'r' } ],
398             [ &BLACK, &QUEEN, { 'e8' => 'k', 'a8' => 'r' } ],
399             );
400              
401             sub validate ( $ ) {
402 19     19   177 my ($self) = @_;
403 19         21 my (%n,%m);
404 19         198 @n{split '', $PIECE_CODES} = (0) x 13;
405 19         151 @m{split '', $PIECE_CODES} = (0) x 13;
406            
407             # Count the number of each type of piece.
408 19         62 foreach my $x (0 .. 7) {
409 152         250 foreach my $y (0 .. 7) {
410 1216         3017 ++$n{$self->at($x,$y)->code};
411             }
412 152         348 ++$m{$self->at($x,0)->code};
413 152         444 ++$m{$self->at($x,7)->code};
414             }
415            
416             # More than 8 pawns per side?
417 19 100       83 $n{p} <= 8 or return error("Black has $n{p} pawns");
418 18 100       56 $n{P} <= 8 or return error("White has $n{P} pawns");
419            
420             # Pawn + promoted piece count plausible?
421 17 100       148 ($n{'p'} + (2<$n{'n'} ? $n{'n'}-2 : 0) + (2<$n{'b'} ? $n{'b'}-2 : 0)
    100          
    100          
    100          
    100          
422             + (2<$n{'r'} ? $n{'r'}-2 : 0) + (1<$n{'q'} ? $n{'q'}-1 : 0) <= 8)
423             or return error("Black has more than 8 pawns plus promoted pieces");
424 16 100       216 ($n{'P'} + (2<$n{'N'} ? $n{'N'}-2 : 0) + (2<$n{'B'} ? $n{'B'}-2 : 0)
    100          
    100          
    100          
    100          
425             + (2<$n{'R'} ? $n{'R'}-2 : 0) + (1<$n{'Q'} ? $n{'Q'}-1 : 0) <= 8)
426             or return error("White has more than 8 pawns plus promoted pieces");
427            
428             # Not exactly 1 king per side?
429 15 100       43 $n{'k'} == 1 or return error("Black has $n{'k'} kings");
430 14 100       41 $n{'K'} == 1 or return error("White has $n{'K'} kings");
431            
432             # Pawns on ranks 1 or 8?
433 13 100       37 $m{'p'} == 0 or return error("Black has a pawn on rank 1 or rank 8");
434 11 100       33 $m{'P'} == 0 or return error("White has a pawn on rank 1 or rank 8");
435              
436             # Impossible en passant target square?
437 9         21 my $ep = $self->{'en_passant'};
438 9 50       33 if ($ep) {
439 0 0       0 if ($self->{'player_to_move'} == &WHITE) {
440 0 0       0 $ep->[1] == 5 or return
441             error("White to move but EP square is @$ep");
442 0 0       0 $self->at($ep->[0],6)->code == ' ' or return
443             error("EP square is @$ep but rank 7 is not empty");
444 0 0       0 $self->at($ep->[0],5)->code == ' ' or return
445             error("EP square is @$ep but is not empty");
446 0 0       0 $self->at($ep->[0],4)->code == 'p' or return
447             error("EP square is @$ep but rank 5 does not contain a black pawn");
448             } else {
449 0 0       0 $ep->[1] == 2 or return
450             error("Black to move but EP square is @$ep");
451 0 0       0 $self->at($ep->[0],1)->code == ' ' or return
452             error("EP square is @$ep but rank 2 is not empty");
453 0 0       0 $self->at($ep->[0],2)->code == ' ' or return
454             error("EP square is @$ep but is not empty");
455 0 0       0 $self->at($ep->[0],3)->code == 'P' or return
456             error("EP square is @$ep but rank 4 does not contain a white pawn");
457             }
458             }
459            
460             # Castling availability inconsistent with position?
461 9         27 foreach my $c (@CASTLE_TESTS) {
462 36         108 my $p = Games::Chess::Piece->new($c->[0], $c->[1]);
463 36 100       114 if ($self->can_castle($c->[0], $c->[1])) {
464 18         27 foreach my $sq (keys %{$c->[2]}) {
  18         67  
465 36         80 my $colour = $p->colour_name;
466 36         88 my $side = $p->piece_name;
467 36         66 my $required = $c->[2]{$sq};
468 36         115 my $req_name = Games::Chess::Piece->new($required)->piece_name;
469 36 50       92 $self->at(algebraic_to_xy($sq))->code eq $required or return
470             error("$colour can castle ${side}side but no $req_name on $sq");
471             }
472             }
473             }
474            
475             # Check halfmove count and move number.
476 9         24 my $h = $self->{'halfmove'};
477 9 50       38 0 <= $h or return error("Negative halfmove count $h");
478 9 50       29 $h == int $h or return error("Non-integer halfmove count $h");
479 9 50       24 $h <= 50 or return error("Halfmove count $h > 50: game should have drawn");
480 9         54 my $m = $self->{'move'};
481 9 50       25 1 <= $m or return error("Move number $m not positive");
482 9 50       19 $m == int $m or return error("Non-integer move count $m");
483            
484             # Everything checks out OK.
485 9         63 return 1;
486             }
487              
488             #------------------------------------------------------------------------------
489             # Output Games::Chess::Position in varying formats.
490             #------------------------------------------------------------------------------
491              
492             sub to_FEN ( $ ) {
493 5     5   44 my ($self) = @_;
494 40         45 my $position = join '/', map {
495 5         18 my $y = $_;
496 40         59 my $rank = join '', map { $self->at($_,$y)->code } 0 .. 7;
  320         592  
497 40         211 $rank =~ s/( +)/length $1/eg;
  48         119  
498 40         105 $rank;
499             } reverse 0 .. 7;
500             return join ' ',
501             ( $position,
502             ( $self->{'player_to_move'} == &BLACK ? 'b' : 'w'),
503 0         0 ( join '', sort keys %{$self->{'can_castle'}} or '-' ),
504             ( defined $self->{'en_passant'}
505 5 100 100     29 ? xy_to_algebraic(@{$self->{'en_passant'}}) : '-' ),
    50          
506             $self->{'halfmove'},
507             $self->{'move'} );
508             }
509              
510             sub to_text ( $ ) {
511 4     4   11 my ($self) = @_;
512 32         43 join "\n", map {
513 4         11 my $y = $_;
514 256         469 join ' ', map {
515 32         58 my $sq = $self->at($_,$y)->code;
516 256 100 100     1109 $sq = '.' if $sq eq ' ' and ($y + $_) % 2 == 0;
517 256         622 $sq;
518             } 0 .. 7;
519             } reverse 0 .. 7;
520             }
521              
522             # Width and height of the GIF images for the pieces.
523             my ($width,$height) = (33,33);
524              
525             sub to_GIF ( $ ) {
526 1     1   5 my ($self) = shift;
527 1         573 require GD;
528 0           my %opts = ( lmargin => 20, bmargin => 20, border => 2,
529             font => GD::Font->Giant, letters => 1, @_ );
530              
531             # Check options.
532 0 0         $opts{lmargin} = $opts{bmargin} = 0 unless $opts{letters};
533 0           foreach (qw(lmargin bmargin border)) {
534 0 0         0 <= $opts{$_} or return error("Option $_ $opts{$_} must be >= 0.");
535             }
536 0 0         UNIVERSAL::isa($opts{font}, 'GD::Font')
537             or return error("$opts{font} does not belong to the GD::Font class.");
538              
539             # Image parameters:
540             # $iwidth Total image width
541             # $iheight Total image height
542 0           my ($iwidth, $iheight) = ($opts{lmargin} + 8 * $width + 2 * $opts{border},
543             8 * $height + $opts{bmargin} + 2 * $opts{border});
544 0           my $img = GD::Image->new($iwidth, $iheight);
545              
546             # Colours:
547             # $white White squares on the chess board
548             # $grey Black squares on the chess board
549             # $black The border and the lettering
550             # $transparent The margins
551 0           my $white = $img->colorAllocate(255,255,255);
552 0           my $grey = $img->colorAllocate(191,191,191);
553 0           my $black = $img->colorAllocate(0,0,0);
554 0           my $transparent = $img->colorAllocate(255,192,192);
555 0           $img->transparent($transparent);
556              
557             # Colour the board and the margins; draw a border round the board.
558 0           $img->filledRectangle(0, 0, $iwidth-1, $iheight-1, $transparent);
559 0           $img->filledRectangle($opts{lmargin}, 0, $iwidth-1,
560             $iheight-1-$opts{bmargin}, $white);
561 0           for (my $i = 0; $i < $opts{border}; ++$i) {
562 0           $img->rectangle($opts{lmargin} + $i, $i, $iwidth - 1 - $i,
563             $iheight - 1 - $opts{bmargin} - $i, $black);
564             }
565              
566             # Draw the file letters a-h and the rank numbers 1-8.
567 0 0         if ($opts{letters}) {
568 0           my ($fw,$fh) = ($opts{font}->width, $opts{font}->height);
569 0           foreach my $n (0 .. 7) {
570 0           $img->string($opts{font}, ($opts{lmargin} - $fw) / 2,
571             $opts{border} + $n * $height + ($height - $fh) / 2,
572             8 - $n, $black);
573 0           $img->string($opts{font},
574             $opts{lmargin} + $opts{border} + $n*$width + ($width-$fw)/2,
575             $iheight - $opts{bmargin} + ($opts{bmargin}-$fh)/2,
576             chr(ord('a')+$n), $black);
577             }
578             }
579              
580             # Draw the backgrounds to the black squares and draw the pieces.
581 0           my $gifs = piece_gifs();
582 0           foreach my $x (0 .. 7) {
583 0           foreach my $y (0 .. 7) {
584 0           my ($left,$top) = ($opts{lmargin} + $opts{border} + $x * $width,
585             (7 - $y) * $height + $opts{border});
586 0 0         $img->filledRectangle($left,$top,$left+$width-1,$top+$height-1,$grey)
587             if ($x + $y) % 2 == 0;
588 0           my $c = $self->at($x,$y)->code;
589 0 0         next if $c eq ' ';
590 0           $img->copy($gifs->{$c}, $left, $top, 0, 0, $width, $height);
591             }
592             }
593              
594             # Convert image to GIF and return.
595 0           return $img->gif;
596             }
597              
598 4     4   33 use vars '%gifs';
  4         7  
  4         1895  
599              
600             my %piece_images =
601             ( 'p' => '5555555555555555555555555555555555555555ff75555555555555fff7555555555555dfff5555555555555fff7555555555555dfff5555555555555fff755555555555ffffff7555555555fffffff755555555ffffffff75555555fffffffff7555555dfffffffff5555555dffffffff5555555555dfff5555555555555fff7555555555555dfff555555555555dffff555555555555ffff755555555555dffff555555555555ffff755555555555dffff555555555555ffff755555555555dffff555555555555ffff75555555555dffffff555555555ffffffff75555555fffffffff7555555dfffffffff555555dffffffffff555555ffffffffff75555555555555555555555555555555555510',
602             'n' => '5555555555555555555555f755555555555557df5555555555555dffff755555555555ffefff7555555555dfffffff555555555fffffaff55555555dfffffaef5555555dffffffaef5555555fffffffaef555555ffffffffaef55555dffffffffaef5555dffaffffffaf75555ffbefffffbaf7555ffffffffffbef555fffffffffffaef55ffffffffffffae75ffffffffffffbaf5dfffbffffffffbe75fbebfffffffffae5dffbf75dffffffaf5ff7f75dffffffbe75ffd75dfffffffaf555555dffffffffe755555fffffffffbf55555ffffffffffe75555dfffffffffbf5555dfffffffffff75555ffffffffffff5555dfffffffffff55555fffffffffff755555dfffffffff55555555555555555510',
603             'b' => '55555555555555555555555f55f555555555555ff5ff55555555555df7df755555555555ff5ff55555555555dffff755555555555fffbf75555555555ffffaf7555555555ffffbaf555555555fffffbaf55555555dfffffbe75555555dffffffbe75555555fffffffaf5555555ffffffffa7555555dffffffffe5555555ffffffffb7555555dffffffffe5555555ffffffffb75555555fffffffff5555555dffffffff75555555ffffffff755555555ffaaaef755555555daaffbae555555555dffffff555555555dfffffff555555555ffaaaef755555555daaffbae555555fffeffffffeff755ffffffffffffff75fffffffffffffff7dfffffffffffffff5dfffff755ffffff5555555555555555510',
604             'r' => '55555555555555555555555555555555555fff55df55dff7555dfff5dff5dfff5555ffffffffffff7555dffffffffffff5555ffffffffffff7555dffffffffffff55555ffffbaffff7555555dffaaaeff55555555fbaefaaf755555555beffffa755555555dfffffff555555555fffffff755555555dfffffff555555555fffffff755555555dfffffff555555555fffffff755555555dfffffff555555555fffffff755555555dfffffff555555555fffaeff755555555dfbaaaff55555555dfaafbaef5555555dbaffffbaf555555dffffffffff55555dfffffffffff5555dffffffffffff555dfffffffffffff55dffffffffffffff55ffffffffffffff75dffffffffffffff5555555555555555510',
605             'q' => '555555555555555555555f75555f755555555ff7555ff75555555dff555dff55555555ff7555ff755555555f75555f755555555df5555df555555555f75555f755555555dff55dff555555555ff755ff75555f755dfffdfff555fff755fff7fff755ffff55dfffffff55dfff75fffffffff5dffdfffffffffffffff5dffffffffffffff55dfffffffffffff555ffffffaefffff755dfffbaaaaaffff555dfaaafffbaaef5555faffffffffbe75555fffffbfffff75555dffffbafffff55555fffffbfffff75555dfffffffffff55555fffffffffff75555dfffbaaaffff55555dfaaeffaaef555555faffffffbe755555dffffffffff555555dfffffffff55555555fffffff75555555555555555555510',
606             'k' => '555555dfff555555555555dffff555555555555fabe755555555555dbeaf555555555555fbaf755555555555dbeaf555555555555fabe7555555dfff7dffff5ffff5dfffffffffffffffdffbafffbfffbafffffbeaeffeffaeeffffbefbeffffaffaffffaffaefffaefbefffbeffbfffbfffaffffbeffefffeffafffffbefbeffbffaffffffbffbffbefbfff7fffaffeffefbeff7dffbefbaaaffafff5dffbaaafbaaafff55dfbaefffffaaff555dfefffbffffef5555dffffbafffff55555dffffbfffff555555ffffffffff755555dffffffffff555555ffffbaffff755555dffbaaaafff555555faaaffbaae755555dffffffffff555555dfffffffff5555555dffffffff55555555dfffffff555510',
607             'P' => '5555555555555555555555555555555555555555ff75555555555555fff7555555555555dbaf5555555555555fae7555555555555dbaf5555555555555fbf755555555555fffeff7555555555ffbaaff755555555fbaaaaaf75555555faaaaaaae7555555dfffffffff55555555ffffffff5555555555dfef5555555555555fbf7555555555555dbaf555555555555dfaef555555555555faae755555555555dbaaf555555555555faae755555555555dbaaf555555555555faae755555555555dfaef555555555555fbaf75555555555dffaeff555555555fffaaeff75555555ffaaaaaef7555555dbaaaaaaaf555555dffffffffff555555ffffffffff75555555555555555555555555555555555510',
608             'N' => '5555555555555555555555f755555555555555df55555555555555dfff755555555555dfffff75555555555ffaffff555555555ffbaaeff55555555dfbaaaaff5555555dfaaaaaaef5555555fbaaaaaaef555555fbaaaaaaaef55555dbefaaaaaaf75555dfafbaaaaaaf75555faefaaaaaaef5555faaaaaaaaaaef555fbaaaaaaaaaae755fbaaaaaaaaaaaf75dbaaaaaebaaaaef5dbaaaffffaaaaae75feaaffffaaaaaaf5dbabf75dbaaaaaef5ffff75dfaaaaaaf75fff55dfaaaaaaaf555555dfaaaaaaae755555fbaaaaaaaef55555fbaaaaaaaaf75555dbaaaaaaaaef5555dfaaaaaaaaaf75555fbaaaaaaaaef5555dbaaffffbaaf55555fffffffffff755555dfffffffff55555555555555555510',
609             'B' => '55555555555555555555555f55f555555555555ff5ff55555555555df7df755555555555ff5ff55555555555dffff755555555555fefff75555555555faefaf7555555555dbafbaf555555555fbaaebef55555555dbaaafbe75555555dfaaaafaf75555555faaaaafaf5555555fbaaaaefe7555555dbaaaaaebf5555555faaaaaaff7555555dbaaaaaaff5555555fbaaaaaef75555555faaaaaaff5555555dbaaaaaef75555555ffffffff755555555fffffff755555555dfaabaef555555555fbafbaf755555555dfaabaef555555555fffffff755555555dfffffff55555dfffffaaaefffff5dfffffefffefffff5baaaaeffffaaaaa7dfffffffffffffff5dfffff755ffffff5555555555555555510',
610             'R' => '55555555555555555555555555555555555fff55df55dff7555dfff5dff5dfff5555faeffbafffae7555dbaafbaafbaaf5555faaaaaaaaaae7555dfbaaaaaaaaff55555fbaaefaaaf7555555dbafffbaf55555555fefbaffe755555555ffaaaef755555555dfaaaaef555555555faaaaae755555555dbaaaaaf555555555faaaaae755555555dbaaaaaf555555555faaaaae755555555dbaaaaaf555555555faaaaae755555555dbaefaaf555555555fafffbe755555555dffbafff55555555dffaaaeff5555555dfaaaaaaef555555dfaaaaaaaef55555dfaaaaaaaaef5555dfaaaaaaaaaef555dfaaaaaaaaaaef55dfaaaaaaaaaaaef55ffffffffffffff75dffffffffffffff5555555555555555510',
611             'Q' => '555555555555555555555f75555f755555555ff7555ff75555555dbf555dbf55555555ff7555ff755555555f75555f755555555df5555df555555555f75555f755555555dff55dff555555555fe755fe75555f755dbffdfbf555fff755faf7fbe755ffbf55dbaffbaf55dbfff5dbaeffaaf5dffdffffaaefaaeffff5dfffbaaeaaaffff55dbfbaaaaaaaefe555fbaaaaaaaaaae755dfaaffffffbaef555dffffffffffff5555fffaaaaaaeff75555faaaaeaaaae75555dbaaaefaaaaf55555faaaaeaaaae75555dbaaaaaaaaaf55555faafffffbae75555dfffffffffff55555dffbaaaafff555555fbaaaaaaaf755555dffbaaaafff555555dfffffffff55555555fffffff75555555555555555555510',
612             'K' => '555555dfff555555555555dffff555555555555ffff755555555555dfaef555555555555ffff755555555555dbfbf555555555555fefe7555555dfff7dffff5ffff5dffffffbafffffffdfbaafffffffbaafffbaaaefffffaaaaffbaaaaebaafaaaaaffaaaaaebafaaaaaefbaaaaafaebaaaaaffaaaaaababaaaaaefbaaaaaebfaaaaaaffbaaaaafebaaaaae7fbaaaaabbaaaaae7dfaaaaaefaaaaaef5dfaaeffffffaaaf55dfffffffffffff555dffbaaaaaafff5555dfaaaabaaaef55555dbaaafbaaaf555555faaaabaaae755555dbaaaaaaaaf555555faefffffbe755555dffffffffff555555fffaaaaaff755555dfaaaaaaaaf555555dfbaaaaaef5555555dffffffff55555555dfffffff555510',
613             );
614              
615             sub piece_gifs () {
616 0 0   0     unless (%gifs) {
617             # Create GIF image files for the 12 pieces.
618 0           foreach my $code (keys %PIECE_CODES) {
619 0 0         next if $code eq ' ';
620 0           $gifs{$code} = GD::Image->new($width,$height);
621 0           my $white = $gifs{$code}->colorAllocate(255,255,255);
622 0           my $black = $gifs{$code}->colorAllocate(0,0,0);
623 0           my $transparent = $gifs{$code}->colorAllocate(0,255,0);
624 0           $gifs{$code}->transparent($transparent);
625 0           my $v = pack('h*', $piece_images{$code});
626 0           foreach my $x (0 .. $width-1) {
627 0           foreach my $y (0 .. $width-1) {
628 0           $gifs{$code}->setPixel($x,$y,($transparent,$white,$black)
629             [vec($v, $y * 33 + $x, 2) - 1]);
630             }
631             }
632             }
633             }
634 0           return \%gifs;
635             }
636              
637             1;