921de3d9c45f715e7084796aeb1a005bc5270093
[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                 $nb->[$row] = [ @{$board->[$row]} ];
29         }
30
31         return bless $nb;
32 }
33
34 # Returns a new board.
35 sub make_move {
36         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
37         my $move = move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
38         my $piece = $board->[$from_row][$from_col];
39         my $nb = $board->clone();
40
41         if ($piece eq '-') {
42                 die "Invalid move $move";
43         }
44
45         # white short castling
46         if ($move eq 'e1g1' && $piece eq 'K') {
47                 # king
48                 $nb->[7][4] = '-';
49                 $nb->[7][6] = $piece;
50
51                 # rook
52                 $nb->[7][7] = '-';
53                 $nb->[7][5] = 'R';
54
55                 return $nb;
56         }
57
58         # white long castling
59         if ($move eq 'e1c1' && $piece eq 'K') {
60                 # king
61                 $nb->[7][4] = '-';
62                 $nb->[7][2] = $piece;
63
64                 # rook
65                 $nb->[7][0] = '-';
66                 $nb->[7][3] = 'R';
67
68                 return $nb;
69         }
70
71         # black short castling
72         if ($move eq 'e8g8' && $piece eq 'k') {
73                 # king
74                 $nb->[0][4] = '-';
75                 $nb->[0][6] = $piece;
76
77                 # rook
78                 $nb->[0][7] = '-';
79                 $nb->[0][5] = 'r';
80
81                 return $nb;
82         }
83
84         # black long castling
85         if ($move eq 'e8c8' && $piece eq 'k') {
86                 # king
87                 $nb->[0][4] = '-';
88                 $nb->[0][2] = $piece;
89
90                 # rook
91                 $nb->[0][0] = '-';
92                 $nb->[0][3] = 'r';
93
94                 return $nb;
95         }
96
97         # check if the from-piece is a pawn
98         if (lc($piece) eq 'p') {
99                 # attack?
100                 if ($from_col != $to_col) {
101                         # en passant?
102                         if ($board->[$to_row][$to_col] eq '-') {
103                                 if ($piece eq 'p') {
104                                         $nb->[$to_row - 1][$to_col] = '-';
105                                 } else {
106                                         $nb->[$to_row + 1][$to_col] = '-';
107                                 }
108                         }
109                 }
110                 if (defined($promo) && $promo ne '') {
111                         if ($piece eq 'p') {
112                                 $piece = lc($promo);
113                         } else {
114                                 $piece = uc($promo);
115                         }
116                 }
117         }
118
119         # update the board
120         $nb->[$from_row][$from_col] = '-';
121         $nb->[$to_row][$to_col] = $piece;
122
123         return $nb;
124 }
125
126 sub _pos_to_square {
127         my ($row, $col) = @_;
128         return sprintf("%c%d", ord('a') + $col, 8 - $row);
129 }
130
131 sub _col_letter_to_num {
132         return ord(shift) - ord('a');
133 }
134
135 sub _row_letter_to_num {
136         return 7 - (ord(shift) - ord('1'));
137 }
138
139 sub _square_to_pos {
140         my ($square) = @_;
141         $square =~ /^([a-h])([1-8])$/ or die "Invalid square $square";  
142         return (_row_letter_to_num($2), _col_letter_to_num($1));
143 }
144
145 sub move_to_uci_notation {
146         my ($from_row, $from_col, $to_row, $to_col, $promo) = @_;
147         $promo //= "";
148         return _pos_to_square($from_row, $from_col) . _pos_to_square($to_row, $to_col) . $promo;
149 }
150
151 # Note: This is in general not a validation that the move is actually allowed
152 # (e.g. you can castle even though you're in check).
153 sub parse_pretty_move {
154         my ($board, $move, $toplay) = @_;
155
156         # Strip check or mate
157         $move =~ s/[+#]$//;
158
159         if ($move eq '0-0' or $move eq 'O-O') {
160                 if ($toplay eq 'W') {
161                         return (_square_to_pos('e1'), _square_to_pos('g1'));
162                 } else {
163                         return (_square_to_pos('e8'), _square_to_pos('g8'));
164                 }
165         } elsif ($move eq '0-0-0' or $move eq 'O-O-O') {
166                 if ($toplay eq 'W') {
167                         return (_square_to_pos('e1'), _square_to_pos('c1'));
168                 } else {
169                         return (_square_to_pos('e8'), _square_to_pos('c8'));
170                 }
171         }
172
173         # Parse promo
174         my $promo;
175         if ($move =~ s/=?([QRNB])$//) {
176                 $promo = $1;
177         }
178
179         $move =~ /^([KQRBN])?([a-h])?([1-8])?x?([a-h][1-8])$/ or die "Invalid move $move";
180         my $piece = $1 // 'P';
181         my $from_col = defined($2) ? _col_letter_to_num($2) : undef;
182         my $from_row = defined($3) ? _row_letter_to_num($3) : undef;
183         my ($to_row, $to_col) = _square_to_pos($4);
184         
185         # Find all possible from-squares that could have been meant.
186         my @squares = ();
187         my $side = 'K';
188         if ($toplay eq 'B') {
189                 $piece = lc($piece);
190                 $side = 'k';
191         }
192         for my $row (0..7) {
193                 next if (defined($from_row) && $from_row != $row);
194                 for my $col (0..7) {
195                         next if (defined($from_col) && $from_col != $col);
196                         next if ($board->[$row][$col] ne $piece);
197                         push @squares, [ $row, $col ];
198                 }
199         }
200         if (scalar @squares > 1) {
201                 # Filter out pieces which cannot reach this square.
202                 @squares = grep { $board->can_reach($piece, $_->[0], $_->[1], $to_row, $to_col) } @squares;
203         }
204         if (scalar @squares > 1) {
205                 # See if doing this move would put us in check
206                 # (yes, there are clients that expect us to do this).
207                 @squares = grep { !$board->make_move($_->[0], $_->[1], $to_row, $to_col, $promo)->in_check($side) } @squares;
208         }
209         if (scalar @squares == 0) {
210                 die "Impossible move $move";
211         }
212         if (scalar @squares != 1) {
213                 die "Ambigious move $move";
214         }
215         return (@{$squares[0]}, $to_row, $to_col, $promo);
216 }
217
218 sub fen {
219         my ($board) = @_;
220         my @rows = ();
221         for my $row (0..7) {
222                 my $str = join('', @{$board->[$row]});
223                 $str =~ s/(-+)/length($1)/ge;
224                 push @rows, $str;
225         }
226
227         return join('/', @rows);
228 }
229
230 # Returns a compact bit string describing the same data as fen().
231 # This is encoded using a Huffman-like encoding, and should be
232 # typically about 1/3 the number of bytes.
233 sub bitpacked_fen {
234         my ($board) = @_;
235         my $bits = "";
236
237         for my $row (0..7) {
238                 for my $col (0..7) {
239                         my $piece = $board->[$row][$col];
240                         if ($piece eq '-') {
241                                 $bits .= "0";
242                                 next;
243                         }
244
245                         my $color = (lc($piece) eq $piece) ? 0 : 1;
246                         $bits .= "1" . $color;
247
248                         if (lc($piece) eq 'p') {
249                                 $bits .= "0";
250                         } elsif (lc($piece) eq 'n') {
251                                 $bits .= "100";
252                         } elsif (lc($piece) eq 'b') {
253                                 $bits .= "101";
254                         } elsif (lc($piece) eq 'r') {
255                                 $bits .= "1110";
256                         } elsif (lc($piece) eq 'q') {
257                                 $bits .= "11110";
258                         } elsif (lc($piece) eq 'k') {
259                                 $bits .= "11111";
260                         } else {
261                                 die "Unknown piece $piece";
262                         }
263                 }
264         }
265
266         return pack('b*', $bits);
267 }
268
269 sub can_reach {
270         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
271         
272         # can't eat your own piece
273         my $dest_piece = $board->[$to_row][$to_col];
274         if ($dest_piece ne '-') {
275                 return 0 if (($piece eq lc($piece)) == ($dest_piece eq lc($dest_piece)));
276         }
277         
278         if ($piece eq 'p') {
279                 # black pawn
280                 if ($to_col == $from_col && $to_row == $from_row + 1) {
281                         return ($dest_piece eq '-');
282                 }
283                 if ($to_col == $from_col && $from_row == 1 && $to_row == 3) {
284                         my $middle_piece = $board->[2][$to_col];
285                         return ($dest_piece eq '-' && $middle_piece eq '-');
286                 }
287                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) {
288                         if ($dest_piece eq '-') {
289                                 # En passant. TODO: check that the last move was indeed an EP move
290                                 return ($to_row == 5 && $board->[4][$to_col] eq 'P');
291                         } else {
292                                 return 1;
293                         }
294                 }
295                 return 0;
296         }
297         if ($piece eq 'P') {
298                 # white pawn
299                 if ($to_col == $from_col && $to_row == $from_row - 1) {
300                         return ($dest_piece eq '-');
301                 }
302                 if ($to_col == $from_col && $from_row == 6 && $to_row == 4) {
303                         my $middle_piece = $board->[5][$to_col];
304                         return ($dest_piece eq '-' && $middle_piece eq '-');
305                 }
306                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) {
307                         if ($dest_piece eq '-') {
308                                 # En passant. TODO: check that the last move was indeed an EP move
309                                 return ($to_row == 2 && $board->[3][$to_col] eq 'p');
310                         } else {
311                                 return 1;
312                         }
313                 }
314                 return 0;
315         }
316         
317         if (lc($piece) eq 'r') {
318                 return 0 unless ($from_row == $to_row || $from_col == $to_col);
319
320                 # check that there's a clear passage
321                 if ($from_row == $to_row) {
322                         if ($from_col > $to_col) {
323                                 ($to_col, $from_col) = ($from_col, $to_col);
324                         }
325
326                         for my $c (($from_col+1)..($to_col-1)) {
327                                 my $middle_piece = $board->[$to_row][$c];
328                                 return 0 if ($middle_piece ne '-');
329                         }
330
331                         return 1;
332                 } else {
333                         if ($from_row > $to_row) {
334                                 ($to_row, $from_row) = ($from_row, $to_row);
335                         }
336
337                         for my $r (($from_row+1)..($to_row-1)) {
338                                 my $middle_piece = $board->[$r][$to_col];
339                                 return 0 if ($middle_piece ne '-');     
340                         }
341
342                         return 1;
343                 }
344         }
345         if (lc($piece) eq 'b') {
346                 return 0 unless (abs($from_row - $to_row) == abs($from_col - $to_col));
347
348                 my $dr = ($to_row - $from_row) / abs($to_row - $from_row);
349                 my $dc = ($to_col - $from_col) / abs($to_col - $from_col);
350
351                 my $r = $from_row + $dr;
352                 my $c = $from_col + $dc;
353
354                 while ($r != $to_row) {
355                         my $middle_piece = $board->[$r][$c];
356                         return 0 if ($middle_piece ne '-');
357                         
358                         $r += $dr;
359                         $c += $dc;
360                 }
361
362                 return 1;
363         }
364         if (lc($piece) eq 'n') {
365                 my $diff_r = abs($from_row - $to_row);
366                 my $diff_c = abs($from_col - $to_col);
367                 return 1 if ($diff_r == 2 && $diff_c == 1);
368                 return 1 if ($diff_r == 1 && $diff_c == 2);
369                 return 0;
370         }
371         if ($piece eq 'q') {
372                 return (can_reach($board, 'r', $from_row, $from_col, $to_row, $to_col) ||
373                         can_reach($board, 'b', $from_row, $from_col, $to_row, $to_col));
374         }
375         if ($piece eq 'Q') {
376                 return (can_reach($board, 'R', $from_row, $from_col, $to_row, $to_col) ||
377                         can_reach($board, 'B', $from_row, $from_col, $to_row, $to_col));
378         }
379         if (lc($piece) eq 'k') {
380                 return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1);
381         }
382
383         # unknown piece
384         return 0;
385 }
386
387 my %pieces_against_side = (
388         k => { K => 1, Q => 1, R => 1, N => 1, B => 1, P => 1 },
389         K => { k => 1, q => 1, r => 1, n => 1, b => 1, p => 1 },
390 );
391
392 # Returns whether the given side (given as k or K for black and white) is in check.
393 sub in_check {
394         my ($board, $side) = @_;
395         my ($kr, $kc) = _find_piece($board, $side);
396
397         # check all pieces for the possibility of threatening this king
398         for my $row (0..7) {
399                 next unless grep { exists($pieces_against_side{$side}{$_}) } @{$board->[$row]};
400                 for my $col (0..7) {
401                         my $piece = $board->[$row][$col];
402                         next if ($piece eq '-');
403                         return 1 if ($board->can_reach($piece, $row, $col, $kr, $kc));
404                 }
405         }
406
407         return 0;
408 }
409
410 sub _find_piece {
411         my ($board, $piece) = @_;
412
413         for my $row (0..7) {
414                 next unless grep { $_ eq $piece } @{$board->[$row]};
415                 for my $col (0..7) {
416                         if ($board->[$row][$col] eq $piece) {
417                                 return ($row, $col);
418                         }
419                 }
420         }
421
422         return (undef, undef);
423 }
424
425 # Returns if the given side (given as k or K) is in mate.
426 sub in_mate {
427         my ($board, $side, $in_check) = @_;
428         return 0 if (!$in_check);
429
430         # try all possible moves for the side in check
431         for my $row (0..7) {
432                 for my $col (0..7) {
433                         my $piece = $board->[$row][$col];
434                         next if ($piece eq '-');
435
436                         if ($side eq 'K') {
437                                 next if ($piece eq lc($piece));
438                         } else {
439                                 next if ($piece eq uc($piece));
440                         }
441
442                         for my $dest_row (0..7) {
443                                 for my $dest_col (0..7) {
444                                         next if ($row == $dest_row && $col == $dest_col);
445                                         next unless ($board->can_reach($piece, $row, $col, $dest_row, $dest_col));
446
447                                         my $nb = $board->make_move($row, $col, $dest_row, $dest_col);
448                                         return 0 if (!$nb->in_check($side));
449                                 }
450                         }
451                 }
452         }
453
454         # nothing to do; mate
455         return 1;
456 }
457
458 # Returns the short algebraic form of the move, as well as the new position.
459 sub prettyprint_move {
460         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
461         my $pretty = $board->_prettyprint_move_no_check_or_mate($from_row, $from_col, $to_row, $to_col, $promo);
462
463         my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo);
464
465         my $piece = $board->[$from_row][$from_col];
466         my $other_side = (uc($piece) eq $piece) ? 'k' : 'K';
467         my $in_check = $nb->in_check($other_side);
468         if ($nb->in_mate($other_side, $in_check)) {
469                 $pretty .= '#';
470         } elsif ($in_check) {
471                 $pretty .= '+';
472         }
473         return ($pretty, $nb);
474 }
475
476 sub num_pieces {
477         my ($board) = @_;
478
479         my $num = 0;
480         for my $row (0..7) {
481                 for my $col (0..7) {
482                         my $piece = $board->[$row][$col];
483                         ++$num if ($piece ne '-');
484                 }
485         }
486         return $num;    
487 }
488
489 sub _prettyprint_move_no_check_or_mate {
490         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
491         my $piece = $board->[$from_row][$from_col];
492         my $move = move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
493
494         if ($piece eq '-') {
495                 die "Invalid move $move";
496         }
497
498         # white short castling
499         if ($move eq 'e1g1' && $piece eq 'K') {
500                 return '0-0';
501         }
502
503         # white long castling
504         if ($move eq 'e1c1' && $piece eq 'K') {
505                 return '0-0-0';
506         }
507
508         # black short castling
509         if ($move eq 'e8g8' && $piece eq 'k') {
510                 return '0-0';
511         }
512
513         # black long castling
514         if ($move eq 'e8c8' && $piece eq 'k') {
515                 return '0-0-0';
516         }
517
518         my $pretty;
519
520         # check if the from-piece is a pawn
521         if (lc($piece) eq 'p') {
522                 # attack?
523                 if ($from_col != $to_col) {
524                         $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col);
525                 } else {
526                         $pretty = _pos_to_square($to_row, $to_col);
527
528                         if (defined($promo) && $promo ne '') {
529                                 # promotion
530                                 $pretty .= "=";
531                                 $pretty .= uc($promo);
532                         }
533                 }
534                 return $pretty;
535         }
536
537         $pretty = uc($piece);
538
539         # see how many of these pieces could go here, in all
540         my $num_total = 0;
541         for my $col (0..7) {
542                 for my $row (0..7) {
543                         next unless ($board->[$row][$col] eq $piece);
544                         ++$num_total if ($board->can_reach($piece, $row, $col, $to_row, $to_col));
545                 }
546         }
547
548         # see how many of these pieces from the given row could go here
549         my $num_row = 0;
550         for my $col (0..7) {
551                 next unless ($board->[$from_row][$col] eq $piece);
552                 ++$num_row if ($board->can_reach($piece, $from_row, $col, $to_row, $to_col));
553         }
554
555         # and same for columns
556         my $num_col = 0;
557         for my $row (0..7) {
558                 next unless ($board->[$row][$from_col] eq $piece);
559                 ++$num_col if ($board->can_reach($piece, $row, $from_col, $to_row, $to_col));
560         }
561
562         # see if we need to disambiguate
563         if ($num_total > 1) {
564                 if ($num_col == 1) {
565                         $pretty .= substr($move, 0, 1);
566                 } elsif ($num_row == 1) {
567                         $pretty .= substr($move, 1, 1);
568                 } else {
569                         $pretty .= substr($move, 0, 2);
570                 }
571         }
572
573         # attack?
574         if ($board->[$to_row][$to_col] ne '-') {
575                 $pretty .= 'x';
576         }
577
578         $pretty .= _pos_to_square($to_row, $to_col);
579         return $pretty;
580 }
581
582 1;