From 846a689bd70341e7a680c6446baeafa34ab78402 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 6 Nov 2024 18:26:06 -0500 Subject: [PATCH] uci: Add support for WDL in info lines When UCI_ShowWDL is set, stockfish will additionally include win, loss, and draw probability values in its info evaluation lines. This commit augments uci.Info.UnmarshalText() to parse this information when available and store it within uci.Score. (cherry picked from commit b83ccd91734a89169f21ab360d0e9a4d6df5342f) --- uci/engine_test.go | 23 ++++++++++++---- uci/info.go | 65 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/uci/engine_test.go b/uci/engine_test.go index af23ce1..0e57594 100644 --- a/uci/engine_test.go +++ b/uci/engine_test.go @@ -55,15 +55,28 @@ func TestEngine(t *testing.T) { t.Fatal(err) } defer eng.Close() - setOpt := uci.CmdSetOption{Name: "UCI_Elo", Value: "1500"} + setEloOpt := uci.CmdSetOption{Name: "UCI_Elo", Value: "1500"} + setWdlOpt := uci.CmdSetOption{Name: "UCI_ShowWDL", Value: "true"} setPos := uci.CmdPosition{Position: chess.StartingPosition()} - setGo := uci.CmdGo{MoveTime: time.Second / 10} - if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, setOpt, uci.CmdUCINewGame, setPos, setGo); err != nil { + setGo := uci.CmdGo{MoveTime: 5 * time.Second} + if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, setEloOpt, setWdlOpt, uci.CmdUCINewGame, setPos, setGo); err != nil { t.Fatal(err) } - if eng.SearchResults().BestMove.S2() != chess.E4 { + if eng.SearchResults().BestMove.S2() != chess.D4 { t.Fatal("expected a different move") } + // these wdl values are sensitive to stockfish version, KNPS, cpu, and + // search time. current values were set via stockfish version 17 on + // AMD Ryzen 7 5800X3D + if eng.SearchResults().Info.Score.Win != 86 { + t.Fatalf("expected a different win probability: %v", eng.SearchResults().Info.Score.Win) + } + if eng.SearchResults().Info.Score.Draw != 894 { + t.Fatalf("expected a different draw probability: %v", eng.SearchResults().Info.Score.Draw) + } + if eng.SearchResults().Info.Score.Loss != 20 { + t.Fatalf("expected a different draw probability: %v", eng.SearchResults().Info.Score.Loss) + } pos := &chess.Position{} pos.UnmarshalText([]byte("r4r2/1b2bppk/ppq1p3/2pp3n/5P2/1P2P3/PBPPQ1PP/R4RK1 w - - 0 2")) setPos.Position = pos @@ -90,7 +103,7 @@ func TestStop(t *testing.T) { if err := eng.Run(uci.CmdStop); err != nil { t.Fatal(err) } - if eng.SearchResults().BestMove.S2() != chess.D4 { + if eng.SearchResults().BestMove.S2() != chess.E4 { t.Fatal("expected a different move") } } diff --git a/uci/info.go b/uci/info.go index 9ed4a8b..32fb4c2 100644 --- a/uci/info.go +++ b/uci/info.go @@ -9,6 +9,8 @@ import ( "github.com/notnil/chess" ) +var missingWdlErr = errors.New("uci: wdl unavailable; this is mostly likely because UCI_ShowWDL has not been set") + // SearchResults is the result from the most recent CmdGo invocation. It includes // data such as the following: // info depth 21 seldepth 31 multipv 1 score cp 39 nodes 862438 nps 860716 hashfull 409 tbhits 0 time 1002 pv e2e4 @@ -110,16 +112,62 @@ type Info struct { // the score is just a lower bound. // * upperbound // the score is just an upper bound. +// * win +// the probability value of a win from the engine's point of view +// * draw +// the probability value of a draw from the engine's point of view +// * loss +// the probability value of a loss from the engine's point of view + type Score struct { CP int Mate int LowerBound bool UpperBound bool + Win int + Draw int + Loss int +} + +// WinPct returns the probability a given position can be converted into a +// win from the engine's perspective on a scale from 0.0->1.0. The engine +// must support the UCI_ShowWDL option and it must be set in order for this +// function to work. +func (score *Score) WinPct() (float32, error) { + total := score.Win + score.Draw + score.Loss + if total == 0 { + return 0.0, missingWdlErr + } + return float32(score.Win) / float32(total), nil +} + +// DrawPct returns the probability a given position can be converted into a +// draw from the engine's perspective on a scale from 0.0->1.0. The engine +// must support the UCI_ShowWDL option and it must be set in order for this +// function to work. +func (score *Score) DrawPct() (float32, error) { + total := score.Win + score.Draw + score.Loss + if total == 0 { + return 0.0, missingWdlErr + } + return float32(score.Draw) / float32(total), nil +} + +// LossPct returns the probability a given position can be converted into a +// draw from the engine's perspective on a scale from 0.0->1.0. The engine +// must support the UCI_ShowWDL option and it must be set in order for this +// function to work. +func (score *Score) LossPct() (float32, error) { + total := score.Win + score.Draw + score.Loss + if total == 0 { + return 0.0, missingWdlErr + } + return float32(score.Loss) / float32(total), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface and parses // data like the following: -// info depth 24 seldepth 32 multipv 1 score cp 29 nodes 5130101 nps 819897 hashfull 967 tbhits 0 time 6257 pv d2d4 +// info depth 24 seldepth 32 multipv 1 score cp 29 wdl 791 209 0 nodes 5130101 nps 819897 hashfull 967 tbhits 0 time 6257 pv d2d4 func (info *Info) UnmarshalText(text []byte) error { parts := strings.Split(string(text), " ") if len(parts) == 0 { @@ -170,6 +218,21 @@ func (info *Info) UnmarshalText(text []byte) error { return err } info.Score.CP = v + case "wdl": + var err error + info.Score.Win, err = strconv.Atoi(parts[i]) + if err != nil { + return err + } + info.Score.Draw, err = strconv.Atoi(parts[i+1]) + if err != nil { + return err + } + info.Score.Loss, err = strconv.Atoi(parts[i+2]) + if err != nil { + return err + } + i += 2 case "nodes": v, err := strconv.Atoi(s) if err != nil {