diff --git a/lib/src/model/game/exported_game.dart b/lib/src/model/game/exported_game.dart index ba9662175..f5d76e6ad 100644 --- a/lib/src/model/game/exported_game.dart +++ b/lib/src/model/game/exported_game.dart @@ -221,7 +221,7 @@ ExportedGame _archivedGameFromPick(RequiredPick pick, {bool withBookmarked = fal GameStep( sanMove: SanMove(san, move), position: position, - diff: MaterialDiff.fromBoard(position.board), + diff: MaterialDiff.fromPosition(position), archivedWhiteClock: index.isOdd ? stepClock : clock, archivedBlackClock: index.isEven ? stepClock : clock, ), diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index a2d0ad6f6..89b05aa2c 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -412,7 +412,7 @@ IList stepsFromJson(String json) { GameStep( position: position, sanMove: SanMove(san, move), - diff: MaterialDiff.fromBoard(position.board), + diff: MaterialDiff.fromPosition(position), ), ); } diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 5bfeb0e58..443ef8784 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -202,7 +202,7 @@ class GameController extends AsyncNotifier { final newStep = GameStep( position: newPos, sanMove: sanMove, - diff: MaterialDiff.fromBoard(newPos.board), + diff: MaterialDiff.fromPosition(newPos), ); state = AsyncValue.data( @@ -286,7 +286,7 @@ class GameController extends AsyncNotifier { final newStep = GameStep( position: newPos, sanMove: sanMove, - diff: MaterialDiff.fromBoard(newPos.board), + diff: MaterialDiff.fromPosition(newPos), ); state = AsyncValue.data( @@ -671,7 +671,7 @@ class GameController extends AsyncNotifier { final newStep = GameStep( sanMove: sanMove, position: newPos, - diff: MaterialDiff.fromBoard(newPos.board), + diff: MaterialDiff.fromPosition(newPos), ); newState = newState.copyWith( diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 625544da8..b179eaaba 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -10,7 +10,13 @@ sealed class MaterialDiffSide with _$MaterialDiffSide { required IMap pieces, required int score, required IMap capturedPieces, + + /// Number of checks given by this side. Only relevant in the 3-check variant, null otherwise. + required int? checksGiven, }) = _MaterialDiffSide; + + factory MaterialDiffSide.empty() => + MaterialDiffSide(pieces: IMap(), score: 0, capturedPieces: IMap(), checksGiven: null); } const IMap pieceScores = IMapConst({ @@ -29,15 +35,19 @@ sealed class MaterialDiff with _$MaterialDiff { const factory MaterialDiff({required MaterialDiffSide black, required MaterialDiffSide white}) = _MaterialDiff; - factory MaterialDiff.fromBoard(Board board, {Board? startingPosition}) { - int score = 0; - final IMap blackCount = board.materialCount(Side.black); - final IMap whiteCount = board.materialCount(Side.white); + factory MaterialDiff.fromPosition(Position position) { + if (position.rule == Rule.crazyhouse || position.rule == Rule.horde) { + return MaterialDiff(black: MaterialDiffSide.empty(), white: MaterialDiffSide.empty()); + } - final IMap blackStartingCount = - startingPosition?.materialCount(Side.black) ?? Board.standard.materialCount(Side.black); - final IMap whiteStartingCount = - startingPosition?.materialCount(Side.white) ?? Board.standard.materialCount(Side.white); + int score = 0; + final IMap blackCount = position.board.materialCount(Side.black); + final IMap whiteCount = position.board.materialCount(Side.white); + + final startingPosition = Position.initialPosition(position.rule); + + final IMap blackStartingCount = startingPosition.board.materialCount(Side.black); + final IMap whiteStartingCount = startingPosition.board.materialCount(Side.white); IMap subtractPieceCounts( IMap startingCount, @@ -95,16 +105,24 @@ sealed class MaterialDiff with _$MaterialDiff { } }); + int? checksGiven(Side side) => switch (position) { + ThreeCheck(remainingChecks: (final white, final black)) => + 3 - (side == Side.white ? white : black), + _ => null, + }; + return MaterialDiff( black: MaterialDiffSide( pieces: black.toIMap(), score: -score, capturedPieces: blackCapturedPieces, + checksGiven: checksGiven(Side.black), ), white: MaterialDiffSide( pieces: white.toIMap(), score: score, capturedPieces: whiteCapturedPieces, + checksGiven: checksGiven(Side.white), ), ); } diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 58423ec1a..958cc2b13 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -207,7 +207,7 @@ PlayableGame _playableGameFromPick(RequiredPick pick) { GameStep( sanMove: SanMove(san, move), position: position, - diff: MaterialDiff.fromBoard(position.board), + diff: MaterialDiff.fromPosition(position), ), ); } diff --git a/lib/src/model/offline_computer/offline_computer_game_controller.dart b/lib/src/model/offline_computer/offline_computer_game_controller.dart index 3a58fc89c..132dcb941 100644 --- a/lib/src/model/offline_computer/offline_computer_game_controller.dart +++ b/lib/src/model/offline_computer/offline_computer_game_controller.dart @@ -168,7 +168,7 @@ class OfflineComputerGameController extends Notifier { final newStep = GameStep( position: newPos, sanMove: sanMove, - diff: MaterialDiff.fromBoard(newPos.board), + diff: MaterialDiff.fromPosition(newPos), ); _clearHints(); diff --git a/lib/src/model/over_the_board/over_the_board_game_controller.dart b/lib/src/model/over_the_board/over_the_board_game_controller.dart index 6c2ed3270..986b071bf 100644 --- a/lib/src/model/over_the_board/over_the_board_game_controller.dart +++ b/lib/src/model/over_the_board/over_the_board_game_controller.dart @@ -61,7 +61,7 @@ class OverTheBoardGameController extends Notifier { final newStep = GameStep( position: newPos, sanMove: sanMove, - diff: MaterialDiff.fromBoard(newPos.board), + diff: MaterialDiff.fromPosition(newPos), ); // In an over-the-board game, we support "implicit takebacks": diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index 3599a62db..6ddf32ae9 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -213,7 +213,7 @@ class TvController extends AsyncNotifier { final newStep = GameStep( sanMove: sanMove, position: newPos, - diff: MaterialDiff.fromBoard(newPos.board), + diff: MaterialDiff.fromPosition(newPos), ); TvState newState = curState.copyWith( diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 8347372b4..bb12c5a1e 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -332,7 +332,7 @@ class _BodyState extends ConsumerState<_Body> { final newStep = GameStep( position: newPos, sanMove: sanMove, - diff: MaterialDiff.fromBoard(newPos.board), + diff: MaterialDiff.fromPosition(newPos), ); setState(() { diff --git a/lib/src/widgets/material_diff.dart b/lib/src/widgets/material_diff.dart index 71d8e0b15..1f56cc360 100644 --- a/lib/src/widgets/material_diff.dart +++ b/lib/src/widgets/material_diff.dart @@ -32,13 +32,15 @@ class MaterialDifferenceDisplay extends StatelessWidget { : materialDiff!.pieces) : IMap(); + Icon roleIcon(Role role) => Icon(_iconByRole[role], size: 13, color: textShade(context, 0.5)); + return materialDifferenceFormat?.visible ?? true ? Row( mainAxisSize: MainAxisSize.min, children: [ for (final role in Role.values) - for (int i = 0; i < (piecesToRender.get(role) ?? 0); i++) - Icon(_iconByRole[role], size: 13, color: textShade(context, 0.5)), + for (int i = 0; i < (piecesToRender.get(role) ?? 0); i++) roleIcon(role), + ...Iterable.generate(materialDiff?.checksGiven ?? 0, (_) => roleIcon(Role.king)), const SizedBox(width: 3), Text( // a text font size of 14 is used to ensure that the text will take more vertical space diff --git a/test/example_data.dart b/test/example_data.dart index 8f6759267..01cf0193a 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -77,7 +77,7 @@ IList makeSteps(String pgn) { GameStep( position: position, sanMove: SanMove(san, move), - diff: MaterialDiff.fromBoard(position.board), + diff: MaterialDiff.fromPosition(position), ), ); } diff --git a/test/model/game/game_storage_test.dart b/test/model/game/game_storage_test.dart index c9aa224e7..0708b3fe2 100644 --- a/test/model/game/game_storage_test.dart +++ b/test/model/game/game_storage_test.dart @@ -58,7 +58,7 @@ IList _makeSteps(String pgn) { GameStep( position: position, sanMove: SanMove(san, move), - diff: MaterialDiff.fromBoard(position.board), + diff: MaterialDiff.fromPosition(position), ), ); } diff --git a/test/model/game/material_diff_test.dart b/test/model/game/material_diff_test.dart index 264f25b58..3dfd6e909 100644 --- a/test/model/game/material_diff_test.dart +++ b/test/model/game/material_diff_test.dart @@ -5,9 +5,12 @@ import 'package:lichess_mobile/src/model/game/material_diff.dart'; void main() { group('GameMaterialDiff', () { - test('generation from board', () { - final Board board = Board.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1'); - final MaterialDiff diff = MaterialDiff.fromBoard(board); + test('generation from position', () { + final Position position = Position.setupPosition( + Rule.chess, + Setup.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1'), + ); + final MaterialDiff diff = MaterialDiff.fromPosition(position); expect(diff.bySide(Side.black).score, equals(-10)); expect(diff.bySide(Side.white).score, equals(10)); @@ -63,6 +66,51 @@ void main() { }), ), ); + + expect(diff.bySide(Side.white).checksGiven, null); + expect(diff.bySide(Side.black).checksGiven, null); + }); + test('three-check', () { + final Position position = Position.setupPosition( + Rule.threecheck, + Setup.parseFen('rnbqkbnr/ppp1pppp/3p4/1B6/4P3/8/PPPP1PPP/RNBQK1NR b KQkq - 2+3 1 2'), + ); + final MaterialDiff diff = MaterialDiff.fromPosition(position); + + expect(diff.bySide(Side.white).checksGiven, 1); + expect(diff.bySide(Side.black).checksGiven, 0); + }); + + test('horde returns empty material diff', () { + final Position position = Position.setupPosition( + Rule.horde, + Setup.parseFen('rnbqkbnr/pppppppp/8/8/pppppppp/pppppppp/PPPPPPPP/PPPPPPPP w kq - 0 1'), + ); + final MaterialDiff diff = MaterialDiff.fromPosition(position); + + for (final side in Side.values) { + final sideDiff = diff.bySide(side); + expect(sideDiff.score, 0); + expect(sideDiff.pieces.isEmpty, isTrue); + expect(sideDiff.capturedPieces.isEmpty, isTrue); + expect(sideDiff.checksGiven, null); + } + }); + + test('crazyhouse returns empty material diff', () { + final Position position = Position.setupPosition( + Rule.crazyhouse, + Setup.parseFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'), + ); + final MaterialDiff diff = MaterialDiff.fromPosition(position); + + for (final side in Side.values) { + final sideDiff = diff.bySide(side); + expect(sideDiff.score, 0); + expect(sideDiff.pieces.isEmpty, isTrue); + expect(sideDiff.capturedPieces.isEmpty, isTrue); + expect(sideDiff.checksGiven, null); + } }); }); }