Add some routines that are useful to parse PGNs.
[remoteglot] / Board.pm
1 #! /usr/bin/perl
2 #
3 # There are too many chess modules on CPAN already, so here's another one...
4 #
5 use strict;
6 use warnings;
7
8 package Board;
9
10 sub new {
11         my ($class, @rows) = @_;
12         my $board = [];
13
14         for my $row (0..7) {
15                 for my $col (0..7) {
16                         $board->[$row][$col] = substr($rows[$row], $col, 1);
17                 }
18         }
19
20         return bless $board;
21 }
22
23 sub clone {
24         my ($board) = shift;
25         my $nb = [];
26
27         for my $row (0..7) {
28                 for my $col (0..7) {
29                         $nb->[$row][$col] = $board->[$row][$col];
30                 }
31         }
32
33         return bless $nb;
34 }
35
36 # Returns a new board.
37 sub make_move {
38         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
39         my $move = _move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
40         my $piece = $board->[$from_row][$from_col];
41         my $nb = $board->clone();
42
43         if ($piece eq '-') {
44                 die "Invalid move $move";
45         }
46
47         # white short castling
48         if ($move eq 'e1g1' && $piece eq 'K') {
49                 # king
50                 $nb->[7][4] = '-';
51                 $nb->[7][6] = $piece;
52
53                 # rook
54                 $nb->[7][7] = '-';
55                 $nb->[7][5] = 'R';
56
57                 return $nb;
58         }
59
60         # white long castling
61         if ($move eq 'e1c1' && $piece eq 'K') {
62                 # king
63                 $nb->[7][4] = '-';
64                 $nb->[7][2] = $piece;
65
66                 # rook
67                 $nb->[7][0] = '-';
68                 $nb->[7][3] = 'R';
69
70                 return $nb;
71         }
72
73         # black short castling
74         if ($move eq 'e8g8' && $piece eq 'k') {
75                 # king
76                 $nb->[0][4] = '-';
77                 $nb->[0][6] = $piece;
78
79                 # rook
80                 $nb->[0][7] = '-';
81                 $nb->[0][5] = 'r';
82
83                 return $nb;
84         }
85
86         # black long castling
87         if ($move eq 'e8c8' && $piece eq 'k') {
88                 # king
89                 $nb->[0][4] = '-';
90                 $nb->[0][2] = $piece;
91
92                 # rook
93                 $nb->[0][0] = '-';
94                 $nb->[0][3] = 'r';
95
96                 return $nb;
97         }
98
99         # check if the from-piece is a pawn
100         if (lc($piece) eq 'p') {
101                 # attack?
102                 if ($from_col != $to_col) {
103                         # en passant?
104                         if ($board->[$to_row][$to_col] eq '-') {
105                                 if ($piece eq 'p') {
106                                         $nb->[$to_row + 1][$to_col] = '-';
107                                 } else {
108                                         $nb->[$to_row - 1][$to_col] = '-';
109                                 }
110                         }
111                 } else {
112                         if (defined($promo) && $promo ne '') {
113                                 if ($piece eq 'p') {
114                                         $piece = $promo;
115                                 } else {
116                                         $piece = uc($promo);
117                                 }
118                         }
119                 }
120         }
121
122         # update the board
123         $nb->[$from_row][$from_col] = '-';
124         $nb->[$to_row][$to_col] = $piece;
125
126         return $nb;
127 }
128
129 sub _pos_to_square {
130         my ($row, $col) = @_;
131         return sprintf("%c%d", ord('a') + $col, 8 - $row);
132 }
133
134 sub _col_letter_to_num {
135         return ord(shift) - ord('a');
136 }
137
138 sub _row_letter_to_num {
139         return 7 - (ord(shift) - ord('1'));
140 }
141
142 sub _square_to_pos {
143         my ($square) = @_;
144         $square =~ /^([a-h])([1-8])$/ or die "Invalid square $square";  
145         return (_row_letter_to_num($2), _col_letter_to_num($1));
146 }
147
148 sub _move_to_uci_notation {
149         my ($from_row, $from_col, $to_row, $to_col, $promo) = @_;
150         $promo //= "";
151         return _pos_to_square($from_row, $from_col) . _pos_to_square($to_row, $to_col) . $promo;
152 }
153
154 sub parse_pretty_move {
155         my ($board, $move, $toplay) = @_;
156
157         # Strip check or mate
158         $move =~ s/[+#]$//;
159
160         if ($move eq '0-0' or $move eq 'O-O') {
161                 if ($toplay eq 'W') {
162                         return (_square_to_pos('e1'), _square_to_pos('g1'));
163                 } else {
164                         return (_square_to_pos('e8'), _square_to_pos('g8'));
165                 }
166         } elsif ($move eq '0-0-0' or $move eq 'O-O-O') {
167                 if ($toplay eq 'W') {
168                         return (_square_to_pos('e1'), _square_to_pos('c1'));
169                 } else {
170                         return (_square_to_pos('e8'), _square_to_pos('c8'));
171                 }
172         }
173
174         # Parse promo
175         my $promo;
176         if ($move =~ s/=([QRNB])$//) {
177                 $promo = $1;
178         }
179
180         $move =~ /^([KQRBN])?([a-h])?([1-8])?x?([a-h][1-8])$/ or die "Invalid move $move";
181         my $piece = $1 // 'P';
182         my $from_col = defined($2) ? _col_letter_to_num($2) : undef;
183         my $from_row = defined($3) ? _row_letter_to_num($3) : undef;
184         my ($to_row, $to_col) = _square_to_pos($4);
185         
186         # Find all possible from-squares that could have been meant.
187         my @squares = ();
188         if ($toplay eq 'B') {
189                 $piece = lc($piece);
190         }
191         for my $row (0..7) {
192                 next if (defined($from_row) && $from_row != $row);
193                 for my $col (0..7) {
194                         next if (defined($from_col) && $from_col != $col);
195                         next if ($board->[$row][$col] ne $piece);
196                         next if (!$board->can_reach($piece, $row, $col, $to_row, $to_col));
197                         push @squares, [ $row, $col ];
198                 }
199         }
200         if (scalar @squares != 1) {
201                 die "Ambigious or impossible move $move";
202         }
203         return (@{$squares[0]}, $to_row, $to_col, $promo);
204 }
205
206 sub fen {
207         my ($board) = @_;
208         my @rows = ();
209         for my $row (0..7) {
210                 my $str = join('', @{$board->[$row]});
211                 $str =~ s/(-+)/length($1)/ge;
212                 push @rows, $str;
213         }
214
215         return join('/', @rows);
216 }
217
218 sub can_reach {
219         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
220         
221         # can't eat your own piece
222         my $dest_piece = $board->[$to_row][$to_col];
223         if ($dest_piece ne '-') {
224                 return 0 if (($piece eq lc($piece)) == ($dest_piece eq lc($dest_piece)));
225         }
226
227         if (lc($piece) eq 'k') {
228                 return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1);
229         }
230         if (lc($piece) eq 'r') {
231                 return 0 unless ($from_row == $to_row || $from_col == $to_col);
232
233                 # check that there's a clear passage
234                 if ($from_row == $to_row) {
235                         if ($from_col > $to_col) {
236                                 ($to_col, $from_col) = ($from_col, $to_col);
237                         }
238
239                         for my $c (($from_col+1)..($to_col-1)) {
240                                 my $middle_piece = $board->[$to_row][$c];
241                                 return 0 if ($middle_piece ne '-');
242                         }
243
244                         return 1;
245                 } else {
246                         if ($from_row > $to_row) {
247                                 ($to_row, $from_row) = ($from_row, $to_row);
248                         }
249
250                         for my $r (($from_row+1)..($to_row-1)) {
251                                 my $middle_piece = $board->[$r][$to_col];
252                                 return 0 if ($middle_piece ne '-');     
253                         }
254
255                         return 1;
256                 }
257         }
258         if (lc($piece) eq 'b') {
259                 return 0 unless (abs($from_row - $to_row) == abs($from_col - $to_col));
260
261                 my $dr = ($to_row - $from_row) / abs($to_row - $from_row);
262                 my $dc = ($to_col - $from_col) / abs($to_col - $from_col);
263
264                 my $r = $from_row + $dr;
265                 my $c = $from_col + $dc;
266
267                 while ($r != $to_row) {
268                         my $middle_piece = $board->[$r][$c];
269                         return 0 if ($middle_piece ne '-');
270                         
271                         $r += $dr;
272                         $c += $dc;
273                 }
274
275                 return 1;
276         }
277         if (lc($piece) eq 'n') {
278                 my $diff_r = abs($from_row - $to_row);
279                 my $diff_c = abs($from_col - $to_col);
280                 return 1 if ($diff_r == 2 && $diff_c == 1);
281                 return 1 if ($diff_r == 1 && $diff_c == 2);
282                 return 0;
283         }
284         if ($piece eq 'q') {
285                 return (can_reach($board, 'r', $from_row, $from_col, $to_row, $to_col) ||
286                         can_reach($board, 'b', $from_row, $from_col, $to_row, $to_col));
287         }
288         if ($piece eq 'Q') {
289                 return (can_reach($board, 'R', $from_row, $from_col, $to_row, $to_col) ||
290                         can_reach($board, 'B', $from_row, $from_col, $to_row, $to_col));
291         }
292
293         # TODO: en passant
294         if ($piece eq 'p') {
295                 # black pawn
296                 if ($to_col == $from_col && $to_row == $from_row + 1) {
297                         return ($dest_piece eq '-');
298                 }
299                 if ($to_col == $from_col && $from_row == 1 && $to_row == 3) {
300                         my $middle_piece = $board->[2][$to_col];
301                         return ($dest_piece eq '-' && $middle_piece eq '-');
302                 }
303                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) {
304                         return ($dest_piece ne '-');
305                 }
306                 return 0;
307         }
308         if ($piece eq 'P') {
309                 # white pawn
310                 if ($to_col == $from_col && $to_row == $from_row - 1) {
311                         return ($dest_piece eq '-');
312                 }
313                 if ($to_col == $from_col && $from_row == 6 && $to_row == 4) {
314                         my $middle_piece = $board->[5][$to_col];
315                         return ($dest_piece eq '-' && $middle_piece eq '-');
316                 }
317                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) {
318                         return ($dest_piece ne '-');
319                 }
320                 return 0;
321         }
322         
323         # unknown piece
324         return 0;
325 }
326
327 # Returns 'none', 'white', 'black' or 'both', depending on which sides are in check.
328 # The latter naturally indicates an invalid position.
329 sub in_check {
330         my $board = shift;
331         my ($black_check, $white_check) = (0, 0);
332
333         my ($wkr, $wkc, $bkr, $bkc) = _find_kings($board);
334
335         # check all pieces for the possibility of threatening the two kings
336         for my $row (0..7) {
337                 for my $col (0..7) {
338                         my $piece = $board->[$row][$col];
339                         next if ($piece eq '-');
340                 
341                         if (uc($piece) eq $piece) {
342                                 # white piece
343                                 $black_check = 1 if ($board->can_reach($piece, $row, $col, $bkr, $bkc));
344                         } else {
345                                 # black piece
346                                 $white_check = 1 if ($board->can_reach($piece, $row, $col, $wkr, $wkc));
347                         }
348                 }
349         }
350
351         if ($black_check && $white_check) {
352                 return 'both';
353         } elsif ($black_check) {
354                 return 'black';
355         } elsif ($white_check) {
356                 return 'white';
357         } else {
358                 return 'none';
359         }
360 }
361
362 sub _find_kings {
363         my $board = shift;
364         my ($wkr, $wkc, $bkr, $bkc);
365
366         for my $row (0..7) {
367                 for my $col (0..7) {
368                         my $piece = $board->[$row][$col];
369                         if ($piece eq 'K') {
370                                 ($wkr, $wkc) = ($row, $col);
371                         } elsif ($piece eq 'k') {
372                                 ($bkr, $bkc) = ($row, $col);
373                         }
374                 }
375         }
376
377         return ($wkr, $wkc, $bkr, $bkc);
378 }
379
380 # Returns if any side is in mate.
381 sub in_mate {
382         my $board = shift;
383         my $check = $board->in_check();
384         return 0 if ($check eq 'none');
385
386         # try all possible moves for the side in check
387         for my $row (0..7) {
388                 for my $col (0..7) {
389                         my $piece = $board->[$row][$col];
390                         next if ($piece eq '-');
391
392                         if ($check eq 'white') {
393                                 next if ($piece eq lc($piece));
394                         } else {
395                                 next if ($piece eq uc($piece));
396                         }
397
398                         for my $dest_row (0..7) {
399                                 for my $dest_col (0..7) {
400                                         next if ($row == $dest_row && $col == $dest_col);
401                                         next unless ($board->can_reach($piece, $row, $col, $dest_row, $dest_col));
402
403                                         my $nb = $board->clone();
404                                         $nb->[$row][$col] = '-';
405                                         $nb->[$dest_row][$dest_col] = $piece;
406                                         my $new_check = $nb->in_check();
407                                         return 0 if ($new_check ne $check && $new_check ne 'both');
408                                 }
409                         }
410                 }
411         }
412
413         # nothing to do; mate
414         return 1;
415 }
416
417 # Returns the short algebraic form of the move, as well as the new position.
418 sub prettyprint_move {
419         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
420         my $pretty = $board->_prettyprint_move_no_check_or_mate($from_row, $from_col, $to_row, $to_col, $promo);
421
422         my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo);
423         if ($nb->in_mate()) {
424                 $pretty .= '#';
425         } elsif ($nb->in_check() ne 'none') {
426                 $pretty .= '+';
427         }
428         return ($pretty, $nb);
429 }
430
431 sub _prettyprint_move_no_check_or_mate {
432         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
433         my $piece = $board->[$from_row][$from_col];
434         my $move = _move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
435
436         if ($piece eq '-') {
437                 die "Invalid move $move";
438         }
439
440         # white short castling
441         if ($move eq 'e1g1' && $piece eq 'K') {
442                 return '0-0';
443         }
444
445         # white long castling
446         if ($move eq 'e1c1' && $piece eq 'K') {
447                 return '0-0-0';
448         }
449
450         # black short castling
451         if ($move eq 'e8g8' && $piece eq 'k') {
452                 return '0-0';
453         }
454
455         # black long castling
456         if ($move eq 'e8c8' && $piece eq 'k') {
457                 return '0-0-0';
458         }
459
460         my $pretty;
461
462         # check if the from-piece is a pawn
463         if (lc($piece) eq 'p') {
464                 # attack?
465                 if ($from_col != $to_col) {
466                         $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col);
467                 } else {
468                         $pretty = _pos_to_square($to_row, $to_col);
469
470                         if (defined($promo) && $promo ne '') {
471                                 # promotion
472                                 $pretty .= "=";
473                                 $pretty .= $promo;
474                         }
475                 }
476                 return $pretty;
477         }
478
479         $pretty = uc($piece);
480
481         # see how many of these pieces could go here, in all
482         my $num_total = 0;
483         for my $col (0..7) {
484                 for my $row (0..7) {
485                         next unless ($board->[$row][$col] eq $piece);
486                         ++$num_total if ($board->can_reach($piece, $row, $col, $to_row, $to_col));
487                 }
488         }
489
490         # see how many of these pieces from the given row could go here
491         my $num_row = 0;
492         for my $col (0..7) {
493                 next unless ($board->[$from_row][$col] eq $piece);
494                 ++$num_row if ($board->can_reach($piece, $from_row, $col, $to_row, $to_col));
495         }
496
497         # and same for columns
498         my $num_col = 0;
499         for my $row (0..7) {
500                 next unless ($board->[$row][$from_col] eq $piece);
501                 ++$num_col if ($board->can_reach($piece, $row, $from_col, $to_row, $to_col));
502         }
503
504         # see if we need to disambiguate
505         if ($num_total > 1) {
506                 if ($num_col == 1) {
507                         $pretty .= substr($move, 0, 1);
508                 } elsif ($num_row == 1) {
509                         $pretty .= substr($move, 1, 1);
510                 } else {
511                         $pretty .= substr($move, 0, 2);
512                 }
513         }
514
515         # attack?
516         if ($board->[$to_row][$to_col] ne '-') {
517                 $pretty .= 'x';
518         }
519
520         $pretty .= _pos_to_square($to_row, $to_col);
521         return $pretty;
522 }
523
524 1;