github.com/frankkopp/FrankyGo@v1.0.3/internal/uci/uci.go (about) 1 // 2 // FrankyGo - UCI chess engine in GO for learning purposes 3 // 4 // MIT License 5 // 6 // Copyright (c) 2018-2020 Frank Kopp 7 // 8 // Permission is hereby granted, free of charge, to any person obtaining a copy 9 // of this software and associated documentation files (the "Software"), to deal 10 // in the Software without restriction, including without limitation the rights 11 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 // copies of the Software, and to permit persons to whom the Software is 13 // furnished to do so, subject to the following conditions: 14 // 15 // The above copyright notice and this permission notice shall be included in all 16 // copies or substantial portions of the Software. 17 // 18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 // SOFTWARE. 25 // 26 27 // Package uci contains the UciHandler data structure and functionality to 28 // handle the UCI protocol communication between the Chess User Interface 29 // and the chess engine. 30 package uci 31 32 import ( 33 "bufio" 34 "bytes" 35 "fmt" 36 golog "log" 37 "os" 38 "path/filepath" 39 "regexp" 40 "strconv" 41 "strings" 42 "time" 43 44 "github.com/op/go-logging" 45 "golang.org/x/text/language" 46 "golang.org/x/text/message" 47 48 "github.com/frankkopp/FrankyGo/internal/config" 49 myLogging "github.com/frankkopp/FrankyGo/internal/logging" 50 "github.com/frankkopp/FrankyGo/internal/movegen" 51 "github.com/frankkopp/FrankyGo/internal/moveslice" 52 "github.com/frankkopp/FrankyGo/internal/position" 53 "github.com/frankkopp/FrankyGo/internal/search" 54 . "github.com/frankkopp/FrankyGo/internal/types" 55 "github.com/frankkopp/FrankyGo/internal/uciInterface" 56 "github.com/frankkopp/FrankyGo/internal/util" 57 "github.com/frankkopp/FrankyGo/internal/version" 58 ) 59 60 var out = message.NewPrinter(language.German) 61 var log *logging.Logger 62 63 // UciHandler handles all communication with the chess ui via UCI 64 // and controls options and search. 65 // Create an instance with NewUciHandler() 66 type UciHandler struct { 67 InIo *bufio.Scanner 68 OutIo *bufio.Writer 69 myMoveGen *movegen.Movegen 70 mySearch *search.Search 71 myPosition *position.Position 72 myPerft *movegen.Perft 73 uciLog *logging.Logger 74 } 75 76 // /////////////////////////////////////////////////////////// 77 // Public 78 // /////////////////////////////////////////////////////////// 79 80 // NewUciHandler creates a new UciHandler instance. 81 // Input / Output io can be replaced by changing the instance's 82 // InIo and OutIo members. 83 // Example: 84 // u.InIo = bufio.NewScanner(os.Stdin) 85 // u.OutIo = bufio.NewWriter(os.Stdout) 86 func NewUciHandler() *UciHandler { 87 if log == nil { 88 log = myLogging.GetLog() 89 } 90 u := &UciHandler{ 91 InIo: bufio.NewScanner(os.Stdin), 92 OutIo: bufio.NewWriter(os.Stdout), 93 myMoveGen: movegen.NewMoveGen(), 94 mySearch: search.NewSearch(), 95 myPosition: position.NewPosition(), 96 myPerft: movegen.NewPerft(), 97 uciLog: getUciLog(), 98 } 99 var uciDriver uciInterface.UciDriver 100 uciDriver = u 101 u.mySearch.SetUciHandler(uciDriver) 102 return u 103 } 104 105 // Loop starts the main loop to receive commands through 106 // input stream (pipe or user) 107 func (u *UciHandler) Loop() { 108 u.loop() 109 } 110 111 // Command handles a single line of UCI protocol aka command. 112 // Returns the uci response as string output. 113 // Mostly useful for debugging and unit testing. 114 func (u *UciHandler) Command(cmd string) string { 115 tmp := u.OutIo 116 buffer := new(bytes.Buffer) 117 u.OutIo = bufio.NewWriter(buffer) 118 u.handleReceivedCommand(cmd) 119 _ = u.OutIo.Flush() 120 u.OutIo = tmp 121 return buffer.String() 122 } 123 124 // SendReadyOk tells the UciDriver to send the uci response "readyok" to the UCI user interface 125 func (u *UciHandler) SendReadyOk() { 126 u.send("readyok") 127 } 128 129 // SendInfoString send a arbitrary string to the UCI user interface 130 func (u *UciHandler) SendInfoString(info string) { 131 u.send(out.Sprintf("info string %s", info)) 132 } 133 134 // SendIterationEndInfo sends information about the last search depth iteration to the UCI ui 135 func (u *UciHandler) SendIterationEndInfo(depth int, seldepth int, value Value, nodes uint64, nps uint64, time time.Duration, pv moveslice.MoveSlice) { 136 u.send(fmt.Sprintf("info depth %d seldepth %d multipv 1 score %s nodes %d nps %d time %d pv %s", 137 depth, seldepth, value.String(), nodes, nps, time.Milliseconds(), pv.StringUci())) 138 } 139 140 // SendSearchUpdate sends a periodically update about search stats to the UCI ui 141 func (u *UciHandler) SendSearchUpdate(depth int, seldepth int, nodes uint64, nps uint64, time time.Duration, hashfull int) { 142 u.send(fmt.Sprintf("info depth %d seldepth %d nodes %d nps %d time %d hashfull %d", 143 depth, seldepth, nodes, nps, time.Milliseconds(), hashfull)) 144 } 145 146 // SendAspirationResearchInfo sends information about Aspiration researches to the UCI ui 147 func (u *UciHandler) SendAspirationResearchInfo(depth int, seldepth int, value Value, bound string, nodes uint64, nps uint64, time time.Duration, pv moveslice.MoveSlice) { 148 u.send(fmt.Sprintf("info depth %d seldepth %d %s multipv 1 score %s nodes %d nps %d time %d pv %s", 149 depth, seldepth, value.String(), bound, nodes, nps, time.Milliseconds(), pv.StringUci())) 150 } 151 152 // SendCurrentRootMove sends the currently searched root move to the UCI ui 153 func (u *UciHandler) SendCurrentRootMove(currMove Move, moveNumber int) { 154 u.send(fmt.Sprintf("info currmove %s currmovenumber %d", currMove.StringUci(), moveNumber)) 155 } 156 157 // SendCurrentLine sends a periodically update about the currently searched variation ti the UCI ui 158 func (u *UciHandler) SendCurrentLine(moveList moveslice.MoveSlice) { 159 u.send(fmt.Sprintf("info currline %s", moveList.StringUci())) 160 } 161 162 // SendResult send the search result to the UCI ui after the search has ended are has been stopped 163 func (u *UciHandler) SendResult(bestMove Move, ponderMove Move) { 164 var resultStr strings.Builder 165 resultStr.WriteString("bestmove ") 166 resultStr.WriteString(bestMove.StringUci()) 167 if ponderMove != MoveNone { 168 resultStr.WriteString(" ponder ") 169 resultStr.WriteString(ponderMove.StringUci()) 170 } 171 u.send(resultStr.String()) 172 } 173 174 // /////////////////////////////////////////////////////////// 175 // Private 176 // /////////////////////////////////////////////////////////// 177 178 func (u *UciHandler) loop() { 179 // infinite loop until "quit" command is received 180 for { 181 log.Debugf("Waiting for command:") 182 // read from stdin or other in stream 183 for u.InIo.Scan() { 184 if u.handleReceivedCommand(u.InIo.Text()) { 185 // quit command received 186 return 187 } 188 log.Debugf("Waiting for command:") 189 } 190 } 191 } 192 193 var regexWhiteSpace = regexp.MustCompile("\\s+") 194 195 func (u *UciHandler) handleReceivedCommand(cmd string) bool { 196 if len(cmd) == 0 { 197 return false 198 } 199 log.Debugf("Received command: %s", cmd) 200 u.uciLog.Infof("<< %s", cmd) 201 // find command and execute by calling command function 202 tokens := regexWhiteSpace.Split(cmd, -1) 203 strings.TrimSpace(tokens[0]) 204 switch tokens[0] { 205 case "quit": 206 return true 207 case "uci": 208 u.uciCommand() 209 case "setoption": 210 u.setOptionCommand(tokens) 211 case "isready": 212 u.isReadyCommand() 213 case "ucinewgame": 214 u.uciNewGameCommand() 215 case "position": 216 u.positionCommand(tokens) 217 case "go": 218 u.goCommand(tokens) 219 case "stop": 220 u.stopCommand() 221 case "ponderhit": 222 u.ponderHitCommand() 223 case "register": 224 u.registerCommand() 225 case "debug": 226 u.debugCommand() 227 case "perft": 228 u.perftCommand(tokens) 229 case "noop": 230 default: 231 log.Warningf("Error: Unknown command: %s", cmd) 232 } 233 log.Debugf("Processed command: %s", cmd) 234 return false 235 } 236 237 // command handler when the "uci" cmd has been received. 238 // Responds with "id" and "options" 239 func (u *UciHandler) uciCommand() { 240 u.send("id name FrankyGo " + version.Version()) 241 u.send("id author Frank Kopp, Germany") 242 options := uciOptions.GetOptions() 243 for _, o := range *options { 244 u.send(o) 245 } 246 u.send("uciok") 247 } 248 249 // the set option command reads the option name and the optional value 250 // and checks if the uci option exists. If it does its new value will 251 // be stored and its handler function will be called 252 func (u *UciHandler) setOptionCommand(tokens []string) { 253 name := "" 254 value := "" 255 if len(tokens) > 1 && tokens[1] == "name" { 256 i := 2 257 for i < len(tokens) && tokens[i] != "value" { 258 name += tokens[i] + " " 259 i++ 260 } 261 name = strings.TrimSpace(name) 262 if len(tokens) > i && tokens[i] == "value" && len(tokens) > i+1 { 263 value += tokens[i+1] 264 } 265 } else { 266 msg := "Command 'setoption' is malformed" 267 u.SendInfoString(msg) 268 log.Warning(msg) 269 return 270 } 271 o, found := uciOptions[name] 272 if found { 273 o.CurrentValue = value 274 o.HandlerFunc(u, o) 275 } else { 276 msg := out.Sprintf("Command 'setoption': No such option '%s'", name) 277 u.SendInfoString(msg) 278 log.Warning(msg) 279 return 280 } 281 } 282 283 // requests the isready status from the Search which in turn might 284 // initialize itself 285 func (u *UciHandler) isReadyCommand() { 286 u.mySearch.IsReady() 287 } 288 289 // ponderhit signals that the move which was suggested as ponder move 290 // has been made by the opponent. 291 func (u *UciHandler) ponderHitCommand() { 292 u.mySearch.PonderHit() 293 } 294 295 // sends a stop signal to search or perft 296 func (u *UciHandler) stopCommand() { 297 u.mySearch.StopSearch() 298 u.myPerft.Stop() 299 } 300 301 // starts a perft test with the given depth 302 func (u *UciHandler) perftCommand(tokens []string) { 303 depth := 4 // default 304 var err error = nil 305 if len(tokens) > 1 { 306 depth, err = strconv.Atoi(tokens[1]) 307 if err != nil { 308 log.Warningf("Can't perft on depth='%s'", tokens[1]) 309 } 310 } 311 depth2 := depth 312 if len(tokens) > 2 { 313 tmp, err := strconv.Atoi(tokens[2]) 314 if err != nil { 315 log.Warningf("Can't use second perft depth2='%s'", tokens[2]) 316 } else { 317 depth2 = tmp 318 } 319 } 320 go u.myPerft.StartPerftMulti(position.StartFen, depth, depth2, true) 321 } 322 323 // starts a search after reading in the search limits provided 324 func (u *UciHandler) goCommand(tokens []string) { 325 searchLimits, err := u.readSearchLimits(tokens) 326 if err { 327 return 328 } 329 // start the search 330 u.mySearch.StartSearch(*u.myPosition, *searchLimits) 331 } 332 333 // sets the current position as given by the uci command 334 func (u *UciHandler) positionCommand(tokens []string) { 335 // build initial position 336 fen := position.StartFen 337 i := 1 338 switch tokens[i] { 339 case "startpos": 340 i++ 341 case "fen": 342 i++ 343 var fenb strings.Builder 344 for i < len(tokens) && tokens[i] != "moves" { 345 fenb.WriteString(tokens[i]) 346 fenb.WriteString(" ") 347 i++ 348 } 349 fen = strings.TrimSpace(fenb.String()) 350 if len(fen) > 0 { 351 break 352 } 353 // fen empty fall through to err msg 354 fallthrough 355 default: 356 msg := out.Sprintf("Command 'position' malformed. %s", tokens) 357 u.SendInfoString(msg) 358 log.Warning(msg) 359 return 360 } 361 u.myPosition, _ = position.NewPositionFen(fen) 362 363 // check for moves to make 364 if i < len(tokens) { 365 if tokens[i] == "moves" { 366 i++ 367 for i < len(tokens) && tokens[i] != "moves" { 368 move := u.myMoveGen.GetMoveFromUci(u.myPosition, tokens[i]) 369 if move.IsValid() { 370 u.myPosition.DoMove(move) 371 } else { 372 msg := out.Sprintf("Command 'position' malformed. Invalid move '%s' (%s)", tokens[i], tokens) 373 u.SendInfoString(msg) 374 log.Warning(msg) 375 return 376 } 377 i++ 378 } 379 } else { 380 msg := out.Sprintf("Command 'position' malformed moves. %s", tokens) 381 u.SendInfoString(msg) 382 log.Warning(msg) 383 return 384 } 385 } 386 log.Debugf("New position: %s", u.myPosition.StringFen()) 387 } 388 389 // Signals the search to stop a running search and that a new game should 390 // be started. Usually this means resetting all search related data e.g. 391 // hash tables etc. 392 func (u *UciHandler) uciNewGameCommand() { 393 u.myPosition = position.NewPosition() 394 u.mySearch.NewGame() 395 } 396 397 // will not be implemented 398 func (u *UciHandler) debugCommand() { 399 msg := "Command 'debug' not implemented" 400 u.SendInfoString(msg) 401 log.Warning(msg) 402 } 403 404 // will not be implemented 405 func (u *UciHandler) registerCommand() { 406 msg := "Command 'register' not implemented" 407 u.SendInfoString(msg) 408 log.Warning(msg) 409 } 410 411 func (u *UciHandler) readSearchLimits(tokens []string) (*search.Limits, bool) { 412 searchLimits := search.NewSearchLimits() 413 i := 1 414 for i < len(tokens) { 415 var err error = nil 416 switch tokens[i] { 417 case "moves": 418 i++ 419 for i < len(tokens) { 420 move := u.myMoveGen.GetMoveFromUci(u.myPosition, tokens[i]) 421 if move.IsValid() { 422 searchLimits.Moves.PushBack(move) 423 i++ 424 } else { 425 break 426 } 427 } 428 case "infinite": 429 i++ 430 searchLimits.Infinite = true 431 case "ponder": 432 i++ 433 searchLimits.Ponder = true 434 case "depth": 435 i++ 436 searchLimits.Depth, err = strconv.Atoi(tokens[i]) 437 if err != nil { 438 msg := out.Sprintf("UCI command go malformed. Depth value not an number: %s", tokens[i]) 439 u.SendInfoString(msg) 440 log.Warning(msg) 441 return nil, true 442 } 443 i++ 444 case "nodes": 445 i++ 446 parseInt, err := strconv.ParseInt(tokens[i], 10, 64) 447 if err != nil { 448 msg := out.Sprintf("UCI command go malformed. Nodes value not an number: %s", tokens[i]) 449 u.SendInfoString(msg) 450 log.Warning(msg) 451 return nil, true 452 } 453 searchLimits.Nodes = uint64(parseInt) 454 i++ 455 case "mate": 456 i++ 457 searchLimits.Mate, err = strconv.Atoi(tokens[i]) 458 if err != nil { 459 msg := out.Sprintf("UCI command go malformed. Mate value not an number: %s", tokens[i]) 460 u.SendInfoString(msg) 461 log.Warning(msg) 462 return nil, true 463 } 464 i++ 465 case "movetime": 466 // UCI wants moveTime but STS test suite uses movetime - this catches this 467 fallthrough 468 case "moveTime": 469 i++ 470 parseInt, err := strconv.ParseInt(tokens[i], 10, 64) 471 if err != nil { 472 msg := out.Sprintf("UCI command go malformed. MoveTime value not an number: %s", tokens[i]) 473 u.SendInfoString(msg) 474 log.Warning(msg) 475 return nil, true 476 } 477 searchLimits.MoveTime = time.Duration(parseInt * 1_000_000) 478 searchLimits.TimeControl = true 479 i++ 480 case "wtime": 481 i++ 482 parseInt, err := strconv.ParseInt(tokens[i], 10, 64) 483 if err != nil { 484 msg := out.Sprintf("UCI command go malformed. WhiteTime value not an number: %s", tokens[i]) 485 u.SendInfoString(msg) 486 log.Warning(msg) 487 return nil, true 488 } 489 searchLimits.WhiteTime = time.Duration(parseInt * 1_000_000) 490 searchLimits.TimeControl = true 491 i++ 492 case "btime": 493 i++ 494 parseInt, err := strconv.ParseInt(tokens[i], 10, 64) 495 if err != nil { 496 msg := out.Sprintf("UCI command go malformed. Black value not an number: %s", tokens[i]) 497 u.SendInfoString(msg) 498 log.Warning(msg) 499 return nil, true 500 } 501 searchLimits.BlackTime = time.Duration(parseInt * 1_000_000) 502 searchLimits.TimeControl = true 503 i++ 504 case "winc": 505 i++ 506 parseInt, err := strconv.ParseInt(tokens[i], 10, 64) 507 if err != nil { 508 msg := out.Sprintf("UCI command go malformed. WhiteInc value not an number: %s", tokens[i]) 509 u.SendInfoString(msg) 510 log.Warning(msg) 511 return nil, true 512 } 513 searchLimits.WhiteInc = time.Duration(parseInt * 1_000_000) 514 i++ 515 case "binc": 516 i++ 517 parseInt, err := strconv.ParseInt(tokens[i], 10, 64) 518 if err != nil { 519 msg := out.Sprintf("UCI command go malformed. BlackInc value not an number: %s", tokens[i]) 520 u.SendInfoString(msg) 521 log.Warning(msg) 522 return nil, true 523 } 524 searchLimits.BlackInc = time.Duration(parseInt * 1_000_000) 525 i++ 526 case "movestogo": 527 i++ 528 searchLimits.MovesToGo, err = strconv.Atoi(tokens[i]) 529 if err != nil { 530 msg := out.Sprintf("UCI command go malformed. Movestogo value not an number: %s", tokens[i]) 531 u.SendInfoString(msg) 532 log.Warning(msg) 533 return nil, true 534 } 535 i++ 536 default: 537 msg := out.Sprintf("UCI command go malformed. Invalid subcommand: %s", tokens[i]) 538 u.SendInfoString(msg) 539 log.Warning(msg) 540 return nil, true 541 } 542 } 543 // sanity check / minimum settings 544 if !(searchLimits.Infinite || 545 searchLimits.Ponder || 546 searchLimits.Depth > 0 || 547 searchLimits.Nodes > 0 || 548 searchLimits.Mate > 0 || 549 searchLimits.TimeControl) { 550 551 msg := out.Sprintf("UCI command go malformed. No effective limits set %s", tokens) 552 u.SendInfoString(msg) 553 log.Warning(msg) 554 return nil, true 555 } 556 // sanity check time control 557 if searchLimits.TimeControl && searchLimits.MoveTime == 0 { 558 if u.myPosition.NextPlayer() == White && searchLimits.WhiteTime == 0 { 559 msg := out.Sprintf("UCI command go invalid. White to move but time for white is zero! %s", tokens) 560 u.SendInfoString(msg) 561 log.Warning(msg) 562 return nil, true 563 } else if u.myPosition.NextPlayer() == Black && searchLimits.BlackTime == 0 { 564 msg := out.Sprintf("UCI command go invalid. Black to move but time for white is zero! %s", tokens) 565 u.SendInfoString(msg) 566 log.Warning(msg) 567 return nil, true 568 } 569 } 570 return searchLimits, false 571 } 572 573 // getUciLog returns an instance of a special Logger preconfigured for 574 // myLogging all UCI protocol communication to os.Stdout or file 575 // Format is very simple "time UCI <uci command>" 576 func getUciLog() *logging.Logger { 577 // create logger 578 uciLog := logging.MustGetLogger("UCI ") 579 580 // Stdout backend 581 uciFormat := logging.MustStringFormatter(`%{time:15:04:05.000} UCI %{message}`) 582 backend1 := logging.NewLogBackend(os.Stdout, "", golog.Lmsgprefix) 583 backend1Formatter := logging.NewBackendFormatter(backend1, uciFormat) 584 uciBackEnd1 := logging.AddModuleLevel(backend1Formatter) 585 uciBackEnd1.SetLevel(logging.DEBUG, "") 586 uciLog.SetBackend(uciBackEnd1) 587 588 // File backend 589 programName, _ := os.Executable() 590 exeName := strings.TrimSuffix(filepath.Base(programName), ".exe") 591 592 // find log path 593 logPath, err := util.ResolveFolder(config.Settings.Log.LogPath) 594 if err != nil { 595 golog.Println("Log folder could not be found:", err) 596 return uciLog 597 } 598 logFilePath := filepath.Join(logPath, exeName+"_uci.log") 599 600 // create file backend 601 uciLogFile, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 602 if err != nil { 603 golog.Println("Logfile could not be created:", err) 604 return uciLog 605 } 606 backend2 := logging.NewLogBackend(uciLogFile, "", golog.Lmsgprefix) 607 backend2Formatter := logging.NewBackendFormatter(backend2, uciFormat) 608 uciBackEnd2 := logging.AddModuleLevel(backend2Formatter) 609 uciBackEnd2.SetLevel(logging.DEBUG, "") 610 // multi := logging2.SetBackend(uciBackEnd1, uciBackEnd2) 611 uciLog.SetBackend(uciBackEnd2) 612 uciLog.Infof("Log %s started at %s:", uciLogFile.Name(), time.Now().String()) 613 return uciLog 614 } 615 616 // sends any string to the UCI user interface 617 func (u *UciHandler) send(s string) { 618 u.uciLog.Infof(">> %s", s) 619 _, _ = u.OutIo.WriteString(s + "\n") 620 _ = u.OutIo.Flush() 621 }