gostockfish

executable exits early when using io.WriteString


I'm using the io package to work with an executable defined in my PATH. The executable is called "Stockfish" (Chess Engine) and obviously usable via command line tools.

In order to let the engine search for the best move, you use "go depth n" - the higher the depth - the longer it takes to search. Using my command line tool it searches for about 5 seconds using a depth of 20, and it looks like this:

go depth 20 info string NNUE evaluation using nn-3475407dc199.nnue enabled info depth 1 seldepth 1 multipv 1 score cp -161 nodes 26 nps 3714 tbhits 0 time 7 pv e7e6 info depth 2 seldepth 2 multipv 1 score cp -161 nodes 51 nps 6375 tbhits 0 time 8 pv e7e6 f1d3 info depth 3 seldepth 3 multipv 1 score cp -161 nodes 79 nps 7900 tbhits 0 time 10 pv e7e6 f1d3 g8f6 info depth 4 seldepth 4 multipv 1 score cp -161 nodes 113 nps 9416 tbhits 0 time 12 pv e7e6 f1d3 g8f6 b1c3 [...] bestmove e7e6 ponder h2h4

Now, using io.WriteString it finishes after milliseconds without any (visible) calculation: (That's also the output of the code below)

Stockfish 14 by the Stockfish developers (see AUTHORS file) info string NNUE evaluation using nn-3475407dc199.nnue enabled bestmove b6b5

Here's the code I use:

func useStockfish(commands []string) string {
    cmd := exec.Command("stockfish")
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Fatal(err)
    }
    for _, cmd := range commands {
        writeString(cmd, stdin)
    }
    err = stdin.Close()
    if err != nil {
        log.Fatal(err)
    }
    out, err := cmd.CombinedOutput()
    if err != nil {
        log.Fatal(err)
    }
    return string(out)
}

func writeString(cmd string, stdin io.WriteCloser) {
    _, err := io.WriteString(stdin, cmd)
    if err != nil {
        log.Fatal(err)
    }

And this is an example of how I use it. The first command is setting the position, the second one is calculation the next best move with a depth of 20. The result is showed above.

func FetchComputerMove(game *internal.Game) {
    useStockfish([]string{"position exmaplepos\n", "go depth 20"})
}

Solution

  • To leverage engines like stockfish - you need to start the process and keep it running.

    You are executing it, passing 2 commands via a Stdin pipe, then closing the pipe. Closing the pipe indicates to the program that you are no longer interested in what the engine has to say.

    To run it - and keep it running - you need something like:

    func startEngine(enginePath string) (stdin io.WriteCloser, stdout io.ReadCloser, err error) {
        cmd := exec.Command(enginePath )
    
        stdin, err = cmd.StdinPipe()
        if err != nil {
            return
        }
        stdout, err = cmd.StdoutPipe()
        if err != nil {
            return
        }
    
        err = cmd.Start() // start command - but don't wait for it to complete
        return
    }
    

    The returned pipes allow you to send commands & see the output live:

    stdin, stdout, err := startEngine("/usr/local/bin/stockfish")
    
    
    sendCmd := func(cmd string) error {
        _, err := stdin.Write([]byte(cmd + "\n"))
        return err
    }
    
    sendCmd("position examplepos")
    sendCmd("go depth 20")
    

    then to crudely read the asynchronous response:

    b := make([]byte, 10240)
    
    for {
        n, err := stdout.Read(b)
        if err != nil {
            log.Fatalf("read error: %v", err)
        }
        log.Println(string(b[:n]))
    }
    

    once a line like bestmove d2d4 ponder g8f6 appears, you know the current analysis command has completed.

    You can then either close the engine (by closing the stdin pipe) if that's all you need, or keep it open for further command submissions.