Skip to content

Commit

Permalink
feat: scale arrows in analysis based on winning chances
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-anders committed Jul 8, 2024
1 parent 2f45b10 commit 0d76fdf
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 44 deletions.
43 changes: 34 additions & 9 deletions lib/src/model/common/eval.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:math' as math;

import 'package:collection/collection.dart';
import 'package:dartchess/dartchess.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
Expand Down Expand Up @@ -47,6 +48,16 @@ class ExternalEval with _$ExternalEval implements Eval {
}
}

double _toWhiteWinningChanges(int? cp, int? mate) {
if (mate != null) {
return mateWinningChances(mate);
} else if (cp != null) {
return cpWinningChances(cp);
} else {
return 0;
}
}

/// The eval from the client's own engine, typically stockfish.
@freezed
class ClientEval with _$ClientEval implements Eval {
Expand All @@ -71,10 +82,11 @@ class ClientEval with _$ClientEval implements Eval {
return Move.fromUci(uci);
}

IList<Move?> get bestMoves {
IList<MoveWithWinningChanges> bestMoves() {
return pvs
.where((e) => e.moves.isNotEmpty)
.map((e) => Move.fromUci(e.moves.first))
.map((e) => e.firstMoveWithWinningChances(position.turn))
.whereNotNull()
.toIList();
}

Expand All @@ -89,13 +101,7 @@ class ClientEval with _$ClientEval implements Eval {
double winningChances(Side side) => _toPov(side, _whiteWinningChances);

double get _whiteWinningChances {
if (mate != null) {
return mateWinningChances(mate!);
} else if (cp != null) {
return cpWinningChances(cp!);
} else {
return 0;
}
return _toWhiteWinningChanges(cp, mate);
}
}

Expand Down Expand Up @@ -136,6 +142,25 @@ class PvData with _$PvData {
}
return res;
}

MoveWithWinningChanges? firstMoveWithWinningChances(Side sideToMove) {
final uciMove = (moves.isNotEmpty) ? Move.fromUci(moves.first) : null;
return (uciMove != null)
? MoveWithWinningChanges(
move: uciMove,
winningChances: _toPov(sideToMove, _toWhiteWinningChanges(cp, mate)),
)
: null;
}
}

@freezed
class MoveWithWinningChanges with _$MoveWithWinningChanges {
const MoveWithWinningChanges._();
const factory MoveWithWinningChanges({
required Move move,
required double winningChances,
}) = _MoveWithWinningChanges;
}

double cpToPawns(int cp) => cp / 100;
Expand Down
97 changes: 62 additions & 35 deletions lib/src/view/analysis/analysis_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,65 @@ class _Board extends ConsumerStatefulWidget {
class _BoardState extends ConsumerState<_Board> {
ISet<cg.Shape> userShapes = ISet();

ISet<cg.Shape> _computeBestMoveShapes(IList<MoveWithWinningChanges> moves) {
// Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move
// (assume moves are ordered by their winning chances, so index==0 is the best move)
double scaleArrowAgainstBestMove(int index) {
final bestMove = moves[0];
final winningDiffComparedToBestMove =
bestMove.winningChances - moves[index].winningChances;
const double minScale = 0.25;
// Force minimum scale if...
// 1) The best move is significantly better than this move
// 2) The signs differ, e.g. the best move is winning while this move draws (or even looses)
if (winningDiffComparedToBestMove > 0.3 ||
bestMove.winningChances.sign != moves[index].winningChances.sign) {
return minScale;
}
return math.max(
minScale,
1.4 - 5.0 * winningDiffComparedToBestMove,
);
}

return ISet(
moves.mapIndexed(
(i, m) {
final move = m.move;
// Like in the Web UI, the best move has a slightly higher opacity,
// all other moves have the same lower opacity
final color =
const Color(0x40003088).withOpacity((i == 0) ? 0.4 : 0.25);
switch (move) {
case NormalMove(from: _, to: _, promotion: final promRole):
return [
cg.Arrow(
color: color,
orig: move.cg.from,
dest: move.cg.to,
scale: scaleArrowAgainstBestMove(i),
),
if (promRole != null)
cg.PieceShape(
color: color,
orig: move.cg.to,
role: promRole.cg,
),
];
case DropMove(role: final role, to: _):
return [
cg.PieceShape(
color: color,
orig: move.cg.to,
role: role.cg,
),
];
}
},
).expand((e) => e),
);
}

@override
Widget build(BuildContext context) {
final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options);
Expand All @@ -389,52 +448,20 @@ class _BoardState extends ConsumerState<_Board> {
);

final evalBestMoves = ref.watch(
engineEvaluationProvider.select((s) => s.eval?.bestMoves),
engineEvaluationProvider.select((s) => s.eval?.bestMoves()),
);

final currentNode = analysisState.currentNode;
final annotation = makeAnnotation(currentNode.nags);

final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves;
final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves();

final sanMove = currentNode.sanMove;

final ISet<cg.Shape> bestMoveShapes = showBestMoveArrow &&
analysisState.isEngineAvailable &&
bestMoves != null
? ISet(
bestMoves.where((move) => move != null).mapIndexed(
(i, move) {
switch (move!) {
case NormalMove(from: _, to: _, promotion: final promRole):
return [
cg.Arrow(
color:
const Color(0x40003088).withOpacity(0.4 - 0.15 * i),
orig: move.cg.from,
dest: move.cg.to,
),
if (promRole != null)
cg.PieceShape(
color: const Color(0x40003088)
.withOpacity(0.4 - 0.15 * i),
orig: move.cg.to,
role: promRole.cg,
),
];
case DropMove(role: final role, to: _):
return [
cg.PieceShape(
color:
const Color(0x40003088).withOpacity(0.4 - 0.15 * i),
orig: move.cg.to,
role: role.cg,
),
];
}
},
).expand((e) => e),
)
? _computeBestMoveShapes(bestMoves)
: ISet();

return cg.Board(
Expand Down

0 comments on commit 0d76fdf

Please sign in to comment.