@@ -524,6 +524,194 @@ export const useAnalysisController = (game: AnalyzedGame) => {
524
524
return data
525
525
} , [ moveEvaluation ] )
526
526
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
+
527
715
const move = useMemo ( ( ) => {
528
716
if ( ! currentMove ) return undefined
529
717
@@ -559,5 +747,6 @@ export const useAnalysisController = (game: AnalyzedGame) => {
559
747
moveRecommendations,
560
748
moveMap,
561
749
blunderMeter,
750
+ boardDescription,
562
751
}
563
752
}
0 commit comments