]> git.sesse.net Git - remoteglot/blob - Board.pm
When parsing moves, use illegal moves to disambiguate.
[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
198                         # See if doing this move would put us in check
199                         # (yes, there are clients that expect us to do this).
200                         my $check = $board->make_move($row, $col, $to_row, $to_col, $promo)->in_check();
201                         next if ($check eq 'both' ||
202                                  ($toplay eq 'W' && $check eq 'white') ||
203                                  ($toplay eq 'B' && $check eq 'black'));
204
205                         push @squares, [ $row, $col ];
206                 }
207         }
208         if (scalar @squares != 1) {
209                 die "Ambigious or impossible move $move";
210         }
211         return (@{$squares[0]}, $to_row, $to_col, $promo);
212 }
213
214 sub fen {
215         my ($board) = @_;
216         my @rows = ();
217         for my $row (0..7) {
218                 my $str = join('', @{$board->[$row]});
219                 $str =~ s/(-+)/length($1)/ge;
220                 push @rows, $str;
221         }
222
223         return join('/', @rows);
224 }
225
226 sub can_reach {
227         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
228         
229         # can't eat your own piece
230         my $dest_piece = $board->[$to_row][$to_col];
231         if ($dest_piece ne '-') {
232                 return 0 if (($piece eq lc($piece)) == ($dest_piece eq lc($dest_piece)));
233         }
234
235         if (lc($piece) eq 'k') {
236                 return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1);
237         }
238         if (lc($piece) eq 'r') {
239                 return 0 unless ($from_row == $to_row || $from_col == $to_col);
240
241                 # check that there's a clear passage
242                 if ($from_row == $to_row) {
243                         if ($from_col > $to_col) {
244                                 ($to_col, $from_col) = ($from_col, $to_col);
245                         }
246
247                         for my $c (($from_col+1)..($to_col-1)) {
248                                 my $middle_piece = $board->[$to_row][$c];
249                                 return 0 if ($middle_piece ne '-');
250                         }
251
252                         return 1;
253                 } else {
254                         if ($from_row > $to_row) {
255                                 ($to_row, $from_row) = ($from_row, $to_row);
256                         }
257
258                         for my $r (($from_row+1)..($to_row-1)) {
259                                 my $middle_piece = $board->[$r][$to_col];
260                                 return 0 if ($middle_piece ne '-');     
261                         }
262
263                         return 1;
264                 }
265         }
266         if (lc($piece) eq 'b') {
267                 return 0 unless (abs($from_row - $to_row) == abs($from_col - $to_col));
268
269                 my $dr = ($to_row - $from_row) / abs($to_row - $from_row);
270                 my $dc = ($to_col - $from_col) / abs($to_col - $from_col);
271
272                 my $r = $from_row + $dr;
273                 my $c = $from_col + $dc;
274
275                 while ($r != $to_row) {
276                         my $middle_piece = $board->[$r][$c];
277                         return 0 if ($middle_piece ne '-');
278                         
279                         $r += $dr;
280                         $c += $dc;
281                 }
282
283                 return 1;
284         }
285         if (lc($piece) eq 'n') {
286                 my $diff_r = abs($from_row - $to_row);
287                 my $diff_c = abs($from_col - $to_col);
288                 return 1 if ($diff_r == 2 && $diff_c == 1);
289                 return 1 if ($diff_r == 1 && $diff_c == 2);
290                 return 0;
291         }
292         if ($piece eq 'q') {
293                 return (can_reach($board, 'r', $from_row, $from_col, $to_row, $to_col) ||
294                         can_reach($board, 'b', $from_row, $from_col, $to_row, $to_col));
295         }
296         if ($piece eq 'Q') {
297                 return (can_reach($board, 'R', $from_row, $from_col, $to_row, $to_col) ||
298                         can_reach($board, 'B', $from_row, $from_col, $to_row, $to_col));
299         }
300
301         # TODO: en passant
302         if ($piece eq 'p') {
303                 # black pawn
304                 if ($to_col == $from_col && $to_row == $from_row + 1) {
305                         return ($dest_piece eq '-');
306                 }
307                 if ($to_col == $from_col && $from_row == 1 && $to_row == 3) {
308                         my $middle_piece = $board->[2][$to_col];
309                         return ($dest_piece eq '-' && $middle_piece eq '-');
310                 }
311                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) {
312                         return ($dest_piece ne '-');
313                 }
314                 return 0;
315         }
316         if ($piece eq 'P') {
317                 # white pawn
318                 if ($to_col == $from_col && $to_row == $from_row - 1) {
319                         return ($dest_piece eq '-');
320                 }
321                 if ($to_col == $from_col && $from_row == 6 && $to_row == 4) {
322                         my $middle_piece = $board->[5][$to_col];
323                         return ($dest_piece eq '-' && $middle_piece eq '-');
324                 }
325                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) {
326                         return ($dest_piece ne '-');
327                 }
328                 return 0;
329         }
330         
331         # unknown piece
332         return 0;
333 }
334
335 # Returns 'none', 'white', 'black' or 'both', depending on which sides are in check.
336 # The latter naturally indicates an invalid position.
337 sub in_check {
338         my $board = shift;
339         my ($black_check, $white_check) = (0, 0);
340
341         my ($wkr, $wkc, $bkr, $bkc) = _find_kings($board);
342
343         # check all pieces for the possibility of threatening the two kings
344         for my $row (0..7) {
345                 for my $col (0..7) {
346                         my $piece = $board->[$row][$col];
347                         next if ($piece eq '-');
348                 
349                         if (uc($piece) eq $piece) {
350                                 # white piece
351                                 $black_check = 1 if ($board->can_reach($piece, $row, $col, $bkr, $bkc));
352                         } else {
353                                 # black piece
354                                 $white_check = 1 if ($board->can_reach($piece, $row, $col, $wkr, $wkc));
355                         }
356                 }
357         }
358
359         if ($black_check && $white_check) {
360                 return 'both';
361         } elsif ($black_check) {
362                 return 'black';
363         } elsif ($white_check) {
364                 return 'white';
365         } else {
366                 return 'none';
367         }
368 }
369
370 sub _find_kings {
371         my $board = shift;
372         my ($wkr, $wkc, $bkr, $bkc);
373
374         for my $row (0..7) {
375                 for my $col (0..7) {
376                         my $piece = $board->[$row][$col];
377                         if ($piece eq 'K') {
378                                 ($wkr, $wkc) = ($row, $col);
379                         } elsif ($piece eq 'k') {
380                                 ($bkr, $bkc) = ($row, $col);
381                         }
382                 }
383         }
384
385         return ($wkr, $wkc, $bkr, $bkc);
386 }
387
388 # Returns if any side is in mate.
389 sub in_mate {
390         my $board = shift;
391         my $check = $board->in_check();
392         return 0 if ($check eq 'none');
393
394         # try all possible moves for the side in check
395         for my $row (0..7) {
396                 for my $col (0..7) {
397                         my $piece = $board->[$row][$col];
398                         next if ($piece eq '-');
399
400                         if ($check eq 'white') {
401                                 next if ($piece eq lc($piece));
402                         } else {
403                                 next if ($piece eq uc($piece));
404                         }
405
406                         for my $dest_row (0..7) {
407                                 for my $dest_col (0..7) {
408                                         next if ($row == $dest_row && $col == $dest_col);
409                                         next unless ($board->can_reach($piece, $row, $col, $dest_row, $dest_col));
410
411                                         my $nb = $board->clone();
412                                         $nb->[$row][$col] = '-';
413                                         $nb->[$dest_row][$dest_col] = $piece;
414                                         my $new_check = $nb->in_check();
415                                         return 0 if ($new_check ne $check && $new_check ne 'both');
416                                 }
417                         }
418                 }
419         }
420
421         # nothing to do; mate
422         return 1;
423 }
424
425 # Returns the short algebraic form of the move, as well as the new position.
426 sub prettyprint_move {
427         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
428         my $pretty = $board->_prettyprint_move_no_check_or_mate($from_row, $from_col, $to_row, $to_col, $promo);
429
430         my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo);
431         if ($nb->in_mate()) {
432                 $pretty .= '#';
433         } elsif ($nb->in_check() ne 'none') {
434                 $pretty .= '+';
435         }
436         return ($pretty, $nb);
437 }
438
439 sub _prettyprint_move_no_check_or_mate {
440         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
441         my $piece = $board->[$from_row][$from_col];
442         my $move = _move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
443
444         if ($piece eq '-') {
445                 die "Invalid move $move";
446         }
447
448         # white short castling
449         if ($move eq 'e1g1' && $piece eq 'K') {
450                 return '0-0';
451         }
452
453         # white long castling
454         if ($move eq 'e1c1' && $piece eq 'K') {
455                 return '0-0-0';
456         }
457
458         # black short castling
459         if ($move eq 'e8g8' && $piece eq 'k') {
460                 return '0-0';
461         }
462
463         # black long castling
464         if ($move eq 'e8c8' && $piece eq 'k') {
465                 return '0-0-0';
466         }
467
468         my $pretty;
469
470         # check if the from-piece is a pawn
471         if (lc($piece) eq 'p') {
472                 # attack?
473                 if ($from_col != $to_col) {
474                         $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col);
475                 } else {
476                         $pretty = _pos_to_square($to_row, $to_col);
477
478                         if (defined($promo) && $promo ne '') {
479                                 # promotion
480                                 $pretty .= "=";
481                                 $pretty .= $promo;
482                         }
483                 }
484                 return $pretty;
485         }
486
487         $pretty = uc($piece);
488
489         # see how many of these pieces could go here, in all
490         my $num_total = 0;
491         for my $col (0..7) {
492                 for my $row (0..7) {
493                         next unless ($board->[$row][$col] eq $piece);
494                         ++$num_total if ($board->can_reach($piece, $row, $col, $to_row, $to_col));
495                 }
496         }
497
498         # see how many of these pieces from the given row could go here
499         my $num_row = 0;
500         for my $col (0..7) {
501                 next unless ($board->[$from_row][$col] eq $piece);
502                 ++$num_row if ($board->can_reach($piece, $from_row, $col, $to_row, $to_col));
503         }
504
505         # and same for columns
506         my $num_col = 0;
507         for my $row (0..7) {
508                 next unless ($board->[$row][$from_col] eq $piece);
509                 ++$num_col if ($board->can_reach($piece, $row, $from_col, $to_row, $to_col));
510         }
511
512         # see if we need to disambiguate
513         if ($num_total > 1) {
514                 if ($num_col == 1) {
515                         $pretty .= substr($move, 0, 1);
516                 } elsif ($num_row == 1) {
517                         $pretty .= substr($move, 1, 1);
518                 } else {
519                         $pretty .= substr($move, 0, 2);
520                 }
521         }
522
523         # attack?
524         if ($board->[$to_row][$to_col] ne '-') {
525                 $pretty .= 'x';
526         }
527
528         $pretty .= _pos_to_square($to_row, $to_col);
529         return $pretty;
530 }
531
532 1;