From 0d1add722b35f1ca395375083f1073d950ae3528 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 19 Oct 2024 15:05:05 +0200 Subject: [PATCH 1/3] Fix castling rights parsing --- lib/src/castles.dart | 34 +++++++++--------- lib/src/position.dart | 5 +-- lib/src/setup.dart | 75 ++++++++++++++++++-------------------- test/castles_test.dart | 2 +- test/position_test.dart | 80 ++++++++++++++++++++++++++++++++++++++--- test/setup_test.dart | 13 +++---- 6 files changed, 138 insertions(+), 71 deletions(-) diff --git a/lib/src/castles.dart b/lib/src/castles.dart index b7853d9..503e94f 100644 --- a/lib/src/castles.dart +++ b/lib/src/castles.dart @@ -10,7 +10,7 @@ import 'square_set.dart'; abstract class Castles { /// Creates a new [Castles] instance. const factory Castles({ - required SquareSet unmovedRooks, + required SquareSet castlingRights, Square? whiteRookQueenSide, Square? whiteRookKingSide, Square? blackRookQueenSide, @@ -22,7 +22,7 @@ abstract class Castles { }) = _Castles; const Castles._({ - required this.unmovedRooks, + required this.castlingRights, Square? whiteRookQueenSide, Square? whiteRookKingSide, Square? blackRookQueenSide, @@ -41,7 +41,7 @@ abstract class Castles { _blackPathKingSide = blackPathKingSide; /// SquareSet of rooks that have not moved yet. - final SquareSet unmovedRooks; + final SquareSet castlingRights; final Square? _whiteRookQueenSide; final Square? _whiteRookKingSide; @@ -53,7 +53,7 @@ abstract class Castles { final SquareSet _blackPathKingSide; static const standard = Castles( - unmovedRooks: SquareSet.corners, + castlingRights: SquareSet.corners, whiteRookQueenSide: Square.a1, whiteRookKingSide: Square.h1, blackRookQueenSide: Square.a8, @@ -65,7 +65,7 @@ abstract class Castles { ); static const empty = Castles( - unmovedRooks: SquareSet.empty, + castlingRights: SquareSet.empty, whitePathQueenSide: SquareSet.empty, whitePathKingSide: SquareSet.empty, blackPathQueenSide: SquareSet.empty, @@ -73,7 +73,7 @@ abstract class Castles { ); static const horde = Castles( - unmovedRooks: SquareSet(0x8100000000000000), + castlingRights: SquareSet(0x8100000000000000), blackRookKingSide: Square.h8, blackRookQueenSide: Square.a8, whitePathKingSide: SquareSet.empty, @@ -85,7 +85,7 @@ abstract class Castles { /// Creates a [Castles] instance from a [Setup]. factory Castles.fromSetup(Setup setup) { Castles castles = Castles.empty; - final rooks = setup.unmovedRooks & setup.board.rooks; + final rooks = setup.castlingRights & setup.board.rooks; for (final side in Side.values) { final backrank = SquareSet.backrankOf(side); final king = setup.board.kingOf(side); @@ -161,7 +161,7 @@ abstract class Castles { /// Returns a new [Castles] instance with the given rook discarded. Castles discardRookAt(Square square) { return copyWith( - unmovedRooks: unmovedRooks.withoutSquare(square), + castlingRights: castlingRights.withoutSquare(square), whiteRookQueenSide: _whiteRookQueenSide == square ? null : _whiteRookQueenSide, whiteRookKingSide: @@ -176,7 +176,7 @@ abstract class Castles { /// Returns a new [Castles] instance with the given side discarded. Castles discardSide(Side side) { return copyWith( - unmovedRooks: unmovedRooks.diff(SquareSet.backrankOf(side)), + castlingRights: castlingRights.diff(SquareSet.backrankOf(side)), whiteRookQueenSide: side == Side.white ? null : _whiteRookQueenSide, whiteRookKingSide: side == Side.white ? null : _whiteRookKingSide, blackRookQueenSide: side == Side.black ? null : _blackRookQueenSide, @@ -193,7 +193,7 @@ abstract class Castles { .withoutSquare(king) .withoutSquare(rook); return copyWith( - unmovedRooks: unmovedRooks.withSquare(rook), + castlingRights: castlingRights.withSquare(rook), whiteRookQueenSide: side == Side.white && cs == CastlingSide.queen ? rook : _whiteRookQueenSide, @@ -219,14 +219,14 @@ abstract class Castles { @override String toString() { - return 'Castles(unmovedRooks: ${unmovedRooks.toHexString()})'; + return 'Castles(castlingRights: ${castlingRights.toHexString()})'; } @override bool operator ==(Object other) => identical(this, other) || other is Castles && - other.unmovedRooks == unmovedRooks && + other.castlingRights == castlingRights && other._whiteRookQueenSide == _whiteRookQueenSide && other._whiteRookKingSide == _whiteRookKingSide && other._blackRookQueenSide == _blackRookQueenSide && @@ -238,7 +238,7 @@ abstract class Castles { @override int get hashCode => Object.hash( - unmovedRooks, + castlingRights, _whiteRookQueenSide, _whiteRookKingSide, _blackRookQueenSide, @@ -249,7 +249,7 @@ abstract class Castles { _blackPathKingSide); Castles copyWith({ - SquareSet? unmovedRooks, + SquareSet? castlingRights, Square? whiteRookQueenSide, Square? whiteRookKingSide, Square? blackRookQueenSide, @@ -287,7 +287,7 @@ Square kingCastlesTo(Side side, CastlingSide cs) => switch (side) { class _Castles extends Castles { const _Castles({ - required super.unmovedRooks, + required super.castlingRights, super.whiteRookQueenSide, super.whiteRookKingSide, super.blackRookQueenSide, @@ -300,7 +300,7 @@ class _Castles extends Castles { @override Castles copyWith({ - SquareSet? unmovedRooks, + SquareSet? castlingRights, Object? whiteRookQueenSide = _uniqueObjectInstance, Object? whiteRookKingSide = _uniqueObjectInstance, Object? blackRookQueenSide = _uniqueObjectInstance, @@ -311,7 +311,7 @@ class _Castles extends Castles { SquareSet? blackPathKingSide, }) { return _Castles( - unmovedRooks: unmovedRooks ?? this.unmovedRooks, + castlingRights: castlingRights ?? this.castlingRights, whiteRookQueenSide: whiteRookQueenSide == _uniqueObjectInstance ? _whiteRookQueenSide : whiteRookQueenSide as Square?, diff --git a/lib/src/position.dart b/lib/src/position.dart index 077a2e8..5604fb9 100644 --- a/lib/src/position.dart +++ b/lib/src/position.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'attacks.dart'; import 'castles.dart'; +import 'debug.dart'; import 'models.dart'; import 'board.dart'; import 'setup.dart'; @@ -130,7 +131,7 @@ abstract class Position> { board: board, pockets: pockets, turn: turn, - unmovedRooks: castles.unmovedRooks, + castlingRights: castles.castlingRights, epSquare: _legalEpSquare(), halfmoves: halfmoves, fullmoves: fullmoves, @@ -1739,7 +1740,7 @@ abstract class ThreeCheck extends Position { return Setup( board: board, turn: turn, - unmovedRooks: castles.unmovedRooks, + castlingRights: castles.castlingRights, epSquare: _legalEpSquare(), halfmoves: halfmoves, fullmoves: fullmoves, diff --git a/lib/src/setup.dart b/lib/src/setup.dart index 9306bf4..826d68f 100644 --- a/lib/src/setup.dart +++ b/lib/src/setup.dart @@ -13,7 +13,7 @@ class Setup { required this.board, this.pockets, required this.turn, - required this.unmovedRooks, + required this.castlingRights, this.epSquare, required this.halfmoves, required this.fullmoves, @@ -73,12 +73,12 @@ class Setup { } // Castling - SquareSet unmovedRooks; + SquareSet castlingRights; if (parts.isEmpty) { - unmovedRooks = SquareSet.empty; + castlingRights = SquareSet.empty; } else { final castlingPart = parts.removeAt(0); - unmovedRooks = _parseCastlingFen(board, castlingPart); + castlingRights = _parseCastlingFen(board, castlingPart); } // En passant square @@ -131,7 +131,7 @@ class Setup { board: board, pockets: pockets, turn: turn, - unmovedRooks: unmovedRooks, + castlingRights: castlingRights, epSquare: epSquare, halfmoves: halfmoves, fullmoves: fullmoves, @@ -149,7 +149,7 @@ class Setup { final Side turn; /// Unmoved rooks positions used to determine castling rights. - final SquareSet unmovedRooks; + final SquareSet castlingRights; /// En passant target square. /// @@ -169,7 +169,7 @@ class Setup { static const standard = Setup( board: Board.standard, turn: Side.white, - unmovedRooks: SquareSet.corners, + castlingRights: SquareSet.corners, halfmoves: 0, fullmoves: 1, ); @@ -181,7 +181,7 @@ class Setup { String get fen => [ board.fen + (pockets != null ? _makePockets(pockets!) : ''), turnLetter, - _makeCastlingFen(board, unmovedRooks), + _makeCastlingFen(board, castlingRights), if (epSquare != null) epSquare!.name else '-', if (remainingChecks != null) _makeRemainingChecks(remainingChecks!), math.max(0, math.min(halfmoves, 9999)), @@ -194,7 +194,7 @@ class Setup { other is Setup && other.board == board && other.turn == turn && - other.unmovedRooks == unmovedRooks && + other.castlingRights == castlingRights && other.epSquare == epSquare && other.halfmoves == halfmoves && other.fullmoves == fullmoves; @@ -204,7 +204,7 @@ class Setup { int get hashCode => Object.hash( board, turn, - unmovedRooks, + castlingRights, epSquare, halfmoves, fullmoves, @@ -312,43 +312,38 @@ Pockets _parsePockets(String pocketPart) { } SquareSet _parseCastlingFen(Board board, String castlingPart) { - SquareSet unmovedRooks = SquareSet.empty; + SquareSet castlingRights = SquareSet.empty; if (castlingPart == '-') { - return unmovedRooks; + return castlingRights; } - for (int i = 0; i < castlingPart.length; i++) { - final c = castlingPart[i]; + for (final rune in castlingPart.runes) { + final c = String.fromCharCode(rune); final lower = c.toLowerCase(); - final color = c == lower ? Side.black : Side.white; - final backrankMask = SquareSet.backrankOf(color); - final backrank = backrankMask & board.bySide(color); - - Iterable candidates; - if (lower == 'q') { - candidates = backrank.squares; - } else if (lower == 'k') { - candidates = backrank.squaresReversed; - } else if ('a'.compareTo(lower) <= 0 && lower.compareTo('h') <= 0) { - candidates = - (SquareSet.fromFile(File(lower.codeUnitAt(0) - 'a'.codeUnitAt(0))) & - backrank) - .squares; + final lowerCode = lower.codeUnitAt(0); + final side = c == lower ? Side.black : Side.white; + final rank = side == Side.white ? Rank.first : Rank.eighth; + if ('a'.codeUnitAt(0) <= lowerCode && lowerCode <= 'h'.codeUnitAt(0)) { + castlingRights = castlingRights.withSquare( + Square.fromCoords(File(lowerCode - 'a'.codeUnitAt(0)), rank)); + } else if (lower == 'k' || lower == 'q') { + final rooksAndKings = (board.bySide(side) & SquareSet.backrankOf(side)) & + (board.rooks | board.kings); + final candidate = lower == 'k' + ? rooksAndKings.squares.lastOrNull + : rooksAndKings.squares.firstOrNull; + castlingRights = castlingRights.withSquare( + candidate != null && board.rooks.has(candidate) + ? candidate + : Square.fromCoords(lower == 'k' ? File.h : File.a, rank)); } else { throw const FenException(IllegalFenCause.castling); } - for (final square in candidates) { - if (board.kings.has(square)) break; - if (board.rooks.has(square)) { - unmovedRooks = unmovedRooks.withSquare(square); - break; - } - } } - if ((const SquareSet.fromRank(Rank.first) & unmovedRooks).size > 2 || - (const SquareSet.fromRank(Rank.eighth) & unmovedRooks).size > 2) { + if (Side.values.any((color) => + SquareSet.backrankOf(color).intersect(castlingRights).size > 2)) { throw const FenException(IllegalFenCause.castling); } - return unmovedRooks; + return castlingRights; } String _makePockets(Pockets pockets) { @@ -363,14 +358,14 @@ String _makePockets(Pockets pockets) { return '[${wPart.toUpperCase()}$bPart]'; } -String _makeCastlingFen(Board board, SquareSet unmovedRooks) { +String _makeCastlingFen(Board board, SquareSet castlingRights) { final buffer = StringBuffer(); for (final color in Side.values) { final backrank = SquareSet.backrankOf(color); final king = board.kingOf(color); final candidates = board.byPiece(Piece(color: color, role: Role.rook)) & backrank; - for (final rook in (unmovedRooks & candidates).squaresReversed) { + for (final rook in (castlingRights & backrank).squaresReversed) { if (rook == candidates.first && king != null && rook < king) { buffer.write(color == Side.white ? 'Q' : 'q'); } else if (rook == candidates.last && king != null && king < rook) { diff --git a/test/castles_test.dart b/test/castles_test.dart index a4b1dff..552bf25 100644 --- a/test/castles_test.dart +++ b/test/castles_test.dart @@ -18,7 +18,7 @@ void main() { }); test('fromSetup', () { final castles = Castles.fromSetup(Setup.standard); - expect(castles.unmovedRooks, SquareSet.corners); + expect(castles.castlingRights, SquareSet.corners); expect(castles, Castles.standard); expect(castles.rookOf(Side.white, CastlingSide.queen), Square.a1); diff --git a/test/position_test.dart b/test/position_test.dart index 1fb65c2..edeb481 100644 --- a/test/position_test.dart +++ b/test/position_test.dart @@ -13,12 +13,12 @@ void main() { test('Chess.toString()', () { expect(Chess.initial.toString(), - 'Chess(board: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR, turn: Side.white, castles: Castles(unmovedRooks: 0x8100000000000081), halfmoves: 0, fullmoves: 1)'); + 'Chess(board: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR, turn: Side.white, castles: Castles(castlingRights: 0x8100000000000081), halfmoves: 0, fullmoves: 1)'); }); test('Antichess.toString()', () { expect(Antichess.initial.toString(), - 'Antichess(board: $kInitialBoardFEN, turn: Side.white, castles: Castles(unmovedRooks: 0), halfmoves: 0, fullmoves: 1)'); + 'Antichess(board: $kInitialBoardFEN, turn: Side.white, castles: Castles(castlingRights: 0), halfmoves: 0, fullmoves: 1)'); }); test('ply', () { @@ -397,6 +397,76 @@ void main() { expect(pos.legalMovesOf(Square.e1), const SquareSet(0x00000000000000A9)); }); + test('castling chess960 legal moves', () { + for (final fen in [ + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R w KQkq - 0 1', + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R w CHch - 0 1', + ]) { + final pos = Chess.fromSetup(Setup.parseFen(fen)); + expect(pos.legalMovesOf(Square.f1), makeSquareSet(''' +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . 1 . 1 . 1 1 +''')); + } + + for (final fen in [ + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R b KQkq - 0 1', + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R b CHch - 0 1', + ]) { + final pos = Chess.fromSetup(Setup.parseFen(fen)); + expect(pos.legalMovesOf(Square.f8), makeSquareSet(''' +. . 1 . 1 . 1 1 +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +''')); + } + + for (final fen in [ + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R w Qkq - 0 1', + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R w Cch - 0 1', + ]) { + final pos = Chess.fromSetup(Setup.parseFen(fen)); + expect(pos.legalMovesOf(Square.f1), makeSquareSet(''' +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . 1 . 1 . 1 . +''')); + } + + for (final fen in [ + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R b k - 0 1', + '1qr2k1r/pppppppp/6n1/8/8/5BN1/PPPPPPPP/1QR2K1R b h - 0 1', + ]) { + final pos = Chess.fromSetup(Setup.parseFen(fen)); + expect(pos.legalMovesOf(Square.f8), makeSquareSet(''' +. . . . 1 . 1 1 +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +''')); + } + }); + test('isCheck', () { expect( Chess.fromSetup(Setup.parseFen( @@ -598,7 +668,7 @@ void main() { CastlingSide.queen: Square.a1, CastlingSide.king: null }))); - expect(pos.castles.unmovedRooks.has(Square.h1), false); + expect(pos.castles.castlingRights.has(Square.h1), false); }); test('capturing a rook removes castling right', () { @@ -607,7 +677,7 @@ void main() { .play(const NormalMove(from: Square.g7, to: Square.a1)); expect(pos.castles.rookOf(Side.white, CastlingSide.queen), isNull); expect(pos.castles.rookOf(Side.white, CastlingSide.king), Square.h1); - expect(pos.castles.unmovedRooks.has(Square.a1), false); + expect(pos.castles.castlingRights.has(Square.a1), false); }); test('king captures unmoved rook', () { @@ -639,7 +709,7 @@ void main() { expect(pos.board.pieceAt(Square.g1), Piece.whiteKing); expect(pos.board.pieceAt(Square.f1), Piece.whiteRook); expect( - pos.castles.unmovedRooks + pos.castles.castlingRights .isIntersected(const SquareSet.fromRank(Rank.first)), false); expect(pos.castles.rookOf(Side.white, CastlingSide.king), isNull); diff --git a/test/setup_test.dart b/test/setup_test.dart index b6dfef4..095dd6e 100644 --- a/test/setup_test.dart +++ b/test/setup_test.dart @@ -14,18 +14,18 @@ void main() { test('parse castling fen, standard initial board', () { expect( Setup.parseFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq') - .unmovedRooks, + .castlingRights, SquareSet.corners); expect( Setup.parseFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w -') - .unmovedRooks, + .castlingRights, SquareSet.empty); }); test('parse castling fen, shredder notation', () { expect( Setup.parseFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha') - .unmovedRooks, + .castlingRights, SquareSet.corners); }); @@ -33,7 +33,7 @@ void main() { expect( () => Setup.parseFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w BGL') - .unmovedRooks, + .castlingRights, throwsA(predicate( (e) => e is FenException && e.cause == IllegalFenCause.castling))); }); @@ -52,7 +52,7 @@ void main() { expect(setup, Setup.standard); expect(setup.board, Board.standard); expect(setup.turn, Side.white); - expect(setup.unmovedRooks, SquareSet.corners); + expect(setup.castlingRights, SquareSet.corners); expect(setup.epSquare, null); expect(setup.halfmoves, 0); expect(setup.fullmoves, 1); @@ -62,7 +62,7 @@ void main() { final setup = Setup.parseFen(kInitialBoardFEN); expect(setup.board, Board.standard); expect(setup.turn, Side.white); - expect(setup.unmovedRooks, SquareSet.empty); + expect(setup.castlingRights, SquareSet.empty); expect(setup.epSquare, null); expect(setup.halfmoves, 0); expect(setup.fullmoves, 1); @@ -95,6 +95,7 @@ void main() { 'rnb1kbnr/ppp1pppp/2Pp2PP/1P3PPP/PPP1PPPP/PPP1PPPP/PPP1PPP1/PPPqPP2 w kq - 0 1', '5b1r/1p5p/4ppp1/4Bn2/1PPP1PP1/4P2P/3k4/4K2R w K - 1 1', 'rnbqkb1r/p1p1nppp/2Pp4/3P1PP1/PPPPPP1P/PPP1PPPP/PPPnbqkb/PPPPPPPP w ha - 1 6', + 'rnbNRbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQhb - 2 3', ]) { final setup = Setup.parseFen(fen); expect(setup.fen, fen); From 88564d9cbea083113163f126f21fb73e9a73135a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 19 Oct 2024 15:07:49 +0200 Subject: [PATCH 2/3] Bump version --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b0df0..66f0e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2 + +- Fixes castling rights parsing from FEN. + ## 0.9.1 - Fixes bugs in the PGN parser. diff --git a/pubspec.yaml b/pubspec.yaml index 0b24d82..58fa1f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: dartchess description: Provides chess and chess variants rules and operations including chess move generation, read and write FEN, read and write PGN. repository: https://github.com/lichess-org/dartchess -version: 0.9.1 +version: 0.9.2 platforms: android: ios: From e46db9ec241c2b3b1b7fea1692ca7ff65f66e558 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 19 Oct 2024 15:08:19 +0200 Subject: [PATCH 3/3] Remove unused import --- lib/src/position.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/position.dart b/lib/src/position.dart index 5604fb9..291596a 100644 --- a/lib/src/position.dart +++ b/lib/src/position.dart @@ -3,7 +3,6 @@ import 'dart:math' as math; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'attacks.dart'; import 'castles.dart'; -import 'debug.dart'; import 'models.dart'; import 'board.dart'; import 'setup.dart';