]> git.sesse.net Git - remoteglot/blob - Board.pm
Split some UCI stuff into a separate module.
[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 ($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 _move_to_uci_notation {
135         my ($from_row, $from_col, $to_row, $to_col, $promo) = @_;
136         $promo //= "";
137         return _pos_to_square($from_row, $from_col) . _pos_to_square($to_row, $to_col) . $promo;
138 }
139
140 sub fen {
141         my ($board) = @_;
142         my @rows = ();
143         for my $row (0..7) {
144                 my $str = join('', @{$board->[$row]});
145                 $str =~ s/(-+)/length($1)/ge;
146                 push @rows, $str;
147         }
148
149         return join('/', @rows);
150 }
151
152 sub can_reach {
153         my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_;
154         
155         # can't eat your own piece
156         my $dest_piece = $board->[$to_row][$to_col];
157         if ($dest_piece ne '-') {
158                 return 0 if (($piece eq lc($piece)) == ($dest_piece eq lc($dest_piece)));
159         }
160
161         if (lc($piece) eq 'k') {
162                 return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1);
163         }
164         if (lc($piece) eq 'r') {
165                 return 0 unless ($from_row == $to_row || $from_col == $to_col);
166
167                 # check that there's a clear passage
168                 if ($from_row == $to_row) {
169                         if ($from_col > $to_col) {
170                                 ($to_col, $from_col) = ($from_col, $to_col);
171                         }
172
173                         for my $c (($from_col+1)..($to_col-1)) {
174                                 my $middle_piece = $board->[$to_row][$c];
175                                 return 0 if ($middle_piece ne '-');
176                         }
177
178                         return 1;
179                 } else {
180                         if ($from_row > $to_row) {
181                                 ($to_row, $from_row) = ($from_row, $to_row);
182                         }
183
184                         for my $r (($from_row+1)..($to_row-1)) {
185                                 my $middle_piece = $board->[$r][$to_col];
186                                 return 0 if ($middle_piece ne '-');     
187                         }
188
189                         return 1;
190                 }
191         }
192         if (lc($piece) eq 'b') {
193                 return 0 unless (abs($from_row - $to_row) == abs($from_col - $to_col));
194
195                 my $dr = ($to_row - $from_row) / abs($to_row - $from_row);
196                 my $dc = ($to_col - $from_col) / abs($to_col - $from_col);
197
198                 my $r = $from_row + $dr;
199                 my $c = $from_col + $dc;
200
201                 while ($r != $to_row) {
202                         my $middle_piece = $board->[$r][$c];
203                         return 0 if ($middle_piece ne '-');
204                         
205                         $r += $dr;
206                         $c += $dc;
207                 }
208
209                 return 1;
210         }
211         if (lc($piece) eq 'n') {
212                 my $diff_r = abs($from_row - $to_row);
213                 my $diff_c = abs($from_col - $to_col);
214                 return 1 if ($diff_r == 2 && $diff_c == 1);
215                 return 1 if ($diff_r == 1 && $diff_c == 2);
216                 return 0;
217         }
218         if ($piece eq 'q') {
219                 return (can_reach($board, 'r', $from_row, $from_col, $to_row, $to_col) ||
220                         can_reach($board, 'b', $from_row, $from_col, $to_row, $to_col));
221         }
222         if ($piece eq 'Q') {
223                 return (can_reach($board, 'R', $from_row, $from_col, $to_row, $to_col) ||
224                         can_reach($board, 'B', $from_row, $from_col, $to_row, $to_col));
225         }
226
227         # TODO: en passant
228         if ($piece eq 'p') {
229                 # black pawn
230                 if ($to_col == $from_col && $to_row == $from_row + 1) {
231                         return ($dest_piece eq '-');
232                 }
233                 if ($to_col == $from_col && $from_row == 1 && $to_row == 3) {
234                         my $middle_piece = $board->[2][$to_col];
235                         return ($dest_piece eq '-' && $middle_piece eq '-');
236                 }
237                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) {
238                         return ($dest_piece ne '-');
239                 }
240                 return 0;
241         }
242         if ($piece eq 'P') {
243                 # white pawn
244                 if ($to_col == $from_col && $to_row == $from_row - 1) {
245                         return ($dest_piece eq '-');
246                 }
247                 if ($to_col == $from_col && $from_row == 6 && $to_row == 4) {
248                         my $middle_piece = $board->[5][$to_col];
249                         return ($dest_piece eq '-' && $middle_piece eq '-');
250                 }
251                 if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) {
252                         return ($dest_piece ne '-');
253                 }
254                 return 0;
255         }
256         
257         # unknown piece
258         return 0;
259 }
260
261 # Returns 'none', 'white', 'black' or 'both', depending on which sides are in check.
262 # The latter naturally indicates an invalid position.
263 sub in_check {
264         my $board = shift;
265         my ($black_check, $white_check) = (0, 0);
266
267         my ($wkr, $wkc, $bkr, $bkc) = _find_kings($board);
268
269         # check all pieces for the possibility of threatening the two kings
270         for my $row (0..7) {
271                 for my $col (0..7) {
272                         my $piece = $board->[$row][$col];
273                         next if ($piece eq '-');
274                 
275                         if (uc($piece) eq $piece) {
276                                 # white piece
277                                 $black_check = 1 if ($board->can_reach($piece, $row, $col, $bkr, $bkc));
278                         } else {
279                                 # black piece
280                                 $white_check = 1 if ($board->can_reach($piece, $row, $col, $wkr, $wkc));
281                         }
282                 }
283         }
284
285         if ($black_check && $white_check) {
286                 return 'both';
287         } elsif ($black_check) {
288                 return 'black';
289         } elsif ($white_check) {
290                 return 'white';
291         } else {
292                 return 'none';
293         }
294 }
295
296 sub _find_kings {
297         my $board = shift;
298         my ($wkr, $wkc, $bkr, $bkc);
299
300         for my $row (0..7) {
301                 for my $col (0..7) {
302                         my $piece = $board->[$row][$col];
303                         if ($piece eq 'K') {
304                                 ($wkr, $wkc) = ($row, $col);
305                         } elsif ($piece eq 'k') {
306                                 ($bkr, $bkc) = ($row, $col);
307                         }
308                 }
309         }
310
311         return ($wkr, $wkc, $bkr, $bkc);
312 }
313
314 # Returns if any side is in mate.
315 sub in_mate {
316         my $board = shift;
317         my $check = $board->in_check();
318         return 0 if ($check eq 'none');
319
320         # try all possible moves for the side in check
321         for my $row (0..7) {
322                 for my $col (0..7) {
323                         my $piece = $board->[$row][$col];
324                         next if ($piece eq '-');
325
326                         if ($check eq 'white') {
327                                 next if ($piece eq lc($piece));
328                         } else {
329                                 next if ($piece eq uc($piece));
330                         }
331
332                         for my $dest_row (0..7) {
333                                 for my $dest_col (0..7) {
334                                         next if ($row == $dest_row && $col == $dest_col);
335                                         next unless ($board->can_reach($piece, $row, $col, $dest_row, $dest_col));
336
337                                         my $nb = $board->clone();
338                                         $nb->[$row][$col] = '-';
339                                         $nb->[$dest_row][$dest_col] = $piece;
340                                         my $new_check = $nb->in_check();
341                                         return 0 if ($new_check ne $check && $new_check ne 'both');
342                                 }
343                         }
344                 }
345         }
346
347         # nothing to do; mate
348         return 1;
349 }
350
351 # Returns the short algebraic form of the move, as well as the new position.
352 sub prettyprint_move {
353         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
354         my $pretty = $board->_prettyprint_move_no_check_or_mate($from_row, $from_col, $to_row, $to_col, $promo);
355
356         my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo);
357         if ($nb->in_mate()) {
358                 $pretty .= '#';
359         } elsif ($nb->in_check() ne 'none') {
360                 $pretty .= '+';
361         }
362         return ($pretty, $nb);
363 }
364
365 sub _prettyprint_move_no_check_or_mate {
366         my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_;
367         my $piece = $board->[$from_row][$from_col];
368         my $move = _move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo);
369
370         if ($piece eq '-') {
371                 die "Invalid move $move";
372         }
373
374         # white short castling
375         if ($move eq 'e1g1' && $piece eq 'K') {
376                 return '0-0';
377         }
378
379         # white long castling
380         if ($move eq 'e1c1' && $piece eq 'K') {
381                 return '0-0-0';
382         }
383
384         # black short castling
385         if ($move eq 'e8g8' && $piece eq 'k') {
386                 return '0-0';
387         }
388
389         # black long castling
390         if ($move eq 'e8c8' && $piece eq 'k') {
391                 return '0-0-0';
392         }
393
394         my $pretty;
395
396         # check if the from-piece is a pawn
397         if (lc($piece) eq 'p') {
398                 # attack?
399                 if ($from_col != $to_col) {
400                         $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col);
401                 } else {
402                         $pretty = _pos_to_square($to_row, $to_col);
403
404                         if (defined($promo) && $promo ne '') {
405                                 # promotion
406                                 $pretty .= "=";
407                                 $pretty .= $promo;
408                         }
409                 }
410                 return $pretty;
411         }
412
413         $pretty = uc($piece);
414
415         # see how many of these pieces could go here, in all
416         my $num_total = 0;
417         for my $col (0..7) {
418                 for my $row (0..7) {
419                         next unless ($board->[$row][$col] eq $piece);
420                         ++$num_total if ($board->can_reach($piece, $row, $col, $to_row, $to_col));
421                 }
422         }
423
424         # see how many of these pieces from the given row could go here
425         my $num_row = 0;
426         for my $col (0..7) {
427                 next unless ($board->[$from_row][$col] eq $piece);
428                 ++$num_row if ($board->can_reach($piece, $from_row, $col, $to_row, $to_col));
429         }
430
431         # and same for columns
432         my $num_col = 0;
433         for my $row (0..7) {
434                 next unless ($board->[$row][$from_col] eq $piece);
435                 ++$num_col if ($board->can_reach($piece, $row, $from_col, $to_row, $to_col));
436         }
437
438         # see if we need to disambiguate
439         if ($num_total > 1) {
440                 if ($num_col == 1) {
441                         $pretty .= substr($move, 0, 1);
442                 } elsif ($num_row == 1) {
443                         $pretty .= substr($move, 1, 1);
444                 } else {
445                         $pretty .= substr($move, 0, 2);
446                 }
447         }
448
449         # attack?
450         if ($board->[$to_row][$to_col] ne '-') {
451                 $pretty .= 'x';
452         }
453
454         $pretty .= _pos_to_square($to_row, $to_col);
455         return $pretty;
456 }
457
458 1;