diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index d651278256..6635ef372b 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -40,6 +40,7 @@ class AnalysisOptions with _$AnalysisOptions { required String pgn, int? initialMoveCursor, LightOpening? opening, + Division? division, /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index 946f0cb37c..8349898cd3 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -168,6 +168,17 @@ class LightOpening with _$LightOpening implements Opening { _$LightOpeningFromJson(json); } +@Freezed(fromJson: true, toJson: true) +class Division with _$Division { + const factory Division({ + double? middlegame, + double? endgame, + }) = _Division; + + factory Division.fromJson(Map json) => + _$DivisionFromJson(json); +} + @freezed class FullOpening with _$FullOpening implements Opening { const FullOpening._(); diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 2d41b9a91f..657bc947cb 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -127,6 +127,7 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) { final clocks = pick('clocks').asListOrNull( (p0) => Duration(milliseconds: p0.asIntOrThrow() * 10), ); + final division = pick('division').letOrNull(_divisionFromPick); final initialFen = pick('initialFen').asStringOrNull(); @@ -147,6 +148,7 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) { ) : null, opening: data.opening, + division: division, ), data: data, status: data.status, @@ -243,3 +245,10 @@ PlayerAnalysis _playerAnalysisFromPick(RequiredPick pick) { accuracy: pick('accuracy').asIntOrNull(), ); } + +Division _divisionFromPick(RequiredPick pick) { + return Division( + middlegame: pick('middle').asDoubleOrNull(), + endgame: pick('end').asDoubleOrNull(), + ); +} diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 5d148c7388..c8e42f32bd 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -305,6 +305,9 @@ class GameMeta with _$GameMeta { /// Opening of the game, only available once finished LightOpening? opening, + + /// Game phases of the game, only avaible once finished + Division? division, }) = _GameMeta; factory GameMeta.fromJson(Map json) => diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 183835a81c..23802d2cce 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -961,5 +961,6 @@ class GameState with _$GameState { id: gameFullId, opening: game.meta.opening, serverAnalysis: game.serverAnalysis, + division: game.meta.division, ); } diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index c6faa1eb8f..64e372d8a2 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -223,6 +223,7 @@ GameMeta _playableGameMetaFromPick(RequiredPick pick) { ), ), ), + division: pick('division').letOrNull(_divisionFromPick), ); } @@ -273,3 +274,10 @@ Message _messageFromPick(RequiredPick pick) { username: pick('u').asStringOrThrow(), ); } + +Division _divisionFromPick(RequiredPick pick) { + return Division( + middlegame: pick('middle').asDoubleOrNull(), + endgame: pick('end').asDoubleOrNull(), + ); +} diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index c419836d9e..a0c7be8d28 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -800,6 +800,8 @@ class ServerAnalysisSummary extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AcplChart(options), + // may be removed if game phases text is displayed vertically instead of horizontally + const SizedBox(height: 10.0), Center( child: SizedBox( width: math.min(MediaQuery.sizeOf(context).width, 500), @@ -1030,6 +1032,17 @@ class AcplChart extends ConsumerWidget { final belowLineColor = Colors.white.withOpacity(0.7); final aboveLineColor = Colors.grey.shade800.withOpacity(0.8); + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: const TextStyle(fontSize: 10), + labelResolver: (line) => label, + show: true, + ), + ); + final data = ref.watch( analysisControllerProvider(options) .select((value) => value.acplChartData), @@ -1116,6 +1129,14 @@ class AcplChart extends ConsumerWidget { color: mainLineColor, strokeWidth: 1.0, ), + phaseVerticalBar(0.0, 'Opening'), + if (options.division?.endgame != null) + phaseVerticalBar(options.division!.endgame!, 'Endgame'), + if (options.division?.middlegame != null) + phaseVerticalBar( + options.division!.middlegame!, + 'Middlegame', + ), ], ), gridData: const FlGridData(show: false), diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 535bbbe64d..f17691bc49 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -295,6 +295,7 @@ class _BodyState extends ConsumerState<_Body> { initialMoveCursor: stepCursor, orientation: game.youAre, id: game.id, + division: game.meta.division, ), title: context.l10n.analysis, ), diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 055de29ce8..94f7eef747 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -287,6 +287,7 @@ class _BottomBar extends ConsumerWidget { id: gameData.id, opening: gameData.opening, serverAnalysis: game.serverAnalysis, + division: game.meta.division, ), ), ); diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index c3285ffcfc..e6b52ce069 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -151,6 +151,7 @@ class _ContextMenu extends ConsumerWidget { id: game.id, opening: game.opening, serverAnalysis: serverAnalysis, + division: archivedGame.meta.division, ), ), ); diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index c6eeb71c70..5532db1472 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -80,7 +80,7 @@ void main() { group('GameRepository.getGame', () { test('game with clocks', () async { const testResponse = ''' -{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180}} +{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"division":{"middle":18,"end":42}} '''; when( @@ -104,7 +104,7 @@ void main() { test('threeCheck game', () async { const testResponse = ''' -{"id":"1vdsvmxp","rated":true,"variant":"threeCheck","speed":"bullet","perf":"threeCheck","createdAt":1604194310939,"lastMoveAt":1604194361831,"status":"variantEnd","players":{"white":{"user":{"name":"Zhigalko_Sergei","title":"GM","patron":true,"id":"zhigalko_sergei"},"rating":2448,"ratingDiff":6,"analysis":{"inaccuracy":1,"mistake":1,"blunder":1,"acpl":75}},"black":{"user":{"name":"catask","id":"catask"},"rating":2485,"ratingDiff":-6,"analysis":{"inaccuracy":1,"mistake":0,"blunder":2,"acpl":115}}},"winner":"white","opening":{"eco":"B02","name":"Alekhine Defense: Scandinavian Variation, Geschev Gambit","ply":6},"moves":"e4 c6 Nc3 d5 exd5 Nf6 Nf3 e5 Bc4 Bd6 O-O O-O h3 e4 Kh1 exf3 Qxf3 cxd5 Nxd5 Nxd5 Bxd5 Nc6 Re1 Be6 Rxe6 fxe6 Bxe6+ Kh8 Qh5 h6 Qg6 Qf6 Qh7+ Kxh7 Bf5+","analysis":[{"eval":340},{"eval":359},{"eval":231},{"eval":300,"best":"g8f6","variation":"Nf6 e5 d5 d4 Ne4 Bd3 Bf5 Nf3 e6 O-O","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nf6 was best."}},{"eval":262},{"eval":286},{"eval":184,"best":"f1c4","variation":"Bc4 e6 dxe6 Bxe6 Bxe6 fxe6 Qe2 Qd7 Nf3 Bd6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bc4 was best."}},{"eval":235},{"eval":193},{"eval":243},{"eval":269},{"eval":219},{"eval":-358,"best":"d2d3","variation":"d3 Bg4 h3 e4 Nxe4 Bh2+ Kh1 Nxe4 dxe4 Qf6","judgment":{"name":"Blunder","comment":"Blunder. d3 was best."}},{"eval":-376},{"eval":-386},{"eval":-383},{"eval":-405},{"eval":-363},{"eval":-372},{"eval":-369},{"eval":-345},{"eval":-276},{"eval":-507,"best":"b2b3","variation":"b3 Be6","judgment":{"name":"Mistake","comment":"Mistake. b3 was best."}},{"eval":-49,"best":"c6e5","variation":"Ne5 Qh5","judgment":{"name":"Blunder","comment":"Blunder. Ne5 was best."}},{"eval":-170},{"mate":7,"best":"g8h8","variation":"Kh8 Rh6","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. Kh8 was best."}},{"mate":6},{"mate":6},{"mate":5},{"mate":3},{"mate":2},{"mate":2},{"mate":1},{"mate":1}],"clock":{"initial":60,"increment":0,"totalTime":60}} +{"id":"1vdsvmxp","rated":true,"variant":"threeCheck","speed":"bullet","perf":"threeCheck","createdAt":1604194310939,"lastMoveAt":1604194361831,"status":"variantEnd","players":{"white":{"user":{"name":"Zhigalko_Sergei","title":"GM","patron":true,"id":"zhigalko_sergei"},"rating":2448,"ratingDiff":6,"analysis":{"inaccuracy":1,"mistake":1,"blunder":1,"acpl":75}},"black":{"user":{"name":"catask","id":"catask"},"rating":2485,"ratingDiff":-6,"analysis":{"inaccuracy":1,"mistake":0,"blunder":2,"acpl":115}}},"winner":"white","opening":{"eco":"B02","name":"Alekhine Defense: Scandinavian Variation, Geschev Gambit","ply":6},"moves":"e4 c6 Nc3 d5 exd5 Nf6 Nf3 e5 Bc4 Bd6 O-O O-O h3 e4 Kh1 exf3 Qxf3 cxd5 Nxd5 Nxd5 Bxd5 Nc6 Re1 Be6 Rxe6 fxe6 Bxe6+ Kh8 Qh5 h6 Qg6 Qf6 Qh7+ Kxh7 Bf5+","analysis":[{"eval":340},{"eval":359},{"eval":231},{"eval":300,"best":"g8f6","variation":"Nf6 e5 d5 d4 Ne4 Bd3 Bf5 Nf3 e6 O-O","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nf6 was best."}},{"eval":262},{"eval":286},{"eval":184,"best":"f1c4","variation":"Bc4 e6 dxe6 Bxe6 Bxe6 fxe6 Qe2 Qd7 Nf3 Bd6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bc4 was best."}},{"eval":235},{"eval":193},{"eval":243},{"eval":269},{"eval":219},{"eval":-358,"best":"d2d3","variation":"d3 Bg4 h3 e4 Nxe4 Bh2+ Kh1 Nxe4 dxe4 Qf6","judgment":{"name":"Blunder","comment":"Blunder. d3 was best."}},{"eval":-376},{"eval":-386},{"eval":-383},{"eval":-405},{"eval":-363},{"eval":-372},{"eval":-369},{"eval":-345},{"eval":-276},{"eval":-507,"best":"b2b3","variation":"b3 Be6","judgment":{"name":"Mistake","comment":"Mistake. b3 was best."}},{"eval":-49,"best":"c6e5","variation":"Ne5 Qh5","judgment":{"name":"Blunder","comment":"Blunder. Ne5 was best."}},{"eval":-170},{"mate":7,"best":"g8h8","variation":"Kh8 Rh6","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. Kh8 was best."}},{"mate":6},{"mate":6},{"mate":5},{"mate":3},{"mate":2},{"mate":2},{"mate":1},{"mate":1}],"clock":{"initial":60,"increment":0,"totalTime":60},"division":{"middle":18,"end":42}} '''; when(