Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 148 additions & 47 deletions lib/src/model/analysis/retro_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ final retroControllerProvider = AsyncNotifierProvider.autoDispose
name: 'RetroControllerProvider',
);

class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMixin {
class RetroController extends AsyncNotifier<RetroState>
with EngineEvaluationMixin {
RetroController(this.options);

final RetroOptions options;
Expand All @@ -103,31 +104,20 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
final serverAnalysisService = ref.watch(serverAnalysisServiceProvider);

ref.onDispose(() {
serverAnalysisService.lastAnalysisEvent.removeListener(_listenToServerAnalysisEvents);
serverAnalysisService.lastAnalysisEvent.removeListener(
_listenToServerAnalysisEvents,
);
});

socketClient = ref.watch(socketPoolProvider).open(AnalysisController.socketUri);
socketClient = ref
.watch(socketPoolProvider)
.open(AnalysisController.socketUri);

_game = await ref.watch(archivedGameProvider(options.id).future);

_root = _game.makeTree();

if (_game.serverAnalysis == null) {
await serverAnalysisService.requestAnalysis(options.id);

_serverAnalysisCompleter.future.timeout(
kMaxWaitForServerAnalysis,
onTimeout: () {
_logger.warning(
'Server analysis did not finish within $kMaxWaitForServerAnalysis for game ${options.id}',
);
state = AsyncError(
Exception('Server analysis did not finish within $kMaxWaitForServerAnalysis'),
StackTrace.current,
);
},
);

final retroState = RetroState(
serverAnalysisAvailable: false,
mistakes: const IList.empty(),
Expand All @@ -148,9 +138,66 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix

state = AsyncValue.data(retroState);

serverAnalysisService.lastAnalysisEvent.addListener(_listenToServerAnalysisEvents);
// Attach listener BEFORE possibly requesting analysis,
// so we don't miss the first progress event.
serverAnalysisService.lastAnalysisEvent.addListener(
_listenToServerAnalysisEvents,
);

// Reuse an already available event immediately if it belongs to this game.
final existingEvent = serverAnalysisService.lastAnalysisEvent.value;
if (existingEvent != null && existingEvent.$1 == options.id) {
ServerAnalysisService.mergeOngoingAnalysis(
_root,
existingEvent.$2.tree,
);

final progress =
existingEvent.$2.evals.where((e) => e.hasEval).length /
_root.mainline.length;

state = AsyncValue.data(
state.requireValue.copyWith(serverAnalysisProgress: progress),
);

if (existingEvent.$2.isAnalysisComplete) {
if (!_serverAnalysisCompleter.isCompleted) {
_serverAnalysisCompleter.complete();
}

state = AsyncData(await _computeMistakes(options.initialSide));

socketClient.firstConnection.then((_) {
requestEval();
});

return state.requireValue;
}
}

// Only request analysis if this exact game is not already being analyzed.
if (serverAnalysisService.currentAnalysis.value != options.id) {
await serverAnalysisService.requestAnalysis(options.id);
}

unawaited(
_serverAnalysisCompleter.future.timeout(
kMaxWaitForServerAnalysis,
onTimeout: () {
_logger.warning(
'Server analysis did not finish within $kMaxWaitForServerAnalysis for game ${options.id}',
);
state = AsyncError(
Exception(
'Server analysis did not finish within $kMaxWaitForServerAnalysis',
),
StackTrace.current,
);
},
),
);

return retroState;
return state.requireValue;
}

state = AsyncData(await _computeMistakes(options.initialSide));
Expand All @@ -177,9 +224,11 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
}

final bigEvalSwing =
Eval.winningChancesPovDiff(side, eval, newEval).abs() > _kEvalSwingThreshold;
Eval.winningChancesPovDiff(side, eval, newEval).abs() >
_kEvalSwingThreshold;

final lostEasyMate = eval.mate != null && newEval.mate == null && eval.mate!.abs() <= 3;
final lostEasyMate =
eval.mate != null && newEval.mate == null && eval.mate!.abs() <= 3;

final hasSolution = branch.children.length > 1;

Expand All @@ -192,7 +241,10 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
try {
final entry = await ref
.read(openingExplorerRepositoryProvider)
.getMasterDatabase(branch.position.fen, since: MasterDb.kEarliestYear);
.getMasterDatabase(
branch.position.fen,
since: MasterDb.kEarliestYear,
);

final masterMovesPlayedMoreThanOnce = entry.moves.where(
(move) => move.white + move.draws + move.black > 1,
Expand All @@ -216,18 +268,25 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
}
}

return Mistake(branch: branch.view, openingExplorerSolutions: openingExplorerSolutions);
return Mistake(
branch: branch.view,
openingExplorerSolutions: openingExplorerSolutions,
);
}),
)).nonNulls.toIList();

return RetroState(
serverAnalysisAvailable: true,
mistakes: mistakes.toIList(),
currentMistakeIndex: 0,
feedback: mistakes.isNotEmpty ? RetroFeedback.findMove : RetroFeedback.done,
feedback: mistakes.isNotEmpty
? RetroFeedback.findMove
: RetroFeedback.done,
mainlinePath: _root.mainlinePath,
pov: side,
currentNode: RetroCurrentNode.fromNode(mistakes.firstOrNull?.branch.branch ?? _root),
currentNode: RetroCurrentNode.fromNode(
mistakes.firstOrNull?.branch.branch ?? _root,
),
lastMove: mistakes.firstOrNull?.branch.sanMove.move,
variant: _game.meta.variant,
root: _root.view,
Expand All @@ -245,12 +304,16 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
void onUserMove(Move move) {
if (!state.requireValue.currentPosition.isLegal(move)) return;

if (move case NormalMove() when isPromotionPawnMove(state.requireValue.currentPosition, move)) {
if (move case NormalMove()
when isPromotionPawnMove(state.requireValue.currentPosition, move)) {
state = AsyncValue.data(state.requireValue.copyWith(promotionMove: move));
return;
}

final (newPath, isNewNode) = _root.addMoveAt(state.requireValue.currentPath, move);
final (newPath, isNewNode) = _root.addMoveAt(
state.requireValue.currentPath,
move,
);
if (newPath != null) {
_setPath(newPath);
}
Expand Down Expand Up @@ -287,12 +350,16 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
final currentMistake = state.value?.currentMistake;
if (currentMistake != null) {
onUserMove(currentMistake.serverMove);
state = AsyncValue.data(state.requireValue.copyWith(feedback: RetroFeedback.viewingSolution));
state = AsyncValue.data(
state.requireValue.copyWith(feedback: RetroFeedback.viewingSolution),
);
}
}

Future<void> flipSide() async {
state = AsyncValue.data(await _computeMistakes(state.requireValue.pov.opposite));
state = AsyncValue.data(
await _computeMistakes(state.requireValue.pov.opposite),
);
}

void restart() {
Expand All @@ -309,7 +376,9 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix

_setPath(
_root.mainlinePath.truncate(
mistake?.branch.position.ply ?? lastMistake?.branch.position.ply ?? _root.mainlinePath.size,
mistake?.branch.position.ply ??
lastMistake?.branch.position.ply ??
_root.mainlinePath.size,
),
);

Expand Down Expand Up @@ -342,7 +411,9 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
if (!isNavigating && isForward) {
final isCheck = currentNode.sanMove.isCheck;
if (currentNode.sanMove.isCapture) {
ref.read(moveFeedbackServiceProvider).captureFeedback(state.variant, check: isCheck);
ref
.read(moveFeedbackServiceProvider)
.captureFeedback(state.variant, check: isCheck);
} else {
ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck);
}
Expand Down Expand Up @@ -379,20 +450,26 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
}

if (pathChange) {
this.state = AsyncValue.data(this.state.requireValue.copyWith(engineInThreatMode: false));
this.state = AsyncValue.data(
this.state.requireValue.copyWith(engineInThreatMode: false),
);
requestEval();
}

_updateFeedback();
}

void _onIncorrectMove() {
state = AsyncValue.data(state.requireValue.copyWith(feedback: RetroFeedback.incorrect));
state = AsyncValue.data(
state.requireValue.copyWith(feedback: RetroFeedback.incorrect),
);
userPrevious();
}

void _onCorrectMove() {
state = AsyncValue.data(state.requireValue.copyWith(feedback: RetroFeedback.correct));
state = AsyncValue.data(
state.requireValue.copyWith(feedback: RetroFeedback.correct),
);
}

@override
Expand Down Expand Up @@ -428,7 +505,9 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
void _refreshCurrentNode({bool recomputeRootView = false}) {
state = AsyncData(
state.requireValue.copyWith(
currentNode: RetroCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)),
currentNode: RetroCurrentNode.fromNode(
_root.nodeAt(state.requireValue.currentPath),
),
),
);
}
Expand All @@ -438,16 +517,21 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
switch (state.feedback) {
case RetroFeedback.incorrect:
case RetroFeedback.findMove:
if (state.currentPosition.ply == state.currentMistake!.serverBranch.position.ply) {
if (state.currentPosition.ply ==
state.currentMistake!.serverBranch.position.ply) {
if (state.currentMistake!.isSolution(state.currentNode)) {
_onCorrectMove();
} else if (state.currentPosition == state.currentMistake!.userBranch.position) {
} else if (state.currentPosition ==
state.currentMistake!.userBranch.position) {
Timer(const Duration(milliseconds: 500), () {
_onIncorrectMove();
});
} else {
this.state = AsyncValue.data(
state.copyWith(feedback: RetroFeedback.evalMove, evalRequestedAt: DateTime.now()),
state.copyWith(
feedback: RetroFeedback.evalMove,
evalRequestedAt: DateTime.now(),
),
);
// Be sure to get enough depth to evaluate the move properly
requestEval(goDeeper: true);
Expand All @@ -460,11 +544,17 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
Future<void> _listenToServerAnalysisEvents() async {
if (!state.hasValue) return;

final event = ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value;
final event = ref
.read(serverAnalysisServiceProvider)
.lastAnalysisEvent
.value;
if (event != null && event.$1 == options.id) {
ServerAnalysisService.mergeOngoingAnalysis(_root, event.$2.tree);
final progress = event.$2.evals.where((e) => e.hasEval).length / _root.mainline.length;
state = AsyncValue.data(state.requireValue.copyWith(serverAnalysisProgress: progress));
final progress =
event.$2.evals.where((e) => e.hasEval).length / _root.mainline.length;
state = AsyncValue.data(
state.requireValue.copyWith(serverAnalysisProgress: progress),
);

if (event.$2.isAnalysisComplete) {
if (_serverAnalysisCompleter.isCompleted == false) {
Expand All @@ -477,7 +567,14 @@ class RetroController extends AsyncNotifier<RetroState> with EngineEvaluationMix
}
}

enum RetroFeedback { findMove, evalMove, correct, incorrect, viewingSolution, done }
enum RetroFeedback {
findMove,
evalMove,
correct,
incorrect,
viewingSolution,
done,
}

@freezed
sealed class RetroState
Expand Down Expand Up @@ -518,10 +615,12 @@ sealed class RetroState
feedback == RetroFeedback.incorrect ||
feedback == RetroFeedback.evalMove;

Duration? get evalTime =>
evalRequestedAt != null ? DateTime.now().difference(evalRequestedAt!) : null;
Duration? get evalTime => evalRequestedAt != null
? DateTime.now().difference(evalRequestedAt!)
: null;

double get evalProgress => feedback == RetroFeedback.evalMove && currentNode.eval != null
double get evalProgress =>
feedback == RetroFeedback.evalMove && currentNode.eval != null
? min(1.0, currentNode.eval!.depth / _kEvalDepthThreshold)
: 0.0;

Expand Down Expand Up @@ -552,7 +651,9 @@ sealed class RetroState
}

@freezed
sealed class RetroCurrentNode with _$RetroCurrentNode implements AnalysisCurrentNodeInterface {
sealed class RetroCurrentNode
with _$RetroCurrentNode
implements AnalysisCurrentNodeInterface {
const RetroCurrentNode._();

const factory RetroCurrentNode({
Expand Down
Loading
Loading