Skip to content

Commit d9b78e3

Browse files
authored
Crazyhouse support 🎉 (#2662)
1 parent 52127fe commit d9b78e3

29 files changed

+552
-113
lines changed

lib/src/constants.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ final List<BoxShadow> boardShadows = defaultTargetPlatform == TargetPlatform.iOS
6161

6262
const kMaxClockTextScaleFactor = 1.94;
6363
const kEmptyWidget = SizedBox.shrink();
64-
const kEmptyFen = '8/8/8/8/8/8/8/8 w - - 0 1';
6564
const kTabletBoardTableSidePadding = 16.0;
65+
66+
/// In crazyhouse, when displaying pockets above/below the board, add this much additional side padding to make the board smaller and avoid overflows.
67+
const kAdditionalBoardSidePaddingForPockets = 70.0;
6668
const kBottomBarHeight = 56.0;
6769
const kMaterialPopupMenuMaxWidth = 500.0;
6870

lib/src/model/common/chess.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const ISet<Variant> readSupportedVariants = ISetConst({
9494
Variant.threeCheck,
9595
Variant.racingKings,
9696
Variant.horde,
97+
Variant.crazyhouse,
9798
});
9899

99100
/// Set of supported variants for playing a game.
@@ -133,6 +134,8 @@ enum Variant {
133134

134135
bool get isPlaySupported => playSupportedVariants.contains(this);
135136

137+
bool get hasDropMoves => this == Variant.crazyhouse;
138+
136139
static final IMap<String, Variant> nameMap = IMap(values.asNameMap());
137140

138141
static Variant fromRule(Rule rule) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:chessground/chessground.dart';
2+
import 'package:dartchess/dartchess.dart';
3+
import 'package:freezed_annotation/freezed_annotation.dart';
4+
import 'package:lichess_mobile/src/model/common/chess.dart';
5+
6+
part 'game_board_params.freezed.dart';
7+
8+
@freezed
9+
sealed class GameBoardParams with _$GameBoardParams {
10+
const GameBoardParams._();
11+
12+
const factory GameBoardParams.readonly({
13+
required String fen,
14+
required Variant variant,
15+
required Pockets? pockets,
16+
}) = ReadonlyBoardParams;
17+
18+
const factory GameBoardParams.interactive({
19+
required Variant variant,
20+
required Position position,
21+
required PlayerSide playerSide,
22+
required NormalMove? promotionMove,
23+
required void Function(Move, {bool? viaDragAndDrop}) onMove,
24+
required void Function(Role? role) onPromotionSelection,
25+
required Premovable? premovable,
26+
}) = InteractiveBoardParams;
27+
28+
static const emptyBoard = ReadonlyBoardParams(
29+
fen: kEmptyFEN,
30+
variant: Variant.standard,
31+
pockets: null,
32+
);
33+
34+
String get fen => switch (this) {
35+
ReadonlyBoardParams(:final fen) => fen,
36+
InteractiveBoardParams(:final position) => position.fen,
37+
};
38+
39+
Pockets? get pockets => switch (this) {
40+
ReadonlyBoardParams(:final pockets) => pockets,
41+
InteractiveBoardParams(:final position) => position.pockets,
42+
};
43+
}

lib/src/model/tv/tv_channel.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ enum TvChannel {
1616
// atomic('Atomic', LichessIcons.atom),
1717
horde('Horde', LichessIcons.horde),
1818
racingKings('Racing Kings', LichessIcons.racing_kings),
19-
// crazyhouse('Crazyhouse', LichessIcons.h_square),
19+
crazyhouse('Crazyhouse', LichessIcons.h_square),
2020
ultraBullet('UltraBullet', LichessIcons.ultrabullet),
2121
bot('Bot', LichessIcons.cogs),
2222
computer('Computer', LichessIcons.cogs);

lib/src/utils/screen.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:lichess_mobile/src/constants.dart';
3+
import 'package:lichess_mobile/src/widgets/pockets.dart';
34

45
/// Returns the estimated height of what is left after removing the height of the board from the screen.
56
double estimateHeightMinusBoard(MediaQueryData mediaQuery) {
@@ -44,6 +45,14 @@ bool isTabletOrLarger(BuildContext context) {
4445
return getScreenType(context) >= ScreenType.tablet;
4546
}
4647

48+
/// How big a square in the [PocketsMenu] should be, based on the size of the board and whether the device is a tablet.
49+
double pocketSquareSize({required double boardSize, required bool isTablet}) {
50+
final squareSize = boardSize / 8;
51+
// On tablets, displaying the pockets at the same size as regular pieces
52+
// can lead to overflows and looks weird, so reduce the size a bit.
53+
return isTablet ? 0.7 * squareSize : squareSize;
54+
}
55+
4756
enum ScreenType { watch, handset, tablet, desktop }
4857

4958
extension ScreenTypeComparisonOperators on ScreenType {

lib/src/view/analysis/analysis_board.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ abstract class AnalysisBoardState<
166166
onClearShapes: _onClearShapes,
167167
newShapeColor: boardPrefs.shapeColor.color,
168168
),
169+
enableDropMoves: analysisState.variant.hasDropMoves,
169170
),
170171
);
171172
}

lib/src/view/analysis/analysis_layout.dart

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/utils/screen.dart';
99
import 'package:lichess_mobile/src/view/engine/engine_gauge.dart';
1010
import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart';
1111
import 'package:lichess_mobile/src/widgets/buttons.dart';
12+
import 'package:lichess_mobile/src/widgets/pockets.dart';
1213

1314
/// The height of the board header or footer in the analysis layout.
1415
const kAnalysisBoardHeaderOrFooterHeight = 26.0;
@@ -129,11 +130,13 @@ class AnalysisLayout extends StatelessWidget {
129130
required this.boardBuilder,
130131
required this.children,
131132
required this.pov,
133+
required this.sideToMove,
132134
this.boardHeader,
133135
this.boardFooter,
134136
this.engineGaugeBuilder,
135137
this.engineLines,
136138
this.bottomBar,
139+
this.pockets,
137140
super.key,
138141
});
139142

@@ -146,6 +149,9 @@ class AnalysisLayout extends StatelessWidget {
146149
/// The side the board is displayed from.
147150
final Side pov;
148151

152+
/// The side to move. In crazyhouse, this enables the [PocketsMenu] of this side.
153+
final Side? sideToMove;
154+
149155
/// A widget to show above the board.
150156
///
151157
/// The widget will included in a parent container with a height of
@@ -173,6 +179,11 @@ class AnalysisLayout extends StatelessWidget {
173179
/// A widget to show at the bottom of the screen.
174180
final Widget? bottomBar;
175181

182+
/// Current state of the pockets, in variants like crazyhouse.
183+
///
184+
/// If not null, will render a [PocketsMenu] for each player.
185+
final Pockets? pockets;
186+
176187
@override
177188
Widget build(BuildContext context) {
178189
return Column(
@@ -202,6 +213,7 @@ class AnalysisLayout extends StatelessWidget {
202213
: constraints.biggest.longestSide / kGoldenRatio -
203214
(kTabletBoardTableSidePadding * 2)) -
204215
headerAndFooterHeight;
216+
205217
return Padding(
206218
padding: const EdgeInsets.all(kTabletBoardTableSidePadding),
207219
child: Row(
@@ -269,13 +281,39 @@ class AnalysisLayout extends StatelessWidget {
269281
crossAxisAlignment: CrossAxisAlignment.stretch,
270282
children: [
271283
if (engineLines != null) engineLines!,
284+
if (pockets != null)
285+
Align(
286+
alignment: Alignment.center,
287+
child: PocketsMenu(
288+
side: pov.opposite,
289+
sideToMove: sideToMove,
290+
pockets: pockets!,
291+
squareSize: pocketSquareSize(
292+
boardSize: boardSize,
293+
isTablet: isTablet,
294+
),
295+
),
296+
),
272297
Expanded(
273298
child: Card(
274299
clipBehavior: Clip.hardEdge,
275300
semanticContainer: false,
276301
child: TabBarView(controller: tabController, children: children),
277302
),
278303
),
304+
if (pockets != null)
305+
Align(
306+
alignment: Alignment.center,
307+
child: PocketsMenu(
308+
side: pov,
309+
sideToMove: sideToMove,
310+
pockets: pockets!,
311+
squareSize: pocketSquareSize(
312+
boardSize: boardSize,
313+
isTablet: isTablet,
314+
),
315+
),
316+
),
279317
],
280318
),
281319
),
@@ -289,7 +327,10 @@ class AnalysisLayout extends StatelessWidget {
289327
final isSmallScreen = remainingHeight < kSmallHeightMinusBoard;
290328
final evalGaugeSize = engineGaugeBuilder != null ? evalGaugeWidth : 0.0;
291329
final boardSize = isTablet || isSmallScreen
292-
? defaultBoardSize - evalGaugeSize - kTabletBoardTableSidePadding * 2
330+
? defaultBoardSize -
331+
evalGaugeSize -
332+
kTabletBoardTableSidePadding * 2 -
333+
(pockets != null ? kAdditionalBoardSidePaddingForPockets : 0.0)
293334
: defaultBoardSize - evalGaugeSize;
294335

295336
return Column(
@@ -304,6 +345,16 @@ class AnalysisLayout extends StatelessWidget {
304345
: EdgeInsets.zero,
305346
child: Column(
306347
children: [
348+
if (pockets != null)
349+
PocketsMenu(
350+
side: pov.opposite,
351+
sideToMove: sideToMove,
352+
pockets: pockets!,
353+
squareSize: pocketSquareSize(
354+
boardSize: boardSize,
355+
isTablet: isTablet,
356+
),
357+
),
307358
if (boardHeader != null)
308359
// This key is used to preserve the state of the board header when the pov changes
309360
Container(
@@ -321,6 +372,7 @@ class AnalysisLayout extends StatelessWidget {
321372
child: boardHeader,
322373
),
323374
Row(
375+
mainAxisAlignment: MainAxisAlignment.center,
324376
children: [
325377
boardBuilder(
326378
context,
@@ -349,6 +401,16 @@ class AnalysisLayout extends StatelessWidget {
349401
height: kAnalysisBoardHeaderOrFooterHeight,
350402
child: boardFooter,
351403
),
404+
if (pockets != null)
405+
PocketsMenu(
406+
side: pov,
407+
sideToMove: sideToMove,
408+
pockets: pockets!,
409+
squareSize: pocketSquareSize(
410+
boardSize: boardSize,
411+
isTablet: isTablet,
412+
),
413+
),
352414
],
353415
),
354416
),

lib/src/view/analysis/analysis_screen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ class _Body extends ConsumerWidget {
361361
child: AnalysisLayout(
362362
tabController: controller,
363363
pov: pov,
364+
sideToMove: analysisState.currentPosition.turn,
364365
boardBuilder: (context, boardSize, borderRadius) =>
365366
GameAnalysisBoard(options: options, boardSize: boardSize, boardRadius: borderRadius),
366367
boardHeader: boardHeader,
@@ -379,6 +380,7 @@ class _Body extends ConsumerWidget {
379380
)
380381
: null,
381382
bottomBar: _BottomBar(options: options),
383+
pockets: analysisState.currentPosition.pockets,
382384
children: [
383385
ExplorerView(
384386
pov: pov,

lib/src/view/analysis/retro_screen.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class _RetroScreen extends ConsumerWidget {
125125

126126
return AnalysisLayout(
127127
pov: state.pov,
128+
sideToMove: state.currentPosition.turn,
128129
boardBuilder: (context, boardSize, borderRadius) =>
129130
RetroAnalysisBoard(options, boardSize: boardSize, boardRadius: borderRadius),
130131
engineGaugeBuilder: (context) {

lib/src/view/board_editor/board_editor_screen.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ class _BottomBar extends ConsumerWidget {
395395
BottomSheetAction(
396396
makeLabel: (context) => Text(context.l10n.clearBoard),
397397
onPressed: () {
398-
ref.read(editorController.notifier).loadFen(kEmptyFen);
398+
ref.read(editorController.notifier).loadFen(kEmptyFEN);
399399
},
400400
),
401401
],

0 commit comments

Comments
 (0)