|
|
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(>PCommand{
|
|
|
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 %.1f", 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
|
|
|
}
|