Skip to content

Commit ae44570

Browse files
authored
Merge pull request #43 from lichess-org/fix_castling_right_parsing
Fix castling rights parsing
2 parents 0f00faa + e46db9e commit ae44570

File tree

8 files changed

+142
-72
lines changed

8 files changed

+142
-72
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.2
2+
3+
- Fixes castling rights parsing from FEN.
4+
15
## 0.9.1
26

37
- Fixes bugs in the PGN parser.

lib/src/castles.dart

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'square_set.dart';
1010
abstract class Castles {
1111
/// Creates a new [Castles] instance.
1212
const factory Castles({
13-
required SquareSet unmovedRooks,
13+
required SquareSet castlingRights,
1414
Square? whiteRookQueenSide,
1515
Square? whiteRookKingSide,
1616
Square? blackRookQueenSide,
@@ -22,7 +22,7 @@ abstract class Castles {
2222
}) = _Castles;
2323

2424
const Castles._({
25-
required this.unmovedRooks,
25+
required this.castlingRights,
2626
Square? whiteRookQueenSide,
2727
Square? whiteRookKingSide,
2828
Square? blackRookQueenSide,
@@ -41,7 +41,7 @@ abstract class Castles {
4141
_blackPathKingSide = blackPathKingSide;
4242

4343
/// SquareSet of rooks that have not moved yet.
44-
final SquareSet unmovedRooks;
44+
final SquareSet castlingRights;
4545

4646
final Square? _whiteRookQueenSide;
4747
final Square? _whiteRookKingSide;
@@ -53,7 +53,7 @@ abstract class Castles {
5353
final SquareSet _blackPathKingSide;
5454

5555
static const standard = Castles(
56-
unmovedRooks: SquareSet.corners,
56+
castlingRights: SquareSet.corners,
5757
whiteRookQueenSide: Square.a1,
5858
whiteRookKingSide: Square.h1,
5959
blackRookQueenSide: Square.a8,
@@ -65,15 +65,15 @@ abstract class Castles {
6565
);
6666

6767
static const empty = Castles(
68-
unmovedRooks: SquareSet.empty,
68+
castlingRights: SquareSet.empty,
6969
whitePathQueenSide: SquareSet.empty,
7070
whitePathKingSide: SquareSet.empty,
7171
blackPathQueenSide: SquareSet.empty,
7272
blackPathKingSide: SquareSet.empty,
7373
);
7474

7575
static const horde = Castles(
76-
unmovedRooks: SquareSet(0x8100000000000000),
76+
castlingRights: SquareSet(0x8100000000000000),
7777
blackRookKingSide: Square.h8,
7878
blackRookQueenSide: Square.a8,
7979
whitePathKingSide: SquareSet.empty,
@@ -85,7 +85,7 @@ abstract class Castles {
8585
/// Creates a [Castles] instance from a [Setup].
8686
factory Castles.fromSetup(Setup setup) {
8787
Castles castles = Castles.empty;
88-
final rooks = setup.unmovedRooks & setup.board.rooks;
88+
final rooks = setup.castlingRights & setup.board.rooks;
8989
for (final side in Side.values) {
9090
final backrank = SquareSet.backrankOf(side);
9191
final king = setup.board.kingOf(side);
@@ -161,7 +161,7 @@ abstract class Castles {
161161
/// Returns a new [Castles] instance with the given rook discarded.
162162
Castles discardRookAt(Square square) {
163163
return copyWith(
164-
unmovedRooks: unmovedRooks.withoutSquare(square),
164+
castlingRights: castlingRights.withoutSquare(square),
165165
whiteRookQueenSide:
166166
_whiteRookQueenSide == square ? null : _whiteRookQueenSide,
167167
whiteRookKingSide:
@@ -176,7 +176,7 @@ abstract class Castles {
176176
/// Returns a new [Castles] instance with the given side discarded.
177177
Castles discardSide(Side side) {
178178
return copyWith(
179-
unmovedRooks: unmovedRooks.diff(SquareSet.backrankOf(side)),
179+
castlingRights: castlingRights.diff(SquareSet.backrankOf(side)),
180180
whiteRookQueenSide: side == Side.white ? null : _whiteRookQueenSide,
181181
whiteRookKingSide: side == Side.white ? null : _whiteRookKingSide,
182182
blackRookQueenSide: side == Side.black ? null : _blackRookQueenSide,
@@ -193,7 +193,7 @@ abstract class Castles {
193193
.withoutSquare(king)
194194
.withoutSquare(rook);
195195
return copyWith(
196-
unmovedRooks: unmovedRooks.withSquare(rook),
196+
castlingRights: castlingRights.withSquare(rook),
197197
whiteRookQueenSide: side == Side.white && cs == CastlingSide.queen
198198
? rook
199199
: _whiteRookQueenSide,
@@ -219,14 +219,14 @@ abstract class Castles {
219219

220220
@override
221221
String toString() {
222-
return 'Castles(unmovedRooks: ${unmovedRooks.toHexString()})';
222+
return 'Castles(castlingRights: ${castlingRights.toHexString()})';
223223
}
224224

225225
@override
226226
bool operator ==(Object other) =>
227227
identical(this, other) ||
228228
other is Castles &&
229-
other.unmovedRooks == unmovedRooks &&
229+
other.castlingRights == castlingRights &&
230230
other._whiteRookQueenSide == _whiteRookQueenSide &&
231231
other._whiteRookKingSide == _whiteRookKingSide &&
232232
other._blackRookQueenSide == _blackRookQueenSide &&
@@ -238,7 +238,7 @@ abstract class Castles {
238238

239239
@override
240240
int get hashCode => Object.hash(
241-
unmovedRooks,
241+
castlingRights,
242242
_whiteRookQueenSide,
243243
_whiteRookKingSide,
244244
_blackRookQueenSide,
@@ -249,7 +249,7 @@ abstract class Castles {
249249
_blackPathKingSide);
250250

251251
Castles copyWith({
252-
SquareSet? unmovedRooks,
252+
SquareSet? castlingRights,
253253
Square? whiteRookQueenSide,
254254
Square? whiteRookKingSide,
255255
Square? blackRookQueenSide,
@@ -287,7 +287,7 @@ Square kingCastlesTo(Side side, CastlingSide cs) => switch (side) {
287287

288288
class _Castles extends Castles {
289289
const _Castles({
290-
required super.unmovedRooks,
290+
required super.castlingRights,
291291
super.whiteRookQueenSide,
292292
super.whiteRookKingSide,
293293
super.blackRookQueenSide,
@@ -300,7 +300,7 @@ class _Castles extends Castles {
300300

301301
@override
302302
Castles copyWith({
303-
SquareSet? unmovedRooks,
303+
SquareSet? castlingRights,
304304
Object? whiteRookQueenSide = _uniqueObjectInstance,
305305
Object? whiteRookKingSide = _uniqueObjectInstance,
306306
Object? blackRookQueenSide = _uniqueObjectInstance,
@@ -311,7 +311,7 @@ class _Castles extends Castles {
311311
SquareSet? blackPathKingSide,
312312
}) {
313313
return _Castles(
314-
unmovedRooks: unmovedRooks ?? this.unmovedRooks,
314+
castlingRights: castlingRights ?? this.castlingRights,
315315
whiteRookQueenSide: whiteRookQueenSide == _uniqueObjectInstance
316316
? _whiteRookQueenSide
317317
: whiteRookQueenSide as Square?,

lib/src/position.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ abstract class Position<T extends Position<T>> {
130130
board: board,
131131
pockets: pockets,
132132
turn: turn,
133-
unmovedRooks: castles.unmovedRooks,
133+
castlingRights: castles.castlingRights,
134134
epSquare: _legalEpSquare(),
135135
halfmoves: halfmoves,
136136
fullmoves: fullmoves,
@@ -1739,7 +1739,7 @@ abstract class ThreeCheck extends Position<ThreeCheck> {
17391739
return Setup(
17401740
board: board,
17411741
turn: turn,
1742-
unmovedRooks: castles.unmovedRooks,
1742+
castlingRights: castles.castlingRights,
17431743
epSquare: _legalEpSquare(),
17441744
halfmoves: halfmoves,
17451745
fullmoves: fullmoves,

lib/src/setup.dart

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Setup {
1313
required this.board,
1414
this.pockets,
1515
required this.turn,
16-
required this.unmovedRooks,
16+
required this.castlingRights,
1717
this.epSquare,
1818
required this.halfmoves,
1919
required this.fullmoves,
@@ -73,12 +73,12 @@ class Setup {
7373
}
7474

7575
// Castling
76-
SquareSet unmovedRooks;
76+
SquareSet castlingRights;
7777
if (parts.isEmpty) {
78-
unmovedRooks = SquareSet.empty;
78+
castlingRights = SquareSet.empty;
7979
} else {
8080
final castlingPart = parts.removeAt(0);
81-
unmovedRooks = _parseCastlingFen(board, castlingPart);
81+
castlingRights = _parseCastlingFen(board, castlingPart);
8282
}
8383

8484
// En passant square
@@ -131,7 +131,7 @@ class Setup {
131131
board: board,
132132
pockets: pockets,
133133
turn: turn,
134-
unmovedRooks: unmovedRooks,
134+
castlingRights: castlingRights,
135135
epSquare: epSquare,
136136
halfmoves: halfmoves,
137137
fullmoves: fullmoves,
@@ -149,7 +149,7 @@ class Setup {
149149
final Side turn;
150150

151151
/// Unmoved rooks positions used to determine castling rights.
152-
final SquareSet unmovedRooks;
152+
final SquareSet castlingRights;
153153

154154
/// En passant target square.
155155
///
@@ -169,7 +169,7 @@ class Setup {
169169
static const standard = Setup(
170170
board: Board.standard,
171171
turn: Side.white,
172-
unmovedRooks: SquareSet.corners,
172+
castlingRights: SquareSet.corners,
173173
halfmoves: 0,
174174
fullmoves: 1,
175175
);
@@ -181,7 +181,7 @@ class Setup {
181181
String get fen => [
182182
board.fen + (pockets != null ? _makePockets(pockets!) : ''),
183183
turnLetter,
184-
_makeCastlingFen(board, unmovedRooks),
184+
_makeCastlingFen(board, castlingRights),
185185
if (epSquare != null) epSquare!.name else '-',
186186
if (remainingChecks != null) _makeRemainingChecks(remainingChecks!),
187187
math.max(0, math.min(halfmoves, 9999)),
@@ -194,7 +194,7 @@ class Setup {
194194
other is Setup &&
195195
other.board == board &&
196196
other.turn == turn &&
197-
other.unmovedRooks == unmovedRooks &&
197+
other.castlingRights == castlingRights &&
198198
other.epSquare == epSquare &&
199199
other.halfmoves == halfmoves &&
200200
other.fullmoves == fullmoves;
@@ -204,7 +204,7 @@ class Setup {
204204
int get hashCode => Object.hash(
205205
board,
206206
turn,
207-
unmovedRooks,
207+
castlingRights,
208208
epSquare,
209209
halfmoves,
210210
fullmoves,
@@ -312,43 +312,38 @@ Pockets _parsePockets(String pocketPart) {
312312
}
313313

314314
SquareSet _parseCastlingFen(Board board, String castlingPart) {
315-
SquareSet unmovedRooks = SquareSet.empty;
315+
SquareSet castlingRights = SquareSet.empty;
316316
if (castlingPart == '-') {
317-
return unmovedRooks;
317+
return castlingRights;
318318
}
319-
for (int i = 0; i < castlingPart.length; i++) {
320-
final c = castlingPart[i];
319+
for (final rune in castlingPart.runes) {
320+
final c = String.fromCharCode(rune);
321321
final lower = c.toLowerCase();
322-
final color = c == lower ? Side.black : Side.white;
323-
final backrankMask = SquareSet.backrankOf(color);
324-
final backrank = backrankMask & board.bySide(color);
325-
326-
Iterable<Square> candidates;
327-
if (lower == 'q') {
328-
candidates = backrank.squares;
329-
} else if (lower == 'k') {
330-
candidates = backrank.squaresReversed;
331-
} else if ('a'.compareTo(lower) <= 0 && lower.compareTo('h') <= 0) {
332-
candidates =
333-
(SquareSet.fromFile(File(lower.codeUnitAt(0) - 'a'.codeUnitAt(0))) &
334-
backrank)
335-
.squares;
322+
final lowerCode = lower.codeUnitAt(0);
323+
final side = c == lower ? Side.black : Side.white;
324+
final rank = side == Side.white ? Rank.first : Rank.eighth;
325+
if ('a'.codeUnitAt(0) <= lowerCode && lowerCode <= 'h'.codeUnitAt(0)) {
326+
castlingRights = castlingRights.withSquare(
327+
Square.fromCoords(File(lowerCode - 'a'.codeUnitAt(0)), rank));
328+
} else if (lower == 'k' || lower == 'q') {
329+
final rooksAndKings = (board.bySide(side) & SquareSet.backrankOf(side)) &
330+
(board.rooks | board.kings);
331+
final candidate = lower == 'k'
332+
? rooksAndKings.squares.lastOrNull
333+
: rooksAndKings.squares.firstOrNull;
334+
castlingRights = castlingRights.withSquare(
335+
candidate != null && board.rooks.has(candidate)
336+
? candidate
337+
: Square.fromCoords(lower == 'k' ? File.h : File.a, rank));
336338
} else {
337339
throw const FenException(IllegalFenCause.castling);
338340
}
339-
for (final square in candidates) {
340-
if (board.kings.has(square)) break;
341-
if (board.rooks.has(square)) {
342-
unmovedRooks = unmovedRooks.withSquare(square);
343-
break;
344-
}
345-
}
346341
}
347-
if ((const SquareSet.fromRank(Rank.first) & unmovedRooks).size > 2 ||
348-
(const SquareSet.fromRank(Rank.eighth) & unmovedRooks).size > 2) {
342+
if (Side.values.any((color) =>
343+
SquareSet.backrankOf(color).intersect(castlingRights).size > 2)) {
349344
throw const FenException(IllegalFenCause.castling);
350345
}
351-
return unmovedRooks;
346+
return castlingRights;
352347
}
353348

354349
String _makePockets(Pockets pockets) {
@@ -363,14 +358,14 @@ String _makePockets(Pockets pockets) {
363358
return '[${wPart.toUpperCase()}$bPart]';
364359
}
365360

366-
String _makeCastlingFen(Board board, SquareSet unmovedRooks) {
361+
String _makeCastlingFen(Board board, SquareSet castlingRights) {
367362
final buffer = StringBuffer();
368363
for (final color in Side.values) {
369364
final backrank = SquareSet.backrankOf(color);
370365
final king = board.kingOf(color);
371366
final candidates =
372367
board.byPiece(Piece(color: color, role: Role.rook)) & backrank;
373-
for (final rook in (unmovedRooks & candidates).squaresReversed) {
368+
for (final rook in (castlingRights & backrank).squaresReversed) {
374369
if (rook == candidates.first && king != null && rook < king) {
375370
buffer.write(color == Side.white ? 'Q' : 'q');
376371
} else if (rook == candidates.last && king != null && king < rook) {

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: dartchess
22
description: Provides chess and chess variants rules and operations including chess move generation, read and write FEN, read and write PGN.
33
repository: https://github.com/lichess-org/dartchess
4-
version: 0.9.1
4+
version: 0.9.2
55
platforms:
66
android:
77
ios:

test/castles_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ void main() {
1818
});
1919
test('fromSetup', () {
2020
final castles = Castles.fromSetup(Setup.standard);
21-
expect(castles.unmovedRooks, SquareSet.corners);
21+
expect(castles.castlingRights, SquareSet.corners);
2222
expect(castles, Castles.standard);
2323

2424
expect(castles.rookOf(Side.white, CastlingSide.queen), Square.a1);

0 commit comments

Comments
 (0)