diff --git a/README b/README index bc74ff8..e6b91b1 100644 --- a/README +++ b/README @@ -12,6 +12,9 @@ chessterm [-font path] [-bg col] [-fg col] player directory + uciconnect [-think time] [-wait time] [-player p] -dir game + command + rc/play [-r] player directory rc/spectate [-r] directory @@ -75,6 +78,15 @@ and foreground colors can also be set, based on which the board is drawn. + Uciconnect can be used to connect a game directory, with a + process serving the UCI protocol via standard input and out- + put. There are two mandatory arguments: the game directory + and the command to execute. The -think arguments sets the + time the engine is allowed to find a move. Between commands + and reads, the program sleeps a duration, which can be set + via the -wait option. By default, the engine plays the white + player, which can be set with the -player flag. + Play is an interactive script which prints the board and game status, and waits for your or your opponent's move. The -r flag transcribes the UTF-8 piece symbols into other sym- diff --git a/chessfs.4 b/chessfs.4 index 56a8e0c..84eeb12 100644 --- a/chessfs.4 +++ b/chessfs.4 @@ -6,6 +6,8 @@ chessfs [-verbose] [-addr addr] [-srv srv] [-group gid] chessterm [-font path] [-bg col] [-fg col] player directory +uciconnect [-think time] [-wait time] [-player p] -dir game command + rc/play [-r] player directory rc/spectate [-r] directory @@ -90,6 +92,17 @@ from the environment variable, or from the argument. Additionally, the background and foreground colors can also be set, based on which the board is drawn. .PP +Uciconnect can be used to connect a game directory, with a process serving +the UCI protocol via standard input and output. There are two mandatory +arguments: the game directory and the command to execute. The +.BI -think +arguments sets the time the engine is allowed to find a move. Between +commands and reads, the program sleeps a duration, which can be set via the +.BI -wait +option. By default, the engine plays the white player, which can be set with the +.BI -player +flag. +.PP Play is an interactive script which prints the board and game status, and waits for your or your opponent's move. The -r flag transcribes the UTF-8 piece symbols into other symbols, in case the used font diff --git a/cmd/uciconnect/uciconnect.go b/cmd/uciconnect/uciconnect.go new file mode 100644 index 0000000..b98efd8 --- /dev/null +++ b/cmd/uciconnect/uciconnect.go @@ -0,0 +1,230 @@ +package main + +import ( + "fmt" + "flag" + "log" + "os" + "strings" + "time" + + "github.com/notnil/chess" + "github.com/notnil/chess/uci" +) + +const ( + DefaultThink = "0.25s" + DefaultWait = "0.5s" +) + +type GameDir struct { + dir string + player string +} + +func NewGameDir(dir, player string) (*GameDir, error) { + var err error + _, err = os.Stat(dir) + if err != nil { + return nil, fmt.Errorf("game dir stat: %v", err) + } + _, err = os.Stat(dir + "/ctl") + if err != nil { + return nil, fmt.Errorf("game dir stat ctl: %v", err) + } + _, err = os.Stat(dir + "/fen") + if err != nil { + return nil, fmt.Errorf("game dir stat fen: %v", err) + } + _, err = os.Stat(dir + "/" + player) + if err != nil { + return nil, fmt.Errorf("game dir stat %s: %v", player, err) + } + return &GameDir { dir, player }, nil +} + +func (g *GameDir) GetNew() (bool, error) { + ctl, err := os.ReadFile(g.dir + "/ctl") + if err != nil { + return false, err + } + + ctllines := strings.Split(string(ctl), "\n") + if ctllines[0] == "new" { + return true, nil + } + return false, nil +} + +func (g *GameDir) GetOngoing() (bool, error) { + ctl, err := os.ReadFile(g.dir + "/ctl") + if err != nil { + return false, err + } + + ctllines := strings.Split(string(ctl), "\n") + if ctllines[0] == "ongoing" { + return true, nil + } + return false, nil +} + +func (g *GameDir) GetTurn() (string, error) { + ctl, err := os.ReadFile(g.dir + "/ctl") + if err != nil { + return "", err + } + + ctllines := strings.Split(string(ctl), "\n") + turn := strings.Split(ctllines[1], "'") + if len(turn) != 2 { + return "", fmt.Errorf("malformed player turn line") + } + + return turn[0], nil +} + +func (g *GameDir) GetBoard() (*chess.Game, error) { + fenf, err := os.ReadFile(g.dir + "/fen") + if err != nil { + return nil, err + } + + fens := strings.Split(string(fenf), "\n") + if len(fens) < 1 { + return nil, fmt.Errorf("fen file empty") + } + + fen, err := chess.FEN(fens[0]) + if err != nil { + return nil, err + } + + return chess.NewGame(fen), nil +} + +func (g *GameDir) MakeMove(move string) error { + err := os.WriteFile(g.dir + "/" + g.player, []byte(move), os.FileMode(os.O_WRONLY)) + if err != nil { + return err + } + return nil +} + +func main() { + player := flag.String("player", "white", "which player to play") + gameDir := flag.String("dir", "", "game directory") + thinks := flag.String("think", DefaultThink, "thinking time") + waits := flag.String("wait", DefaultWait, "waiting time") + flag.Parse() + + if flag.NArg() == 0 { + log.Fatalf("no command supplied") + flag.Usage() + os.Exit(1) + } + command := strings.Join(flag.Args(), " ") + + if *player != "white" && *player != "black" { + log.Fatalf("player can either be black or white\n") + os.Exit(1) + } + + think, err := time.ParseDuration(*thinks) + if err != nil { + log.Fatalf("%v\n", err) + os.Exit(1) + } + wait, err := time.ParseDuration(*waits) + if err != nil { + log.Fatalf("%v\n", err) + os.Exit(1) + } + + log.Printf("opening game dir %s\n", *gameDir) + game, err := NewGameDir(*gameDir, *player) + if err != nil { + log.Fatalf("%v\n", err) + os.Exit(1) + } + + log.Printf("starting engine\n") + engine, err := uci.New(command) + if err != nil { + log.Fatalf("error starting engine: %v\n", err) + os.Exit(1) + } + + notation := chess.LongAlgebraicNotation{} + var playable = true + var isNew bool + var isOngoing bool + isNew, err = game.GetNew() + if err != nil { + log.Fatalf("error: %v\n", err) + os.Exit(1) + } + isOngoing, err = game.GetOngoing() + if err != nil { + log.Fatalf("error: %v\n", err) + os.Exit(1) + } + playable = isNew || isOngoing + for playable { + isOngoing, err = game.GetOngoing() + if err != nil { + log.Fatalf("error getting status: %v\n", err) + os.Exit(1) + } + if !isOngoing { + time.Sleep(wait) + continue + } + + turn, err := game.GetTurn() + if err != nil { + log.Fatalf("error getting turn: %v\n", err) + os.Exit(1) + } + + if turn != *player { + time.Sleep(wait) + continue + } + board, err := game.GetBoard() + if err != nil { + log.Fatalf("error getting game: %v\n", err) + os.Exit(1) + } + + cmdPos := uci.CmdPosition{Position: board.Position()} + cmdGo := uci.CmdGo{MoveTime: think} + if err := engine.Run(cmdPos, cmdGo); err != nil { + log.Fatalf("error thinking: %v\n", err) + os.Exit(1) + } + move := engine.SearchResults().BestMove + + err = board.Move(move) + if err != nil { + log.Fatalf("error attempting client move: %v\n", err) + os.Exit(1) + } + + moves := board.Moves() + positions := board.Positions() + err = game.MakeMove(notation.Encode(positions[len(moves) - 1], moves[len(moves) - 1])) + if err != nil { + log.Fatalf("error moving: %v\n", err) + os.Exit(1) + } + + time.Sleep(wait) + playable, err = game.GetOngoing() + if err != nil { + log.Fatalf("error getting status: %v\n", err) + os.Exit(1) + } + playable = isOngoing + } +} diff --git a/main.go b/main.go index e5ad1e3..109c006 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,6 @@ import ( "github.com/knusbaum/go9p" "github.com/knusbaum/go9p/fs" - // "github.com/knusbaum/go9p/proto" ) func main() {