]> git.sesse.net Git - remoteglot/blob - Board.pm
Implement Chess960 castling rules on the backend.
[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') {
50                         # king
51                         $nb->[7][$from_col] = '-';
52                         $nb->[7][6] = $piece;
53
54                         # rook
55                         $nb->[7][$to_col] = '-';
56                         $nb->[7][5] = 'R';
57
58                         return $nb;
59                 }
60
61                 # White long castling (regular or Chess960 king-takes-rook)
62                 if ($move eq 'e1c1' || $dst_piece eq 'R') {
63                         # king
64                         $nb->[7][$from_col] = '-';
65                         $nb->[7][2] = $piece;
66
67                         # rook
68                         $nb->[7][$to_col] = '-';
69                         $nb->[7][3] = 'R';
70
71                         return $nb;
72                 }
73         } elsif ($piece eq 'k') {
74                 my $dst_piece = $board->[$to_row][$to_col];
75
76                 # Black short castling (regular or Chess960 king-takes-rook)
77                 if ($move eq 'e8g8' || $dst_piece eq 'r') {
78                         # king
79                         $nb->[0][$from_col] = '-';
80                         $nb->[0][6] = $piece;
81
82                         # rook
83                         $nb->[0][$from_col] = '-';
84                         $nb->[0][5] = 'r';
85
86                         return $nb;
87                 }
88
89                 # black long castling
90                 if ($move eq 'e8c8' || $dst_piece eq 'r') {
91                         # king
92                         $nb->[0][$from_col] = '-';
93                         $nb->[0][2] = $piece;
94
95                         # rook
96                         $nb->[0][$to_col] = '-';
97                         $nb->[0][3] = 'r';
98
99                         return $nb;
100                 }
101         }
102
103         # check if the from-piece is a pawn
104         if (lc($piece) eq 'p') {
105                 # attack?
106                 if ($from_col != $to_col) {
107                         # en passant?
108                         if ($board->[$to_row][$to_col] eq '-') {
109                                 if ($piece eq 'p') {
110                                         $nb->[$to_row - 1][$to_col] = '-';
111                                 } else {
112                                         $nb->[$to_row + 1][$to_col] = '-';
113                                 }
114                         }
115                 }
116                 if (defined($promo) && $promo ne '') {
117                         if ($piece eq 'p') {
118                                 $piece = lc($promo);
119                         } else {
120                                 $piece = uc($promo);
121                         }
122                 }
123         }
124
125         # update the board
126         $nb->[$from_row][$from_col] = '-';
127         $nb->[$to_row][$to_col] = $piece;
128
129         return $nb;
130 }
131
132 sub _pos_to_square {
133         my ($row, $col) = @_;
134         return sprintf("%c%d", ord('a') + $col, 8 - $row);
135 }
136
137 sub _col_letter_to_num {
138         return ord(shift) - ord('a');
139 }
140
141 sub _row_letter_to_num {
142         return 7 - (ord(shift) - ord('1'));
143 }
144
145 use Carp;
146
147 sub _square_to_pos {
148         my ($square) = @_;
149         #$square =~ /^([a-h])([1-8])$/ or die "Invalid square $square"; 
150         $square =~ /^([a-h])([1-8])$/ or Carp::confess("Invalid square $square");
151         return (_row_letter_to_num($2), _col_letter_to_num($1));
152 }
153
154 sub move_to_uci_notation {
155         my ($from_row, $from_col, $to_row, $to_col, $promo) = @_;
156         $promo //= "";
157         return _pos_to_square($from_row, $from_col) . _pos_to_square($to_row, $to_col) . $promo;
158 }
159
160 sub _find_piece_col {
161         my ($row, $piece) = @_;
162         for my $col (0..7) {
163                 return $col if ($row->[$col] eq $piece);
164         }
165         die "Could not find piece $piece";
166 }
167
168 # Note: This is in general not a validation that the move is actually allowed
169 # (e.g. you can castle even though you're in check).
170 sub parse_pretty_move {
171         my ($board, $move, $toplay, $chess960, $white_castle_k, $white_castle_q, $black_castle_k, $black_castle_q) = @_;
172
173         # Strip check or mate
174         $move =~ s/[+#]$//;
175
176         if ($move eq '0-0' or $move eq 'O-O') {
177                 if ($toplay eq 'W') {
178                         if ($chess960) {
179                                 # King takes rook.
180                                 return (7, _find_piece_col($board->[7], 'K'), _square_to_pos($white_castle_k . '1'));
181                         } else {
182                                 return (_square_to_pos('e1'), _square_to_pos('g1'));
183                         }
184                 } else {
185                         if ($chess960) {
186                                 # King takes rook.
187                                 return (0, _find_piece_col($board->[0], 'k'), _square_to_pos($black_castle_k . '8'));
188                         } else {
189                                 return (_square_to_pos('e8'), _square_to_pos('g8'));
190                         }
191                 }
192         } elsif ($move eq '0-0-0' or $move eq 'O-O-O') {
193                 if ($toplay eq 'W') {
194                         if ($chess960) {
195                                 # King takes rook.
196                                 return (7, _find_piece_col($board->[7], 'K'), _square_to_pos($white_castle_q . '1'));
197                         } else {
198                                 return (_square_to_pos('e1'), _square_to_pos('c1'));
199                         }
200                 } else {
201                         if ($chess960) {
202                                 # King takes rook.
203                                 return (0, _find_piece_col($board->[0], 'k'), _square_to_pos($black_castle_q . '8'));
204                         } else {
205                                 return (_square_to_pos('e8'), _square_to_pos('c8'));
206                         }
207                 }
208         }
209
210         # Parse promo
211         my $promo;
212         if ($move =~ s/=?([QRNB])$//) {
213                 $promo = $1;
214         }
215
216         $move =~ /^([KQRBN])?([a-h])?([1-8])?x?([a-h][1-8])$/ or die "Invalid move $move";
217         my $piece = $1;
218         my $from_col = defined($2) ? _col_letter_to_num($2) : undef;
219         my $from_row = defined($3) ? _row_letter_to_num($3) : undef;
220         if (!defined($piece) && (!defined($from_col) || !defined($from_row))) {
221                 $piece = 'P';
222         }
223         my ($to_row, $to_col) = _square_to_pos($4);
224         
225         # Find all possible from-squares that could have been meant.
226         my @squares = ();
227         my $side = 'K';
228         if ($toplay eq 'B') {
229                 $piece = lc($piece) if defined($piece);
230                 $side = 'k';
231         }
232         for my $row (0..7) {
233                 next if (defined($from_row) && $from_row != $row);
234                 for my $col (0..7) {
235                         next if (defined($from_col) && $from_col != $col);
236                         next if (defined($piece) && $board->[$row][$col] ne $piece);
237                         push @squares, [ $row, $col ];
238                 }
239         }
240         if (scalar @squares > 1) {
241                 # Filter out pieces which cannot reach this square.
242                 @squares = grep { $board->can_reach($piece, $_->[0], $_->[1], $to_row, $to_col) } @squares;
243         }
244         if (scalar @squares > 1) {
245                 # See if doing this move would put us in check
246                 # (yes, there are clients that expect us to do this).
247                 @squares = grep { !$board->make_move($_->[0], $_->[1], $to_row, $to_col, $promo)->in_check($side) } @squares;
248         }
249         if (scalar @squares == 0) {
250                 die "Impossible move $move";
251         }
252         if (scalar @squares != 1) {
253                 die "Ambigious move $move";
254         }
255         return (@{$squares[0]}, $to_row, $to_col, $promo);
256 }
257
258 sub fen {
259         my ($board) = @_;
260         my @rows = ();
261         for my $row (0..7) {
262                 my $str = join('', @{$board->[$row]});
263                 $str =~ s/(-+)/length($1)/ge;
264                 push @rows, $str;
265         }
266
267         return join('/', @rows);
268 }
269
270 sub can_reach {
271         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
272         
273         # Can't eat your own piece (Chess960 uses king-takes-rook for castling,
274         # but castling is irrelevant for reachability)
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         if ($piece eq 'K') {
515                 # white short/long castling
516                 return 'O-O' if ($move eq 'e1g1');
517                 return 'O-O-O' if ($move eq 'e1c1');
518
519                 # white 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         } elsif ($piece eq 'k') {
525                 # black short/long castling
526                 return 'O-O' if ($move eq 'e8g8');
527                 return 'O-O-O' if ($move eq 'e8c8');
528
529                 # black short/long chess960-style castling (king takes own rook)
530                 my $dst_piece = $board->[$to_row][$to_col];
531                 if ($dst_piece eq 'r') {
532                         return ($to_col > $from_col) ? 'O-O' : 'O-O-O';
533                 }
534         }
535
536         my $pretty;
537
538         # check if the from-piece is a pawn
539         if (lc($piece) eq 'p') {
540                 # attack?
541                 if ($from_col != $to_col) {
542                         $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col);
543                 } else {
544                         $pretty = _pos_to_square($to_row, $to_col);
545                 }
546
547                 if (defined($promo) && $promo ne '') {
548                         # promotion
549                         $pretty .= "=";
550                         $pretty .= uc($promo);
551                 }
552                 return $pretty;
553         }
554
555         $pretty = uc($piece);
556
557         # see how many of these pieces could go here, in all
558         my $num_total = 0;
559         for my $col (0..7) {
560                 for my $row (0..7) {
561                         next unless ($board->[$row][$col] eq $piece);
562                         ++$num_total if ($board->can_legally_reach($piece, $row, $col, $to_row, $to_col));
563                 }
564         }
565
566         # see how many of these pieces from the given row could go here
567         my $num_row = 0;
568         for my $col (0..7) {
569                 next unless ($board->[$from_row][$col] eq $piece);
570                 ++$num_row if ($board->can_legally_reach($piece, $from_row, $col, $to_row, $to_col));
571         }
572
573         # and same for columns
574         my $num_col = 0;
575         for my $row (0..7) {
576                 next unless ($board->[$row][$from_col] eq $piece);
577                 ++$num_col if ($board->can_legally_reach($piece, $row, $from_col, $to_row, $to_col));
578         }
579
580         # see if we need to disambiguate
581         if ($num_total > 1) {
582                 if ($num_col == 1) {
583                         $pretty .= substr($move, 0, 1);
584                 } elsif ($num_row == 1) {
585                         $pretty .= substr($move, 1, 1);
586                 } else {
587                         $pretty .= substr($move, 0, 2);
588                 }
589         }
590
591         # attack?
592         if ($board->[$to_row][$to_col] ne '-') {
593                 $pretty .= 'x';
594         }
595
596         $pretty .= _pos_to_square($to_row, $to_col);
597         return $pretty;
598 }
599
600 1;