Skip to content

Commit

Permalink
Merge pull request #1474 from lichess-org/eval_in_move_list
Browse files Browse the repository at this point in the history
Show eval in move list when 2 columns
  • Loading branch information
veloce authored Feb 21, 2025
2 parents 5017cb8 + 3018e7d commit 848b32b
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 46 deletions.
12 changes: 8 additions & 4 deletions lib/src/model/analysis/analysis_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -515,9 +515,10 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier
}
}

void _refreshCurrentNode() {
void _refreshCurrentNode({bool shouldRecomputeRootView = false}) {
state = AsyncData(
state.requireValue.copyWith(
root: shouldRecomputeRootView ? _root.view : state.requireValue.root,
currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)),
),
);
Expand Down Expand Up @@ -563,8 +564,11 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier
?.forEach((t) {
final (work, eval) = t;
_root.updateAt(work.path, (node) => node.eval = eval);
if (work.path == curState.currentPath && eval.searchTime >= work.searchTime) {
_refreshCurrentNode();
if (work.path == curState.currentPath) {
_refreshCurrentNode(
shouldRecomputeRootView:
eval.evalString != state.valueOrNull?.currentNode.eval?.evalString,
);
}
});
}
Expand All @@ -578,7 +582,7 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier
void _stopEngineEval() {
ref.read(evaluationServiceProvider).stop();
// update the current node with last cached eval
_refreshCurrentNode();
_refreshCurrentNode(shouldRecomputeRootView: true);
}

void _listenToServerAnalysisEvents() {
Expand Down
15 changes: 9 additions & 6 deletions lib/src/model/broadcast/broadcast_analysis_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController implemen
final pgnHeaders = IMap(game.headers);
final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c)));

_root = Root.fromPgnGame(game);
_root = Root.fromPgnGame(game, isLichessAnalysis: true);
final currentPath = _root.mainlinePath;
final currentNode = _root.nodeAt(currentPath);
final lastMove = _root.branchAt(_root.mainlinePath)?.sanMove.move;
Expand Down Expand Up @@ -520,15 +520,19 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController implemen
?.forEach((t) {
final (work, eval) = t;
_root.updateAt(work.path, (node) => node.eval = eval);
if (work.path == state.requireValue.currentPath && eval.searchTime >= work.searchTime) {
_refreshCurrentNode();
if (work.path == state.requireValue.currentPath) {
_refreshCurrentNode(
shouldRecomputeRootView:
eval.evalString != state.valueOrNull?.currentNode.eval?.evalString,
);
}
});
}

void _refreshCurrentNode() {
void _refreshCurrentNode({bool shouldRecomputeRootView = false}) {
state = AsyncData(
state.requireValue.copyWith(
root: shouldRecomputeRootView ? _root.view : state.requireValue.root,
currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)),
),
);
Expand All @@ -544,8 +548,7 @@ class BroadcastAnalysisController extends _$BroadcastAnalysisController implemen
if (!state.hasValue) return;

ref.read(evaluationServiceProvider).stop();
// update the current node with last cached eval
_refreshCurrentNode();
_refreshCurrentNode(shouldRecomputeRootView: true);
}

({Duration? parentClock, Duration? clock}) _getClocks(UciPath path) {
Expand Down
14 changes: 14 additions & 0 deletions lib/src/model/common/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,20 @@ class ViewBranch extends ViewNode with _$ViewBranch {
return clockComment?.emt;
}

/// The evaluation from the PGN comments.
///
/// For now we only trust the eval coming from lichess analysis.
ExternalEval? get serverEval {
final pgnEval = lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval;
return pgnEval != null
? ExternalEval(
cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null,
mate: pgnEval.mate,
depth: pgnEval.depth,
)
: null;
}

@override
UciCharPair get id => UciCharPair.fromMove(sanMove.move);
}
2 changes: 1 addition & 1 deletion lib/src/model/engine/evaluation_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class EvaluationService {

final evalStream = engine
.start(work)
.throttle(const Duration(milliseconds: 200), trailing: true);
.throttle(const Duration(milliseconds: 300), trailing: true);

evalStream.forEach((t) {
final (work, eval) = t;
Expand Down
13 changes: 8 additions & 5 deletions lib/src/model/study/study_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -478,9 +478,10 @@ class StudyController extends _$StudyController implements PgnTreeNotifier {
}
}

void _refreshCurrentNode() {
void _refreshCurrentNode({bool shouldRecomputeRootView = false}) {
state = AsyncData(
state.requireValue.copyWith(
root: shouldRecomputeRootView ? _root.view : state.requireValue.root,
currentNode: StudyCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)),
),
);
Expand All @@ -502,8 +503,11 @@ class StudyController extends _$StudyController implements PgnTreeNotifier {
?.forEach((t) {
final (work, eval) = t;
_root.updateAt(work.path, (node) => node.eval = eval);
if (work.path == state.requireValue.currentPath && eval.searchTime >= work.searchTime) {
_refreshCurrentNode();
if (work.path == state.requireValue.currentPath) {
_refreshCurrentNode(
shouldRecomputeRootView:
eval.evalString != state.valueOrNull?.currentNode.eval?.evalString,
);
}
});
}
Expand All @@ -519,8 +523,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier {

if (!state.hasValue) return;

// update the current node with last cached eval
_refreshCurrentNode();
_refreshCurrentNode(shouldRecomputeRootView: true);
}
}

Expand Down
12 changes: 4 additions & 8 deletions lib/src/view/analysis/tree_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,17 @@ class AnalysisTreeView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final ctrlProvider = analysisControllerProvider(options);

final root = ref.watch(ctrlProvider.select((value) => value.requireValue.root));
final currentPath = ref.watch(ctrlProvider.select((value) => value.requireValue.currentPath));
final pgnRootComments = ref.watch(
ctrlProvider.select((value) => value.requireValue.pgnRootComments),
);
final analysisState = ref.watch(ctrlProvider).requireValue;
final prefs = ref.watch(analysisPreferencesProvider);
// enable computer analysis takes effect here only if it's a lichess game
final enableComputerAnalysis = !options.isLichessGameAnalysis || prefs.enableComputerAnalysis;

return SingleChildScrollView(
padding: EdgeInsets.zero,
child: DebouncedPgnTreeView(
root: root,
currentPath: currentPath,
pgnRootComments: pgnRootComments,
root: analysisState.root,
currentPath: analysisState.currentPath,
pgnRootComments: analysisState.pgnRootComments,
notifier: ref.read(ctrlProvider.notifier),
shouldShowComputerVariations: enableComputerAnalysis,
shouldShowComments: enableComputerAnalysis && prefs.showPgnComments,
Expand Down
15 changes: 4 additions & 11 deletions lib/src/view/study/study_tree_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,13 @@ class StudyTreeView extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final studyState = ref.watch(studyControllerProvider(id)).requireValue;
final root =
ref.watch(studyControllerProvider(id).select((value) => value.requireValue.root)) ??
studyState.root ??
// If root is null, the study chapter's position is illegal.
// We still want to display the root comments though, so create a dummy root.
const ViewRoot(position: Chess.initial, children: IList.empty());

final currentPath = ref.watch(
studyControllerProvider(id).select((value) => value.requireValue.currentPath),
);

final pgnRootComments = ref.watch(
studyControllerProvider(id).select((value) => value.requireValue.pgnRootComments),
);

final analysisPrefs = ref.watch(analysisPreferencesProvider);

return CustomScrollView(
Expand All @@ -43,8 +36,8 @@ class StudyTreeView extends ConsumerWidget {
Expanded(
child: DebouncedPgnTreeView(
root: root,
currentPath: currentPath,
pgnRootComments: pgnRootComments,
currentPath: studyState.currentPath,
pgnRootComments: studyState.pgnRootComments,
notifier: ref.read(studyControllerProvider(id).notifier),
shouldShowAnnotations: analysisPrefs.showAnnotations,
displayMode:
Expand Down
43 changes: 32 additions & 11 deletions lib/src/widgets/pgn.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ class DebouncedPgnTreeView extends ConsumerStatefulWidget {
/// Only applied to lichess game analysis.
final bool shouldShowComputerVariations;

/// Display mode of the tree view.
///
/// Either [PgnTreeDisplayMode.twoColumn] or [PgnTreeDisplayMode.inlineNotation].
final PgnTreeDisplayMode displayMode;

/// Whether to show NAG annotations like '!' and '??'.
Expand Down Expand Up @@ -703,6 +706,7 @@ class _TwoColumnMainlinePart extends ConsumerWidget {
path: path + mainlineNode.id,
params: params,
showIndex: false,
showEval: true,
);
path = path + mainlineNode.id;
return move as Widget;
Expand Down Expand Up @@ -1162,6 +1166,7 @@ class InlineMove extends ConsumerWidget {
required this.lineInfo,
required this.params,
this.showIndex = true,
this.showEval = false,
super.key,
});

Expand All @@ -1175,6 +1180,7 @@ class InlineMove extends ConsumerWidget {
final _PgnTreeViewParams params;

final bool showIndex;
final bool showEval;

static const borderRadius = BorderRadius.all(Radius.circular(4.0));

Expand All @@ -1199,14 +1205,12 @@ class InlineMove extends ConsumerWidget {
.watch(pieceNotationProvider)
.maybeWhen(data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation);
final moveFontFamily = pieceNotation == PieceNotation.symbol ? 'ChessFont' : null;

final moveTextStyle = textStyle.copyWith(
fontFamily: moveFontFamily,
fontWeight: lineInfo.type == _LineType.inlineSideline ? FontWeight.normal : FontWeight.w600,
);

final indexTextStyle = textStyle.copyWith(color: _textColor(context, kIndexOpacity));

final indexText =
showIndex
? branch.position.ply.isOdd
Expand All @@ -1223,8 +1227,11 @@ class InlineMove extends ConsumerWidget {
: '');

final nag = params.shouldShowAnnotations ? branch.nags?.firstOrNull : null;

final ply = branch.position.ply;

final eval =
params.shouldShowComputerVariations && showEval ? branch.eval ?? branch.serverEval : null;

return AdaptiveInkWell(
key: isCurrentMove ? params.currentMoveKey : null,
borderRadius: borderRadius,
Expand All @@ -1251,18 +1258,32 @@ class InlineMove extends ConsumerWidget {
child: Container(
padding: kInlineMovePadding,
decoration: _boxDecoration(context, isCurrentMove, isBroadcastLiveMove),
child: Text.rich(
TextSpan(
children: [
if (indexText != null) indexText,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text.rich(
TextSpan(
text: moveWithNag,
children: [
if (indexText != null) indexText,
TextSpan(
text: moveWithNag,
style: moveTextStyle.copyWith(
color: _textColor(context, isCurrentMove ? 1 : 0.9, nag: nag),
),
),
],
),
),
if (eval != null)
Text(
eval.evalString,
style: moveTextStyle.copyWith(
color: _textColor(context, isCurrentMove ? 1 : 0.9, nag: nag),
fontSize: moveTextStyle.fontSize != null ? moveTextStyle.fontSize! - 3.0 : null,
color: _textColor(context, 0.4),
),
),
],
),
],
),
),
);
Expand Down

0 comments on commit 848b32b

Please sign in to comment.