Files
NowChess-Frontend/src/app/services/analysis.service.ts
T
2026-06-17 08:17:55 +02:00

103 lines
3.1 KiB
TypeScript

import { Injectable, inject } from '@angular/core';
import { Observable, forkJoin, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { GameApiService } from './game-api.service';
import { AnnotatedMove, AnalysisResponse, MoveQuality } from '../models/analysis.models';
const DEFAULT_DEPTH = 14;
@Injectable({ providedIn: 'root' })
export class AnalysisService {
private readonly gameApi = inject(GameApiService);
analyzePosition(fen: string, depth = DEFAULT_DEPTH): Observable<AnalysisResponse> {
return this.gameApi.analyzePosition({ fen, depth });
}
/**
* Analyse a sequence of FEN positions (one per ply) and return annotated moves.
* fenHistory[0] is the starting position; fenHistory[n] is reached after move san[n-1].
*/
analyzeGame(
sans: string[],
fenHistory: string[],
depth = DEFAULT_DEPTH,
): Observable<AnnotatedMove[]> {
if (sans.length === 0 || fenHistory.length < 2) {
return of([]);
}
const requests = fenHistory.map((fen) => this.gameApi.analyzePosition({ fen, depth }));
return forkJoin(requests).pipe(
map((results) => this.buildAnnotations(sans, fenHistory, results)),
);
}
private buildAnnotations(
sans: string[],
fenHistory: string[],
results: AnalysisResponse[],
): AnnotatedMove[] {
return sans.map((san, i) => {
const evalBefore = results[i]?.eval ?? null;
const winChanceBefore = results[i]?.winChance ?? null;
const evalAfter = results[i + 1]?.eval ?? null;
const winChanceAfter = results[i + 1]?.winChance ?? null;
const bestMove = results[i]?.bestMove ?? null;
const quality = this.classifyQuality(
evalBefore,
evalAfter,
winChanceBefore,
winChanceAfter,
san,
bestMove,
);
return {
san,
fen: fenHistory[i + 1] ?? fenHistory[i],
evalBefore,
evalAfter,
quality,
bestMove,
winChanceBefore,
winChanceAfter,
};
});
}
/**
* Classify move quality based on win-chance delta.
* Win-chance is from the engine's perspective (side to move before the move).
* After the move the side has flipped, so we invert the after value.
*/
private classifyQuality(
_evalBefore: number | null,
_evalAfter: number | null,
winChanceBefore: number | null,
winChanceAfter: number | null,
san: string,
bestMove: string | null,
): MoveQuality | null {
if (winChanceBefore === null || winChanceAfter === null) return null;
// The engine expresses win chance for the mover. After our move the mover
// has switched, so the opponent's win chance = winChanceAfter. Our remaining
// winning chance from our own perspective is 1 - winChanceAfter.
const wcAfterOurPerspective = 1 - winChanceAfter;
const delta = wcAfterOurPerspective - winChanceBefore; // negative = we lost win chance
if (bestMove && san === bestMove) {
if (delta >= -0.01) return 'brilliant';
return 'best';
}
if (delta >= -0.02) return 'good';
if (delta >= -0.05) return 'inaccuracy';
if (delta >= -0.1) return 'mistake';
return 'blunder';
}
}