You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ntool-biz/ngochess/gtp/gtp.go

495 lines
10 KiB
Go

package gtp
import (
"errors"
"fmt"
"git.noahlan.cn/noahlan/ntool/narr"
"git.noahlan.cn/noahlan/ntool/ncmd"
"git.noahlan.cn/noahlan/ntool/nlog"
"git.noahlan.cn/noahlan/ntool/nmath"
"git.noahlan.cn/noahlan/ntool/nstr"
"git.noahlan.cn/noahlan/ntool/nsys/atomic"
"strings"
"sync"
"time"
)
var (
DefaultMaxMessageId uint = 999
DefaultTimeout = 3 * time.Second
)
var ErrTimeout = errors.New("timeout")
type (
Options struct {
DevMode bool `json:",default=false"` // 开发模式
MaxMessageId uint `json:",default=999"` // 最大消息ID
Timeout time.Duration `json:""` // 单条命令最大等待时间默认3s
}
Option func(options *Options)
pendingMsg struct {
chWait chan struct{}
resp *GTPResponse
}
GtpEngine struct {
*Options
Cmd *ncmd.Cmd
serializer *GTPSerializer
mid *atomic.AtomicInt64
pendingMsg map[string]*pendingMsg
mu sync.RWMutex
}
)
func NewGtpEngine(opts ...Option) *GtpEngine {
serializer := NewGTPSerializer()
ret := &GtpEngine{
Options: &Options{
DevMode: false,
},
mid: atomic.NewAtomicInt64(),
pendingMsg: make(map[string]*pendingMsg),
mu: sync.RWMutex{},
}
for _, opt := range opts {
opt(ret.Options)
}
if ret.MaxMessageId == 0 {
ret.MaxMessageId = DefaultMaxMessageId
}
if ret.Timeout == 0 {
ret.Timeout = DefaultTimeout
}
ret.serializer = serializer
ret.Cmd = ncmd.NewCmd(ncmd.WithOptions(&ncmd.Options{
Marshaler: serializer,
Buffered: false,
CombinedOutput: false,
Streaming: true,
LineBufferSize: ncmd.DEFAULT_LINE_BUFFER_SIZE,
DevMode: ret.DevMode,
}))
return ret
}
// Bind 绑定实例到id
func (e *GtpEngine) Bind(id int64) {
e.Cmd.Session.SetId(id)
// Set in_use flag
e.Cmd.Session.SetAttribute(KeyInUse, true)
}
// Release 释放示例,以便其它调用
func (e *GtpEngine) Release() {
e.Cmd.Session.SetId(0)
e.Cmd.Session.SetAttribute(KeyInUse, false)
}
func (e *GtpEngine) Session() *ncmd.Session {
return e.Cmd.Session
}
func (e *GtpEngine) StartSafe(name string, args ...string) <-chan ncmd.Status {
ret := e.Cmd.StatusChan()
if !e.Cmd.Started() {
if !e.Cmd.Stopped() {
ret = e.Start(name, args...)
} else {
// 被停止
e.Cmd = e.Cmd.Clone()
ret = e.Start(name, args...)
}
}
return ret
}
func (e *GtpEngine) Start(name string, args ...string) <-chan ncmd.Status {
statusChan := e.Cmd.Start(name, args...)
go e.handleMessage()
return statusChan
}
func (e *GtpEngine) Send(id, command string, args ...string) (*GTPResponse, error) {
if len(id) == 0 || len(id) > 3 {
id = e.nextId()
}
pMsg := &pendingMsg{
chWait: make(chan struct{}, 1),
}
e.mu.Lock()
e.pendingMsg[id] = pMsg
e.mu.Unlock()
err := e.Cmd.Send(&GTPCommand{
ID: id,
Cmd: command,
Args: args,
})
if err != nil {
return nil, err
}
// waiting pending message with timeout
timer := time.NewTimer(e.Timeout)
select {
case <-timer.C:
// error
e.mu.Lock()
delete(e.pendingMsg, id)
e.mu.Unlock()
return nil, ErrTimeout
case <-pMsg.chWait:
return pMsg.resp, nil
}
}
func (e *GtpEngine) nextId() string {
idInt := e.mid.IncrementAndGet()
if idInt > int64(e.MaxMessageId) {
e.mid.Reset()
idInt = e.mid.IncrementAndGet()
}
return nstr.SafeString(idInt)
}
func (e *GtpEngine) handleMessage() {
for e.Cmd.Stdout != nil || e.Cmd.Stderr != nil {
select {
case line, open := <-e.Cmd.Stdout:
if !open {
e.Cmd.Stdout = nil
continue
}
//if e.DevMode {
// nlog.Debugf("接收单条消息: %s", line)
//}
resp, ok := e.serializer.Unmarshal(line)
if !ok {
continue
}
if e.DevMode {
nlog.Debugf("接收完整消息: %+v", resp)
}
e.mu.RLock()
pMsg, ok := e.pendingMsg[resp.ID]
e.mu.RUnlock()
if !ok {
continue
}
pMsg.resp = resp
pMsg.chWait <- struct{}{}
case line, open := <-e.Cmd.Stderr:
if !open {
e.Cmd.Stdout = nil
continue
}
nlog.Errorf("错误消息: %s\n", line)
}
}
}
// Name 查看gtp软件名称
func (e *GtpEngine) Name() string {
cmd := "name"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return "unknown"
}
return resp.Content
}
// Version 查看gtp软件版本
func (e *GtpEngine) Version() string {
cmd := "version"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return "0"
}
return resp.Content
}
// ProtocolVersion 查看gtp协议版本
func (e *GtpEngine) ProtocolVersion() string {
cmd := "protocol_version"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return "1"
}
return resp.Content
}
// ListCommands 列举gtp软件可用命令列表
func (e *GtpEngine) ListCommands() []string {
cmd := "list_commands"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return []string{}
}
return strings.Split(resp.Content, "\n")
}
// KnowCommand 判断命令是否支持
func (e *GtpEngine) KnowCommand(cmd string) bool {
command := fmt.Sprintf("known_command %s", cmd)
resp, err := e.Send(e.nextId(), command)
if !e.Check(resp, err, command) {
return false
}
if strings.ToLower(strings.TrimSpace(resp.Content)) != "true" {
return false
}
return true
}
// Komi 设置贴目
func (e *GtpEngine) Komi(komi float64) bool {
cmd := fmt.Sprintf("komi %.2f", komi)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// BoardSize 设置棋盘大小
func (e *GtpEngine) BoardSize(size int) bool {
cmd := fmt.Sprintf("boardsize %d", size)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// ClearBoard 清理棋盘
func (e *GtpEngine) ClearBoard() bool {
cmd := "clear_board"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// Play 下棋 color: B/W vex: A1
func (e *GtpEngine) Play(color, vex string) bool {
cmd := fmt.Sprintf("play %s %s", color, vex)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// GenMove 生成一手棋 color: B/W
func (e *GtpEngine) GenMove(color string) string {
cmd := fmt.Sprintf("genmove %s", color)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return ""
}
return resp.Content
}
// SetFreeHandicap 设置自由的让子点位
func (e *GtpEngine) SetFreeHandicap(vexArr ...string) bool {
if len(vexArr) < 2 {
return true
}
cmd := fmt.Sprintf("set_free_handicap %s", strings.Join(vexArr, " "))
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// Sync 同步
// handicaps: points of all handicaps
// plays: [][2]string -> [["B","A1"]]
func (e *GtpEngine) Sync(size int, komi float64, level int, handicaps []string, plays [][]string) bool {
// 1. komi
// 2. boardsize
// 3. set_level
// 4. clear_board
// 5. set_free_handicap pos1 pos2 ...
// 6~n. play X XX
playAllFn := func() bool {
return narr.Every(plays, func(_ int, v []string) bool {
if len(v) < 2 {
return false
}
color, pos := v[0], v[1]
return e.Play(color, pos)
})
}
return e.Komi(komi) &&
e.BoardSize(size) &&
e.SetLevel(level) &&
e.ClearBoard() &&
e.SetFreeHandicap(handicaps...) &&
playAllFn()
}
// LoadSgf 加载SGF文件
func (e *GtpEngine) LoadSgf(file string) bool {
cmd := fmt.Sprintf("loadsgf %s", file)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// FinalStatusList 获取当前盘面形势判断
func (e *GtpEngine) FinalStatusList(cmd string) string {
command := fmt.Sprintf("final_status_list %s", cmd)
resp, err := e.Send(e.nextId(), command)
if !e.Check(resp, err, command) {
return ""
}
return resp.Content
}
// SetLevel 设置AI级别
func (e *GtpEngine) SetLevel(level int) bool {
cmd := fmt.Sprintf("level %d", level)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// SetRandomSeed 设置AI随机数
func (e *GtpEngine) SetRandomSeed(seed int) bool {
cmd := fmt.Sprintf("set_random_seed %d", seed)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// ShowBoard 显示棋盘
func (e *GtpEngine) ShowBoard() string {
cmd := "showboard"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return ""
}
return resp.Content
}
// PrintSgf 打印SGF
func (e *GtpEngine) PrintSgf() string {
cmd := "printsgf"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return ""
}
return resp.Content
}
// TimeSetting 设置时间规则
func (e *GtpEngine) TimeSetting(baseTime, byoTime, byoStones int) bool {
cmd := fmt.Sprintf("time_settings %d %d %d", baseTime, byoTime, byoStones)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// KGSTimeSetting 设置KGS time
func (e *GtpEngine) KGSTimeSetting(mainTime, readTime, readLimit int) bool {
cmd := fmt.Sprintf("kgs-time_settings byoyomi %d %d %d", mainTime, readTime, readLimit)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// FinalScore 获取结果
// returns winner, score, ok
func (e *GtpEngine) FinalScore() (string, float64, bool) {
cmd := "final_score"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return "", 0, false
}
splited := strings.Split(resp.Content, "+")
if len(splited) < 2 {
return "", 0, false
}
score, _ := nmath.Float(splited[1])
return splited[0], score, true
}
// Undo 悔棋
func (e *GtpEngine) Undo() bool {
cmd := "undo"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// TimeLeft 设置时间
func (e *GtpEngine) TimeLeft(color string, mainTime, stones int) bool {
cmd := fmt.Sprintf("time_left %s %d %d", color, mainTime, stones)
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
return true
}
// Quit 退出
func (e *GtpEngine) Quit() bool {
cmd := "quit"
resp, err := e.Send(e.nextId(), cmd)
if !e.Check(resp, err, cmd) {
return false
}
_ = e.Cmd.Stop()
return true
}
func (e *GtpEngine) Check(resp *GTPResponse, err error, cmd string) bool {
if err != nil {
nlog.Errorf("发送命令[%s]失败 %v", cmd, err)
return false
}
if resp.Err != nil {
nlog.Errorf("接收到GTP错误消息 %v", resp.Err)
return false
}
return true
}