Repository: GediminasMasaitis/CcrlExtensions
Pull request #11: Add multipv and fix mate score handling
Author: login: TheRealGioviok
Created: 2025-03-16T23:20:46Z | Merged: 2025-04-06T13:24:34Z

Code refactor, multipv support and algebraic notation in pv.

Known issues:
- [x] Analysis size got smaller (inspite not changing ratio constants) (https://github.com/TheRealGioviok/CcrlExtensions/commit/d9fc86cefe0af34b35ba80b53a2d433fa3a1f808#commitcomment-153807490)
- [x] Multipv visualization problem when the number of legal moves goes from >= multipv to < multipv (for example when in check)
- [x] Visualization problems when the window size becomes too small to accomodate all the webpage elements (board, chat, analysis) (_issue was already there?_) -> Created https://github.com/GediminasMasaitis/CcrlExtensions/issues/13 to tacke this in the future


Diff:
diff --git a/src/back/CcrlExtensionsHost/CcrlExtensionsHost.csproj b/src/back/CcrlExtensionsHost/CcrlExtensionsHost.csproj
index b165255..fa607aa 100644
--- a/src/back/CcrlExtensionsHost/CcrlExtensionsHost.csproj
+++ b/src/back/CcrlExtensionsHost/CcrlExtensionsHost.csproj
@@ -4,6 +4,7 @@
     <TargetFramework>net8.0</TargetFramework>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
+    <NoWarn>$(NoWarn);IDE0290</NoWarn>
   </PropertyGroup>
 
   <ItemGroup>
diff --git a/src/back/CcrlExtensionsHost/Models/EngineInfo.cs b/src/back/CcrlExtensionsHost/Models/EngineInfo.cs
index 468fc88..c277702 100644
--- a/src/back/CcrlExtensionsHost/Models/EngineInfo.cs
+++ b/src/back/CcrlExtensionsHost/Models/EngineInfo.cs
@@ -8,4 +8,7 @@ public class EngineInfo
     public long Nodes { get; set; }
     public long Nps { get; set; }
     public string? Pv { get; set; }
+
+    // Multipv
+    public List<MultipvInfo> Multipv { get; set; } = [];
 }
diff --git a/src/back/CcrlExtensionsHost/Models/MultipvInfo.cs b/src/back/CcrlExtensionsHost/Models/MultipvInfo.cs
new file mode 100644
index 0000000..06b5d89
--- /dev/null
+++ b/src/back/CcrlExtensionsHost/Models/MultipvInfo.cs
@@ -0,0 +1,11 @@
+namespace CcrlExtensionsHost.Models;
+
+public class MultipvInfo
+{
+    public int Multipv { get; set; }
+    public string? Score { get; set; }
+    public string? Mate { get; set; }
+    public int Depth { get; set; }
+    public string? Pv { get; set; }
+    public int? OrderKey { get; set; }
+}
diff --git a/src/back/CcrlExtensionsHost/Services/Engine.cs b/src/back/CcrlExtensionsHost/Services/Engine.cs
index d1a8b1e..1cfe88a 100644
--- a/src/back/CcrlExtensionsHost/Services/Engine.cs
+++ b/src/back/CcrlExtensionsHost/Services/Engine.cs
@@ -1,4 +1,5 @@
 ﻿using System.Diagnostics;
+using System.Globalization;
 using System.Text.RegularExpressions;
 using CcrlExtensionsHost.Configs;
 using CcrlExtensionsHost.Models;
@@ -10,6 +11,13 @@ public partial class Engine
     private const int GracefulShutdownTime = 3_000;
 
     private readonly ILogger<Engine> _logger;
+
+    private static readonly string[] _stringsToIgnore =
+    [
+        "lowerbound",
+        "upperbound",
+        "currmove"
+    ];
     private Process? _process;
     private string? _currentFen;
 
@@ -77,17 +85,21 @@ private void HandleOutput(string? line)
     {
         _logger.LogDebug("{EngineName} >> {EngineOutput}", Config?.Name, line);
 
-        var success = TryGetEngineInfo(line, out var info);
-        if (success)
-        {
-            CurrentEngineInfo = info;
-        }
+        _ = TryUpdateEngineInfo(line);
     }
 
+    [GeneratedRegex(" multipv (\\d+)", RegexOptions.Compiled)]
+    private static partial Regex MultipvRegex();
+    private readonly Regex _multipvRegex = MultipvRegex();
+
     [GeneratedRegex(" score cp (-?\\d+)", RegexOptions.Compiled)]
     private static partial Regex CpRegex();
     private readonly Regex _cpRegex = CpRegex();
 
+    [GeneratedRegex(" score mate (-?\\d+)", RegexOptions.Compiled)]
+    private static partial Regex MateRegex();
+    private readonly Regex _mateRegex = MateRegex();
+
     [GeneratedRegex(" depth (\\d+)", RegexOptions.Compiled)]
     private static partial Regex DepthRegex();
     private readonly Regex _depthRegex = DepthRegex();
@@ -104,89 +116,110 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
-        if (string.IsNullOrEmpty(line))
-        {
+        if (string.IsNullOrEmpty(line) || _stringsToIgnore.Any(line.Contains))
             return false;
-        }
+
+        CurrentEngineInfo ??= new EngineInfo();
 
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                CurrentEngineInfo.Name = Config?.Name;
                 return true;
             }
-
             return false;
         }
 
-        if (line.Contains("lowerbound"))
-        {
-            return false;
-        }
+        int multipvNumber = 1;
+        int depth = 0, cp = 0;
+        long nodes = 0, nps = 0;
+        string score = "0", pv = "";
+        int orderKey = 0;
 
-        if (line.Contains("upperbound"))
+        // Default multipv number is 1
+        var multipvMatch = _multipvRegex.Match(line);
+        if (multipvMatch.Success && int.TryParse(multipvMatch.Groups[1].Value, out var multiPvNumberParsed))
         {
-            return false;
+            multipvNumber = multiPvNumberParsed;
         }
 
-        if (line.Contains("currmove"))
+        var mateMatch = _mateRegex.Match(line);
+        if (mateMatch.Success && int.TryParse(mateMatch.Groups[1].Value, out var mateMoves))
         {
-            return false;
+            orderKey = -(10000000 - mateMoves) * Math.Sign(mateMoves);
+            if (_currentFen?.Contains(" b ") == true) mateMoves = -mateMoves;
+            if (mateMoves < 0)
+            {
+                score = $"-M{-mateMoves}";
+            }
+            else
+            {
+                score = $"+M{mateMoves}";
+            }
         }
-
-        info.Name = Config?.Name;
-
-        var cpMatch = _cpRegex.Match(line);
-        if (cpMatch.Success)
+        else
         {
-            if (int.TryParse(cpMatch.Groups[1].Value, out var cp))
+            var cpMatch = _cpRegex.Match(line);
+            if (cpMatch.Success && int.TryParse(cpMatch.Groups[1].Value, out var cpParsed))
             {
-                if (_currentFen?.Contains(" b ") == true)
-                {
-                    cp = -cp;
-                }
-                info.Score = cp.ToString();
+                cp = cpParsed;
+                orderKey = -cp;
+                if (_currentFen?.Contains(" b ") == true) cp = -cp;
+                score = $"{((float)cp / 100).ToString(CultureInfo.InvariantCulture)}";
             }
         }
 
         var depthMatch = _depthRegex.Match(line);
-        if (depthMatch.Success)
+        if (depthMatch.Success && int.TryParse(depthMatch.Groups[1].Value, out var depthParsed))
         {
-            if (int.TryParse(depthMatch.Groups[1].Value, out var depth))
-            {
-                info.Depth = depth;
-            }
+            depth = depthParsed;
         }
 
         var nodesMatch = _nodesRegex.Match(line);
-        if (nodesMatch.Success)
+        if (nodesMatch.Success && long.TryParse(nodesMatch.Groups[1].Value, out var nodesParsed))
         {
-            if (long.TryParse(nodesMatch.Groups[1].Value, out var nodes))
-            {
-                info.Nodes = nodes;
-            }
+            nodes = nodesParsed;
         }
 
         var npsMatch = _npsRegex.Match(line);
-        if (npsMatch.Success)
+        if (npsMatch.Success && long.TryParse(npsMatch.Groups[1].Value, out var npsParsed))
         {
-            if (long.TryParse(npsMatch.Groups[1].Value, out var nps))
-            {
-                info.Nps = nps;
-            }
+            nps = npsParsed;
         }
 
         var pvMatch = _pvRegex.Match(line);
         if (pvMatch.Success)
         {
-            info.Pv = pvMatch.Groups[1].Value;
+            pv = pvMatch.Groups[1].Value;
         }
 
+        var existing = CurrentEngineInfo.Multipv.FirstOrDefault(m => m.Multipv == multipvNumber);
+        if (existing != null)
+        {
+            existing.Depth = depth;
+            existing.Score = score;
+            existing.Pv = pv;
+            existing.OrderKey = orderKey;
+        }
+        else
+        {
+            CurrentEngineInfo.Multipv.Add(new MultipvInfo { Multipv = multipvNumber, Depth = depth, Score = score, Pv = pv, OrderKey = orderKey });
+        }
+
+        if (multipvNumber == 1)
+        {
+            CurrentEngineInfo.Score = score;
+            CurrentEngineInfo.Depth = depth;
+            CurrentEngineInfo.Pv = pv;
+        }
+
+        CurrentEngineInfo.Nps = nps;
+        CurrentEngineInfo.Nodes = nodes;
+        CurrentEngineInfo.Name = Config?.Name;
+
         return true;
     }
 
@@ -213,6 +246,8 @@ public async Task SetFenAsync(string fen)
                 await SendAsync("ucinewgame");
             }
             await SendAsync($"position fen {fen}");
+            // Reset the EngineInfo struct
+            CurrentEngineInfo?.Multipv.Clear();
             await SendAsync("go infinite");
         }
     }
diff --git a/src/back/CcrlExtensionsHost/appsettings.json b/src/back/CcrlExtensionsHost/appsettings.json
index 9454b6c..0fa303e 100644
--- a/src/back/CcrlExtensionsHost/appsettings.json
+++ b/src/back/CcrlExtensionsHost/appsettings.json
@@ -8,6 +8,10 @@
         {
           "Name": "Threads",
           "Value": "1"
+        },
+        {
+          "Name": "Multipv",
+          "Value": "3"
         }
       ]
     },
@@ -37,4 +41,4 @@
       "Default": "Debug"
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/front/ccrlExtensions.js b/src/front/ccrlExtensions.js
index 8d856de..df9ad8d 100644
--- a/src/front/ccrlExtensions.js
+++ b/src/front/ccrlExtensions.js
@@ -8,194 +8,335 @@
 // @description Kibitzer and eval graph support for CCRL
 // @require     https://code.jquery.com/jquery-3.7.0.min.js
 // @require     https://cdn.jsdelivr.net/npm/chart.js
+// @require     https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
 // ==/UserScript==
 
-(function() {
+(function () {
   'use strict';
 
-    var container = $('.container');
-    container.css('max-width', '180vh');
-
-    var layout = $('.main-layout');
-    layout.css('grid-template-columns', '40% 30% 30%');
-
-    for(var kibitzerIndex = 0; kibitzerIndex < 2; kibitzerIndex++)
-    {
-      var kibitzerNum = kibitzerIndex + 1;
-      layout.append(`<div id="kibitzer${kibitzerNum}-info"></div>`);
-      var kibitzerInfo = $(`#kibitzer${kibitzerNum}-info`);
-      var row = kibitzerIndex % 2 == 0 ? '1' : '4';
-      kibitzerInfo.css('grid-row-start', row);
-      kibitzerInfo.css('grid-column-start', '3');
-
-      kibitzerInfo.append(`<h3 id="kibitzer${kibitzerNum}-name">Kibitzer ${kibitzerNum} inactive</h3>`);
-      kibitzerInfo.append(`
-    <div class="card fluid">
-      <div class="row">
-        <div class="col-sm">
-          <div class="row">
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Score</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-score">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Depth</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-depth">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nodes</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nodes">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nps</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nps">0</p>
+  // Utility functions
+  const formatCompactNumber = (number) => {
+    if (number < 1000) return number;
+    const units = ['k', 'M', 'B', 'T'];
+    let unitIndex = -1;
+    while (number >= 1000 && unitIndex < units.length - 1) {
+      number /= 1000;
+      unitIndex++;
+    }
+    return `${number.toFixed(2)}${units[unitIndex]}`;
+  };
+
+  const getFullmoveNumberFromFen = (fen) => {
+    const parts = fen.split(" ");
+    return parts.length === 6 ? parseInt(parts[5]) : 1;
+  };
+
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
+
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
+        groups.push({
+          moveNumber: currentMoveNumber,
+          white: moveObj.san,
+          black: null
+        });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
+      }
+
+      isWhiteToMove = chess.turn() === 'w';
+    });
+
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    // Format: "+MX" where X is a positive integer (mate for white)
+    if (scoreText.startsWith("+M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+
+      // Use a high constant (e.g. 1000) so that mate scores are always above normal evals.
+      const value = 30 - (isNaN(mateMoves) ? 0 : mateMoves * 0.01);
+      return { value: value, tooltip: scoreText };
+    }
+    // Format: "-MX" where X is a positive integer (mate for black)
+    else if (scoreText.startsWith("-M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+      // Use a low constant so that mate scores are always below normal evals.
+      const value = -30 + (isNaN(mateMoves) ? 0 : mateMoves * 0.01);
+      return { value: value, tooltip: scoreText };
+    }
+    else {
+      const numStr = scoreText;
+      const value = Math.min(Math.max(parseFloat(numStr), -20), 20);
+      if (!isNaN(value)) {
+        return { value: value, tooltip: scoreText };
+      }
+    }
+    // If none of the formats match, return null.
+    return null;
+  }
+
+  // Create page layout
+  const container = $('.container').css('max-width', '180vh');
+
+  // Create a wrapper for the whole page content
+  const pageWrapper = $('<div id="page-wrapper"></div>').css({
+    'display': 'flex',
+    'flex-direction': 'row',
+    'width': '100%',
+    'gap': '15px'
+  });
+
+  // Move all existing content from the container to a main content div
+  const mainContent = $('<div id="main-content"></div>').css({
+    'flex': '7',
+    'min-width': '0'
+  });
+
+  // Create the sidebar for analysis
+  const sidebarAnalysis = $('<div id="sidebar-analysis"></div>').css({
+    'flex': '3',
+    'display': 'flex',
+    'flex-direction': 'column',
+    'gap': '15px', 
+    'min-width': '0',
+    'max-width': 'none'
+  });
+
+  // Move existing container children to mainContent
+  container.children().appendTo(mainContent);
+
+  // Clear container and add our new structure
+  container.empty().append(pageWrapper);
+  pageWrapper.append(mainContent).append(sidebarAnalysis);
+
+  // Create the first kibitzer
+  const kibitzerInfo1 = $(`<div id="kibitzer1-info"></div>`).css({
+    'margin-bottom': '15px',
+    'overflow': 'auto',
+    'max-height': '30vh'
+  });
+
+  sidebarAnalysis.append(kibitzerInfo1);
+
+  kibitzerInfo1
+    .append(`<h3 id="kibitzer1-name">Kibitzer 1 inactive</h3>`)
+    .append(`
+      <div class="card fluid">
+        <div class="row">
+          <div class="col-sm">
+            <div class="row">
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Score</small></p>
+                <p class="small-margin info-value" id="kibitzer1-score">0</p>
+              </div>
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Depth</small></p>
+                <p class="small-margin info-value" id="kibitzer1-depth">0</p>
+              </div>
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Nodes</small></p>
+                <p class="small-margin info-value" id="kibitzer1-nodes">0</p>
+              </div>
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Nps</small></p>
+                <p class="small-margin info-value" id="kibitzer1-nps">0</p>
+              </div>
             </div>
           </div>
-        </div>
-        <div class="col-sm-3" style="text-align: right">
-          <h3><small id="kibitzer${kibitzerNum}-time"><mark>	&#8734; </mark></small></h3>
-        </div>
-        <div class="col-sm-12">
-          <p class="pv"><small id="kibitzer${kibitzerNum}-pv"></small></p>
+          <div class="col-sm-3" style="text-align: right">
+            <h3><small id="kibitzer1-time"><mark>&#8734;</mark></small></h3>
+          </div>
+          <div class="col-sm-12">
+            <p class="pv"><small id="kibitzer1-pv"></small></p>
+          </div>
         </div>
       </div>
-    </div>`);
-    }
+    `)
+    .append(`<p class="mainline" id="kibitzer1-mainline" style="margin-top: 5px; font-style: italic;"></p>`);
 
+  // Setup chart container
+  const evalChartContainer = $('<div id="eval-chart-container"><canvas id="eval-chart"></canvas></div>').css({
+    'width': '100%',
+    'height': '300px',
+    'margin-bottom': '15px'
+  });
+
+  // Append the chart container to the sidebar
+  sidebarAnalysis.append(evalChartContainer);
 
-    layout.append('<div id="eval-chart-container"><canvas id="eval-chart"></canvas></div>')
-    var evalChartContainer = $("#eval-chart-container");
-    evalChartContainer.css('grid-row-start', '2');
-    evalChartContainer.css('grid-row-end', '4');
-    evalChartContainer.css('grid-column-start', '3');
+  // Create the second kibitzer (placed after the chart)
+  const kibitzerInfo2 = $(`<div id="kibitzer2-info"></div>`).css({
+    'margin-bottom': '15px',
+    'overflow': 'auto',
+    'max-height': '30vh'
+  });
 
-    layout.append('<div id="kibitzer2-info"></div>');
-    var kibitzerInfo = $('#kibitzer2-info');
-    kibitzerInfo.css('grid-row-start', '4');
-    kibitzerInfo.css('grid-column-start', '3');
+  sidebarAnalysis.append(kibitzerInfo2);
 
-  const ctx = $("#eval-chart").get(0);
-  var chart = new Chart(ctx, {
+  kibitzerInfo2
+    .append(`<h3 id="kibitzer2-name">Kibitzer 2 inactive</h3>`)
+    .append(`
+      <div class="card fluid">
+        <div class="row">
+          <div class="col-sm">
+            <div class="row">
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Score</small></p>
+                <p class="small-margin info-value" id="kibitzer2-score">0</p>
+              </div>
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Depth</small></p>
+                <p class="small-margin info-value" id="kibitzer2-depth">0</p>
+              </div>
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Nodes</small></p>
+                <p class="small-margin info-value" id="kibitzer2-nodes">0</p>
+              </div>
+              <div class="col-sm info">
+                <p class="small-margin"><small class="info-header">Nps</small></p>
+                <p class="small-margin info-value" id="kibitzer2-nps">0</p>
+              </div>
+            </div>
+          </div>
+          <div class="col-sm-3" style="text-align: right">
+            <h3><small id="kibitzer2-time"><mark>&#8734;</mark></small></h3>
+          </div>
+          <div class="col-sm-12">
+            <p class="pv"><small id="kibitzer2-pv"></small></p>
+          </div>
+        </div>
+      </div>
+    `)
+    .append(`<p class="mainline" id="kibitzer2-mainline" style="margin-top: 5px; font-style: italic;"></p>`);
+
+  const ctx = $("#eval-chart")[0];
+  const chart = new Chart(ctx, {
     type: 'line',
     data: {
       labels: [],
-      datasets: [{
-        label: 'White',
-        data: [],
-        borderColor: 'rgb(220, 220, 220)'
-      },
-      {
-        label: 'Black',
-        data: [],
-        borderColor: 'rgb(100, 100, 100)'
-      },
-      {
-        label: 'Kibitzer 1',
-        data: [],
-        borderColor: 'rgb(220, 100, 100)'
-      },
-      {
-        label: 'Kibitzer 2',
-        data: [],
-        borderColor: 'rgb(100, 100, 220)'
-      },
+      datasets: [
+        {
+          label: 'White',
+          data: [],
+          borderColor: 'rgb(220, 220, 220)'
+        },
+        {
+          label: 'Black',
+          data: [],
+          borderColor: 'rgb(100, 100, 100)'
+        },
+        {
+          label: 'Kibitzer 1',
+          data: [],
+          borderColor: 'rgb(220, 100, 100)'
+        },
+        {
+          label: 'Kibitzer 2',
+          data: [],
+          borderColor: 'rgb(100, 100, 220)'
+        }
       ]
     },
     options: {
+      responsive: true,
+      maintainAspectRatio: false,
       scales: {
         y: {
           beginAtZero: true
         }
+      },
+      plugins: {
+        tooltip: {
+          callbacks: {
+            label: (context) => {
+              const pt = context.raw;
+              // If pt is an object and has tooltip, return that. Otherwise, use the y value.
+              return pt.tooltip || pt.y;
+            }
+          }
+        }
       }
     }
   });
 
-  var currentFen = "";
-  var labels = [];
-  var scores = [{},{},{},{}];
-
-  function getFen(){
-    return $("#fen").text();
-  }
-
-  function sendFen() {
-    var fen = getFen();
 
-    if(fen == "") {
-      return;
-    }
+  // State
+  let currentFen = "";
+  let labels = [];
+  let scores = [{}, {}, {}, {}];
 
-    if(fen == "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") {
+  // Update FEN and send data
+  const getFen = () => $("#fen").text();
+  const sendFen = () => {
+    const fen = getFen();
+    if (!fen || fen === currentFen) return;
+    currentFen = fen;
+    if (fen === "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") {
       labels = [];
-      scores = [{},{},{},{}];
+      scores = [{}, {}, {}, {}];
     }
-
-    if(fen == currentFen){
-      return;
-    }
-
-    currentFen = fen;
-
-    var request = {
-      fen: fen
-    };
-    var requestJson = JSON.stringify(request);
-
     GM.xmlHttpRequest({
       method: "POST",
       url: "http://127.0.0.1:5210/fen",
       headers: {
         "Content-Type": "application/json"
       },
-      data: requestJson,
-      onload: function(response){
-        var responseObj = JSON.parse(response.responseText);
-        if(responseObj.engines.length > 0){
-          $("#kibitzer1-name").text(responseObj.engines[0].name)
-        }
-        if(responseObj.engines.length > 1){
-          $("#kibitzer2-name").text(responseObj.engines[1].name)
-        }
+      data: JSON.stringify({
+        fen
+      }),
+      onload: (response) => {
+        const engines = JSON.parse(response.responseText).engines;
+        if (engines.length > 0) $("#kibitzer1-name").text(engines[0].name);
+        if (engines.length > 1) $("#kibitzer2-name").text(engines[1].name);
       }
     });
-  }
-
-  $('#fen').on('DOMSubtreeModified', function(){
-    sendFen();
-  });
+  };
+  $('#fen').on('DOMSubtreeModified', sendFen);
 
-  function formatCompactNumber(number) {
-    if (number < 1000) {
-      return number;
-    } else if (number >= 1000 && number < 1_000_000) {
-      return (number / 1000).toFixed(2) + "k";
-    } else if (number >= 1_000_000 && number < 1_000_000_000) {
-      return (number / 1_000_000).toFixed(2) + "M";
-    } else if (number >= 1_000_000_000 && number < 1_000_000_000_000) {
-      return (number / 1_000_000_000).toFixed(2) + "B";
-    } else if (number >= 1_000_000_000_000 && number < 1_000_000_000_000_000) {
-      return (number / 1_000_000_000_000).toFixed(2) + "T";
-    }
-  }
-
-  function updateChart(){
-
-  }
+  // Update the graph and kibitzers every second
+  setInterval(() => {
+    const fen = getFen();
+    if (!fen) return;
 
-  setInterval(function() {
-    var fen = getFen();
-    if(fen == ""){
-      return;
-    }
+    // Correct ply calculation
+    const fenParts = fen.split(" ");
+    const fullmoveNumber = parseInt(fenParts[5]) || 1;
+    const activeColor = fenParts[1];
+    const ply = (fullmoveNumber - 1) * 2 + (activeColor === 'b' ? 1 : 0);
+    const plyStr = ply.toString();
 
-    var fenWords = fen.split(" ");
-    var lastFenWord = fenWords[fenWords.length - 1];
-    var ply = parseInt(lastFenWord) * 2;
-    if(fen.includes(" b ")){
-      ply += 1;
-    }
-    var plyStr = ply.toString();
-    if(!labels.includes(plyStr)){
+    if (!labels.includes(plyStr)) {
       labels.push(plyStr);
       chart.data.labels = labels;
     }
@@ -203,45 +344,60 @@
     GM.xmlHttpRequest({
       method: "GET",
       url: "http://127.0.0.1:5210/query",
-      onload: function(response){
-        var responseObj = JSON.parse(response.responseText);
-        for(var engineIndex = 0; engineIndex < responseObj.engineInfos.length; engineIndex++) {
-          var kibitzerNum = engineIndex+1;
-          var info = responseObj.engineInfos[engineIndex];
-          var score = info.score / 100;
-          $("#kibitzer" + kibitzerNum + "-score").text(score);
-          $("#kibitzer" + kibitzerNum + "-depth").text(info.depth);
-          $("#kibitzer" + kibitzerNum + "-nodes").text(formatCompactNumber(info.nodes));
-          $("#kibitzer" + kibitzerNum + "-nps").text(formatCompactNumber(info.nps));
-          $("#kibitzer" + kibitzerNum + "-pv").text(info.pv);
-
-          var scoreIndex = engineIndex + 2;
-          scores[scoreIndex][plyStr] = score;
-          chart.data.datasets[scoreIndex].label = info.name;
-        }
+      onload: (response) => {
+        const engineInfos = JSON.parse(response.responseText).engineInfos;
+        engineInfos.forEach((info, index) => {
+          const kibitzerNum = index + 1;
+          $(`#kibitzer${kibitzerNum}-score`).text(parseScore(info.score).tooltip);
+          $(`#kibitzer${kibitzerNum}-depth`).text(info.depth);
+          $(`#kibitzer${kibitzerNum}-nodes`).text(formatCompactNumber(info.nodes));
+          $(`#kibitzer${kibitzerNum}-nps`).text(formatCompactNumber(info.nps));
+
+          // Update kibitzer scores for the chart (positions 2 and 3 in the scores array)
+          const datasetIndex = 2 + index;
+          scores[datasetIndex][plyStr] = { x: plyStr, y: parseScore(info.score).value, tooltip: parseScore(info.score).tooltip };
+
+
+          let multipvHtml = "";
+          if (info.multipv) {
+            info.multipv.sort((a, b) => a.orderKey - b.orderKey).forEach((variation) => {
+              const algebraicPV = convertPVToAlgebraic(variation.pv, currentFen);
+              multipvHtml += `
+                <div class="multipv-box" style="border: 1px solid #ccc; margin-bottom: 5px; padding: 5px;">
+                  <div class="multipv-header" style="font-weight: bold;color: #aaa;">
+                    Depth: ${variation.depth} | Eval: ${variation.score}
+                  </div>
+                  <div class="multipv-pv" style="margin-top: 3px;">${algebraicPV}</div>
+                </div>`;
+            });
+          }
+          $(`#kibitzer${kibitzerNum}-pv`).html(multipvHtml);
+        });
       },
-      onerror: function(){
-        console.log("Failed querying backend");
-      }
+      onerror: () => console.log("Failed querying backend")
     });
 
-    var whiteScore = parseFloat($("#white-score").text());
-    scores[0][plyStr] = whiteScore
-    var blackScore = parseFloat($("#black-score").text());
-    scores[1][plyStr] = blackScore
-
-    var chartData = [];
-    for (var i = 0; i < scores.length; i++){
-       chartData.push([]);
-       // iterate over the labels array to ensure alignment with the x-axis
-       for (var j = 0; j < labels.length; j++){
-          var plyKey = labels[j];
-          // If the score exists for that ply, use it; otherwise, use null (or 0)
-          chartData[i].push(scores[i][plyKey] !== undefined ? scores[i][plyKey] : null);
-       }
-       chart.data.datasets[i].data = chartData[i];
+    // Handle White and Black scores with validation
+    const whiteScoreText = $("#white-score").text();
+    const blackScoreText = $("#black-score").text();
+    const whiteScore = parseFloat(whiteScoreText);
+    const blackScore = parseFloat(blackScoreText);
+
+    if (!isNaN(whiteScore)) {
+      scores[0][plyStr] = { x: plyStr, y: whiteScore, tooltip: whiteScore.toString() };
+    }
+    if (!isNaN(blackScore)) {
+      scores[1][plyStr] = { x: plyStr, y: blackScore, tooltip: blackScore.toString() };
     }
+
+    // Update chart datasets
+    chart.data.datasets.forEach((dataset, i) => {
+      dataset.data = labels.map(plyStr => {
+        const scoreObj = scores[i][plyStr];
+        return scoreObj ? scoreObj : { x: plyStr, y: null };
+      });
+    });
     chart.update();
   }, 1000);
 
-})();
+})();
\ No newline at end of file


Reviews:
user: login: eduherminio
body: Backend review: great job, comments and mostly nits 😅
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T00:20:26Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:21:09Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:21:18Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T02:21:29Z
user: login: eduherminio
body: Haven't tested the binaries yet, but I highlighted a few things that caught my attention.

Mate score handling was an issue already present in the codebase, so I'd be happy to merge the PR without these last changes, and move them to a separated pull request to streamline this one.

The only thing that was stopping us from merging the PR before this last commit was the unchecked item in the description (`number of legal moves goes from >= multipv to < multipv`).
How does this last commit address that?
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-30T21:29:56Z
user: login: eduherminio
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-30T23:30:24Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T04:46:30Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T04:47:14Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T04:47:34Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T04:47:47Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T04:48:07Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T04:48:20Z
user: login: eduherminio
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-31T15:52:10Z
user: login: eduherminio
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-31T15:52:27Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T16:37:56Z
user: login: TheRealGioviok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T16:38:09Z
user: login: eduherminio
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-06T13:23:29Z

Review comments:
diff_hunk: @@ -1,11 +1,21 @@
-﻿namespace CcrlExtensionsHost.Models;
+﻿public class MultipvInfo
+{
+    public int Multipv { get; set; }
+    public string? Score { get; set; }
+    public int Depth { get; set; }
+    public string? Pv { get; set; }
+}
 
 public class EngineInfo
 {
+    // It is a bit redundant yes, but i dont want to change too much stuff.
path: src/back/CcrlExtensionsHost/Models/EngineInfo.cs
user: login: eduherminio
body: Understood, please remove the comment before merging
created_at: 2025-03-16T23:27:14Z
author_association: COLLABORATOR
diff_hunk: @@ -104,88 +104,107 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
         if (string.IsNullOrEmpty(line))
-        {
             return false;
-        }
 
+        // Only process lines containing "info" and "uciok"
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                if (CurrentEngineInfo == null)
+                    CurrentEngineInfo = new EngineInfo();
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: ```suggestion
                CurrentEngineInfo ??= new EngineInfo();
```
created_at: 2025-03-16T23:29:24Z
author_association: COLLABORATOR
diff_hunk: @@ -104,88 +104,107 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
         if (string.IsNullOrEmpty(line))
-        {
             return false;
-        }
 
+        // Only process lines containing "info" and "uciok"
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                if (CurrentEngineInfo == null)
+                    CurrentEngineInfo = new EngineInfo();
+                CurrentEngineInfo.Name = Config?.Name;
                 return true;
             }
-
             return false;
         }
 
-        if (line.Contains("lowerbound"))
-        {
+        if (line.Contains("lowerbound") || line.Contains("upperbound") || line.Contains("currmove"))
             return false;
-        }
 
-        if (line.Contains("upperbound"))
-        {
-            return false;
-        }
+        if (CurrentEngineInfo == null)
+            CurrentEngineInfo = new EngineInfo();
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: ```suggestion
        CurrentEngineInfo ??= new EngineInfo();
```
created_at: 2025-03-16T23:32:34Z
author_association: COLLABORATOR
diff_hunk: @@ -104,88 +104,107 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
         if (string.IsNullOrEmpty(line))
-        {
             return false;
-        }
 
+        // Only process lines containing "info" and "uciok"
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                if (CurrentEngineInfo == null)
+                    CurrentEngineInfo = new EngineInfo();
+                CurrentEngineInfo.Name = Config?.Name;
                 return true;
             }
-
             return false;
         }
 
-        if (line.Contains("lowerbound"))
-        {
+        if (line.Contains("lowerbound") || line.Contains("upperbound") || line.Contains("currmove"))
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: Maybe it's a good moment to refactor this into an even better:
```csharp
    private static readonly string[] _stringsToIgnore =
    [
        "lowerbound",
        "upperbound",
        "currmove"
    ];
```

and here:

```csharp
        if (_stringsToIgnore.Any(line.Contains))
        {
            return false;
        }
```
created_at: 2025-03-16T23:35:42Z
author_association: COLLABORATOR
diff_hunk: @@ -104,88 +104,107 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
         if (string.IsNullOrEmpty(line))
-        {
             return false;
-        }
 
+        // Only process lines containing "info" and "uciok"
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                if (CurrentEngineInfo == null)
+                    CurrentEngineInfo = new EngineInfo();
+                CurrentEngineInfo.Name = Config?.Name;
                 return true;
             }
-
             return false;
         }
 
-        if (line.Contains("lowerbound"))
-        {
+        if (line.Contains("lowerbound") || line.Contains("upperbound") || line.Contains("currmove"))
             return false;
-        }
 
-        if (line.Contains("upperbound"))
-        {
-            return false;
-        }
+        if (CurrentEngineInfo == null)
+            CurrentEngineInfo = new EngineInfo();
 
-        if (line.Contains("currmove"))
+        // Default multipv number is 1
+        int multipvNumber = 1;
+        var multipvMatch = _multipvRegex.Match(line);
+        if (multipvMatch.Success)
         {
-            return false;
+            int.TryParse(multipvMatch.Groups[1].Value, out multipvNumber);
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: This emits a warning for not checking the result of the `int.TryParse()`.
It can be avoided by doing something like:
```csharp
        if (!(multipvMatch.Success && int.TryParse(multipvMatch.Groups[1].Value, out var multipvNumber)))
        {
            // Default multipv number is 1
            multipvNumber = 1;
        }
```

But better to follow the approach you used later to be consistent, I don't really have a strong preference between both approaches anyway
```csharp
        if (multipvMatch.Success && int.TryParse(multipvMatch.Groups[1].Value, out var multiPvNumberParsed))
        {
            multipvNumber = multiPvNumberParsed;
        }
```
created_at: 2025-03-16T23:41:11Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,21 @@
-﻿namespace CcrlExtensionsHost.Models;
+﻿public class MultipvInfo
+{
+    public int Multipv { get; set; }
+    public string? Score { get; set; }
+    public int Depth { get; set; }
+    public string? Pv { get; set; }
+}
 
 public class EngineInfo
 {
+    // It is a bit redundant yes, but i dont want to change too much stuff.
     public string? Name { get; set; }
     public string? Score { get; set; }
     public int Depth { get; set; }
     public long Nodes { get; set; }
     public long Nps { get; set; }
     public string? Pv { get; set; }
+
+    // Multipv
+    public List<MultipvInfo> Multipv { get; set; } = new List<MultipvInfo>();
path: src/back/CcrlExtensionsHost/Models/EngineInfo.cs
user: login: eduherminio
body: ```suggestion
    public List<MultipvInfo> Multipv { get; set; } = [];
```
created_at: 2025-03-16T23:49:45Z
author_association: COLLABORATOR
diff_hunk: @@ -104,88 +104,107 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
         if (string.IsNullOrEmpty(line))
-        {
             return false;
-        }
 
+        // Only process lines containing "info" and "uciok"
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                if (CurrentEngineInfo == null)
+                    CurrentEngineInfo = new EngineInfo();
+                CurrentEngineInfo.Name = Config?.Name;
                 return true;
             }
-
             return false;
         }
 
-        if (line.Contains("lowerbound"))
-        {
+        if (line.Contains("lowerbound") || line.Contains("upperbound") || line.Contains("currmove"))
             return false;
-        }
 
-        if (line.Contains("upperbound"))
-        {
-            return false;
-        }
+        if (CurrentEngineInfo == null)
+            CurrentEngineInfo = new EngineInfo();
 
-        if (line.Contains("currmove"))
+        // Default multipv number is 1
+        int multipvNumber = 1;
+        var multipvMatch = _multipvRegex.Match(line);
+        if (multipvMatch.Success)
         {
-            return false;
+            int.TryParse(multipvMatch.Groups[1].Value, out multipvNumber);
         }
 
-        info.Name = Config?.Name;
-
+        // Parse the score (cp), depth, nodes, nps, and pv as before.
+        int cp = 0;
         var cpMatch = _cpRegex.Match(line);
-        if (cpMatch.Success)
+        if (cpMatch.Success && int.TryParse(cpMatch.Groups[1].Value, out var cpParsed))
         {
-            if (int.TryParse(cpMatch.Groups[1].Value, out var cp))
-            {
-                if (_currentFen?.Contains(" b ") == true)
-                {
-                    cp = -cp;
-                }
-                info.Score = cp.ToString();
-            }
+            cp = cpParsed;
+            if (_currentFen?.Contains(" b ") == true)
+                cp = -cp;
         }
 
+        int depth = 0;
         var depthMatch = _depthRegex.Match(line);
-        if (depthMatch.Success)
+        if (depthMatch.Success && int.TryParse(depthMatch.Groups[1].Value, out var depthParsed))
         {
-            if (int.TryParse(depthMatch.Groups[1].Value, out var depth))
-            {
-                info.Depth = depth;
-            }
+            depth = depthParsed;
         }
 
+        long nodes = 0;
         var nodesMatch = _nodesRegex.Match(line);
-        if (nodesMatch.Success)
+        if (nodesMatch.Success && long.TryParse(nodesMatch.Groups[1].Value, out var nodesParsed))
         {
-            if (long.TryParse(nodesMatch.Groups[1].Value, out var nodes))
-            {
-                info.Nodes = nodes;
-            }
+            nodes = nodesParsed;
         }
 
+        long nps = 0;
         var npsMatch = _npsRegex.Match(line);
-        if (npsMatch.Success)
+        if (npsMatch.Success && long.TryParse(npsMatch.Groups[1].Value, out var npsParsed))
         {
-            if (long.TryParse(npsMatch.Groups[1].Value, out var nps))
-            {
-                info.Nps = nps;
-            }
+            nps = npsParsed;
         }
 
+        string pv = "";
         var pvMatch = _pvRegex.Match(line);
         if (pvMatch.Success)
         {
-            info.Pv = pvMatch.Groups[1].Value;
+            pv = pvMatch.Groups[1].Value;
+        }
+
+        // Update or add the multipv variation.
+        var existing = CurrentEngineInfo.Multipv.FirstOrDefault(m => m.Multipv == multipvNumber);
+        if (existing != null)
+        {
+            existing.Depth = depth;
+            existing.Score = cp.ToString();
+            existing.Pv = pv;
+        }
+        else
+        {
+            // Add new variation if not present.
+            CurrentEngineInfo.Multipv.Add(new MultipvInfo
+            {
+                Multipv = multipvNumber,
+                Depth = depth,
+                Score = cp.ToString(),
+                Pv = pv
+            });
+        }
+
+        // Also update the default (multipv 1) fields for convenience since they are used by main visualizer.
+        if (multipvNumber == 1)
+        {
+            CurrentEngineInfo.Score = cp.ToString();
+            CurrentEngineInfo.Depth = depth;
+            CurrentEngineInfo.Pv = pv;
         }
+        // Always update nps and nodes.
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: ```suggestion

        // Always update nps and nodes.
```
created_at: 2025-03-16T23:51:53Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,21 @@
-﻿namespace CcrlExtensionsHost.Models;
+﻿public class MultipvInfo
path: src/back/CcrlExtensionsHost/Models/EngineInfo.cs
user: login: eduherminio
body: Move it its own file, `MultipvInfo.cs`.
created_at: 2025-03-16T23:58:18Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,21 @@
-﻿namespace CcrlExtensionsHost.Models;
path: src/back/CcrlExtensionsHost/Models/EngineInfo.cs
user: login: eduherminio
body: Re-add namespace
created_at: 2025-03-16T23:58:27Z
author_association: COLLABORATOR
diff_hunk: @@ -8,6 +8,10 @@
         {
           "Name": "Threads",
           "Value": "1"
+        },
+        {
+          "Name": "multipv",
path: src/back/CcrlExtensionsHost/appsettings.json
user: login: eduherminio
body: ```suggestion
          "Name": "Multipv",
```
created_at: 2025-03-17T00:15:09Z
author_association: COLLABORATOR
diff_hunk: @@ -104,88 +104,107 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
         if (string.IsNullOrEmpty(line))
-        {
             return false;
-        }
 
+        // Only process lines containing "info" and "uciok"
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                if (CurrentEngineInfo == null)
+                    CurrentEngineInfo = new EngineInfo();
+                CurrentEngineInfo.Name = Config?.Name;
                 return true;
             }
-
             return false;
         }
 
-        if (line.Contains("lowerbound"))
-        {
+        if (line.Contains("lowerbound") || line.Contains("upperbound") || line.Contains("currmove"))
             return false;
-        }
 
-        if (line.Contains("upperbound"))
-        {
-            return false;
-        }
+        if (CurrentEngineInfo == null)
+            CurrentEngineInfo = new EngineInfo();
 
-        if (line.Contains("currmove"))
+        // Default multipv number is 1
+        int multipvNumber = 1;
+        var multipvMatch = _multipvRegex.Match(line);
+        if (multipvMatch.Success)
         {
-            return false;
+            int.TryParse(multipvMatch.Groups[1].Value, out multipvNumber);
         }
 
-        info.Name = Config?.Name;
-
+        // Parse the score (cp), depth, nodes, nps, and pv as before.
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: ```suggestion
        // Parse the score (cp), depth, nodes, nps, and pv.
```
created_at: 2025-03-17T00:15:25Z
author_association: COLLABORATOR
diff_hunk: @@ -104,88 +104,107 @@ private void HandleOutput(string? line)
     private static partial Regex PvRegex();
     private readonly Regex _pvRegex = PvRegex();
 
-    private bool TryGetEngineInfo(string? line, out EngineInfo info)
+    private bool TryUpdateEngineInfo(string? line)
     {
-        info = new EngineInfo();
         if (string.IsNullOrEmpty(line))
-        {
             return false;
-        }
 
+        // Only process lines containing "info" and "uciok"
         if (!line.Contains("info"))
         {
             if (line.Contains("uciok"))
             {
-                info.Name = Config?.Name;
-
+                if (CurrentEngineInfo == null)
+                    CurrentEngineInfo = new EngineInfo();
+                CurrentEngineInfo.Name = Config?.Name;
                 return true;
             }
-
             return false;
         }
 
-        if (line.Contains("lowerbound"))
-        {
+        if (line.Contains("lowerbound") || line.Contains("upperbound") || line.Contains("currmove"))
             return false;
-        }
 
-        if (line.Contains("upperbound"))
-        {
-            return false;
-        }
+        if (CurrentEngineInfo == null)
+            CurrentEngineInfo = new EngineInfo();
 
-        if (line.Contains("currmove"))
+        // Default multipv number is 1
+        int multipvNumber = 1;
+        var multipvMatch = _multipvRegex.Match(line);
+        if (multipvMatch.Success)
         {
-            return false;
+            int.TryParse(multipvMatch.Groups[1].Value, out multipvNumber);
         }
 
-        info.Name = Config?.Name;
-
+        // Parse the score (cp), depth, nodes, nps, and pv as before.
+        int cp = 0;
         var cpMatch = _cpRegex.Match(line);
-        if (cpMatch.Success)
+        if (cpMatch.Success && int.TryParse(cpMatch.Groups[1].Value, out var cpParsed))
         {
-            if (int.TryParse(cpMatch.Groups[1].Value, out var cp))
-            {
-                if (_currentFen?.Contains(" b ") == true)
-                {
-                    cp = -cp;
-                }
-                info.Score = cp.ToString();
-            }
+            cp = cpParsed;
+            if (_currentFen?.Contains(" b ") == true)
+                cp = -cp;
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: ```suggestion
            if (_currentFen?.Contains(" b ") == true)
            {
                cp = -cp;
            }
```
created_at: 2025-03-17T00:16:06Z
author_association: COLLABORATOR
diff_hunk: @@ -1,11 +1,21 @@
-﻿namespace CcrlExtensionsHost.Models;
path: src/back/CcrlExtensionsHost/Models/EngineInfo.cs
user: login: TheRealGioviok
body: Solved in https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/aa13ba0d340e3882192db512a8279fe4ee8e84ee
created_at: 2025-03-17T02:21:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,21 @@
-﻿namespace CcrlExtensionsHost.Models;
+﻿public class MultipvInfo
path: src/back/CcrlExtensionsHost/Models/EngineInfo.cs
user: login: TheRealGioviok
body: Solved in https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/aa13ba0d340e3882192db512a8279fe4ee8e84ee
created_at: 2025-03-17T02:21:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,11 +1,21 @@
-﻿namespace CcrlExtensionsHost.Models;
+﻿public class MultipvInfo
+{
+    public int Multipv { get; set; }
+    public string? Score { get; set; }
+    public int Depth { get; set; }
+    public string? Pv { get; set; }
+}
 
 public class EngineInfo
 {
+    // It is a bit redundant yes, but i dont want to change too much stuff.
path: src/back/CcrlExtensionsHost/Models/EngineInfo.cs
user: login: TheRealGioviok
body: Solved in https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/aa13ba0d340e3882192db512a8279fe4ee8e84ee
created_at: 2025-03-17T02:21:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,109 +118,87 @@ private void HandleOutput(string? line)
 
     private bool TryUpdateEngineInfo(string? line)
     {
-        if (string.IsNullOrEmpty(line))
-            return false;
-
-        // Only process lines containing "info" and "uciok"
-        if (!line.Contains("info"))
-        {
-            if (line.Contains("uciok"))
-            {
-                CurrentEngineInfo ??= new EngineInfo();
-                CurrentEngineInfo.Name = Config?.Name;
-                return true;
-            }
-            return false;
-        }
-
-        if (_stringsToIgnore.Any(line.Contains))
-        {
+        if (string.IsNullOrEmpty(line) || !line.Contains("info") || _stringsToIgnore.Any(line.Contains))
             return false;
-        }
 
         CurrentEngineInfo ??= new EngineInfo();
 
-        // Default multipv number is 1
         int multipvNumber = 1;
+        int depth = 0, cp = 0;
+        long nodes = 0, nps = 0;
+        string score = "0", pv = "";
+        int orderKey = 0;
+
+        // Default multipv number is 1
         var multipvMatch = _multipvRegex.Match(line);
         if (multipvMatch.Success && int.TryParse(multipvMatch.Groups[1].Value, out var multiPvNumberParsed))
         {
             multipvNumber = multiPvNumberParsed;
         }
 
-        // Parse the score (cp), depth, nodes, nps, and pv.
-        int cp = 0;
-        var cpMatch = _cpRegex.Match(line);
-        if (cpMatch.Success && int.TryParse(cpMatch.Groups[1].Value, out var cpParsed))
+        var mateMatch = _mateRegex.Match(line);
+        if (mateMatch.Success && int.TryParse(mateMatch.Groups[1].Value, out var mateMoves))
         {
-            cp = cpParsed;
-            if (_currentFen?.Contains(" b ") == true)
+            if (_currentFen?.Contains(" b ") == true) mateMoves = -mateMoves;
+            if (mateMoves < 0)
+            {
+                score = $"-M{-mateMoves}";
+            }
+            else
             {
-                cp = -cp;
+                score = $"M{mateMoves}";
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: Doesn't the client script expect `$"+M{mateMoves}"` here, with the `+` sign?
created_at: 2025-03-30T21:12:53Z
author_association: COLLABORATOR
diff_hunk: @@ -311,25 +349,24 @@ const convertPVToAlgebraic = (pv, startingFen) => {
         const engineInfos = JSON.parse(response.responseText).engineInfos;
         engineInfos.forEach((info, index) => {
           const kibitzerNum = index + 1;
-          const score = info.score / 100;
-          $(`#kibitzer${kibitzerNum}-score`).text(score.toFixed(2));
+          $(`#kibitzer${kibitzerNum}-score`).text(parseScore(info.score).tooltip);
           $(`#kibitzer${kibitzerNum}-depth`).text(info.depth);
           $(`#kibitzer${kibitzerNum}-nodes`).text(formatCompactNumber(info.nodes));
           $(`#kibitzer${kibitzerNum}-nps`).text(formatCompactNumber(info.nps));
 
           // Update kibitzer scores for the chart (positions 2 and 3 in the scores array)
           const datasetIndex = 2 + index;
-          scores[datasetIndex][plyStr] = score;
+          scores[datasetIndex][plyStr] = { x: plyStr, y: parseScore(info.score).value, tooltip: parseScore(info.score).tooltip };
+
 
           let multipvHtml = "";
           if (info.multipv) {
-            info.multipv.sort((a, b) => isWhiteToMove(currentFen) ? -a.score + b.score : a.score - b.score).forEach((variation) => {
-              const scoreNum = (parseInt(variation.score) / 100).toFixed(2);
+            info.multipv.sort((a, b) => a.orderKey - b.orderKey).forEach((variation) => {
               const algebraicPV = convertPVToAlgebraic(variation.pv, currentFen);
               multipvHtml += `
                 <div class="multipv-box" style="border: 1px solid #ccc; margin-bottom: 5px; padding: 5px;">
                   <div class="multipv-header" style="font-weight: bold;color: #aaa;">
-                    Depth: ${variation.depth} | Eval: ${scoreNum}
+                    Depth: ${variation.depth} | Eval: ${variation.score} | OrderKey: ${variation.orderKey}
path: src/front/ccrlExtensions.js
user: login: eduherminio
body: I don't think we should be printing here `OrderKey` other than for debugging, right?
created_at: 2025-03-30T21:14:52Z
author_association: COLLABORATOR
diff_hunk: @@ -113,109 +118,87 @@ private void HandleOutput(string? line)
 
     private bool TryUpdateEngineInfo(string? line)
     {
-        if (string.IsNullOrEmpty(line))
-            return false;
-
-        // Only process lines containing "info" and "uciok"
-        if (!line.Contains("info"))
-        {
-            if (line.Contains("uciok"))
-            {
-                CurrentEngineInfo ??= new EngineInfo();
-                CurrentEngineInfo.Name = Config?.Name;
-                return true;
-            }
-            return false;
-        }
-
-        if (_stringsToIgnore.Any(line.Contains))
-        {
+        if (string.IsNullOrEmpty(line) || !line.Contains("info") || _stringsToIgnore.Any(line.Contains))
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: eduherminio
body: This change is breaking the logic for detecting `uciok`, unless I'm missing something
created_at: 2025-03-30T21:16:00Z
author_association: COLLABORATOR
diff_hunk: @@ -105,8 +130,8 @@ const convertPVToAlgebraic = (pv, startingFen) => {
     'display': 'flex',
     'flex-direction': 'column',
     'gap': '15px',
-    'min-width': '0',
-    'max-width': 'none'
+    'min-width': '300px',
+    'max-width': '400px'
path: src/front/ccrlExtensions.js
user: login: eduherminio
body: Isn't this change bringing back the issue with the width restriction (https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/f3c2558c970c41043df7c78437382318e1712422)?
created_at: 2025-03-30T21:18:24Z
author_association: COLLABORATOR
diff_hunk: @@ -4,6 +4,8 @@ public class MultipvInfo
 {
     public int Multipv { get; set; }
     public string? Score { get; set; }
+    public string? Mate { get; set; }
     public int Depth { get; set; }
     public string? Pv { get; set; }
+    public int? orderKey { get; set; }
path: src/back/CcrlExtensionsHost/Models/MultipvInfo.cs
user: login: eduherminio
body: Style: this needs to be capitalized.
created_at: 2025-03-30T21:19:19Z
author_association: COLLABORATOR
diff_hunk: @@ -31,56 +31,81 @@
     return parts.length === 6 ? parseInt(parts[5]) : 1;
   };
 
-  const isWhiteToMove = (fen) => {
-    const parts = fen.split(" ");
-    return parts[1] == "w" ? true : false
-  };
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
 
-const convertPVToAlgebraic = (pv, startingFen) => {
-  if (typeof Chess === 'undefined') {
-    console.error("Chess.js not loaded");
-    return pv;
-  }
-  const chess = new Chess(startingFen);
-  const moves = pv.trim().split(/\s+/);
-  let groups = [];
-  let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
-  let isWhiteToMove = chess.turn() === 'w';
-
-  moves.forEach((move, i) => {
-    let moveObj = chess.move(move, { sloppy: true });
-    if (!moveObj) return;
-
-    if (isWhiteToMove) {
-      groups.push({
-        moveNumber: currentMoveNumber,
-        white: moveObj.san,
-        black: null
-      });
-    } else {
-      if (groups.length > 0) {
-        groups[groups.length - 1].black = moveObj.san;
-      } else {
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
         groups.push({
           moveNumber: currentMoveNumber,
-          white: "...",  // <-- Place "..." after move number when Black moves first
-          black: moveObj.san
+          white: moveObj.san,
+          black: null
         });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
       }
-      currentMoveNumber++;
-    }
 
-    isWhiteToMove = chess.turn() === 'w';
-  });
+      isWhiteToMove = chess.turn() === 'w';
+    });
 
-  return groups.map(group => {
-    let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
-    let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
-    let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
 
-    return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
-  }).join(" ");
-};
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    console.log(scoreText)
+    // Format: "+MX" where X is a positive integer (mate for white)
+    if (scoreText.startsWith("+M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+
+      // Use a high constant (e.g. 1000) so that mate scores are always above normal evals.
path: src/front/ccrlExtensions.js
user: login: eduherminio
body: Is this showing as a real +-1000 in the graph, or did you introduce something else to avoid such a huge gap?
created_at: 2025-03-30T21:26:10Z
author_association: COLLABORATOR
diff_hunk: @@ -31,56 +31,81 @@
     return parts.length === 6 ? parseInt(parts[5]) : 1;
   };
 
-  const isWhiteToMove = (fen) => {
-    const parts = fen.split(" ");
-    return parts[1] == "w" ? true : false
-  };
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
 
-const convertPVToAlgebraic = (pv, startingFen) => {
-  if (typeof Chess === 'undefined') {
-    console.error("Chess.js not loaded");
-    return pv;
-  }
-  const chess = new Chess(startingFen);
-  const moves = pv.trim().split(/\s+/);
-  let groups = [];
-  let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
-  let isWhiteToMove = chess.turn() === 'w';
-
-  moves.forEach((move, i) => {
-    let moveObj = chess.move(move, { sloppy: true });
-    if (!moveObj) return;
-
-    if (isWhiteToMove) {
-      groups.push({
-        moveNumber: currentMoveNumber,
-        white: moveObj.san,
-        black: null
-      });
-    } else {
-      if (groups.length > 0) {
-        groups[groups.length - 1].black = moveObj.san;
-      } else {
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
         groups.push({
           moveNumber: currentMoveNumber,
-          white: "...",  // <-- Place "..." after move number when Black moves first
-          black: moveObj.san
+          white: moveObj.san,
+          black: null
         });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
       }
-      currentMoveNumber++;
-    }
 
-    isWhiteToMove = chess.turn() === 'w';
-  });
+      isWhiteToMove = chess.turn() === 'w';
+    });
 
-  return groups.map(group => {
-    let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
-    let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
-    let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
 
-    return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
-  }).join(" ");
-};
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    console.log(scoreText)
+    // Format: "+MX" where X is a positive integer (mate for white)
+    if (scoreText.startsWith("+M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+
+      // Use a high constant (e.g. 1000) so that mate scores are always above normal evals.
path: src/front/ccrlExtensions.js
user: login: eduherminio
body: After a brief discord discussion, it might be better to use something like 50 - mate, and potentially cap cp to +15 or +20 so that mates are still guaranteed to show up higher than the highest potential non-mate ecore
created_at: 2025-03-30T23:30:23Z
author_association: COLLABORATOR
diff_hunk: @@ -4,6 +4,8 @@ public class MultipvInfo
 {
     public int Multipv { get; set; }
     public string? Score { get; set; }
+    public string? Mate { get; set; }
     public int Depth { get; set; }
     public string? Pv { get; set; }
+    public int? orderKey { get; set; }
path: src/back/CcrlExtensionsHost/Models/MultipvInfo.cs
user: login: TheRealGioviok
body: Solved by [2be0903](https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/2be09031e94d68697a87c59124462680e11609f8)
created_at: 2025-03-31T04:46:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,56 +31,81 @@
     return parts.length === 6 ? parseInt(parts[5]) : 1;
   };
 
-  const isWhiteToMove = (fen) => {
-    const parts = fen.split(" ");
-    return parts[1] == "w" ? true : false
-  };
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
 
-const convertPVToAlgebraic = (pv, startingFen) => {
-  if (typeof Chess === 'undefined') {
-    console.error("Chess.js not loaded");
-    return pv;
-  }
-  const chess = new Chess(startingFen);
-  const moves = pv.trim().split(/\s+/);
-  let groups = [];
-  let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
-  let isWhiteToMove = chess.turn() === 'w';
-
-  moves.forEach((move, i) => {
-    let moveObj = chess.move(move, { sloppy: true });
-    if (!moveObj) return;
-
-    if (isWhiteToMove) {
-      groups.push({
-        moveNumber: currentMoveNumber,
-        white: moveObj.san,
-        black: null
-      });
-    } else {
-      if (groups.length > 0) {
-        groups[groups.length - 1].black = moveObj.san;
-      } else {
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
         groups.push({
           moveNumber: currentMoveNumber,
-          white: "...",  // <-- Place "..." after move number when Black moves first
-          black: moveObj.san
+          white: moveObj.san,
+          black: null
         });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
       }
-      currentMoveNumber++;
-    }
 
-    isWhiteToMove = chess.turn() === 'w';
-  });
+      isWhiteToMove = chess.turn() === 'w';
+    });
 
-  return groups.map(group => {
-    let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
-    let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
-    let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
 
-    return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
-  }).join(" ");
-};
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    console.log(scoreText)
+    // Format: "+MX" where X is a positive integer (mate for white)
+    if (scoreText.startsWith("+M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+
+      // Use a high constant (e.g. 1000) so that mate scores are always above normal evals.
path: src/front/ccrlExtensions.js
user: login: TheRealGioviok
body: Solved by [120127f](https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/120127f82b20e543873c76d540139d58ee23582b)
created_at: 2025-03-31T04:47:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,8 +130,8 @@ const convertPVToAlgebraic = (pv, startingFen) => {
     'display': 'flex',
     'flex-direction': 'column',
     'gap': '15px',
-    'min-width': '0',
-    'max-width': 'none'
+    'min-width': '300px',
+    'max-width': '400px'
path: src/front/ccrlExtensions.js
user: login: TheRealGioviok
body: Solved by [9b74d2e](https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/9b74d2ec5f708b537bf87d7ccee8dac62e871a9f)
created_at: 2025-03-31T04:47:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,109 +118,87 @@ private void HandleOutput(string? line)
 
     private bool TryUpdateEngineInfo(string? line)
     {
-        if (string.IsNullOrEmpty(line))
-            return false;
-
-        // Only process lines containing "info" and "uciok"
-        if (!line.Contains("info"))
-        {
-            if (line.Contains("uciok"))
-            {
-                CurrentEngineInfo ??= new EngineInfo();
-                CurrentEngineInfo.Name = Config?.Name;
-                return true;
-            }
-            return false;
-        }
-
-        if (_stringsToIgnore.Any(line.Contains))
-        {
+        if (string.IsNullOrEmpty(line) || !line.Contains("info") || _stringsToIgnore.Any(line.Contains))
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: TheRealGioviok
body: Solved by [3f01ed3](https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/3f01ed3018e22f049c5014b4a884e081e8708c61)
created_at: 2025-03-31T04:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -311,25 +349,24 @@ const convertPVToAlgebraic = (pv, startingFen) => {
         const engineInfos = JSON.parse(response.responseText).engineInfos;
         engineInfos.forEach((info, index) => {
           const kibitzerNum = index + 1;
-          const score = info.score / 100;
-          $(`#kibitzer${kibitzerNum}-score`).text(score.toFixed(2));
+          $(`#kibitzer${kibitzerNum}-score`).text(parseScore(info.score).tooltip);
           $(`#kibitzer${kibitzerNum}-depth`).text(info.depth);
           $(`#kibitzer${kibitzerNum}-nodes`).text(formatCompactNumber(info.nodes));
           $(`#kibitzer${kibitzerNum}-nps`).text(formatCompactNumber(info.nps));
 
           // Update kibitzer scores for the chart (positions 2 and 3 in the scores array)
           const datasetIndex = 2 + index;
-          scores[datasetIndex][plyStr] = score;
+          scores[datasetIndex][plyStr] = { x: plyStr, y: parseScore(info.score).value, tooltip: parseScore(info.score).tooltip };
+
 
           let multipvHtml = "";
           if (info.multipv) {
-            info.multipv.sort((a, b) => isWhiteToMove(currentFen) ? -a.score + b.score : a.score - b.score).forEach((variation) => {
-              const scoreNum = (parseInt(variation.score) / 100).toFixed(2);
+            info.multipv.sort((a, b) => a.orderKey - b.orderKey).forEach((variation) => {
               const algebraicPV = convertPVToAlgebraic(variation.pv, currentFen);
               multipvHtml += `
                 <div class="multipv-box" style="border: 1px solid #ccc; margin-bottom: 5px; padding: 5px;">
                   <div class="multipv-header" style="font-weight: bold;color: #aaa;">
-                    Depth: ${variation.depth} | Eval: ${scoreNum}
+                    Depth: ${variation.depth} | Eval: ${variation.score} | OrderKey: ${variation.orderKey}
path: src/front/ccrlExtensions.js
user: login: TheRealGioviok
body: Solved by [37266dd](https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/37266ddc97c3b26b2fb8cab33df022833a4ea34d)
created_at: 2025-03-31T04:48:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,109 +118,87 @@ private void HandleOutput(string? line)
 
     private bool TryUpdateEngineInfo(string? line)
     {
-        if (string.IsNullOrEmpty(line))
-            return false;
-
-        // Only process lines containing "info" and "uciok"
-        if (!line.Contains("info"))
-        {
-            if (line.Contains("uciok"))
-            {
-                CurrentEngineInfo ??= new EngineInfo();
-                CurrentEngineInfo.Name = Config?.Name;
-                return true;
-            }
-            return false;
-        }
-
-        if (_stringsToIgnore.Any(line.Contains))
-        {
+        if (string.IsNullOrEmpty(line) || !line.Contains("info") || _stringsToIgnore.Any(line.Contains))
             return false;
-        }
 
         CurrentEngineInfo ??= new EngineInfo();
 
-        // Default multipv number is 1
         int multipvNumber = 1;
+        int depth = 0, cp = 0;
+        long nodes = 0, nps = 0;
+        string score = "0", pv = "";
+        int orderKey = 0;
+
+        // Default multipv number is 1
         var multipvMatch = _multipvRegex.Match(line);
         if (multipvMatch.Success && int.TryParse(multipvMatch.Groups[1].Value, out var multiPvNumberParsed))
         {
             multipvNumber = multiPvNumberParsed;
         }
 
-        // Parse the score (cp), depth, nodes, nps, and pv.
-        int cp = 0;
-        var cpMatch = _cpRegex.Match(line);
-        if (cpMatch.Success && int.TryParse(cpMatch.Groups[1].Value, out var cpParsed))
+        var mateMatch = _mateRegex.Match(line);
+        if (mateMatch.Success && int.TryParse(mateMatch.Groups[1].Value, out var mateMoves))
         {
-            cp = cpParsed;
-            if (_currentFen?.Contains(" b ") == true)
+            if (_currentFen?.Contains(" b ") == true) mateMoves = -mateMoves;
+            if (mateMoves < 0)
+            {
+                score = $"-M{-mateMoves}";
+            }
+            else
             {
-                cp = -cp;
+                score = $"M{mateMoves}";
path: src/back/CcrlExtensionsHost/Services/Engine.cs
user: login: TheRealGioviok
body: Solved by [9abd991](https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/9abd991260c6abd1a200e8db1ce370e1e17caa9e)
created_at: 2025-03-31T04:48:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,240 +8,397 @@
 // @description Kibitzer and eval graph support for CCRL
 // @require     https://code.jquery.com/jquery-3.7.0.min.js
 // @require     https://cdn.jsdelivr.net/npm/chart.js
+// @require     https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
 // ==/UserScript==
 
-(function() {
+(function () {
   'use strict';
 
-    var container = $('.container');
-    container.css('max-width', '180vh');
-
-    var layout = $('.main-layout');
-    layout.css('grid-template-columns', '40% 30% 30%');
-
-    for(var kibitzerIndex = 0; kibitzerIndex < 2; kibitzerIndex++)
-    {
-      var kibitzerNum = kibitzerIndex + 1;
-      layout.append(`<div id="kibitzer${kibitzerNum}-info"></div>`);
-      var kibitzerInfo = $(`#kibitzer${kibitzerNum}-info`);
-      var row = kibitzerIndex % 2 == 0 ? '1' : '4';
-      kibitzerInfo.css('grid-row-start', row);
-      kibitzerInfo.css('grid-column-start', '3');
-
-      kibitzerInfo.append(`<h3 id="kibitzer${kibitzerNum}-name">Kibitzer ${kibitzerNum} inactive</h3>`);
-      kibitzerInfo.append(`
-    <div class="card fluid">
-      <div class="row">
-        <div class="col-sm">
-          <div class="row">
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Score</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-score">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Depth</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-depth">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nodes</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nodes">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nps</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nps">0</p>
+  // Utility functions
+  const formatCompactNumber = (number) => {
+    if (number < 1000) return number;
+    const units = ['k', 'M', 'B', 'T'];
+    let unitIndex = -1;
+    while (number >= 1000 && unitIndex < units.length - 1) {
+      number /= 1000;
+      unitIndex++;
+    }
+    return `${number.toFixed(2)}${units[unitIndex]}`;
+  };
+
+  const getFullmoveNumberFromFen = (fen) => {
+    const parts = fen.split(" ");
+    return parts.length === 6 ? parseInt(parts[5]) : 1;
+  };
+
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
+
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
+        groups.push({
+          moveNumber: currentMoveNumber,
+          white: moveObj.san,
+          black: null
+        });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
+      }
+
+      isWhiteToMove = chess.turn() === 'w';
+    });
+
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    console.log(scoreText)
+    // Format: "+MX" where X is a positive integer (mate for white)
+    if (scoreText.startsWith("+M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+
+      // Use a high constant (e.g. 1000) so that mate scores are always above normal evals.
+      const value = 50 - (isNaN(mateMoves) ? 0 : mateMoves * 0.01);
+      return { value: value, tooltip: scoreText };
+    }
+    // Format: "-MX" where X is a positive integer (mate for black)
+    else if (scoreText.startsWith("-M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+      // Use a low constant so that mate scores are always below normal evals.
+      const value = -50 + (isNaN(mateMoves) ? 0 : mateMoves * 0.01);
+      return { value: value, tooltip: scoreText };
+    }
+    else {
+      const numStr = scoreText;
+      const value = parseFloat(numStr).clamp(-20,20);
path: src/front/ccrlExtensions.js
user: login: eduherminio
body: As discussed, this fails for me but not for Gio.
Let's do `Math.min(Math.max(parseFloat(numStr), -20), 20);` to play it safe, I guess, unless we can think of a more clever idea/solution.
created_at: 2025-03-31T15:52:10Z
author_association: COLLABORATOR
diff_hunk: @@ -8,240 +8,397 @@
 // @description Kibitzer and eval graph support for CCRL
 // @require     https://code.jquery.com/jquery-3.7.0.min.js
 // @require     https://cdn.jsdelivr.net/npm/chart.js
+// @require     https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
 // ==/UserScript==
 
-(function() {
+(function () {
   'use strict';
 
-    var container = $('.container');
-    container.css('max-width', '180vh');
-
-    var layout = $('.main-layout');
-    layout.css('grid-template-columns', '40% 30% 30%');
-
-    for(var kibitzerIndex = 0; kibitzerIndex < 2; kibitzerIndex++)
-    {
-      var kibitzerNum = kibitzerIndex + 1;
-      layout.append(`<div id="kibitzer${kibitzerNum}-info"></div>`);
-      var kibitzerInfo = $(`#kibitzer${kibitzerNum}-info`);
-      var row = kibitzerIndex % 2 == 0 ? '1' : '4';
-      kibitzerInfo.css('grid-row-start', row);
-      kibitzerInfo.css('grid-column-start', '3');
-
-      kibitzerInfo.append(`<h3 id="kibitzer${kibitzerNum}-name">Kibitzer ${kibitzerNum} inactive</h3>`);
-      kibitzerInfo.append(`
-    <div class="card fluid">
-      <div class="row">
-        <div class="col-sm">
-          <div class="row">
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Score</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-score">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Depth</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-depth">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nodes</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nodes">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nps</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nps">0</p>
+  // Utility functions
+  const formatCompactNumber = (number) => {
+    if (number < 1000) return number;
+    const units = ['k', 'M', 'B', 'T'];
+    let unitIndex = -1;
+    while (number >= 1000 && unitIndex < units.length - 1) {
+      number /= 1000;
+      unitIndex++;
+    }
+    return `${number.toFixed(2)}${units[unitIndex]}`;
+  };
+
+  const getFullmoveNumberFromFen = (fen) => {
+    const parts = fen.split(" ");
+    return parts.length === 6 ? parseInt(parts[5]) : 1;
+  };
+
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
+
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
+        groups.push({
+          moveNumber: currentMoveNumber,
+          white: moveObj.san,
+          black: null
+        });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
+      }
+
+      isWhiteToMove = chess.turn() === 'w';
+    });
+
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    console.log(scoreText)
path: src/front/ccrlExtensions.js
user: login: eduherminio
body: Let's maybe comment this bit.
created_at: 2025-03-31T15:52:27Z
author_association: COLLABORATOR
diff_hunk: @@ -8,240 +8,397 @@
 // @description Kibitzer and eval graph support for CCRL
 // @require     https://code.jquery.com/jquery-3.7.0.min.js
 // @require     https://cdn.jsdelivr.net/npm/chart.js
+// @require     https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
 // ==/UserScript==
 
-(function() {
+(function () {
   'use strict';
 
-    var container = $('.container');
-    container.css('max-width', '180vh');
-
-    var layout = $('.main-layout');
-    layout.css('grid-template-columns', '40% 30% 30%');
-
-    for(var kibitzerIndex = 0; kibitzerIndex < 2; kibitzerIndex++)
-    {
-      var kibitzerNum = kibitzerIndex + 1;
-      layout.append(`<div id="kibitzer${kibitzerNum}-info"></div>`);
-      var kibitzerInfo = $(`#kibitzer${kibitzerNum}-info`);
-      var row = kibitzerIndex % 2 == 0 ? '1' : '4';
-      kibitzerInfo.css('grid-row-start', row);
-      kibitzerInfo.css('grid-column-start', '3');
-
-      kibitzerInfo.append(`<h3 id="kibitzer${kibitzerNum}-name">Kibitzer ${kibitzerNum} inactive</h3>`);
-      kibitzerInfo.append(`
-    <div class="card fluid">
-      <div class="row">
-        <div class="col-sm">
-          <div class="row">
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Score</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-score">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Depth</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-depth">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nodes</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nodes">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nps</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nps">0</p>
+  // Utility functions
+  const formatCompactNumber = (number) => {
+    if (number < 1000) return number;
+    const units = ['k', 'M', 'B', 'T'];
+    let unitIndex = -1;
+    while (number >= 1000 && unitIndex < units.length - 1) {
+      number /= 1000;
+      unitIndex++;
+    }
+    return `${number.toFixed(2)}${units[unitIndex]}`;
+  };
+
+  const getFullmoveNumberFromFen = (fen) => {
+    const parts = fen.split(" ");
+    return parts.length === 6 ? parseInt(parts[5]) : 1;
+  };
+
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
+
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
+        groups.push({
+          moveNumber: currentMoveNumber,
+          white: moveObj.san,
+          black: null
+        });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
+      }
+
+      isWhiteToMove = chess.turn() === 'w';
+    });
+
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    console.log(scoreText)
path: src/front/ccrlExtensions.js
user: login: TheRealGioviok
body: Solved by a379391ab1805389ceb32af68ced4610dab72d59
created_at: 2025-04-05T16:37:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,240 +8,397 @@
 // @description Kibitzer and eval graph support for CCRL
 // @require     https://code.jquery.com/jquery-3.7.0.min.js
 // @require     https://cdn.jsdelivr.net/npm/chart.js
+// @require     https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js
 // ==/UserScript==
 
-(function() {
+(function () {
   'use strict';
 
-    var container = $('.container');
-    container.css('max-width', '180vh');
-
-    var layout = $('.main-layout');
-    layout.css('grid-template-columns', '40% 30% 30%');
-
-    for(var kibitzerIndex = 0; kibitzerIndex < 2; kibitzerIndex++)
-    {
-      var kibitzerNum = kibitzerIndex + 1;
-      layout.append(`<div id="kibitzer${kibitzerNum}-info"></div>`);
-      var kibitzerInfo = $(`#kibitzer${kibitzerNum}-info`);
-      var row = kibitzerIndex % 2 == 0 ? '1' : '4';
-      kibitzerInfo.css('grid-row-start', row);
-      kibitzerInfo.css('grid-column-start', '3');
-
-      kibitzerInfo.append(`<h3 id="kibitzer${kibitzerNum}-name">Kibitzer ${kibitzerNum} inactive</h3>`);
-      kibitzerInfo.append(`
-    <div class="card fluid">
-      <div class="row">
-        <div class="col-sm">
-          <div class="row">
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Score</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-score">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Depth</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-depth">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nodes</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nodes">0</p>
-            </div>
-            <div class="col-sm info">
-              <p class="small-margin"><small class="info-header">Nps</small></p>
-              <p class="small-margin info-value" id="kibitzer${kibitzerNum}-nps">0</p>
+  // Utility functions
+  const formatCompactNumber = (number) => {
+    if (number < 1000) return number;
+    const units = ['k', 'M', 'B', 'T'];
+    let unitIndex = -1;
+    while (number >= 1000 && unitIndex < units.length - 1) {
+      number /= 1000;
+      unitIndex++;
+    }
+    return `${number.toFixed(2)}${units[unitIndex]}`;
+  };
+
+  const getFullmoveNumberFromFen = (fen) => {
+    const parts = fen.split(" ");
+    return parts.length === 6 ? parseInt(parts[5]) : 1;
+  };
+
+  const convertPVToAlgebraic = (pv, startingFen) => {
+    if (typeof Chess === 'undefined') {
+      console.error("Chess.js not loaded");
+      return pv;
+    }
+    const chess = new Chess(startingFen);
+    const moves = pv.trim().split(/\s+/);
+    let groups = [];
+    let currentMoveNumber = getFullmoveNumberFromFen(startingFen);
+    let isWhiteToMove = chess.turn() === 'w';
+
+    moves.forEach((move, i) => {
+      let moveObj = chess.move(move, { sloppy: true });
+      if (!moveObj) return;
+
+      if (isWhiteToMove) {
+        groups.push({
+          moveNumber: currentMoveNumber,
+          white: moveObj.san,
+          black: null
+        });
+      } else {
+        if (groups.length > 0) {
+          groups[groups.length - 1].black = moveObj.san;
+        } else {
+          groups.push({
+            moveNumber: currentMoveNumber,
+            white: "...",  // <-- Place "..." after move number when Black moves first
+            black: moveObj.san
+          });
+        }
+        currentMoveNumber++;
+      }
+
+      isWhiteToMove = chess.turn() === 'w';
+    });
+
+    return groups.map(group => {
+      let moveNumberHtml = `<span style="color: #f5c276; font-weight: bold;">${group.moveNumber}.</span>`;
+      let whiteMove = group.white ? `<span style="color: #ffffff;">${group.white}</span>` : '';
+      let blackMove = group.black ? `<span style="color: #ffffff;">${group.black}</span>` : '';
+
+      return `${moveNumberHtml} ${whiteMove} ${blackMove}`.trim();
+    }).join(" ");
+  };
+
+  // A helper function to parse the score string into a numeric value and display string
+  const parseScore = (scoreText) => {
+    scoreText = scoreText.trim();
+    console.log(scoreText)
+    // Format: "+MX" where X is a positive integer (mate for white)
+    if (scoreText.startsWith("+M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+
+      // Use a high constant (e.g. 1000) so that mate scores are always above normal evals.
+      const value = 50 - (isNaN(mateMoves) ? 0 : mateMoves * 0.01);
+      return { value: value, tooltip: scoreText };
+    }
+    // Format: "-MX" where X is a positive integer (mate for black)
+    else if (scoreText.startsWith("-M")) {
+      const mateMoves = parseInt(scoreText.substring(2));
+      // Use a low constant so that mate scores are always below normal evals.
+      const value = -50 + (isNaN(mateMoves) ? 0 : mateMoves * 0.01);
+      return { value: value, tooltip: scoreText };
+    }
+    else {
+      const numStr = scoreText;
+      const value = parseFloat(numStr).clamp(-20,20);
path: src/front/ccrlExtensions.js
user: login: TheRealGioviok
body: Solved by 110b0bf8400f5aa5e2cb8c5c583c8d3ddef6b139
created_at: 2025-04-05T16:38:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: TheRealGioviok
created_at: 2025-03-16T23:47:41Z
body: Size of the analysis being to small seems to be a problem related to a min-max width constraint that i for some reason have added and forgot about. https://github.com/TheRealGioviok/CcrlExtensions/commit/f3c2558c970c41043df7c78437382318e1712422 should fix this. 
author_association: CONTRIBUTOR
user: login: eduherminio
created_at: 2025-03-17T00:21:24Z
body: @TheRealGioviok I managed to catch a non-ordered set of pvs
![image](https://github.com/user-attachments/assets/cc27c414-8381-4b94-b8d1-173fea0bce02)

author_association: COLLABORATOR
user: login: TheRealGioviok
created_at: 2025-03-17T01:58:11Z
body: About the non ordered set of pvs, there is something strange with the third line not having a pv at all. Do you have anything else that i can use to pinpoint this behaviour?

author_association: CONTRIBUTOR
user: login: eduherminio
created_at: 2025-03-17T10:02:26Z
body: BE changes LGTM, thanks!

Regarding the unordered pvs, unfortunately I was dumb enough not to screenshot the whole page to at least know the position.

I'll keep trying it this week while watching D7 to try to get a scenario to reproduce, but it seems to happen while for a new depth some pvs are not provided (maybe because the case you mentioned of legal moves < multipv?). I'll try to pay attention to endgames.

In that scenario when there's some pv missing, can we hide the eval (or print `Eval: —` instead)?
I assume that might be easier than removing removing the whole block for that pv, which is what lichess does.
author_association: COLLABORATOR
user: login: eduherminio
created_at: 2025-03-17T10:38:48Z
body: It seems to happen more often right after starting the engine, and stabilizes a couple of seconds later
![image](https://github.com/user-attachments/assets/3108d0d1-8ca6-41a0-8233-420f8ff030e9)

----

But I also noticed similar issues happening at 'lower' depths even after the backend being running for a while:

![image](https://github.com/user-attachments/assets/8a9c9674-a050-47f4-aded-d3cdd67c9876)

In this scenario:
Backend:
```
[11:27:06 DBG] Stockfish 17 >> info depth 18 seldepth 28 multipv 1 score cp 12 nodes 932235 nps 585207 hashfull 303 tbhits 0 time 1593 pv f2f4 a5b3 c1d1 b3d4 d3g6 f7g6 d1d4 d8c7 d4d2 b6b5 h2g4 g8f7 g2g3
[11:27:06 DBG] Stockfish 17 >> info depth 18 seldepth 32 multipv 2 score cp 0 nodes 932235 nps 585207 hashfull 303 tbhits 0 time 1593 pv b2b4 e7g5 f2f4 a5b3 c1e3 d8c8 c3e2 g5e7 h2g4 e8a4 g4f6 g8g7 f4f5 e6f5 e3g5 c8h8 d3f5 h8h6 g5h6 g7h6 f6g8 h6g5 f5c2 e7d8 g2g3 b6b5 e2f4 b3d4 f4h3 g5h5 h3f4
[11:27:06 DBG] Stockfish 17 >> info depth 18 seldepth 24 multipv 3 score cp -2 nodes 932235 nps 585207 hashfull 303 tbhits 0 time 1593 pv h2f3 a5c4 a3a4 g8g7 g2g3 e8c6 g3g2 d8h8 g2g3 a7a6 b2b3 c4a5 d3a6 a5b3 c1b2 h8a8
[11:27:06 DBG] Stockfish 17 >> info depth 18 seldepth 31 multipv 4 score cp -54 nodes 932235 nps 585207 hashfull 303 tbhits 0 time 1593 pv h2g4 e7g5 f2f4 g5e7 b2b4 a5c4 a3a4 a7a6 c1h1 e7h4 g2f1 c4b2 d3c2 g8g7
[11:27:06 DBG] Stockfish 17 >> info depth 18 seldepth 25 multipv 5 score cp -61 nodes 932235 nps 585207 hashfull 303 tbhits 0 time 1593 pv c3d1 e7g5 f2f4 g5e7 h2f3 a5c4 d1f2 e8a4 f2g4 g8g7 d3c4 d5c4 c1c4 a4d1 g4f2 d1f3 g2f3 b6b5 c4c3 d8a8 f3g3 a8h8

// 🔽
[11:27:06 DBG] Stockfish 17 >> info depth 19 seldepth 31 multipv 1 score cp 4 nodes 1301695 nps 568922 hashfull 405 tbhits 0 time 2288 pv f2f4 a5b3 c1d1 b3d4 d3g6 f7g6 d1d4 d8c7 d4d2 b6b5 h2f3 a7a5 f3d4 b5b4 a3b4 e7b4 d4e6 c7c8 f4f5 c8c4 d2d4 g6f5
[11:27:06 DBG] Stockfish 17 >> info depth 19 seldepth 33 multipv 2 score cp 0 nodes 1301695 nps 568922 hashfull 405 tbhits 0 time 2288 pv b2b4 e7g5 f2f4 a5b3 c1e3 d8c8 c3e2 g5e7 h2g4 e8a4 g4f6 g8g7 f4f5 e6f5 e3g5 c8h8 d3f5 h8h6 g5h6 g7h6 f6g8 h6g5 f5c2 e7d8 g2g3 b6b5 e2f4 b3d2 f4h3 g5h5 h3f4 h5g5
[11:27:06 DBG] Stockfish 17 >> info depth 19 seldepth 21 multipv 3 score cp -2 nodes 1301695 nps 568922 hashfull 405 tbhits 0 time 2288 pv h2f3 a5c4 a3a4 g8g7 g2g3 e8c6 g3g2 c6d7 c3b5 a7a6 d3c4 d5c4 b5d6
[11:27:06 DBG] Stockfish 17 >> info depth 19 seldepth 30 multipv 4 score cp -58 nodes 1301695 nps 568922 hashfull 405 tbhits 0 time 2288 pv h2g4 e7g5 f2f4 g5e7 b2b4 a5c4 a3a4 a7a6 c1h1 c4b2 d3a6 b2a4 c3e2 a4b2 h1h3 b6b5 h3h6
[11:27:06 DBG] Stockfish 17 >> info depth 19 seldepth 33 multipv 5 score cp -58 nodes 1301695 nps 568922 hashfull 405 tbhits 0 time 2288 pv c3d1 e7g5 f2f4 g5e7 h2f3 a5c4 d1f2 g8g7 f2g4 e8a4 d3c4 d5c4 c1c4 a4d1 g4f2 d1f3 g2f3 d8a8 f2e4 a8h8 c4f1 h8h5 f3e3 h5h2


[11:27:07 DBG] Stockfish 17 >> info depth 20 seldepth 31 multipv 1 score cp 0 nodes 1848169 nps 525197 hashfull 541 tbhits 0 time 3519 pv f2f4 a5b3 c1d1 b3d4 d3g6 f7g6 d1d4 d8c7 g2g3 b6b5 h2g4 c7c4 g4f6 g8f8 d4a7 e7c5 a7b8 c4d3 g3g4 d3f5 g4f3 f5d3 f3g4
[11:27:07 DBG] Stockfish 17 >> info depth 20 seldepth 32 multipv 2 score cp 0 nodes 1848169 nps 525197 hashfull 541 tbhits 0 time 3519 pv b2b4 e7g5 f2f4 a5b3 c1e3 d8c8 c3e2 g5e7 h2g4 e8a4 g4f6 g8g7 f4f5 e6f5 e3g5 c8h8 d3f5 h8h6 g5h6 g7h6 f6g8 h6g5 f5d3
[11:27:07 DBG] Stockfish 17 >> info depth 20 seldepth 26 multipv 3 score cp -2 nodes 1848169 nps 525197 hashfull 541 tbhits 0 time 3519 pv h2f3 a5c4 a3a4 g8g7 g2g3 e8c6 g3g2 a7a6 b2b3 c4a5 d3a6 a5b3 c1f4 b3a5 a6b5 c6b5 c3b5 a5c4 f4g3 d8a8 g3f4 a8a4 b5d6
[11:27:07 DBG] Stockfish 17 >> info depth 20 seldepth 30 multipv 4 score cp -59 nodes 1848169 nps 525197 hashfull 541 tbhits 0 time 3519 pv h2g4 e7g5 f2f4 g5e7 b2b4 a5c4 a3a4 a7a6 c1h1 c4b2 d3c2 e7h4 h1h3 g8g7 b4b5 a6b5 a4b5 e8d7
```

Frontend: see screenshot, cp seems to be parsed correctly but moves aren't. No console errors
author_association: COLLABORATOR
user: login: TheRealGioviok
created_at: 2025-03-31T04:54:10Z
body: > The only thing that was stopping us from merging the PR before this last commit was the unchecked item in the description (`number of legal moves goes from >= multipv to < multipv`). How does this last commit address that?

The reason that this bug happened in the first place was that we are reusing multipv slots to make it so when a depth is not completed for all multipv lines, then the not completed moves have still their lower depth line shown. This is a problem the moment some of these lines are not updated anymore when the number of moves goes from >= multipv to < multipv. To address this, I simply opted to wipe the multipv list when a new fen is parsed ([101c956](https://github.com/GediminasMasaitis/CcrlExtensions/pull/11/commits/101c956a4c1e913b19d1ef8a14ff64819b03b91f)). Thus, when the new lines will be added in the low moves position, they will not go beyond multipv by default.
author_association: CONTRIBUTOR
user: login: eduherminio
created_at: 2025-03-31T15:47:42Z
body: M+42
![+42](https://github.com/user-attachments/assets/49188233-d376-4624-9f6e-ce8ea3f4a6da)

M+24, a bit later:
![image](https://github.com/user-attachments/assets/45d2d27f-4ea4-4d9f-90d0-6ee1d435d4a8)

We're doing `50 - (isNaN(mateMoves) ? 0 : mateMoves * 0.01);` and capping scores to 20.

@TheRealGioviok What do you think about doing `30 - mateMoves * 0.1`? A 50 mate score would still show 25, (a big enough jump on top of 20?, and mate score increase would still be noticeable)
I'd even throw a +-15 cap into the equation to make the gap more noticeable while still keeping the graph more contained if that helps to convince you 😄
author_association: COLLABORATOR
user: login: TheRealGioviok
created_at: 2025-04-05T16:40:17Z
body: I think i spotted a bug in mate visualization for blackside in your image, wait before merging so i can double check
author_association: CONTRIBUTOR
user: login: TheRealGioviok
created_at: 2025-04-05T16:44:00Z
body: > I think i spotted a bug in mate visualization for blackside in your image, wait before merging so i can double check

Should be fixed in 8682267531928eb24a040b9aab8a2f6a4661c3f8 , but i couldn't double check in ccrl.
author_association: CONTRIBUTOR
user: login: eduherminio
created_at: 2025-04-06T13:30:05Z
body: Thanks a lot for this contribution!
author_association: COLLABORATOR