From 6cc299874de8d81e0b759d8abfc49b5d898e023d Mon Sep 17 00:00:00 2001 From: kmorrison Date: Mon, 11 Apr 2022 17:13:34 -0400 Subject: [PATCH 1/6] Add multipv support to uci.SearchResults, slight refactor of CmdGo result scanner to make testing easier. --- uci/cmd.go | 34 ++++++++++++++++----- uci/engine.go | 7 +++++ uci/engine_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++ uci/info.go | 1 + 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/uci/cmd.go b/uci/cmd.go index 2b59fca..27b2bea 100644 --- a/uci/cmd.go +++ b/uci/cmd.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "fmt" + "log" "strings" "time" @@ -278,23 +279,35 @@ func (cmd CmdGo) String() string { // ProcessResponse implements the Cmd interface func (CmdGo) ProcessResponse(e *Engine) error { scanner := bufio.NewScanner(e.out) + results, err := ProcessEngineOutput(scanner, e.getDebugLogger()) + if err != nil { + return err + } + e.results = *results + return nil +} + +func ProcessEngineOutput(scanner *bufio.Scanner, debugLogger *log.Logger) (*SearchResults, error) { results := SearchResults{} for scanner.Scan() { - text := e.readLine(scanner) + text := scanner.Text() + if debugLogger != nil { + debugLogger.Println(text) + } if strings.HasPrefix(text, "bestmove") { parts := strings.Split(text, " ") if len(parts) <= 1 { - return errors.New("best move not found " + text) + return nil, errors.New("best move not found " + text) } bestMove, err := chess.UCINotation{}.Decode(nil, parts[1]) if err != nil { - return err + return nil, err } results.BestMove = bestMove if len(parts) >= 4 { ponderMove, err := chess.UCINotation{}.Decode(nil, parts[3]) if err != nil { - return err + return nil, err } results.Ponder = ponderMove } @@ -303,12 +316,17 @@ func (CmdGo) ProcessResponse(e *Engine) error { info := &Info{} err := info.UnmarshalText([]byte(text)) - if err == nil { - results.Info = *info + if err != nil { + continue + } + results.Info = *info + if info.Multipv == 1 { + // We've received the first PV line, so we can clear the multipvInfo + results.MultiPV = []*Info{} } + results.MultiPV = append(results.MultiPV, info) } - e.results = results - return nil + return &results, nil } func parseIDLine(s string) (string, string, error) { diff --git a/uci/engine.go b/uci/engine.go index 2008059..83729e3 100644 --- a/uci/engine.go +++ b/uci/engine.go @@ -38,6 +38,13 @@ func Logger(logger *log.Logger) func(e *Engine) { } } +func (e *Engine) getDebugLogger() *log.Logger { + if e.debug { + return e.logger + } + return nil +} + // New constructs an engine from the executable path (found using exec.LookPath). // New also starts running the executable process in the background. Once created // the Engine can be controlled via the Run method. diff --git a/uci/engine_test.go b/uci/engine_test.go index af23ce1..495d2af 100644 --- a/uci/engine_test.go +++ b/uci/engine_test.go @@ -1,6 +1,7 @@ package uci_test import ( + "bufio" "bytes" "fmt" "log" @@ -125,6 +126,54 @@ func TestLogger(t *testing.T) { } } +func TestEngineOutputParsing(t *testing.T){ + scanner := bufio.NewScanner(strings.NewReader(logOutput)) + + results, err := uci.ProcessEngineOutput(scanner, nil) + if err != nil { + t.Fatal(err) + } + if len(results.MultiPV) != 1 { + t.Fatalf("expected 1 multipv results but got %d", len(results.MultiPV)) + } + + if results.MultiPV[0].Score.CP != 50 { + t.Fatalf("expected score of 50 but got %d", results.MultiPV[0].Score.CP) + } + if results.Info.Score.CP != 50 { + t.Fatalf("expected score of 50 but got %d", results.Info.Score.CP) + } + + // Parse BestMove and assert it is expected + if results.BestMove.S2() != chess.E4 { + t.Fatalf("expected E4 to be best move square, got %s", results.BestMove.S2()) + } +} + +func TestMultiPVEngineOutputParsing(t *testing.T){ + scanner := bufio.NewScanner(strings.NewReader(multipvTestingString)) + + results, err := uci.ProcessEngineOutput(scanner, nil) + if err != nil { + t.Fatal(err) + } + if len(results.MultiPV) != 10 { + t.Fatalf("expected 10 multipv results but got %d", len(results.MultiPV)) + } + + if results.MultiPV[0].Score.CP != 47 { + t.Fatalf("expected score of 47 but got %d", results.MultiPV[0].Score.CP) + } + if results.MultiPV[1].Score.CP != 31 { + t.Fatalf("expected score of 31 but got %d", results.MultiPV[1].Score.CP) + } + + // Parse BestMove and assert it is expected + if results.BestMove.S2() != chess.E4 { + t.Fatalf("expected E4 to be best move square, got %s", results.BestMove.S2()) + } +} + var ( infoRegex = regexp.MustCompile("(?m)[\r\n]+^.*info.*$") ) @@ -178,3 +227,29 @@ info depth 11 seldepth 14 multipv 1 score cp 50 nodes 34551 nps 575850 tbhits 0 info depth 12 seldepth 14 multipv 1 score cp 50 nodes 55039 nps 534359 tbhits 0 time 103 pv e2e4 e7e5 g1f3 b8c6 d2d4 e5d4 f3d4 g8f6 b1c3 f8b4 bestmove e2e4 ponder c7c5` ) + +const ( + multipvTestingString = ` +info depth 19 currmove f2f3 currmovenumber 13 +info depth 19 currmove g1h3 currmovenumber 14 +info depth 19 currmove f2f4 currmovenumber 15 +info depth 19 currmove b1a3 currmovenumber 16 +info depth 19 currmove a2a4 currmovenumber 17 +info depth 19 currmove h2h4 currmovenumber 18 +info depth 19 currmove b2b4 currmovenumber 19 +info depth 19 currmove g2g4 currmovenumber 20 +info depth 19 currmove d2d3 currmovenumber 10 +info depth 19 currmove b2b3 currmovenumber 11 + +info depth 19 seldepth 23 multipv 1 score cp 47 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv e2e4 c7c5 g1f3 b8c6 d2d4 c5d4 f3d4 g7g6 c2c4 g8f6 b1c3 d7d6 f2f3 c6d4 d1d4 f8g7 c1e3 c8e6 f1e2 e8g8 d4d2 a8c8 c3d5 +info depth 19 seldepth 23 multipv 2 score cp 31 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv g1f3 g8f6 c2c4 e7e6 b1c3 d7d5 d2d4 f8b4 c1g5 b8d7 c4d5 e6d5 d1c2 h7h6 g5f6 b4c3 b2c3 d7f6 e2e3 e8g8 f1d3 +info depth 19 seldepth 22 multipv 3 score cp 30 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv c2c4 g8f6 g1f3 e7e6 b1c3 d7d5 d2d4 f8b4 c1g5 b8d7 c4d5 e6d5 d1c2 h7h6 g5f6 d8f6 a2a3 b4c3 c2c3 c7c6 e2e3 e8g8 +info depth 19 seldepth 22 multipv 4 score cp 29 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv d2d4 g8f6 c2c4 e7e6 g1f3 d7d5 b1c3 f8b4 c1g5 b8d7 c4d5 e6d5 d1c2 h7h6 g5f6 b4c3 b2c3 d7f6 e2e3 e8g8 f1d3 c8g4 +info depth 19 seldepth 24 multipv 5 score cp 23 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv g2g3 d7d5 g1f3 c7c5 f1g2 g8f6 e1g1 e7e6 d2d4 c5d4 f3d4 e6e5 d4b3 c8e6 c2c4 b8c6 c4d5 f6d5 b1c3 d5c3 b2c3 a8c8 +info depth 19 seldepth 21 multipv 6 score cp 13 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv e2e3 d7d5 d2d4 g8f6 g1f3 e7e6 f1d3 c7c5 b2b3 b7b6 e1g1 f8d6 b1d2 c5d4 e3d4 b8c6 a2a3 e8g8 c1b2 +info depth 19 seldepth 22 multipv 7 score cp 4 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv c2c3 g8f6 d2d4 c7c5 g1f3 d7d5 d4c5 e7e6 c1e3 f8e7 e3d4 d8c7 e2e3 e7c5 d4c5 c7c5 f1d3 e8g8 e1g1 +info depth 19 seldepth 25 multipv 8 score cp 0 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv a2a3 e7e5 e2e4 g8f6 b1c3 d7d5 e4d5 f6d5 d1h5 d5f6 h5e5 f8e7 c3b5 b8a6 e5d4 c8d7 b5c3 e8g8 f1a6 b7a6 g1f3 c7c5 d4f4 f6h5 f4e5 +info depth 19 seldepth 28 multipv 9 score cp 0 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv b1c3 d7d5 d2d4 g8f6 c1f4 c7c5 e2e3 c5d4 e3d4 a7a6 f1d3 b8c6 c3e2 d8b6 c2c3 b6b2 g1f3 c8g4 a2a4 g4f3 +info depth 19 seldepth 26 multipv 10 score cp -22 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv d2d3 d7d5 e2e4 d5e4 d3e4 d8d1 e1d1 e7e5 c1e3 g8f6 f2f3 c8e6 b1d2 b8d7 d2c4 e8c8 d1e1 f6e8 b2b3 e8d6 c4d6 f8d6 +bestmove e2e4 ponder c7c5` +) diff --git a/uci/info.go b/uci/info.go index 9ed4a8b..984877d 100644 --- a/uci/info.go +++ b/uci/info.go @@ -17,6 +17,7 @@ type SearchResults struct { BestMove *chess.Move Ponder *chess.Move Info Info + MultiPV []*Info } // Info corresponds to the "info" engine output: From 8dbe85dc733fd01047f7e9077531d328d345d245 Mon Sep 17 00:00:00 2001 From: Charalampos Mitsakis Date: Thu, 29 Sep 2022 12:26:05 +0300 Subject: [PATCH 2/6] minor formatting fix --- uci/info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uci/info.go b/uci/info.go index 984877d..60bee90 100644 --- a/uci/info.go +++ b/uci/info.go @@ -17,7 +17,7 @@ type SearchResults struct { BestMove *chess.Move Ponder *chess.Move Info Info - MultiPV []*Info + MultiPV []*Info } // Info corresponds to the "info" engine output: From 43c2f507dd8816de63ad34a7556c012d7369ba6f Mon Sep 17 00:00:00 2001 From: Charalampos Mitsakis Date: Thu, 29 Sep 2022 12:33:31 +0300 Subject: [PATCH 3/6] fix: get score in MultiPV search from info line with 'multipv 1' --- uci/cmd.go | 7 +++++-- uci/engine_test.go | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/uci/cmd.go b/uci/cmd.go index 27b2bea..dc5c488 100644 --- a/uci/cmd.go +++ b/uci/cmd.go @@ -319,10 +319,13 @@ func ProcessEngineOutput(scanner *bufio.Scanner, debugLogger *log.Logger) (*Sear if err != nil { continue } - results.Info = *info - if info.Multipv == 1 { + switch info.Multipv { + case 1: // We've received the first PV line, so we can clear the multipvInfo results.MultiPV = []*Info{} + results.Info = *info + case 0: + results.Info = *info } results.MultiPV = append(results.MultiPV, info) } diff --git a/uci/engine_test.go b/uci/engine_test.go index 495d2af..9f235e7 100644 --- a/uci/engine_test.go +++ b/uci/engine_test.go @@ -167,6 +167,9 @@ func TestMultiPVEngineOutputParsing(t *testing.T){ if results.MultiPV[1].Score.CP != 31 { t.Fatalf("expected score of 31 but got %d", results.MultiPV[1].Score.CP) } + if results.Info.Score.CP != 47 { + t.Fatalf("expected score of 47 but got %d", results.Info.Score.CP) + } // Parse BestMove and assert it is expected if results.BestMove.S2() != chess.E4 { From 2d7487320b3f3235022f51d4b7ea1b45684c6a01 Mon Sep 17 00:00:00 2001 From: Charalampos Mitsakis Date: Thu, 29 Sep 2022 13:15:54 +0300 Subject: [PATCH 4/6] refactor: change type of SearchResults.MultiPV from []*Info to []Info --- uci/cmd.go | 8 ++++---- uci/info.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/uci/cmd.go b/uci/cmd.go index dc5c488..d0eceeb 100644 --- a/uci/cmd.go +++ b/uci/cmd.go @@ -314,7 +314,7 @@ func ProcessEngineOutput(scanner *bufio.Scanner, debugLogger *log.Logger) (*Sear break } - info := &Info{} + var info Info err := info.UnmarshalText([]byte(text)) if err != nil { continue @@ -322,10 +322,10 @@ func ProcessEngineOutput(scanner *bufio.Scanner, debugLogger *log.Logger) (*Sear switch info.Multipv { case 1: // We've received the first PV line, so we can clear the multipvInfo - results.MultiPV = []*Info{} - results.Info = *info + results.MultiPV = []Info{} + results.Info = info case 0: - results.Info = *info + results.Info = info } results.MultiPV = append(results.MultiPV, info) } diff --git a/uci/info.go b/uci/info.go index 60bee90..bc889f4 100644 --- a/uci/info.go +++ b/uci/info.go @@ -17,7 +17,7 @@ type SearchResults struct { BestMove *chess.Move Ponder *chess.Move Info Info - MultiPV []*Info + MultiPV []Info } // Info corresponds to the "info" engine output: From 901eff027447c6a49843ecd61a51d692179e2824 Mon Sep 17 00:00:00 2001 From: Charalampos Mitsakis Date: Thu, 29 Sep 2022 13:56:50 +0300 Subject: [PATCH 5/6] append to results.MultiPV only if info.Multipv > 0 --- uci/cmd.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uci/cmd.go b/uci/cmd.go index d0eceeb..50c7671 100644 --- a/uci/cmd.go +++ b/uci/cmd.go @@ -327,7 +327,9 @@ func ProcessEngineOutput(scanner *bufio.Scanner, debugLogger *log.Logger) (*Sear case 0: results.Info = info } - results.MultiPV = append(results.MultiPV, info) + if info.Multipv > 0 { + results.MultiPV = append(results.MultiPV, info) + } } return &results, nil } From c3340e14a48a6ee1396597560653a3563f7a46a8 Mon Sep 17 00:00:00 2001 From: Charalampos Mitsakis Date: Thu, 29 Sep 2022 14:06:28 +0300 Subject: [PATCH 6/6] change multipvTestingString to have 2 sets of 10 multipv results --- uci/engine_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/uci/engine_test.go b/uci/engine_test.go index 9f235e7..994e5e7 100644 --- a/uci/engine_test.go +++ b/uci/engine_test.go @@ -233,6 +233,16 @@ bestmove e2e4 ponder c7c5` const ( multipvTestingString = ` +info depth 19 seldepth 23 multipv 1 score cp 470 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv e2e4 c7c5 g1f3 b8c6 d2d4 c5d4 f3d4 g7g6 c2c4 g8f6 b1c3 d7d6 f2f3 c6d4 d1d4 f8g7 c1e3 c8e6 f1e2 e8g8 d4d2 a8c8 c3d5 +info depth 19 seldepth 23 multipv 2 score cp 310 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv g1f3 g8f6 c2c4 e7e6 b1c3 d7d5 d2d4 f8b4 c1g5 b8d7 c4d5 e6d5 d1c2 h7h6 g5f6 b4c3 b2c3 d7f6 e2e3 e8g8 f1d3 +info depth 19 seldepth 22 multipv 3 score cp 300 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv c2c4 g8f6 g1f3 e7e6 b1c3 d7d5 d2d4 f8b4 c1g5 b8d7 c4d5 e6d5 d1c2 h7h6 g5f6 d8f6 a2a3 b4c3 c2c3 c7c6 e2e3 e8g8 +info depth 19 seldepth 22 multipv 4 score cp 290 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv d2d4 g8f6 c2c4 e7e6 g1f3 d7d5 b1c3 f8b4 c1g5 b8d7 c4d5 e6d5 d1c2 h7h6 g5f6 b4c3 b2c3 d7f6 e2e3 e8g8 f1d3 c8g4 +info depth 19 seldepth 24 multipv 5 score cp 230 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv g2g3 d7d5 g1f3 c7c5 f1g2 g8f6 e1g1 e7e6 d2d4 c5d4 f3d4 e6e5 d4b3 c8e6 c2c4 b8c6 c4d5 f6d5 b1c3 d5c3 b2c3 a8c8 +info depth 19 seldepth 21 multipv 6 score cp 130 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv e2e3 d7d5 d2d4 g8f6 g1f3 e7e6 f1d3 c7c5 b2b3 b7b6 e1g1 f8d6 b1d2 c5d4 e3d4 b8c6 a2a3 e8g8 c1b2 +info depth 19 seldepth 22 multipv 7 score cp 40 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv c2c3 g8f6 d2d4 c7c5 g1f3 d7d5 d4c5 e7e6 c1e3 f8e7 e3d4 d8c7 e2e3 e7c5 d4c5 c7c5 f1d3 e8g8 e1g1 +info depth 19 seldepth 25 multipv 8 score cp 0 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv a2a3 e7e5 e2e4 g8f6 b1c3 d7d5 e4d5 f6d5 d1h5 d5f6 h5e5 f8e7 c3b5 b8a6 e5d4 c8d7 b5c3 e8g8 f1a6 b7a6 g1f3 c7c5 d4f4 f6h5 f4e5 +info depth 19 seldepth 28 multipv 9 score cp 0 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv b1c3 d7d5 d2d4 g8f6 c1f4 c7c5 e2e3 c5d4 e3d4 a7a6 f1d3 b8c6 c3e2 d8b6 c2c3 b6b2 g1f3 c8g4 a2a4 g4f3 +info depth 19 seldepth 26 multipv 10 score cp -220 nodes 5953192 nps 550813 hashfull 953 tbhits 0 time 10808 pv d2d3 d7d5 e2e4 d5e4 d3e4 d8d1 e1d1 e7e5 c1e3 g8f6 f2f3 c8e6 b1d2 b8d7 d2c4 e8c8 d1e1 f6e8 b2b3 e8d6 c4d6 f8d6 info depth 19 currmove f2f3 currmovenumber 13 info depth 19 currmove g1h3 currmovenumber 14 info depth 19 currmove f2f4 currmovenumber 15