X-Git-Url: https://git.sesse.net/?p=remoteglot;a=blobdiff_plain;f=Board.pm;h=6fb413ac906ed5670930f3d62750f8519260207f;hp=6af89f39819d16e520d4ff753f0b3db161b901e2;hb=fae57f5f7ea4fee8a25c506b6c04f67c49833350;hpb=762b4cb898d3300233b773eff370a1562a445efe diff --git a/Board.pm b/Board.pm index 6af89f3..6fb413a 100644 --- a/Board.pm +++ b/Board.pm @@ -25,9 +25,7 @@ sub clone { my $nb = []; for my $row (0..7) { - for my $col (0..7) { - $nb->[$row][$col] = $board->[$row][$col]; - } + $nb->[$row] = [ @{$board->[$row]} ]; } return bless $nb; @@ -36,7 +34,7 @@ sub clone { # Returns a new board. sub make_move { my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_; - my $move = _move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo); + my $move = move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo); my $piece = $board->[$from_row][$from_col]; my $nb = $board->clone(); @@ -44,56 +42,60 @@ sub make_move { die "Invalid move $move"; } - # white short castling - if ($move eq 'e1g1' && $piece eq 'K') { - # king - $nb->[7][4] = '-'; - $nb->[7][6] = $piece; + if ($piece eq 'K') { + # Convert to Chess960 king-takes-rook. + $to_col = 7 if ($move eq 'e1g1' && $board->[$to_row][$to_col] ne 'R'); + $to_col = 0 if ($move eq 'e1c1' && $board->[$to_row][$to_col] ne 'R'); - # rook - $nb->[7][7] = '-'; - $nb->[7][5] = 'R'; + my $dst_piece = $board->[$to_row][$to_col]; - return $nb; - } + # White short castling. + if ($dst_piece eq 'R' && $to_col > $from_col) { + # king + $nb->[7][$from_col] = '-'; + $nb->[7][$to_col] = '-'; + $nb->[7][6] = 'K'; + $nb->[7][5] = 'R'; - # white long castling - if ($move eq 'e1c1' && $piece eq 'K') { - # king - $nb->[7][4] = '-'; - $nb->[7][2] = $piece; - - # rook - $nb->[7][0] = '-'; - $nb->[7][3] = 'R'; + return $nb; + } - return $nb; - } + # Black short castling. + if ($dst_piece eq 'R' && $to_col < $from_col) { + $nb->[7][$from_col] = '-'; + $nb->[7][$to_col] = '-'; + $nb->[7][2] = 'K'; + $nb->[7][3] = 'R'; - # black short castling - if ($move eq 'e8g8' && $piece eq 'k') { - # king - $nb->[0][4] = '-'; - $nb->[0][6] = $piece; + return $nb; + } + } elsif ($piece eq 'k') { + # Convert to Chess960 king-takes-rook. + $to_col = 7 if ($move eq 'e8g8' && $board->[$to_row][$to_col] ne 'r'); + $to_col = 0 if ($move eq 'e8c8' && $board->[$to_row][$to_col] ne 'r'); - # rook - $nb->[0][7] = '-'; - $nb->[0][5] = 'r'; + my $dst_piece = $board->[$to_row][$to_col]; - return $nb; - } + # Black short castling. + if ($dst_piece eq 'r' && $to_col > $from_col) { + $nb->[0][$from_col] = '-'; + $nb->[0][$to_col] = '-'; + $nb->[0][6] = 'k'; + $nb->[0][5] = 'r'; - # black long castling - if ($move eq 'e8c8' && $piece eq 'k') { - # king - $nb->[0][4] = '-'; - $nb->[0][2] = $piece; + return $nb; + } - # rook - $nb->[0][0] = '-'; - $nb->[0][3] = 'r'; + # Black long castling. + if ($dst_piece eq 'r' && $to_col < $from_col) { + # king + $nb->[0][$from_col] = '-'; + $nb->[0][$to_col] = '-'; + $nb->[0][2] = 'k'; + $nb->[0][3] = 'r'; - return $nb; + return $nb; + } } # check if the from-piece is a pawn @@ -103,20 +105,19 @@ sub make_move { # en passant? if ($board->[$to_row][$to_col] eq '-') { if ($piece eq 'p') { - $nb->[$to_row + 1][$to_col] = '-'; - } else { $nb->[$to_row - 1][$to_col] = '-'; - } - } - } else { - if ($promo ne '') { - if ($piece eq 'p') { - $piece = $promo; } else { - $piece = uc($promo); + $nb->[$to_row + 1][$to_col] = '-'; } } } + if (defined($promo) && $promo ne '') { + if ($piece eq 'p') { + $piece = lc($promo); + } else { + $piece = uc($promo); + } + } } # update the board @@ -131,12 +132,127 @@ sub _pos_to_square { return sprintf("%c%d", ord('a') + $col, 8 - $row); } -sub _move_to_uci_notation { +sub _col_letter_to_num { + return ord(shift) - ord('a'); +} + +sub _row_letter_to_num { + return 7 - (ord(shift) - ord('1')); +} + +use Carp; + +sub _square_to_pos { + my ($square) = @_; + #$square =~ /^([a-h])([1-8])$/ or die "Invalid square $square"; + $square =~ /^([a-h])([1-8])$/ or Carp::confess("Invalid square $square"); + return (_row_letter_to_num($2), _col_letter_to_num($1)); +} + +sub move_to_uci_notation { my ($from_row, $from_col, $to_row, $to_col, $promo) = @_; $promo //= ""; return _pos_to_square($from_row, $from_col) . _pos_to_square($to_row, $to_col) . $promo; } +sub _find_piece_col { + my ($row, $piece) = @_; + for my $col (0..7) { + return $col if ($row->[$col] eq $piece); + } + die "Could not find piece $piece"; +} + +# Note: This is in general not a validation that the move is actually allowed +# (e.g. you can castle even though you're in check). +sub parse_pretty_move { + my ($board, $move, $toplay, $chess960, $white_castle_k, $white_castle_q, $black_castle_k, $black_castle_q) = @_; + + # Strip check or mate + $move =~ s/[+#]$//; + + if ($move eq '0-0' or $move eq 'O-O') { + if ($toplay eq 'W') { + if ($chess960) { + # King takes rook. + return (7, _find_piece_col($board->[7], 'K'), _square_to_pos($white_castle_k . '1')); + } else { + return (_square_to_pos('e1'), _square_to_pos('g1')); + } + } else { + if ($chess960) { + # King takes rook. + return (0, _find_piece_col($board->[0], 'k'), _square_to_pos($black_castle_k . '8')); + } else { + return (_square_to_pos('e8'), _square_to_pos('g8')); + } + } + } elsif ($move eq '0-0-0' or $move eq 'O-O-O') { + if ($toplay eq 'W') { + if ($chess960) { + # King takes rook. + return (7, _find_piece_col($board->[7], 'K'), _square_to_pos($white_castle_q . '1')); + } else { + return (_square_to_pos('e1'), _square_to_pos('c1')); + } + } else { + if ($chess960) { + # King takes rook. + return (0, _find_piece_col($board->[0], 'k'), _square_to_pos($black_castle_q . '8')); + } else { + return (_square_to_pos('e8'), _square_to_pos('c8')); + } + } + } + + # Parse promo + my $promo; + if ($move =~ s/=?([QRNB])$//) { + $promo = $1; + } + + $move =~ /^([KQRBN])?([a-h])?([1-8])?x?([a-h][1-8])$/ or die "Invalid move $move"; + my $piece = $1; + my $from_col = defined($2) ? _col_letter_to_num($2) : undef; + my $from_row = defined($3) ? _row_letter_to_num($3) : undef; + if (!defined($piece) && (!defined($from_col) || !defined($from_row))) { + $piece = 'P'; + } + my ($to_row, $to_col) = _square_to_pos($4); + + # Find all possible from-squares that could have been meant. + my @squares = (); + my $side = 'K'; + if ($toplay eq 'B') { + $piece = lc($piece) if defined($piece); + $side = 'k'; + } + for my $row (0..7) { + next if (defined($from_row) && $from_row != $row); + for my $col (0..7) { + next if (defined($from_col) && $from_col != $col); + next if (defined($piece) && $board->[$row][$col] ne $piece); + push @squares, [ $row, $col ]; + } + } + if (scalar @squares > 1) { + # Filter out pieces which cannot reach this square. + @squares = grep { $board->can_reach($piece, $_->[0], $_->[1], $to_row, $to_col) } @squares; + } + if (scalar @squares > 1) { + # See if doing this move would put us in check + # (yes, there are clients that expect us to do this). + @squares = grep { !$board->make_move($_->[0], $_->[1], $to_row, $to_col, $promo)->in_check($side) } @squares; + } + if (scalar @squares == 0) { + die "Impossible move $move"; + } + if (scalar @squares != 1) { + die "Ambigious move $move"; + } + return (@{$squares[0]}, $to_row, $to_col, $promo); +} + sub fen { my ($board) = @_; my @rows = (); @@ -152,15 +268,52 @@ sub fen { sub can_reach { my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_; - # can't eat your own piece + # Can't eat your own piece (Chess960 uses king-takes-rook for castling, + # but castling is irrelevant for reachability) my $dest_piece = $board->[$to_row][$to_col]; if ($dest_piece ne '-') { return 0 if (($piece eq lc($piece)) == ($dest_piece eq lc($dest_piece))); } - - if (lc($piece) eq 'k') { - return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1); + + if ($piece eq 'p') { + # black pawn + if ($to_col == $from_col && $to_row == $from_row + 1) { + return ($dest_piece eq '-'); + } + if ($to_col == $from_col && $from_row == 1 && $to_row == 3) { + my $middle_piece = $board->[2][$to_col]; + return ($dest_piece eq '-' && $middle_piece eq '-'); + } + if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) { + if ($dest_piece eq '-') { + # En passant. TODO: check that the last move was indeed an EP move + return ($to_row == 5 && $board->[4][$to_col] eq 'P'); + } else { + return 1; + } + } + return 0; } + if ($piece eq 'P') { + # white pawn + if ($to_col == $from_col && $to_row == $from_row - 1) { + return ($dest_piece eq '-'); + } + if ($to_col == $from_col && $from_row == 6 && $to_row == 4) { + my $middle_piece = $board->[5][$to_col]; + return ($dest_piece eq '-' && $middle_piece eq '-'); + } + if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) { + if ($dest_piece eq '-') { + # En passant. TODO: check that the last move was indeed an EP move + return ($to_row == 2 && $board->[3][$to_col] eq 'p'); + } else { + return 1; + } + } + return 0; + } + if (lc($piece) eq 'r') { return 0 unless ($from_row == $to_row || $from_col == $to_col); @@ -223,99 +376,70 @@ sub can_reach { return (can_reach($board, 'R', $from_row, $from_col, $to_row, $to_col) || can_reach($board, 'B', $from_row, $from_col, $to_row, $to_col)); } - - # TODO: en passant - if ($piece eq 'p') { - # black pawn - if ($to_col == $from_col && $to_row == $from_row + 1) { - return ($dest_piece eq '-'); - } - if ($to_col == $from_col && $from_row == 1 && $to_row == 3) { - my $middle_piece = $board->[2][$to_col]; - return ($dest_piece eq '-' && $middle_piece eq '-'); - } - if (abs($to_col - $from_col) == 1 && $to_row == $from_row + 1) { - return ($dest_piece ne '-'); - } - return 0; - } - if ($piece eq 'P') { - # white pawn - if ($to_col == $from_col && $to_row == $from_row - 1) { - return ($dest_piece eq '-'); - } - if ($to_col == $from_col && $from_row == 6 && $to_row == 4) { - my $middle_piece = $board->[5][$to_col]; - return ($dest_piece eq '-' && $middle_piece eq '-'); - } - if (abs($to_col - $from_col) == 1 && $to_row == $from_row - 1) { - return ($dest_piece ne '-'); - } - return 0; + if (lc($piece) eq 'k') { + return (abs($from_row - $to_row) <= 1 && abs($from_col - $to_col) <= 1); } - + # unknown piece return 0; } -# Returns 'none', 'white', 'black' or 'both', depending on which sides are in check. -# The latter naturally indicates an invalid position. -sub in_check { - my $board = shift; - my ($black_check, $white_check) = (0, 0); +# Like can_reach, but also checks the move doesn't put the side in check. +# We use this in prettyprint_move to reduce the disambiguation, because Chess.js +# needs moves to be in minimally disambiguated form. +sub can_legally_reach { + my ($board, $piece, $from_row, $from_col, $to_row, $to_col) = @_; + + return 0 if (!can_reach($board, $piece, $from_row, $from_col, $to_row, $to_col)); + + my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col); + my $side = ($piece eq lc($piece)) ? 'k' : 'K'; - my ($wkr, $wkc, $bkr, $bkc) = _find_kings($board); + return !in_check($nb, $side); +} + +my %pieces_against_side = ( + k => { K => 1, Q => 1, R => 1, N => 1, B => 1, P => 1 }, + K => { k => 1, q => 1, r => 1, n => 1, b => 1, p => 1 }, +); - # check all pieces for the possibility of threatening the two kings +# Returns whether the given side (given as k or K for black and white) is in check. +sub in_check { + my ($board, $side) = @_; + my ($kr, $kc) = _find_piece($board, $side); + + # check all pieces for the possibility of threatening this king for my $row (0..7) { + next unless grep { exists($pieces_against_side{$side}{$_}) } @{$board->[$row]}; for my $col (0..7) { my $piece = $board->[$row][$col]; next if ($piece eq '-'); - - if (uc($piece) eq $piece) { - # white piece - $black_check = 1 if ($board->can_reach($piece, $row, $col, $bkr, $bkc)); - } else { - # black piece - $white_check = 1 if ($board->can_reach($piece, $row, $col, $wkr, $wkc)); - } + return 1 if ($board->can_reach($piece, $row, $col, $kr, $kc)); } } - if ($black_check && $white_check) { - return 'both'; - } elsif ($black_check) { - return 'black'; - } elsif ($white_check) { - return 'white'; - } else { - return 'none'; - } + return 0; } -sub _find_kings { - my $board = shift; - my ($wkr, $wkc, $bkr, $bkc); +sub _find_piece { + my ($board, $piece) = @_; for my $row (0..7) { + next unless grep { $_ eq $piece } @{$board->[$row]}; for my $col (0..7) { - my $piece = $board->[$row][$col]; - if ($piece eq 'K') { - ($wkr, $wkc) = ($row, $col); - } elsif ($piece eq 'k') { - ($bkr, $bkc) = ($row, $col); + if ($board->[$row][$col] eq $piece) { + return ($row, $col); } } } - return ($wkr, $wkc, $bkr, $bkc); + return (undef, undef); } -# Returns if any side is in mate. +# Returns if the given side (given as k or K) is in mate. sub in_mate { - my $board = shift; - my $check = $board->in_check(); - return 0 if ($check eq 'none'); + my ($board, $side, $in_check) = @_; + return 0 if (!$in_check); # try all possible moves for the side in check for my $row (0..7) { @@ -323,7 +447,7 @@ sub in_mate { my $piece = $board->[$row][$col]; next if ($piece eq '-'); - if ($check eq 'white') { + if ($side eq 'K') { next if ($piece eq lc($piece)); } else { next if ($piece eq uc($piece)); @@ -334,11 +458,8 @@ sub in_mate { next if ($row == $dest_row && $col == $dest_col); next unless ($board->can_reach($piece, $row, $col, $dest_row, $dest_col)); - my $nb = $board->clone(); - $nb->[$row][$col] = '-'; - $nb->[$dest_row][$dest_col] = $piece; - my $new_check = $nb->in_check(); - return 0 if ($new_check ne $check && $new_check ne 'both'); + my $nb = $board->make_move($row, $col, $dest_row, $dest_col); + return 0 if (!$nb->in_check($side)); } } } @@ -354,41 +475,60 @@ sub prettyprint_move { my $pretty = $board->_prettyprint_move_no_check_or_mate($from_row, $from_col, $to_row, $to_col, $promo); my $nb = $board->make_move($from_row, $from_col, $to_row, $to_col, $promo); - if ($nb->in_mate()) { + + my $piece = $board->[$from_row][$from_col]; + my $other_side = (uc($piece) eq $piece) ? 'k' : 'K'; + my $in_check = $nb->in_check($other_side); + if ($nb->in_mate($other_side, $in_check)) { $pretty .= '#'; - } elsif ($nb->in_check() ne 'none') { + } elsif ($in_check) { $pretty .= '+'; } return ($pretty, $nb); } +sub num_pieces { + my ($board) = @_; + + my $num = 0; + for my $row (0..7) { + for my $col (0..7) { + my $piece = $board->[$row][$col]; + ++$num if ($piece ne '-'); + } + } + return $num; +} + sub _prettyprint_move_no_check_or_mate { my ($board, $from_row, $from_col, $to_row, $to_col, $promo) = @_; my $piece = $board->[$from_row][$from_col]; - my $move = _move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo); + my $move = move_to_uci_notation($from_row, $from_col, $to_row, $to_col, $promo); if ($piece eq '-') { die "Invalid move $move"; } - # white short castling - if ($move eq 'e1g1' && $piece eq 'K') { - return '0-0'; - } - - # white long castling - if ($move eq 'e1c1' && $piece eq 'K') { - return '0-0-0'; - } - - # black short castling - if ($move eq 'e8g8' && $piece eq 'k') { - return '0-0'; - } + if ($piece eq 'K') { + # white short/long castling + return 'O-O' if ($move eq 'e1g1'); + return 'O-O-O' if ($move eq 'e1c1'); - # black long castling - if ($move eq 'e8c8' && $piece eq 'k') { - return '0-0-0'; + # white short/long chess960-style castling (king takes own rook) + my $dst_piece = $board->[$to_row][$to_col]; + if ($dst_piece eq 'R') { + return ($to_col > $from_col) ? 'O-O' : 'O-O-O'; + } + } elsif ($piece eq 'k') { + # black short/long castling + return 'O-O' if ($move eq 'e8g8'); + return 'O-O-O' if ($move eq 'e8c8'); + + # black short/long chess960-style castling (king takes own rook) + my $dst_piece = $board->[$to_row][$to_col]; + if ($dst_piece eq 'r') { + return ($to_col > $from_col) ? 'O-O' : 'O-O-O'; + } } my $pretty; @@ -400,12 +540,12 @@ sub _prettyprint_move_no_check_or_mate { $pretty = substr($move, 0, 1) . 'x' . _pos_to_square($to_row, $to_col); } else { $pretty = _pos_to_square($to_row, $to_col); + } - if (defined($promo) && $promo ne '') { - # promotion - $pretty .= "="; - $pretty .= $promo; - } + if (defined($promo) && $promo ne '') { + # promotion + $pretty .= "="; + $pretty .= uc($promo); } return $pretty; } @@ -417,7 +557,7 @@ sub _prettyprint_move_no_check_or_mate { for my $col (0..7) { for my $row (0..7) { next unless ($board->[$row][$col] eq $piece); - ++$num_total if ($board->can_reach($piece, $row, $col, $to_row, $to_col)); + ++$num_total if ($board->can_legally_reach($piece, $row, $col, $to_row, $to_col)); } } @@ -425,14 +565,14 @@ sub _prettyprint_move_no_check_or_mate { my $num_row = 0; for my $col (0..7) { next unless ($board->[$from_row][$col] eq $piece); - ++$num_row if ($board->can_reach($piece, $from_row, $col, $to_row, $to_col)); + ++$num_row if ($board->can_legally_reach($piece, $from_row, $col, $to_row, $to_col)); } # and same for columns my $num_col = 0; for my $row (0..7) { next unless ($board->[$row][$from_col] eq $piece); - ++$num_col if ($board->can_reach($piece, $row, $from_col, $to_row, $to_col)); + ++$num_col if ($board->can_legally_reach($piece, $row, $from_col, $to_row, $to_col)); } # see if we need to disambiguate