|
| 1 | +//go:build engine |
| 2 | + |
1 | 3 | package uci_test |
2 | 4 |
|
3 | 5 | import ( |
4 | | - "bytes" |
5 | 6 | "fmt" |
6 | | - "log" |
7 | | - "os" |
8 | | - "path/filepath" |
9 | | - "regexp" |
10 | | - "strings" |
| 7 | + "os/exec" |
11 | 8 | "testing" |
12 | 9 | "time" |
13 | 10 |
|
14 | 11 | "github.com/corentings/chess/v2" |
15 | 12 | "github.com/corentings/chess/v2/uci" |
16 | 13 | ) |
17 | 14 |
|
18 | | -var StockfishPath string |
| 15 | +var engines = []string{"stockfish", "lc0"} |
19 | 16 |
|
20 | | -//nolint:gochecknoinits // This is a test file |
21 | | -func init() { |
22 | | - dir, _ := os.Getwd() |
23 | | - StockfishPath = filepath.Join(dir, "..", "stockfish") |
| 17 | +func isEngineAvailable(engine string) bool { |
| 18 | + _, err := exec.LookPath(engine) |
| 19 | + return err == nil |
24 | 20 | } |
25 | 21 |
|
26 | 22 | func Test_EngineInfo(t *testing.T) { |
27 | | - t.SkipNow() |
28 | | - fenStr := "2n1k1r1/8/4B3/3p4/3P4/8/K7/N7 w - - 0 1" |
29 | | - pos := &chess.Position{} |
30 | | - if err := pos.UnmarshalText([]byte(fenStr)); err != nil { |
31 | | - t.Fatal("failed to parse FEN", err) |
32 | | - } |
33 | | - |
34 | | - eng, err := uci.New(StockfishPath, uci.Debug) |
35 | | - if err != nil { |
36 | | - t.Fatal(err) |
37 | | - } |
38 | | - defer eng.Close() |
39 | | - |
40 | | - cmdMultiPV := uci.CmdSetOption{Name: "multipv", Value: "3"} |
41 | | - cmdPos := uci.CmdPosition{Position: pos} |
42 | | - cmdGo := uci.CmdGo{MoveTime: time.Second / 10} |
43 | | - if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, uci.CmdUCINewGame, cmdMultiPV, cmdPos, cmdGo); err != nil { |
44 | | - t.Fatal("failed to run command", err) |
45 | | - } |
46 | | - |
47 | | - move := eng.SearchResults().Info.PV[0] |
48 | | - moveStr := chess.AlgebraicNotation{}.Encode(pos, move) |
49 | | - |
50 | | - if moveStr != "Bg8" { |
51 | | - t.Errorf("expected Bg8, got %s", moveStr) |
| 23 | + for _, name := range engines { |
| 24 | + fenStr := "r1bq1rk1/ppp2ppp/2n2n2/3pp3/3P4/2P1PN2/PP1N1PPP/R1BQ1RK1 w - - 0 8" |
| 25 | + |
| 26 | + t.Run("EngineInfo_"+name, func(t *testing.T) { |
| 27 | + if !isEngineAvailable(name) { |
| 28 | + t.Skipf("engine %s not available", name) |
| 29 | + } |
| 30 | + |
| 31 | + pos := &chess.Position{} |
| 32 | + if err := pos.UnmarshalText([]byte(fenStr)); err != nil { |
| 33 | + t.Fatal("failed to parse FEN", err) |
| 34 | + } |
| 35 | + |
| 36 | + eng, err := uci.New(name, uci.Debug) |
| 37 | + if err != nil { |
| 38 | + t.Fatal(err) |
| 39 | + } |
| 40 | + defer eng.Close() |
| 41 | + |
| 42 | + cmdMultiPV := uci.CmdSetOption{Name: "multipv", Value: "2"} |
| 43 | + cmdPos := uci.CmdPosition{Position: pos} |
| 44 | + cmdGo := uci.CmdGo{MoveTime: time.Second / 10} |
| 45 | + if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, uci.CmdUCINewGame, cmdMultiPV, cmdPos, cmdGo); err != nil { |
| 46 | + t.Fatal("failed to run command", err) |
| 47 | + } |
| 48 | + |
| 49 | + move := eng.SearchResults().Info.PV[0] |
| 50 | + moveStr := chess.AlgebraicNotation{}.Encode(pos, move) |
| 51 | + |
| 52 | + if moveStr != "Ne5" { |
| 53 | + t.Errorf("expected Ne5, got %s", moveStr) |
| 54 | + } |
| 55 | + }) |
52 | 56 | } |
53 | 57 | } |
54 | 58 |
|
55 | 59 | func Test_EngineMultiPVInfo(t *testing.T) { |
56 | | - t.SkipNow() |
57 | | - fenStr := "2n1k1r1/8/4B3/3p4/3P4/8/K7/N7 w - - 0 1" |
58 | | - pos := &chess.Position{} |
59 | | - if err := pos.UnmarshalText([]byte(fenStr)); err != nil { |
60 | | - t.Fatal("failed to parse FEN", err) |
61 | | - } |
62 | | - |
63 | | - eng, err := uci.New(StockfishPath, uci.Debug) |
64 | | - if err != nil { |
65 | | - t.Fatal(err) |
66 | | - } |
67 | | - defer eng.Close() |
68 | | - |
69 | | - cmdMultiPV := uci.CmdSetOption{Name: "multipv", Value: "3"} |
70 | | - cmdPos := uci.CmdPosition{Position: pos} |
71 | | - cmdGo := uci.CmdGo{MoveTime: time.Second / 10} |
72 | | - if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, uci.CmdUCINewGame, cmdMultiPV, cmdPos, cmdGo); err != nil { |
73 | | - t.Fatal("failed to run command", err) |
74 | | - } |
75 | | - |
76 | | - multiPVInfo := eng.SearchResults().MultiPVInfo |
77 | | - |
78 | | - if len(multiPVInfo) != 3 { |
79 | | - t.Errorf("expected 3 MultiPV lines, got %d", len(multiPVInfo)) |
80 | | - } |
81 | | - |
82 | | - move := multiPVInfo[0].PV[0] |
83 | | - moveStr := chess.AlgebraicNotation{}.Encode(pos, move) |
84 | | - if moveStr != "Bg8" { |
85 | | - t.Errorf("expected Bg8, got %s", moveStr) |
86 | | - } |
87 | | - |
88 | | - move = multiPVInfo[1].PV[0] |
89 | | - moveStr = chess.AlgebraicNotation{}.Encode(pos, move) |
90 | | - if moveStr != "Bc8" { |
91 | | - t.Errorf("expected Bc8, got %s", moveStr) |
92 | | - } |
93 | | - |
94 | | - move = multiPVInfo[2].PV[0] |
95 | | - moveStr = chess.AlgebraicNotation{}.Encode(pos, move) |
96 | | - if moveStr != "Bd5" { |
97 | | - t.Errorf("expected Bd5, got %s", moveStr) |
| 60 | + for _, name := range engines { |
| 61 | + fenStr := "r1bq1rk1/ppp2ppp/2n2n2/3pp3/3P4/2P1PN2/PP1N1PPP/R1BQ1RK1 w - - 0 8" |
| 62 | + |
| 63 | + t.Run("EngineMultiPVInfo_"+name, func(t *testing.T) { |
| 64 | + if !isEngineAvailable(name) { |
| 65 | + t.Skipf("engine %s not available", name) |
| 66 | + } |
| 67 | + |
| 68 | + pos := &chess.Position{} |
| 69 | + if err := pos.UnmarshalText([]byte(fenStr)); err != nil { |
| 70 | + t.Fatal("failed to parse FEN", err) |
| 71 | + } |
| 72 | + |
| 73 | + eng, err := uci.New(name, uci.Debug) |
| 74 | + if err != nil { |
| 75 | + t.Fatal(err) |
| 76 | + } |
| 77 | + defer eng.Close() |
| 78 | + |
| 79 | + cmdMultiPV := uci.CmdSetOption{Name: "multipv", Value: "2"} |
| 80 | + cmdPos := uci.CmdPosition{Position: pos} |
| 81 | + cmdGo := uci.CmdGo{MoveTime: time.Second / 10} |
| 82 | + if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, uci.CmdUCINewGame, cmdMultiPV, cmdPos, cmdGo); err != nil { |
| 83 | + t.Fatal("failed to run command", err) |
| 84 | + } |
| 85 | + |
| 86 | + multiPVInfo := eng.SearchResults().MultiPVInfo |
| 87 | + |
| 88 | + if len(multiPVInfo) != 2 { |
| 89 | + t.Errorf("expected 2 MultiPV lines, got %d", len(multiPVInfo)) |
| 90 | + } |
| 91 | + |
| 92 | + move := multiPVInfo[0].PV[0] |
| 93 | + moveStr := chess.AlgebraicNotation{}.Encode(pos, move) |
| 94 | + if moveStr != "Ne5" { |
| 95 | + t.Errorf("expected Ne5, got %s", moveStr) |
| 96 | + } |
| 97 | + |
| 98 | + move = multiPVInfo[1].PV[0] |
| 99 | + moveStr = chess.AlgebraicNotation{}.Encode(pos, move) |
| 100 | + if moveStr != "e5" { |
| 101 | + t.Errorf("expected e5, got %s", moveStr) |
| 102 | + } |
| 103 | + }) |
98 | 104 | } |
99 | 105 | } |
100 | 106 |
|
101 | 107 | func Test_UCIMovesTags(t *testing.T) { |
102 | | - t.SkipNow() |
103 | | - eng, err := uci.New(StockfishPath, uci.Debug) |
104 | | - if err != nil { |
105 | | - t.Fatal(err) |
106 | | - } |
107 | | - defer eng.Close() |
108 | | - setOpt := uci.CmdSetOption{Name: "UCI_Elo", Value: "1500"} |
109 | | - setPos := uci.CmdPosition{Position: chess.StartingPosition()} |
110 | | - setGo := uci.CmdGo{MoveTime: time.Second / 10} |
111 | | - if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, setOpt, uci.CmdUCINewGame, setPos, setGo); err != nil { |
112 | | - t.Fatal("failed to run command", err) |
113 | | - } |
114 | | - |
115 | | - game := chess.NewGame() |
116 | | - notation := chess.AlgebraicNotation{} |
117 | | - |
118 | | - for game.Outcome() == chess.NoOutcome { |
119 | | - cmdPos := uci.CmdPosition{Position: game.Position()} |
120 | | - cmdGo := uci.CmdGo{MoveTime: time.Second / 100} |
121 | | - if err2 := eng.Run(cmdPos, cmdGo); err2 != nil { |
122 | | - t.Fatal("failed to run command", err2) |
123 | | - } |
124 | | - |
125 | | - move := eng.SearchResults().BestMove |
126 | | - pos := game.Position() |
127 | | - san := notation.Encode(pos, move) |
128 | | - |
129 | | - err = game.PushMove(san, nil) |
130 | | - if err != nil { |
131 | | - t.Fatal(fmt.Sprintf("failed to push move %s - %s - %v. Pos: %s", san, move.String(), move.HasTag(chess.Capture), pos.String()), err) |
132 | | - } |
| 108 | + for _, name := range engines { |
| 109 | + t.Run("UCIMovesTags_"+name, func(t *testing.T) { |
| 110 | + if !isEngineAvailable(name) { |
| 111 | + t.Skipf("engine %s not available", name) |
| 112 | + } |
| 113 | + |
| 114 | + eng, err := uci.New(name, uci.Debug) |
| 115 | + if err != nil { |
| 116 | + t.Fatal(err) |
| 117 | + } |
| 118 | + defer eng.Close() |
| 119 | + setOpt := uci.CmdSetOption{Name: "UCI_Elo", Value: "1500"} |
| 120 | + setPos := uci.CmdPosition{Position: chess.StartingPosition()} |
| 121 | + setGo := uci.CmdGo{MoveTime: time.Second / 10} |
| 122 | + if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, setOpt, uci.CmdUCINewGame, setPos, setGo); err != nil { |
| 123 | + t.Fatal("failed to run command", err) |
| 124 | + } |
| 125 | + |
| 126 | + game := chess.NewGame() |
| 127 | + notation := chess.AlgebraicNotation{} |
| 128 | + |
| 129 | + for game.Outcome() == chess.NoOutcome { |
| 130 | + cmdPos := uci.CmdPosition{Position: game.Position()} |
| 131 | + cmdGo := uci.CmdGo{MoveTime: time.Second / 100} |
| 132 | + if err2 := eng.Run(cmdPos, cmdGo); err2 != nil { |
| 133 | + t.Fatal("failed to run command", err2) |
| 134 | + } |
| 135 | + |
| 136 | + move := eng.SearchResults().BestMove |
| 137 | + pos := game.Position() |
| 138 | + san := notation.Encode(pos, move) |
| 139 | + |
| 140 | + err = game.PushMove(san, nil) |
| 141 | + if err != nil { |
| 142 | + t.Fatal(fmt.Sprintf("failed to push move %s - %s - %v. Pos: %s", san, move.String(), move.HasTag(chess.Capture), pos.String()), err) |
| 143 | + } |
| 144 | + } |
| 145 | + }) |
133 | 146 | } |
134 | 147 | } |
135 | | - |
136 | | -func TestLogger(t *testing.T) { |
137 | | - t.SkipNow() |
138 | | - |
139 | | - b := bytes.NewBuffer([]byte{}) |
140 | | - logger := log.New(b, "", 0) |
141 | | - eng, err := uci.New(StockfishPath, uci.Debug, uci.Logger(logger)) |
142 | | - if err != nil { |
143 | | - t.Fatal(err) |
144 | | - } |
145 | | - defer eng.Close() |
146 | | - setOpt := uci.CmdSetOption{Name: "UCI_Elo", Value: "1500"} |
147 | | - setPos := uci.CmdPosition{Position: chess.StartingPosition()} |
148 | | - setGo := uci.CmdGo{MoveTime: time.Second / 10} |
149 | | - if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, setOpt, uci.CmdUCINewGame, setPos, setGo); err != nil { |
150 | | - t.Fatal(err) |
151 | | - } |
152 | | - expected := infoRegex.ReplaceAllString(logOutput, "") |
153 | | - expectedSplit := strings.Split(expected, "\n") |
154 | | - actual := b.String() |
155 | | - actual = infoRegex.ReplaceAllString(actual, "") |
156 | | - actualSplit := strings.Split(actual, "\n") |
157 | | - |
158 | | - for i, expectedSplitted := range expectedSplit { |
159 | | - expectedSplitted = strings.TrimSpace(expectedSplitted) |
160 | | - actual := strings.TrimSpace(actualSplit[i]) |
161 | | - if expectedSplitted != actual { |
162 | | - t.Fatalf("expected %s but got %s", expectedSplitted, actual) |
163 | | - } |
164 | | - } |
165 | | -} |
166 | | - |
167 | | -var infoRegex = regexp.MustCompile("(?m)[\r\n]+^.*info.*$") |
168 | | - |
169 | | -const ( |
170 | | - logOutput = `uci |
171 | | -Stockfish 14.1 by the Stockfish developers (see AUTHORS file) |
172 | | -id name Stockfish 14.1 |
173 | | -id author the Stockfish developers (see AUTHORS file) |
174 | | - |
175 | | -option name Debug Log File type string default |
176 | | -option name Threads type spin default 1 min 1 max 512 |
177 | | -option name Hash type spin default 16 min 1 max 33554432 |
178 | | -option name Clear Hash type button |
179 | | -option name Ponder type check default false |
180 | | -option name MultiPV type spin default 1 min 1 max 500 |
181 | | -option name Skill Level type spin default 20 min 0 max 20 |
182 | | -option name Move Overhead type spin default 10 min 0 max 5000 |
183 | | -option name Slow Mover type spin default 100 min 10 max 1000 |
184 | | -option name nodestime type spin default 0 min 0 max 10000 |
185 | | -option name UCI_Chess960 type check default false |
186 | | -option name UCI_AnalyseMode type check default false |
187 | | -option name UCI_LimitStrength type check default false |
188 | | -option name UCI_Elo type spin default 1350 min 1350 max 2850 |
189 | | -option name UCI_ShowWDL type check default false |
190 | | -option name SyzygyPath type string default <empty> |
191 | | -option name SyzygyProbeDepth type spin default 1 min 1 max 100 |
192 | | -option name Syzygy50MoveRule type check default true |
193 | | -option name SyzygyProbeLimit type spin default 7 min 0 max 7 |
194 | | -option name Use NNUE type check default true |
195 | | -option name EvalFile type string default nn-13406b1dcbe0.nnue |
196 | | -uciok |
197 | | -isready |
198 | | -readyok |
199 | | -setoption name UCI_Elo value 1500 |
200 | | -ucinewgame |
201 | | -position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 |
202 | | -go movetime 100 |
203 | | -info string NNUE evaluation using nn-13406b1dcbe0.nnue enabled |
204 | | -info depth 1 seldepth 1 multipv 1 score cp 38 nodes 20 nps 20000 tbhits 0 time 1 pv d2d4 |
205 | | -info depth 2 seldepth 2 multipv 1 score cp 82 nodes 51 nps 51000 tbhits 0 time 1 pv e2e4 a7a6 |
206 | | -info depth 3 seldepth 3 multipv 1 score cp 55 nodes 154 nps 154000 tbhits 0 time 1 pv e2e4 c7c6 d2d4 |
207 | | -info depth 4 seldepth 4 multipv 1 score cp 22 nodes 807 nps 269000 tbhits 0 time 3 pv g1f3 d7d5 d2d4 g8f6 |
208 | | -info depth 5 seldepth 5 multipv 1 score cp 54 nodes 1061 nps 353666 tbhits 0 time 3 pv e2e4 c7c5 g1f3 |
209 | | -info depth 6 seldepth 6 multipv 1 score cp 54 nodes 1761 nps 440250 tbhits 0 time 4 pv e2e4 c7c5 g1f3 d7d5 e4d5 d8d5 |
210 | | -info depth 7 seldepth 8 multipv 1 score cp 50 nodes 5459 nps 545900 tbhits 0 time 10 pv e2e4 e7e5 g1f3 b8c6 b1c3 g8f6 d2d4 |
211 | | -info depth 8 seldepth 8 multipv 1 score cp 50 nodes 6998 nps 583166 tbhits 0 time 12 pv e2e4 e7e5 g1f3 b8c6 d2d4 e5d4 f3d4 g8f6 b1c3 |
212 | | -info depth 9 seldepth 11 multipv 1 score cp 54 nodes 12053 nps 573952 tbhits 0 time 21 pv e2e4 e7e5 g1f3 b8c6 d2d4 e5d4 f3d4 g8f6 d4c6 |
213 | | -info depth 10 seldepth 13 multipv 1 score cp 35 nodes 28785 nps 564411 tbhits 0 time 51 pv e2e4 e7e6 d2d4 d7d5 e4d5 e6d5 g1f3 f8d6 f1d3 g8f6 e1g1 e8g8 |
214 | | -info depth 11 seldepth 14 multipv 1 score cp 50 nodes 34551 nps 575850 tbhits 0 time 60 pv e2e4 e7e5 g1f3 b8c6 d2d4 e5d4 f3d4 g8f6 b1c3 f8b4 |
215 | | -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 |
216 | | -bestmove e2e4 ponder c7c5` |
217 | | -) |
0 commit comments