Fix castling rights parsing

This commit is contained in:
Vincent Velociter
2024-10-19 15:05:05 +02:00
parent 0f00faafe2
commit 0d1add722b
6 changed files with 138 additions and 71 deletions
+17 -17
View File
@@ -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?,
+3 -2
View File
@@ -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<T extends Position<T>> {
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<ThreeCheck> {
return Setup(
board: board,
turn: turn,
unmovedRooks: castles.unmovedRooks,
castlingRights: castles.castlingRights,
epSquare: _legalEpSquare(),
halfmoves: halfmoves,
fullmoves: fullmoves,
+35 -40
View File
@@ -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<Square> 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) {
+1 -1
View File
@@ -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);
+75 -5
View File
@@ -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);
+7 -6
View File
@@ -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);