]> git.sesse.net Git - remoteglot/blob - Board.pm
e6c04a30fef78a63a413efcebfd348efe586c1ce
[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         if ($piece eq 'p') {
302                 # black pawn
303                 if ($to_col == $from_col && $to_row == $from_row + 1) {
304                         return ($dest_piece eq '-');
305                 }
306                 if ($to_col == $from_col && $from_row == 1 && $to_row == 3) {
307                         my $middle_piece = $board->[2][$to_col];
308                         return ($dest_piece eq '-' && $middle_piece eq '-');
309                 }
310                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) {
311                         if ($dest_piece eq '-') {
312                                 # En passant. TODO: check that the last move was indeed an EP move
313                                 return ($to_row == 5 && $board->[4][$to_col] eq 'P');
314                         } else {
315                                 return 1;
316                         }
317                 }
318                 return 0;
319         }
320         if ($piece eq 'P') {
321                 # white pawn
322                 if ($to_col == $from_col && $to_row == $from_row - 1) {
323                         return ($dest_piece eq '-');
324                 }
325                 if ($to_col == $from_col && $from_row == 6 && $to_row == 4) {
326                         my $middle_piece = $board->[5][$to_col];
327                         return ($dest_piece eq '-' && $middle_piece eq '-');
328                 }
329                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) {
330                         if ($dest_piece eq '-') {
331                                 # En passant. TODO: check that the last move was indeed an EP move
332                                 return ($to_row == 2 && $board->[3][$to_col] eq 'p');
333                         } else {
334                                 return 1;
335                         }
336                 }
337                 return 0;
338         }
339         
340         # unknown piece
341         return 0;
342 }
343
344 # Returns 'none', 'white', 'black' or 'both', depending on which sides are in check.
345 # The latter naturally indicates an invalid position.
346 sub in_check {
347         my $board = shift;
348         my ($black_check, $white_check) = (0, 0);
349
350         my ($wkr, $wkc, $bkr, $bkc) = _find_kings($board);
351
352         # check all pieces for the possibility of threatening the two kings
353         for my $row (0..7) {
354                 for my $col (0..7) {
355                         my $piece = $board->[$row][$col];
356                         next if ($piece eq '-');
357                 
358                         if (uc($piece) eq $piece) {
359                                 # white piece
360                                 $black_check = 1 if ($board->can_reach($piece, $row, $col, $bkr, $bkc));
361                         } else {
362                                 # black piece
363                                 $white_check = 1 if ($board->can_reach($piece, $row, $col, $wkr, $wkc));
364                         }
365                 }
366         }
367
368         if ($black_check && $white_check) {
369                 return 'both';
370         } elsif ($black_check) {
371                 return 'black';
372         } elsif ($white_check) {
373                 return 'white';
374         } else {
375                 return 'none';
376         }
377 }
378
379 sub _find_kings {
380         my $board = shift;
381         my ($wkr, $wkc, $bkr, $bkc);
382
383         for my $row (0..7) {
384                 for my $col (0..7) {
385                         my $piece = $board->[$row][$col];
386                         if ($piece eq 'K') {
387                                 ($wkr, $wkc) = ($row, $col);
388                         } elsif ($piece eq 'k') {
389                                 ($bkr, $bkc) = ($row, $col);
390                         }
391                 }
392         }
393
394         return ($wkr, $wkc, $bkr, $bkc);
395 }
396
397 # Returns if any side is in mate.
398 sub in_mate {
399         my $board = shift;
400         my $check = $board->in_check();
401         return 0 if ($check eq 'none');
402
403         # try all possible moves for the side in check
404         for my $row (0..7) {
405                 for my $col (0..7) {
406                         my $piece = $board->[$row][$col];
407                         next if ($piece eq '-');
408
409                         if ($check eq 'white') {
410                                 next if ($piece eq lc($piece));
411                         } else {
412                                 next if ($piece eq uc($piece));
413                         }
414
415                         for my $dest_row (0..7) {
416                                 for my $dest_col (0..7) {
417                                         next if ($row == $dest_row && $col == $dest_col);
418                                         next unless ($board->can_reach($piece, $row, $col, $dest_row, $dest_col));
419
420                                         my $nb = $board->clone();
421                                         $nb->[$row][$col] = '-';
422                                         $nb->[$dest_row][$dest_col] = $piece;
423                                         my $new_check = $nb->in_check();
424                                         return 0 if ($new_check ne $check && $new_check ne 'both');
425                                 }
426                         }
427                 }
428         }
429
430         # nothing to do; mate
431         return 1;
432 }
433
434 # Returns the short algebraic form of the move, as well as the new position.
435 sub prettyprint_move {
436         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
437         my $pretty = $board->_prettyprint_move_no_check_or_mate($from_row, $from_col, $to_row, $to_col, $promo);
438
439         my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo);
440         if ($nb->in_mate()) {
441                 $pretty .= '#';
442         } elsif ($nb->in_check() ne 'none') {
443                 $pretty .= '+';
444         }
445         return ($pretty, $nb);
446 }
447
448 sub _prettyprint_move_no_check_or_mate {
449         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
450         my $piece = $board->[$from_row][$from_col];
451         my $move = _move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
452
453         if ($piece eq '-') {
454                 die "Invalid move $move";
455         }
456
457         # white short castling
458         if ($move eq 'e1g1' && $piece eq 'K') {
459                 return '0-0';
460         }
461
462         # white long castling
463         if ($move eq 'e1c1' && $piece eq 'K') {
464                 return '0-0-0';
465         }
466
467         # black short castling
468         if ($move eq 'e8g8' && $piece eq 'k') {
469                 return '0-0';
470         }
471
472         # black long castling
473         if ($move eq 'e8c8' && $piece eq 'k') {
474                 return '0-0-0';
475         }
476
477         my $pretty;
478
479         # check if the from-piece is a pawn
480         if (lc($piece) eq 'p') {
481                 # attack?
482                 if ($from_col != $to_col) {
483                         $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col);
484                 } else {
485                         $pretty = _pos_to_square($to_row, $to_col);
486
487                         if (defined($promo) && $promo ne '') {
488                                 # promotion
489                                 $pretty .= "=";
490                                 $pretty .= $promo;
491                         }
492                 }
493                 return $pretty;
494         }
495
496         $pretty = uc($piece);
497
498         # see how many of these pieces could go here, in all
499         my $num_total = 0;
500         for my $col (0..7) {
501                 for my $row (0..7) {
502                         next unless ($board->[$row][$col] eq $piece);
503                         ++$num_total if ($board->can_reach($piece, $row, $col, $to_row, $to_col));
504                 }
505         }
506
507         # see how many of these pieces from the given row could go here
508         my $num_row = 0;
509         for my $col (0..7) {
510                 next unless ($board->[$from_row][$col] eq $piece);
511                 ++$num_row if ($board->can_reach($piece, $from_row, $col, $to_row, $to_col));
512         }
513
514         # and same for columns
515         my $num_col = 0;
516         for my $row (0..7) {
517                 next unless ($board->[$row][$from_col] eq $piece);
518                 ++$num_col if ($board->can_reach($piece, $row, $from_col, $to_row, $to_col));
519         }
520
521         # see if we need to disambiguate
522         if ($num_total > 1) {
523                 if ($num_col == 1) {
524                         $pretty .= substr($move, 0, 1);
525                 } elsif ($num_row == 1) {
526                         $pretty .= substr($move, 1, 1);
527                 } else {
528                         $pretty .= substr($move, 0, 2);
529                 }
530         }
531
532         # attack?
533         if ($board->[$to_row][$to_col] ne '-') {
534                 $pretty .= 'x';
535         }
536
537         $pretty .= _pos_to_square($to_row, $to_col);
538         return $pretty;
539 }
540
541 1;