Skip to content

Commit

Permalink
uci: Add support for WDL in info lines
Browse files Browse the repository at this point in the history
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 b83ccd9)
  • Loading branch information
mikeb26 committed Nov 26, 2024
1 parent e70b770 commit 846a689
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 6 deletions.
23 changes: 18 additions & 5 deletions uci/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}
}
Expand Down
65 changes: 64 additions & 1 deletion uci/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 846a689

Please sign in to comment.