Skip to content

Commit

Permalink
Fix castling rights parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
veloce committed Oct 19, 2024
1 parent 0f00faa commit 0d1add7
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 71 deletions.
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
5 changes: 3 additions & 2 deletions lib/src/position.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
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 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

0 comments on commit 0d1add7

Please sign in to comment.