@@ -524,6 +524,194 @@ export const useAnalysisController = (game: AnalyzedGame) => {
524524 return data
525525 } , [ moveEvaluation ] )
526526
527+ const boardDescription = useMemo ( ( ) => {
528+ if (
529+ ! controller . currentNode ||
530+ ! moveEvaluation ?. stockfish ||
531+ ! moveEvaluation ?. maia ||
532+ moveEvaluation . stockfish . depth < 12
533+ ) {
534+ return ''
535+ }
536+
537+ const isBlackTurn = controller . currentNode . turn === 'b'
538+ const playerColor = isBlackTurn ? 'Black' : 'White'
539+ const opponent = isBlackTurn ? 'White' : 'Black'
540+ const stockfish = moveEvaluation . stockfish
541+ const maia = moveEvaluation . maia
542+ const topMaiaMove = Object . entries ( maia . policy ) . sort (
543+ ( a , b ) => b [ 1 ] - a [ 1 ] ,
544+ ) [ 0 ]
545+ const topStockfishMoves = Object . entries ( stockfish . cp_vec )
546+ . sort ( ( a , b ) => ( isBlackTurn ? a [ 1 ] - b [ 1 ] : b [ 1 ] - a [ 1 ] ) )
547+ . slice ( 0 , 3 )
548+
549+ const cp = stockfish . model_optimal_cp
550+ const absCP = Math . abs ( cp )
551+ const cpAdvantage = cp > 0 ? 'White' : cp < 0 ? 'Black' : 'Neither player'
552+ const topStockfishMove = topStockfishMoves [ 0 ]
553+
554+ // Check if top Maia move matches top Stockfish move
555+ const maiaMatchesStockfish = topMaiaMove [ 0 ] === topStockfishMove [ 0 ]
556+
557+ // Get top few Maia moves and their cumulative probability
558+ const top3MaiaMoves = Object . entries ( maia . policy )
559+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
560+ . slice ( 0 , 3 )
561+ const top3MaiaProbability =
562+ top3MaiaMoves . reduce ( ( sum , [ _ , prob ] ) => sum + prob , 0 ) * 100
563+
564+ // Get second best moves to analyze move clarity
565+ const secondBestMaiaMove = top3MaiaMoves [ 1 ]
566+ const secondBestMaiaProbability = secondBestMaiaMove
567+ ? secondBestMaiaMove [ 1 ] * 100
568+ : 0
569+
570+ // Calculate spread between first and second-best moves
571+ const probabilitySpread = topMaiaMove [ 1 ] * 100 - secondBestMaiaProbability
572+
573+ // Get move classifications
574+ const blunderProbability = blunderMeter . blunderMoves . probability
575+ const okProbability = blunderMeter . okMoves . probability
576+ const goodProbability = blunderMeter . goodMoves . probability
577+
578+ // Check for patterns in stockfish evaluation
579+ const stockfishTop3Spread =
580+ topStockfishMoves . length > 2
581+ ? Math . abs ( topStockfishMoves [ 0 ] [ 1 ] - topStockfishMoves [ 2 ] [ 1 ] )
582+ : 0
583+
584+ // Get move spreads to detect sharp positions
585+ const moveCpSpread = Object . values ( stockfish . cp_relative_vec ) . reduce (
586+ ( maxDiff , cp , _ , arr ) => {
587+ const min = Math . min ( ...arr )
588+ const max = Math . max ( ...arr )
589+ return Math . max ( maxDiff , max - min )
590+ } ,
591+ 0 ,
592+ )
593+
594+ // Calculate position complexity based on distribution of move quality
595+ const isPositionComplicated =
596+ ( blunderProbability > 30 && okProbability > 20 && goodProbability < 50 ) ||
597+ moveCpSpread > 300 ||
598+ stockfishTop3Spread > 100
599+
600+ // Check for tactical position
601+ const isTacticalPosition = moveCpSpread > 500 || stockfishTop3Spread > 150
602+
603+ // Check if there's a clear best move
604+ const topMaiaProbability = topMaiaMove [ 1 ] * 100
605+ const isClearBestMove = topMaiaProbability > 70 || probabilitySpread > 40
606+
607+ // Check if there are multiple equally good moves
608+ const hasMultipleGoodMoves =
609+ top3MaiaProbability > 75 && topMaiaProbability < 50
610+
611+ // Calculate agreement between Maia rating levels
612+ const maiaModelsAgree = Object . entries (
613+ controller . currentNode . analysis . maia || { } ,
614+ )
615+ . filter ( ( [ key ] ) => MAIA_MODELS . includes ( key ) )
616+ . every ( ( [ _ , evaluation ] ) => {
617+ const topMove = Object . entries ( evaluation . policy ) . sort (
618+ ( a , b ) => b [ 1 ] - a [ 1 ] ,
619+ ) [ 0 ]
620+ return topMove && topMove [ 0 ] === topMaiaMove [ 0 ]
621+ } )
622+
623+ // Check if evaluation is decisive
624+ const isDecisiveAdvantage = absCP > 300
625+ const isOverwhelming = absCP > 800
626+
627+ // Check for high blunder probability
628+ const isBlunderProne = blunderProbability > 50
629+ const isVeryBlunderProne = blunderProbability > 70
630+
631+ // Check if there's forced play
632+ const isForcedPlay = topMaiaProbability > 85 && maiaMatchesStockfish
633+
634+ // Check if position is balanced but with complexity
635+ const isBalancedButComplex = absCP < 50 && isPositionComplicated
636+
637+ // Generate descriptions
638+ let evaluation = ''
639+ let suggestion = ''
640+
641+ // Evaluation description
642+ if ( isOverwhelming ) {
643+ evaluation = `${ cpAdvantage } is completely winning and should convert without difficulty.`
644+ } else if ( cp === 0 ) {
645+ evaluation = isBalancedButComplex
646+ ? 'The position is balanced but filled with complications.'
647+ : 'The position is completely equal.'
648+ } else if ( absCP < 30 ) {
649+ evaluation = `The evaluation is almost perfectly balanced with only the slightest edge for ${ cpAdvantage } .`
650+ } else if ( absCP < 80 ) {
651+ evaluation = `${ cpAdvantage } has a slight but tangible advantage in this position.`
652+ } else if ( absCP < 150 ) {
653+ evaluation = `${ cpAdvantage } has a clear positional advantage that could be decisive with careful play.`
654+ } else if ( absCP < 300 ) {
655+ evaluation = `${ cpAdvantage } has a significant advantage that should be convertible with proper technique.`
656+ } else if ( absCP < 500 ) {
657+ evaluation = `${ cpAdvantage } has a winning position that only requires avoiding major blunders.`
658+ } else {
659+ evaluation = `${ cpAdvantage } has a completely winning position that should be straightforward to convert.`
660+ }
661+
662+ // Suggestion/description of move quality
663+ if ( isVeryBlunderProne ) {
664+ suggestion = `This critical position is extremely treacherous with a ${ blunderProbability . toFixed ( 0 ) } % chance of ${ playerColor } making a significant error.`
665+ } else if ( isBlunderProne && isTacticalPosition ) {
666+ suggestion = `The sharp tactical nature of this position creates many opportunities for mistakes (${ blunderProbability . toFixed ( 0 ) } % blunder chance).`
667+ } else if ( isBlunderProne ) {
668+ suggestion = `This position is quite treacherous with ${ blunderProbability . toFixed ( 0 ) } % chance of ${ playerColor } making a significant mistake.`
669+ } else if ( isForcedPlay ) {
670+ const moveSan = colorSanMapping [ topMaiaMove [ 0 ] ] ?. san || topMaiaMove [ 0 ]
671+ suggestion = `${ playerColor } must play ${ moveSan } , as all other moves lead to a significantly worse position.`
672+ } else if ( isTacticalPosition && maiaMatchesStockfish ) {
673+ const moveSan = colorSanMapping [ topMaiaMove [ 0 ] ] ?. san || topMaiaMove [ 0 ]
674+ suggestion = `The tactical complexity demands precision, with ${ moveSan } being the only move that maintains the balance.`
675+ } else if ( isPositionComplicated && hasMultipleGoodMoves ) {
676+ suggestion = `This complex position offers several equally promising continuations for ${ playerColor } .`
677+ } else if ( isPositionComplicated ) {
678+ suggestion = `This is a complex position requiring careful calculation of the many reasonable options.`
679+ } else if ( isClearBestMove && maiaMatchesStockfish && maiaModelsAgree ) {
680+ const moveSan = colorSanMapping [ topMaiaMove [ 0 ] ] ?. san || topMaiaMove [ 0 ]
681+ suggestion = `Players of all levels agree ${ moveSan } stands out as clearly best in this position.`
682+ } else if ( isClearBestMove && maiaMatchesStockfish ) {
683+ const moveSan = colorSanMapping [ topMaiaMove [ 0 ] ] ?. san || topMaiaMove [ 0 ]
684+ suggestion = `${ playerColor } should play ${ moveSan } , which both human intuition and concrete calculation confirm as best.`
685+ } else if ( isClearBestMove && maiaModelsAgree ) {
686+ const moveSan = colorSanMapping [ topMaiaMove [ 0 ] ] ?. san || topMaiaMove [ 0 ]
687+ suggestion = `Human players at all levels strongly prefer ${ moveSan } (${ topMaiaProbability . toFixed ( 0 ) } %), though the engine suggests otherwise.`
688+ } else if ( isClearBestMove ) {
689+ const moveSan = colorSanMapping [ topMaiaMove [ 0 ] ] ?. san || topMaiaMove [ 0 ]
690+ suggestion = `Maia strongly suggests ${ moveSan } (${ topMaiaProbability . toFixed ( 0 ) } % likely), though Stockfish calculates a different approach.`
691+ } else if ( goodProbability > 80 ) {
692+ suggestion = `This is a forgiving position where almost any reasonable move by ${ playerColor } maintains the evaluation.`
693+ } else if ( goodProbability > 60 ) {
694+ suggestion = `Most moves ${ playerColor } is likely to consider will maintain the current position assessment.`
695+ } else if ( maiaMatchesStockfish ) {
696+ const moveSan = colorSanMapping [ topMaiaMove [ 0 ] ] ?. san || topMaiaMove [ 0 ]
697+ suggestion = `Both human intuition and engine calculation agree that ${ moveSan } is the best continuation here.`
698+ } else if ( hasMultipleGoodMoves ) {
699+ suggestion = `${ playerColor } has several equally strong options, suggesting flexibility in planning.`
700+ } else if ( top3MaiaProbability < 50 ) {
701+ suggestion = `This unusual position creates difficulties for human calculation, with no clearly favored continuation.`
702+ } else {
703+ suggestion = `There are several reasonable options for ${ playerColor } to consider in this position.`
704+ }
705+
706+ return `${ evaluation } ${ suggestion } `
707+ } , [
708+ controller . currentNode ,
709+ moveEvaluation ,
710+ blunderMeter ,
711+ colorSanMapping ,
712+ MAIA_MODELS ,
713+ ] )
714+
527715 const move = useMemo ( ( ) => {
528716 if ( ! currentMove ) return undefined
529717
@@ -559,5 +747,6 @@ export const useAnalysisController = (game: AnalyzedGame) => {
559747 moveRecommendations,
560748 moveMap,
561749 blunderMeter,
750+ boardDescription,
562751 }
563752}
0 commit comments