From 217f715f23d325d3633446204a4a8cfb1c8efddb Mon Sep 17 00:00:00 2001 From: NoahLan <6995syu@163.com> Date: Tue, 19 Dec 2023 01:15:56 +0800 Subject: [PATCH] feat: katago --- ngochess/engine_analysis.go | 109 ------------ ngochess/engine_gtp.go | 168 ------------------ ngochess/gtp/{gtp.go => engine.go} | 3 - ngochess/katago/engine_analysis.go | 266 +++++++++++++++++++++++++++++ ngochess/katago/req.go | 72 ++++++++ ngochess/katago/resp.go | 68 ++++++++ ngochess/katago/serializer.go | 51 ++++++ ngochess/req/req_analysis.go | 67 -------- ngochess/resp/resp_analysis.go | 70 -------- 9 files changed, 457 insertions(+), 417 deletions(-) delete mode 100644 ngochess/engine_analysis.go delete mode 100644 ngochess/engine_gtp.go rename ngochess/gtp/{gtp.go => engine.go} (99%) create mode 100644 ngochess/katago/engine_analysis.go create mode 100644 ngochess/katago/req.go create mode 100644 ngochess/katago/resp.go create mode 100644 ngochess/katago/serializer.go delete mode 100644 ngochess/req/req_analysis.go delete mode 100644 ngochess/resp/resp_analysis.go diff --git a/ngochess/engine_analysis.go b/ngochess/engine_analysis.go deleted file mode 100644 index 8dd7ed5..0000000 --- a/ngochess/engine_analysis.go +++ /dev/null @@ -1,109 +0,0 @@ -package ngochess - -// -//var ( -// analStartupDecidedFunc cmdn.LineFunc = func(sb *strings.Builder, line string) bool { -// return strings.Contains(line, "Started, ready to begin handling requests") -// } -// analEndLineDecidedFunc cmdn.LineFunc = func(sb *strings.Builder, line string) bool { -// return nstr.IsJSON(sb.String()) -// } -// analReadIDFunc cmdn.ReadIDFunc = func(serializer ndef.Serializer, data string) (string, error) { -// var s struct { -// ID string `json:"id"` -// } -// err := serializer.Unmarshal([]byte(data), &s) -// if err != nil { -// return "", err -// } -// return s.ID, nil -// } -//) -// -//type AnalysisEngine struct { -// *KatagoEngine -//} -// -//// NewAnalysisEngine 创建Analysis引擎 -//func NewAnalysisEngine(name string, args ...string) (*AnalysisEngine, error) { -// engine := &AnalysisEngine{KatagoEngine: &KatagoEngine{ -// Processor: cmdn.NewProcessor( -// false, -// cmdn.WithSerializer(njson.NewJsonSerializer()), -// cmdn.WithStartupDecidedFunc(analStartupDecidedFunc), -// cmdn.WithEndLineDecidedFunc(analEndLineDecidedFunc), -// cmdn.WithReadIDFunc(analReadIDFunc), -// ), -// Name: name, -// Mode: EngineAnalysis, -// }} -// err := engine.Processor.Run(name, args...) -// return engine, err -//} -// -//// Query 分析查询 -//func (eng *AnalysisEngine) Query(req *req.AnalQueryReq, callback func(queryResp *resp.AnalQueryResp)) error { -// if len(req.ID) == 0 { -// req.ID = nrandom.SnowflakeIdStr() -// } -// return eng.Processor.Exec(req, func(serializer ndef.Serializer, data string) { -// var tmp resp.AnalQueryResp -// tmp.Err = serializer.Unmarshal([]byte(data), &tmp) -// -// callback(&tmp) -// }) -//} -// -//// QueryVersion 查询版本号 -//func (eng *AnalysisEngine) QueryVersion(callback func(versionResp *resp.AnalQueryVersionResp)) error { -// return eng.Processor.Exec(&req.AnalQueryActionReq{ -// ID: nrandom.SnowflakeIdStr(), -// Action: req.AnalActionQueryVersion, -// }, func(serializer ndef.Serializer, data string) { -// var tmp resp.AnalQueryVersionResp -// tmp.Err = serializer.Unmarshal([]byte(data), &tmp) -// callback(&tmp) -// }) -//} -// -//// ClearCache 清除缓存 -//func (eng *AnalysisEngine) ClearCache(callback func(cacheResp *resp.AnalClearCacheResp)) error { -// return eng.Processor.Exec(&req.AnalQueryActionReq{ -// ID: nrandom.SnowflakeIdStr(), -// Action: req.AnalActionClearCache, -// }, func(serializer ndef.Serializer, data string) { -// var tmp resp.AnalClearCacheResp -// tmp.Err = serializer.Unmarshal([]byte(data), &tmp) -// callback(&tmp) -// }) -//} -// -//// Terminate 终止某次分析 -//// terminateId: 分析轮次ID -//// turnNumbers: 需要终止分析的回合数(手数),可空:所有回合 -//func (eng *AnalysisEngine) Terminate(terminateId string, turnNumbers []int32, callback func(terminateResp *resp.AnalTerminateResp)) error { -// return eng.Processor.Exec(&req.AnalQueryActionReq{ -// ID: nrandom.SnowflakeIdStr(), -// Action: req.AnalActionTerminate, -// TerminateId: terminateId, -// TurnNumbers: turnNumbers, -// }, func(serializer ndef.Serializer, data string) { -// var tmp resp.AnalTerminateResp -// tmp.Err = serializer.Unmarshal([]byte(data), &tmp) -// callback(&tmp) -// }) -//} -// -//// TerminateAll 终止所有分析 -//// turnNumbers: 需要终止所有分析轮次的回合数,可空:所有回合 -//func (eng *AnalysisEngine) TerminateAll(turnNumbers []int32, callback func(allResp *resp.AnalTerminateAllResp)) error { -// return eng.Processor.Exec(&req.AnalQueryActionReq{ -// ID: nrandom.SnowflakeIdStr(), -// Action: req.AnalActionTerminateAll, -// TurnNumbers: turnNumbers, -// }, func(serializer ndef.Serializer, data string) { -// var tmp resp.AnalTerminateAllResp -// tmp.Err = serializer.Unmarshal([]byte(data), &tmp) -// callback(&tmp) -// }) -//} diff --git a/ngochess/engine_gtp.go b/ngochess/engine_gtp.go deleted file mode 100644 index a46aa6f..0000000 --- a/ngochess/engine_gtp.go +++ /dev/null @@ -1,168 +0,0 @@ -package ngochess - -// -//import ( -// "fmt" -// "git.noahlan.cn/noahlan/ntool-biz/nkatago/codec" -// "git.noahlan.cn/noahlan/ntool-biz/nkatago/req" -// "git.noahlan.cn/noahlan/ntool-biz/nkatago/resp" -// "git.noahlan.cn/noahlan/ntool/ndef" -// "git.noahlan.cn/noahlan/ntool/nsys/cmdn" -// "strings" -//) -// -//var ( -// gtpStartupDecidedFunc cmdn.LineFunc = func(sb *strings.Builder, line string) bool { -// return strings.Contains(line, "GTP ready") -// } -// gtpEndLineDecidedFunc cmdn.LineFunc = func(sb *strings.Builder, line string) bool { -// return len(line) == 0 -// } -// gtpReadIDFunc cmdn.ReadIDFunc = func(serializer ndef.Serializer, data string) (string, error) { -// return "", nil -// } -//) -// -//type GtpEngine struct { -// *KatagoEngine -// -// lastID string -//} -// -//// NewGtpEngine 创建GTP引擎 -//func NewGtpEngine(name string, args ...string) (*GtpEngine, error) { -// engine := &GtpEngine{KatagoEngine: &KatagoEngine{ -// Processor: cmdn.NewProcessor( -// true, -// cmdn.WithSerializer(codec.NewGtpSerializer()), -// cmdn.WithStartupDecidedFunc(gtpStartupDecidedFunc), -// cmdn.WithEndLineDecidedFunc(gtpEndLineDecidedFunc), -// cmdn.WithReadIDFunc(gtpReadIDFunc), -// ), -// Name: name, -// Mode: EngineGTP, -// }} -// err := engine.Processor.Run(name, args...) -// return engine, err -//} -// -//func (eng *GtpEngine) ID(id string) { -// eng.lastID = id -//} -// -//// Exec 同步方式自定义命令 -//func (eng *GtpEngine) Exec(id, cmd string) (*resp.GtpResponse, error) { -// cmds := strings.Fields(cmd) -// -// ret := resp.NewGtpResponse(cmds[0]) -// err := eng.Processor.ExecAsync(req.NewGtpReq(id, cmd, cmds[1:]...), func(serializer ndef.Serializer, data string) { -// ret.Err = serializer.Unmarshal([]byte(data), ret) -// }) -// return ret, err -//} -// -//// KnowCommand 判断命令是否支持 -//func (eng *GtpEngine) KnowCommand(cmd string) bool { -// value, err := eng.Exec(eng.lastID, "known_command "+cmd) -// if err != nil { -// return false -// } -// if strings.ToLower(strings.TrimSpace(value.Content)) != "true" { -// return false -// } -// return true -//} -// -//// Komi 设置贴目 -//func (eng *GtpEngine) Komi(komi float64) (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, fmt.Sprintf("komi %v", komi)) -//} -// -//// BoardSize 设置棋盘大小 -//func (eng *GtpEngine) BoardSize(size int) (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, fmt.Sprintf("boardsize %v", size)) -//} -// -//// GenMove AI落子 -//func (eng *GtpEngine) GenMove(color string) (*resp.GtpResponse, error) { -// color = strings.ToUpper(color) -// command := "genmove " + color -// return eng.Exec(eng.lastID, command) -//} -// -//// Play 手工落子 -//func (eng *GtpEngine) Play(color, coor string) (*resp.GtpResponse, error) { -// color = strings.ToUpper(color) -// return eng.Exec(eng.lastID, fmt.Sprintf("play %s %s", color, coor)) -//} -// -//// LoadSgf 加载SGF文件 -//func (eng *GtpEngine) LoadSgf(file string) (*resp.GtpResponse, error) { -// command := fmt.Sprintf("loadsgf %s", file) -// return eng.Exec(eng.lastID, command) -//} -// -//// FinalStatusList 获取当前盘面形势判断 -//func (eng *GtpEngine) FinalStatusList(cmd string) (*resp.GtpResponse, error) { -// command := fmt.Sprintf("final_status_list %s", cmd) -// return eng.Exec(eng.lastID, command) -//} -// -//// SetLevel 设置AI级别 -//func (eng *GtpEngine) SetLevel(seed int) (*resp.GtpResponse, error) { -// command := fmt.Sprintf("level %d", seed) -// return eng.Exec(eng.lastID, command) -//} -// -//// SetRandomSeed 设置AI随机数 -//func (eng *GtpEngine) SetRandomSeed(seed int) (*resp.GtpResponse, error) { -// command := fmt.Sprintf("set_random_seed %d", seed) -// return eng.Exec(eng.lastID, command) -//} -// -//// ShowBoard 显示棋盘 -//func (eng *GtpEngine) ShowBoard() (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, "showboard") -//} -// -//// ClearBoard 清空棋盘 -//func (eng *GtpEngine) ClearBoard() (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, "clear_board") -//} -// -//// PrintSgf 打印SGF -//func (eng *GtpEngine) PrintSgf() (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, "printsgf") -//} -// -//// TimeSetting 设置时间规则 -//func (eng *GtpEngine) TimeSetting(baseTime, byoTime, byoStones int) (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, fmt.Sprintf("time_settings %d %d %d", baseTime, byoTime, byoStones)) -//} -// -//// KGSTimeSetting 设置KGS time -//func (eng *GtpEngine) KGSTimeSetting(mainTime, readTime, readLimit int) (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, fmt.Sprintf("kgs-time_settings byoyomi %d %d %d", mainTime, readTime, readLimit)) -//} -// -//// FinalScore 获取结果 -//func (eng *GtpEngine) FinalScore() (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, "final_score") -//} -// -//// Undo 悔棋 -//func (eng *GtpEngine) Undo() (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, "undo") -//} -// -//// TimeLeft 设置时间 -//func (eng *GtpEngine) TimeLeft(color string, mainTime, stones int) (*resp.GtpResponse, error) { -// return eng.Exec(eng.lastID, fmt.Sprintf("time_left %s %d %d", color, mainTime, stones)) -//} -// -//// Quit 退出 -//func (eng *GtpEngine) Quit() (*resp.GtpResponse, error) { -// r, err := eng.Exec(eng.lastID, "quit") -// err = eng.Processor.Stop() -// return r, err -//} diff --git a/ngochess/gtp/gtp.go b/ngochess/gtp/engine.go similarity index 99% rename from ngochess/gtp/gtp.go rename to ngochess/gtp/engine.go index e29bc32..4a44c2c 100644 --- a/ngochess/gtp/gtp.go +++ b/ngochess/gtp/engine.go @@ -177,9 +177,6 @@ func (e *GtpEngine) handleMessage() { e.Cmd.Stdout = nil continue } - //if e.DevMode { - // nlog.Debugf("接收单条消息: %s", line) - //} resp, ok := e.serializer.Unmarshal(line) if !ok { diff --git a/ngochess/katago/engine_analysis.go b/ngochess/katago/engine_analysis.go new file mode 100644 index 0000000..8a891a6 --- /dev/null +++ b/ngochess/katago/engine_analysis.go @@ -0,0 +1,266 @@ +package katago + +import ( + "errors" + "git.noahlan.cn/noahlan/ntool/ncmd" + "git.noahlan.cn/noahlan/ntool/nlog" + "git.noahlan.cn/noahlan/ntool/nstr" + "git.noahlan.cn/noahlan/ntool/nsys/atomic" + "sync" + "time" +) + +var ( + DefaultMaxMessageId uint64 = 9999999 + DefaultTimeout = 10 * time.Minute +) + +var ErrTimeout = errors.New("timeout") + +type ( + Options struct { + DevMode bool `json:",default=false"` // 开发模式 + MaxMessageId uint64 `json:",default=9999999"` // 最大消息ID + Timeout time.Duration `json:""` // 单条命令最大等待时间,默认10min + } + Option func(options *Options) + + pendingMsg struct { + chWait chan struct{} + respJson string + } + + AnalysisEngine struct { + *Options + + Cmd *ncmd.Cmd + serializer *Serializer + mid *atomic.AtomicInt64 + + pendingMsg map[string]*pendingMsg + + mu sync.RWMutex + } +) + +func NewKatagoAnalysisEngine(opts ...Option) *AnalysisEngine { + serializer := NewSerializer() + ret := &AnalysisEngine{ + 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 +} + +func (e *AnalysisEngine) Start(name string, args ...string) <-chan ncmd.Status { + statusChan := e.Cmd.Start(name, args...) + + go e.handleMessage() + + return statusChan +} + +func (e *AnalysisEngine) 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 *AnalysisEngine) Send(data BaseMsg, resp any) error { + pMsg := &pendingMsg{ + chWait: make(chan struct{}, 1), + } + id := e.nextId() + data.SetMessageId(id) + + e.mu.Lock() + e.pendingMsg[id] = pMsg + e.mu.Unlock() + + err := e.Cmd.Send(data) + if err != nil { + return 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 ErrTimeout + case <-pMsg.chWait: + ok, err := e.serializer.Unmarshal(pMsg.respJson, resp) + if err != nil { + return err + } + if !ok { + return errors.New("消息不完整") + } + return nil + } +} + +func (e *AnalysisEngine) nextId() string { + idInt := e.mid.IncrementAndGet() + if idInt > int64(e.MaxMessageId) { + e.mid.Reset() + idInt = e.mid.IncrementAndGet() + } + return nstr.SafeString(idInt) +} + +func (e *AnalysisEngine) 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) + //} + var baseResp BaseResp + ok, err := e.serializer.Unmarshal(line, &baseResp) + if err != nil { + nlog.Errorf("解释json失败 %+v", err) + continue + } + if e.DevMode { + nlog.Debugf("接收完整消息: %+v", e.serializer.contentBuf.String()) + } + + e.mu.RLock() + pMsg, ok := e.pendingMsg[baseResp.ID] + e.mu.RUnlock() + if !ok { + continue + } + + pMsg.respJson = e.serializer.LastJson() + pMsg.chWait <- struct{}{} + case line, open := <-e.Cmd.Stderr: + if !open { + e.Cmd.Stdout = nil + continue + } + nlog.Errorf("错误消息: %s\n", line) + } + } +} + +func (e *AnalysisEngine) check(err error, req any) bool { + if err != nil { + nlog.Errorf("发送命令[%+v]失败 %v", req, err) + return false + } + return true +} + +//////////////////////////////// + +// QueryVersion 查询版本号 +func (e *AnalysisEngine) QueryVersion(req *QueryActionReq) (*QueryVersionResp, error) { + var resp QueryVersionResp + + err := e.Send(req, &resp) + if !e.check(err, req) { + return nil, err + } + return &resp, nil +} + +// Query 分析查询 +func (e *AnalysisEngine) Query(req *QueryReq) (*QueryResp, error) { + var resp QueryResp + + err := e.Send(req, &resp) + if !e.check(err, req) { + return nil, err + } + return &resp, nil +} + +// ClearCache 清除缓存 +func (e *AnalysisEngine) ClearCache() (*ClearCacheResp, error) { + var resp ClearCacheResp + req := &QueryActionReq{ + Action: AnalActionClearCache, + } + err := e.Send(req, &resp) + if !e.check(err, req) { + return nil, err + } + return &resp, nil +} + +// Terminate 终止某次分析 +// terminateId: 分析轮次ID +// turnNumbers: 需要终止分析的回合数(手数),可空:所有回合 +func (e *AnalysisEngine) Terminate(terminateId string, turnNumbers []int32) (*TerminateResp, error) { + var resp TerminateResp + req := &QueryActionReq{ + Action: AnalActionTerminate, + TerminateId: terminateId, + TurnNumbers: turnNumbers, + } + err := e.Send(req, &resp) + if !e.check(err, req) { + return nil, err + } + return &resp, nil +} + +// TerminateAll 终止所有分析 +// turnNumbers: 需要终止所有分析轮次的回合数,可空:所有回合 +func (e *AnalysisEngine) TerminateAll(turnNumbers []int32) (*TerminateAllResp, error) { + var resp TerminateAllResp + req := &QueryActionReq{ + Action: AnalActionTerminateAll, + TurnNumbers: turnNumbers, + } + err := e.Send(req, &resp) + if !e.check(err, req) { + return nil, err + } + return &resp, nil +} diff --git a/ngochess/katago/req.go b/ngochess/katago/req.go new file mode 100644 index 0000000..42c58a0 --- /dev/null +++ b/ngochess/katago/req.go @@ -0,0 +1,72 @@ +package katago + +import "git.noahlan.cn/noahlan/ntool/njson" + +type BaseMsg interface { + SetMessageId(id string) + MessageId() string +} + +type Stone struct { + Player string // 玩家 "B" / "W" + Location string // "C4" GTP标准协议,或 "CC" 行列协议 +} + +func (s Stone) MarshalJSON() ([]byte, error) { + ret := []string{s.Player, s.Location} + return njson.Marshal(ret) +} + +type QueryReq struct { + Id string `json:"id"` // ID + Moves []Stone `json:"moves"` // moves + InitialStones []Stone `json:"initialStones,omitempty,optional"` // 初始化棋子(让子棋,棋谱等等) + Rules string `json:"rules"` // 规则, chinese / japanese / tromp-taylor .etc... + Komi float32 `json:"komi"` // 贴目,中国规则一般7.5目 + BoardXSize int64 `json:"boardXSize"` // 棋盘X大小 + BoardYSize int64 `json:"boardYSize"` // 棋盘Y大小 + AnalyzeTurns []int32 `json:"analyzeTurns,omitempty,optional"` // 需要分析的回合数,与moves对应,若不指定,则分析最后一回合 + InitialPlayer string `json:"initialPlayer,omitempty,optional"` // 指定第一手的玩家 "B" / "W",当moves为空时很有用 + /// .etc... +} + +func (r *QueryReq) SetMessageId(id string) { + r.Id = id +} + +func (r *QueryReq) MessageId() string { + return r.Id +} + +func (r *QueryReq) AddMove(player, location string) { + r.Moves = append(r.Moves, Stone{Player: player, Location: location}) +} + +func (r *QueryReq) AddInitialStone(player, location string) { + r.InitialStones = append(r.InitialStones, Stone{Player: player, Location: location}) +} + +const ( + AnalActionQueryVersion = "query_version" + AnalActionClearCache = "clear_cache" + AnalActionTerminate = "terminate" + AnalActionTerminateAll = "terminate_all" +) + +type QueryActionReq struct { + Id string `json:"id"` // ID + Action string `json:"action"` // 查询动作 + + // terminate + TerminateId string `json:"terminateId,omitempty,optional"` + // terminate | terminate_all + TurnNumbers []int32 `json:"turnNumbers,omitempty,optional"` +} + +func (r *QueryActionReq) SetMessageId(id string) { + r.Id = id +} + +func (r *QueryActionReq) MessageId() string { + return r.Id +} diff --git a/ngochess/katago/resp.go b/ngochess/katago/resp.go new file mode 100644 index 0000000..0360060 --- /dev/null +++ b/ngochess/katago/resp.go @@ -0,0 +1,68 @@ +package katago + +type BaseResp struct { + ID string `json:"id"` // ID 必有 + Action string `json:"action,omitempty,optional"` // Action 动作,不是必填值 + // 其它东西,通过组合 + Err error +} + +type QueryResp struct { + BaseResp + IsDuringSearch bool `json:"isDuringSearch"` // 是否是搜索的过程结果,发送时指定 reportDuringSearchEvery,则会在过程中进行回复 + TurnNumber int32 `json:"turnNumber"` // 正在分析的回合数(手数) + MoveInfos []struct { + Move string `json:"move"` // 正在分析的移动,通常为棋盘坐标 + Visits int32 `json:"visits"` // 访问次数(神经网络) + WinRate float64 `json:"winrate"` // 胜率 [0~1] [0%~100%] 越高越好 + ScoreMean float64 `json:"scoreMean"` // 预计平均分 + ScoreLead float64 `json:"scoreLead"` // 预计平均分 与 ScoreMean 一致 + ScoreStdev float64 `json:"scoreStdev"` // 预计平均分标准差,由于MCTS机制,该分值可能偏高 + ScoreSelfplay float64 `json:"scoreSelfplay"` // 自我对弈时的预计分数,该值目前偏高 + Prior float64 `json:"prior"` // 策略优先级 [0~1],通常会选优先级高的下棋(直觉) + Utility float64 `json:"utility"` // 综合效用值,结合winrate和score,值域 [-C,C] + Lcb float64 `json:"lcb"` // LCB胜率,但值可能有误 [0~1] 可能会越界 + UtilityLcb float64 `json:"utilityLcb"` // LCB综合效用值 + Weight float64 `json:"weight"` // Visits 的平均权重 + Order int32 `json:"order"` // Katago神经网络中的直觉优先级,值[0~max]越低,优先级越高 + PV []string `json:"pv"` // 此次移动之后预测之后的移动列表 + // PvVisits + // pvEdgeVisits + // ownership + // ownershipStdev + } `json:"moveInfos"` // 移动信息 + RootInfo struct { + ThisHash string `json:"thisHash"` // 本次移动的唯一编码 + SymHash string `json:"symHash"` // + CurrentPlayer string `json:"currentPlayer"` // "B" or "W" + // RawStWrError + // rawStScoreError + // rawVarTimeLeft + Visits int32 `json:"visits"` // 访问次数(神经网络) + WinRate float64 `json:"winrate"` // 胜率 [0~1] [0%~100%] 越高越好 + ScoreLead float64 `json:"scoreLead"` // 预计平均分 与 ScoreMean 一致 + ScoreSelfplay float64 `json:"scoreSelfplay"` // 自我对弈时的预计分数,该值目前偏高 + Utility float64 `json:"utility"` // 综合效用值,结合winrate和score,值域 [-C,C] + } `json:"rootInfo"` // 根信息 +} + +type QueryVersionResp struct { + BaseResp + GitHash string `json:"git_hash"` // GitHash + Version string `json:"version"` // 版本号 +} + +type ClearCacheResp struct { + BaseResp +} + +type TerminateResp struct { + BaseResp + IsDuringSearch bool `json:"isDuringSearch"` // 是否是搜索的过程结果,发送时指定 reportDuringSearchEvery,则会在过程中进行回复 + TurnNumber int64 `json:"turnNumber"` // 本次停止的回合数 +} + +type TerminateAllResp struct { + BaseResp + TurnNumbers []int64 `json:"turnNumbers"` // 本次停止的回合数 +} diff --git a/ngochess/katago/serializer.go b/ngochess/katago/serializer.go new file mode 100644 index 0000000..447a1c2 --- /dev/null +++ b/ngochess/katago/serializer.go @@ -0,0 +1,51 @@ +package katago + +import ( + "bytes" + "encoding/json" + "git.noahlan.cn/noahlan/ntool/njson" +) + +type Serializer struct { + contentBuf *bytes.Buffer + + lastJson string +} + +func NewSerializer() *Serializer { + ret := &Serializer{ + contentBuf: bytes.NewBuffer([]byte{}), + } + return ret +} + +func (s *Serializer) reset() { + s.contentBuf.Reset() +} + +func (s *Serializer) LastJson() string { + return s.lastJson +} + +func (s *Serializer) Marshal(v any) ([]byte, error) { + return njson.Marshal(v) +} + +func (s *Serializer) Unmarshal(line string, v any) (bool, error) { + s.contentBuf.WriteString(line) + + if json.Valid(s.contentBuf.Bytes()) { + defer func() { + s.lastJson = s.contentBuf.String() + s.reset() + }() + + err := njson.Unmarshal(s.contentBuf.Bytes(), v) + if err != nil { + return false, err + } + return true, nil + } else { + return false, nil + } +} diff --git a/ngochess/req/req_analysis.go b/ngochess/req/req_analysis.go deleted file mode 100644 index 3391020..0000000 --- a/ngochess/req/req_analysis.go +++ /dev/null @@ -1,67 +0,0 @@ -package req - -// -//import ( -// "encoding/json" -// "git.noahlan.cn/noahlan/ntool/nsys/cmdn" -//) -// -//type Stone struct { -// Player string // 玩家 "B" / "W" -// Location string // "C4" GTP标准协议,或 "CC" 行列协议 -//} -// -//func (s Stone) MarshalJSON() ([]byte, error) { -// ret := []string{s.Player, s.Location} -// return json.Marshal(ret) -//} -// -//var _ cmdn.ICommand = (*AnalQueryReq)(nil) -// -//type AnalQueryReq struct { -// ID string `json:"id"` // ID -// Moves []Stone `json:"moves"` // moves -// InitialStones []Stone `json:"initialStones,omitempty,optional"` // 初始化棋子(让子棋,棋谱等等) -// Rules string `json:"rules"` // 规则, chinese / japanese / tromp-taylor .etc... -// Komi float32 `json:"komi"` // 贴目,中国规则一般7.5目 -// BoardXSize int64 `json:"boardXSize"` // 棋盘X大小 -// BoardYSize int64 `json:"boardYSize"` // 棋盘Y大小 -// AnalyzeTurns []int32 `json:"analyzeTurns,omitempty,optional"` // 需要分析的回合数,与moves对应,若不指定,则分析最后一回合 -// InitialPlayer string `json:"initialPlayer,omitempty,optional"` // 指定第一手的玩家 "B" / "W",当moves为空时很有用 -// /// .etc... -//} -// -//func (r *AnalQueryReq) MessageID() string { -// return r.ID -//} -// -//func (r *AnalQueryReq) AddMove(player, location string) { -// r.Moves = append(r.Moves, Stone{Player: player, Location: location}) -//} -// -//func (r *AnalQueryReq) AddInitialStone(player, location string) { -// r.InitialStones = append(r.InitialStones, Stone{Player: player, Location: location}) -//} -// -//var _ cmdn.ICommand = (*AnalQueryActionReq)(nil) -// -//const ( -// AnalActionQueryVersion = "query_version" -// AnalActionClearCache = "clear_cache" -// AnalActionTerminate = "terminate" -// AnalActionTerminateAll = "terminate_all" -//) -// -//type AnalQueryActionReq struct { -// ID string `json:"id"` // ID -// Action string `json:"action"` // 查询动作 -// -// // terminate -// TerminateId string `json:"terminateId,omitempty,optional"` -// // terminate | terminate_all -// TurnNumbers []int32 `json:"turnNumbers,omitempty,optional"` -//} -// -//func (r *AnalQueryActionReq) MessageID() string { -// return r.ID -//} diff --git a/ngochess/resp/resp_analysis.go b/ngochess/resp/resp_analysis.go deleted file mode 100644 index 99de296..0000000 --- a/ngochess/resp/resp_analysis.go +++ /dev/null @@ -1,70 +0,0 @@ -package resp - -// -//type AnalBaseResp struct { -// ID string `json:"id"` // ID 必有 -// Action string `json:"action,omitempty,optional"` // Action 动作,不是必填值 -// // 其它东西,通过组合 -// -// Err error -//} -// -//type AnalQueryResp struct { -// AnalBaseResp -// IsDuringSearch bool `json:"isDuringSearch"` // 是否是搜索的过程结果,发送时指定 reportDuringSearchEvery,则会在过程中进行回复 -// TurnNumber int32 `json:"turnNumber"` // 正在分析的回合数(手数) -// MoveInfos []struct { -// Move string `json:"move"` // 正在分析的移动,通常为棋盘坐标 -// Visits int32 `json:"visits"` // 访问次数(神经网络) -// WinRate float64 `json:"winrate"` // 胜率 [0~1] [0%~100%] 越高越好 -// ScoreMean float64 `json:"scoreMean"` // 预计平均分 -// ScoreLead float64 `json:"scoreLead"` // 预计平均分 与 ScoreMean 一致 -// ScoreStdev float64 `json:"scoreStdev"` // 预计平均分标准差,由于MCTS机制,该分值可能偏高 -// ScoreSelfplay float64 `json:"scoreSelfplay"` // 自我对弈时的预计分数,该值目前偏高 -// Prior float64 `json:"prior"` // 策略优先级 [0~1],通常会选优先级高的下棋(直觉) -// Utility float64 `json:"utility"` // 综合效用值,结合winrate和score,值域 [-C,C] -// Lcb float64 `json:"lcb"` // LCB胜率,但值可能有误 [0~1] 可能会越界 -// UtilityLcb float64 `json:"utilityLcb"` // LCB综合效用值 -// Weight float64 `json:"weight"` // Visits 的平均权重 -// Order int32 `json:"order"` // Katago神经网络中的直觉优先级,值[0~max]越低,优先级越高 -// PV []string `json:"pv"` // 此次移动之后预测之后的移动列表 -// // PvVisits -// // pvEdgeVisits -// // ownership -// // ownershipStdev -// } `json:"moveInfos"` // 移动信息 -// RootInfo struct { -// ThisHash string `json:"thisHash"` // 本次移动的唯一编码 -// SymHash string `json:"symHash"` // -// CurrentPlayer string `json:"currentPlayer"` // "B" or "W" -// // RawStWrError -// // rawStScoreError -// // rawVarTimeLeft -// Visits int32 `json:"visits"` // 访问次数(神经网络) -// WinRate float64 `json:"winrate"` // 胜率 [0~1] [0%~100%] 越高越好 -// ScoreLead float64 `json:"scoreLead"` // 预计平均分 与 ScoreMean 一致 -// ScoreSelfplay float64 `json:"scoreSelfplay"` // 自我对弈时的预计分数,该值目前偏高 -// Utility float64 `json:"utility"` // 综合效用值,结合winrate和score,值域 [-C,C] -// } `json:"rootInfo"` // 根信息 -//} -// -//type AnalQueryVersionResp struct { -// AnalBaseResp -// GitHash string `json:"git_hash"` // GitHash -// Version string `json:"version"` // 版本号 -//} -// -//type AnalClearCacheResp struct { -// AnalBaseResp -//} -// -//type AnalTerminateResp struct { -// AnalBaseResp -// IsDuringSearch bool `json:"isDuringSearch"` // 是否是搜索的过程结果,发送时指定 reportDuringSearchEvery,则会在过程中进行回复 -// TurnNumber int64 `json:"turnNumber"` // 本次停止的回合数 -//} -// -//type AnalTerminateAllResp struct { -// AnalBaseResp -// TurnNumbers []int64 `json:"turnNumbers"` // 本次停止的回合数 -//}