Skip to content

Commit

Permalink
Merge pull request #1096 from lichess-org/fix_chess960_castling
Browse files Browse the repository at this point in the history
Fix chess960 castling
  • Loading branch information
veloce authored Oct 21, 2024
2 parents 33904fc + 092587e commit e53428d
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 104 deletions.
8 changes: 4 additions & 4 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,9 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77
Expand All @@ -252,11 +252,11 @@ SPEC CHECKSUMS:
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
Expand Down
6 changes: 4 additions & 2 deletions lib/src/model/analysis/analysis_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,10 @@ class AnalysisState with _$AnalysisState {
/// Whether the analysis is for a lichess game.
bool get isLichessGameAnalysis => gameAnyId != null;

IMap<Square, ISet<Square>> get validMoves =>
makeLegalMoves(currentNode.position);
IMap<Square, ISet<Square>> get validMoves => makeLegalMoves(
currentNode.position,
isChess960: variant == Variant.chess960,
);

/// Whether the user can request server analysis.
///
Expand Down
129 changes: 95 additions & 34 deletions lib/src/model/board_editor/board_editor_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,23 @@ part 'board_editor_controller.g.dart';
class BoardEditorController extends _$BoardEditorController {
@override
BoardEditorState build(String? initialFen) {
final setup = Setup.parseFen(initialFen ?? kInitialFEN);
return BoardEditorState(
orientation: Side.white,
sideToPlay: Side.white,
pieces: readFen(initialFen ?? kInitialFEN).lock,
unmovedRooks: SquareSet.corners,
castlingRights: IMap(const {
CastlingRight.whiteKing: true,
CastlingRight.whiteQueen: true,
CastlingRight.blackKing: true,
CastlingRight.blackQueen: true,
}),
editorPointerMode: EditorPointerMode.drag,
enPassantOptions: SquareSet.empty,
enPassantSquare: null,
pieceToAddOnEdit: null,
halfmoves: setup.halfmoves,
fullmoves: setup.fullmoves,
);
}

Expand Down Expand Up @@ -126,27 +134,40 @@ class BoardEditorController extends _$BoardEditorController {
void setCastling(Side side, CastlingSide castlingSide, bool allowed) {
switch (side) {
case Side.white:
if (castlingSide == CastlingSide.king) {
_setRookUnmoved(Square.h1, allowed);
} else {
_setRookUnmoved(Square.a1, allowed);
switch (castlingSide) {
case CastlingSide.king:
state = state.copyWith(
castlingRights:
state.castlingRights.add(CastlingRight.whiteKing, allowed),
);
case CastlingSide.queen:
state = state.copyWith(
castlingRights:
state.castlingRights.add(CastlingRight.whiteQueen, allowed),
);
}
case Side.black:
if (castlingSide == CastlingSide.king) {
_setRookUnmoved(Square.h8, allowed);
} else {
_setRookUnmoved(Square.a8, allowed);
switch (castlingSide) {
case CastlingSide.king:
state = state.copyWith(
castlingRights:
state.castlingRights.add(CastlingRight.blackKing, allowed),
);
case CastlingSide.queen:
state = state.copyWith(
castlingRights:
state.castlingRights.add(CastlingRight.blackQueen, allowed),
);
}
}
}
}

void _setRookUnmoved(Square square, bool unmoved) {
state = state.copyWith(
unmovedRooks: unmoved
? state.unmovedRooks.withSquare(square)
: state.unmovedRooks.withoutSquare(square),
);
}
enum CastlingRight {
whiteKing,
whiteQueen,
blackKing,
blackQueen,
}

@freezed
Expand All @@ -157,10 +178,12 @@ class BoardEditorState with _$BoardEditorState {
required Side orientation,
required Side sideToPlay,
required IMap<Square, Piece> pieces,
required SquareSet unmovedRooks,
required IMap<CastlingRight, bool> castlingRights,
required EditorPointerMode editorPointerMode,
required SquareSet enPassantOptions,
required Square? enPassantSquare,
required int halfmoves,
required int fullmoves,

/// When null, clears squares when in edit mode. Has no effect in drag mode.
required Piece? pieceToAddOnEdit,
Expand All @@ -169,26 +192,61 @@ class BoardEditorState with _$BoardEditorState {
bool isCastlingAllowed(Side side, CastlingSide castlingSide) =>
switch (side) {
Side.white => switch (castlingSide) {
CastlingSide.king => unmovedRooks.has(Square.h1),
CastlingSide.queen => unmovedRooks.has(Square.a1),
CastlingSide.king => castlingRights[CastlingRight.whiteKing]!,
CastlingSide.queen => castlingRights[CastlingRight.whiteQueen]!,
},
Side.black => switch (castlingSide) {
CastlingSide.king => unmovedRooks.has(Square.h8),
CastlingSide.queen => unmovedRooks.has(Square.a8),
CastlingSide.king => castlingRights[CastlingRight.blackKing]!,
CastlingSide.queen => castlingRights[CastlingRight.blackQueen]!,
},
};

Setup get _setup {
final boardFen = writeFen(pieces.unlock);
final board = Board.parseFen(boardFen);
return Setup(
board: board,
unmovedRooks: unmovedRooks,
turn: sideToPlay == Side.white ? Side.white : Side.black,
epSquare: enPassantSquare,
halfmoves: 0,
fullmoves: 1,
);
/// Returns the castling rights part of the FEN string.
///
/// If the rook is missing on one side of the king, or the king is missing on the
/// backrank, the castling right is removed.
String get _castlingRightsPart {
final parts = <String>[];
final Map<CastlingRight, bool> hasRook = {};
final Board board = Board.parseFen(writeFen(pieces.unlock));
for (final side in Side.values) {
final backrankKing = SquareSet.backrankOf(side) & board.kings;
final rooksAndKings = (board.bySide(side) & SquareSet.backrankOf(side)) &
(board.rooks | board.kings);
for (final castlingSide in CastlingSide.values) {
final candidate = castlingSide == CastlingSide.king
? rooksAndKings.squares.lastOrNull
: rooksAndKings.squares.firstOrNull;
final isCastlingPossible = candidate != null &&
board.rooks.has(candidate) &&
backrankKing.singleSquare != null;
switch ((side, castlingSide)) {
case (Side.white, CastlingSide.king):
hasRook[CastlingRight.whiteKing] = isCastlingPossible;
case (Side.white, CastlingSide.queen):
hasRook[CastlingRight.whiteQueen] = isCastlingPossible;
case (Side.black, CastlingSide.king):
hasRook[CastlingRight.blackKing] = isCastlingPossible;
case (Side.black, CastlingSide.queen):
hasRook[CastlingRight.blackQueen] = isCastlingPossible;
}
}
}
for (final right in CastlingRight.values) {
if (hasRook[right]! && castlingRights[right]!) {
switch (right) {
case CastlingRight.whiteKing:
parts.add('K');
case CastlingRight.whiteQueen:
parts.add('Q');
case CastlingRight.blackKing:
parts.add('k');
case CastlingRight.blackQueen:
parts.add('q');
}
}
}
return parts.isEmpty ? '-' : parts.join('');
}

Piece? get activePieceOnEdit =>
Expand All @@ -197,14 +255,17 @@ class BoardEditorState with _$BoardEditorState {
bool get deletePiecesActive =>
editorPointerMode == EditorPointerMode.edit && pieceToAddOnEdit == null;

String get fen => _setup.fen;
String get fen {
final boardFen = writeFen(pieces.unlock);
return '$boardFen ${sideToPlay == Side.white ? 'w' : 'b'} $_castlingRightsPart ${enPassantSquare?.name ?? '-'} $halfmoves $fullmoves';
}

/// Returns the PGN representation of the current position if it is valid.
///
/// Returns `null` if the position is invalid.
String? get pgn {
try {
final position = Chess.fromSetup(_setup);
final position = Chess.fromSetup(Setup.parseFen(fen));
return PgnGame(
headers: {'FEN': position.fen},
moves: PgnNode<PgnNodeData>(),
Expand Down
10 changes: 3 additions & 7 deletions lib/src/model/common/chess960.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@ final _random = Random.secure();
Position randomChess960Position() {
final rank8 = _positions[_random.nextInt(_positions.length)];

return Chess(
board: Board.parseFen(
'$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()}',
return Chess.fromSetup(
Setup.parseFen(
'$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()} w KQkq - 0 1',
),
turn: Side.white,
castles: Castles.standard,
halfmoves: 0,
fullmoves: 1,
);
}

Expand Down
10 changes: 10 additions & 0 deletions lib/src/model/game/game_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,16 @@ class GameState with _$GameState {
GameFullId? redirectGameId,
}) = _GameState;

/// The [Position] and its legal moves at the current cursor.
(Position, IMap<Square, ISet<Square>>) get currentPosition {
final position = game.positionAt(stepCursor);
final legalMoves = makeLegalMoves(
position,
isChess960: game.meta.variant == Variant.chess960,
);
return (position, legalMoves);
}

/// Whether the zen mode is active
bool get isZenModeActive =>
game.playable ? isZenModeEnabled : game.prefs?.zenMode == Zen.yes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ class OverTheBoardGameState with _$OverTheBoardGameState {
? NormalMove.fromUci(game.steps[stepCursor].sanMove!.move.uci)
: null;

IMap<Square, ISet<Square>> get legalMoves => makeLegalMoves(
currentPosition,
isChess960: game.meta.variant == Variant.chess960,
);

MaterialDiffSide? currentMaterialDiff(Side side) {
return game.steps[stepCursor].diff?.bySide(side);
}
Expand Down
49 changes: 28 additions & 21 deletions lib/src/view/board_editor/board_editor_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,33 +42,40 @@ class BoardEditorMenu extends ConsumerWidget {
),
Padding(
padding: Styles.bodySectionPadding,
child: Text(context.l10n.castling, style: Styles.subtitle),
child: Text(context.l10n.castling, style: Styles.title),
),
...Side.values.map((side) {
return Padding(
padding: Styles.horizontalBodyPadding,
child: Wrap(
child: Row(
spacing: 8.0,
children:
[CastlingSide.king, CastlingSide.queen].map((castlingSide) {
return ChoiceChip(
label: Text(
castlingSide == CastlingSide.king
? side == Side.white
? context.l10n.whiteCastlingKingside
: context.l10n.blackCastlingKingside
: 'O-O-O',
children: [
SizedBox(
width: 100.0,
child: Text(
side == Side.white
? context.l10n.white
: context.l10n.black,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
selected: editorState.isCastlingAllowed(side, castlingSide),
onSelected: (selected) {
ref.read(editorController.notifier).setCastling(
side,
castlingSide,
selected,
);
},
);
}).toList(),
),
...[CastlingSide.king, CastlingSide.queen].map((castlingSide) {
return ChoiceChip(
label: Text(
castlingSide == CastlingSide.king ? 'O-O' : 'O-O-O',
),
selected: editorState.isCastlingAllowed(side, castlingSide),
onSelected: (selected) {
ref.read(editorController.notifier).setCastling(
side,
castlingSide,
selected,
);
},
);
}),
],
),
);
}),
Expand Down
4 changes: 4 additions & 0 deletions lib/src/view/board_editor/board_editor_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ class _BottomBar extends ConsumerWidget {
builder: (BuildContext context) => BoardEditorMenu(
initialFen: initialFen,
),
showDragHandle: true,
constraints: BoxConstraints(
minHeight: MediaQuery.sizeOf(context).height * 0.5,
),
),
icon: Icons.tune,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,10 @@ class _BodyState extends ConsumerState<_Body> {
: PlayerSide.none,
isCheck: position.isCheck,
sideToMove: sideToMove,
validMoves: makeLegalMoves(position),
validMoves: makeLegalMoves(
position,
isChess960: game.variant == Variant.chess960,
),
promotionMove: promotionMove,
onMove: (move, {isDrop, captured}) {
onUserMove(move);
Expand Down
4 changes: 2 additions & 2 deletions lib/src/view/game/game_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class GameBody extends ConsumerWidget {

return gameStateAsync.when(
data: (gameState) {
final position = gameState.game.positionAt(gameState.stepCursor);
final (position, legalMoves) = gameState.currentPosition;
final youAre = gameState.game.youAre ?? Side.white;
final archivedBlackClock =
gameState.game.archivedBlackClockAt(gameState.stepCursor);
Expand Down Expand Up @@ -253,7 +253,7 @@ class GameBody extends ConsumerWidget {
isCheck: boardPreferences.boardHighlights &&
position.isCheck,
sideToMove: position.turn,
validMoves: makeLegalMoves(position),
validMoves: legalMoves,
promotionMove: gameState.promotionMove,
onMove: (move, {isDrop}) {
ref.read(ctrlProvider.notifier).userMove(
Expand Down
2 changes: 1 addition & 1 deletion lib/src/view/over_the_board/over_the_board_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class _BodyState extends ConsumerState<_Body> {
? PlayerSide.white
: PlayerSide.black,
sideToMove: gameState.turn,
validMoves: makeLegalMoves(gameState.currentPosition),
validMoves: gameState.legalMoves,
onPromotionSelection: ref
.read(overTheBoardGameControllerProvider.notifier)
.onPromotionSelection,
Expand Down
Loading

0 comments on commit e53428d

Please sign in to comment.