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/engine.go

492 lines
10 KiB
Go

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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