shithub: chessfs

Download patch

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() {