diff --git a/README.md b/README.md index 8dfdcf3..bed0d67 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ PHP library for reading and editing FEN and PGN chess formats. ## Features ### FEN class - - load FEN representing chess position + - load FEN representing chess position (standard or Shredder-FEN) - read and modify all FEN fields - export FEN - read and set piece placement @@ -15,6 +15,7 @@ PHP library for reading and editing FEN and PGN chess formats. - perform move in given position - test if move is legal in given position - list all legal moves in given position + - supports Fisher's random (Chess960) ### PGN class - load PGN representing chess game @@ -22,6 +23,7 @@ PHP library for reading and editing FEN and PGN chess formats. - export PGN - read and add moves - get FEN position after any move + - supports Fisher's random (Chess960) ## Installation Install with composer: @@ -44,7 +46,7 @@ echo($fen->get_board()); // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR echo($fen->get_active_color()); // w -echo($fen->get_castling_string()); +echo($fen->get_castling()); // KQkq echo($fen->get_en_passant()); // - @@ -76,7 +78,7 @@ echo($fen->get_board()); // rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR echo($fen->get_active_color()); // b -echo($fen->get_castling_string()); +echo($fen->get_castling()); // KQq echo($fen->get_en_passant()); // c6 @@ -124,7 +126,7 @@ echo($fen->export()); // rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 $fen->set_board('rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR'); $fen->set_active_color('b'); -$fen->set_castling_string('KQq'); +$fen->set_castling('KQq'); $fen->set_en_passant('c6'); $fen->set_halfmove(1); $fen->set_fullmove(2); @@ -185,6 +187,7 @@ Test check, mate, stalemate: $fen = new Onspli\Chess\FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/8/8/8/K7'); +$fen->set_castling('-'); echo($fen->is_check() ? 'true' : 'false'); // false echo($fen->is_stalemate() ? 'true' : 'false'); diff --git a/README.template.md b/README.template.md index 69b8578..b8d1b0a 100644 --- a/README.template.md +++ b/README.template.md @@ -5,7 +5,7 @@ PHP library for reading and editing FEN and PGN chess formats. ## Features ### FEN class - - load FEN representing chess position + - load FEN representing chess position (standard or Shredder-FEN) - read and modify all FEN fields - export FEN - read and set piece placement @@ -15,6 +15,7 @@ PHP library for reading and editing FEN and PGN chess formats. - perform move in given position - test if move is legal in given position - list all legal moves in given position + - supports Fisher's random (Chess960) ### PGN class - load PGN representing chess game @@ -22,6 +23,7 @@ PHP library for reading and editing FEN and PGN chess formats. - export PGN - read and add moves - get FEN position after any move + - supports Fisher's random (Chess960) ## Installation Install with composer: @@ -40,7 +42,7 @@ echo($fen->export()); echo($fen->export_short()); echo($fen->get_board()); echo($fen->get_active_color()); -echo($fen->get_castling_string()); +echo($fen->get_castling()); echo($fen->get_en_passant()); echo($fen->get_halfmove()); echo($fen->get_fullmove()); @@ -54,7 +56,7 @@ echo($fen->export()); echo($fen->export_short()); echo($fen->get_board()); echo($fen->get_active_color()); -echo($fen->get_castling_string()); +echo($fen->get_castling()); echo($fen->get_en_passant()); echo($fen->get_halfmove()); echo($fen->get_fullmove()); @@ -76,7 +78,7 @@ $fen = new Onspli\Chess\FEN; echo($fen->export()); $fen->set_board('rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR'); $fen->set_active_color('b'); -$fen->set_castling_string('KQq'); +$fen->set_castling('KQq'); $fen->set_en_passant('c6'); $fen->set_halfmove(1); $fen->set_fullmove(2); @@ -105,6 +107,7 @@ Test check, mate, stalemate: $fen = new Onspli\Chess\FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/8/8/8/K7'); +$fen->set_castling('-'); echo($fen->is_check() ? 'true' : 'false'); echo($fen->is_stalemate() ? 'true' : 'false'); echo($fen->preview()); diff --git a/docs/README.md b/docs/README.md index a91b282..95959ae 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ | [Board::get_defended_squares](#Boardget_defended_squares) | Get array of all squares defended (or attacked) by $defender being on $defender_square. | | [Board::get_reachable_squares](#Boardget_reachable_squares) | Get array of all squares reachable from $origin_square by $moving_piece. | | [Board::find_squares_with_piece](#Boardfind_squares_with_piece) | Returns array of squares containing piece. | +| [Board::find_square_with_piece](#Boardfind_square_with_piece) | Returns square containing piece. If there are more pieces, throws. | | [Board::get_color_of_piece](#Boardget_color_of_piece) | Returns the color of the piece. | | [Board::get_piece_of_color](#Boardget_piece_of_color) | Converts piece to requested color. | | [Board::get_opposite_color](#Boardget_opposite_color) | Get color opposite to color passed as an argument. | @@ -22,7 +23,7 @@ | [Board::is_square_attacked](#Boardis_square_attacked) | Tells whether the square is attacked by the color specified. | | [Board::is_check](#Boardis_check) | Tells whether the king of color specified is in check. | | [**FEN**](#FEN) | FEN is a standard notation for describing a particular board position of a chess game | -| [FEN::__construct](#FEN__construct) | Load FEN or setup starting position. | +| [FEN::__construct](#FEN__construct) | Load FEN (or Shredder-FEN) or setup starting position. | | [FEN::__clone](#FEN__clone) | | | [FEN::export](#FENexport) | Export whole FEN string. | | [FEN::export_short](#FENexport_short) | Export FEN string without halfmoves count and fullmove number. | @@ -34,10 +35,8 @@ | [FEN::set_square](#FENset_square) | Set piece on a particular square. | | [FEN::get_active_color](#FENget_active_color) | Active color. | | [FEN::set_active_color](#FENset_active_color) | Set active color. | -| [FEN::get_castling_string](#FENget_castling_string) | Castling availability. | -| [FEN::set_castling_string](#FENset_castling_string) | Set castling availability. | -| [FEN::get_castling_availability](#FENget_castling_availability) | Get castling availability of particular type. | -| [FEN::set_castling_availability](#FENset_castling_availability) | Set castling availability of particular type. | +| [FEN::get_castling](#FENget_castling) | Castling availability. | +| [FEN::set_castling](#FENset_castling) | Set castling availability. | | [FEN::get_en_passant](#FENget_en_passant) | Get En Passant target square. | | [FEN::set_en_passant](#FENset_en_passant) | Set En Passant target square. | | [FEN::get_halfmove](#FENget_halfmove) | Get Halfmove clock | @@ -367,6 +366,32 @@ Board::find_squares_with_piece( string piece, bool as_object = false ): array +--- +### Board::find_square_with_piece + +Returns square containing piece. If there are more pieces, throws. + +```php +Board::find_square_with_piece( string piece, bool as_object = false ): mixed +``` + + + + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `piece` | **string** | | +| `as_object` | **bool** | | + + +**Return Value:** + + + + + --- ### Board::get_color_of_piece @@ -539,7 +564,7 @@ move according to chess rules. ### FEN::__construct -Load FEN or setup starting position. +Load FEN (or Shredder-FEN) or setup starting position. ```php FEN::__construct( string fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' ): void @@ -818,12 +843,12 @@ FEN::set_active_color( string color ): void --- -### FEN::get_castling_string +### FEN::get_castling Castling availability. ```php -FEN::get_castling_string( ): string +FEN::get_castling( ): string ``` If neither side can castle, this is "-". @@ -831,6 +856,9 @@ Otherwise, this has one or more letters: "K" (White can castle kingside), "Q" (White can castle queenside), "k" (Black can castle kingside), and/or "q" (Black can castle queenside). A move that temporarily prevents castling does not negate this notation. +Shredder-FEN is also supported - instead of K and Q the letters +of files containing rooks are used. For standard chess its AHah. +X-FEN is not supported. @@ -841,12 +869,12 @@ castling availability string --- -### FEN::set_castling_string +### FEN::set_castling Set castling availability. ```php -FEN::set_castling_string( string castling ): void +FEN::set_castling( string castling ): void ``` If neither side can castle, this is "-". @@ -854,6 +882,9 @@ Otherwise, this has one or more letters: "K" (White can castle kingside), "Q" (White can castle queenside), "k" (Black can castle kingside), and/or "q" (Black can castle queenside). A move that temporarily prevents castling does not negate this notation. +Shredder-FEN is also supported - instead of K and Q the letters +of files containing rooks are used. For standard chess its AHah. +X-FEN is not supported. **Parameters:** @@ -869,63 +900,6 @@ does not negate this notation. ---- -### FEN::get_castling_availability - -Get castling availability of particular type. - -```php -FEN::get_castling_availability( string type ): bool -``` - -Possible castling types: "K" (White can castle kingside), -"Q" (White can castle queenside), "k" (Black can castle kingside), and/or -"q" (Black can castle queenside). A move that temporarily prevents castling -does not negate this notation. - - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `type` | **string** | | - - -**Return Value:** - - - - - ---- -### FEN::set_castling_availability - -Set castling availability of particular type. - -```php -FEN::set_castling_availability( string type, bool avalability ): void -``` - -Possible castling types: "K" (White can castle kingside), -"Q" (White can castle queenside), "k" (Black can castle kingside), and/or -"q" (Black can castle queenside). A move that temporarily prevents castling -does not negate this notation. - - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `type` | **string** | | -| `avalability` | **bool** | | - - -**Return Value:** - - - - - --- ### FEN::get_en_passant diff --git a/src/Board.php b/src/Board.php index 30f565f..19d86e7 100644 --- a/src/Board.php +++ b/src/Board.php @@ -390,6 +390,18 @@ public function find_squares_with_piece(string $piece, bool $as_object = false) return $arr; } + /** + * Returns square containing piece. If there are more pieces, throws. + */ + public function find_square_with_piece(string $piece, bool $as_object = false) + { + $squares = $this->find_squares_with_piece($piece, $as_object); + if (sizeof($squares) != 1) { + throw new RulesException("There are " . sizeof($squares) . " pieces '" . $piece . "' on the board."); + } + return $squares[0]; + } + /** * Returns the color of the piece. * @return string w|b @@ -493,13 +505,8 @@ public function is_square_attacked($square, string $attacking_color) : bool public function is_check(string $color) : bool { self::validate_color($color); - - $king_squares = $this->find_squares_with_piece(self::get_piece_of_color('K', $color), true); - if (sizeof($king_squares) != 1) { - throw new RulesException("There are " . sizeof($king_squares) . " kings of active color on the board."); - } - - return $this->is_square_attacked($king_squares[0], self::get_opposite_color($color)); + $king_square = $this->find_square_with_piece(self::get_piece_of_color('K', $color), true); + return $this->is_square_attacked($king_square, self::get_opposite_color($color)); } private static function validate_color(string $color) : void diff --git a/src/FEN.php b/src/FEN.php index 88fb6fd..bd9e5e2 100644 --- a/src/FEN.php +++ b/src/FEN.php @@ -18,13 +18,18 @@ class FEN { private $board; private $active = 'w'; - private $castling = 'KQkq'; private $en_passant; private $halfmove = 0; private $fullmove = 1; + private $kings_rook_file = 'h'; + private $queens_rook_file = 'a'; + private $kings_file = 'e'; + private $use_shredder_fen = false; + private $castling_rights = [ "K" => true, "Q" => true, "k" => true, "q" => true]; + /** - * Load FEN or setup starting position. + * Load FEN (or Shredder-FEN) or setup starting position. * @param string $fen * @return void */ @@ -39,7 +44,7 @@ function __construct(string $fen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR $this->set_board($parts[0]); $this->set_active_color($parts[1]); - $this->set_castling_string($parts[2]); + $this->set_castling($parts[2]); $this->set_en_passant($parts[3]); $this->set_halfmove($parts[4] ?? 0); $this->set_fullmove($parts[5] ?? 1); @@ -57,7 +62,7 @@ public function __clone() */ public function export() : string { - return implode(' ', [$this->get_board(), $this->get_active_color(), $this->get_castling_string(), $this->get_en_passant(), $this->get_halfmove(), $this->get_fullmove()]); + return implode(' ', [$this->get_board(), $this->get_active_color(), $this->get_castling(), $this->get_en_passant(), $this->get_halfmove(), $this->get_fullmove()]); } /** @@ -67,7 +72,7 @@ public function export() : string */ public function export_short() : string { - return implode(' ', [$this->get_board(), $this->get_active_color(), $this->get_castling_string(), $this->get_en_passant()]); + return implode(' ', [$this->get_board(), $this->get_active_color(), $this->get_castling(), $this->get_en_passant()]); } public function __toString() : string @@ -188,12 +193,32 @@ public function set_active_color(string $color) : void * "Q" (White can castle queenside), "k" (Black can castle kingside), and/or * "q" (Black can castle queenside). A move that temporarily prevents castling * does not negate this notation. + * Shredder-FEN is also supported - instead of K and Q the letters + * of files containing rooks are used. For standard chess its AHah. + * X-FEN is not supported. * * @return string castling availability string */ - public function get_castling_string() : string + public function get_castling() : string { - return $this->castling; + if ($this->use_shredder_fen) + { + $str = + ($this->castling_rights['Q'] ? strtoupper($this->queens_rook_file) : '') . + ($this->castling_rights['K'] ? strtoupper($this->kings_rook_file) : '') . + ($this->castling_rights['q'] ? $this->queens_rook_file : ''). + ($this->castling_rights['k'] ? $this->kings_rook_file : ''); + return $str ? $str : '-'; + } + else + { + $str = + ($this->castling_rights['K'] ? 'K' : '') . + ($this->castling_rights['Q'] ? 'Q' : '') . + ($this->castling_rights['k'] ? 'k' : '') . + ($this->castling_rights['q'] ? 'q' : ''); + return $str ? $str : '-'; + } } /** @@ -204,15 +229,116 @@ public function get_castling_string() : string * "Q" (White can castle queenside), "k" (Black can castle kingside), and/or * "q" (Black can castle queenside). A move that temporarily prevents castling * does not negate this notation. + * Shredder-FEN is also supported - instead of K and Q the letters + * of files containing rooks are used. For standard chess its AHah. + * X-FEN is not supported. * * @param string castling availability string */ - public function set_castling_string(string $castling) : void + public function set_castling(string $castling) : void { - if (!in_array($castling, ['-', 'KQkq', 'KQk', 'KQq', 'KQ', 'Kkq', 'Kk', 'Kq', 'K', 'Qkq', 'Qk', 'Qq', 'Q', 'kq', 'k', 'q'])) { - throw new ParseException("Invalid castling string '$castling'."); + if (in_array($castling, ['-', 'KQkq', 'KQk', 'KQq', 'KQ', 'Kkq', 'Kk', 'Kq', 'K', 'Qkq', 'Qk', 'Qq', 'Q', 'kq', 'k', 'q'])) { + $new_use_shredder_fen = false; + $new_kings_rook_file = 'h'; + $new_queens_rook_file = 'a'; + $new_kings_file = 'e'; + } else { + $new_use_shredder_fen = true; + $new_kings_rook_file = null; + $new_queens_rook_file = null; + $new_kings_file = null; + } + $new_castling_rights = [ "K" => false, "Q" => false, "k" => false, "q" => false]; + + if ($castling == '-') { + $castling_rights = []; + } else { + $castling_rights = str_split($castling); + } + + $white_king_square = $this->board->find_square_with_piece('K', true); + $black_king_square = $this->board->find_square_with_piece('k', true); + foreach ($castling_rights as $castling_right) + { + if ($new_use_shredder_fen && strtolower($castling_right) >= 'a' && strtolower($castling_right) <= 'h') + { + $new_kings_file = $this->board->find_square_with_piece(Board::get_piece_of_color('K', ctype_upper($castling_right) ? 'w' : 'b'), true)->get_file(); + $rooks_file = strtolower($castling_right); + if ($rooks_file < $new_kings_file) { + $castling_type = ctype_upper($castling_right)? 'Q' : 'q'; + $new_queens_rook_file = $rooks_file; + } else { + $castling_type = ctype_upper($castling_right)? 'K' : 'k'; + $new_kings_rook_file = $rooks_file; + } + } + else if (!$new_use_shredder_fen) + { + $castling_type = $castling_right; + } + else + { + throw new ParseException("Invalid castling right '$castling_right' in string '$castling'."); + } + $new_castling_rights[$castling_type] = true; + } + + $this->validate_castling($new_castling_rights, $new_kings_file, $new_kings_rook_file, $new_queens_rook_file); + $this->kings_file = $new_kings_file; + $this->kings_rook_file = $new_kings_rook_file; + $this->queens_rook_file = $new_queens_rook_file; + $this->castling_rights = $new_castling_rights; + $this->use_shredder_fen = $new_use_shredder_fen; + } + + private function validate_castling(array $castling_rights, $kings_file, $kings_rook_file, $queens_rook_file) : void + { + if ($kings_file === null || $kings_rook_file === null || $queens_rook_file === null) + { + throw new ParseException("Invalid castling string."); + } + + if ($castling_rights['K'] || $castling_rights['Q']) + { + if ($this->board->get_square($kings_file.'1') != 'K') { + throw new ParseException("Invalid castling string. White king not in initial position."); + } + } + + if ($castling_rights['k'] || $castling_rights['q']) + { + if ($this->board->get_square($kings_file.'8') != 'k') { + throw new ParseException("Invalid castling string. Black king not in initial position."); + } + } + + if ($castling_rights['K']) + { + if ($this->board->get_square($kings_rook_file.'1') != 'R') { + throw new ParseException("Invalid castling string. White king's rook not in initial position."); + } + } + + if ($castling_rights['k']) + { + if ($this->board->get_square($kings_rook_file.'8') != 'r') { + throw new ParseException("Invalid castling string. Black king's rook not in initial position."); + } + } + + if ($castling_rights['Q']) + { + if ($this->board->get_square($queens_rook_file.'1') != 'R') { + throw new ParseException("Invalid castling string. White queen's rook not in initial position."); + } + } + + if ($castling_rights['q']) + { + if ($this->board->get_square($queens_rook_file.'8') != 'r') { + throw new ParseException("Invalid castling string. Black queen's rook not in initial position."); + } } - $this->castling = $castling; } /** @@ -225,11 +351,10 @@ public function set_castling_string(string $castling) : void * * @param string castling type */ - public function get_castling_availability(string $type) : bool + private function get_castling_availability(string $type) : bool { self::validate_castling_type($type); - $castling = str_split($this->castling); - return in_array($type, $castling); + return $this->castling_rights[$type]; } /** @@ -243,34 +368,10 @@ public function get_castling_availability(string $type) : bool * @param string castling type * @param bool set availability */ - public function set_castling_availability(string $type, bool $avalability) : void + private function set_castling_availability(string $type, bool $avalability) : void { self::validate_castling_type($type); - if ($this->get_castling_availability($type) === $avalability) { - return; - } - - // convert str to array of available types - if ($this->castling == '-') { - $castling = []; - } - else { - $castling = str_split($this->castling); - } - - // add or remove castling availability for type - if ($avalability === false) { - $castling = array_diff($castling, [$type]); - } - else { - $castling = array_merge($castling, [$type]); - } - - // sort and convert array back to string - sort($castling); - $castling = sizeof($castling) ? implode('', $castling) : '-'; - - $this->set_castling_string($castling); + $this->castling_rights[$type] = $avalability; } private static function validate_castling_type(string $type) : void @@ -515,6 +616,9 @@ static private function is_promoting_square(Square $target_square) : bool */ public function get_legal_moves() : array { + // assert + $this->validate_castling($this->castling_rights, $this->kings_file, $this->kings_rook_file, $this->queens_rook_file); + $pseudolegal_moves = []; $this->push_pawns_pseudolegal_moves_to_array($pseudolegal_moves, $this->get_active_color()); @@ -555,37 +659,17 @@ private function castle_kingside(Move &$move) : void } if ($this->get_active_color() == 'w') { - $origin = new Square('e1'); + $rank = 1; } else { - $origin = new Square('e8'); + $rank = 8; } - if ($this->get_square($origin) != $this->get_active_piece('K')) { - throw new RulesException("Castling not available. King not in initial position."); - } - if ($this->get_square($origin->get_relative_square(1, 0)) || $this->get_square($origin->get_relative_square(2, 0))) { - throw new RulesException("Castling not available. There are pieces in the way."); - } - if ($this->get_square($origin->get_relative_square(3, 0)) != $this->get_active_piece('R')) { - throw new RulesException("Castling not available. Rook not in initial position"); - } - if ($this->is_check()) { - throw new RulesException("Castling not available. King in check."); - } + $origin_king = new Square($this->kings_file.$rank); + $target_king = new Square('g'.$rank); + $origin_rook = new Square($this->kings_rook_file.$rank); + $target_rook = new Square('f'.$rank); - $new_board = clone $this->board; - $new_board->set_square($origin, ''); - $new_board->set_square($origin->get_relative_square(1, 0), $this->get_active_piece('K')); - if ($new_board->is_check($this->get_active_color())) { - throw new RulesException("Castling not available. King in check."); - } - - $new_board->set_square($origin->get_relative_square(1, 0), $this->get_active_piece('R')); - $new_board->set_square($origin->get_relative_square(3, 0), ''); - $new_board->set_square($origin->get_relative_square(2, 0), $this->get_active_piece('K')); - - $this->set_new_board($new_board); - $move->set_origin($origin); + $this->castle_generic($move, $origin_king, $target_king, $origin_rook, $target_rook); } private function castle_queenside(Move &$move) : void @@ -595,37 +679,71 @@ private function castle_queenside(Move &$move) : void } if ($this->get_active_color() == 'w') { - $origin = new Square('e1'); + $rank = 1; } else { - $origin = new Square('e8'); + $rank = 8; } - if ($this->get_square($origin) != $this->get_active_piece('K')) { - throw new RulesException("Castling not available. King not in initial position."); - } - if ($this->get_square($origin->get_relative_square(-1, 0)) || $this->get_square($origin->get_relative_square(-2, 0)) || $this->get_square($origin->get_relative_square(-3, 0))) { - throw new RulesException("Castling not available. There are pieces in the way."); + $origin_king = new Square($this->kings_file.$rank); + $target_king = new Square('c'.$rank); + $origin_rook = new Square($this->queens_rook_file.$rank); + $target_rook = new Square('d'.$rank); + + $this->castle_generic($move, $origin_king, $target_king, $origin_rook, $target_rook); + } + + private function castle_generic(Move &$move, Square $origin_king, Square $target_king, Square $origin_rook, Square $target_rook) + { + if (!$this->are_castling_squares_vacant($origin_king, $target_king, $origin_rook->get_file())) { + throw new RulesException("Castling not available. There are pieces in the way of King."); } - if ($this->get_square($origin->get_relative_square(-4, 0)) != $this->get_active_piece('R')) { - throw new RulesException("Castling not available. Rook not in initial position"); + if (!$this->are_castling_squares_vacant($origin_rook, $target_rook, $origin_rook->get_file())) { + throw new RulesException("Castling not available. There are pieces in the way of Rook."); } - if ($this->is_check()) { + if (!$this->are_castling_squares_safe($origin_king, $target_king)) { throw new RulesException("Castling not available. King in check."); } - $new_board = clone $this->board; - $new_board->set_square($origin, ''); - $new_board->set_square($origin->get_relative_square(-1, 0), $this->get_active_piece('K')); - if ($new_board->is_check($this->get_active_color())) { - throw new RulesException("Castling not available. King in check."); + $this->board->set_square($origin_king, ''); + $this->board->set_square($origin_rook, ''); + $this->board->set_square($target_rook, $this->get_active_piece('R')); + $this->board->set_square($target_king, $this->get_active_piece('K')); + + $move->set_origin($origin_king); + } + + private function are_castling_squares_vacant(Square $origin, Square $target, $allowed_rook_file) + { + $min_file = min($origin->get_file(), $target->get_file()); + $max_file = max($origin->get_file(), $target->get_file()); + + $rank = $origin->get_rank(); + for ($file = $min_file; $file <= $max_file; $file++) + { + if ($file == $allowed_rook_file || $file == $this->kings_file) { + continue; + } + $piece = $this->board->get_square($file.$rank); + if ($piece) { + return false; + } } + return true; + } - $new_board->set_square($origin->get_relative_square(-1, 0), $this->get_active_piece('R')); - $new_board->set_square($origin->get_relative_square(-4, 0), ''); - $new_board->set_square($origin->get_relative_square(-2, 0), $this->get_active_piece('K')); + private function are_castling_squares_safe(Square $origin, Square $target) + { + $min_file = min($origin->get_file(), $target->get_file()); + $max_file = max($origin->get_file(), $target->get_file()); - $this->set_new_board($new_board); - $move->set_origin($origin); + $rank = $origin->get_rank(); + for ($file = $min_file; $file <= $max_file; $file++) + { + if ($this->board->is_square_attacked($file.$rank, Board::get_opposite_color($this->get_active_color()))) { + return false; + } + } + return true; } /** @@ -766,6 +884,9 @@ private function set_new_board(Board $new_board) : void */ public function move(string $move) : void { + // assert + $this->validate_castling($this->castling_rights, $this->kings_file, $this->kings_rook_file, $this->queens_rook_file); + $move = new Move($move); switch ($move->get_castling()) { @@ -781,21 +902,38 @@ public function move(string $move) : void $this->after_move_update_halfmove($move); $this->after_move_set_en_passant($move); - $this->after_move_update_castling_availability($move->get_piece_type(), $move->get_origin(true)); + $this->after_move_update_castling_availability($move->get_piece_type(), $move->get_origin(true), $move->get_target(true)); $this->after_move_change_active_color(); } - private function after_move_update_castling_availability(string $piece_type, Square $origin_square) : void + private function after_move_update_castling_availability(string $piece_type, Square $origin_square, Square $target_square) : void { + // moving king or rooks prevents castling $origin_file = $origin_square->get_file(); - if ($piece_type == 'R' && $origin_file == 'a') { + if ($piece_type == 'R' && $origin_file == $this->queens_rook_file) { $this->set_castling_availability($this->get_active_piece('Q'), false); - } else if ($piece_type == 'R' && $origin_file == 'h') { + } else if ($piece_type == 'R' && $origin_file == $this->kings_rook_file) { $this->set_castling_availability($this->get_active_piece('K'), false); - } else if ($piece_type == 'K' && $origin_file == 'e') { + } else if ($piece_type == 'K' && $origin_file == $this->kings_file) { $this->set_castling_availability($this->get_active_piece('Q'), false); $this->set_castling_availability($this->get_active_piece('K'), false); } + + // capturing opponents rooks prevents castling + $active_color = $this->get_active_color(); + $target_square_str = $target_square->export(); + if ($active_color == 'b' && $target_square_str == $this->queens_rook_file.'1') { + $this->set_castling_availability($this->get_opponents_piece('Q'), false); + } else if ($active_color == 'b' && $target_square_str == $this->kings_rook_file.'1') { + $this->set_castling_availability($this->get_opponents_piece('K'), false); + } else if ($active_color == 'w' && $target_square_str == $this->queens_rook_file.'8') { + $this->set_castling_availability($this->get_opponents_piece('Q'), false); + } else if ($active_color == 'w' && $target_square_str == $this->kings_rook_file.'8') { + $this->set_castling_availability($this->get_opponents_piece('K'), false); + } + + // assert + $this->validate_castling($this->castling_rights, $this->kings_file, $this->kings_rook_file, $this->queens_rook_file); } private function after_move_change_active_color() : void diff --git a/tests/unit/FENTest.php b/tests/unit/FENTest.php index cdaf551..e63e513 100644 --- a/tests/unit/FENTest.php +++ b/tests/unit/FENTest.php @@ -119,84 +119,86 @@ public function testSetActiveInvalid() : void public function testCastling() : void { $fen = new FEN; - $this->assertEquals('KQkq', $fen->get_castling_string()); - $fen->set_castling_string('-'); - $this->assertEquals('-', $fen->get_castling_string()); - $fen->set_castling_string('KQkq'); - $this->assertEquals('KQkq', $fen->get_castling_string()); - $fen->set_castling_string('k'); - $this->assertEquals('k', $fen->get_castling_string()); + $this->assertEquals('KQkq', $fen->get_castling()); + $fen->set_castling('-'); + $this->assertEquals('-', $fen->get_castling()); + $fen->set_castling('KQkq'); + $this->assertEquals('KQkq', $fen->get_castling()); + $fen->set_castling('k'); + $this->assertEquals('k', $fen->get_castling()); + $fen->set_castling('AHah'); + $this->assertEquals('AHah', $fen->get_castling()); + + $fen = new FEN('rnbkqbrn/pppppppp/8/8/8/8/PPPPPPPP/RNBKQBRN w AGag - 0 1'); + $this->assertEquals('AGag', $fen->get_castling()); + + $fen = new FEN('rnbkqbrn/pppppppp/8/8/8/8/PPPPPPPP/RNBKQBRN w Ag - 0 1'); + $this->assertEquals('Ag', $fen->get_castling()); + + $fen = new FEN('rnbkqbrn/pppppppp/8/8/8/8/PPPPPPPP/RNBKQBRN w - - 0 1'); + $this->assertEquals('-', $fen->get_castling()); } - public function testSetCastlingInvalid() : void + public function testSetCastlingInvalid1() : void { $fen = new FEN; $this->expectException(ParseException::class); - $fen->set_castling_string('KQxq'); + $fen->set_castling('KQxq'); } - public function testSetCastlingAvailibilityInvalid1() : void + public function testSetCastlingInvalid2() : void { $fen = new FEN; $this->expectException(ParseException::class); - $fen->get_castling_availability('x'); + $fen->set_castling('KQah'); } - public function testSetCastlingAvailibilityInvalid2() : void + public function testSetCastlingInvalid3() : void + { + $this->expectException(ParseException::class); + $fen = new FEN('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w AHbh - 0 1'); + } + + public function testSetCastlingInvalid4() : void + { + $this->expectException(ParseException::class); + $fen = new FEN('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w AHag - 0 1'); + } + + public function testSetCastlingInvalid5() : void + { + $this->expectException(ParseException::class); + $fen = new FEN('rnbkqbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBKqBNR w KQkq - 0 1'); + } + + public function testSetCastlingInvalid6() : void + { + $this->expectException(ParseException::class); + $fen = new FEN('nrbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/NRBQKBNR w KQkq - 0 1'); + } + + public function testSetCastlingInvalid7() : void { - $fen = new FEN; $this->expectException(ParseException::class); - $fen->set_castling_availability('x', true); - } - - public function testCastlingAvailibility() : void - { - $fen = new FEN; - $this->assertEquals(true, $fen->get_castling_availability('K')); - $this->assertEquals(true, $fen->get_castling_availability('Q')); - $this->assertEquals(true, $fen->get_castling_availability('k')); - $this->assertEquals(true, $fen->get_castling_availability('q')); - $this->assertEquals('KQkq', $fen->get_castling_string()); - - $fen->set_castling_availability('k', false); - $this->assertEquals(true, $fen->get_castling_availability('K')); - $this->assertEquals(true, $fen->get_castling_availability('Q')); - $this->assertEquals(false, $fen->get_castling_availability('k')); - $this->assertEquals(true, $fen->get_castling_availability('q')); - $this->assertEquals('KQq', $fen->get_castling_string()); - - $fen->set_castling_availability('Q', false); - $this->assertEquals(true, $fen->get_castling_availability('K')); - $this->assertEquals(false, $fen->get_castling_availability('Q')); - $this->assertEquals(false, $fen->get_castling_availability('k')); - $this->assertEquals(true, $fen->get_castling_availability('q')); - $this->assertEquals('Kq', $fen->get_castling_string()); - - - $fen->set_castling_availability('k', true); - $this->assertEquals(true, $fen->get_castling_availability('K')); - $this->assertEquals(false, $fen->get_castling_availability('Q')); - $this->assertEquals(true, $fen->get_castling_availability('k')); - $this->assertEquals(true, $fen->get_castling_availability('q')); - $this->assertEquals('Kkq', $fen->get_castling_string()); - - $fen->set_castling_availability('K', false); - $fen->set_castling_availability('Q', false); - $fen->set_castling_availability('k', false); - $fen->set_castling_availability('q', false); - $this->assertEquals(false, $fen->get_castling_availability('K')); - $this->assertEquals(false, $fen->get_castling_availability('Q')); - $this->assertEquals(false, $fen->get_castling_availability('k')); - $this->assertEquals(false, $fen->get_castling_availability('q')); - $this->assertEquals('-', $fen->get_castling_string()); - - $fen->set_castling_availability('k', true); - $fen->set_castling_availability('K', true); - $this->assertEquals(true, $fen->get_castling_availability('K')); - $this->assertEquals(false, $fen->get_castling_availability('Q')); - $this->assertEquals(true, $fen->get_castling_availability('k')); - $this->assertEquals(false, $fen->get_castling_availability('q')); - $this->assertEquals('Kk', $fen->get_castling_string()); + $fen = new FEN('rnbqkbrn/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBRN w KQkq - 0 1'); + } + + public function testSetCastlingInvalid8() : void + { + $this->expectException(ParseException::class); + $fen = new FEN('rnbqkbrn/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBRN w AHah - 0 1'); + } + + public function testSetCastlingInvalid9() : void + { + $this->expectException(ParseException::class); + $fen = new FEN('rnbrkbqn/pppppppp/8/8/8/8/PPPPPPPP/RNBRKBQN w ADad - 0 1'); + } + + public function testSetCastlingInvalid10() : void + { + $this->expectException(ParseException::class); + $fen = new FEN('qnbnkbrr/pppppppp/8/8/8/8/PPPPPPPP/qNBNKBRR w GHgh - 0 1'); } public function testFiftyMove() : void @@ -241,7 +243,7 @@ public function testInitializationAndExport() : void $this->assertEquals('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -', $fen->export_short()); $this->assertEquals('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', $fen->get_board()); $this->assertEquals('w', $fen->get_active_color()); - $this->assertEquals('KQkq', $fen->get_castling_string()); + $this->assertEquals('KQkq', $fen->get_castling()); $this->assertEquals('-', $fen->get_en_passant()); $this->assertEquals(0, $fen->get_halfmove()); $this->assertEquals(1, $fen->get_fullmove()); @@ -250,7 +252,7 @@ public function testInitializationAndExport() : void $this->assertEquals('rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR b KQq c6 1 2', $fen->export()); $this->assertEquals('rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR', $fen->get_board()); $this->assertEquals('b', $fen->get_active_color()); - $this->assertEquals('KQq', $fen->get_castling_string()); + $this->assertEquals('KQq', $fen->get_castling()); $this->assertEquals('c6', $fen->get_en_passant()); $this->assertEquals(1, $fen->get_halfmove()); $this->assertEquals(2, $fen->get_fullmove()); @@ -276,6 +278,7 @@ public function testCheckDetection() : void $this->assertFalse($fen->is_check()); $fen->set_board('7k/6P1/8/8/8/8/3P4/4K3'); + $fen->set_castling('-'); $this->assertTrue($fen->is_check()); $fen->set_active_color('w'); $this->assertFalse($fen->is_check()); @@ -305,6 +308,7 @@ public function testMoveToCheck() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/8/8/8/K7'); + $fen->set_castling('-'); $this->expectException(RulesException::class); $fen->move('Kb1'); } @@ -314,6 +318,7 @@ public function testMoveTargetNotReachableByPiece() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/8/8/8/K7'); + $fen->set_castling('-'); $this->expectException(RulesException::class); $fen->move('Ka3'); } @@ -323,6 +328,7 @@ public function testPromotion() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/P7/8/8/8/8/8/K7'); + $fen->set_castling('-'); $fen->move('a8=Q'); $this->assertEquals('Qq5k/8/8/8/8/8/8/K7', $fen->get_board()); @@ -336,6 +342,7 @@ public function testPromotionNotSpecified() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/P7/8/8/8/8/8/K7'); + $fen->set_castling('-'); $this->expectException(RulesException::class); $fen->move('a8'); } @@ -344,6 +351,7 @@ public function testAmbiguousMoveException() : void { $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/8/8/N3N3/K7'); + $fen->set_castling('-'); $this->expectException(RulesException::class); $fen->move('Nc3'); } @@ -352,6 +360,7 @@ public function testAmbiguousMoveByFile() : void { $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/8/8/N3N3/K7'); + $fen->set_castling('-'); $fen->move('Nac3'); $this->assertEquals('1q5k/8/8/8/8/2N5/4N3/K7', $fen->get_board()); } @@ -360,6 +369,7 @@ public function testAmbiguousMoveByRank() : void { $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/4N3/8/4N3/K7'); + $fen->set_castling('-'); $fen->move('N4c3'); $this->assertEquals('1q5k/8/8/8/8/2N5/4N3/K7', $fen->get_board()); } @@ -369,6 +379,7 @@ public function testCaptureOnEmptySquareException() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/8/8/8/8/8/8/K7'); + $fen->set_castling('-'); $this->expectException(RulesException::class); $fen->move('Kxa2'); } @@ -412,17 +423,35 @@ public function testMovingRooksDismissCastling() : void $fen = new FEN; $fen->move('a4'); $fen->move('a5'); - $this->assertEquals('KQkq', $fen->get_castling_string()); + $this->assertEquals('KQkq', $fen->get_castling()); $fen->move('Ra3'); - $this->assertEquals('Kkq', $fen->get_castling_string()); + $this->assertEquals('Kkq', $fen->get_castling()); $fen->move('Ra7'); - $this->assertEquals('Kk', $fen->get_castling_string()); + $this->assertEquals('Kk', $fen->get_castling()); $fen->move('h4'); $fen->move('h5'); $fen->move('Rh2'); - $this->assertEquals('k', $fen->get_castling_string()); + $this->assertEquals('k', $fen->get_castling()); $fen->move('Rh6'); - $this->assertEquals('-', $fen->get_castling_string()); + $this->assertEquals('-', $fen->get_castling()); + + + $fen960 = new FEN('nqrkbrnb/pppppppp/8/8/8/8/PPPPPPPP/NQRKBRNB w CFcf -'); + $fen960->move('c4'); + $fen960->move('c5'); + $this->assertEquals('CFcf', $fen960->get_castling()); + $fen960->move('Rc3'); + $this->assertEquals('Fcf', $fen960->get_castling()); + $fen960->move('Rc7'); + $this->assertEquals('Ff', $fen960->get_castling()); + $fen960->move('f4'); + $fen960->move('f5'); + $this->assertEquals('Ff', $fen960->get_castling()); + $fen960->move('Rf2'); + $this->assertEquals('f', $fen960->get_castling()); + $fen960->move('Rf6'); + $this->assertEquals('-', $fen960->get_castling()); + } public function testBongcloud() : void @@ -430,11 +459,21 @@ public function testBongcloud() : void $fen = new FEN; $fen->move('e4'); $fen->move('e5'); - $this->assertEquals('KQkq', $fen->get_castling_string()); + $this->assertEquals('KQkq', $fen->get_castling()); $fen->move('Ke2'); - $this->assertEquals('kq', $fen->get_castling_string()); + $this->assertEquals('kq', $fen->get_castling()); $fen->move('Ke7'); - $this->assertEquals('-', $fen->get_castling_string()); + $this->assertEquals('-', $fen->get_castling()); + + $fen960 = new FEN('nqrkbrnb/pppppppp/8/8/8/8/PPPPPPPP/NQRKBRNB w CFcf -'); + $fen960->move('d4'); + $fen960->move('d5'); + $this->assertEquals('CFcf', $fen960->get_castling()); + $fen960->move('Kd2'); + $this->assertEquals('cf', $fen960->get_castling()); + $fen960->move('Kd7'); + $this->assertEquals('-', $fen960->get_castling()); + } public function testCastlingKingside() : void @@ -448,8 +487,24 @@ public function testCastlingKingside() : void $fen->move('Bg7'); $fen->move('O-O'); $fen->move('O-O'); - $this->assertEquals('-', $fen->get_castling_string()); + $this->assertEquals('-', $fen->get_castling()); $this->assertEquals('rnbq1rk1/ppppppbp/5np1/8/8/5NP1/PPPPPPBP/RNBQ1RK1', $fen->get_board()); + + $fen960 = new FEN('nqrkbrnb/pppppppp/8/8/8/8/PPPPPPPP/NQRKBRNB w CFcf -'); + $fen960->move('g4'); + $fen960->move('g5'); + $fen960->move('d4'); + $fen960->move('d5'); + $fen960->move('Bg2'); + $fen960->move('Bg7'); + $fen960->move('Bd2'); + $fen960->move('Bd7'); + $fen960->move('Nh3'); + $fen960->move('Nh6'); + $fen960->move('O-O'); + $fen960->move('O-O'); + $this->assertEquals('-', $fen960->get_castling()); + $this->assertEquals('nqr2rk1/pppbppbp/7n/3p2p1/3P2P1/7N/PPPBPPBP/NQR2RK1', $fen960->get_board()); } public function testCastlingQueenside() : void @@ -467,16 +522,22 @@ public function testCastlingQueenside() : void $fen->move('Qd7'); $fen->move('O-O-O'); $fen->move('O-O-O'); - $this->assertEquals('-', $fen->get_castling_string()); + $this->assertEquals('-', $fen->get_castling()); $this->assertEquals('2kr1bnr/pbpqpppp/1pn5/3p4/3P4/1PN5/PBPQPPPP/2KR1BNR', $fen->get_board()); + + $fen960 = new FEN('nqrkbrnb/pppppppp/8/8/8/8/PPPPPPPP/NQRKBRNB w CFcf -'); + $fen960->move('O-O-O'); + $fen960->move('O-O-O'); + $fen960 = new FEN('nqkrbrnb/pppppppp/8/8/8/8/PPPPPPPP/NQKRBRNB w - -'); + } public function testCastlingKingsideNotAvailableWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('Qkq'); $fen->set_board('r3k2r/8/8/8/8/8/8/R3K2R'); + $fen->set_castling('Qkq'); $this->expectException(RulesException::class); $fen->move('O-O'); } @@ -485,8 +546,8 @@ public function testCastlingKingsideNotAvailableBlack() : void { $fen = new FEN; $fen->set_active_color('b'); - $fen->set_castling_string('KQq'); $fen->set_board('r3k2r/8/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQq'); $this->expectException(RulesException::class); $fen->move('O-O'); } @@ -495,8 +556,8 @@ public function testCastlingQueensideNotAvailableWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('Kkq'); $fen->set_board('r3k2r/8/8/8/8/8/8/R3K2R'); + $fen->set_castling('Kkq'); $this->expectException(RulesException::class); $fen->move('O-O-O'); } @@ -505,88 +566,68 @@ public function testCastlingQueensideNotAvailableBlack() : void { $fen = new FEN; $fen->set_active_color('b'); - $fen->set_castling_string('KQk'); $fen->set_board('r3k2r/8/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQk'); $this->expectException(RulesException::class); $fen->move('O-O-O'); } - public function testCastlingKingsideNotAvailableKingMovedWhite() : void + // Capturing black's rook prevents castling + public function testCastlingKingsideBlackCapture() : void { - $fen = new FEN; - $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); - $fen->set_board('r4k1r/8/8/8/8/8/8/R4K1R'); + $fen = new FEN('r3k2r/8/8/4B3/4B3/8/8/4K3 w kq - 0 1'); + $fen->move('Bxh8'); + $this->assertEquals('q', $fen->get_castling()); $this->expectException(RulesException::class); $fen->move('O-O'); } - public function testCastlingKingsideNotAvailableKingMovedBlack() : void + // Capturing black's rook prevents castling + public function testCastlingKingsideBlackCapture2() : void { - $fen = new FEN; - $fen->set_active_color('b'); - $fen->set_castling_string('KQkq'); - $fen->set_board('r4k1r/8/8/8/8/8/8/R4K1R'); + $fen = new FEN('1rk1r3/8/8/1B2B3/8/8/8/1RK1R3 w be - 0 1'); + $fen->move('Bxe8'); + $this->assertEquals('b', $fen->get_castling()); $this->expectException(RulesException::class); $fen->move('O-O'); } - public function testCastlingQueensideNotAvailableKingMovedWhite() : void + // Capturing black's rook prevents castling + public function testCastlingQueensideBlackCapture() : void { - $fen = new FEN; - $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); - $fen->set_board('r4k1r/8/8/8/8/8/8/R4K1R'); + $fen = new FEN('r3k2r/8/8/4B3/4B3/8/8/4K3 w kq - 0 1'); + $fen->move('Bxa8'); + $this->assertEquals('k', $fen->get_castling()); $this->expectException(RulesException::class); $fen->move('O-O-O'); } - public function testCastlingQueensideNotAvailableKingMovedBlack() : void + // Capturing black's rook prevents castling + public function testCastlingQueensideBlackCapture2() : void { - $fen = new FEN; - $fen->set_active_color('b'); - $fen->set_castling_string('KQkq'); - $fen->set_board('r4k1r/8/8/8/8/8/8/R4K1R'); + $fen = new FEN('1rk1r3/8/8/1B2B3/8/8/8/1RK1R3 w be - 0 1'); + $fen->move('Bxb8'); + $this->assertEquals('e', $fen->get_castling()); $this->expectException(RulesException::class); $fen->move('O-O-O'); } - public function testCastlingKingsideNotAvailableRookMovedWhite() : void + // Capturing white's rook prevents castling + public function testCastlingKingsideWhiteCapture() : void { - $fen = new FEN; - $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); - $fen->set_board('r3k3/8/8/7r/7R/8/8/R3K3'); + $fen = new FEN('4k3/8/8/4b3/4b3/8/8/R3K2R b KQ - 0 1'); + $fen->move('Bxh1'); + $this->assertEquals('Q', $fen->get_castling()); $this->expectException(RulesException::class); $fen->move('O-O'); } - public function testCastlingKingsideNotAvailableRookMovedBlack() : void + // Capturing white's rook prevents castling + public function testCastlingQueensideWhiteCapture() : void { - $fen = new FEN; - $fen->set_active_color('b'); - $fen->set_castling_string('KQkq'); - $fen->set_board('r3k3/8/8/7r/7R/8/8/R3K3'); - $this->expectException(RulesException::class); - $fen->move('O-O'); - } - - public function testCastlingQueensideNotAvailableRookMovedWhite() : void - { - $fen = new FEN; - $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); - $fen->set_board('4k2r/r7/8/8/8/8/R7/4K2R'); - $this->expectException(RulesException::class); - $fen->move('O-O-O'); - } - - public function testCastlingQueensideNotAvailableRookMovedBlack() : void - { - $fen = new FEN; - $fen->set_active_color('b'); - $fen->set_castling_string('KQkq'); - $fen->set_board('4k2r/r7/8/8/8/8/R7/4K2R'); + $fen = new FEN('4k3/8/8/4b3/4b3/8/8/R3K2R b KQ - 0 1'); + $fen->move('Bxa1'); + $this->assertEquals('K', $fen->get_castling()); $this->expectException(RulesException::class); $fen->move('O-O-O'); } @@ -595,8 +636,8 @@ public function testCastlingKingsidePiecesInWayWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); - $fen->set_board('rn2kb2/8/8/7r/7R/8/8/RN2KB2'); + $fen->set_board('rn2kb1r/8/8/8/8/8/8/RN2KB1R'); + $fen->set_castling('KQkq'); $this->expectException(RulesException::class); $fen->move('O-O'); } @@ -605,8 +646,8 @@ public function testCastlingQueensidePiecesInWayWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); $fen->set_board('rn2kb2/8/8/7r/7R/8/8/RN2KB2'); + $fen->set_castling('Qq'); $this->expectException(RulesException::class); $fen->move('O-O-O'); } @@ -615,8 +656,8 @@ public function testCastlingKingsideCheckWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); $fen->set_board('r3k2r/4q3/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQkq'); $this->expectException(RulesException::class); $fen->move('O-O'); } @@ -625,8 +666,8 @@ public function testCastlingQueensideCheckWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); $fen->set_board('r3k2r/4q3/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQkq'); $this->expectException(RulesException::class); $fen->move('O-O-O'); } @@ -635,8 +676,8 @@ public function testCastlingKingsideCheckOnTheWayWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); $fen->set_board('r3k2r/5q2/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQkq'); $this->expectException(RulesException::class); $fen->move('O-O'); } @@ -645,8 +686,8 @@ public function testCastlingQueensideCheckOnTheWayWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); $fen->set_board('r3k2r/3q4/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQkq'); $this->expectException(RulesException::class); $fen->move('O-O-O'); } @@ -655,8 +696,8 @@ public function testCastlingKingsideCheckOnTargetWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); $fen->set_board('r3k2r/6q1/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQkq'); $this->expectException(RulesException::class); $fen->move('O-O'); } @@ -665,8 +706,8 @@ public function testCastlingQueensideCheckOnTargetWhite() : void { $fen = new FEN; $fen->set_active_color('w'); - $fen->set_castling_string('KQkq'); $fen->set_board('r3k2r/2q5/8/8/8/8/8/R3K2R'); + $fen->set_castling('KQkq'); $this->expectException(RulesException::class); $fen->move('O-O-O'); } @@ -679,11 +720,13 @@ public function testPossibleMoves() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('7k/8/8/8/2n5/1P1P4/8/K7'); + $fen->set_castling('-'); $this->assertEqualsCanonicalizing(['Ka2', 'Kb1', 'bxc4', 'dxc4', 'b4', 'd4'], $fen->get_legal_moves()); $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('7k/8/8/8/2n5/1P6/8/K7'); + $fen->set_castling('-'); $this->assertEqualsCanonicalizing(['Ka2', 'Kb1', 'bxc4', 'b4'], $fen->get_legal_moves()); } @@ -692,16 +735,19 @@ public function testPossibleMovesAbiguious() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('2q4k/8/8/8/8/8/8/NK2N3'); + $fen->set_castling('-'); $this->assertEqualsCanonicalizing(['Ka2', 'Kb2', 'Nac2', 'Nec2', 'Nb3', 'Nd3', 'Nf3', 'Ng2'], $fen->get_legal_moves()); $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('2q4k/8/8/N7/8/8/8/NK6'); + $fen->set_castling('-'); $this->assertEqualsCanonicalizing(['Ka2', 'Kb2', 'N1b3', 'Nc2', 'N5b3', 'Nc4', 'Nc6', 'Nb7'], $fen->get_legal_moves()); $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('2q4k/8/8/8/8/N7/8/NK2N3'); + $fen->set_castling('-'); $this->assertEqualsCanonicalizing(['Ka2', 'Kb2', 'Na1c2', 'Na3c2', 'Ne1c2', 'Nb3', 'Nd3', 'Nf3', 'Ng2', 'Nc4', 'Nb5'], $fen->get_legal_moves()); } @@ -711,6 +757,7 @@ public function testPossibleMovesPromotion() : void $fen = new FEN; $fen->set_active_color('w'); $fen->set_board('1q5k/P7/8/8/8/8/8/K7'); + $fen->set_castling('-'); $this->assertEqualsCanonicalizing(['Ka2', 'a8=N', 'a8=B', 'a8=R', 'a8=Q', 'axb8=N', 'axb8=B', 'axb8=R', 'axb8=Q'], $fen->get_legal_moves()); } diff --git a/tests/unit/PGNTest.php b/tests/unit/PGNTest.php index 2555204..6eaaeca 100644 --- a/tests/unit/PGNTest.php +++ b/tests/unit/PGNTest.php @@ -24,7 +24,34 @@ final class PGNTest extends TestCase 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5 hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6 -Nf2 42. g4 Bd3 43. Re6 1/2-1/2' +Nf2 42. g4 Bd3 43. Re6 1/2-1/2', + +'[Event "Live Chess - Chess960"] +[Site "Chess.com"] +[Date "2021.10.20"] +[Round "?"] +[White "Oleksandr_Bortnyk"] +[Black "Hikaru"] +[Result "0-1"] +[Variant "Chess960"] +[SetUp "1"] +[FEN "bbrkqrnn/pppppppp/8/8/8/8/PPPPPPPP/BBRKQRNN w FCfc -"] +[WhiteElo "2836"] +[BlackElo "3123"] +[TimeControl "180"] +[EndTime "13:37:49 PDT"] +[Termination "Hikaru won by checkmate"] +[initialSetup "bbrkqrnn/pppppppp/8/8/8/8/PPPPPPPP/BBRKQRNN w FCfc -"] + +1. c4 Nf6 2. b3 c5 3. Ng3 b6 4. e4 Ng6 5. Nh3 e5 6. Nf5 Qe6 7. Ng5 Qc6 8. Nxg7 +h6 9. Nh3 Nxe4 10. Nf5 Nf6 11. f3 O-O-O 12. Nxh6 d5 13. Nxf7 Rxf7 14. Bxg6 Rg7 +15. Bf5+ Kb7 16. g4 Ka6 17. Bxe5 Re8 18. f4 Nxg4 19. Bxg7 Rxe1+ 20. Rxe1 d4 21. +Bxg4 Qg2 22. Rg1 Bf3+ 23. Kc2 Be4+ 24. Kd1 Qxh2 25. Be5 Bxe5 26. fxe5 Bg2 27. +Nf2 Qxg1+ 28. Kc2 Qxf2 29. Rd1 Qf4 30. Bc8+ Ka5 31. e6 Be4+ 32. Kc1 Bg6 33. Kb2 +Qe4 34. Ka3 b5 35. cxb5 Qc2 36. Rg1 Qxd2 37. e7 c4 38. b6 Qb4+ 39. Kb2 Qc3+ 40. +Ka3 cxb3 41. axb3 Qc5+ 42. Ka2 Be8 43. Rg2 Kxb6 44. Be6 d3 45. Rb2 d2 46. Rxd2 +Qa5+ 47. Kb2 Qxd2+ 48. Ka3 Qe3 49. Kb4 Qe4+ 50. Ka3 Qxe6 51. b4 Qxe7 52. Kb3 a5 +53. Ka3 Kb5 54. Ka2 Qxb4 55. Ka1 a4 56. Ka2 a3 57. Ka1 Qb2# 0-1' ]; @@ -153,4 +180,10 @@ public function testGetAllTags() : void ], $pgn->get_tags()); } + public function testChess960() : void + { + $pgn = new PGN($this->samples[1]); + $this->assertEquals('4b3/8/8/1k6/8/p7/1q6/K7 w - - 2 58', $pgn->get_current_fen()); + } + }