Skip to content
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 lib/src/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class GameData {
required this.onPromotionSelection,
this.isCheck,
this.premovable,
this.canPromoteToKing = false,
});

/// Side that is allowed to move.
Expand Down Expand Up @@ -67,6 +68,9 @@ class GameData {
///
/// If `null`, the board will not allow premoves.
final Premovable? premovable;

/// Whether the pawn can be promoted to a king (possible for example in Antichess).
final bool canPromoteToKing;
}

/// State of a premovable chessboard.
Expand Down
1 change: 1 addition & 0 deletions lib/src/widgets/board.dart
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ class _BoardState extends State<Chessboard> {
onCancel: () {
widget.game!.onPromotionSelection(null);
},
canPromoteToKing: widget.game!.canPromoteToKing,
),
],
),
Expand Down
34 changes: 18 additions & 16 deletions lib/src/widgets/promotion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'piece.dart';
///
/// This widget should be displayed when a pawn reaches the last rank and must be
/// promoted. The user can select a piece to promote to by tapping on one of
/// the four pieces displayed.
/// the pieces displayed in the widget. Normally there are 4 pieces to choose from (queen, rook, bishop and knight), but a king can also be included as an option if `canPromoteToKing` is true (for example in Antichess).
/// Promotion can be canceled by tapping outside the promotion widget.
class PromotionSelector extends StatelessWidget with ChessboardGeometry {
const PromotionSelector({
Expand All @@ -21,6 +21,7 @@ class PromotionSelector extends StatelessWidget with ChessboardGeometry {
required this.onSelect,
required this.onCancel,
required this.pieceAssets,
required this.canPromoteToKing,
});

/// The move that is being promoted.
Expand All @@ -32,6 +33,9 @@ class PromotionSelector extends StatelessWidget with ChessboardGeometry {
/// The piece assets to use.
final PieceAssets pieceAssets;

/// Whether the pawn can be promoted to a king (possible for example in Antichess).
final bool canPromoteToKing;

@override
final double size;

Expand Down Expand Up @@ -59,20 +63,18 @@ class PromotionSelector extends StatelessWidget with ChessboardGeometry {
isPromotionSquareAtTop
? square
: Square.fromCoords(square.file, orientation == Side.white ? Rank.fourth : Rank.fifth);
final pieces =
isPromotionSquareAtTop
? [
Piece(color: color, role: Role.queen, promoted: true),
Piece(color: color, role: Role.knight, promoted: true),
Piece(color: color, role: Role.rook, promoted: true),
Piece(color: color, role: Role.bishop, promoted: true),
]
: [
Piece(color: color, role: Role.bishop, promoted: true),
Piece(color: color, role: Role.rook, promoted: true),
Piece(color: color, role: Role.knight, promoted: true),
Piece(color: color, role: Role.queen, promoted: true),
];

final topRoles = [
Role.queen,
Role.knight,
Role.rook,
Role.bishop,
if (canPromoteToKing) Role.king,
];
final roles = isPromotionSquareAtTop ? topRoles : topRoles.reversed.toList(growable: false);
final pieces = roles
.map((role) => Piece(color: color, role: role, promoted: true))
.toList(growable: false);

final offset = squareOffset(anchorSquare);

Expand All @@ -87,7 +89,7 @@ class PromotionSelector extends StatelessWidget with ChessboardGeometry {
children: [
Positioned(
width: squareSize,
height: squareSize * 4,
height: squareSize * pieces.length,
left: offset.dx,
top: offset.dy,
child: Column(
Expand Down
98 changes: 98 additions & 0 deletions test/widgets/board_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,100 @@ void main() {
expect(find.byKey(const Key('a2-blackpawn')), findsNothing);
});

testWidgets('default promotion shows 4 pieces without king', (WidgetTester tester) async {
await tester.pumpWidget(
const _TestApp(
initialPlayerSide: PlayerSide.both,
initialFen: '8/5P2/2RK2P1/8/4k3/8/8/7r w - - 0 1',
),
);

await tester.tap(find.byKey(const Key('f7-whitepawn')));
await tester.pump();
await tester.tapAt(squareOffset(tester, Square.f8));
await tester.pump();

// wait for promotion selector to show
await tester.pump();
expect(find.byType(PromotionSelector), findsOneWidget);

// Find all PieceWidget instances within the PromotionSelector
final promotionSelector = find.byType(PromotionSelector);
final piecesInSelector = find.descendant(
of: promotionSelector,
matching: find.byType(PieceWidget),
);

// Should have exactly 4 pieces (queen, knight, rook, bishop)
expect(piecesInSelector, findsNWidgets(4));

// Verify no king piece is present
final pieceWidgets = tester.widgetList<PieceWidget>(piecesInSelector);
final hasKing = pieceWidgets.any((widget) => widget.piece.role == Role.king);
expect(hasKing, false);
});
testWidgets('promotion with canPromoteToKing shows 5 pieces including king', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const _TestApp(
initialPlayerSide: PlayerSide.both,
initialFen: '8/5P2/2RK2P1/8/4k3/8/8/7r w - - 0 1',
canPromoteToKing: true,
),
);

await tester.tap(find.byKey(const Key('f7-whitepawn')));
await tester.pump();
await tester.tapAt(squareOffset(tester, Square.f8));
await tester.pump();

// wait for promotion selector to show
await tester.pump();
expect(find.byType(PromotionSelector), findsOneWidget);

// Find all PieceWidget instances within the PromotionSelector
final promotionSelector = find.byType(PromotionSelector);
final piecesInSelector = find.descendant(
of: promotionSelector,
matching: find.byType(PieceWidget),
);

// has exactly 5 pieces (queen, knight, rook, bishop, king)
expect(piecesInSelector, findsNWidgets(5));

// Verify king piece is present
final pieceWidgets = tester.widgetList<PieceWidget>(piecesInSelector);
final hasKing = pieceWidgets.any((widget) => widget.piece.role == Role.king);
expect(hasKing, true);
});

testWidgets('can promote to king when enabled', (WidgetTester tester) async {
await tester.pumpWidget(
const _TestApp(
initialPlayerSide: PlayerSide.both,
initialFen: '8/5P2/2RK2P1/8/4k3/8/8/7r w - - 0 1',
canPromoteToKing: true,
),
);

await tester.tap(find.byKey(const Key('f7-whitepawn')));
await tester.pump();
await tester.tapAt(squareOffset(tester, Square.f8));
await tester.pump();

// wait for promotion selector to show
await tester.pump();
expect(find.byType(PromotionSelector), findsOneWidget);

// tap on the king is the last option in the promotion selector
await tester.tapAt(squareOffset(tester, Square.f4));
await tester.pump();

expect(find.byKey(const Key('f8-whiteking')), findsOneWidget);
expect(find.byKey(const Key('f7-whitepawn')), findsNothing);
});

testWidgets('cancels promotion', (WidgetTester tester) async {
await tester.pumpWidget(
const _TestApp(
Expand Down Expand Up @@ -1884,6 +1978,7 @@ class _TestApp extends StatefulWidget {
this.gameEventStream,
this.onTouchedSquare,
this.bottomWidget,
this.canPromoteToKing = false,
super.key,
});

Expand All @@ -1907,6 +2002,8 @@ class _TestApp extends StatefulWidget {

final Widget? bottomWidget;

final bool canPromoteToKing;

@override
State<_TestApp> createState() => _TestAppState();
}
Expand Down Expand Up @@ -2075,6 +2172,7 @@ class _TestAppState extends State<_TestApp> {
});
},
),
canPromoteToKing: widget.canPromoteToKing,
),
onTouchedSquare: widget.onTouchedSquare,
shapes: shapes,
Expand Down