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 %.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 }