diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 6fe688f4de..52623a11c0 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -9,6 +9,7 @@ class MaterialDiffSide with _$MaterialDiffSide { const factory MaterialDiffSide({ required IMap pieces, required int score, + required IMap capturedPieces, }) = _MaterialDiffSide; } @@ -30,11 +31,32 @@ class MaterialDiff with _$MaterialDiff { required MaterialDiffSide white, }) = _MaterialDiff; - factory MaterialDiff.fromBoard(Board board) { + factory MaterialDiff.fromBoard(Board board, {Board? startingPosition}) { int score = 0; final IMap blackCount = board.materialCount(Side.black); final IMap whiteCount = board.materialCount(Side.white); + final IMap blackStartingCount = + startingPosition?.materialCount(Side.black) ?? + Board.standard.materialCount(Side.black); + final IMap whiteStartingCount = + startingPosition?.materialCount(Side.white) ?? + Board.standard.materialCount(Side.white); + + IMap subtractPieceCounts( + IMap startingCount, + IMap subtractCount, + ) { + return startingCount.map( + (role, count) => MapEntry(role, count - (subtractCount.get(role) ?? 0)), + ); + } + + final IMap blackCapturedPieces = + subtractPieceCounts(whiteStartingCount, whiteCount); + final IMap whiteCapturedPieces = + subtractPieceCounts(blackStartingCount, blackCount); + Map count; Map black; Map white; @@ -80,8 +102,16 @@ class MaterialDiff with _$MaterialDiff { }); return MaterialDiff( - black: MaterialDiffSide(pieces: black.toIMap(), score: -score), - white: MaterialDiffSide(pieces: white.toIMap(), score: score), + black: MaterialDiffSide( + pieces: black.toIMap(), + score: -score, + capturedPieces: blackCapturedPieces, + ), + white: MaterialDiffSide( + pieces: white.toIMap(), + score: score, + capturedPieces: whiteCapturedPieces, + ), ); } diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 43c741ef93..4a3f50a646 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -1,6 +1,8 @@ import 'package:chessground/chessground.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; @@ -84,9 +86,11 @@ class BoardPreferences extends _$BoardPreferences return save(state.copyWith(dragTargetKind: dragTargetKind)); } - Future toggleShowMaterialDifference() { + Future setMaterialDifferenceFormat( + MaterialDifferenceFormat materialDifferenceFormat, + ) { return save( - state.copyWith(showMaterialDifference: !state.showMaterialDifference), + state.copyWith(materialDifferenceFormat: materialDifferenceFormat), ); } @@ -120,7 +124,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool boardHighlights, required bool coordinates, required bool pieceAnimation, - required bool showMaterialDifference, + required MaterialDifferenceFormat materialDifferenceFormat, required ClockPosition clockPosition, @JsonKey( defaultValue: PieceShiftMethod.either, @@ -150,7 +154,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { boardHighlights: true, coordinates: true, pieceAnimation: true, - showMaterialDifference: true, + materialDifferenceFormat: MaterialDifferenceFormat.materialDifference, clockPosition: ClockPosition.right, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, @@ -317,6 +321,27 @@ enum BoardTheme { ); } +enum MaterialDifferenceFormat { + materialDifference(label: 'Material difference'), + capturedPieces(label: 'Captured pieces'), + hidden(label: 'Hidden'); + + const MaterialDifferenceFormat({ + required this.label, + }); + + final String label; + + bool get visible => this != MaterialDifferenceFormat.hidden; + + String l10n(AppLocalizations l10n) => switch (this) { + //TODO: Add l10n + MaterialDifferenceFormat.materialDifference => materialDifference.label, + MaterialDifferenceFormat.capturedPieces => capturedPieces.label, + MaterialDifferenceFormat.hidden => hidden.label, + }; +} + enum ClockPosition { left, right; diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 0ce0cdae57..d89d631db7 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -142,9 +142,9 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { - final shouldShowMaterialDiff = ref.watch( + final materialDifference = ref.watch( boardPreferencesProvider.select( - (prefs) => prefs.showMaterialDifference, + (prefs) => prefs.materialDifferenceFormat, ), ); @@ -157,9 +157,10 @@ class _BodyState extends ConsumerState<_Body> { final black = GamePlayer( player: game.black, - materialDiff: shouldShowMaterialDiff + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.black) : null, + materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.black, confirmMoveCallbacks: youAre == Side.black && moveToConfirm != null @@ -179,9 +180,10 @@ class _BodyState extends ConsumerState<_Body> { ); final white = GamePlayer( player: game.white, - materialDiff: shouldShowMaterialDiff + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.white) : null, + materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.white, confirmMoveCallbacks: youAre == Side.white && moveToConfirm != null diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 77a5a209d8..0dcb9e9d29 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -122,9 +122,10 @@ class GameBody extends ConsumerWidget { final black = GamePlayer( player: gameState.game.black, - materialDiff: boardPreferences.showMaterialDifference + materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, timeToMove: gameState.game.sideToMove == Side.black ? gameState.timeToMove : null, @@ -174,9 +175,10 @@ class GameBody extends ConsumerWidget { ); final white = GamePlayer( player: gameState.game.white, - materialDiff: boardPreferences.showMaterialDifference + materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, timeToMove: gameState.game.sideToMove == Side.white ? gameState.timeToMove : null, diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 641b2ca228..b46198dea2 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -28,6 +29,7 @@ class GamePlayer extends StatelessWidget { required this.player, this.clock, this.materialDiff, + this.materialDifferenceFormat, this.confirmMoveCallbacks, this.timeToMove, this.shouldLinkToUserProfile = true, @@ -40,6 +42,7 @@ class GamePlayer extends StatelessWidget { final Player player; final Widget? clock; final MaterialDiffSide? materialDiff; + final MaterialDifferenceFormat? materialDifferenceFormat; /// if confirm move preference is enabled, used to display confirmation buttons final ({VoidCallback confirm, VoidCallback cancel})? confirmMoveCallbacks; @@ -148,33 +151,12 @@ class GamePlayer extends StatelessWidget { if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) else if (materialDiff != null) - Row( - mainAxisAlignment: clockPosition == ClockPosition.right - ? MainAxisAlignment.start - : MainAxisAlignment.end, - children: [ - for (final role in Role.values) - for (int i = 0; i < materialDiff!.pieces[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - const SizedBox(width: 3), - Text( - style: const TextStyle( - fontSize: 13, - color: Colors.grey, - ), - materialDiff != null && materialDiff!.score > 0 - ? '+${materialDiff!.score}' - : '', - ), - ], - ) - else - // to avoid shifts use an empty text widget - const Text('', style: TextStyle(fontSize: 13)), + MaterialDifferenceDisplay( + materialDiff: materialDiff!, + materialDifferenceFormat: materialDifferenceFormat, + ), + // to avoid shifts use an empty text widget + const Text('', style: TextStyle(fontSize: 13)), ], ); @@ -347,6 +329,46 @@ class _MoveExpirationState extends ConsumerState { } } +class MaterialDifferenceDisplay extends StatelessWidget { + const MaterialDifferenceDisplay({ + required this.materialDiff, + this.materialDifferenceFormat = MaterialDifferenceFormat.materialDifference, + }); + + final MaterialDiffSide materialDiff; + final MaterialDifferenceFormat? materialDifferenceFormat; + + @override + Widget build(BuildContext context) { + final IMap piecesToRender = + (materialDifferenceFormat == MaterialDifferenceFormat.capturedPieces + ? materialDiff.capturedPieces + : materialDiff.pieces); + + return materialDifferenceFormat?.visible ?? true + ? Row( + children: [ + for (final role in Role.values) + for (int i = 0; i < piecesToRender[role]!; i++) + Icon( + _iconByRole[role], + size: 13, + color: Colors.grey, + ), + const SizedBox(width: 3), + Text( + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + ), + materialDiff.score > 0 ? '+${materialDiff.score}' : '', + ), + ], + ) + : const SizedBox.shrink(); + } +} + const Map _iconByRole = { Role.king: LichessIcons.chess_king, Role.queen: LichessIcons.chess_queen, diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index d91c843b37..d8eed2bd0c 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; - import 'game_screen_providers.dart'; class GameSettings extends ConsumerWidget { diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index 817d236372..7470cac66d 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -301,9 +301,10 @@ class _Player extends ConsumerWidget { name: side.name.capitalize(), ), ), - materialDiff: boardPreferences.showMaterialDifference + materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.currentMaterialDiff(side) : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, shouldLinkToUserProfile: false, clock: clock.timeIncrement.isInfinite ? null diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 799b082214..79a5d2f1fa 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -2,6 +2,7 @@ import 'package:chessground/chessground.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -247,13 +248,42 @@ class _Body extends ConsumerWidget { ), SwitchSettingTile( title: Text( - context.l10n.preferencesMaterialDifference, + context.l10n.preferencesBoardCoordinates, ), - value: boardPrefs.showMaterialDifference, + value: boardPrefs.coordinates, onChanged: (value) { ref .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); + .toggleCoordinates(); + }, + ), + SettingsListTile( + settingsLabel: const Text('Material'), //TODO: l10n + settingsValue: boardPrefs.materialDifferenceFormat + .l10n(AppLocalizations.of(context)), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: MaterialDifferenceFormat.values, + selectedItem: boardPrefs.materialDifferenceFormat, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: + (MaterialDifferenceFormat? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat( + value ?? + MaterialDifferenceFormat.materialDifference, + ), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => + const MaterialDifferenceFormatScreen(), + ); + } }, ), ], @@ -329,6 +359,37 @@ class BoardClockPositionScreen extends ConsumerWidget { } } +class MaterialDifferenceFormatScreen extends ConsumerWidget { + const MaterialDifferenceFormatScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final materialDifferenceFormat = ref.watch( + boardPreferencesProvider + .select((state) => state.materialDifferenceFormat), + ); + void onChanged(MaterialDifferenceFormat? value) => + ref.read(boardPreferencesProvider.notifier).setMaterialDifferenceFormat( + value ?? MaterialDifferenceFormat.materialDifference, + ); + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: MaterialDifferenceFormat.values, + selectedItem: materialDifferenceFormat, + titleBuilder: (t) => Text(t.label), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + class DragTargetKindSettingsScreen extends ConsumerWidget { const DragTargetKindSettingsScreen({super.key}); diff --git a/test/model/game/material_diff_test.dart b/test/model/game/material_diff_test.dart index 7a1e7c6a79..a6391a6b18 100644 --- a/test/model/game/material_diff_test.dart +++ b/test/model/game/material_diff_test.dart @@ -38,6 +38,32 @@ void main() { }), ), ); + expect( + diff.bySide(Side.black).capturedPieces, + equals( + IMap(const { + Role.king: 0, + Role.queen: 0, + Role.rook: 0, + Role.bishop: 2, + Role.knight: 2, + Role.pawn: 4, + }), + ), + ); + expect( + diff.bySide(Side.white).capturedPieces, + equals( + IMap(const { + Role.king: 0, + Role.queen: 1, + Role.rook: 1, + Role.bishop: 1, + Role.knight: 2, + Role.pawn: 3, + }), + ), + ); }); }); }