Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix castling rights parsing #43

Merged
merged 3 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.2

- Fixes castling rights parsing from FEN.

## 0.9.1

- Fixes bugs in the PGN parser.
Expand Down
34 changes: 17 additions & 17 deletions lib/src/castles.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,7 +22,7 @@ abstract class Castles {
}) = _Castles;

const Castles._({
required this.unmovedRooks,
required this.castlingRights,
Square? whiteRookQueenSide,
Square? whiteRookKingSide,
Square? blackRookQueenSide,
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -65,15 +65,15 @@ abstract class Castles {
);

static const empty = Castles(
unmovedRooks: SquareSet.empty,
castlingRights: SquareSet.empty,
whitePathQueenSide: SquareSet.empty,
whitePathKingSide: SquareSet.empty,
blackPathQueenSide: SquareSet.empty,
blackPathKingSide: SquareSet.empty,
);

static const horde = Castles(
unmovedRooks: SquareSet(0x8100000000000000),
castlingRights: SquareSet(0x8100000000000000),
blackRookKingSide: Square.h8,
blackRookQueenSide: Square.a8,
whitePathKingSide: SquareSet.empty,
Expand All @@ -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);
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 &&
Expand All @@ -238,7 +238,7 @@ abstract class Castles {

@override
int get hashCode => Object.hash(
unmovedRooks,
castlingRights,
_whiteRookQueenSide,
_whiteRookKingSide,
_blackRookQueenSide,
Expand All @@ -249,7 +249,7 @@ abstract class Castles {
_blackPathKingSide);

Castles copyWith({
SquareSet? unmovedRooks,
SquareSet? castlingRights,
Square? whiteRookQueenSide,
Square? whiteRookKingSide,
Square? blackRookQueenSide,
Expand Down Expand Up @@ -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,
Expand All @@ -300,7 +300,7 @@ class _Castles extends Castles {

@override
Castles copyWith({
SquareSet? unmovedRooks,
SquareSet? castlingRights,
Object? whiteRookQueenSide = _uniqueObjectInstance,
Object? whiteRookKingSide = _uniqueObjectInstance,
Object? blackRookQueenSide = _uniqueObjectInstance,
Expand All @@ -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?,
Expand Down
4 changes: 2 additions & 2 deletions lib/src/position.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,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,
Expand Down Expand Up @@ -1739,7 +1739,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,
Expand Down
75 changes: 35 additions & 40 deletions lib/src/setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -131,7 +131,7 @@ class Setup {
board: board,
pockets: pockets,
turn: turn,
unmovedRooks: unmovedRooks,
castlingRights: castlingRights,
epSquare: epSquare,
halfmoves: halfmoves,
fullmoves: fullmoves,
Expand All @@ -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.
///
Expand All @@ -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,
);
Expand All @@ -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)),
Expand All @@ -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;
Expand All @@ -204,7 +204,7 @@ class Setup {
int get hashCode => Object.hash(
board,
turn,
unmovedRooks,
castlingRights,
epSquare,
halfmoves,
fullmoves,
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion test/castles_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading