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