ref: eb8f0cbd6b03c83d0ec53c73ce721dae347217de
parent: c975372b175fdf14e5c1158c705303fc73c116c7
author: kitzman <kitzman@disroot.org>
date: Fri Jan 26 07:45:33 EST 2024
added uciconnect
--- 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
@@ -74,6 +77,15 @@
variable, or from the argument. Additionally, the background
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
--- a/chessfs.4
+++ b/chessfs.4
@@ -6,6 +6,8 @@
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
@@ -89,6 +91,17 @@
.CW font
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
--- /dev/null
+++ b/cmd/uciconnect/uciconnect.go
@@ -1,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
+ }
+}
--- a/main.go
+++ b/main.go
@@ -8,7 +8,6 @@
"github.com/knusbaum/go9p"
"github.com/knusbaum/go9p/fs"
- // "github.com/knusbaum/go9p/proto"
)
func main() {