]> git.sesse.net Git - remoteglot/blob - Board.pm
Merge commit '83d2eb4'
[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;
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         if (!defined($piece) && (!defined($from_col) || !defined($from_row))) {
184                 $piece = 'P';
185         }
186         my ($to_row, $to_col) = _square_to_pos($4);
187         
188         # Find all possible from-squares that could have been meant.
189         my @squares = ();
190         my $side = 'K';
191         if ($toplay eq 'B') {
192                 $piece = lc($piece) if defined($piece);
193                 $side = 'k';
194         }
195         for my $row (0..7) {
196                 next if (defined($from_row) && $from_row != $row);
197                 for my $col (0..7) {
198                         next if (defined($from_col) && $from_col != $col);
199                         next if (defined($piece) && $board->[$row][$col] ne $piece);
200                         push @squares, [ $row, $col ];
201                 }
202         }
203
204         # Filter out pieces which cannot reach this square.
205         @squares = grep { $board->can_reach($piece, $_->[0], $_->[1], $to_row, $to_col) } @squares;
206
207         # See if doing this move would put us in check
208         # (yes, there are clients that expect us to do this).
209         @squares = grep { !$board->make_move($_->[0], $_->[1], $to_row, $to_col, $promo)->in_check($side) } @squares;
210
211         if (scalar @squares == 0) {
212                 die "Impossible move $move";
213         }
214         if (scalar @squares != 1) {
215                 die "Ambigious move $move";
216         }
217         return (@{$squares[0]}, $to_row, $to_col, $promo);
218 }
219
220 sub fen {
221         my ($board) = @_;
222         my @rows = ();
223         for my $row (0..7) {
224                 my $str = join('', @{$board->[$row]});
225                 $str =~ s/(-+)/length($1)/ge;
226                 push @rows, $str;
227         }
228
229         return join('/', @rows);
230 }
231
232 # Returns a compact bit string describing the same data as fen().
233 # This is encoded using a Huffman-like encoding, and should be
234 # typically about 1/3 the number of bytes.
235 sub bitpacked_fen {
236         my ($board) = @_;
237         my $bits = "";
238
239         for my $row (0..7) {
240                 for my $col (0..7) {
241                         my $piece = $board->[$row][$col];
242                         if ($piece eq '-') {
243                                 $bits .= "0";
244                                 next;
245                         }
246
247                         my $color = (lc($piece) eq $piece) ? 0 : 1;
248                         $bits .= "1" . $color;
249
250                         if (lc($piece) eq 'p') {
251                                 $bits .= "0";
252                         } elsif (lc($piece) eq 'n') {
253                                 $bits .= "100";
254                         } elsif (lc($piece) eq 'b') {
255                                 $bits .= "101";
256                         } elsif (lc($piece) eq 'r') {
257                                 $bits .= "1110";
258                         } elsif (lc($piece) eq 'q') {
259                                 $bits .= "11110";
260                         } elsif (lc($piece) eq 'k') {
261                                 $bits .= "11111";
262                         } else {
263                                 die "Unknown piece $piece";
264                         }
265                 }
266         }
267
268         return pack('b*', $bits);
269 }
270
271 sub can_reach {
272         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
273         
274         # can't eat your own piece
275         my $dest_piece = $board->[$to_row][$to_col];
276         if ($dest_piece ne '-') {
277                 return 0 if (($piece eq lc($piece)) == ($dest_piece eq lc($dest_piece)));
278         }
279         
280         if ($piece eq 'p') {
281                 # black pawn
282                 if ($to_col == $from_col && $to_row == $from_row + 1) {
283                         return ($dest_piece eq '-');
284                 }
285                 if ($to_col == $from_col && $from_row == 1 && $to_row == 3) {
286                         my $middle_piece = $board->[2][$to_col];
287                         return ($dest_piece eq '-' && $middle_piece eq '-');
288                 }
289                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) {
290                         if ($dest_piece eq '-') {
291                                 # En passant. TODO: check that the last move was indeed an EP move
292                                 return ($to_row == 5 && $board->[4][$to_col] eq 'P');
293                         } else {
294                                 return 1;
295                         }
296                 }
297                 return 0;
298         }
299         if ($piece eq 'P') {
300                 # white pawn
301                 if ($to_col == $from_col && $to_row == $from_row - 1) {
302                         return ($dest_piece eq '-');
303                 }
304                 if ($to_col == $from_col && $from_row == 6 && $to_row == 4) {
305                         my $middle_piece = $board->[5][$to_col];
306                         return ($dest_piece eq '-' && $middle_piece eq '-');
307                 }
308                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) {
309                         if ($dest_piece eq '-') {
310                                 # En passant. TODO: check that the last move was indeed an EP move
311                                 return ($to_row == 2 && $board->[3][$to_col] eq 'p');
312                         } else {
313                                 return 1;
314                         }
315                 }
316                 return 0;
317         }
318         
319         if (lc($piece) eq 'r') {
320                 return 0 unless ($from_row == $to_row || $from_col == $to_col);
321
322                 # check that there's a clear passage
323                 if ($from_row == $to_row) {
324                         if ($from_col > $to_col) {
325                                 ($to_col, $from_col) = ($from_col, $to_col);
326                         }
327
328                         for my $c (($from_col+1)..($to_col-1)) {
329                                 my $middle_piece = $board->[$to_row][$c];
330                                 return 0 if ($middle_piece ne '-');
331                         }
332
333                         return 1;
334                 } else {
335                         if ($from_row > $to_row) {
336                                 ($to_row, $from_row) = ($from_row, $to_row);
337                         }
338
339                         for my $r (($from_row+1)..($to_row-1)) {
340                                 my $middle_piece = $board->[$r][$to_col];
341                                 return 0 if ($middle_piece ne '-');     
342                         }
343
344                         return 1;
345                 }
346         }
347         if (lc($piece) eq 'b') {
348                 return 0 unless (abs($from_row - $to_row) == abs($from_col - $to_col));
349
350                 my $dr = ($to_row - $from_row) / abs($to_row - $from_row);
351                 my $dc = ($to_col - $from_col) / abs($to_col - $from_col);
352
353                 my $r = $from_row + $dr;
354                 my $c = $from_col + $dc;
355
356                 while ($r != $to_row) {
357                         my $middle_piece = $board->[$r][$c];
358                         return 0 if ($middle_piece ne '-');
359                         
360                         $r += $dr;
361                         $c += $dc;
362                 }
363
364                 return 1;
365         }
366         if (lc($piece) eq 'n') {
367                 my $diff_r = abs($from_row - $to_row);
368                 my $diff_c = abs($from_col - $to_col);
369                 return 1 if ($diff_r == 2 && $diff_c == 1);
370                 return 1 if ($diff_r == 1 && $diff_c == 2);
371                 return 0;
372         }
373         if ($piece eq 'q') {
374                 return (can_reach($board, 'r', $from_row, $from_col, $to_row, $to_col) ||
375                         can_reach($board, 'b', $from_row, $from_col, $to_row, $to_col));
376         }
377         if ($piece eq 'Q') {
378                 return (can_reach($board, 'R', $from_row, $from_col, $to_row, $to_col) ||
379                         can_reach($board, 'B', $from_row, $from_col, $to_row, $to_col));
380         }
381         if (lc($piece) eq 'k') {
382                 return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1);
383         }
384
385         # unknown piece
386         return 0;
387 }
388
389 # Like can_reach, but also checks the move doesn't put the side in check.
390 # We use this in prettyprint_move to reduce the disambiguation, because Chess.js
391 # needs moves to be in minimally disambiguated form.
392 sub can_legally_reach {
393         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
394
395         return 0 if (!can_reach($board, $piece, $from_row, $from_col, $to_row, $to_col));
396
397         my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col);
398         my $side = ($piece eq lc($piece)) ? 'k' : 'K';
399
400         return !in_check($nb, $side);
401 }
402
403 my %pieces_against_side = (
404         k => { K => 1, Q => 1, R => 1, N => 1, B => 1, P => 1 },
405         K => { k => 1, q => 1, r => 1, n => 1, b => 1, p => 1 },
406 );
407
408 # Returns whether the given side (given as k or K for black and white) is in check.
409 sub in_check {
410         my ($board, $side) = @_;
411         my ($kr, $kc) = _find_piece($board, $side);
412
413         # check all pieces for the possibility of threatening this king
414         for my $row (0..7) {
415                 next unless grep { exists($pieces_against_side{$side}{$_}) } @{$board->[$row]};
416                 for my $col (0..7) {
417                         my $piece = $board->[$row][$col];
418                         next if ($piece eq '-');
419                         return 1 if ($board->can_reach($piece, $row, $col, $kr, $kc));
420                 }
421         }
422
423         return 0;
424 }
425
426 sub _find_piece {
427         my ($board, $piece) = @_;
428
429         for my $row (0..7) {
430                 next unless grep { $_ eq $piece } @{$board->[$row]};
431                 for my $col (0..7) {
432                         if ($board->[$row][$col] eq $piece) {
433                                 return ($row, $col);
434                         }
435                 }
436         }
437
438         return (undef, undef);
439 }
440
441 # Returns if the given side (given as k or K) is in mate.
442 sub in_mate {
443         my ($board, $side, $in_check) = @_;
444         return 0 if (!$in_check);
445
446         # try all possible moves for the side in check
447         for my $row (0..7) {
448                 for my $col (0..7) {
449                         my $piece = $board->[$row][$col];
450                         next if ($piece eq '-');
451
452                         if ($side eq 'K') {
453                                 next if ($piece eq lc($piece));
454                         } else {
455                                 next if ($piece eq uc($piece));
456                         }
457
458                         for my $dest_row (0..7) {
459                                 for my $dest_col (0..7) {
460                                         next if ($row == $dest_row && $col == $dest_col);
461                                         next unless ($board->can_reach($piece, $row, $col, $dest_row, $dest_col));
462
463                                         my $nb = $board->make_move($row, $col, $dest_row, $dest_col);
464                                         return 0 if (!$nb->in_check($side));
465                                 }
466                         }
467                 }
468         }
469
470         # nothing to do; mate
471         return 1;
472 }
473
474 # Returns the short algebraic form of the move, as well as the new position.
475 sub prettyprint_move {
476         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
477         my $pretty = $board->_prettyprint_move_no_check_or_mate($from_row, $from_col, $to_row, $to_col, $promo);
478
479         my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo);
480
481         my $piece = $board->[$from_row][$from_col];
482         my $other_side = (uc($piece) eq $piece) ? 'k' : 'K';
483         my $in_check = $nb->in_check($other_side);
484         if ($nb->in_mate($other_side, $in_check)) {
485                 $pretty .= '#';
486         } elsif ($in_check) {
487                 $pretty .= '+';
488         }
489         return ($pretty, $nb);
490 }
491
492 sub num_pieces {
493         my ($board) = @_;
494
495         my $num = 0;
496         for my $row (0..7) {
497                 for my $col (0..7) {
498                         my $piece = $board->[$row][$col];
499                         ++$num if ($piece ne '-');
500                 }
501         }
502         return $num;    
503 }
504
505 sub _prettyprint_move_no_check_or_mate {
506         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
507         my $piece = $board->[$from_row][$from_col];
508         my $move = move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
509
510         if ($piece eq '-') {
511                 die "Invalid move $move";
512         }
513
514         # white short castling
515         if ($move eq 'e1g1' && $piece eq 'K') {
516                 return 'O-O';
517         }
518
519         # white long castling
520         if ($move eq 'e1c1' && $piece eq 'K') {
521                 return 'O-O-O';
522         }
523
524         # black short castling
525         if ($move eq 'e8g8' && $piece eq 'k') {
526                 return 'O-O';
527         }
528
529         # black long castling
530         if ($move eq 'e8c8' && $piece eq 'k') {
531                 return 'O-O-O';
532         }
533
534         my $pretty;
535
536         # check if the from-piece is a pawn
537         if (lc($piece) eq 'p') {
538                 # attack?
539                 if ($from_col != $to_col) {
540                         $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col);
541                 } else {
542                         $pretty = _pos_to_square($to_row, $to_col);
543                 }
544
545                 if (defined($promo) && $promo ne '') {
546                         # promotion
547                         $pretty .= "=";
548                         $pretty .= uc($promo);
549                 }
550                 return $pretty;
551         }
552
553         $pretty = uc($piece);
554
555         # see how many of these pieces could go here, in all
556         my $num_total = 0;
557         for my $col (0..7) {
558                 for my $row (0..7) {
559                         next unless ($board->[$row][$col] eq $piece);
560                         ++$num_total if ($board->can_legally_reach($piece, $row, $col, $to_row, $to_col));
561                 }
562         }
563
564         # see how many of these pieces from the given row could go here
565         my $num_row = 0;
566         for my $col (0..7) {
567                 next unless ($board->[$from_row][$col] eq $piece);
568                 ++$num_row if ($board->can_legally_reach($piece, $from_row, $col, $to_row, $to_col));
569         }
570
571         # and same for columns
572         my $num_col = 0;
573         for my $row (0..7) {
574                 next unless ($board->[$row][$from_col] eq $piece);
575                 ++$num_col if ($board->can_legally_reach($piece, $row, $from_col, $to_row, $to_col));
576         }
577
578         # see if we need to disambiguate
579         if ($num_total > 1) {
580                 if ($num_col == 1) {
581                         $pretty .= substr($move, 0, 1);
582                 } elsif ($num_row == 1) {
583                         $pretty .= substr($move, 1, 1);
584                 } else {
585                         $pretty .= substr($move, 0, 2);
586                 }
587         }
588
589         # attack?
590         if ($board->[$to_row][$to_col] ne '-') {
591                 $pretty .= 'x';
592         }
593
594         $pretty .= _pos_to_square($to_row, $to_col);
595         return $pretty;
596 }
597
598 1;