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