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 {