From 95a3100eb2556187f9c8b423e8227b30fbbe0f4b Mon Sep 17 00:00:00 2001 From: littletrainee Date: Mon, 13 Jun 2022 20:59:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=94=B9=E8=AE=8A=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E6=96=87=E5=AD=97=E7=9A=84=E9=96=8B=E9=A0=AD=E5=A4=A7=E5=AF=AB?= =?UTF-8?q?=E4=BD=BF=E5=85=B6=E9=A1=9E=E4=BC=BC=E7=89=A9=E4=BB=B6=E5=B0=8E?= =?UTF-8?q?=E5=90=91=E8=AA=9E=E8=A8=80=E7=9A=84public=EF=BC=8C=E4=B8=A6?= =?UTF-8?q?=E4=B8=94=E5=84=AA=E5=8C=96main.go=E9=83=A8=E5=88=86=E7=A8=8B?= =?UTF-8?q?=E5=BC=8F=E7=A2=BC=E9=A1=AF=E7=A4=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/launch.json | 16 +++ cli.go | 17 +-- go.sum | 22 +--- main.go | 180 ++++++++++++++++-------------- platform/majsoul/api/liqi_test.go | 13 ++- 5 files changed, 129 insertions(+), 119 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8e280f3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // 使用 IntelliSense 以得知可用的屬性。 + // 暫留以檢視現有屬性的描述。 + // 如需詳細資訊,請瀏覽: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/cli.go b/cli.go index 3a7b60e..8eb6548 100644 --- a/cli.go +++ b/cli.go @@ -2,11 +2,12 @@ package main import ( "fmt" - "github.com/EndlessCheng/mahjong-helper/util" - "github.com/fatih/color" "math" "sort" "strings" + + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/fatih/color" ) func printAccountInfo(accountID int) { @@ -421,11 +422,11 @@ func printWaitsWithImproves13_twoRows(result13 *util.Hand13AnalysisResult, disca fmt.Printf("进张") } else { // shanten == 1 fmt.Printf("数") - if showAgariAboveShanten1 { + if ShowAgariAboveShanten1 { fmt.Printf("(%.2f%% 参考和率)", result13.AvgAgariRate) } } - if showScore { + if ShowScore { mixedScore := result13.MixedWaitsScore //for i := 2; i <= shanten; i++ { // mixedScore /= 4 @@ -559,7 +560,7 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() { } // 局收支 - if showScore && result13.MixedRoundPoint != 0.0 { + if ShowScore && result13.MixedRoundPoint != 0.0 { fmt.Print(" ") color.New(color.FgHiGreen).Printf("[局收支%4d]", int(math.Round(result13.MixedRoundPoint))) } @@ -583,7 +584,7 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() { if len(result13.YakuTypes) > 0 { // 役种(两向听以内开启显示) if result13.Shanten <= 2 { - if !showAllYakuTypes && !debugMode { + if !ShowAllYakuTypes && !debugMode { shownYakuTypes := []int{} for yakuType := range result13.YakuTypes { for _, yt := range yakuTypesToAlert { @@ -625,7 +626,7 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() { } // 改良数 - if showScore { + if ShowScore { fmt.Print(" ") if len(result13.Improves) > 0 { fmt.Printf("[%2d改良]", len(result13.Improves)) @@ -644,7 +645,7 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() { fmt.Println() - if showImproveDetail { + if ShowImproveDetail { for tile, waits := range result13.Improves { fmt.Printf("摸 %s 改良成 %s\n", util.Mahjong[tile], waits.String()) } diff --git a/go.sum b/go.sum index 56e21a9..aa3247f 100644 --- a/go.sum +++ b/go.sum @@ -11,15 +11,11 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -34,28 +30,23 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/labstack/echo/v4 v4.1.11 h1:z0BZoArY4FqdpUEl+wlHp4hnr/oSR6MTmQmv8OHSoww= -github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo= github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/levigross/grequests v0.0.0-20190908174114-253788527a1a h1:DGFy/362j92vQRE3ThU1yqg9TuJS8YJOSbQuB7BP9cA= github.com/levigross/grequests v0.0.0-20190908174114-253788527a1a/go.mod h1:jVntzcUU+2BtVohZBQmSHWUmh8B55LCNfPhcNCIvvIg= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= @@ -70,13 +61,10 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -90,8 +78,6 @@ golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 h1:MlY3mEfbnWGmUi4rtHOtNnnnN4UJRGSyLPx+DXA5Sq4= -golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -103,7 +89,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -112,7 +97,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -121,18 +105,17 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.31.1 h1:SfXqXS5hkufcdZ/mHtYCh53P2b+92WQq/DZcKLgsFRs= @@ -145,7 +128,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= diff --git a/main.go b/main.go index 38db009..a10d74f 100644 --- a/main.go +++ b/main.go @@ -3,73 +3,87 @@ package main import ( "flag" "fmt" - "github.com/EndlessCheng/mahjong-helper/util" - "github.com/EndlessCheng/mahjong-helper/util/model" - "github.com/fatih/color" "math/rand" "strings" "time" -) -var ( - considerOldYaku bool - - isMajsoul bool - isTenhou bool - isAnalysis bool - isInteractive bool + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/EndlessCheng/mahjong-helper/util/model" + "github.com/fatih/color" +) - showImproveDetail bool - showAgariAboveShanten1 bool - showScore bool - showAllYakuTypes bool +// Enum +const ( + Tenhou int = iota + MahJongSoul +) - humanDoraTiles string +// define Platform Class parameter +type Platform struct { + Name string + Type []string + Code int +} - port int +// declare variable +var ( + // bool + ConsiderOldYaku bool = false + IsMajsoul bool = false + IsTenhou bool = false + IsAnalysis bool = false + IsInteractive bool = false + ShowImproveDetail bool = false + ShowAgariAboveShanten1 bool = false + ShowScore bool = false + ShowAllYakuTypes bool = false + + //int + Port int = 0 + + // string + HumanDoraTiles string = "" + + // struct + Platforms []Platform = []Platform{ + { + Name: "天鳳", + Type: []string{ + "Web", + "4K"}, + Code: Tenhou, + }, + { + Name: "雀魂", + Type: []string{ + "國際中文服", + "日服", + "国际服"}, + Code: MahJongSoul, + }, + } ) func init() { rand.Seed(time.Now().UnixNano()) - flag.BoolVar(&considerOldYaku, "old", false, "允许古役") - flag.BoolVar(&isMajsoul, "majsoul", false, "雀魂助手") - flag.BoolVar(&isTenhou, "tenhou", false, "天凤助手") - flag.BoolVar(&isAnalysis, "analysis", false, "分析模式") - flag.BoolVar(&isInteractive, "interactive", false, "交互模式") - flag.BoolVar(&isInteractive, "i", false, "同 -interactive") - flag.BoolVar(&showImproveDetail, "detail", false, "显示改良细节") - flag.BoolVar(&showAgariAboveShanten1, "agari", false, "显示听牌前的估计和率") - flag.BoolVar(&showAgariAboveShanten1, "a", false, "同 -agari") - flag.BoolVar(&showScore, "score", false, "显示局收支") - flag.BoolVar(&showScore, "s", false, "同 -score") - flag.BoolVar(&showAllYakuTypes, "yaku", false, "显示所有役种") - flag.BoolVar(&showAllYakuTypes, "y", false, "同 -yaku") - flag.StringVar(&humanDoraTiles, "dora", "", "指定哪些牌是宝牌") - flag.StringVar(&humanDoraTiles, "d", "", "同 -dora") - flag.IntVar(&port, "port", 12121, "指定服务端口") - flag.IntVar(&port, "p", 12121, "同 -port") -} - -const ( - platformTenhou = 0 - platformMajsoul = 1 - - defaultPlatform = platformMajsoul -) - -var platforms = map[int][]string{ - platformTenhou: { - "天凤", - "Web", - "4K", - }, - platformMajsoul: { - "雀魂", - "国际中文服", - "日服", - "国际服", - }, + flag.BoolVar(&ConsiderOldYaku, "old", false, "允许古役") + flag.BoolVar(&IsMajsoul, "majsoul", false, "雀魂助手") + flag.BoolVar(&IsTenhou, "tenhou", false, "天凤助手") + flag.BoolVar(&IsAnalysis, "analysis", false, "分析模式") + flag.BoolVar(&IsInteractive, "interactive", false, "交互模式") + flag.BoolVar(&IsInteractive, "i", false, "同 -interactive") + flag.BoolVar(&ShowImproveDetail, "detail", false, "显示改良细节") + flag.BoolVar(&ShowAgariAboveShanten1, "agari", false, "显示听牌前的估计和率") + flag.BoolVar(&ShowAgariAboveShanten1, "a", false, "同 -agari") + flag.BoolVar(&ShowScore, "score", false, "显示局收支") + flag.BoolVar(&ShowScore, "s", false, "同 -score") + flag.BoolVar(&ShowAllYakuTypes, "yaku", false, "显示所有役种") + flag.BoolVar(&ShowAllYakuTypes, "y", false, "同 -yaku") + flag.StringVar(&HumanDoraTiles, "dora", "", "指定哪些牌是宝牌") + flag.StringVar(&HumanDoraTiles, "d", "", "同 -dora") + flag.IntVar(&Port, "port", 12121, "指定服务端口") + flag.IntVar(&Port, "p", 12121, "同 -port") } const readmeURL = "https://github.com/EndlessCheng/mahjong-helper/blob/master/README.md" @@ -83,31 +97,22 @@ func welcome() int { fmt.Println("吐槽群:" + qqGroupNum) fmt.Println() - fmt.Println("请输入数字,选择对应网站:") - for i, cnt := 0, 0; cnt < len(platforms); i++ { - if platformInfo, ok := platforms[i]; ok { - info := platformInfo[0] + " [" + strings.Join(platformInfo[1:], ",") + "]" - fmt.Printf("%d - %s\n", i, info) - cnt++ - } +RenterPlatform: // wrong enter goto label + // print platforms + for _, element := range Platforms { + fmt.Printf("%d - %s %v\n", element.Code, element.Name, element.Type) } + fmt.Print("請選擇對應的網站(0或1),如未選擇則預設雀魂(1): ") - choose := defaultPlatform - fmt.Scanln(&choose) // 直接回车也无妨 - platformInfo, ok := platforms[choose] - var platformName string - if ok { - platformName = platformInfo[0] - } - if !ok { - choose = defaultPlatform - platformName = platforms[choose][0] - } + // set default value to int MahJongSoul(1) can exclude not int type + choose := MahJongSoul + fmt.Scanln(&choose) clearConsole() - color.HiGreen("已选择 - %s", platformName) - - if choose == platformMajsoul { + if choose == Tenhou { // choose TenHou + color.HiGreen("已選擇 - %s", Platforms[0].Name) + } else if choose == MahJongSoul { // choose MahJongSoul + color.HiGreen("已選擇 - %s", Platforms[1].Name) if len(gameConf.MajsoulAccountIDs) == 0 { color.HiYellow(` 提醒:首次启用时,请开启一局人机对战,或者重登游戏。 @@ -116,8 +121,11 @@ func welcome() int { 若助手无响应,请确认您已按步骤安装完成。 相关链接 ` + issueCommonQuestions) } + } else { // the choice not in selection + fmt.Printf("輸入錯誤,請重新輸入選擇\n\n") + // goto RenterPlatform label + goto RenterPlatform } - return choose } @@ -129,28 +137,28 @@ func main() { go checkNewVersion(version) } - util.SetConsiderOldYaku(considerOldYaku) + util.SetConsiderOldYaku(ConsiderOldYaku) humanTiles := strings.Join(flag.Args(), " ") humanTilesInfo := &model.HumanTilesInfo{ HumanTiles: humanTiles, - HumanDoraTiles: humanDoraTiles, + HumanDoraTiles: HumanDoraTiles, } var err error switch { - case isMajsoul: - err = runServer(true, port) - case isTenhou || isAnalysis: - err = runServer(true, port) - case isInteractive: // 交互模式 + case IsMajsoul: + err = runServer(true, Port) + case IsTenhou || IsAnalysis: + err = runServer(true, Port) + case IsInteractive: // 交互模式 err = interact(humanTilesInfo) case len(flag.Args()) > 0: // 静态分析 _, err = analysisHumanTiles(humanTilesInfo) default: // 服务器模式 choose := welcome() - isHTTPS := choose == platformMajsoul - err = runServer(isHTTPS, port) + isHTTPS := choose == MahJongSoul + err = runServer(isHTTPS, Port) } if err != nil { errorExit(err) diff --git a/platform/majsoul/api/liqi_test.go b/platform/majsoul/api/liqi_test.go index 2db4675..1216e00 100644 --- a/platform/majsoul/api/liqi_test.go +++ b/platform/majsoul/api/liqi_test.go @@ -4,12 +4,13 @@ import ( "crypto/hmac" "crypto/sha256" "fmt" - "github.com/EndlessCheng/mahjong-helper/platform/majsoul/proto/lq" - "github.com/EndlessCheng/mahjong-helper/platform/majsoul/tool" - "github.com/satori/go.uuid" "os" "testing" "time" + + "github.com/EndlessCheng/mahjong-helper/platform/majsoul/proto/lq" + "github.com/EndlessCheng/mahjong-helper/platform/majsoul/tool" + uuid "github.com/satori/go.uuid" ) func _genReqLogin(t *testing.T) *lq.ReqLogin { @@ -30,7 +31,8 @@ func _genReqLogin(t *testing.T) *lq.ReqLogin { // randomKey 最好是个固定值 randomKey, ok := os.LookupEnv("RANDOM_KEY") if !ok { - rawRandomKey, _ := uuid.NewV4() + // delete _ + rawRandomKey := uuid.NewV4() randomKey = rawRandomKey.String() } @@ -58,7 +60,8 @@ func _genReqLogin(t *testing.T) *lq.ReqLogin { func _genReqOauth2Login(t *testing.T, accessToken string) *lq.ReqOauth2Login { randomKey, ok := os.LookupEnv("RANDOM_KEY") if !ok { - rawRandomKey, _ := uuid.NewV4() + // delete _ + rawRandomKey := uuid.NewV4() randomKey = rawRandomKey.String() } From fab8836e583c92626b1b6d925c8e0518cb4e51be Mon Sep 17 00:00:00 2001 From: littletrainee Date: Mon, 13 Jun 2022 22:22:38 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=B0=87=E5=A4=A7=E9=83=A8=E5=88=86?= =?UTF-8?q?=E7=9A=84=E8=AE=8A=E6=95=B8=E8=88=87=E6=96=B9=E6=B3=95=E6=94=B9?= =?UTF-8?q?=E7=82=BA=E9=A7=9D=E5=B3=B0=E5=BC=8F=E7=9A=84=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E6=B3=95=EF=BC=8C=E4=BB=A5=E5=A2=9E=E5=8A=A0=E9=96=B1=E8=AE=80?= =?UTF-8?q?=E4=BE=BF=E5=88=A9=E6=80=A7=EF=BC=8C=20=E4=B8=A6=E4=BB=A5?= =?UTF-8?q?=E5=B0=8F=E5=AF=AB=E7=82=BAunexported(private)=EF=BC=8C?= =?UTF-8?q?=E5=A4=A7=E5=AF=AB=E7=82=BAexported(public)=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E9=A1=AF=E7=A4=BA=E5=AD=98=E8=A8=B1=E7=AF=84?= =?UTF-8?q?=E5=9C=8D=E6=AC=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analysis.go | 9 +- analysis_cache.go | 175 ++++++++--------- analysis_test.go | 3 +- cli.go | 4 +- config.go | 10 +- console.go | 4 +- core.go | 139 +++++++------- core_helper.go | 4 +- core_test.go | 35 ++-- interact.go | 9 +- main.go | 28 +-- majsoul.go | 137 +++++++------- majsoul_record.go | 31 +-- server.go | 466 ++++++++++++++++++++++++---------------------- server_test.go | 23 +-- tenhou.go | 163 ++++++++-------- tenhou_test.go | 27 +-- utils.go | 7 +- version.go | 17 +- version_test.go | 3 +- 20 files changed, 664 insertions(+), 630 deletions(-) diff --git a/analysis.go b/analysis.go index 9e3c178..bf64acb 100644 --- a/analysis.go +++ b/analysis.go @@ -1,11 +1,12 @@ package main import ( - "github.com/EndlessCheng/mahjong-helper/util" "fmt" "strings" - "github.com/fatih/color" + + "github.com/EndlessCheng/mahjong-helper/util" "github.com/EndlessCheng/mahjong-helper/util/model" + "github.com/fatih/color" ) func simpleBestDiscardTile(playerInfo *model.PlayerInfo) int { @@ -91,7 +92,7 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab printResults14WithRisk(incShantenResults14, mixedRiskTable) default: err := fmt.Errorf("参数错误: %d 张牌", countOfTiles) - if debugMode { + if DebugMode { panic(err) } return err @@ -153,7 +154,7 @@ func analysisMeld(playerInfo *model.PlayerInfo, targetTile34 int, isRedFive bool return nil } -func analysisHumanTiles(humanTilesInfo *model.HumanTilesInfo) (playerInfo *model.PlayerInfo, err error) { +func AnalysisHumanTiles(humanTilesInfo *model.HumanTilesInfo) (playerInfo *model.PlayerInfo, err error) { defer func() { if er := recover(); er != nil { err = er.(error) diff --git a/analysis_cache.go b/analysis_cache.go index be492c0..ce3531a 100644 --- a/analysis_cache.go +++ b/analysis_cache.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/EndlessCheng/mahjong-helper/util" "github.com/fatih/color" ) @@ -9,14 +10,14 @@ import ( type analysisOpType int const ( - analysisOpTypeTsumo analysisOpType = iota - analysisOpTypeChiPonKan // 吃 碰 明杠 - analysisOpTypeKan // 加杠 暗杠 + AnalysisOpTypeTsumo analysisOpType = iota + AnalysisOpTypeChiPonKan // 吃 碰 明杠 + AnalysisOpTypeKan // 加杠 暗杠 ) // TODO: 提醒「此处应该副露,不应跳过」 -type analysisCache struct { +type AnalysisCache struct { analysisOpType analysisOpType selfDiscardTile int @@ -36,28 +37,28 @@ type analysisCache struct { tenpaiRate []float64 // TODO: 三家听牌率 } -type roundAnalysisCache struct { - isStart bool - isEnd bool - cache []*analysisCache +type RoundAnalysisCache struct { + IsStart bool + IsEnd bool + Cache []*AnalysisCache - analysisCacheBeforeChiPon *analysisCache + analysisCacheBeforeChiPon *AnalysisCache } -func (rc *roundAnalysisCache) print() { +func (roundAnalysisCache *RoundAnalysisCache) Print() { const ( - baseInfo = "助手正在计算推荐舍牌,请稍等……(计算结果仅供参考)" - emptyInfo = "--" - sep = " " + BaseInfo = "助手正在计算推荐舍牌,请稍等……(计算结果仅供参考)" + EmptyInfo = "--" + Sep = " " ) - done := rc != nil && rc.isEnd + done := roundAnalysisCache != nil && roundAnalysisCache.IsEnd if !done { - color.HiGreen(baseInfo) + color.HiGreen(BaseInfo) } else { // 检查最后的是否自摸,若为自摸则去掉推荐 - if len(rc.cache) > 0 { - latestCache := rc.cache[len(rc.cache)-1] + if len(roundAnalysisCache.Cache) > 0 { + latestCache := roundAnalysisCache.Cache[len(roundAnalysisCache.Cache)-1] if latestCache.selfDiscardTile == -1 { latestCache.aiAttackDiscardTile = -1 latestCache.aiDefenceDiscardTile = -1 @@ -67,19 +68,19 @@ func (rc *roundAnalysisCache) print() { fmt.Print("巡目  ") if done { - for i := range rc.cache { - fmt.Printf("%s%2d", sep, i+1) + for i := range roundAnalysisCache.Cache { + fmt.Printf("%s%2d", Sep, i+1) } } fmt.Println() printTileInfo := func(tile int, risk float64, suffix string) { - info := emptyInfo + info := EmptyInfo if tile != -1 { info = util.Mahjong[tile] } - fmt.Print(sep) - if info == emptyInfo || risk < 5 { + fmt.Print(Sep) + if info == EmptyInfo || risk < 5 { fmt.Print(info) } else { color.New(getNumRiskColor(risk)).Print(info) @@ -89,11 +90,11 @@ func (rc *roundAnalysisCache) print() { fmt.Print("自家切牌") if done { - for i, c := range rc.cache { + for i, c := range roundAnalysisCache.Cache { suffix := "" if c.isRiichiWhenDiscard { suffix = "[立直]" - } else if c.selfDiscardTile == -1 && i == len(rc.cache)-1 { + } else if c.selfDiscardTile == -1 && i == len(roundAnalysisCache.Cache)-1 { //suffix = "[自摸]" // TODO: 流局 } @@ -104,7 +105,7 @@ func (rc *roundAnalysisCache) print() { fmt.Print("进攻推荐") if done { - for _, c := range rc.cache { + for _, c := range roundAnalysisCache.Cache { printTileInfo(c.aiAttackDiscardTile, c.aiAttackDiscardTileRisk, "") } } @@ -112,7 +113,7 @@ func (rc *roundAnalysisCache) print() { fmt.Print("防守推荐") if done { - for _, c := range rc.cache { + for _, c := range roundAnalysisCache.Cache { printTileInfo(c.aiDefenceDiscardTile, c.aiDefenceDiscardTileRisk, "") } } @@ -122,18 +123,18 @@ func (rc *roundAnalysisCache) print() { } // (摸牌后、鸣牌后的)实际舍牌 -func (rc *roundAnalysisCache) addSelfDiscardTile(tile int, risk float64, isRiichiWhenDiscard bool) { - latestCache := rc.cache[len(rc.cache)-1] +func (rc *RoundAnalysisCache) AddSelfDiscardTile(tile int, risk float64, isRiichiWhenDiscard bool) { + latestCache := rc.Cache[len(rc.Cache)-1] latestCache.selfDiscardTile = tile latestCache.selfDiscardTileRisk = risk latestCache.isRiichiWhenDiscard = isRiichiWhenDiscard } // 摸牌时的切牌推荐 -func (rc *roundAnalysisCache) addAIDiscardTileWhenDrawTile(attackTile int, defenceTile int, attackTileRisk float64, defenceDiscardTileRisk float64) { +func (rc *RoundAnalysisCache) AddAIDiscardTileWhenDrawTile(attackTile int, defenceTile int, attackTileRisk float64, defenceDiscardTileRisk float64) { // 摸牌,巡目+1 - rc.cache = append(rc.cache, &analysisCache{ - analysisOpType: analysisOpTypeTsumo, + rc.Cache = append(rc.Cache, &AnalysisCache{ + analysisOpType: AnalysisOpTypeTsumo, selfDiscardTile: -1, aiAttackDiscardTile: attackTile, aiDefenceDiscardTile: defenceTile, @@ -144,47 +145,47 @@ func (rc *roundAnalysisCache) addAIDiscardTileWhenDrawTile(attackTile int, defen } // 加杠 暗杠 -func (rc *roundAnalysisCache) addKan(meldType int) { +func (rc *RoundAnalysisCache) AddKan(meldType int) { // latestCache 是摸牌 - latestCache := rc.cache[len(rc.cache)-1] - latestCache.analysisOpType = analysisOpTypeKan + latestCache := rc.Cache[len(rc.Cache)-1] + latestCache.analysisOpType = AnalysisOpTypeKan latestCache.meldType = meldType // 杠完之后又会摸牌,巡目+1 } // 吃 碰 明杠 -func (rc *roundAnalysisCache) addChiPonKan(meldType int) { +func (rc *RoundAnalysisCache) AddChiPonKan(meldType int) { if meldType == meldTypeMinkan { // 暂时忽略明杠,巡目不+1,留给摸牌时+1 return } // 巡目+1 - var newCache *analysisCache + var newCache *AnalysisCache if rc.analysisCacheBeforeChiPon != nil { newCache = rc.analysisCacheBeforeChiPon // 见 addPossibleChiPonKan - newCache.analysisOpType = analysisOpTypeChiPonKan + newCache.analysisOpType = AnalysisOpTypeChiPonKan newCache.meldType = meldType rc.analysisCacheBeforeChiPon = nil } else { // 此处代码应该不会触发 - if debugMode { + if DebugMode { panic("rc.analysisCacheBeforeChiPon == nil") } - newCache = &analysisCache{ - analysisOpType: analysisOpTypeChiPonKan, + newCache = &AnalysisCache{ + analysisOpType: AnalysisOpTypeChiPonKan, selfDiscardTile: -1, aiAttackDiscardTile: -1, aiDefenceDiscardTile: -1, meldType: meldType, } } - rc.cache = append(rc.cache, newCache) + rc.Cache = append(rc.Cache, newCache) } // 吃 碰 杠 跳过 -func (rc *roundAnalysisCache) addPossibleChiPonKan(attackTile int, attackTileRisk float64) { - rc.analysisCacheBeforeChiPon = &analysisCache{ - analysisOpType: analysisOpTypeChiPonKan, +func (rc *RoundAnalysisCache) AddPossibleChiPonKan(attackTile int, attackTileRisk float64) { + rc.analysisCacheBeforeChiPon = &AnalysisCache{ + analysisOpType: AnalysisOpTypeChiPonKan, selfDiscardTile: -1, aiAttackDiscardTile: attackTile, aiDefenceDiscardTile: -1, @@ -194,24 +195,24 @@ func (rc *roundAnalysisCache) addPossibleChiPonKan(attackTile int, attackTileRis // -type gameAnalysisCache struct { +type GameAnalysisCache struct { // 局数 本场数 - wholeGameCache [][]*roundAnalysisCache + WholeGameCache [][]*RoundAnalysisCache - majsoulRecordUUID string + MahJongSoulRecordUUID string - selfSeat int + SelfSeat int } -func newGameAnalysisCache(majsoulRecordUUID string, selfSeat int) *gameAnalysisCache { - cache := make([][]*roundAnalysisCache, 3*4) // 最多到西四 +func newGameAnalysisCache(majsoulRecordUUID string, selfSeat int) *GameAnalysisCache { + cache := make([][]*RoundAnalysisCache, 3*4) // 最多到西四 for i := range cache { - cache[i] = make([]*roundAnalysisCache, 100) // 最多连庄 + cache[i] = make([]*RoundAnalysisCache, 100) // 最多连庄 } - return &gameAnalysisCache{ - wholeGameCache: cache, - majsoulRecordUUID: majsoulRecordUUID, - selfSeat: selfSeat, + return &GameAnalysisCache{ + WholeGameCache: cache, + MahJongSoulRecordUUID: majsoulRecordUUID, + SelfSeat: selfSeat, } } @@ -219,31 +220,31 @@ func newGameAnalysisCache(majsoulRecordUUID string, selfSeat int) *gameAnalysisC // TODO: 重构成 struct var ( - _analysisCacheList = make([]*gameAnalysisCache, 4) - _currentSeat int + analysisCacheList = make([]*GameAnalysisCache, 4) + currentSeat int ) -func resetAnalysisCache() { - _analysisCacheList = make([]*gameAnalysisCache, 4) +func ResetAnalysisCache() { + analysisCacheList = make([]*GameAnalysisCache, 4) } -func setAnalysisCache(analysisCache *gameAnalysisCache) { - _analysisCacheList[analysisCache.selfSeat] = analysisCache - _currentSeat = analysisCache.selfSeat +func SetAnalysisCache(analysisCache *GameAnalysisCache) { + analysisCacheList[analysisCache.SelfSeat] = analysisCache + currentSeat = analysisCache.SelfSeat } -func getAnalysisCache(seat int) *gameAnalysisCache { +func GetAnalysisCache(seat int) *GameAnalysisCache { if seat == -1 { return nil } - return _analysisCacheList[seat] + return analysisCacheList[seat] } -func getCurrentAnalysisCache() *gameAnalysisCache { - return getAnalysisCache(_currentSeat) +func GetCurrentAnalysisCache() *GameAnalysisCache { + return GetAnalysisCache(currentSeat) } -func (c *gameAnalysisCache) runMajsoulRecordAnalysisTask(actions majsoulRoundActions) error { +func (cache *GameAnalysisCache) RunMahJongSoulRecordAnalysisTask(actions MahJongSoulRoundActions) error { // 从第一个 action 中取出局和场 if len(actions) == 0 { return fmt.Errorf("数据异常:此局数据为空") @@ -253,15 +254,15 @@ func (c *gameAnalysisCache) runMajsoulRecordAnalysisTask(actions majsoulRoundAct data := newRoundAction.Action roundNumber := 4*(*data.Chang) + *data.Ju ben := *data.Ben - roundCache := c.wholeGameCache[roundNumber][ben] // TODO: 建议用原子操作 + roundCache := cache.WholeGameCache[roundNumber][ben] // TODO: 建议用原子操作 if roundCache == nil { - roundCache = &roundAnalysisCache{isStart: true} - if debugMode { + roundCache = &RoundAnalysisCache{IsStart: true} + if DebugMode { fmt.Println("助手正在计算推荐舍牌…… 创建 roundCache") } - c.wholeGameCache[roundNumber][ben] = roundCache - } else if roundCache.isStart { - if debugMode { + cache.WholeGameCache[roundNumber][ben] = roundCache + } else if roundCache.IsStart { + if DebugMode { fmt.Println("无需重复计算") } return nil @@ -271,35 +272,35 @@ func (c *gameAnalysisCache) runMajsoulRecordAnalysisTask(actions majsoulRoundAct // 若为摸牌操作,计算出此时的 AI 进攻舍牌和防守舍牌 // 若为鸣牌操作,计算出此时的 AI 进攻舍牌(无进攻舍牌则设为 -1),防守舍牌设为 -1 // TODO: 玩家跳过,但是 AI 觉得应鸣牌? - majsoulRoundData := &majsoulRoundData{selfSeat: c.selfSeat} // 注意这里是用的一个新的 majsoulRoundData 去计算的,不会有数据冲突 - majsoulRoundData.roundData = newGame(majsoulRoundData) - majsoulRoundData.roundData.gameMode = gameModeRecordCache - majsoulRoundData.skipOutput = true + majsoulRoundData := &MahJongSoulRoundData{SelfSeat: cache.SelfSeat} // 注意这里是用的一个新的 majsoulRoundData 去计算的,不会有数据冲突 + majsoulRoundData.RoundData = NewGame(majsoulRoundData) + majsoulRoundData.RoundData.GameMode = GameModeRecordCache + majsoulRoundData.SkipOutput = true for i, action := range actions[:len(actions)-1] { - if c.majsoulRecordUUID != getMajsoulCurrentRecordUUID() { - if debugMode { + if cache.MahJongSoulRecordUUID != GetMajsoulCurrentRecordUUID() { + if DebugMode { fmt.Println("用户退出该牌谱") } // 提前退出,减少不必要的计算 return nil } - if debugMode { + if DebugMode { fmt.Println("助手正在计算推荐舍牌…… action", i) } - majsoulRoundData.msg = action.Action - majsoulRoundData.analysis() + majsoulRoundData.Message = action.Action + majsoulRoundData.Analysis() } - roundCache.isEnd = true + roundCache.IsEnd = true - if c.majsoulRecordUUID != getMajsoulCurrentRecordUUID() { - if debugMode { + if cache.MahJongSoulRecordUUID != GetMajsoulCurrentRecordUUID() { + if DebugMode { fmt.Println("用户退出该牌谱") } return nil } - clearConsole() - roundCache.print() + ClearConsole() + roundCache.Print() return nil } diff --git a/analysis_test.go b/analysis_test.go index d2aac4f..e5be0c0 100644 --- a/analysis_test.go +++ b/analysis_test.go @@ -2,6 +2,7 @@ package main import ( "testing" + "github.com/EndlessCheng/mahjong-helper/util/model" ) @@ -57,7 +58,7 @@ func TestAnalysis(t *testing.T) { raw = "23777m 45677s # 777p + 7s" // *片听 raw = "456789m 1123678p 6z" raw = "44779m 889p 78s # 666z + 8p?" - if _, err := analysisHumanTiles(model.NewSimpleHumanTilesInfo(raw)); err != nil { + if _, err := AnalysisHumanTiles(model.NewSimpleHumanTilesInfo(raw)); err != nil { t.Fatal(err) } } diff --git a/cli.go b/cli.go index 8eb6548..fe945b0 100644 --- a/cli.go +++ b/cli.go @@ -192,7 +192,7 @@ func (l riskInfoList) printWithHands(hands []int, leftCounts []int) { names := []string{"", "下家", "对家", "上家"} for i := len(l) - 1; i >= 1; i-- { tenpaiRate := l[i].tenpaiRate - if len(l[i].riskTable) > 0 && (debugMode || tenpaiRate > minShownTenpaiRate) { + if len(l[i].riskTable) > 0 && (DebugMode || tenpaiRate > minShownTenpaiRate) { dangerousPlayerCount++ fmt.Print(names[i] + "安牌:") //if debugMode { @@ -584,7 +584,7 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() { if len(result13.YakuTypes) > 0 { // 役种(两向听以内开启显示) if result13.Shanten <= 2 { - if !ShowAllYakuTypes && !debugMode { + if !ShowAllYakuTypes && !DebugMode { shownYakuTypes := []int{} for yakuType := range result13.YakuTypes { for _, yt := range yakuTypesToAlert { diff --git a/config.go b/config.go index 73ccb5a..2be3a51 100644 --- a/config.go +++ b/config.go @@ -1,9 +1,9 @@ package main import ( - "io/ioutil" - "encoding/json" "bytes" + "encoding/json" + "io/ioutil" "os" ) @@ -30,14 +30,14 @@ func init() { data, err := ioutil.ReadFile(configFile) if err != nil { - if debugMode { + if DebugMode { panic(err) } return } if err := json.NewDecoder(bytes.NewReader(data)).Decode(gameConf); err != nil { - if debugMode { + if DebugMode { panic(err) } return @@ -76,4 +76,4 @@ func (c *gameConfig) addMajsoulAccountID(majsoulAccountID int) error { func (c *gameConfig) setMajsoulAccountID(majsoulAccountID int) { c.currentActiveMajsoulAccountID = majsoulAccountID -} \ No newline at end of file +} diff --git a/console.go b/console.go index e177ce1..76a9fc4 100644 --- a/console.go +++ b/console.go @@ -1,8 +1,8 @@ package main import ( - "os/exec" "os" + "os/exec" "runtime" ) @@ -23,7 +23,7 @@ func init() { } } -func clearConsole() { +func ClearConsole() { if clearFunc, ok := clearFuncMap[runtime.GOOS]; ok { clearFunc() } diff --git a/core.go b/core.go index 3ec5f60..2c49b56 100644 --- a/core.go +++ b/core.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/EndlessCheng/mahjong-helper/util" "github.com/EndlessCheng/mahjong-helper/util/model" "github.com/fatih/color" @@ -166,12 +167,12 @@ func (p *playerInfo) doraNum(doraList []int) (doraCount int) { // -type roundData struct { +type RoundData struct { parser DataParser - gameMode gameMode + GameMode gameMode - skipOutput bool + SkipOutput bool // 玩家数,3 为三麻,4 为四麻 playerNumber int @@ -211,7 +212,7 @@ type roundData struct { players []*playerInfo } -func newRoundData(parser DataParser, roundNumber int, benNumber int, dealer int) *roundData { +func newRoundData(parser DataParser, roundNumber int, benNumber int, dealer int) *RoundData { // 无论是三麻还是四麻,都视作四个人 const playerNumber = 4 roundWindTile := 27 + roundNumber/playerNumber @@ -219,7 +220,7 @@ func newRoundData(parser DataParser, roundNumber int, benNumber int, dealer int) for i := 0; i < playerNumber; i++ { playerWindTile[i] = 27 + (playerNumber-dealer+i)%playerNumber } - return &roundData{ + return &RoundData{ parser: parser, roundNumber: roundNumber, benNumber: benNumber, @@ -238,18 +239,18 @@ func newRoundData(parser DataParser, roundNumber int, benNumber int, dealer int) } } -func newGame(parser DataParser) *roundData { +func NewGame(parser DataParser) *RoundData { return newRoundData(parser, 0, 0, 0) } // 新的一局 -func (d *roundData) reset(roundNumber int, benNumber int, dealer int) { - skipOutput := d.skipOutput - gameMode := d.gameMode +func (d *RoundData) reset(roundNumber int, benNumber int, dealer int) { + skipOutput := d.SkipOutput + gameMode := d.GameMode playerNumber := d.playerNumber newData := newRoundData(d.parser, roundNumber, benNumber, dealer) - newData.skipOutput = skipOutput - newData.gameMode = gameMode + newData.SkipOutput = skipOutput + newData.GameMode = gameMode newData.playerNumber = playerNumber if playerNumber == 3 { // 三麻没有 2-8m @@ -261,15 +262,15 @@ func (d *roundData) reset(roundNumber int, benNumber int, dealer int) { *d = *newData } -func (d *roundData) newGame() { +func (d *RoundData) newGame() { d.reset(0, 0, 0) } -func (d *roundData) descLeftCounts(tile int) { +func (d *RoundData) descLeftCounts(tile int) { d.leftCounts[tile]-- if d.leftCounts[tile] < 0 { info := fmt.Sprintf("数据异常: %s 数量为 %d", util.MahjongZH[tile], d.leftCounts[tile]) - if debugMode { + if DebugMode { panic(info) } else { fmt.Println(info) @@ -278,11 +279,11 @@ func (d *roundData) descLeftCounts(tile int) { } // 杠! -func (d *roundData) newDora(kanDoraIndicator int) { +func (d *RoundData) newDora(kanDoraIndicator int) { d.doraIndicators = append(d.doraIndicators, kanDoraIndicator) d.descLeftCounts(kanDoraIndicator) - if d.skipOutput { + if d.SkipOutput { return } @@ -290,11 +291,11 @@ func (d *roundData) newDora(kanDoraIndicator int) { } // 根据宝牌指示牌计算出宝牌 -func (d *roundData) doraList() (dl []int) { +func (d *RoundData) doraList() (dl []int) { return model.DoraList(d.doraIndicators, d.playerNumber == 3) } -func (d *roundData) printDiscards() { +func (d *RoundData) printDiscards() { // 三麻的北家是不需要打印的 for i := len(d.players) - 1; i >= 1; i-- { if player := d.players[i]; d.playerNumber != 3 || player.selfWindTile != 30 { @@ -305,7 +306,7 @@ func (d *roundData) printDiscards() { // 分析34种牌的危险度 // 可以用来判断自家手牌的安全度,以及他家是否在进攻(多次切出危险度高的牌) -func (d *roundData) analysisTilesRisk() (riList riskInfoList) { +func (d *RoundData) analysisTilesRisk() (riList riskInfoList) { riList = make(riskInfoList, len(d.players)) for who := range riList { riList[who] = &riskInfo{ @@ -415,7 +416,7 @@ func (d *roundData) analysisTilesRisk() (riList riskInfoList) { } // TODO: 特殊处理w立直 -func (d *roundData) isPlayerDaburii(who int) bool { +func (d *RoundData) isPlayerDaburii(who int) bool { // w立直成立的前提是没有任何玩家副露 for _, p := range d.players { if len(p.melds) > 0 { @@ -430,7 +431,7 @@ func (d *roundData) isPlayerDaburii(who int) bool { } // 自家的 PlayerInfo -func (d *roundData) newModelPlayerInfo() *model.PlayerInfo { +func (d *RoundData) newModelPlayerInfo() *model.PlayerInfo { const wannpaiTilesCount = 14 leftDrawTilesCount := util.CountOfTiles34(d.leftCounts) - (wannpaiTilesCount - len(d.doraIndicators)) for _, player := range d.players[1:] { @@ -469,8 +470,8 @@ func (d *roundData) newModelPlayerInfo() *model.PlayerInfo { } } -func (d *roundData) analysis() error { - if !debugMode { +func (d *RoundData) Analysis() error { + if !DebugMode { defer func() { if err := recover(); err != nil { fmt.Println("内部错误:", err) @@ -478,7 +479,7 @@ func (d *roundData) analysis() error { }() } - if debugMode { + if DebugMode { if msg := d.parser.GetMessage(); len(msg) > 0 { const printLimit = 500 if len(msg) > printLimit { @@ -503,32 +504,32 @@ func (d *roundData) analysis() error { return nil } - if debugMode { + if DebugMode { fmt.Println("当前座位为", d.parser.GetSelfSeat()) } - var currentRoundCache *roundAnalysisCache - if analysisCache := getAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { - currentRoundCache = analysisCache.wholeGameCache[d.roundNumber][d.benNumber] + var currentRoundCache *RoundAnalysisCache + if analysisCache := GetAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { + currentRoundCache = analysisCache.WholeGameCache[d.roundNumber][d.benNumber] } switch { case d.parser.IsInit(): // round 开始/重连 - if !debugMode && !d.skipOutput { - clearConsole() + if !DebugMode && !d.SkipOutput { + ClearConsole() } roundNumber, benNumber, dealer, doraIndicators, hands, numRedFives := d.parser.ParseInit() switch d.parser.GetDataSourceType() { case dataSourceTypeTenhou: d.reset(roundNumber, benNumber, dealer) - d.gameMode = gameModeMatch // TODO: 牌谱模式? + d.GameMode = gameModeMatch // TODO: 牌谱模式? case dataSourceTypeMajsoul: if dealer != -1 { // 先就坐,还没洗牌呢~ // 设置第一局的 dealer d.reset(0, 0, dealer) - d.gameMode = gameModeMatch + d.GameMode = gameModeMatch fmt.Printf("游戏即将开始,您分配到的座位是:") color.HiGreen(util.MahjongZH[d.players[0].selfWindTile]) return nil @@ -543,8 +544,8 @@ func (d *roundData) analysis() error { } // 由于 reset 了,重新获取 currentRoundCache - if analysisCache := getAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { - currentRoundCache = analysisCache.wholeGameCache[d.roundNumber][d.benNumber] + if analysisCache := GetAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { + currentRoundCache = analysisCache.WholeGameCache[d.roundNumber][d.benNumber] } d.doraIndicators = doraIndicators @@ -560,17 +561,17 @@ func (d *roundData) analysis() error { playerInfo := d.newModelPlayerInfo() // 牌谱分析模式下,记录舍牌推荐 - if d.gameMode == gameModeRecordCache && len(hands) == 14 { - currentRoundCache.addAIDiscardTileWhenDrawTile(simpleBestDiscardTile(playerInfo), -1, 0, 0) + if d.GameMode == GameModeRecordCache && len(hands) == 14 { + currentRoundCache.AddAIDiscardTileWhenDrawTile(simpleBestDiscardTile(playerInfo), -1, 0, 0) } - if d.skipOutput { + if d.SkipOutput { return nil } // 牌谱模式下,打印舍牌推荐 - if d.gameMode == gameModeRecord { - currentRoundCache.print() + if d.GameMode == gameModeRecord { + currentRoundCache.Print() } color.New(color.FgHiGreen).Printf("%s", util.MahjongZH[d.roundWindTile]) @@ -618,8 +619,8 @@ func (d *roundData) analysis() error { // 由于均为自家操作,宝牌数是不变的 // 牌谱分析模式下,记录加杠操作 - if d.gameMode == gameModeRecordCache { - currentRoundCache.addKan(meldType) + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddKan(meldType) } } // 修改原副露 @@ -633,7 +634,7 @@ func (d *roundData) analysis() error { } } - if debugMode { + if DebugMode { if who == 0 { if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) @@ -662,8 +663,8 @@ func (d *roundData) analysis() error { d.counts[meldTiles[0]] = 0 // 牌谱分析模式下,记录暗杠操作 - if d.gameMode == gameModeRecordCache { - currentRoundCache.addKan(meldType) + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddKan(meldType) } } else { d.counts[calledTile]++ @@ -676,12 +677,12 @@ func (d *roundData) analysis() error { } // 牌谱分析模式下,记录吃碰明杠操作 - if d.gameMode == gameModeRecordCache { - currentRoundCache.addChiPonKan(meldType) + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddChiPonKan(meldType) } } - if debugMode { + if DebugMode { if meldType == meldTypeMinkan || meldType == meldTypeAnkan { if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) @@ -709,7 +710,7 @@ func (d *roundData) analysis() error { // // 重连 case d.parser.IsFuriten(): // 振听 - if d.skipOutput { + if d.SkipOutput { return nil } color.HiYellow("振听") @@ -718,8 +719,8 @@ func (d *roundData) analysis() error { //case "HELO", "RANKING", "TAIKYOKU", "UN", "LN", "SAIKAI": // // 其他 case d.parser.IsSelfDraw(): - if !debugMode && !d.skipOutput { - clearConsole() + if !DebugMode && !d.SkipOutput { + ClearConsole() } // 自家(从牌山 d.leftCounts)摸牌(至手牌 d.counts) tile, isRedFive, kanDoraIndicator := d.parser.ParseSelfDraw() @@ -739,7 +740,7 @@ func (d *roundData) analysis() error { mixedRiskTable := riskTables.mixedRiskTable() // 牌谱分析模式下,记录舍牌推荐 - if d.gameMode == gameModeRecordCache { + if d.GameMode == GameModeRecordCache { bestAttackDiscardTile := simpleBestDiscardTile(playerInfo) bestDefenceDiscardTile := mixedRiskTable.getBestDefenceTile(playerInfo.HandTiles34) bestAttackDiscardTileRisk, bestDefenceDiscardTileRisk := 0.0, 0.0 @@ -747,16 +748,16 @@ func (d *roundData) analysis() error { bestAttackDiscardTileRisk = mixedRiskTable[bestAttackDiscardTile] bestDefenceDiscardTileRisk = mixedRiskTable[bestDefenceDiscardTile] } - currentRoundCache.addAIDiscardTileWhenDrawTile(bestAttackDiscardTile, bestDefenceDiscardTile, bestAttackDiscardTileRisk, bestDefenceDiscardTileRisk) + currentRoundCache.AddAIDiscardTileWhenDrawTile(bestAttackDiscardTile, bestDefenceDiscardTile, bestAttackDiscardTileRisk, bestDefenceDiscardTileRisk) } - if d.skipOutput { + if d.SkipOutput { return nil } // 牌谱模式下,打印舍牌推荐 - if d.gameMode == gameModeRecord { - currentRoundCache.print() + if d.GameMode == gameModeRecord { + currentRoundCache.Print() } // 打印他家舍牌信息 @@ -799,11 +800,11 @@ func (d *roundData) analysis() error { } // 牌谱分析模式下,记录自家舍牌 - if d.gameMode == gameModeRecordCache { - currentRoundCache.addSelfDiscardTile(discardTile, mixedRiskTable[discardTile], isReach) + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddSelfDiscardTile(discardTile, mixedRiskTable[discardTile], isReach) } - if debugMode { + if DebugMode { if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) } @@ -833,7 +834,7 @@ func (d *roundData) analysis() error { player.reachTileAtGlobal = len(d.globalDiscardTiles) - 1 player.reachTileAt = len(player.discardTiles) - 1 // 若该玩家摸切立直,打印提示信息 - if isTsumogiri && !d.skipOutput { + if isTsumogiri && !d.SkipOutput { color.HiYellow("%s 摸切立直!", player.name) } } else if len(player.meldDiscardsAt) != len(player.melds) { @@ -856,7 +857,7 @@ func (d *roundData) analysis() error { mixedRiskTable := riskTables.mixedRiskTable() // 牌谱分析模式下,记录可能的鸣牌 - if d.gameMode == gameModeRecordCache { + if d.GameMode == GameModeRecordCache { allowChi := who == 3 _, results14, incShantenResults14 := util.CalculateMeld(playerInfo, discardTile, isRedFive, allowChi) bestAttackDiscardTile := -1 @@ -871,11 +872,11 @@ func (d *roundData) analysis() error { if bestDefenceDiscardTile >= 0 { bestAttackDiscardTileRisk = mixedRiskTable[bestAttackDiscardTile] } - currentRoundCache.addPossibleChiPonKan(bestAttackDiscardTile, bestAttackDiscardTileRisk) + currentRoundCache.AddPossibleChiPonKan(bestAttackDiscardTile, bestAttackDiscardTileRisk) } } - if d.skipOutput { + if d.SkipOutput { return nil } @@ -884,13 +885,13 @@ func (d *roundData) analysis() error { // return nil //} - if !debugMode { - clearConsole() + if !DebugMode { + ClearConsole() } // 牌谱模式下,打印舍牌推荐 - if d.gameMode == gameModeRecord { - currentRoundCache.print() + if d.GameMode == gameModeRecord { + currentRoundCache.Print() } // 打印他家舍牌信息 @@ -898,7 +899,7 @@ func (d *roundData) analysis() error { fmt.Println() riskTables.printWithHands(d.counts, d.leftCounts) - if d.gameMode == gameModeMatch && !canBeMeld { + if d.GameMode == gameModeMatch && !canBeMeld { return nil } @@ -909,8 +910,8 @@ func (d *roundData) analysis() error { case d.parser.IsRoundWin(): // TODO: 解析天凤牌谱 - 注意 skipOutput - if !debugMode { - clearConsole() + if !DebugMode { + ClearConsole() } fmt.Println("和牌,本局结束") whos, points := d.parser.ParseRoundWin() diff --git a/core_helper.go b/core_helper.go index de5a30d..e5ed9b6 100644 --- a/core_helper.go +++ b/core_helper.go @@ -1,6 +1,6 @@ package main -var debugMode = false +var DebugMode = false type gameMode int @@ -8,7 +8,7 @@ const ( // TODO: 感觉有点杂乱需要重构 gameModeMatch gameMode = iota // 对战 - IsInit gameModeRecord // 解析牌谱 - gameModeRecordCache // 解析牌谱 - runMajsoulRecordAnalysisTask + GameModeRecordCache // 解析牌谱 - runMajsoulRecordAnalysisTask gameModeLive // 解析观战 ) diff --git a/core_test.go b/core_test.go index 43209cc..aa65575 100644 --- a/core_test.go +++ b/core_test.go @@ -1,17 +1,18 @@ package main import ( - "testing" + "encoding/json" + "fmt" "io/ioutil" "strings" - "fmt" - "encoding/json" + "testing" + "github.com/EndlessCheng/mahjong-helper/util/debug" "github.com/stretchr/testify/assert" ) func Test_majsoul_analysis(t *testing.T) { - debugMode = true + DebugMode = true logFile := "log/gamedata-x.log" logData, err := ioutil.ReadFile(logFile) @@ -28,8 +29,8 @@ func Test_majsoul_analysis(t *testing.T) { startLo := -1 endLo := -1 - majsoulRoundData := &majsoulRoundData{} - majsoulRoundData.roundData = newGame(majsoulRoundData) + majsoulRoundData := &MahJongSoulRoundData{} + majsoulRoundData.RoundData = NewGame(majsoulRoundData) lines := strings.Split(string(logData), "\n") if startLo == -1 { @@ -63,22 +64,22 @@ func Test_majsoul_analysis(t *testing.T) { } msg := s.Message - d := majsoulMessage{} + d := MaJongSoulMessage{} if err := json.Unmarshal([]byte(msg), &d); err != nil { fmt.Println(err) continue } - majsoulRoundData.msg = &d - majsoulRoundData.originJSON = msg - if err := majsoulRoundData.analysis(); err != nil { + majsoulRoundData.Message = &d + majsoulRoundData.OriginJSON = msg + if err := majsoulRoundData.Analysis(); err != nil { fmt.Println("错误:", err) } } } func Test_tenhou_analysis(t *testing.T) { - debugMode = true + DebugMode = true logFile := "log/gamedata-20190715-201349.log" logData, err := ioutil.ReadFile(logFile) @@ -95,8 +96,8 @@ func Test_tenhou_analysis(t *testing.T) { startLo := -1 endLo := -1 - tenhouRoundData := &tenhouRoundData{isRoundEnd: true} - tenhouRoundData.roundData = newGame(tenhouRoundData) + tenhouRoundData := &TenHouRoundData{IsRoundEnd: true} + tenhouRoundData.RoundData = NewGame(tenhouRoundData) lines := strings.Split(string(logData), "\n") if startLo == -1 { @@ -130,15 +131,15 @@ func Test_tenhou_analysis(t *testing.T) { } msg := s.Message - d := tenhouMessage{} + d := TenhouMessage{} if err := json.Unmarshal([]byte(msg), &d); err != nil { fmt.Println(err) continue } - tenhouRoundData.msg = &d - tenhouRoundData.originJSON = msg - if err := tenhouRoundData.analysis(); err != nil { + tenhouRoundData.Msg = &d + tenhouRoundData.OriginJSON = msg + if err := tenhouRoundData.Analysis(); err != nil { fmt.Println("错误:", err) } } diff --git a/interact.go b/interact.go index 012188c..3682b9f 100644 --- a/interact.go +++ b/interact.go @@ -1,14 +1,15 @@ package main import ( - "github.com/EndlessCheng/mahjong-helper/util" "fmt" "os" + + "github.com/EndlessCheng/mahjong-helper/util" "github.com/EndlessCheng/mahjong-helper/util/model" ) -func interact(humanTilesInfo *model.HumanTilesInfo) error { - if !debugMode { +func Interact(humanTilesInfo *model.HumanTilesInfo) error { + if !DebugMode { defer func() { if err := recover(); err != nil { fmt.Println("内部错误:", err) @@ -16,7 +17,7 @@ func interact(humanTilesInfo *model.HumanTilesInfo) error { }() } - playerInfo, err := analysisHumanTiles(humanTilesInfo) + playerInfo, err := AnalysisHumanTiles(humanTilesInfo) if err != nil { return err } diff --git a/main.go b/main.go index a10d74f..769c093 100644 --- a/main.go +++ b/main.go @@ -108,7 +108,7 @@ RenterPlatform: // wrong enter goto label choose := MahJongSoul fmt.Scanln(&choose) - clearConsole() + ClearConsole() if choose == Tenhou { // choose TenHou color.HiGreen("已選擇 - %s", Platforms[0].Name) } else if choose == MahJongSoul { // choose MahJongSoul @@ -132,35 +132,35 @@ RenterPlatform: // wrong enter goto label func main() { flag.Parse() - color.HiGreen("日本麻将助手 %s (by EndlessCheng)", version) - if version != versionDev { - go checkNewVersion(version) + color.HiGreen("日本麻将助手 %s (by EndlessCheng)", Version) + if Version != VersionDev { + go CheckNewVersion(Version) } util.SetConsiderOldYaku(ConsiderOldYaku) - humanTiles := strings.Join(flag.Args(), " ") - humanTilesInfo := &model.HumanTilesInfo{ - HumanTiles: humanTiles, + HumanTiles := strings.Join(flag.Args(), " ") + HumanTilesInfo := &model.HumanTilesInfo{ + HumanTiles: HumanTiles, HumanDoraTiles: HumanDoraTiles, } var err error switch { case IsMajsoul: - err = runServer(true, Port) + err = RunServer(true, Port) case IsTenhou || IsAnalysis: - err = runServer(true, Port) + err = RunServer(true, Port) case IsInteractive: // 交互模式 - err = interact(humanTilesInfo) + err = Interact(HumanTilesInfo) case len(flag.Args()) > 0: // 静态分析 - _, err = analysisHumanTiles(humanTilesInfo) + _, err = AnalysisHumanTiles(HumanTilesInfo) default: // 服务器模式 choose := welcome() - isHTTPS := choose == MahJongSoul - err = runServer(isHTTPS, Port) + IsHTTPS := choose == MahJongSoul + err = RunServer(IsHTTPS, Port) } if err != nil { - errorExit(err) + ErrorExit(err) } } diff --git a/majsoul.go b/majsoul.go index ba9158a..6d8b3ae 100644 --- a/majsoul.go +++ b/majsoul.go @@ -2,15 +2,16 @@ package main import ( "fmt" - "github.com/fatih/color" - "github.com/EndlessCheng/mahjong-helper/util" - "github.com/EndlessCheng/mahjong-helper/util/model" "sort" "time" + "github.com/EndlessCheng/mahjong-helper/platform/majsoul/proto/lq" + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/EndlessCheng/mahjong-helper/util/model" + "github.com/fatih/color" ) -type majsoulMessage struct { +type MaJongSoulMessage struct { // 对应到服务器用户数据库中的ID,该值越小表示您的注册时间越早 AccountID int `json:"account_id"` @@ -18,16 +19,16 @@ type majsoulMessage struct { Friends lq.FriendList `json:"friends"` // 新获取到的牌谱基本信息列表 - RecordBaseInfoList []*majsoulRecordBaseInfo `json:"record_list"` + RecordBaseInfoList []*MahJongSoulRecordBaseInfo `json:"record_list"` // 分享的牌谱基本信息 - SharedRecordBaseInfo *majsoulRecordBaseInfo `json:"shared_record_base_info"` + SharedRecordBaseInfo *MahJongSoulRecordBaseInfo `json:"shared_record_base_info"` // 当前正在观看的牌谱的 UUID CurrentRecordUUID string `json:"current_record_uuid"` // 当前正在观看的牌谱的全部操作 - RecordActions []*majsoulRecordAction `json:"record_actions"` + RecordActions []*MahJongSoulRecordAction `json:"record_actions"` // 玩家在网页上的(点击)操作(网页响应了的) RecordClickAction string `json:"record_click_action"` @@ -36,14 +37,14 @@ type majsoulMessage struct { // 观战 LiveBaseInfo *majsoulLiveRecordBaseInfo `json:"live_head"` - LiveFastAction *majsoulRecordAction `json:"live_fast_action"` - LiveAction *majsoulRecordAction `json:"live_action"` + LiveFastAction *MahJongSoulRecordAction `json:"live_fast_action"` + LiveAction *MahJongSoulRecordAction `json:"live_action"` // 座位变更 ChangeSeatTo *int `json:"change_seat_to"` // 游戏重连时收到的数据 - SyncGameActions []*majsoulRecordAction `json:"sync_game_actions"` + SyncGameActions []*MahJongSoulRecordAction `json:"sync_game_actions"` // ResAuthGame // {"seat_list":[x,x,x,x],"is_game_start":false,"game_config":{"category":1,"mode":{"mode":1,"ai":true,"detail_rule":{"time_fixed":60,"time_add":0,"dora_count":3,"shiduan":1,"init_point":25000,"fandian":30000,"bianjietishi":true,"ai_level":1,"fanfu":1}},"meta":{"room_id":18269}},"ready_id_list":[0,0,0]} @@ -125,20 +126,20 @@ const ( majsoulMeldTypeAnkan ) -type majsoulRoundData struct { - *roundData +type MahJongSoulRoundData struct { + *RoundData - originJSON string - msg *majsoulMessage + OriginJSON string + Message *MaJongSoulMessage - selfSeat int // 自家初始座位:0-第一局的东家 1-第一局的南家 2-第一局的西家 3-第一局的北家 + SelfSeat int // 自家初始座位:0-第一局的东家 1-第一局的南家 2-第一局的西家 3-第一局的北家 } -func (d *majsoulRoundData) fatalParse(info string, msg string) { +func (d *MahJongSoulRoundData) fatalParse(info string, msg string) { panic(fmt.Sprintln(info, len(msg), msg, []byte(msg))) } -func (d *majsoulRoundData) normalTiles(tiles interface{}) (majsoulTiles []string) { +func (d *MahJongSoulRoundData) normalTiles(tiles interface{}) (majsoulTiles []string) { _tiles, ok := tiles.([]interface{}) if !ok { _tile, ok := tiles.(string) @@ -159,14 +160,14 @@ func (d *majsoulRoundData) normalTiles(tiles interface{}) (majsoulTiles []string return majsoulTiles } -func (d *majsoulRoundData) parseWho(seat int) int { +func (d *MahJongSoulRoundData) parseWho(seat int) int { // 转换成 0=自家, 1=下家, 2=对家, 3=上家 // 对三麻四麻均适用 who := (seat + d.dealer - d.roundNumber%4 + 4) % 4 return who } -func (d *majsoulRoundData) mustParseMajsoulTile(humanTile string) (tile34 int, isRedFive bool) { +func (d *MahJongSoulRoundData) mustParseMajsoulTile(humanTile string) (tile34 int, isRedFive bool) { tile34, isRedFive, err := util.StrToTile34(humanTile) if err != nil { panic(err) @@ -174,7 +175,7 @@ func (d *majsoulRoundData) mustParseMajsoulTile(humanTile string) (tile34 int, i return } -func (d *majsoulRoundData) mustParseMajsoulTiles(majsoulTiles []string) (tiles []int, numRedFive int) { +func (d *MahJongSoulRoundData) mustParseMajsoulTiles(majsoulTiles []string) (tiles []int, numRedFive int) { tiles = make([]int, len(majsoulTiles)) for i, majsoulTile := range majsoulTiles { var isRedFive bool @@ -186,24 +187,24 @@ func (d *majsoulRoundData) mustParseMajsoulTiles(majsoulTiles []string) (tiles [ return } -func (d *majsoulRoundData) isNewDora(doras []string) bool { +func (d *MahJongSoulRoundData) isNewDora(doras []string) bool { return len(doras) > len(d.doraIndicators) } -func (d *majsoulRoundData) GetDataSourceType() int { +func (d *MahJongSoulRoundData) GetDataSourceType() int { return dataSourceTypeMajsoul } -func (d *majsoulRoundData) GetSelfSeat() int { - return d.selfSeat +func (d *MahJongSoulRoundData) GetSelfSeat() int { + return d.SelfSeat } -func (d *majsoulRoundData) GetMessage() string { - return d.originJSON +func (d *MahJongSoulRoundData) GetMessage() string { + return d.OriginJSON } -func (d *majsoulRoundData) SkipMessage() bool { - msg := d.msg +func (d *MahJongSoulRoundData) SkipMessage() bool { + msg := d.Message // 没有账号 skip if gameConf.currentActiveMajsoulAccountID == -1 { @@ -230,13 +231,13 @@ func (d *majsoulRoundData) SkipMessage() bool { return false } -func (d *majsoulRoundData) IsLogin() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsLogin() bool { + msg := d.Message return msg.AccountID > 0 || msg.SeatList != nil } -func (d *majsoulRoundData) HandleLogin() { - msg := d.msg +func (d *MahJongSoulRoundData) HandleLogin() { + msg := d.Message if accountID := msg.AccountID; accountID > 0 { gameConf.addMajsoulAccountID(accountID) @@ -282,26 +283,26 @@ func (d *majsoulRoundData) HandleLogin() { } } -func (d *majsoulRoundData) IsInit() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsInit() bool { + msg := d.Message // ResAuthGame || ActionNewRound RecordNewRound return msg.IsGameStart != nil || msg.MD5 != "" } -func (d *majsoulRoundData) ParseInit() (roundNumber int, benNumber int, dealer int, doraIndicators []int, handTiles []int, numRedFives []int) { - msg := d.msg +func (d *MahJongSoulRoundData) ParseInit() (roundNumber int, benNumber int, dealer int, doraIndicators []int, handTiles []int, numRedFives []int) { + msg := d.Message if playerNumber := len(msg.SeatList); playerNumber >= 3 { d.playerNumber = playerNumber // 获取自家初始座位:0-第一局的东家 1-第一局的南家 2-第一局的西家 3-第一局的北家 for i, accountID := range msg.SeatList { if accountID == gameConf.currentActiveMajsoulAccountID { - d.selfSeat = i + d.SelfSeat = i break } } // dealer: 0=自家, 1=下家, 2=对家, 3=上家 - dealer = (4 - d.selfSeat) % 4 + dealer = (4 - d.SelfSeat) % 4 return } else if len(msg.Tiles2) > 0 { if len(msg.Tiles3) > 0 { @@ -329,7 +330,7 @@ func (d *majsoulRoundData) ParseInit() (roundNumber int, benNumber int, dealer i if msg.Tiles != nil { // 实战 majsoulTiles = d.normalTiles(msg.Tiles) } else { // 牌谱、观战 - majsoulTiles = [][]string{msg.Tiles0, msg.Tiles1, msg.Tiles2, msg.Tiles3}[d.selfSeat] + majsoulTiles = [][]string{msg.Tiles0, msg.Tiles1, msg.Tiles2, msg.Tiles3}[d.SelfSeat] } for _, majsoulTile := range majsoulTiles { tile, isRedFive := d.mustParseMajsoulTile(majsoulTile) @@ -342,14 +343,14 @@ func (d *majsoulRoundData) ParseInit() (roundNumber int, benNumber int, dealer i return } -func (d *majsoulRoundData) IsSelfDraw() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsSelfDraw() bool { + msg := d.Message // ActionDealTile RecordDealTile return msg.Seat != nil && msg.Tile != "" && msg.Moqie == nil && d.parseWho(*msg.Seat) == 0 } -func (d *majsoulRoundData) ParseSelfDraw() (tile int, isRedFive bool, kanDoraIndicator int) { - msg := d.msg +func (d *MahJongSoulRoundData) ParseSelfDraw() (tile int, isRedFive bool, kanDoraIndicator int) { + msg := d.Message tile, isRedFive = d.mustParseMajsoulTile(msg.Tile) kanDoraIndicator = -1 if d.isNewDora(msg.Doras) { @@ -358,14 +359,14 @@ func (d *majsoulRoundData) ParseSelfDraw() (tile int, isRedFive bool, kanDoraInd return } -func (d *majsoulRoundData) IsDiscard() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsDiscard() bool { + msg := d.Message // ActionDiscardTile RecordDiscardTile return msg.IsLiqi != nil } -func (d *majsoulRoundData) ParseDiscard() (who int, discardTile int, isRedFive bool, isTsumogiri bool, isReach bool, canBeMeld bool, kanDoraIndicator int) { - msg := d.msg +func (d *MahJongSoulRoundData) ParseDiscard() (who int, discardTile int, isRedFive bool, isTsumogiri bool, isReach bool, canBeMeld bool, kanDoraIndicator int) { + msg := d.Message who = d.parseWho(*msg.Seat) discardTile, isRedFive = d.mustParseMajsoulTile(msg.Tile) isTsumogiri = *msg.Moqie @@ -381,14 +382,14 @@ func (d *majsoulRoundData) ParseDiscard() (who int, discardTile int, isRedFive b return } -func (d *majsoulRoundData) IsOpen() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsOpen() bool { + msg := d.Message // ActionChiPengGang RecordChiPengGang || ActionAnGangAddGang RecordAnGangAddGang return msg.Tiles != nil && len(d.normalTiles(msg.Tiles)) <= 4 } -func (d *majsoulRoundData) ParseOpen() (who int, meld *model.Meld, kanDoraIndicator int) { - msg := d.msg +func (d *MahJongSoulRoundData) ParseOpen() (who int, meld *model.Meld, kanDoraIndicator int) { + msg := d.Message who = d.parseWho(*msg.Seat) @@ -464,26 +465,26 @@ func (d *majsoulRoundData) ParseOpen() (who int, meld *model.Meld, kanDoraIndica return } -func (d *majsoulRoundData) IsReach() bool { +func (d *MahJongSoulRoundData) IsReach() bool { return false } -func (d *majsoulRoundData) ParseReach() (who int) { +func (d *MahJongSoulRoundData) ParseReach() (who int) { return 0 } -func (d *majsoulRoundData) IsFuriten() bool { +func (d *MahJongSoulRoundData) IsFuriten() bool { return false } -func (d *majsoulRoundData) IsRoundWin() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsRoundWin() bool { + msg := d.Message // ActionHule RecordHule return msg.Hules != nil } -func (d *majsoulRoundData) ParseRoundWin() (whos []int, points []int) { - msg := d.msg +func (d *MahJongSoulRoundData) ParseRoundWin() (whos []int, points []int) { + msg := d.Message for _, result := range msg.Hules { who := d.parseWho(result.Seat) @@ -505,38 +506,38 @@ func (d *majsoulRoundData) ParseRoundWin() (whos []int, points []int) { return } -func (d *majsoulRoundData) IsRyuukyoku() bool { +func (d *MahJongSoulRoundData) IsRyuukyoku() bool { // TODO // ActionLiuJu RecordLiuJu return false } -func (d *majsoulRoundData) ParseRyuukyoku() (type_ int, whos []int, points []int) { +func (d *MahJongSoulRoundData) ParseRyuukyoku() (type_ int, whos []int, points []int) { // TODO return } // 拔北宝牌 -func (d *majsoulRoundData) IsNukiDora() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsNukiDora() bool { + msg := d.Message // ActionBaBei RecordBaBei return msg.Seat != nil && msg.Moqie != nil && msg.Tile == "" } -func (d *majsoulRoundData) ParseNukiDora() (who int, isTsumogiri bool) { - msg := d.msg +func (d *MahJongSoulRoundData) ParseNukiDora() (who int, isTsumogiri bool) { + msg := d.Message return d.parseWho(*msg.Seat), *msg.Moqie } // 在最后处理该项 -func (d *majsoulRoundData) IsNewDora() bool { - msg := d.msg +func (d *MahJongSoulRoundData) IsNewDora() bool { + msg := d.Message // ActionDealTile return d.isNewDora(msg.Doras) } -func (d *majsoulRoundData) ParseNewDora() (kanDoraIndicator int) { - msg := d.msg +func (d *MahJongSoulRoundData) ParseNewDora() (kanDoraIndicator int) { + msg := d.Message kanDoraIndicator, _ = d.mustParseMajsoulTile(msg.Doras[len(msg.Doras)-1]) return diff --git a/majsoul_record.go b/majsoul_record.go index 68ee77e..1b34f87 100644 --- a/majsoul_record.go +++ b/majsoul_record.go @@ -2,10 +2,11 @@ package main import ( "fmt" - "github.com/EndlessCheng/mahjong-helper/util" + "sort" "strconv" "time" - "sort" + + "github.com/EndlessCheng/mahjong-helper/util" ) type _majsoulRecordAccount struct { @@ -16,7 +17,7 @@ type _majsoulRecordAccount struct { } // 牌谱基本信息 -type majsoulRecordBaseInfo struct { +type MahJongSoulRecordBaseInfo struct { UUID string `json:"uuid"` StartTime int64 `json:"start_time"` EndTime int64 `json:"end_time"` @@ -26,7 +27,7 @@ type majsoulRecordBaseInfo struct { Accounts []_majsoulRecordAccount `json:"accounts"` } -func (i *majsoulRecordBaseInfo) sort() { +func (i *MahJongSoulRecordBaseInfo) sort() { sort.Slice(i.Accounts, func(i_, j int) bool { return i.Accounts[i_].Seat < i.Accounts[j].Seat }) @@ -34,7 +35,7 @@ func (i *majsoulRecordBaseInfo) sort() { var seatNameZH = []string{"东", "南", "西", "北"} -func (i *majsoulRecordBaseInfo) String() string { +func (i *MahJongSoulRecordBaseInfo) String() string { i.sort() const timeFormat = "2006-01-02 15:04:05" @@ -51,7 +52,7 @@ func (i *majsoulRecordBaseInfo) String() string { return output } -func (i *majsoulRecordBaseInfo) getSelfSeat(accountID int) (int, error) { +func (i *MahJongSoulRecordBaseInfo) getSelfSeat(accountID int) (int, error) { if len(i.Accounts) == 0 { return -1, fmt.Errorf("牌谱基本信息为空") } @@ -67,21 +68,21 @@ func (i *majsoulRecordBaseInfo) getSelfSeat(accountID int) (int, error) { // // 牌谱、观战中的单个操作信息 -type majsoulRecordAction struct { - Name string `json:"name"` - Action *majsoulMessage `json:"data"` +type MahJongSoulRecordAction struct { + Name string `json:"name"` + Action *MaJongSoulMessage `json:"data"` } -type majsoulRoundActions []*majsoulRecordAction +type MahJongSoulRoundActions []*MahJongSoulRecordAction -func (l majsoulRoundActions) append(action *majsoulRecordAction) (majsoulRoundActions, error) { +func (l MahJongSoulRoundActions) Append(action *MahJongSoulRecordAction) (MahJongSoulRoundActions, error) { if action == nil { return nil, fmt.Errorf("数据异常:拿到的操作内容为空") } newL := l if action.Name == "RecordNewRound" { - newL = majsoulRoundActions{action} + newL = MahJongSoulRoundActions{action} } else { if len(newL) == 0 { return nil, fmt.Errorf("数据异常:未收到 RecordNewRound") @@ -92,18 +93,18 @@ func (l majsoulRoundActions) append(action *majsoulRecordAction) (majsoulRoundAc return newL, nil } -func parseMajsoulRecordAction(actions []*majsoulRecordAction) (roundActionsList []majsoulRoundActions, err error) { +func parseMajsoulRecordAction(actions []*MahJongSoulRecordAction) (roundActionsList []MahJongSoulRoundActions, err error) { if len(actions) == 0 { return nil, fmt.Errorf("数据异常:拿到的牌谱内容为空") } - var currentRoundActions majsoulRoundActions + var currentRoundActions MahJongSoulRoundActions for _, action := range actions { if action.Name == "RecordNewRound" { if len(currentRoundActions) > 0 { roundActionsList = append(roundActionsList, currentRoundActions) } - currentRoundActions = []*majsoulRecordAction{action} + currentRoundActions = []*MahJongSoulRecordAction{action} } else { if len(currentRoundActions) == 0 { return nil, fmt.Errorf("数据异常:未收到 RecordNewRound") diff --git a/server.go b/server.go index 432d113..bdd9123 100644 --- a/server.go +++ b/server.go @@ -4,14 +4,6 @@ import ( "crypto/tls" "encoding/json" "fmt" - "github.com/EndlessCheng/mahjong-helper/platform/tenhou" - "github.com/EndlessCheng/mahjong-helper/util" - "github.com/EndlessCheng/mahjong-helper/util/debug" - "github.com/EndlessCheng/mahjong-helper/util/model" - "github.com/fatih/color" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/labstack/gommon/log" "io/ioutil" stdLog "log" "net" @@ -20,11 +12,20 @@ import ( "path/filepath" "strconv" "time" + + "github.com/EndlessCheng/mahjong-helper/platform/tenhou" + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/EndlessCheng/mahjong-helper/util/debug" + "github.com/EndlessCheng/mahjong-helper/util/model" + "github.com/fatih/color" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/labstack/gommon/log" ) -const defaultPort = 12121 +const DefaultPort = 12121 -func newLogFilePath() (filePath string, err error) { +func NewLogFilePath() (filePath string, err error) { const logDir = "log" if err = os.MkdirAll(logDir, os.ModePerm); err != nil { return @@ -34,85 +35,88 @@ func newLogFilePath() (filePath string, err error) { return filepath.Abs(filePath) } -type mjHandler struct { - log echo.Logger +type MahJongHandler struct { + Log echo.Logger - analysing bool + Analysing bool - tenhouMessageReceiver *tenhou.MessageReceiver - tenhouRoundData *tenhouRoundData + TenhouMessageReceiver *tenhou.MessageReceiver + TenHouRoundData *TenHouRoundData - majsoulMessageQueue chan []byte - majsoulRoundData *majsoulRoundData + majsoulMessageQueue chan []byte + MahJongSoulRoundData *MahJongSoulRoundData - majsoulRecordMap map[string]*majsoulRecordBaseInfo - majsoulCurrentRecordUUID string - majsoulCurrentRecordActionsList []majsoulRoundActions - majsoulCurrentRoundIndex int - majsoulCurrentActionIndex int + MahJongSoulRecordMap map[string]*MahJongSoulRecordBaseInfo + MahJongSoulCurrentRecordUUID string + MahJongSoulCurrentRecordActionsList []MahJongSoulRoundActions + MahJongSoulCurrentRoundIndex int + MahJongSoulCurrentActionIndex int - majsoulCurrentRoundActions majsoulRoundActions + MahJongSoulCurrentRoundActions MahJongSoulRoundActions } -func (h *mjHandler) logError(err error) { +// write error to log +func (handler *MahJongHandler) LogError(err error) { fmt.Fprintln(os.Stderr, err) - if !debugMode { - h.log.Error(err) + if !DebugMode { + handler.Log.Error(err) } } // 调试用 -func (h *mjHandler) index(c echo.Context) error { - data, err := ioutil.ReadAll(c.Request().Body) +func (handler *MahJongHandler) Index(echo_context echo.Context) error { + data, err := ioutil.ReadAll(echo_context.Request().Body) if err != nil { - h.log.Error("[mjHandler.index.ioutil.ReadAll]", err) - return c.NoContent(http.StatusInternalServerError) + handler.Log.Error("[MahJongHandler.index.ioutil.ReadAll]", err) + return echo_context.NoContent(http.StatusInternalServerError) } fmt.Println(data, string(data)) - h.log.Info(data) - return c.String(http.StatusOK, time.Now().Format("2006-01-02 15:04:05")) + handler.Log.Info(data) + return echo_context.String(http.StatusOK, time.Now().Format("2006-01-02 15:04:05")) } // 打一摸一分析器 -func (h *mjHandler) analysis(c echo.Context) error { - if h.analysing { - return c.NoContent(http.StatusForbidden) +func (handler *MahJongHandler) Analysis(echo_context echo.Context) error { + if handler.Analysing { + return echo_context.NoContent(http.StatusForbidden) } - h.analysing = true - defer func() { h.analysing = false }() + handler.Analysing = true + defer func() { handler.Analysing = false }() - d := struct { + data := struct { Reset bool `json:"reset"` Tiles string `json:"tiles"` }{} - if err := c.Bind(&d); err != nil { + if err := echo_context.Bind(&data); err != nil { fmt.Println(err) - return c.String(http.StatusBadRequest, err.Error()) + return echo_context.String(http.StatusBadRequest, err.Error()) } - if _, err := analysisHumanTiles(model.NewSimpleHumanTilesInfo(d.Tiles)); err != nil { + if _, err := AnalysisHumanTiles(model.NewSimpleHumanTilesInfo(data.Tiles)); err != nil { fmt.Println(err) - return c.String(http.StatusBadRequest, err.Error()) + return echo_context.String(http.StatusBadRequest, err.Error()) } - return c.NoContent(http.StatusOK) + return echo_context.NoContent(http.StatusOK) } // 分析天凤 WebSocket 数据 -func (h *mjHandler) analysisTenhou(c echo.Context) error { - data, err := ioutil.ReadAll(c.Request().Body) +func (handler *MahJongHandler) AnalysisTenHou(echo_context echo.Context) error { + data, err := ioutil.ReadAll(echo_context.Request().Body) if err != nil { - h.logError(err) - return c.String(http.StatusBadRequest, err.Error()) + handler.LogError(err) + return echo_context.String(http.StatusBadRequest, err.Error()) } - h.tenhouMessageReceiver.Put(data) - return c.NoContent(http.StatusOK) + handler.TenhouMessageReceiver.Put(data) + return echo_context.NoContent(http.StatusOK) } -func (h *mjHandler) runAnalysisTenhouMessageTask() { - if !debugMode { + +// run analysis TenHou Message +func (handler *MahJongHandler) RunAnalysisTenHouMessageTask() { + if !DebugMode { defer func() { if err := recover(); err != nil { fmt.Println("内部错误:", err) @@ -121,39 +125,41 @@ func (h *mjHandler) runAnalysisTenhouMessageTask() { } for { - msg := h.tenhouMessageReceiver.Get() - d := tenhouMessage{} - if err := json.Unmarshal(msg, &d); err != nil { - h.logError(err) + msg := handler.TenhouMessageReceiver.Get() + data := TenhouMessage{} + if err := json.Unmarshal(msg, &data); err != nil { + handler.LogError(err) continue } originJSON := string(msg) - if h.log != nil { - h.log.Info(originJSON) + if handler.Log != nil { + handler.Log.Info(originJSON) } - h.tenhouRoundData.msg = &d - h.tenhouRoundData.originJSON = originJSON - if err := h.tenhouRoundData.analysis(); err != nil { - h.logError(err) + handler.TenHouRoundData.Msg = &data + handler.TenHouRoundData.OriginJSON = originJSON + if err := handler.TenHouRoundData.Analysis(); err != nil { + handler.LogError(err) } } } // 分析雀魂 WebSocket 数据 -func (h *mjHandler) analysisMajsoul(c echo.Context) error { - data, err := ioutil.ReadAll(c.Request().Body) +func (handler *MahJongHandler) AnalysisMajsoul(echo_context echo.Context) error { + data, err := ioutil.ReadAll(echo_context.Request().Body) if err != nil { - h.logError(err) - return c.String(http.StatusBadRequest, err.Error()) + handler.LogError(err) + return echo_context.String(http.StatusBadRequest, err.Error()) } - h.majsoulMessageQueue <- data - return c.NoContent(http.StatusOK) + handler.majsoulMessageQueue <- data + return echo_context.NoContent(http.StatusOK) } -func (h *mjHandler) runAnalysisMajsoulMessageTask() { - if !debugMode { + +// run analysis MahJongSoul Message +func (handler *MahJongHandler) RunAnalysisMahJongSoulMessageTask() { + if !DebugMode { defer func() { if err := recover(); err != nil { fmt.Println("内部错误:", err) @@ -161,16 +167,16 @@ func (h *mjHandler) runAnalysisMajsoulMessageTask() { }() } - for msg := range h.majsoulMessageQueue { - d := &majsoulMessage{} + for msg := range handler.majsoulMessageQueue { + d := &MaJongSoulMessage{} if err := json.Unmarshal(msg, d); err != nil { - h.logError(err) + handler.LogError(err) continue } originJSON := string(msg) - if h.log != nil && debug.Lo == 0 { - h.log.Info(originJSON) + if handler.Log != nil && debug.Lo == 0 { + handler.Log.Info(originJSON) } else { if len(originJSON) > 500 { originJSON = originJSON[:500] @@ -185,24 +191,24 @@ func (h *mjHandler) runAnalysisMajsoulMessageTask() { case len(d.RecordBaseInfoList) > 0: // 牌谱基本信息列表 for _, record := range d.RecordBaseInfoList { - h.majsoulRecordMap[record.UUID] = record + handler.MahJongSoulRecordMap[record.UUID] = record } - color.HiGreen("收到 %2d 个雀魂牌谱(已收集 %d 个),请在网页上点击「查看」", len(d.RecordBaseInfoList), len(h.majsoulRecordMap)) + color.HiGreen("收到 %2d 个雀魂牌谱(已收集 %d 个),请在网页上点击「查看」", len(d.RecordBaseInfoList), len(handler.MahJongSoulRecordMap)) case d.SharedRecordBaseInfo != nil: // 处理分享的牌谱基本信息 // FIXME: 观看自己的牌谱也会有 d.SharedRecordBaseInfo record := d.SharedRecordBaseInfo - h.majsoulRecordMap[record.UUID] = record - if err := h._loadMajsoulRecordBaseInfo(record.UUID); err != nil { - h.logError(err) + handler.MahJongSoulRecordMap[record.UUID] = record + if err := handler.loadMahJongSoulRecordBaseInfo(record.UUID); err != nil { + handler.LogError(err) break } case d.CurrentRecordUUID != "": // 载入某个牌谱 - resetAnalysisCache() - h.majsoulCurrentRecordActionsList = nil + ResetAnalysisCache() + handler.MahJongSoulCurrentRecordActionsList = nil - if err := h._loadMajsoulRecordBaseInfo(d.CurrentRecordUUID); err != nil { + if err := handler.loadMahJongSoulRecordBaseInfo(d.CurrentRecordUUID); err != nil { // 看的是分享的牌谱(先收到 CurrentRecordUUID 和 AccountID,然后收到 SharedRecordBaseInfo) // 或者是比赛场的牌谱 // 记录主视角 ID(可能是 0) @@ -219,129 +225,130 @@ func (h *mjHandler) runAnalysisMajsoulMessageTask() { gameConf.setMajsoulAccountID(d.AccountID) } case len(d.RecordActions) > 0: - if h.majsoulCurrentRecordActionsList != nil { + if handler.MahJongSoulCurrentRecordActionsList != nil { // TODO: 网页发送更恰当的信息? break } - if h.majsoulCurrentRecordUUID == "" { - h.logError(fmt.Errorf("错误:程序未收到所观看的雀魂牌谱的 UUID")) + if handler.MahJongSoulCurrentRecordUUID == "" { + handler.LogError(fmt.Errorf("错误:程序未收到所观看的雀魂牌谱的 UUID")) break } - baseInfo, ok := h.majsoulRecordMap[h.majsoulCurrentRecordUUID] + baseInfo, ok := handler.MahJongSoulRecordMap[handler.MahJongSoulCurrentRecordUUID] if !ok { - h.logError(fmt.Errorf("错误:找不到雀魂牌谱 %s", h.majsoulCurrentRecordUUID)) + handler.LogError(fmt.Errorf("错误:找不到雀魂牌谱 %s", handler.MahJongSoulCurrentRecordUUID)) break } selfAccountID := gameConf.currentActiveMajsoulAccountID if selfAccountID == -1 { - h.logError(fmt.Errorf("错误:当前雀魂账号为空")) + handler.LogError(fmt.Errorf("错误:当前雀魂账号为空")) break } - h.majsoulRoundData.newGame() - h.majsoulRoundData.gameMode = gameModeRecord + handler.MahJongSoulRoundData.newGame() + handler.MahJongSoulRoundData.GameMode = gameModeRecord // 获取并设置主视角初始座位 selfSeat, err := baseInfo.getSelfSeat(selfAccountID) if err != nil { - h.logError(err) + handler.LogError(err) break } - h.majsoulRoundData.selfSeat = selfSeat + handler.MahJongSoulRoundData.SelfSeat = selfSeat // 准备分析…… majsoulCurrentRecordActions, err := parseMajsoulRecordAction(d.RecordActions) if err != nil { - h.logError(err) + handler.LogError(err) break } - h.majsoulCurrentRecordActionsList = majsoulCurrentRecordActions - h.majsoulCurrentRoundIndex = 0 - h.majsoulCurrentActionIndex = 0 + handler.MahJongSoulCurrentRecordActionsList = majsoulCurrentRecordActions + handler.MahJongSoulCurrentRoundIndex = 0 + handler.MahJongSoulCurrentActionIndex = 0 - actions := h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex] + actions := handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex] // 创建分析任务 - analysisCache := newGameAnalysisCache(h.majsoulCurrentRecordUUID, selfSeat) - setAnalysisCache(analysisCache) - go analysisCache.runMajsoulRecordAnalysisTask(actions) + analysisCache := newGameAnalysisCache(handler.MahJongSoulCurrentRecordUUID, selfSeat) + SetAnalysisCache(analysisCache) + go analysisCache.RunMahJongSoulRecordAnalysisTask(actions) // 分析第一局的起始信息 data := actions[0].Action - h._analysisMajsoulRoundData(data, originJSON) + handler.analysisMahJongSoulRoundData(data, originJSON) case d.RecordClickAction != "": // 处理网页上的牌谱点击:上一局/跳到某局/下一局/上一巡/跳到某巡/下一巡/上一步/播放/暂停/下一步/点击桌面 // 暂不能分析他家手牌 - h._onRecordClick(d.RecordClickAction, d.RecordClickActionIndex, d.FastRecordTo) + handler.onRecordClick(d.RecordClickAction, d.RecordClickActionIndex, d.FastRecordTo) case d.LiveBaseInfo != nil: // 观战 gameConf.setMajsoulAccountID(1) // TODO: 重构 - h.majsoulRoundData.newGame() - h.majsoulRoundData.selfSeat = 0 // 观战进来后看的是东起的玩家 - h.majsoulRoundData.gameMode = gameModeLive - clearConsole() + handler.MahJongSoulRoundData.newGame() + handler.MahJongSoulRoundData.SelfSeat = 0 // 观战进来后看的是东起的玩家 + handler.MahJongSoulRoundData.GameMode = gameModeLive + ClearConsole() fmt.Printf("正在载入对战:%s", d.LiveBaseInfo.String()) case d.LiveFastAction != nil: - if err := h._loadLiveAction(d.LiveFastAction, true); err != nil { - h.logError(err) + if err := handler.loadLiveAction(d.LiveFastAction, true); err != nil { + handler.LogError(err) break } case d.LiveAction != nil: - if err := h._loadLiveAction(d.LiveAction, false); err != nil { - h.logError(err) + if err := handler.loadLiveAction(d.LiveAction, false); err != nil { + handler.LogError(err) break } case d.ChangeSeatTo != nil: // 切换座位 changeSeatTo := *(d.ChangeSeatTo) - h.majsoulRoundData.selfSeat = changeSeatTo - if debugMode { + handler.MahJongSoulRoundData.SelfSeat = changeSeatTo + if DebugMode { fmt.Println("座位已切换至", changeSeatTo) } - var actions majsoulRoundActions - if h.majsoulRoundData.gameMode == gameModeLive { // 观战 - actions = h.majsoulCurrentRoundActions + var actions MahJongSoulRoundActions + if handler.MahJongSoulRoundData.GameMode == gameModeLive { // 观战 + actions = handler.MahJongSoulCurrentRoundActions } else { // 牌谱 - fullActions := h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex] - actions = fullActions[:h.majsoulCurrentActionIndex+1] - analysisCache := getAnalysisCache(changeSeatTo) + fullActions := handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex] + actions = fullActions[:handler.MahJongSoulCurrentActionIndex+1] + analysisCache := GetAnalysisCache(changeSeatTo) if analysisCache == nil { - analysisCache = newGameAnalysisCache(h.majsoulCurrentRecordUUID, changeSeatTo) + analysisCache = newGameAnalysisCache(handler.MahJongSoulCurrentRecordUUID, changeSeatTo) } - setAnalysisCache(analysisCache) + SetAnalysisCache(analysisCache) // 创建分析任务 - go analysisCache.runMajsoulRecordAnalysisTask(fullActions) + go analysisCache.RunMahJongSoulRecordAnalysisTask(fullActions) } - h._fastLoadActions(actions) + handler.fastLoadActions(actions) case len(d.SyncGameActions) > 0: - h._fastLoadActions(d.SyncGameActions) + handler.fastLoadActions(d.SyncGameActions) default: // 其他:AI 分析 - h._analysisMajsoulRoundData(d, originJSON) + handler.analysisMahJongSoulRoundData(d, originJSON) } } } -func (h *mjHandler) _loadMajsoulRecordBaseInfo(majsoulRecordUUID string) error { - baseInfo, ok := h.majsoulRecordMap[majsoulRecordUUID] +// private function to load MahJongSoul record base information +func (handler *MahJongHandler) loadMahJongSoulRecordBaseInfo(mahjongsoulRecordUUID string) error { + baseInfo, ok := handler.MahJongSoulRecordMap[mahjongsoulRecordUUID] if !ok { - return fmt.Errorf("错误:找不到雀魂牌谱 %s", majsoulRecordUUID) + return fmt.Errorf("错误:找不到雀魂牌谱 %s", mahjongsoulRecordUUID) } // 标记当前正在观看的牌谱 - h.majsoulCurrentRecordUUID = majsoulRecordUUID - clearConsole() + handler.MahJongSoulCurrentRecordUUID = mahjongsoulRecordUUID + ClearConsole() fmt.Printf("正在解析雀魂牌谱:%s", baseInfo.String()) // 标记古役模式 - isGuyiMode := baseInfo.Config.isGuyiMode() - util.SetConsiderOldYaku(isGuyiMode) - if isGuyiMode { + isOldYaKuMode := baseInfo.Config.isGuyiMode() + util.SetConsiderOldYaku(isOldYaKuMode) + if isOldYaKuMode { fmt.Println() color.HiGreen("古役模式已开启") } @@ -349,148 +356,161 @@ func (h *mjHandler) _loadMajsoulRecordBaseInfo(majsoulRecordUUID string) error { return nil } -func (h *mjHandler) _loadLiveAction(action *majsoulRecordAction, isFast bool) error { - if debugMode { +// private function to load live action +func (handler *MahJongHandler) loadLiveAction(action *MahJongSoulRecordAction, isFast bool) error { + if DebugMode { fmt.Println("[_loadLiveAction] 收到", action, isFast) } - newActions, err := h.majsoulCurrentRoundActions.append(action) + newActions, err := handler.MahJongSoulCurrentRoundActions.Append(action) if err != nil { return err } - h.majsoulCurrentRoundActions = newActions + handler.MahJongSoulCurrentRoundActions = newActions - h.majsoulRoundData.skipOutput = isFast - h._analysisMajsoulRoundData(action.Action, "") + handler.MahJongSoulRoundData.SkipOutput = isFast + handler.analysisMahJongSoulRoundData(action.Action, "") return nil } -func (h *mjHandler) _analysisMajsoulRoundData(data *majsoulMessage, originJSON string) { +// private function analysis MahJongSoul Round Data +func (handler *MahJongHandler) analysisMahJongSoulRoundData(data *MaJongSoulMessage, originJSON string) { //if originJSON == "{}" { // return //} - h.majsoulRoundData.msg = data - h.majsoulRoundData.originJSON = originJSON - if err := h.majsoulRoundData.analysis(); err != nil { - h.logError(err) + handler.MahJongSoulRoundData.Message = data + handler.MahJongSoulRoundData.OriginJSON = originJSON + if err := handler.MahJongSoulRoundData.Analysis(); err != nil { + handler.LogError(err) } } -func (h *mjHandler) _fastLoadActions(actions []*majsoulRecordAction) { +// private function fast load actions +func (handler *MahJongHandler) fastLoadActions(actions []*MahJongSoulRecordAction) { if len(actions) == 0 { return } fastRecordEnd := util.MaxInt(0, len(actions)-3) - h.majsoulRoundData.skipOutput = true + handler.MahJongSoulRoundData.SkipOutput = true // 留最后三个刷新,这样确保会刷新界面 for _, action := range actions[:fastRecordEnd] { - h._analysisMajsoulRoundData(action.Action, "") + handler.analysisMahJongSoulRoundData(action.Action, "") } - h.majsoulRoundData.skipOutput = false + handler.MahJongSoulRoundData.SkipOutput = false for _, action := range actions[fastRecordEnd:] { - h._analysisMajsoulRoundData(action.Action, "") + handler.analysisMahJongSoulRoundData(action.Action, "") } } -func (h *mjHandler) _onRecordClick(clickAction string, clickActionIndex int, fastRecordTo int) { - if debugMode { +// private function on Record Click +func (handler *MahJongHandler) onRecordClick(clickAction string, clickActionIndex int, fastRecordTo int) { + if DebugMode { fmt.Println("[_onRecordClick] 收到", clickAction, clickActionIndex, fastRecordTo) } - analysisCache := getCurrentAnalysisCache() + analysisCache := GetCurrentAnalysisCache() switch clickAction { case "nextStep", "update": - newActionIndex := h.majsoulCurrentActionIndex + 1 - if newActionIndex >= len(h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex]) { + newActionIndex := handler.MahJongSoulCurrentActionIndex + 1 + if newActionIndex >= len(handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex]) { return } - h.majsoulCurrentActionIndex = newActionIndex + handler.MahJongSoulCurrentActionIndex = newActionIndex case "nextRound": - h.majsoulCurrentRoundIndex = (h.majsoulCurrentRoundIndex + 1) % len(h.majsoulCurrentRecordActionsList) - h.majsoulCurrentActionIndex = 0 - go analysisCache.runMajsoulRecordAnalysisTask(h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex]) + handler.MahJongSoulCurrentRoundIndex = (handler.MahJongSoulCurrentRoundIndex + 1) % + len(handler.MahJongSoulCurrentRecordActionsList) + handler.MahJongSoulCurrentActionIndex = 0 + go analysisCache.RunMahJongSoulRecordAnalysisTask( + handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex]) case "preRound": - h.majsoulCurrentRoundIndex = (h.majsoulCurrentRoundIndex - 1 + len(h.majsoulCurrentRecordActionsList)) % len(h.majsoulCurrentRecordActionsList) - h.majsoulCurrentActionIndex = 0 - go analysisCache.runMajsoulRecordAnalysisTask(h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex]) + handler.MahJongSoulCurrentRoundIndex = (handler.MahJongSoulCurrentRoundIndex - 1 + + len(handler.MahJongSoulCurrentRecordActionsList)) % len(handler.MahJongSoulCurrentRecordActionsList) + handler.MahJongSoulCurrentActionIndex = 0 + go analysisCache.RunMahJongSoulRecordAnalysisTask( + handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex]) case "jumpRound": - h.majsoulCurrentRoundIndex = clickActionIndex % len(h.majsoulCurrentRecordActionsList) - h.majsoulCurrentActionIndex = 0 - go analysisCache.runMajsoulRecordAnalysisTask(h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex]) + handler.MahJongSoulCurrentRoundIndex = clickActionIndex % len(handler.MahJongSoulCurrentRecordActionsList) + handler.MahJongSoulCurrentActionIndex = 0 + go analysisCache.RunMahJongSoulRecordAnalysisTask( + handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex]) case "nextXun", "preXun", "jumpXun", "preStep", "jumpToLastRoundXun": if clickAction == "jumpToLastRoundXun" { - h.majsoulCurrentRoundIndex = (h.majsoulCurrentRoundIndex - 1 + len(h.majsoulCurrentRecordActionsList)) % len(h.majsoulCurrentRecordActionsList) - go analysisCache.runMajsoulRecordAnalysisTask(h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex]) + handler.MahJongSoulCurrentRoundIndex = (handler.MahJongSoulCurrentRoundIndex - 1 + len( + handler.MahJongSoulCurrentRecordActionsList)) % len(handler.MahJongSoulCurrentRecordActionsList) + go analysisCache.RunMahJongSoulRecordAnalysisTask( + handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex]) } - h.majsoulRoundData.skipOutput = true - currentRoundActions := h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex] + handler.MahJongSoulRoundData.SkipOutput = true + currentRoundActions := handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex] startActionIndex := 0 endActionIndex := fastRecordTo if clickAction == "nextXun" { - startActionIndex = h.majsoulCurrentActionIndex + 1 + startActionIndex = handler.MahJongSoulCurrentActionIndex + 1 } - if debugMode { - fmt.Printf("快速处理牌谱中的操作:局 %d 动作 %d-%d\n", h.majsoulCurrentRoundIndex, startActionIndex, endActionIndex) + if DebugMode { + fmt.Printf("快速处理牌谱中的操作:局 %d 动作 %d-%d\n", handler.MahJongSoulCurrentRoundIndex, + startActionIndex, endActionIndex) } for i, action := range currentRoundActions[startActionIndex : endActionIndex+1] { - if debugMode { - fmt.Printf("快速处理牌谱中的操作:局 %d 动作 %d\n", h.majsoulCurrentRoundIndex, startActionIndex+i) + if DebugMode { + fmt.Printf("快速处理牌谱中的操作:局 %d 动作 %d\n", handler.MahJongSoulCurrentRoundIndex, + startActionIndex+i) } - h._analysisMajsoulRoundData(action.Action, "") + handler.analysisMahJongSoulRoundData(action.Action, "") } - h.majsoulRoundData.skipOutput = false + handler.MahJongSoulRoundData.SkipOutput = false - h.majsoulCurrentActionIndex = endActionIndex + 1 + handler.MahJongSoulCurrentActionIndex = endActionIndex + 1 default: return } - if debugMode { - fmt.Printf("处理牌谱中的操作:局 %d 动作 %d\n", h.majsoulCurrentRoundIndex, h.majsoulCurrentActionIndex) + if DebugMode { + fmt.Printf("处理牌谱中的操作:局 %d 动作 %d\n", handler.MahJongSoulCurrentRoundIndex, handler.MahJongSoulCurrentActionIndex) } - action := h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex][h.majsoulCurrentActionIndex] - h._analysisMajsoulRoundData(action.Action, "") + action := handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex][handler.MahJongSoulCurrentActionIndex] + handler.analysisMahJongSoulRoundData(action.Action, "") if action.Name == "RecordHule" || action.Name == "RecordLiuJu" || action.Name == "RecordNoTile" { // 播放和牌/流局动画,进入下一局或显示终局动画 - h.majsoulCurrentRoundIndex++ - h.majsoulCurrentActionIndex = 0 - if h.majsoulCurrentRoundIndex == len(h.majsoulCurrentRecordActionsList) { - h.majsoulCurrentRoundIndex = 0 + handler.MahJongSoulCurrentRoundIndex++ + handler.MahJongSoulCurrentActionIndex = 0 + if handler.MahJongSoulCurrentRoundIndex == len(handler.MahJongSoulCurrentRecordActionsList) { + handler.MahJongSoulCurrentRoundIndex = 0 return } time.Sleep(time.Second) - actions := h.majsoulCurrentRecordActionsList[h.majsoulCurrentRoundIndex] - go analysisCache.runMajsoulRecordAnalysisTask(actions) + actions := handler.MahJongSoulCurrentRecordActionsList[handler.MahJongSoulCurrentRoundIndex] + go analysisCache.RunMahJongSoulRecordAnalysisTask(actions) // 分析下一局的起始信息 - data := actions[h.majsoulCurrentActionIndex].Action - h._analysisMajsoulRoundData(data, "") + data := actions[handler.MahJongSoulCurrentActionIndex].Action + handler.analysisMahJongSoulRoundData(data, "") } } -var h *mjHandler +var handler *MahJongHandler -func getMajsoulCurrentRecordUUID() string { - return h.majsoulCurrentRecordUUID +func GetMajsoulCurrentRecordUUID() string { + return handler.MahJongSoulCurrentRecordUUID } -func runServer(isHTTPS bool, port int) (err error) { - e := echo.New() +func RunServer(isHTTPS bool, port int) (err error) { + echo := echo.New() // 移除 echo.Echo 和 http.Server 在控制台上打印的信息 - e.HideBanner = true - e.HidePort = true - e.StdLogger = stdLog.New(ioutil.Discard, "", 0) + echo.HideBanner = true + echo.HidePort = true + echo.StdLogger = stdLog.New(ioutil.Discard, "", 0) // 默认是 log.ERROR - e.Logger.SetLevel(log.INFO) + echo.Logger.SetLevel(log.INFO) // 设置日志输出到 log/gamedata-xxx.log - filePath, err := newLogFilePath() + filePath, err := NewLogFilePath() if err != nil { return } @@ -498,45 +518,45 @@ func runServer(isHTTPS bool, port int) (err error) { if err != nil { return } - e.Logger.SetOutput(logFile) + echo.Logger.SetOutput(logFile) - e.Logger.Info("============================================================================================") - e.Logger.Info("服务启动") + echo.Logger.Info("============================================================================================") + echo.Logger.Info("服务启动") - h = &mjHandler{ - log: e.Logger, + handler = &MahJongHandler{ + Log: echo.Logger, - tenhouMessageReceiver: tenhou.NewMessageReceiver(), - tenhouRoundData: &tenhouRoundData{isRoundEnd: true}, + TenhouMessageReceiver: tenhou.NewMessageReceiver(), + TenHouRoundData: &TenHouRoundData{IsRoundEnd: true}, majsoulMessageQueue: make(chan []byte, 100), - majsoulRoundData: &majsoulRoundData{selfSeat: -1}, - majsoulRecordMap: map[string]*majsoulRecordBaseInfo{}, + MahJongSoulRoundData: &MahJongSoulRoundData{SelfSeat: -1}, + MahJongSoulRecordMap: map[string]*MahJongSoulRecordBaseInfo{}, } - h.tenhouRoundData.roundData = newGame(h.tenhouRoundData) - h.majsoulRoundData.roundData = newGame(h.majsoulRoundData) + handler.TenHouRoundData.RoundData = NewGame(handler.TenHouRoundData) + handler.MahJongSoulRoundData.RoundData = NewGame(handler.MahJongSoulRoundData) - go h.runAnalysisTenhouMessageTask() - go h.runAnalysisMajsoulMessageTask() + go handler.RunAnalysisTenHouMessageTask() + go handler.RunAnalysisMahJongSoulMessageTask() - e.Use(middleware.Recover()) - e.Use(middleware.CORS()) - e.GET("/", h.index) - e.POST("/debug", h.index) - e.POST("/analysis", h.analysis) - e.POST("/tenhou", h.analysisTenhou) - e.POST("/majsoul", h.analysisMajsoul) + echo.Use(middleware.Recover()) + echo.Use(middleware.CORS()) + echo.GET("/", handler.Index) + echo.POST("/debug", handler.Index) + echo.POST("/analysis", handler.Analysis) + echo.POST("/tenhou", handler.AnalysisTenHou) + echo.POST("/majsoul", handler.AnalysisMajsoul) // code.js 也用的该端口 if port == 0 { - port = defaultPort + port = DefaultPort } addr := ":" + strconv.Itoa(port) if !isHTTPS { - e.POST("/", h.analysisTenhou) - err = e.Start(addr) + echo.POST("/", handler.AnalysisTenHou) + err = echo.Start(addr) } else { - e.POST("/", h.analysisMajsoul) - err = startTLS(e, addr) + echo.POST("/", handler.AnalysisMajsoul) + err = StartTLS(echo, addr) } if err != nil { // 检查是否为端口占用错误 @@ -603,7 +623,7 @@ Y5quoWDnJFfyYohaUAC7OAKR ` ) -func startTLS(e *echo.Echo, address string) (err error) { +func StartTLS(e *echo.Echo, address string) (err error) { s := e.TLSServer s.TLSConfig = new(tls.Config) s.TLSConfig.Certificates = make([]tls.Certificate, 1) diff --git a/server_test.go b/server_test.go index 6634c2e..14fc055 100644 --- a/server_test.go +++ b/server_test.go @@ -1,28 +1,29 @@ package main import ( - "testing" - "time" - "fmt" "encoding/json" - "strings" + "fmt" "io/ioutil" + "strings" + "testing" + "time" + "github.com/EndlessCheng/mahjong-helper/util/debug" ) func Test_mjHandler_runAnalysisMajsoulMessageTask(t *testing.T) { - debugMode = true + DebugMode = true logFile := "log/gamedata.log" startLo := 33020 endLo := 33369 - h := &mjHandler{ - majsoulMessageQueue: make(chan []byte, 10000), - majsoulRoundData: &majsoulRoundData{}, - majsoulRecordMap: map[string]*majsoulRecordBaseInfo{}, + h := &MahJongHandler{ + majsoulMessageQueue: make(chan []byte, 10000), + MahJongSoulRoundData: &MahJongSoulRoundData{}, + MahJongSoulRecordMap: map[string]*MahJongSoulRecordBaseInfo{}, } - h.majsoulRoundData.roundData = newGame(h.majsoulRoundData) + h.MahJongSoulRoundData.RoundData = NewGame(h.MahJongSoulRoundData) s := struct { Level string `json:"level"` @@ -50,7 +51,7 @@ func Test_mjHandler_runAnalysisMajsoulMessageTask(t *testing.T) { h.majsoulMessageQueue <- []byte([]byte(s.Message)) } - go h.runAnalysisMajsoulMessageTask() + go h.RunAnalysisMahJongSoulMessageTask() for { if len(h.majsoulMessageQueue) == 0 { diff --git a/tenhou.go b/tenhou.go index 1b87fc2..94440e9 100644 --- a/tenhou.go +++ b/tenhou.go @@ -1,14 +1,15 @@ package main import ( - "strings" - "strconv" "fmt" + "net/url" "regexp" - "github.com/EndlessCheng/mahjong-helper/util/model" "sort" + "strconv" + "strings" + "github.com/EndlessCheng/mahjong-helper/util" - "net/url" + "github.com/EndlessCheng/mahjong-helper/util/model" "github.com/fatih/color" ) @@ -142,7 +143,7 @@ const ( redFiveSou = 88 ) -type tenhouMessage struct { +type TenhouMessage struct { Tag string `json:"tag" xml:"-"` //Name string `json:"name"` // id @@ -225,16 +226,16 @@ type tenhouMessage struct { // -type tenhouRoundData struct { - *roundData +type TenHouRoundData struct { + *RoundData - originJSON string - msg *tenhouMessage + OriginJSON string + Msg *TenhouMessage - isRoundEnd bool // 某人和牌或流局。初始值为 true + IsRoundEnd bool // 某人和牌或流局。初始值为 true } -func (*tenhouRoundData) _tenhouTileToTile34(tenhouTile int) int { +func (*TenHouRoundData) _tenhouTileToTile34(tenhouTile int) int { return tenhouTile / 4 } @@ -242,7 +243,7 @@ func (*tenhouRoundData) _tenhouTileToTile34(tenhouTile int) int { // 36-71 p // 72-107 s // 108- z -func (d *tenhouRoundData) _parseTenhouTile(tenhouTile string) (tile int, isRedFive bool) { +func (d *TenHouRoundData) _parseTenhouTile(tenhouTile string) (tile int, isRedFive bool) { t, err := strconv.Atoi(tenhouTile) if err != nil { panic(err) @@ -274,7 +275,7 @@ CHI Called: Which tile out of the three was called. */ -func (*tenhouRoundData) _parseChi(data int) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { +func (*TenHouRoundData) _parseChi(data int) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { // 吃 meldType = meldTypeChi t0, t1, t2 := (data>>3)&0x3, (data>>5)&0x3, (data>>7)&0x3 @@ -314,7 +315,7 @@ PON or KAKAN Called: Which tile out of the three was called. */ -func (*tenhouRoundData) _parsePonOrKakan(data int) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { +func (*TenHouRoundData) _parsePonOrKakan(data int) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { t4 := (data >> 5) & 0x3 _t := [4][3]int{{1, 2, 3}, {0, 2, 3}, {0, 1, 3}, {0, 1, 2}}[t4] t0, t1, t2 := _t[0], _t[1], _t[2] @@ -354,7 +355,7 @@ KAN Called: Which tile out of the four was called. */ -func (*tenhouRoundData) _parseKan(data int) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { +func (*TenHouRoundData) _parseKan(data int) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { baseAndCalled := data >> 8 base, called := baseAndCalled/4, baseAndCalled%4 tenhouMeldTiles = []int{4 * base, 1 + 4*base, 2 + 4*base, 3 + 4*base} @@ -369,7 +370,7 @@ func (*tenhouRoundData) _parseKan(data int) (meldType int, tenhouMeldTiles []int return } -func (d *tenhouRoundData) _parseTenhouMeld(data string) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { +func (d *TenHouRoundData) _parseTenhouMeld(data string) (meldType int, tenhouMeldTiles []int, tenhouCalledTile int) { bits, err := strconv.Atoi(data) if err != nil { panic(err) @@ -388,11 +389,11 @@ func (d *tenhouRoundData) _parseTenhouMeld(data string) (meldType int, tenhouMel } } -func (*tenhouRoundData) isRedFive(tenhouTile int) bool { +func (*TenHouRoundData) isRedFive(tenhouTile int) bool { return tenhouTile == redFiveMan || tenhouTile == redFivePin || tenhouTile == redFiveSou } -func (d *tenhouRoundData) containRedFive(tenhouTiles []int) bool { +func (d *TenHouRoundData) containRedFive(tenhouTiles []int) bool { for _, tenhouTile := range tenhouTiles { if d.isRedFive(tenhouTile) { return true @@ -401,32 +402,32 @@ func (d *tenhouRoundData) containRedFive(tenhouTiles []int) bool { return false } -func (d *tenhouRoundData) GetDataSourceType() int { +func (d *TenHouRoundData) GetDataSourceType() int { return dataSourceTypeTenhou } -func (d *tenhouRoundData) GetSelfSeat() int { +func (d *TenHouRoundData) GetSelfSeat() int { return -1 } -func (d *tenhouRoundData) GetMessage() string { - return d.originJSON +func (d *TenHouRoundData) GetMessage() string { + return d.OriginJSON } -func (d *tenhouRoundData) SkipMessage() bool { +func (d *TenHouRoundData) SkipMessage() bool { // 注意:即使没有获取到用户名也能正常进行游戏 return false } -func (d *tenhouRoundData) IsLogin() bool { +func (d *TenHouRoundData) IsLogin() bool { // TODO: 重连时要填入 gameConf.currentActiveTenhouUsername - return d.msg.Tag == "HELO" + return d.Msg.Tag == "HELO" } -func (d *tenhouRoundData) HandleLogin() { - username, err := url.QueryUnescape(d.msg.UserName) +func (d *TenHouRoundData) HandleLogin() { + username, err := url.QueryUnescape(d.Msg.UserName) if err != nil { - h.logError(err) + handler.LogError(err) } if username != gameConf.currentActiveTenhouUsername { color.HiGreen("%s 登录成功", username) @@ -434,34 +435,34 @@ func (d *tenhouRoundData) HandleLogin() { } } -func (d *tenhouRoundData) IsInit() bool { - return d.msg.Tag == "INIT" || d.msg.Tag == "REINIT" +func (d *TenHouRoundData) IsInit() bool { + return d.Msg.Tag == "INIT" || d.Msg.Tag == "REINIT" } -func (d *tenhouRoundData) ParseInit() (roundNumber int, benNumber int, dealer int, doraIndicators []int, handTiles []int, numRedFives []int) { - d.isRoundEnd = false +func (d *TenHouRoundData) ParseInit() (roundNumber int, benNumber int, dealer int, doraIndicators []int, handTiles []int, numRedFives []int) { + d.IsRoundEnd = false - seedSplits := strings.Split(d.msg.Seed, ",") + seedSplits := strings.Split(d.Msg.Seed, ",") if len(seedSplits) != 6 { - panic(fmt.Sprintln("seed 解析失败", d.msg.Seed)) + panic(fmt.Sprintln("seed 解析失败", d.Msg.Seed)) } roundNumber, _ = strconv.Atoi(seedSplits[0]) benNumber, _ = strconv.Atoi(seedSplits[1]) // TODO: 重构至 core。parser 不要修改任何东西 if roundNumber == 0 && benNumber == 0 { - if util.InStrings("0", strings.Split(d.msg.Ten, ",")) { + if util.InStrings("0", strings.Split(d.Msg.Ten, ",")) { d.playerNumber = 3 } else { d.playerNumber = 4 } } - dealer, _ = strconv.Atoi(d.msg.Dealer) + dealer, _ = strconv.Atoi(d.Msg.Dealer) doraIndicator, _ := d._parseTenhouTile(seedSplits[5]) doraIndicators = append(doraIndicators, doraIndicator) numRedFives = make([]int, 3) - tenhouTiles := strings.Split(d.msg.Hai, ",") + tenhouTiles := strings.Split(d.Msg.Hai, ",") for _, tenhouTile := range tenhouTiles { tile, isRedFive := d._parseTenhouTile(tenhouTile) handTiles = append(handTiles, tile) @@ -478,12 +479,12 @@ func isTenhouSelfDraw(tag string) bool { return _selfDrawReg.MatchString(tag) } -func (d *tenhouRoundData) IsSelfDraw() bool { - return isTenhouSelfDraw(d.msg.Tag) +func (d *TenHouRoundData) IsSelfDraw() bool { + return isTenhouSelfDraw(d.Msg.Tag) } -func (d *tenhouRoundData) ParseSelfDraw() (tile int, isRedFive bool, kanDoraIndicator int) { - rawTile := d.msg.Tag[1:] +func (d *TenHouRoundData) ParseSelfDraw() (tile int, isRedFive bool, kanDoraIndicator int) { + rawTile := d.Msg.Tag[1:] tile, isRedFive = d._parseTenhouTile(rawTile) kanDoraIndicator = -1 return @@ -491,24 +492,24 @@ func (d *tenhouRoundData) ParseSelfDraw() (tile int, isRedFive bool, kanDoraIndi var _discardReg = regexp.MustCompile("^[DEFGefg][0-9]{1,3}$") -func (d *tenhouRoundData) IsDiscard() bool { - return _discardReg.MatchString(d.msg.Tag) +func (d *TenHouRoundData) IsDiscard() bool { + return _discardReg.MatchString(d.Msg.Tag) } -func (d *tenhouRoundData) ParseDiscard() (who int, discardTile int, isRedFive bool, isTsumogiri bool, isReach bool, canBeMeld bool, kanDoraIndicator int) { +func (d *TenHouRoundData) ParseDiscard() (who int, discardTile int, isRedFive bool, isTsumogiri bool, isReach bool, canBeMeld bool, kanDoraIndicator int) { // D=自家, e/E=下家, f/F=对家, g/G=上家 - who = int(util.Lower(d.msg.Tag[0]) - 'd') - rawTile := d.msg.Tag[1:] + who = int(util.Lower(d.Msg.Tag[0]) - 'd') + rawTile := d.Msg.Tag[1:] discardTile, isRedFive = d._parseTenhouTile(rawTile) - if d.msg.Tag[0] != 'D' { - isTsumogiri = d.msg.Tag[0] >= 'a' - canBeMeld = d.msg.T != "" + if d.Msg.Tag[0] != 'D' { + isTsumogiri = d.Msg.Tag[0] >= 'a' + canBeMeld = d.Msg.T != "" } kanDoraIndicator = -1 return } -func (*tenhouRoundData) isNukiOperator(data string) bool { +func (*TenHouRoundData) isNukiOperator(data string) bool { bits, err := strconv.Atoi(data) if err != nil { panic(err) @@ -516,18 +517,18 @@ func (*tenhouRoundData) isNukiOperator(data string) bool { return bits&0x4 == 0 && bits&0x18 == 0 && bits&0x20 > 0 } -func (d *tenhouRoundData) IsOpen() bool { - if d.msg.Tag != "N" { +func (d *TenHouRoundData) IsOpen() bool { + if d.Msg.Tag != "N" { return false } // 除去拔北 - return !d.isNukiOperator(d.msg.Meld) + return !d.isNukiOperator(d.Msg.Meld) } -func (d *tenhouRoundData) ParseOpen() (who int, meld *model.Meld, kanDoraIndicator int) { - who, _ = strconv.Atoi(d.msg.Who) - meldType, tenhouMeldTiles, tenhouCalledTile := d._parseTenhouMeld(d.msg.Meld) +func (d *TenHouRoundData) ParseOpen() (who int, meld *model.Meld, kanDoraIndicator int) { + who, _ = strconv.Atoi(d.Msg.Who) + meldType, tenhouMeldTiles, tenhouCalledTile := d._parseTenhouMeld(d.Msg.Meld) meldTiles := make([]int, len(tenhouMeldTiles)) for i, tenhouTile := range tenhouMeldTiles { meldTiles[i] = d._tenhouTileToTile34(tenhouTile) @@ -546,30 +547,30 @@ func (d *tenhouRoundData) ParseOpen() (who int, meld *model.Meld, kanDoraIndicat return } -func (d *tenhouRoundData) IsReach() bool { +func (d *TenHouRoundData) IsReach() bool { // Step == "1" 立直宣告 // Step == "2" 立直成功,扣1000点 - return d.msg.Tag == "REACH" && d.msg.Step == "1" + return d.Msg.Tag == "REACH" && d.Msg.Step == "1" } -func (d *tenhouRoundData) ParseReach() (who int) { - who, _ = strconv.Atoi(d.msg.Who) +func (d *TenHouRoundData) ParseReach() (who int) { + who, _ = strconv.Atoi(d.Msg.Who) return } -func (d *tenhouRoundData) IsFuriten() bool { - return d.msg.Tag == "FURITEN" +func (d *TenHouRoundData) IsFuriten() bool { + return d.Msg.Tag == "FURITEN" } -func (d *tenhouRoundData) IsRoundWin() bool { - return d.msg.Tag == "AGARI" +func (d *TenHouRoundData) IsRoundWin() bool { + return d.Msg.Tag == "AGARI" } -func (d *tenhouRoundData) ParseRoundWin() (whos []int, points []int) { - d.isRoundEnd = true +func (d *TenHouRoundData) ParseRoundWin() (whos []int, points []int) { + d.IsRoundEnd = true - who, _ := strconv.Atoi(d.msg.Who) - splits := strings.Split(d.msg.Ten, ",") + who, _ := strconv.Atoi(d.Msg.Who) + splits := strings.Split(d.Msg.Ten, ",") if len(splits) < 2 { return } @@ -577,37 +578,37 @@ func (d *tenhouRoundData) ParseRoundWin() (whos []int, points []int) { return []int{who}, []int{point} } -func (d *tenhouRoundData) IsRyuukyoku() bool { - return d.msg.Tag == "RYUUKYOKU" +func (d *TenHouRoundData) IsRyuukyoku() bool { + return d.Msg.Tag == "RYUUKYOKU" } // "{\"tag\":\"RYUUKYOKU\",\"type\":\"ron3\",\"ba\":\"1,1\",\"sc\":\"290,0,228,0,216,0,256,0\",\"hai0\":\"18,19,30,32,33,41,43,94,95,114,115,117,119\",\"hai2\":\"29,31,74,75\",\"hai3\":\"8,13,17,25,35,46,48,53,78,79\"}" -func (d *tenhouRoundData) ParseRyuukyoku() (type_ int, whos []int, points []int) { - d.isRoundEnd = true +func (d *TenHouRoundData) ParseRyuukyoku() (type_ int, whos []int, points []int) { + d.IsRoundEnd = true // TODO return } -func (d *tenhouRoundData) IsNukiDora() bool { - if d.msg.Tag != "N" { +func (d *TenHouRoundData) IsNukiDora() bool { + if d.Msg.Tag != "N" { return false } - return d.isNukiOperator(d.msg.Meld) + return d.isNukiOperator(d.Msg.Meld) } -func (d *tenhouRoundData) ParseNukiDora() (who int, isTsumogiri bool) { +func (d *TenHouRoundData) ParseNukiDora() (who int, isTsumogiri bool) { // TODO: isTsumogiri - who, _ = strconv.Atoi(d.msg.Who) + who, _ = strconv.Atoi(d.Msg.Who) return } -func (d *tenhouRoundData) IsNewDora() bool { - return d.msg.Tag == "DORA" +func (d *TenHouRoundData) IsNewDora() bool { + return d.Msg.Tag == "DORA" } -func (d *tenhouRoundData) ParseNewDora() (kanDoraIndicator int) { - kanDoraIndicator, _ = d._parseTenhouTile(d.msg.Hai) +func (d *TenHouRoundData) ParseNewDora() (kanDoraIndicator int) { + kanDoraIndicator, _ = d._parseTenhouTile(d.Msg.Hai) return } diff --git a/tenhou_test.go b/tenhou_test.go index 736483c..97a140b 100644 --- a/tenhou_test.go +++ b/tenhou_test.go @@ -2,12 +2,13 @@ package main import ( "testing" + "github.com/EndlessCheng/mahjong-helper/util" "github.com/EndlessCheng/mahjong-helper/util/model" ) func Test_parseTenhouMeld(t *testing.T) { - d := &tenhouRoundData{} + d := &TenHouRoundData{} for _, s := range []string{ "43595", "17511", @@ -17,9 +18,9 @@ func Test_parseTenhouMeld(t *testing.T) { } func TestAnalysisTilesRisk(t *testing.T) { - debugMode = true + DebugMode = true - d := newGame(&tenhouRoundData{}) + d := NewGame(&TenHouRoundData{}) handsTiles34, _, err := util.StrToTiles34("123456789m 123456789p 123456789s 1234567z") if err != nil { t.Fatal(err) @@ -64,27 +65,27 @@ func TestAnalysisTilesRisk(t *testing.T) { } func TestReg(t *testing.T) { - d := &tenhouRoundData{ - msg: &tenhouMessage{ + d := &TenHouRoundData{ + Msg: &TenhouMessage{ Tag: "T123", }, } t.Log(d.IsSelfDraw() == true) - d.msg.Tag = "TATA" + d.Msg.Tag = "TATA" t.Log(d.IsSelfDraw() == false) - d.msg.Tag = "T" + d.Msg.Tag = "T" t.Log(d.IsSelfDraw() == false) - d.msg.Tag = "T1234" + d.Msg.Tag = "T1234" t.Log(d.IsSelfDraw() == false) - d.msg.Tag = "D123" + d.Msg.Tag = "D123" t.Log(d.IsDiscard() == true) - d.msg.Tag = "E123" + d.Msg.Tag = "E123" t.Log(d.IsDiscard() == true) - d.msg.Tag = "EAAA" + d.Msg.Tag = "EAAA" t.Log(d.IsDiscard() == false) - d.msg.Tag = "E" + d.Msg.Tag = "E" t.Log(d.IsDiscard() == false) - d.msg.Tag = "E123123" + d.Msg.Tag = "E123123" t.Log(d.IsDiscard() == false) } diff --git a/utils.go b/utils.go index de55583..dca696b 100644 --- a/utils.go +++ b/utils.go @@ -1,13 +1,14 @@ package main import ( + "bufio" "fmt" "os" + "github.com/fatih/color" - "bufio" ) -func errorExit(args ...interface{}) { +func ErrorExit(args ...interface{}) { fmt.Fprintln(os.Stderr, args...) fmt.Println("按任意键退出...") bufio.NewReader(os.Stdin).ReadByte() @@ -21,7 +22,7 @@ func getWaitsCountColor(shanten int, waitsCount float64) color.Attribute { _getWaitsCountColor := func(fixedWaitsCount float64) color.Attribute { switch { case fixedWaitsCount < 13: // 4.3*3 - return color.FgHiCyan // FgHiBlue FgHiCyan + return color.FgHiCyan // FgHiBlue FgHiCyan case fixedWaitsCount <= 18: // 6*3 return color.FgHiYellow default: // >6*3 diff --git a/version.go b/version.go index 6c23485..79215a5 100644 --- a/version.go +++ b/version.go @@ -1,20 +1,21 @@ package main import ( + "encoding/json" + "fmt" "net/http" "time" - "fmt" - "encoding/json" + "github.com/fatih/color" ) -const versionDev = "dev" +const VersionDev = "dev" // 编译时自动写入版本号 -// go build -ldflags "-X main.version=$(git describe --abbrev=0 --tags)" -o mahjong-helper -var version = versionDev +// go build -ldflags "-X main.Version=$(git describe --abbrev=0 --tags)" -o mahjong-helper +var Version = VersionDev -func fetchLatestVersionTag() (latestVersionTag string, err error) { +func FetchLatestVersionTag() (latestVersionTag string, err error) { const apiGetLatestRelease = "https://api.github.com/repos/EndlessCheng/mahjong-helper/releases/latest" const timeout = 10 * time.Second @@ -39,10 +40,10 @@ func fetchLatestVersionTag() (latestVersionTag string, err error) { return d.TagName, nil } -func checkNewVersion(currentVersionTag string) { +func CheckNewVersion(currentVersionTag string) { const latestReleasePage = "https://github.com/EndlessCheng/mahjong-helper/releases/latest" - latestVersionTag, err := fetchLatestVersionTag() + latestVersionTag, err := FetchLatestVersionTag() if err != nil { // 下次再说~ return diff --git a/version_test.go b/version_test.go index 24e5bbe..0f26070 100644 --- a/version_test.go +++ b/version_test.go @@ -2,11 +2,12 @@ package main import ( "testing" + "github.com/stretchr/testify/assert" ) func Test_checkNewVersion(t *testing.T) { - latestVersionTag, err := fetchLatestVersionTag() + latestVersionTag, err := FetchLatestVersionTag() if err != nil { t.Fatal(err) } From a3e44729b4e03242cf946f06e1e676213d90e0f2 Mon Sep 17 00:00:00 2001 From: littletrainee Date: Thu, 16 Jun 2022 21:40:07 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E5=B0=87core=E8=88=87cli=E6=AA=94=E6=A1=88?= =?UTF-8?q?=E5=81=9A=E4=BA=9B=E5=BE=AE=E5=88=86=E5=89=B2=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E6=9C=AA=E4=BE=86=E7=9A=84=E7=B6=AD=E8=AD=B7=E4=BE=BF?= =?UTF-8?q?=E5=88=A9=E6=80=A7=20=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86functi?= =?UTF-8?q?on=E7=9A=84=E5=A4=A7=E5=B0=8F=E5=AF=AB=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E8=A8=AA=E5=95=8F=E6=AC=8A=E9=99=90=E6=9C=89=E6=89=80=E9=99=90?= =?UTF-8?q?=E5=88=B6=EF=BC=8C=E9=81=BF=E5=85=8D=E7=A8=8B=E5=BC=8F=E7=A2=BC?= =?UTF-8?q?=E7=9A=84=E5=95=8F=E5=AE=9A=E6=80=A7=E4=B8=8B=E9=99=8D=20?= =?UTF-8?q?=E5=B0=87cli=E7=9A=84"=E3=83=89"=E8=AE=8A=E6=9B=B4=E7=82=BA?= =?UTF-8?q?=E7=B4=85=E8=89=B2=E5=89=8D=E8=89=B2=E7=9A=84"=E5=88=87"?= =?UTF-8?q?=EF=BC=8C=E4=B8=A6=E5=84=AA=E5=8C=96=E9=8A=83=E7=89=8C=E9=A2=A8?= =?UTF-8?q?=E9=9A=AA=E7=9A=84=E9=A1=AF=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- console.go => Console/console.go | 4 +- IDataParser.go | 80 +++ README.md | 14 +- analysis.go | 37 +- analysis_cache.go | 17 +- cli.go | 580 ++---------------- cli_AnalysisResult.go | 293 +++++++++ cli_RiskInfo.go | 182 ++++++ cli_RiskTable.go | 68 +++ core.go | 904 +--------------------------- core_PlayerInfo.go | 102 ++++ core_RoundData.go | 768 +++++++++++++++++++++++ main.go | 41 +- majsoul.go | 2 +- majsoul_record.go | 4 +- platform/majsoul/api/liqi_test.go | 4 +- platform/majsoul/proto/lq/helper.go | 2 +- server.go | 9 +- tenhou.go | 2 +- util/agari_test.go | 34 +- util/model/meld.go | 4 +- util/model/player_info.go | 6 +- util/point.go | 4 +- util/shanten_improve.go | 31 +- util/shanten_improve_test.go | 7 +- util/shanten_search_test.go | 22 +- util/tenpai_data.go | 2 +- util/tile.go | 26 +- util/util.go | 2 +- util/yaku_data.go | 45 +- utils.go | 24 +- version.go | 58 +- version_test.go | 2 +- 33 files changed, 1764 insertions(+), 1616 deletions(-) rename console.go => Console/console.go (93%) create mode 100644 IDataParser.go create mode 100644 cli_AnalysisResult.go create mode 100644 cli_RiskInfo.go create mode 100644 cli_RiskTable.go create mode 100644 core_PlayerInfo.go create mode 100644 core_RoundData.go diff --git a/console.go b/Console/console.go similarity index 93% rename from console.go rename to Console/console.go index 76a9fc4..1354c18 100644 --- a/console.go +++ b/Console/console.go @@ -1,4 +1,4 @@ -package main +package Console import ( "os" @@ -23,7 +23,7 @@ func init() { } } -func ClearConsole() { +func ClearScreen() { if clearFunc, ok := clearFuncMap[runtime.GOOS]; ok { clearFunc() } diff --git a/IDataParser.go b/IDataParser.go new file mode 100644 index 0000000..4f270ff --- /dev/null +++ b/IDataParser.go @@ -0,0 +1,80 @@ +package main + +import "github.com/EndlessCheng/mahjong-helper/util/model" + +type DataParser interface { + // 数据来源(是天凤还是雀魂) + GetDataSourceType() int + + // 获取自家初始座位:0-第一局的东家 1-第一局的南家 2-第一局的西家 3-第一局的北家 + // 仅处理雀魂数据,天凤返回 -1 + GetSelfSeat() int + + // 原始 JSON + GetMessage() string + + // 解析前,根据消息内容来决定是否要进行后续解析 + SkipMessage() bool + + // 尝试解析用户名 + IsLogin() bool + HandleLogin() + + // round 开始/重连 + // roundNumber: 场数(如东1为0,东2为1,...,南1为4,...,南4为7,...),对于三麻来说南1也是4 + // benNumber: 本场数 + // dealer: 庄家 0-3 + // doraIndicators: 宝牌指示牌 + // handTiles: 手牌 + // numRedFives: 按照 mps 的顺序,赤5个数 + IsInit() bool + ParseInit() (roundNumber int, benNumber int, dealer int, doraIndicators []int, handTiles []int, numRedFives []int) + + // 自家摸牌 + // tile: 0-33 + // isRedFive: 是否为赤5 + // kanDoraIndicator: 摸牌时,若为暗杠摸的岭上牌,则可以翻出杠宝牌指示牌,否则返回 -1(目前恒为 -1,见 IsNewDora) + IsSelfDraw() bool + ParseSelfDraw() (tile int, isRedFive bool, kanDoraIndicator int) + + // 舍牌 + // who: 0=自家, 1=下家, 2=对家, 3=上家 + // isTsumogiri: 是否为摸切(who=0 时忽略该值) + // isReach: 是否为立直宣言(isReach 对于天凤来说恒为 false,见 IsReach) + // canBeMeld: 是否可以鳴牌(who=0 时忽略该值) + // kanDoraIndicator: 大明杠/加杠的杠宝牌指示牌,在切牌后出现,没有则返回 -1(天凤恒为-1,见 IsNewDora) + IsDiscard() bool + ParseDiscard() (who int, discardTile int, isRedFive bool, isTsumogiri bool, isReach bool, canBeMeld bool, kanDoraIndicator int) + + // 鳴牌(含暗杠、加杠) + // kanDoraIndicator: 暗杠的杠宝牌指示牌,在他家暗杠时出现,没有则返回 -1(天凤恒为-1,见 IsNewDora) + IsOpen() bool + ParseOpen() (who int, meld *model.Meld, kanDoraIndicator int) + + // 立直声明(IsReach 对于雀魂来说恒为 false,见 ParseDiscard) + IsReach() bool + ParseReach() (who int) + + // 振听 + IsFuriten() bool + + // 本局是否和牌 + IsRoundWin() bool + ParseRoundWin() (whos []int, points []int) + + // 是否流局 + // 四风连打 四家立直 四杠散了 九种九牌 三家和了 | 流局听牌 流局未听牌 | 流局满贯 + // 三家和了 + IsRyuukyoku() bool + ParseRyuukyoku() (type_ int, whos []int, points []int) + + // 拔北宝牌 + IsNukiDora() bool + ParseNukiDora() (who int, isTsumogiri bool) + + // 这一项放在末尾处理 + // 杠宝牌(雀魂在暗杠后的摸牌时出现) + // kanDoraIndicator: 0-33 + IsNewDora() bool + ParseNewDora() (kanDoraIndicator int) +} diff --git a/README.md b/README.md index 1bfa9ad..5555541 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - [使用说明](#使用说明) - [示例](#示例) * [牌效率](#牌效率) - * [鸣牌判断](#鸣牌判断) + * [鳴牌判断](#鳴牌判断) * [手摸切与安牌显示](#手摸切与安牌显示) - [牌谱与观战](#牌谱与观战) - [其他功能说明](#其他功能说明) @@ -91,12 +91,12 @@ 补充说明: - 无改良时,不显示改良进张数 -- 鸣牌时会显示用手上的哪些牌去吃/碰,详见后文 +- 鳴牌时会显示用手上的哪些牌去吃/碰,详见后文 - 防守时,切牌的文字颜色会因这张牌的安全程度而不同,详见后文 - 门清听牌时,会显示立直的期望点数(考虑自摸、一发和里宝);若默听有役则会额外显示默听的荣和点数 - 存在高低目的场合会显示加权和率的平均点数 - 役种只对较为特殊的进行提示,如三色、一通、七对等。雀魂乱斗之间会有额外的古役提醒 -- 若鸣牌且无役会提示 `[无役]` +- 若鳴牌且无役会提示 `[无役]` - 听牌或一向听时根据自家舍牌情况提示振听 - m-万子 p-饼子 s-索子 z-字牌,顺序为东南西北白发中 @@ -140,11 +140,11 @@ ![](img/example03a.png) -### 鸣牌判断 +### 鳴牌判断 下图是一个鸣了红中之后,听坎 5s 的例子,宝牌为 6m。 -上家打出 6m 宝牌之后考虑是否鸣牌: +上家打出 6m 宝牌之后考虑是否鳴牌: 这里就可以考虑用 57m 吃,打出 9m,提升打点的同时又能维持听牌。此外,若巡目尚早可以拆掉 46s 追求混一色。 @@ -155,7 +155,7 @@ 下图展示了某局中三家的手摸切情况(宝牌为红中和 6s,自家手牌此时为 345678m 569p 45667s): - 白色为手切,暗灰色为摸切 -- 鸣牌后打出的那张牌会用灰底白字显示,供读牌分析用 +- 鳴牌后打出的那张牌会用灰底白字显示,供读牌分析用 - 副露玩家的手切中张牌(3-7)会有不同颜色的高亮,用来辅助判断其听牌率 - 玩家立直或听牌率较高时会额外显示对该玩家的安牌,用 | 分隔,左侧为现物,右侧按照危险度由低到高排序(No Chance 和 One Chance 的安牌作为补充参考显示在后面,简写为 NC 和 OC) - 下图上家亲家暗杠 2m 后 4p 立直,对家 8s 追立,下家一副露但是手切了很多中张牌,听牌率较高 @@ -200,7 +200,7 @@ `mahjong-helper 234688m 34s # 6666P 234p` -- 分析鸣牌 +- 分析鳴牌 在 `+` 后面添加要鸣的牌,支持用 0 表示的红宝牌 diff --git a/analysis.go b/analysis.go index bf64acb..7539591 100644 --- a/analysis.go +++ b/analysis.go @@ -19,7 +19,7 @@ func simpleBestDiscardTile(playerInfo *model.PlayerInfo) int { } else { return -1 } - if shanten == 1 && len(playerInfo.DiscardTiles) < 9 && len(results14) > 0 && len(incShantenResults14) > 0 && !playerInfo.IsNaki() { // 鸣牌时的向听倒退暂不考虑 + if shanten == 1 && len(playerInfo.DiscardTiles) < 9 && len(results14) > 0 && len(incShantenResults14) > 0 && !playerInfo.IsNaki() { // 鳴牌时的向听倒退暂不考虑 if results14[0].Result13.Waits.AllCount() < 9 && results14[0].Result13.MixedWaitsScore < incShantenResults14[0].Result13.MixedWaitsScore { bestAttackDiscardTile = incShantenResults14[0].DiscardTile } @@ -46,6 +46,7 @@ func humanHands(playerInfo *model.PlayerInfo) string { return humanHands } +// analysis player hand risk func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTable) error { // 手牌 humanTiles := humanHands(playerInfo) @@ -56,13 +57,13 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab switch countOfTiles % 3 { case 1: result := util.CalculateShantenWithImproves13(playerInfo) - fmt.Println("当前" + util.NumberToChineseShanten(result.Shanten) + ":") - r := &analysisResult{ + fmt.Println("當前" + util.NumberToChineseShanten(result.Shanten) + ":") + r := &AnalysisResult{ discardTile34: -1, result13: result, - mixedRiskTable: mixedRiskTable, + MixedRiskTable: mixedRiskTable, } - r.printWaitsWithImproves13_oneRow() + r.PrintWaitsWithImproves13_oneRow() case 2: // 分析手牌 shanten, results14, incShantenResults14 := util.CalculateShantenWithImproves14(playerInfo) @@ -74,7 +75,7 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab if len(results14) > 0 { r13 := results14[0].Result13 if r13.RiichiPoint > 0 && r13.FuritenRate == 0 && r13.DamaPoint >= 5200 && r13.DamaWaits.AllCount() == r13.Waits.AllCount() { - color.HiGreen("默听打点充足:追求和率默听,追求打点立直") + color.HiGreen("默聽打點充足,若追求和率可以默聽,追求打點可以選擇立直") } // 局收支相近时,提示:局收支相近,追求和率打xx,追求打点打xx } @@ -88,8 +89,8 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab // TODO: 接近流局时提示河底是哪家 // 何切分析结果 - printResults14WithRisk(results14, mixedRiskTable) - printResults14WithRisk(incShantenResults14, mixedRiskTable) + PrintResults14WithRisk(results14, mixedRiskTable) + PrintResults14WithRisk(incShantenResults14, mixedRiskTable) default: err := fmt.Errorf("参数错误: %d 张牌", countOfTiles) if DebugMode { @@ -102,7 +103,7 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab return nil } -// 分析鸣牌 +// 分析鳴牌 // playerInfo: 自家信息 // targetTile34: 他家舍牌 // isRedFive: 此舍牌是否为赤5 @@ -120,27 +121,27 @@ func analysisMeld(playerInfo *model.PlayerInfo, targetTile34 int, isRedFive bool return nil // fmt.Errorf("输入错误:无法鸣这张牌") } - // 鸣牌 + // 鳴牌 humanTiles := humanHands(playerInfo) handsTobeNaki := humanTiles + " " + model.SepTargetTile + " " + util.Tile34ToStr(targetTile34) + "?" fmt.Println(handsTobeNaki) fmt.Println(strings.Repeat("=", len(handsTobeNaki))) // 原始手牌分析结果 - fmt.Println("当前" + util.NumberToChineseShanten(result.Shanten) + ":") - r := &analysisResult{ + fmt.Println("當前" + util.NumberToChineseShanten(result.Shanten) + ":") + r := &AnalysisResult{ discardTile34: -1, result13: result, - mixedRiskTable: mixedRiskTable, + MixedRiskTable: mixedRiskTable, } - r.printWaitsWithImproves13_oneRow() + r.PrintWaitsWithImproves13_oneRow() // 提示信息 // TODO: 局收支相近时,提示:局收支相近,追求和率打xx,追求打点打xx if shanten == -1 { color.HiRed("【已和牌】") } else if shanten <= 1 { - // 鸣牌后听牌或一向听,提示型听 + // 鳴牌后听牌或一向听,提示型听 if len(results14) > 0 && results14[0].LeftDrawTilesCount > 0 && results14[0].LeftDrawTilesCount <= 16 { color.HiGreen("考虑型听?") } @@ -148,9 +149,9 @@ func analysisMeld(playerInfo *model.PlayerInfo, targetTile34 int, isRedFive bool // TODO: 接近流局时提示河底是哪家 - // 鸣牌何切分析结果 - printResults14WithRisk(results14, mixedRiskTable) - printResults14WithRisk(incShantenResults14, mixedRiskTable) + // 鳴牌何切分析结果 + PrintResults14WithRisk(results14, mixedRiskTable) + PrintResults14WithRisk(incShantenResults14, mixedRiskTable) return nil } diff --git a/analysis_cache.go b/analysis_cache.go index ce3531a..61322e1 100644 --- a/analysis_cache.go +++ b/analysis_cache.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/EndlessCheng/mahjong-helper/Console" "github.com/EndlessCheng/mahjong-helper/util" "github.com/fatih/color" ) @@ -26,7 +27,7 @@ type AnalysisCache struct { isRiichiWhenDiscard bool meldType int - // 用手牌中的什么牌去鸣牌,空就是跳过不鸣 + // 用手牌中的什么牌去鳴牌,空就是跳过不鸣 selfOpenTiles []int aiAttackDiscardTile int @@ -83,7 +84,7 @@ func (roundAnalysisCache *RoundAnalysisCache) Print() { if info == EmptyInfo || risk < 5 { fmt.Print(info) } else { - color.New(getNumRiskColor(risk)).Print(info) + color.New(GetNumRiskColor(risk)).Print(info) } fmt.Print(suffix) } @@ -103,7 +104,7 @@ func (roundAnalysisCache *RoundAnalysisCache) Print() { } fmt.Println() - fmt.Print("进攻推荐") + fmt.Print("進攻推薦") if done { for _, c := range roundAnalysisCache.Cache { printTileInfo(c.aiAttackDiscardTile, c.aiAttackDiscardTileRisk, "") @@ -111,7 +112,7 @@ func (roundAnalysisCache *RoundAnalysisCache) Print() { } fmt.Println() - fmt.Print("防守推荐") + fmt.Print("防守推薦") if done { for _, c := range roundAnalysisCache.Cache { printTileInfo(c.aiDefenceDiscardTile, c.aiDefenceDiscardTileRisk, "") @@ -122,7 +123,7 @@ func (roundAnalysisCache *RoundAnalysisCache) Print() { fmt.Println() } -// (摸牌后、鸣牌后的)实际舍牌 +// (摸牌后、鳴牌后的)实际舍牌 func (rc *RoundAnalysisCache) AddSelfDiscardTile(tile int, risk float64, isRiichiWhenDiscard bool) { latestCache := rc.Cache[len(rc.Cache)-1] latestCache.selfDiscardTile = tile @@ -270,8 +271,8 @@ func (cache *GameAnalysisCache) RunMahJongSoulRecordAnalysisTask(actions MahJong // 遍历自家舍牌,找到舍牌前的操作 // 若为摸牌操作,计算出此时的 AI 进攻舍牌和防守舍牌 - // 若为鸣牌操作,计算出此时的 AI 进攻舍牌(无进攻舍牌则设为 -1),防守舍牌设为 -1 - // TODO: 玩家跳过,但是 AI 觉得应鸣牌? + // 若为鳴牌操作,计算出此时的 AI 进攻舍牌(无进攻舍牌则设为 -1),防守舍牌设为 -1 + // TODO: 玩家跳过,但是 AI 觉得应鳴牌? majsoulRoundData := &MahJongSoulRoundData{SelfSeat: cache.SelfSeat} // 注意这里是用的一个新的 majsoulRoundData 去计算的,不会有数据冲突 majsoulRoundData.RoundData = NewGame(majsoulRoundData) majsoulRoundData.RoundData.GameMode = GameModeRecordCache @@ -299,7 +300,7 @@ func (cache *GameAnalysisCache) RunMahJongSoulRecordAnalysisTask(actions MahJong return nil } - ClearConsole() + Console.ClearScreen() roundCache.Print() return nil diff --git a/cli.go b/cli.go index fe945b0..6c27d13 100644 --- a/cli.go +++ b/cli.go @@ -2,8 +2,6 @@ package main import ( "fmt" - "math" - "sort" "strings" "github.com/EndlessCheng/mahjong-helper/util" @@ -16,295 +14,11 @@ func printAccountInfo(accountID int) { fmt.Printf(",该数字为雀魂服务器账号数据库中的 ID,该值越小表示您的注册时间越早\n") } -// - -func (p *playerInfo) printDiscards() { - // TODO: 高亮不合理的舍牌或危险舍牌,如 - // - 一开始就切中张 - // - 开始切中张后,手切了幺九牌(也有可能是有人碰了牌,比如 133m 有人碰了 2m) - // - 切了 dora,提醒一下 - // - 切了赤宝牌 - // - 有人立直的情况下,多次切出危险度高的牌(有可能是对方读准了牌,或者对方手里的牌与牌河加起来产生了安牌) - // - 其余可以参考贴吧的《魔神之眼》翻译 https://tieba.baidu.com/p/3311909701 - // 举个简单的例子,如果出现手切了一个对子的情况的话那么基本上就不可能是七对子。 - // 如果对方早巡手切了一个两面搭子的话,那么就可以推理出他在做染手或者牌型是对子型,如果他立直或者鸣牌的话,也比较容易读出他的手牌。 - // https://tieba.baidu.com/p/3311909701 - // 鸣牌之后和终盘的手切牌要尽量记下来,别人手切之前的安牌应该先切掉 - // https://tieba.baidu.com/p/3372239806 - // 吃牌时候打出来的牌的颜色是危险的;碰之后全部的牌都是危险的 - - fmt.Printf(p.name + ":") - for i, disTile := range p.discardTiles { - fmt.Printf(" ") - // TODO: 显示 dora, 赤宝牌 - bgColor := color.BgBlack - fgColor := color.FgWhite - var tile string - if disTile >= 0 { // 手切 - tile = util.Mahjong[disTile] - if disTile >= 27 { - tile = util.MahjongU[disTile] // 关注字牌的手切 - } - if p.isNaki { // 副露 - fgColor = getOtherDiscardAlertColor(disTile) // 高亮中张手切 - if util.InInts(i, p.meldDiscardsAt) { - bgColor = color.BgWhite // 鸣牌时切的那张牌要背景高亮 - fgColor = color.FgBlack - } - } - } else { // 摸切 - disTile = ^disTile - tile = util.Mahjong[disTile] - fgColor = color.FgHiBlack // 暗色显示 - } - color.New(bgColor, fgColor).Print(tile) - } - fmt.Println() -} - -// - type handsRisk struct { tile int risk float64 } -// 34 种牌的危险度 -type riskTable util.RiskTiles34 - -func (t riskTable) printWithHands(hands []int, fixedRiskMulti float64) (containLine bool) { - // 打印铳率=0的牌(现物,或NC且剩余数=0) - safeCount := 0 - for i, c := range hands { - if c > 0 && t[i] == 0 { - fmt.Printf(" " + util.MahjongZH[i]) - safeCount++ - } - } - - // 打印危险牌,按照铳率排序&高亮 - handsRisks := []handsRisk{} - for i, c := range hands { - if c > 0 && t[i] > 0 { - handsRisks = append(handsRisks, handsRisk{i, t[i]}) - } - } - sort.Slice(handsRisks, func(i, j int) bool { - return handsRisks[i].risk < handsRisks[j].risk - }) - if len(handsRisks) > 0 { - if safeCount > 0 { - fmt.Print(" |") - containLine = true - } - for _, hr := range handsRisks { - // 颜色考虑了听牌率 - color.New(getNumRiskColor(hr.risk * fixedRiskMulti)).Printf(" " + util.MahjongZH[hr.tile]) - } - } - - return -} - -func (t riskTable) getBestDefenceTile(tiles34 []int) (result int) { - minRisk := 100.0 - maxRisk := 0.0 - for tile, c := range tiles34 { - if c == 0 { - continue - } - risk := t[tile] - if risk < minRisk { - minRisk = risk - result = tile - } - if risk > maxRisk { - maxRisk = risk - } - } - if maxRisk == 0 { - return -1 - } - return result -} - -// - -type riskInfo struct { - // 三麻为 3,四麻为 4 - playerNumber int - - // 该玩家的听牌率(立直时为 100.0) - tenpaiRate float64 - - // 该玩家的安牌 - // 若该玩家有杠操作,把杠的那张牌也算作安牌,这有助于判断筋壁危险度 - safeTiles34 []bool - - // 各种牌的铳率表 - riskTable riskTable - - // 剩余无筋 123789 - // 总计 18 种。剩余无筋牌数量越少,该无筋牌越危险 - leftNoSujiTiles []int - - // 是否摸切立直 - isTsumogiriRiichi bool - - // 荣和点数 - // 仅调试用 - _ronPoint float64 -} - -type riskInfoList []*riskInfo - -// 考虑了听牌率的综合危险度 -func (l riskInfoList) mixedRiskTable() riskTable { - mixedRiskTable := make(riskTable, 34) - for i := range mixedRiskTable { - mixedRisk := 0.0 - for _, ri := range l[1:] { - if ri.tenpaiRate <= 15 { - continue - } - _risk := ri.riskTable[i] * ri.tenpaiRate / 100 - mixedRisk = mixedRisk + _risk - mixedRisk*_risk/100 - } - mixedRiskTable[i] = mixedRisk - } - return mixedRiskTable -} - -func (l riskInfoList) printWithHands(hands []int, leftCounts []int) { - // 听牌率超过一定值就打印铳率 - const ( - minShownTenpaiRate4 = 50.0 - minShownTenpaiRate3 = 20.0 - ) - - minShownTenpaiRate := minShownTenpaiRate4 - if l[0].playerNumber == 3 { - minShownTenpaiRate = minShownTenpaiRate3 - } - - dangerousPlayerCount := 0 - // 打印安牌,危险牌 - names := []string{"", "下家", "对家", "上家"} - for i := len(l) - 1; i >= 1; i-- { - tenpaiRate := l[i].tenpaiRate - if len(l[i].riskTable) > 0 && (DebugMode || tenpaiRate > minShownTenpaiRate) { - dangerousPlayerCount++ - fmt.Print(names[i] + "安牌:") - //if debugMode { - //fmt.Printf("(%d*%2.2f%%听牌率)", int(l[i]._ronPoint), l[i].tenpaiRate) - //} - containLine := l[i].riskTable.printWithHands(hands, tenpaiRate/100) - - // 打印听牌率 - fmt.Print(" ") - if !containLine { - fmt.Print(" ") - } - fmt.Print("[") - if tenpaiRate == 100 { - fmt.Print("100.%") - } else { - fmt.Printf("%4.1f%%", tenpaiRate) - } - fmt.Print("听牌率]") - - // 打印无筋数量 - fmt.Print(" ") - const badMachiLimit = 3 - noSujiInfo := "" - if l[i].isTsumogiriRiichi { - noSujiInfo = "摸切立直" - } else if len(l[i].leftNoSujiTiles) == 0 { - noSujiInfo = "愚形听牌/振听" - } else if len(l[i].leftNoSujiTiles) <= badMachiLimit { - noSujiInfo = "可能愚形听牌/振听" - } - if noSujiInfo != "" { - fmt.Printf("[%d无筋: ", len(l[i].leftNoSujiTiles)) - color.New(color.FgHiYellow).Printf("%s", noSujiInfo) - fmt.Print("]") - } else { - fmt.Printf("[%d无筋]", len(l[i].leftNoSujiTiles)) - } - - fmt.Println() - } - } - - // 若不止一个玩家立直/副露,打印加权综合铳率(考虑了听牌率) - mixedPlayers := 0 - for _, ri := range l[1:] { - if ri.tenpaiRate > 0 { - mixedPlayers++ - } - } - if dangerousPlayerCount > 0 && mixedPlayers > 1 { - fmt.Print("综合安牌:") - mixedRiskTable := l.mixedRiskTable() - mixedRiskTable.printWithHands(hands, 1) - fmt.Println() - } - - // 打印因 NC OC 产生的安牌 - // TODO: 重构至其他函数 - if dangerousPlayerCount > 0 { - ncSafeTileList := util.CalcNCSafeTiles(leftCounts).FilterWithHands(hands) - ocSafeTileList := util.CalcOCSafeTiles(leftCounts).FilterWithHands(hands) - if len(ncSafeTileList) > 0 { - fmt.Printf("NC:") - for _, safeTile := range ncSafeTileList { - fmt.Printf(" " + util.MahjongZH[safeTile.Tile34]) - } - fmt.Println() - } - if len(ocSafeTileList) > 0 { - fmt.Printf("OC:") - for _, safeTile := range ocSafeTileList { - fmt.Printf(" " + util.MahjongZH[safeTile.Tile34]) - } - fmt.Println() - } - - // 下面这个是另一种显示方式:显示壁牌 - //printedNC := false - //for i, c := range leftCounts[:27] { - // if c != 0 || i%9 == 0 || i%9 == 8 { - // continue - // } - // if !printedNC { - // printedNC = true - // fmt.Printf("NC:") - // } - // fmt.Printf(" " + util.MahjongZH[i]) - //} - //if printedNC { - // fmt.Println() - //} - //printedOC := false - //for i, c := range leftCounts[:27] { - // if c != 1 || i%9 == 0 || i%9 == 8 { - // continue - // } - // if !printedOC { - // printedOC = true - // fmt.Printf("OC:") - // } - // fmt.Printf(" " + util.MahjongZH[i]) - //} - //if printedOC { - // fmt.Println() - //} - fmt.Println() - } -} - -// - func alertBackwardToShanten2(results util.Hand14AnalysisResultList, incShantenResults util.Hand14AnalysisResultList) { if len(results) == 0 || len(incShantenResults) == 0 { return @@ -321,41 +35,41 @@ func alertBackwardToShanten2(results util.Hand14AnalysisResultList, incShantenRe var yakuTypesToAlert = []int{ //util.YakuKokushi, //util.YakuKokushi13, - util.YakuSuuAnkou, - util.YakuSuuAnkouTanki, - util.YakuDaisangen, - util.YakuShousuushii, - util.YakuDaisuushii, - util.YakuTsuuiisou, - util.YakuChinroutou, - util.YakuRyuuiisou, - util.YakuChuuren, - util.YakuChuuren9, + util.YakuSuuAnkou, // 四暗刻 + util.YakuSuuAnkouTanki, // 四暗刻單騎 + util.YakuDaisangen, // 大三元 + util.YakuShousuushii, // 小四喜 + util.YakuDaisuushii, // 大四喜 + util.YakuTsuuiisou, // 字一色 + util.YakuChinroutou, // 清老頭 + util.YakuRyuuiisou, // 綠一色 + util.YakuChuuren, // 九蓮寶燈 + util.YakuChuuren9, // 純正九蓮寶燈 util.YakuSuuKantsu, - //util.YakuTenhou, - //util.YakuChiihou, - - util.YakuChiitoi, - util.YakuPinfu, - util.YakuRyanpeikou, - util.YakuIipeikou, - util.YakuSanshokuDoujun, - util.YakuIttsuu, - util.YakuToitoi, - util.YakuSanAnkou, - util.YakuSanshokuDoukou, - util.YakuSanKantsu, - util.YakuTanyao, - util.YakuChanta, - util.YakuJunchan, - util.YakuHonroutou, - util.YakuShousangen, - util.YakuHonitsu, - util.YakuChinitsu, - - util.YakuShiiaruraotai, - util.YakuUumensai, - util.YakuSanrenkou, + // util.YakuTenhou, // 天胡 + // util.YakuChiihou, // 地胡 + + util.YakuChiitoi, // 七對子 + util.YakuPinfu, // 平胡 + util.YakuRyanpeikou, // 二盃口 + util.YakuIipeikou, // 一盃口 + util.YakuSanshokuDoujun, // 三色同順 + util.YakuIttsuu, // 一氣貫通 + util.YakuToitoi, // 對對胡 + util.YakuSanAnkou, // 三暗刻 + util.YakuSanshokuDoukou, // 三色同刻 + util.YakuSanKantsu, // 三槓子 + util.YakuTanyao, // 斷么九 + util.YakuChanta, // 混全帶么九 + util.YakuJunchan, // 純全帶么九 + util.YakuHonroutou, // 混老頭 + util.YakuShousangen, // 小三元 + util.YakuHonitsu, // 混一色 + util.YakuChinitsu, // 清一色 + + util.YakuShiiaruraotai, // 十二落台 + util.YakuUumensai, // 五門齊 + util.YakuSanrenkou, // 三連刻 util.YakuIsshokusanjun, } @@ -419,9 +133,9 @@ func printWaitsWithImproves13_twoRows(result13 *util.Hand13AnalysisResult, disca color.New(c).Printf("%5.2f", result13.AvgNextShantenWaitsCount) fmt.Printf(" %s", util.NumberToChineseShanten(shanten-1)) if shanten >= 2 { - fmt.Printf("进张") + fmt.Printf("進張") } else { // shanten == 1 - fmt.Printf("数") + fmt.Printf("數") if ShowAgariAboveShanten1 { fmt.Printf("(%.2f%% 参考和率)", result13.AvgAgariRate) } @@ -440,219 +154,7 @@ func printWaitsWithImproves13_twoRows(result13 *util.Hand13AnalysisResult, disca fmt.Println() } -type analysisResult struct { - discardTile34 int - isDiscardTileDora bool - openTiles34 []int - result13 *util.Hand13AnalysisResult - - mixedRiskTable riskTable - - highlightAvgImproveWaitsCount bool - highlightMixedScore bool -} - -/* -4[ 4.56] 切 8饼 => 44.50% 参考和率[ 4 改良] [7p 7s] [默听2000] [三色] [振听] - -4[ 4.56] 切 8饼 => 0.00% 参考和率[ 4 改良] [7p 7s] [无役] - -31[33.58] 切7索 => 5.23听牌数 [19.21速度] [16改良] [6789p 56789s] [局收支3120] [可能振听] - -48[50.64] 切5饼 => 24.25一向听 [12改良] [123456789p 56789s] - -31[33.62] 77索碰,切5饼 => 5.48听牌数 [15 改良] [123456789p] - -*/ -// 打印何切分析结果(单行) -func (r *analysisResult) printWaitsWithImproves13_oneRow() { - discardTile34 := r.discardTile34 - openTiles34 := r.openTiles34 - result13 := r.result13 - - shanten := result13.Shanten - - // 进张数 - waitsCount := result13.Waits.AllCount() - c := getWaitsCountColor(shanten, float64(waitsCount)) - color.New(c).Printf("%2d", waitsCount) - // 改良进张均值 - if len(result13.Improves) > 0 { - if r.highlightAvgImproveWaitsCount { - color.New(color.FgHiWhite).Printf("[%5.2f]", result13.AvgImproveWaitsCount) - } else { - fmt.Printf("[%5.2f]", result13.AvgImproveWaitsCount) - } - } else { - fmt.Print(strings.Repeat(" ", 7)) - } - - fmt.Print(" ") - - // 是否为3k+2张牌的何切分析 - if discardTile34 != -1 { - // 鸣牌分析 - if len(openTiles34) > 0 { - meldType := "吃" - if openTiles34[0] == openTiles34[1] { - meldType = "碰" - } - color.New(color.FgHiWhite).Printf("%s%s", string([]rune(util.MahjongZH[openTiles34[0]])[:1]), util.MahjongZH[openTiles34[1]]) - fmt.Printf("%s,", meldType) - } - // 舍牌 - if r.isDiscardTileDora { - color.New(color.FgHiWhite).Print("ド") - } else { - fmt.Print("切") - } - tileZH := util.MahjongZH[discardTile34] - if discardTile34 >= 27 { - tileZH = " " + tileZH - } - if r.mixedRiskTable != nil { - // 若有实际危险度,则根据实际危险度来显示舍牌危险度 - risk := r.mixedRiskTable[discardTile34] - if risk == 0 { - fmt.Print(tileZH) - } else { - color.New(getNumRiskColor(risk)).Print(tileZH) - } - } else { - fmt.Print(tileZH) - } - } - - fmt.Print(" => ") - - if shanten >= 1 { - // 前进后的进张数均值 - incShanten := shanten - 1 - c := getWaitsCountColor(incShanten, result13.AvgNextShantenWaitsCount) - color.New(c).Printf("%5.2f", result13.AvgNextShantenWaitsCount) - fmt.Printf("%s", util.NumberToChineseShanten(incShanten)) - if incShanten >= 1 { - //fmt.Printf("进张") - } else { // incShanten == 0 - fmt.Printf("数") - //if showAgariAboveShanten1 { - // fmt.Printf("(%.2f%% 参考和率)", result13.AvgAgariRate) - //} - } - } else { // shanten == 0 - // 前进后的和率 - // 若振听或片听,则标红 - if result13.FuritenRate == 1 || result13.IsPartWait { - color.New(color.FgHiRed).Printf("%5.2f%% 参考和率", result13.AvgAgariRate) - } else { - fmt.Printf("%5.2f%% 参考和率", result13.AvgAgariRate) - } - } - - // 手牌速度,用于快速过庄 - if result13.MixedWaitsScore > 0 && shanten >= 1 && shanten <= 2 { - fmt.Print(" ") - if r.highlightMixedScore { - color.New(color.FgHiWhite).Printf("[%5.2f速度]", result13.MixedWaitsScore) - } else { - fmt.Printf("[%5.2f速度]", result13.MixedWaitsScore) - } - } - - // 局收支 - if ShowScore && result13.MixedRoundPoint != 0.0 { - fmt.Print(" ") - color.New(color.FgHiGreen).Printf("[局收支%4d]", int(math.Round(result13.MixedRoundPoint))) - } - - // (默听)荣和点数 - if result13.DamaPoint > 0 { - fmt.Print(" ") - ronType := "荣和" - if !result13.IsNaki { - ronType = "默听" - } - color.New(color.FgHiGreen).Printf("[%s%d]", ronType, int(math.Round(result13.DamaPoint))) - } - - // 立直点数,考虑了自摸、一发、里宝 - if result13.RiichiPoint > 0 { - fmt.Print(" ") - color.New(color.FgHiGreen).Printf("[立直%d]", int(math.Round(result13.RiichiPoint))) - } - - if len(result13.YakuTypes) > 0 { - // 役种(两向听以内开启显示) - if result13.Shanten <= 2 { - if !ShowAllYakuTypes && !DebugMode { - shownYakuTypes := []int{} - for yakuType := range result13.YakuTypes { - for _, yt := range yakuTypesToAlert { - if yakuType == yt { - shownYakuTypes = append(shownYakuTypes, yakuType) - } - } - } - if len(shownYakuTypes) > 0 { - sort.Ints(shownYakuTypes) - fmt.Print(" ") - color.New(color.FgHiGreen).Printf(util.YakuTypesToStr(shownYakuTypes)) - } - } else { - // debug - fmt.Print(" ") - color.New(color.FgHiGreen).Printf(util.YakuTypesWithDoraToStr(result13.YakuTypes, result13.DoraCount)) - } - // 片听 - if result13.IsPartWait { - fmt.Print(" ") - color.New(color.FgHiRed).Printf("[片听]") - } - } - } else if result13.IsNaki && shanten >= 0 && shanten <= 2 { - // 鸣牌时的无役提示(从听牌到两向听) - fmt.Print(" ") - color.New(color.FgHiRed).Printf("[无役]") - } - - // 振听提示 - if result13.FuritenRate > 0 { - fmt.Print(" ") - if result13.FuritenRate < 1 { - color.New(color.FgHiYellow).Printf("[可能振听]") - } else { - color.New(color.FgHiRed).Printf("[振听]") - } - } - - // 改良数 - if ShowScore { - fmt.Print(" ") - if len(result13.Improves) > 0 { - fmt.Printf("[%2d改良]", len(result13.Improves)) - } else { - fmt.Print(strings.Repeat(" ", 4)) - fmt.Print(strings.Repeat(" ", 2)) // 全角空格 - } - } - - // 进张类型 - fmt.Print(" ") - waitTiles := result13.Waits.AvailableTiles() - fmt.Print(util.TilesToStrWithBracket(waitTiles)) - - // - - fmt.Println() - - if ShowImproveDetail { - for tile, waits := range result13.Improves { - fmt.Printf("摸 %s 改良成 %s\n", util.Mahjong[tile], waits.String()) - } - } -} - -func printResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTable riskTable) { +func PrintResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTable riskTable) { if len(results14) == 0 { return } @@ -669,7 +171,7 @@ func printResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTa } if len(results14[0].OpenTiles) > 0 { - fmt.Print("鸣牌后") + fmt.Print("鳴牌後") } fmt.Println(util.NumberToChineseShanten(results14[0].Result13.Shanten) + ":") @@ -687,7 +189,7 @@ func printResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTa } if isDiffPoint { - color.HiGreen("注意切牌选择:打点") + color.HiGreen("注意切牌選擇:打點") } } @@ -697,7 +199,7 @@ func printResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTa // results14 = results14[:maxShown] //} for _, result := range results14 { - r := &analysisResult{ + r := &AnalysisResult{ result.DiscardTile, result.IsDiscardDoraTile, result.OpenTiles, @@ -706,6 +208,6 @@ func printResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTa result.Result13.AvgImproveWaitsCount == maxAvgImproveWaitsCount, result.Result13.MixedWaitsScore == maxMixedScore, } - r.printWaitsWithImproves13_oneRow() + r.PrintWaitsWithImproves13_oneRow() } } diff --git a/cli_AnalysisResult.go b/cli_AnalysisResult.go new file mode 100644 index 0000000..3751de1 --- /dev/null +++ b/cli_AnalysisResult.go @@ -0,0 +1,293 @@ +package main + +import ( + "fmt" + "math" + "sort" + "strings" + + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/fatih/color" +) + +// define struct +type AnalysisResult struct { + discardTile34 int + isDiscardTileDora bool + openTiles34 []int + result13 *util.Hand13AnalysisResult + + MixedRiskTable riskTable + + highlightAvgImproveWaitsCount bool + highlightMixedScore bool +} + +var ( + discardTile34 int + openTiles34 []int + result13 *util.Hand13AnalysisResult + shanten int +) + +/* +4[ 4.56] 切 8饼 => 44.50% 参考和率[ 4 改良] [7p 7s] [默听2000] [三色] [振听] + +4[ 4.56] 切 8饼 => 0.00% 参考和率[ 4 改良] [7p 7s] [无役] + +31[33.58] 切7索 => 5.23听牌数 [19.21速度] [16改良] [6789p 56789s] [局收支3120] [可能振听] + +48[50.64] 切5饼 => 24.25一向听 [12改良] [123456789p 56789s] + +31[33.62] 77索碰,切5饼 => 5.48听牌数 [15 改良] [123456789p] + +*/ +// 打印何切分析结果(单行) +func (r *AnalysisResult) PrintWaitsWithImproves13_oneRow() { + discardTile34 = r.discardTile34 + openTiles34 = r.openTiles34 + result13 = r.result13 + + shanten = result13.Shanten + + // 进张数 + countDrawingAUsefulTile() + + // 改良进张均值 + averageOfChangingDAUT(r) + + // 是否为3k+2张牌的何切分析 + wODOf_M3P2_Analysis(r) + + // 向听前进后的进张数的加权均值 + waOfDAUTAfterShanTenAdvance() + + // 手牌速度,用于快速过庄 + dAUTEfficiency(r) + + // 局收支;(默听)荣和点数;立直点数,考虑了自摸、一发、里宝 + expectedScore() + + // 牌型 + yaKuType() + + // 振听提示 + fuRiTenHint() + + // 改良数 + countChangeDAUT() + + // 进张类型 + typeOfDAUT() + + // 是否可以改變牌型 + theHandCanImproveOrNot() +} + +// count drawing a useful tile funcction +func countDrawingAUsefulTile() { + // 进张数 + waitsCount := result13.Waits.AllCount() + c := getWaitsCountColor(shanten, float64(waitsCount)) + color.New(c).Printf("%2d ", waitsCount) +} + +// average of changing drawing a useful tile(DAUT) +func averageOfChangingDAUT(r *AnalysisResult) { + if len(result13.Improves) > 0 { + if r.highlightAvgImproveWaitsCount { + color.New(color.FgHiWhite).Printf("[%5.2f]", result13.AvgImproveWaitsCount) + } else { + fmt.Printf("[%5.2f]", result13.AvgImproveWaitsCount) + } + } else { + fmt.Print(strings.Repeat(" ", 7)) + } + + fmt.Print(" ") +} + +// is "Whitch one to Discard(WOD)" of (meld * 3 + 2) type(M3P2) analysis +func wODOf_M3P2_Analysis(r *AnalysisResult) { + if discardTile34 != -1 { + // 鳴牌分析 + if len(openTiles34) > 0 { + meldType := "吃" + if openTiles34[0] == openTiles34[1] { + meldType = "碰" + } + color.New(color.FgHiWhite).Printf("%s%s", string([]rune(util.MahjongZH[openTiles34[0]])[:1]), + util.MahjongZH[openTiles34[1]]) + fmt.Printf("%s,", meldType) + } + // 舍牌 + tileZH := util.MahjongZH[discardTile34] + " " + // if it's dora print red text + if r.isDiscardTileDora { + color.New(color.FgHiRed).Printf("切") + } else { + fmt.Print("切") + } + // if tiel kind is z append space before tileZH + if discardTile34 >= 27 { + tileZH = " " + tileZH + } + // if the discard tile is not safe + if r.MixedRiskTable != nil { + // 若有实际危险度,则根据实际危险度来显示舍牌危险度 + risk := r.MixedRiskTable[discardTile34] + if risk == 0 { + fmt.Print(tileZH) + } else { + color.New(GetNumRiskColor(risk)).Print(tileZH) + } + } else { + fmt.Print(tileZH) + } + } + + fmt.Print(" => ") +} + +// weighted-average(wa) of drawing a useful tile after shanten advance +func waOfDAUTAfterShanTenAdvance() { + if shanten >= 1 { + // 前进后的进张数均值 + incShanten := shanten - 1 + c := getWaitsCountColor(incShanten, result13.AvgNextShantenWaitsCount) + color.New(c).Printf("%5.2f ", result13.AvgNextShantenWaitsCount) + fmt.Printf("%s ", util.NumberToChineseShanten(incShanten)) + if incShanten >= 1 { + //fmt.Printf("进张") + } else { // incShanten == 0 + fmt.Printf("數") + //if showAgariAboveShanten1 { + // fmt.Printf("(%.2f%% 参考和率)", result13.AvgAgariRate) + //} + } + } else { // shanten == 0 + // 前进后的和率 + // 若振听或片听,则标红 + if result13.FuritenRate == 1 || result13.IsPartWait { + color.New(color.FgHiRed).Printf("%5.2f%% 参考和率", result13.AvgAgariRate) + } else { + fmt.Printf("%5.2f%% 参考和率", result13.AvgAgariRate) + } + } + +} + +// drawing a useful tile(DAUT) efficiency +func dAUTEfficiency(r *AnalysisResult) { + if result13.MixedWaitsScore > 0 && shanten >= 1 /*&& shanten <= 2*/ { + // fmt.Print(" ") + if r.highlightMixedScore { + color.New(color.FgHiWhite).Printf("[%5.2f速度]", result13.MixedWaitsScore) + } else { + fmt.Printf("[%5.2f速度]", result13.MixedWaitsScore) + } + } + +} + +// expected score +func expectedScore() { + // 局收支 + if ShowScore && result13.MixedRoundPoint != 0.0 { + fmt.Print(" ") + color.New(color.FgHiGreen).Printf("[局收支%4d]", int(math.Round(result13.MixedRoundPoint))) + } + + // (默听)荣和点数 + if result13.DamaPoint > 0 { + fmt.Print(" ") + ronType := "荣和" + if !result13.IsNaki { + ronType = "默听" + } + color.New(color.FgHiGreen).Printf("[%s%d]", ronType, int(math.Round(result13.DamaPoint))) + } + + // 立直点数,考虑了自摸、一发、里宝 + if result13.RiichiPoint > 0 { + fmt.Print(" ") + color.New(color.FgHiGreen).Printf("[立直%d]", int(math.Round(result13.RiichiPoint))) + } +} + +// yaKuType +func yaKuType() { + if len(result13.YakuTypes) > 0 { + // 役种(两向听以内开启显示) + if result13.Shanten <= 2 { + if !ShowAllYakuTypes && !DebugMode { + shownYakuTypes := []int{} + for yakuType := range result13.YakuTypes { + for _, yt := range yakuTypesToAlert { + if yakuType == yt { + shownYakuTypes = append(shownYakuTypes, yakuType) + } + } + } + if len(shownYakuTypes) > 0 { + sort.Ints(shownYakuTypes) + fmt.Print(" ") + color.New(color.FgHiGreen).Printf(util.YakuTypesToStr(shownYakuTypes)) + } + } else { + // debug + fmt.Print(" ") + color.New(color.FgHiGreen).Printf(util.YakuTypesWithDoraToStr(result13.YakuTypes, result13.DoraCount)) + } + // 片听 + if result13.IsPartWait { + fmt.Print(" ") + color.New(color.FgHiRed).Printf("[片聽]") + } + } + } else if result13.IsNaki && shanten >= 0 && shanten <= 2 { + // 鳴牌时的无役提示(从听牌到两向听) + fmt.Print(" ") + color.New(color.FgHiRed).Printf("[無役]") + } +} + +// furiten hint +func fuRiTenHint() { + if result13.FuritenRate > 0 { + fmt.Print(" ") + if result13.FuritenRate < 1 { + color.New(color.FgHiYellow).Printf("[可能振聽]") + } else { + color.New(color.FgHiRed).Printf("[振聽]") + } + } +} + +// count change drawing a useful tile(DAUT) +func countChangeDAUT() { + if ShowScore { + fmt.Print(" ") + if len(result13.Improves) > 0 { + fmt.Printf("[%2d改良]", len(result13.Improves)) + } else { + fmt.Print(strings.Repeat(" ", 4)) + fmt.Print(strings.Repeat(" ", 2)) // 全角空格 + } + } + +} + +// type of drawing a useful til(DAUT) +func typeOfDAUT() { + fmt.Printf(" %s \n", util.TilesToStrWithBracket(result13.Waits.AvailableTiles())) +} + +// the hand can improve or not +func theHandCanImproveOrNot() { + if ShowImproveDetail { + for tile, waits := range result13.Improves { + fmt.Printf("摸 %s 改良成 %s\n", util.Mahjong[tile], waits.String()) + } + } +} diff --git a/cli_RiskInfo.go b/cli_RiskInfo.go new file mode 100644 index 0000000..1c8e5f1 --- /dev/null +++ b/cli_RiskInfo.go @@ -0,0 +1,182 @@ +package main + + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/EndlessCheng/mahjong-helper/util" +) + +type RiskInfo struct { + // 三麻为 3,四麻为 4 + playerNumber int + + // 该玩家的听牌率(立直时为 100.0) + tenpaiRate float64 + + // 该玩家的安牌 + // 若该玩家有杠操作,把杠的那张牌也算作安牌,这有助于判断筋壁危险度 + safeTiles34 []bool + + // 各种牌的铳率表 + riskTable riskTable + + // 剩余无筋 123789 + // 总计 18 种。剩余无筋牌数量越少,该无筋牌越危险 + leftNoSujiTiles []int + + // 是否摸切立直 + isTsumogiriRiichi bool + + // 荣和点数 + // 仅调试用 + _ronPoint float64 +} + +type RiskInfoList []*RiskInfo + +// 考虑了听牌率的综合危险度 +func (l RiskInfoList) mixedRiskTable() riskTable { + mixedRiskTable := make(riskTable, 34) + for i := range mixedRiskTable { + mixedRisk := 0.0 + for _, ri := range l[1:] { + if ri.tenpaiRate <= 15 { + continue + } + _risk := ri.riskTable[i] * ri.tenpaiRate / 100 + mixedRisk = mixedRisk + _risk - mixedRisk*_risk/100 + } + mixedRiskTable[i] = mixedRisk + } + return mixedRiskTable +} + +func (l RiskInfoList) printWithHands(hands []int, leftCounts []int) { + // 听牌率超过一定值就打印铳率 + const ( + minShownTenpaiRate4 = 50.0 + minShownTenpaiRate3 = 20.0 + ) + + minShownTenpaiRate := minShownTenpaiRate4 + if l[0].playerNumber == 3 { + minShownTenpaiRate = minShownTenpaiRate3 + } + + dangerousPlayerCount := 0 + // 打印安牌,危险牌 + names := []string{"", "下家", "對家", "上家"} + for i := len(l) - 1; i >= 1; i-- { + tenpaiRate := l[i].tenpaiRate + if len(l[i].riskTable) > 0 && (DebugMode || tenpaiRate > minShownTenpaiRate) { + dangerousPlayerCount++ + fmt.Print(names[i] + "安牌:") + //if debugMode { + //fmt.Printf("(%d*%2.2f%%听牌率)", int(l[i]._ronPoint), l[i].tenpaiRate) + //} + containLine := l[i].riskTable.printWithHands(hands, tenpaiRate/100) + + // 打印听牌率 + fmt.Print(" ") + if !containLine { + fmt.Print(" ") + } + fmt.Print("[") + if tenpaiRate == 100 { + fmt.Print("100.%") + } else { + fmt.Printf("%4.1f%%", tenpaiRate) + } + fmt.Print("听牌率]") + + // 打印无筋数量 + fmt.Print(" ") + const badMachiLimit = 3 + noSujiInfo := "" + if l[i].isTsumogiriRiichi { + noSujiInfo = "摸切立直" + } else if len(l[i].leftNoSujiTiles) == 0 { + noSujiInfo = "愚形听牌/振听" + } else if len(l[i].leftNoSujiTiles) <= badMachiLimit { + noSujiInfo = "可能愚形听牌/振听" + } + if noSujiInfo != "" { + fmt.Printf("[%d无筋: ", len(l[i].leftNoSujiTiles)) + color.New(color.FgHiYellow).Printf("%s", noSujiInfo) + fmt.Print("]") + } else { + fmt.Printf("[%d无筋]", len(l[i].leftNoSujiTiles)) + } + + fmt.Println() + } + } + + // 若不止一个玩家立直/副露,打印加权综合铳率(考虑了听牌率) + mixedPlayers := 0 + for _, ri := range l[1:] { + if ri.tenpaiRate > 0 { + mixedPlayers++ + } + } + if dangerousPlayerCount > 0 && mixedPlayers > 1 { + fmt.Print("综合安牌:") + mixedRiskTable := l.mixedRiskTable() + mixedRiskTable.printWithHands(hands, 1) + fmt.Println() + } + + // 打印因 NC OC 产生的安牌 + // TODO: 重构至其他函数 + if dangerousPlayerCount > 0 { + ncSafeTileList := util.CalcNCSafeTiles(leftCounts).FilterWithHands(hands) + ocSafeTileList := util.CalcOCSafeTiles(leftCounts).FilterWithHands(hands) + if len(ncSafeTileList) > 0 { + fmt.Printf("NC:") + for _, safeTile := range ncSafeTileList { + fmt.Printf(" " + util.MahjongZH[safeTile.Tile34]) + } + fmt.Println() + } + if len(ocSafeTileList) > 0 { + fmt.Printf("OC:") + for _, safeTile := range ocSafeTileList { + fmt.Printf(" " + util.MahjongZH[safeTile.Tile34]) + } + fmt.Println() + } + + // 下面这个是另一种显示方式:显示壁牌 + //printedNC := false + //for i, c := range leftCounts[:27] { + // if c != 0 || i%9 == 0 || i%9 == 8 { + // continue + // } + // if !printedNC { + // printedNC = true + // fmt.Printf("NC:") + // } + // fmt.Printf(" " + util.MahjongZH[i]) + //} + //if printedNC { + // fmt.Println() + //} + //printedOC := false + //for i, c := range leftCounts[:27] { + // if c != 1 || i%9 == 0 || i%9 == 8 { + // continue + // } + // if !printedOC { + // printedOC = true + // fmt.Printf("OC:") + // } + // fmt.Printf(" " + util.MahjongZH[i]) + //} + //if printedOC { + // fmt.Println() + //} + fmt.Println() + } +} diff --git a/cli_RiskTable.go b/cli_RiskTable.go new file mode 100644 index 0000000..a53b71e --- /dev/null +++ b/cli_RiskTable.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "sort" + + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/fatih/color" +) + +// 34 种牌的危险度 +type riskTable util.RiskTiles34 + +func (t riskTable) printWithHands(hands []int, fixedRiskMulti float64) (containLine bool) { + // 打印铳率=0的牌(现物,或NC且剩余数=0) + safeCount := 0 + for i, c := range hands { + if c > 0 && t[i] == 0 { + fmt.Printf(" " + util.MahjongZH[i]) + safeCount++ + } + } + + // 打印危险牌,按照铳率排序&高亮 + handsRisks := []handsRisk{} + for i, c := range hands { + if c > 0 && t[i] > 0 { + handsRisks = append(handsRisks, handsRisk{i, t[i]}) + } + } + sort.Slice(handsRisks, func(i, j int) bool { + return handsRisks[i].risk < handsRisks[j].risk + }) + if len(handsRisks) > 0 { + if safeCount > 0 { + fmt.Print(" |") + containLine = true + } + for _, hr := range handsRisks { + // 颜色考虑了听牌率 + color.New(GetNumRiskColor(hr.risk * fixedRiskMulti)).Printf(" " + util.MahjongZH[hr.tile]) + } + } + + return +} + +func (t riskTable) getBestDefenceTile(tiles34 []int) (result int) { + minRisk := 100.0 + maxRisk := 0.0 + for tile, c := range tiles34 { + if c == 0 { + continue + } + risk := t[tile] + if risk < minRisk { + minRisk = risk + result = tile + } + if risk > maxRisk { + maxRisk = risk + } + } + if maxRisk == 0 { + return -1 + } + return result +} diff --git a/core.go b/core.go index 2c49b56..3036b31 100644 --- a/core.go +++ b/core.go @@ -1,116 +1,9 @@ package main -import ( - "fmt" +import "github.com/EndlessCheng/mahjong-helper/util" - "github.com/EndlessCheng/mahjong-helper/util" - "github.com/EndlessCheng/mahjong-helper/util/model" - "github.com/fatih/color" -) - -type DataParser interface { - // 数据来源(是天凤还是雀魂) - GetDataSourceType() int - - // 获取自家初始座位:0-第一局的东家 1-第一局的南家 2-第一局的西家 3-第一局的北家 - // 仅处理雀魂数据,天凤返回 -1 - GetSelfSeat() int - - // 原始 JSON - GetMessage() string - - // 解析前,根据消息内容来决定是否要进行后续解析 - SkipMessage() bool - - // 尝试解析用户名 - IsLogin() bool - HandleLogin() - - // round 开始/重连 - // roundNumber: 场数(如东1为0,东2为1,...,南1为4,...,南4为7,...),对于三麻来说南1也是4 - // benNumber: 本场数 - // dealer: 庄家 0-3 - // doraIndicators: 宝牌指示牌 - // handTiles: 手牌 - // numRedFives: 按照 mps 的顺序,赤5个数 - IsInit() bool - ParseInit() (roundNumber int, benNumber int, dealer int, doraIndicators []int, handTiles []int, numRedFives []int) - - // 自家摸牌 - // tile: 0-33 - // isRedFive: 是否为赤5 - // kanDoraIndicator: 摸牌时,若为暗杠摸的岭上牌,则可以翻出杠宝牌指示牌,否则返回 -1(目前恒为 -1,见 IsNewDora) - IsSelfDraw() bool - ParseSelfDraw() (tile int, isRedFive bool, kanDoraIndicator int) - - // 舍牌 - // who: 0=自家, 1=下家, 2=对家, 3=上家 - // isTsumogiri: 是否为摸切(who=0 时忽略该值) - // isReach: 是否为立直宣言(isReach 对于天凤来说恒为 false,见 IsReach) - // canBeMeld: 是否可以鸣牌(who=0 时忽略该值) - // kanDoraIndicator: 大明杠/加杠的杠宝牌指示牌,在切牌后出现,没有则返回 -1(天凤恒为-1,见 IsNewDora) - IsDiscard() bool - ParseDiscard() (who int, discardTile int, isRedFive bool, isTsumogiri bool, isReach bool, canBeMeld bool, kanDoraIndicator int) - - // 鸣牌(含暗杠、加杠) - // kanDoraIndicator: 暗杠的杠宝牌指示牌,在他家暗杠时出现,没有则返回 -1(天凤恒为-1,见 IsNewDora) - IsOpen() bool - ParseOpen() (who int, meld *model.Meld, kanDoraIndicator int) - - // 立直声明(IsReach 对于雀魂来说恒为 false,见 ParseDiscard) - IsReach() bool - ParseReach() (who int) - - // 振听 - IsFuriten() bool - - // 本局是否和牌 - IsRoundWin() bool - ParseRoundWin() (whos []int, points []int) - - // 是否流局 - // 四风连打 四家立直 四杠散了 九种九牌 三家和了 | 流局听牌 流局未听牌 | 流局满贯 - // 三家和了 - IsRyuukyoku() bool - ParseRyuukyoku() (type_ int, whos []int, points []int) - - // 拔北宝牌 - IsNukiDora() bool - ParseNukiDora() (who int, isTsumogiri bool) - - // 这一项放在末尾处理 - // 杠宝牌(雀魂在暗杠后的摸牌时出现) - // kanDoraIndicator: 0-33 - IsNewDora() bool - ParseNewDora() (kanDoraIndicator int) -} - -type playerInfo struct { - name string // 自家/下家/对家/上家 - - selfWindTile int // 自风 - - melds []*model.Meld // 副露 - meldDiscardsAtGlobal []int - meldDiscardsAt []int - isNaki bool // 是否鸣牌(暗杠不算鸣牌) - - // 注意负数(自摸切)要^ - discardTiles []int // 该玩家的舍牌 - latestDiscardAtGlobal int // 该玩家最近一次舍牌在 globalDiscardTiles 中的下标,初始为 -1 - earlyOutsideTiles []int // 立直前的1-5巡的外侧牌 - - isReached bool // 是否立直 - canIppatsu bool // 是否有一发 - - reachTileAtGlobal int // 立直宣言牌在 globalDiscardTiles 中的下标,初始为 -1 - reachTileAt int // 立直宣言牌在 discardTiles 中的下标,初始为 -1 - - nukiDoraNum int // 拔北宝牌数 -} - -func newPlayerInfo(name string, selfWindTile int) *playerInfo { - return &playerInfo{ +func newPlayerInfo(name string, selfWindTile int) *PlayerInfo { + return &PlayerInfo{ name: name, selfWindTile: selfWindTile, latestDiscardAtGlobal: -1, @@ -119,7 +12,7 @@ func newPlayerInfo(name string, selfWindTile int) *playerInfo { } } -func modifySanninPlayerInfoList(lst []*playerInfo, roundNumber int) []*playerInfo { +func modifySanninPlayerInfoList(lst []*PlayerInfo, roundNumber int) []*PlayerInfo { windToIdxMap := map[int]int{} for i, pi := range lst { windToIdxMap[pi.selfWindTile] = i @@ -140,78 +33,6 @@ func modifySanninPlayerInfoList(lst []*playerInfo, roundNumber int) []*playerInf return lst } -func (p *playerInfo) doraNum(doraList []int) (doraCount int) { - for _, meld := range p.melds { - for _, tile := range meld.Tiles { - for _, doraTile := range doraList { - if tile == doraTile { - doraCount++ - } - } - } - if meld.ContainRedFive { - doraCount++ - } - } - if p.nukiDoraNum > 0 { - doraCount += p.nukiDoraNum - // 特殊:西为指示牌 - for _, doraTile := range doraList { - if doraTile == 30 { - doraCount += p.nukiDoraNum - } - } - } - return -} - -// - -type RoundData struct { - parser DataParser - - GameMode gameMode - - SkipOutput bool - - // 玩家数,3 为三麻,4 为四麻 - playerNumber int - - // 场数(如东1为0,东2为1,...,南1为4,...) - roundNumber int - - // 本场数,从 0 开始算 - benNumber int - - // 场风 - roundWindTile int - - // 庄家 0=自家, 1=下家, 2=对家, 3=上家 - // 请用 reset 设置 - dealer int - - // 宝牌指示牌 - doraIndicators []int - - // 自家手牌 - counts []int - - // 按照 mps 的顺序记录自家赤5数量,包含副露的赤5 - // 比如有 0p 和 0s 就是 [1, 0, 1] - numRedFives []int - - // 牌山剩余牌量 - leftCounts []int - - // 全局舍牌 - // 按舍牌顺序,负数表示摸切(-),非负数表示手切(+) - // 可以理解成:- 表示不要/暗色,+ 表示进张/亮色 - globalDiscardTiles []int - - // 0=自家, 1=下家, 2=对家, 3=上家 - players []*playerInfo -} - func newRoundData(parser DataParser, roundNumber int, benNumber int, dealer int) *RoundData { // 无论是三麻还是四麻,都视作四个人 const playerNumber = 4 @@ -230,10 +51,10 @@ func newRoundData(parser DataParser, roundNumber int, benNumber int, dealer int) counts: make([]int, 34), leftCounts: util.InitLeftTiles34(), globalDiscardTiles: []int{}, - players: []*playerInfo{ + players: []*PlayerInfo{ newPlayerInfo("自家", playerWindTile[0]), newPlayerInfo("下家", playerWindTile[1]), - newPlayerInfo("对家", playerWindTile[2]), + newPlayerInfo("對家", playerWindTile[2]), newPlayerInfo("上家", playerWindTile[3]), }, } @@ -242,716 +63,3 @@ func newRoundData(parser DataParser, roundNumber int, benNumber int, dealer int) func NewGame(parser DataParser) *RoundData { return newRoundData(parser, 0, 0, 0) } - -// 新的一局 -func (d *RoundData) reset(roundNumber int, benNumber int, dealer int) { - skipOutput := d.SkipOutput - gameMode := d.GameMode - playerNumber := d.playerNumber - newData := newRoundData(d.parser, roundNumber, benNumber, dealer) - newData.SkipOutput = skipOutput - newData.GameMode = gameMode - newData.playerNumber = playerNumber - if playerNumber == 3 { - // 三麻没有 2-8m - for i := 1; i <= 7; i++ { - newData.leftCounts[i] = 0 - } - newData.players = modifySanninPlayerInfoList(newData.players, roundNumber) - } - *d = *newData -} - -func (d *RoundData) newGame() { - d.reset(0, 0, 0) -} - -func (d *RoundData) descLeftCounts(tile int) { - d.leftCounts[tile]-- - if d.leftCounts[tile] < 0 { - info := fmt.Sprintf("数据异常: %s 数量为 %d", util.MahjongZH[tile], d.leftCounts[tile]) - if DebugMode { - panic(info) - } else { - fmt.Println(info) - } - } -} - -// 杠! -func (d *RoundData) newDora(kanDoraIndicator int) { - d.doraIndicators = append(d.doraIndicators, kanDoraIndicator) - d.descLeftCounts(kanDoraIndicator) - - if d.SkipOutput { - return - } - - color.Yellow("杠宝牌指示牌是 %s", util.MahjongZH[kanDoraIndicator]) -} - -// 根据宝牌指示牌计算出宝牌 -func (d *RoundData) doraList() (dl []int) { - return model.DoraList(d.doraIndicators, d.playerNumber == 3) -} - -func (d *RoundData) printDiscards() { - // 三麻的北家是不需要打印的 - for i := len(d.players) - 1; i >= 1; i-- { - if player := d.players[i]; d.playerNumber != 3 || player.selfWindTile != 30 { - player.printDiscards() - } - } -} - -// 分析34种牌的危险度 -// 可以用来判断自家手牌的安全度,以及他家是否在进攻(多次切出危险度高的牌) -func (d *RoundData) analysisTilesRisk() (riList riskInfoList) { - riList = make(riskInfoList, len(d.players)) - for who := range riList { - riList[who] = &riskInfo{ - playerNumber: d.playerNumber, - safeTiles34: make([]bool, 34), - } - } - - // 先利用振听规则收集各家安牌 - for who, player := range d.players { - if who == 0 { - // TODO: 暂时不计算自家的 - continue - } - - // 舍牌振听产生的安牌 - for _, tile := range normalDiscardTiles(player.discardTiles) { - riList[who].safeTiles34[tile] = true - } - if player.reachTileAtGlobal != -1 { - // 立直后振听产生的安牌 - for _, tile := range normalDiscardTiles(d.globalDiscardTiles[player.reachTileAtGlobal:]) { - riList[who].safeTiles34[tile] = true - } - } else if player.latestDiscardAtGlobal != -1 { - // 同巡振听产生的安牌 - // 即该玩家在最近一次舍牌后,其他玩家的舍牌 - for _, tile := range normalDiscardTiles(d.globalDiscardTiles[player.latestDiscardAtGlobal:]) { - riList[who].safeTiles34[tile] = true - } - } - - // 特殊:杠产生的安牌 - // 很难想象一个人会在有 678888 的时候去开杠(即使有这个可能,本程序也是不防的) - for _, meld := range player.melds { - if meld.IsKan() { - riList[who].safeTiles34[meld.Tiles[0]] = true - } - } - } - - // 计算各种数据 - for who, player := range d.players { - if who == 0 { - // TODO: 暂时不计算自家的 - continue - } - - // 该玩家的巡目 = 为其切过的牌的数目 - turns := util.MinInt(len(player.discardTiles), util.MaxTurns) - if turns == 0 { - turns = 1 - } - - // TODO: 若某人一直摸切,然后突然手切了一张字牌,那他很有可能默听/一向听 - if player.isReached { - riList[who].tenpaiRate = 100.0 - if player.reachTileAtGlobal < len(d.globalDiscardTiles) { // 天凤可能有数据漏掉 - riList[who].isTsumogiriRiichi = d.globalDiscardTiles[player.reachTileAtGlobal] < 0 - } - } else { - rate := util.CalcTenpaiRate(player.melds, player.discardTiles, player.meldDiscardsAt) - if d.playerNumber == 3 { - rate = util.GetTenpaiRate3(rate) - } - riList[who].tenpaiRate = rate - } - - // 估计该玩家荣和点数 - var ronPoint float64 - switch { - case player.canIppatsu: - // 立直一发巡的荣和点数 - ronPoint = util.RonPointRiichiIppatsu - case player.isReached: - // 立直非一发巡的荣和点数 - ronPoint = util.RonPointRiichiHiIppatsu - case player.isNaki: - // 副露时的荣和点数(非常粗略地估计) - doraCount := player.doraNum(d.doraList()) - ronPoint = util.RonPointOtherNakiWithDora(doraCount) - default: - // 默听时的荣和点数 - ronPoint = util.RonPointDama - } - // 亲家*1.5 - if who == d.dealer { - ronPoint *= 1.5 - } - riList[who]._ronPoint = ronPoint - - // 根据该玩家的巡目、现物、立直后通过的牌、NC、Dora、早外、荣和点数来计算每张牌的危险度 - risk34 := util.CalculateRiskTiles34(turns, riList[who].safeTiles34, d.leftCounts, d.doraList(), d.roundWindTile, player.selfWindTile). - FixWithEarlyOutside(player.earlyOutsideTiles). - FixWithPoint(ronPoint) - riList[who].riskTable = riskTable(risk34) - - // 计算剩余筋牌 - if len(player.melds) < 4 { - riList[who].leftNoSujiTiles = util.CalculateLeftNoSujiTiles(riList[who].safeTiles34, d.leftCounts) - } else { - // 大吊车:愚型听牌 - } - } - - return riList -} - -// TODO: 特殊处理w立直 -func (d *RoundData) isPlayerDaburii(who int) bool { - // w立直成立的前提是没有任何玩家副露 - for _, p := range d.players { - if len(p.melds) > 0 { - return false - } - // 对于三麻来说,还不能有拔北 - if p.nukiDoraNum > 0 { - return false - } - } - return d.players[who].reachTileAt == 0 -} - -// 自家的 PlayerInfo -func (d *RoundData) newModelPlayerInfo() *model.PlayerInfo { - const wannpaiTilesCount = 14 - leftDrawTilesCount := util.CountOfTiles34(d.leftCounts) - (wannpaiTilesCount - len(d.doraIndicators)) - for _, player := range d.players[1:] { - leftDrawTilesCount -= 13 - 3*len(player.melds) - } - if d.playerNumber == 3 { - leftDrawTilesCount += 13 - } - - melds := []model.Meld{} - for _, m := range d.players[0].melds { - melds = append(melds, *m) - } - - const self = 0 - selfPlayer := d.players[self] - - return &model.PlayerInfo{ - HandTiles34: d.counts, - Melds: melds, - DoraTiles: d.doraList(), - NumRedFives: d.numRedFives, - - RoundWindTile: d.roundWindTile, - SelfWindTile: selfPlayer.selfWindTile, - IsParent: d.dealer == self, - //IsDaburii: d.isPlayerDaburii(self), // FIXME PLS,应该在立直时就判断 - IsRiichi: selfPlayer.isReached, - - DiscardTiles: normalDiscardTiles(selfPlayer.discardTiles), - LeftTiles34: d.leftCounts, - - LeftDrawTilesCount: leftDrawTilesCount, - - NukiDoraNum: selfPlayer.nukiDoraNum, - } -} - -func (d *RoundData) Analysis() error { - if !DebugMode { - defer func() { - if err := recover(); err != nil { - fmt.Println("内部错误:", err) - } - }() - } - - if DebugMode { - if msg := d.parser.GetMessage(); len(msg) > 0 { - const printLimit = 500 - if len(msg) > printLimit { - msg = msg[:printLimit] - } - fmt.Println("收到", msg) - } - } - - // 先获取用户信息 - if d.parser.IsLogin() { - d.parser.HandleLogin() - } - - if d.parser.SkipMessage() { - return nil - } - - // 若自家立直,则进入看戏模式 - // TODO: 见逃判断 - if !d.parser.IsInit() && !d.parser.IsRoundWin() && !d.parser.IsRyuukyoku() && d.players[0].isReached { - return nil - } - - if DebugMode { - fmt.Println("当前座位为", d.parser.GetSelfSeat()) - } - - var currentRoundCache *RoundAnalysisCache - if analysisCache := GetAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { - currentRoundCache = analysisCache.WholeGameCache[d.roundNumber][d.benNumber] - } - - switch { - case d.parser.IsInit(): - // round 开始/重连 - if !DebugMode && !d.SkipOutput { - ClearConsole() - } - - roundNumber, benNumber, dealer, doraIndicators, hands, numRedFives := d.parser.ParseInit() - switch d.parser.GetDataSourceType() { - case dataSourceTypeTenhou: - d.reset(roundNumber, benNumber, dealer) - d.GameMode = gameModeMatch // TODO: 牌谱模式? - case dataSourceTypeMajsoul: - if dealer != -1 { // 先就坐,还没洗牌呢~ - // 设置第一局的 dealer - d.reset(0, 0, dealer) - d.GameMode = gameModeMatch - fmt.Printf("游戏即将开始,您分配到的座位是:") - color.HiGreen(util.MahjongZH[d.players[0].selfWindTile]) - return nil - } else { - // 根据 selfSeat 和当前的 roundNumber 计算当前局的 dealer - newDealer := (4 - d.parser.GetSelfSeat() + roundNumber) % 4 - // 新的一局 - d.reset(roundNumber, benNumber, newDealer) - } - default: - panic("not impl!") - } - - // 由于 reset 了,重新获取 currentRoundCache - if analysisCache := GetAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { - currentRoundCache = analysisCache.WholeGameCache[d.roundNumber][d.benNumber] - } - - d.doraIndicators = doraIndicators - for _, dora := range doraIndicators { - d.descLeftCounts(dora) - } - for _, tile := range hands { - d.counts[tile]++ - d.descLeftCounts(tile) - } - d.numRedFives = numRedFives - - playerInfo := d.newModelPlayerInfo() - - // 牌谱分析模式下,记录舍牌推荐 - if d.GameMode == GameModeRecordCache && len(hands) == 14 { - currentRoundCache.AddAIDiscardTileWhenDrawTile(simpleBestDiscardTile(playerInfo), -1, 0, 0) - } - - if d.SkipOutput { - return nil - } - - // 牌谱模式下,打印舍牌推荐 - if d.GameMode == gameModeRecord { - currentRoundCache.Print() - } - - color.New(color.FgHiGreen).Printf("%s", util.MahjongZH[d.roundWindTile]) - fmt.Printf("%d局开始,自风为", roundNumber%4+1) - color.New(color.FgHiGreen).Printf("%s", util.MahjongZH[d.players[0].selfWindTile]) - fmt.Println() - info := fmt.Sprintln(util.TilesToMahjongZHInterface(d.doraIndicators)...) - info = info[:len(info)-1] - color.HiYellow("宝牌指示牌是 " + info) - fmt.Println() - // TODO: 显示地和概率 - return analysisPlayerWithRisk(playerInfo, nil) - case d.parser.IsOpen(): - // 某家鸣牌(含暗杠、加杠) - who, meld, kanDoraIndicator := d.parser.ParseOpen() - meldType := meld.MeldType - meldTiles := meld.Tiles - calledTile := meld.CalledTile - - // 任何形式的鸣牌都能破除一发 - for _, player := range d.players { - player.canIppatsu = false - } - - // 杠宝牌指示牌 - if kanDoraIndicator != -1 { - d.newDora(kanDoraIndicator) - } - - player := d.players[who] - - // 不是暗杠则标记该玩家鸣牌了 - if meldType != meldTypeAnkan { - player.isNaki = true - } - - // 加杠单独处理 - if meldType == meldTypeKakan { - if who != 0 { - // (不是自家时)修改牌山剩余量 - d.descLeftCounts(calledTile) - } else { - // 自家加杠成功,修改手牌 - d.counts[calledTile]-- - // 由于均为自家操作,宝牌数是不变的 - - // 牌谱分析模式下,记录加杠操作 - if d.GameMode == GameModeRecordCache { - currentRoundCache.AddKan(meldType) - } - } - // 修改原副露 - for _, _meld := range player.melds { - // 找到原有的碰副露 - if _meld.Tiles[0] == calledTile { - _meld.MeldType = meldTypeKakan - _meld.Tiles = append(_meld.Tiles, calledTile) - _meld.ContainRedFive = meld.ContainRedFive - break - } - } - - if DebugMode { - if who == 0 { - if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { - return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) - } - } - } - - break - } - - // 修改玩家副露数据 - d.players[who].melds = append(d.players[who].melds, meld) - - if who != 0 { - // (不是自家时)修改牌山剩余量 - // 先增后减 - if meldType != meldTypeAnkan { - d.leftCounts[calledTile]++ - } - for _, tile := range meldTiles { - d.descLeftCounts(tile) - } - } else { - // 自家,修改手牌 - if meldType == meldTypeAnkan { - d.counts[meldTiles[0]] = 0 - - // 牌谱分析模式下,记录暗杠操作 - if d.GameMode == GameModeRecordCache { - currentRoundCache.AddKan(meldType) - } - } else { - d.counts[calledTile]++ - for _, tile := range meldTiles { - d.counts[tile]-- - } - if meld.RedFiveFromOthers { - tileType := meldTiles[0] / 9 - d.numRedFives[tileType]++ - } - - // 牌谱分析模式下,记录吃碰明杠操作 - if d.GameMode == GameModeRecordCache { - currentRoundCache.AddChiPonKan(meldType) - } - } - - if DebugMode { - if meldType == meldTypeMinkan || meldType == meldTypeAnkan { - if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { - return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) - } - } else { - if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 2 { - return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) - } - } - } - } - case d.parser.IsReach(): - // 立直宣告 - // 如果是他家立直,进入攻守判断模式 - who := d.parser.ParseReach() - d.players[who].isReached = true - d.players[who].canIppatsu = true - //case "AGARI", "RYUUKYOKU": - // // 某人和牌或流局,round 结束 - //case "PROF": - // // 游戏结束 - //case "BYE": - // // 某人退出 - //case "REJOIN", "GO": - // // 重连 - case d.parser.IsFuriten(): - // 振听 - if d.SkipOutput { - return nil - } - color.HiYellow("振听") - //case "U", "V", "W": - // //(下家,对家,上家 不要其上家的牌)摸牌 - //case "HELO", "RANKING", "TAIKYOKU", "UN", "LN", "SAIKAI": - // // 其他 - case d.parser.IsSelfDraw(): - if !DebugMode && !d.SkipOutput { - ClearConsole() - } - // 自家(从牌山 d.leftCounts)摸牌(至手牌 d.counts) - tile, isRedFive, kanDoraIndicator := d.parser.ParseSelfDraw() - d.descLeftCounts(tile) - d.counts[tile]++ - if isRedFive { - d.numRedFives[tile/9]++ - } - if kanDoraIndicator != -1 { - d.newDora(kanDoraIndicator) - } - - playerInfo := d.newModelPlayerInfo() - - // 安全度分析 - riskTables := d.analysisTilesRisk() - mixedRiskTable := riskTables.mixedRiskTable() - - // 牌谱分析模式下,记录舍牌推荐 - if d.GameMode == GameModeRecordCache { - bestAttackDiscardTile := simpleBestDiscardTile(playerInfo) - bestDefenceDiscardTile := mixedRiskTable.getBestDefenceTile(playerInfo.HandTiles34) - bestAttackDiscardTileRisk, bestDefenceDiscardTileRisk := 0.0, 0.0 - if bestDefenceDiscardTile >= 0 { - bestAttackDiscardTileRisk = mixedRiskTable[bestAttackDiscardTile] - bestDefenceDiscardTileRisk = mixedRiskTable[bestDefenceDiscardTile] - } - currentRoundCache.AddAIDiscardTileWhenDrawTile(bestAttackDiscardTile, bestDefenceDiscardTile, bestAttackDiscardTileRisk, bestDefenceDiscardTileRisk) - } - - if d.SkipOutput { - return nil - } - - // 牌谱模式下,打印舍牌推荐 - if d.GameMode == gameModeRecord { - currentRoundCache.Print() - } - - // 打印他家舍牌信息 - d.printDiscards() - fmt.Println() - - // 打印手牌对各家的安全度 - riskTables.printWithHands(d.counts, d.leftCounts) - - // 打印何切推荐 - // TODO: 根据是否听牌/一向听、打点、巡目、和率等进行攻守判断 - return analysisPlayerWithRisk(playerInfo, mixedRiskTable) - case d.parser.IsDiscard(): - who, discardTile, isRedFive, isTsumogiri, isReach, canBeMeld, kanDoraIndicator := d.parser.ParseDiscard() - - if kanDoraIndicator != -1 { - d.newDora(kanDoraIndicator) - } - - player := d.players[who] - if isReach { - player.isReached = true - player.canIppatsu = true - } - - if who == 0 { - // 特殊处理自家舍牌的情况 - riskTables := d.analysisTilesRisk() - mixedRiskTable := riskTables.mixedRiskTable() - - // 自家(从手牌 d.counts)舍牌(至牌河 d.globalDiscardTiles) - d.counts[discardTile]-- - - d.globalDiscardTiles = append(d.globalDiscardTiles, discardTile) - player.discardTiles = append(player.discardTiles, discardTile) - player.latestDiscardAtGlobal = len(d.globalDiscardTiles) - 1 - - if isRedFive { - d.numRedFives[discardTile/9]-- - } - - // 牌谱分析模式下,记录自家舍牌 - if d.GameMode == GameModeRecordCache { - currentRoundCache.AddSelfDiscardTile(discardTile, mixedRiskTable[discardTile], isReach) - } - - if DebugMode { - if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { - return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) - } - } - - return nil - } - - // 他家舍牌 - d.descLeftCounts(discardTile) - - _disTile := discardTile - if isTsumogiri { - _disTile = ^_disTile - } - d.globalDiscardTiles = append(d.globalDiscardTiles, _disTile) - player.discardTiles = append(player.discardTiles, _disTile) - player.latestDiscardAtGlobal = len(d.globalDiscardTiles) - 1 - - // 标记外侧牌 - if !player.isReached && len(player.discardTiles) <= 5 { - player.earlyOutsideTiles = append(player.earlyOutsideTiles, util.OutsideTiles(discardTile)...) - } - - if player.isReached && player.reachTileAtGlobal == -1 { - // 标记立直宣言牌 - player.reachTileAtGlobal = len(d.globalDiscardTiles) - 1 - player.reachTileAt = len(player.discardTiles) - 1 - // 若该玩家摸切立直,打印提示信息 - if isTsumogiri && !d.SkipOutput { - color.HiYellow("%s 摸切立直!", player.name) - } - } else if len(player.meldDiscardsAt) != len(player.melds) { - // 标记鸣牌的舍牌 - // 注意这里会标记到暗杠后的舍牌上 - // 注意对于连续开杠的情况,len(player.meldDiscardsAt) 和 len(player.melds) 是不等的 - player.meldDiscardsAt = append(player.meldDiscardsAt, len(player.discardTiles)-1) - player.meldDiscardsAtGlobal = append(player.meldDiscardsAtGlobal, len(d.globalDiscardTiles)-1) - } - - // 若玩家在立直后摸牌舍牌,则没有一发 - if player.reachTileAt < len(player.discardTiles)-1 { - player.canIppatsu = false - } - - playerInfo := d.newModelPlayerInfo() - - // 安全度分析 - riskTables := d.analysisTilesRisk() - mixedRiskTable := riskTables.mixedRiskTable() - - // 牌谱分析模式下,记录可能的鸣牌 - if d.GameMode == GameModeRecordCache { - allowChi := who == 3 - _, results14, incShantenResults14 := util.CalculateMeld(playerInfo, discardTile, isRedFive, allowChi) - bestAttackDiscardTile := -1 - if len(results14) > 0 { - bestAttackDiscardTile = results14[0].DiscardTile - } else if len(incShantenResults14) > 0 { - bestAttackDiscardTile = incShantenResults14[0].DiscardTile - } - if bestAttackDiscardTile != -1 { - bestDefenceDiscardTile := mixedRiskTable.getBestDefenceTile(playerInfo.HandTiles34) - bestAttackDiscardTileRisk := 0.0 - if bestDefenceDiscardTile >= 0 { - bestAttackDiscardTileRisk = mixedRiskTable[bestAttackDiscardTile] - } - currentRoundCache.AddPossibleChiPonKan(bestAttackDiscardTile, bestAttackDiscardTileRisk) - } - } - - if d.SkipOutput { - return nil - } - - // 上家舍牌时若无法鸣牌则跳过显示 - //if d.gameMode == gameModeMatch && who == 3 && !canBeMeld { - // return nil - //} - - if !DebugMode { - ClearConsole() - } - - // 牌谱模式下,打印舍牌推荐 - if d.GameMode == gameModeRecord { - currentRoundCache.Print() - } - - // 打印他家舍牌信息 - d.printDiscards() - fmt.Println() - riskTables.printWithHands(d.counts, d.leftCounts) - - if d.GameMode == gameModeMatch && !canBeMeld { - return nil - } - - // 为了方便解析牌谱,这里尽可能地解析副露 - // TODO: 提醒: 消除海底/避免河底 - allowChi := d.playerNumber != 3 && who == 3 && playerInfo.LeftDrawTilesCount > 0 - return analysisMeld(playerInfo, discardTile, isRedFive, allowChi, mixedRiskTable) - case d.parser.IsRoundWin(): - // TODO: 解析天凤牌谱 - 注意 skipOutput - - if !DebugMode { - ClearConsole() - } - fmt.Println("和牌,本局结束") - whos, points := d.parser.ParseRoundWin() - if len(whos) == 3 { - color.HiYellow("凤 凰 级 避 铳") - if d.parser.GetDataSourceType() == dataSourceTypeMajsoul { - color.HiYellow("(快醒醒,这是雀魂)") - } - } - for i, who := range whos { - fmt.Println(d.players[who].name, points[i]) - } - case d.parser.IsRyuukyoku(): - // TODO - d.parser.ParseRyuukyoku() - case d.parser.IsNukiDora(): - who, isTsumogiri := d.parser.ParseNukiDora() - player := d.players[who] - player.nukiDoraNum++ - if who != 0 { - // 减少北的数量 - d.descLeftCounts(30) - // TODO - _ = isTsumogiri - } else { - // 减少自己手牌中北的数量 - d.counts[30]-- - } - // 消除一发 - for _, player := range d.players { - player.canIppatsu = false - } - case d.parser.IsNewDora(): - // 杠宝牌 - // 1. 剩余牌减少 - // 2. 打点提高 - kanDoraIndicator := d.parser.ParseNewDora() - d.newDora(kanDoraIndicator) - default: - } - - return nil -} diff --git a/core_PlayerInfo.go b/core_PlayerInfo.go new file mode 100644 index 0000000..936f8a0 --- /dev/null +++ b/core_PlayerInfo.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/EndlessCheng/mahjong-helper/util/model" + "github.com/fatih/color" +) + +type PlayerInfo struct { + name string // 自家/下家/对家/上家 + + selfWindTile int // 自风 + + melds []*model.Meld // 副露 + meldDiscardsAtGlobal []int + meldDiscardsAt []int + isNaki bool // 是否鳴牌(暗杠不算鳴牌) + + // 注意负数(自摸切)要^ + discardTiles []int // 该玩家的舍牌 + latestDiscardAtGlobal int // 该玩家最近一次舍牌在 globalDiscardTiles 中的下标,初始为 -1 + earlyOutsideTiles []int // 立直前的1-5巡的外侧牌 + + isReached bool // 是否立直 + canIppatsu bool // 是否有一发 + + reachTileAtGlobal int // 立直宣言牌在 globalDiscardTiles 中的下标,初始为 -1 + reachTileAt int // 立直宣言牌在 discardTiles 中的下标,初始为 -1 + + nukiDoraNum int // 拔北宝牌数 +} + +func (p *PlayerInfo) doraNum(doraList []int) (doraCount int) { + for _, meld := range p.melds { + for _, tile := range meld.Tiles { + for _, doraTile := range doraList { + if tile == doraTile { + doraCount++ + } + } + } + if meld.ContainRedFive { + doraCount++ + } + } + if p.nukiDoraNum > 0 { + doraCount += p.nukiDoraNum + // 特殊:西为指示牌 + for _, doraTile := range doraList { + if doraTile == 30 { + doraCount += p.nukiDoraNum + } + } + } + return +} + +func (p *PlayerInfo) printDiscards() { + // TODO: 高亮不合理的舍牌或危险舍牌,如 + // - 一开始就切中张 + // - 开始切中张后,手切了幺九牌(也有可能是有人碰了牌,比如 133m 有人碰了 2m) + // - 切了 dora,提醒一下 + // - 切了赤宝牌 + // - 有人立直的情况下,多次切出危险度高的牌(有可能是对方读准了牌,或者对方手里的牌与牌河加起来产生了安牌) + // - 其余可以参考贴吧的《魔神之眼》翻译 https://tieba.baidu.com/p/3311909701 + // 举个简单的例子,如果出现手切了一个对子的情况的话那么基本上就不可能是七对子。 + // 如果对方早巡手切了一个两面搭子的话,那么就可以推理出他在做染手或者牌型是对子型,如果他立直或者鳴牌的话,也比较容易读出他的手牌。 + // https://tieba.baidu.com/p/3311909701 + // 鳴牌之后和终盘的手切牌要尽量记下来,别人手切之前的安牌应该先切掉 + // https://tieba.baidu.com/p/3372239806 + // 吃牌时候打出来的牌的颜色是危险的;碰之后全部的牌都是危险的 + + fmt.Printf(p.name + ":") + for i, disTile := range p.discardTiles { + fmt.Printf(" ") + // TODO: 显示 dora, 赤宝牌 + bgColor := color.BgBlack + fgColor := color.FgWhite + var tile string + if disTile >= 0 { // 手切 + tile = util.Mahjong[disTile] + if disTile >= 27 { + tile = util.MahjongU[disTile] // 关注字牌的手切 + } + if p.isNaki { // 副露 + fgColor = getOtherDiscardAlertColor(disTile) // 高亮中张手切 + if util.InInts(i, p.meldDiscardsAt) { + bgColor = color.BgWhite // 鳴牌时切的那张牌要背景高亮 + fgColor = color.FgBlack + } + } + } else { // 摸切 + disTile = ^disTile + tile = util.Mahjong[disTile] + fgColor = color.FgHiBlack // 暗色显示 + } + color.New(bgColor, fgColor).Print(tile) + } + fmt.Println() +} diff --git a/core_RoundData.go b/core_RoundData.go new file mode 100644 index 0000000..592951a --- /dev/null +++ b/core_RoundData.go @@ -0,0 +1,768 @@ +package main + +import ( + "fmt" + + "github.com/EndlessCheng/mahjong-helper/Console" + "github.com/EndlessCheng/mahjong-helper/util" + "github.com/EndlessCheng/mahjong-helper/util/model" + "github.com/fatih/color" +) + +type RoundData struct { + parser DataParser + + GameMode gameMode + + SkipOutput bool + + // 玩家数,3 为三麻,4 为四麻 + playerNumber int + + // 场数(如东1为0,东2为1,...,南1为4,...) + roundNumber int + + // 本场数,从 0 开始算 + benNumber int + + // 场风 + roundWindTile int + + // 親家 0=自家, 1=下家, 2=对家, 3=上家 + // 请用 reset 设置 + dealer int + + // 宝牌指示牌 + doraIndicators []int + + // 自家手牌 + counts []int + + // 按照 mps 的顺序记录自家赤5数量,包含副露的赤5 + // 比如有 0p 和 0s 就是 [1, 0, 1] + numRedFives []int + + // 牌山剩余牌量 + leftCounts []int + + // 全局舍牌 + // 按舍牌顺序,负数表示摸切(-),非负数表示手切(+) + // 可以理解成:- 表示不要/暗色,+ 表示进张/亮色 + globalDiscardTiles []int + + // 0=自家, 1=下家, 2=对家, 3=上家 + players []*PlayerInfo +} + +// 新的一局 +func (d *RoundData) reset(roundNumber int, benNumber int, dealer int) { + skipOutput := d.SkipOutput + gameMode := d.GameMode + playerNumber := d.playerNumber + newData := newRoundData(d.parser, roundNumber, benNumber, dealer) + newData.SkipOutput = skipOutput + newData.GameMode = gameMode + newData.playerNumber = playerNumber + if playerNumber == 3 { + // 三麻没有 2-8m + for i := 1; i <= 7; i++ { + newData.leftCounts[i] = 0 + } + newData.players = modifySanninPlayerInfoList(newData.players, roundNumber) + } + *d = *newData +} + +func (d *RoundData) newGame() { + d.reset(0, 0, 0) +} + +func (d *RoundData) descLeftCounts(tile int) { + d.leftCounts[tile]-- + if d.leftCounts[tile] < 0 { + info := fmt.Sprintf("数据异常: %s 数量为 %d", util.MahjongZH[tile], d.leftCounts[tile]) + if DebugMode { + panic(info) + } else { + fmt.Println(info) + } + } +} + +// 杠! +func (d *RoundData) newDora(kanDoraIndicator int) { + d.doraIndicators = append(d.doraIndicators, kanDoraIndicator) + d.descLeftCounts(kanDoraIndicator) + + if d.SkipOutput { + return + } + + color.Yellow("杠宝牌指示牌是 %s", util.MahjongZH[kanDoraIndicator]) +} + +// 根据宝牌指示牌计算出宝牌 +func (d *RoundData) doraList() (dl []int) { + return model.DoraList(d.doraIndicators, d.playerNumber == 3) +} + +func (d *RoundData) printDiscards() { + // 三麻的北家是不需要打印的 + for i := len(d.players) - 1; i >= 1; i-- { + if player := d.players[i]; d.playerNumber != 3 || player.selfWindTile != 30 { + player.printDiscards() + } + } +} + +// 分析34种牌的危险度 +// 可以用来判断自家手牌的安全度,以及他家是否在进攻(多次切出危险度高的牌) +func (d *RoundData) analysisTilesRisk() (riList RiskInfoList) { + riList = make(RiskInfoList, len(d.players)) + for who := range riList { + riList[who] = &RiskInfo{ + playerNumber: d.playerNumber, + safeTiles34: make([]bool, 34), + } + } + + // 先利用振听规则收集各家安牌 + for who, player := range d.players { + if who == 0 { + // TODO: 暂时不计算自家的 + continue + } + + // 舍牌振听产生的安牌 + for _, tile := range normalDiscardTiles(player.discardTiles) { + riList[who].safeTiles34[tile] = true + } + if player.reachTileAtGlobal != -1 { + // 立直后振听产生的安牌 + for _, tile := range normalDiscardTiles(d.globalDiscardTiles[player.reachTileAtGlobal:]) { + riList[who].safeTiles34[tile] = true + } + } else if player.latestDiscardAtGlobal != -1 { + // 同巡振听产生的安牌 + // 即该玩家在最近一次舍牌后,其他玩家的舍牌 + for _, tile := range normalDiscardTiles(d.globalDiscardTiles[player.latestDiscardAtGlobal:]) { + riList[who].safeTiles34[tile] = true + } + } + + // 特殊:杠产生的安牌 + // 很难想象一个人会在有 678888 的时候去开杠(即使有这个可能,本程序也是不防的) + for _, meld := range player.melds { + if meld.IsKan() { + riList[who].safeTiles34[meld.Tiles[0]] = true + } + } + } + + // 计算各种数据 + for who, player := range d.players { + if who == 0 { + // TODO: 暂时不计算自家的 + continue + } + + // 该玩家的巡目 = 为其切过的牌的数目 + turns := util.MinInt(len(player.discardTiles), util.MaxTurns) + if turns == 0 { + turns = 1 + } + + // TODO: 若某人一直摸切,然后突然手切了一张字牌,那他很有可能默听/一向听 + if player.isReached { + riList[who].tenpaiRate = 100.0 + if player.reachTileAtGlobal < len(d.globalDiscardTiles) { // 天凤可能有数据漏掉 + riList[who].isTsumogiriRiichi = d.globalDiscardTiles[player.reachTileAtGlobal] < 0 + } + } else { + rate := util.CalcTenpaiRate(player.melds, player.discardTiles, player.meldDiscardsAt) + if d.playerNumber == 3 { + rate = util.GetTenpaiRate3(rate) + } + riList[who].tenpaiRate = rate + } + + // 估计该玩家荣和点数 + var ronPoint float64 + switch { + case player.canIppatsu: + // 立直一发巡的荣和点数 + ronPoint = util.RonPointRiichiIppatsu + case player.isReached: + // 立直非一发巡的荣和点数 + ronPoint = util.RonPointRiichiHiIppatsu + case player.isNaki: + // 副露时的荣和点数(非常粗略地估计) + doraCount := player.doraNum(d.doraList()) + ronPoint = util.RonPointOtherNakiWithDora(doraCount) + default: + // 默听时的荣和点数 + ronPoint = util.RonPointDama + } + // 亲家*1.5 + if who == d.dealer { + ronPoint *= 1.5 + } + riList[who]._ronPoint = ronPoint + + // 根据该玩家的巡目、现物、立直后通过的牌、NC、Dora、早外、荣和点数来计算每张牌的危险度 + risk34 := util.CalculateRiskTiles34(turns, riList[who].safeTiles34, d.leftCounts, d.doraList(), d.roundWindTile, player.selfWindTile). + FixWithEarlyOutside(player.earlyOutsideTiles). + FixWithPoint(ronPoint) + riList[who].riskTable = riskTable(risk34) + + // 计算剩余筋牌 + if len(player.melds) < 4 { + riList[who].leftNoSujiTiles = util.CalculateLeftNoSujiTiles(riList[who].safeTiles34, d.leftCounts) + } else { + // 大吊车:愚型听牌 + } + } + + return riList +} + +// TODO: 特殊处理w立直 +func (d *RoundData) isPlayerDaburii(who int) bool { + // w立直成立的前提是没有任何玩家副露 + for _, p := range d.players { + if len(p.melds) > 0 { + return false + } + // 对于三麻来说,还不能有拔北 + if p.nukiDoraNum > 0 { + return false + } + } + return d.players[who].reachTileAt == 0 +} + +// 自家的 PlayerInfo +func (d *RoundData) newModelPlayerInfo() *model.PlayerInfo { + const wannpaiTilesCount = 14 + leftDrawTilesCount := util.CountOfTiles34(d.leftCounts) - (wannpaiTilesCount - len(d.doraIndicators)) + for _, player := range d.players[1:] { + leftDrawTilesCount -= 13 - 3*len(player.melds) + } + if d.playerNumber == 3 { + leftDrawTilesCount += 13 + } + + melds := []model.Meld{} + for _, m := range d.players[0].melds { + melds = append(melds, *m) + } + + const self = 0 + selfPlayer := d.players[self] + + return &model.PlayerInfo{ + HandTiles34: d.counts, + Melds: melds, + DoraTiles: d.doraList(), + NumRedFives: d.numRedFives, + + RoundWindTile: d.roundWindTile, + SelfWindTile: selfPlayer.selfWindTile, + IsParent: d.dealer == self, + //IsDaburii: d.isPlayerDaburii(self), // FIXME PLS,应该在立直时就判断 + IsRiichi: selfPlayer.isReached, + + DiscardTiles: normalDiscardTiles(selfPlayer.discardTiles), + LeftTiles34: d.leftCounts, + + LeftDrawTilesCount: leftDrawTilesCount, + + NukiDoraNum: selfPlayer.nukiDoraNum, + } +} + +func (d *RoundData) Analysis() error { + if !DebugMode { + defer func() { + if err := recover(); err != nil { + fmt.Println("内部错误:", err) + } + }() + } + + if DebugMode { + if msg := d.parser.GetMessage(); len(msg) > 0 { + const printLimit = 500 + if len(msg) > printLimit { + msg = msg[:printLimit] + } + fmt.Println("收到", msg) + } + } + + // 先获取用户信息 + if d.parser.IsLogin() { + d.parser.HandleLogin() + } + + if d.parser.SkipMessage() { + return nil + } + + // 若自家立直,则进入看戏模式 + // TODO: 见逃判断 + if !d.parser.IsInit() && !d.parser.IsRoundWin() && !d.parser.IsRyuukyoku() && d.players[0].isReached { + return nil + } + + if DebugMode { + fmt.Println("当前座位为", d.parser.GetSelfSeat()) + } + + var currentRoundCache *RoundAnalysisCache + if analysisCache := GetAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { + currentRoundCache = analysisCache.WholeGameCache[d.roundNumber][d.benNumber] + } + + switch { + case d.parser.IsInit(): + // round 开始/重连 + if !DebugMode && !d.SkipOutput { + Console.ClearScreen() + } + + roundNumber, benNumber, dealer, doraIndicators, hands, numRedFives := d.parser.ParseInit() + switch d.parser.GetDataSourceType() { + case dataSourceTypeTenhou: + d.reset(roundNumber, benNumber, dealer) + d.GameMode = gameModeMatch // TODO: 牌谱模式? + case dataSourceTypeMajsoul: + if dealer != -1 { // 先就坐,还没洗牌呢~ + // 设置第一局的 dealer + d.reset(0, 0, dealer) + d.GameMode = gameModeMatch + fmt.Printf("游戏即将开始,您分配到的座位是:") + color.HiGreen(util.MahjongZH[d.players[0].selfWindTile]) + return nil + } else { + // 根据 selfSeat 和当前的 roundNumber 计算当前局的 dealer + newDealer := (4 - d.parser.GetSelfSeat() + roundNumber) % 4 + // 新的一局 + d.reset(roundNumber, benNumber, newDealer) + } + default: + panic("not impl!") + } + + // 由于 reset 了,重新获取 currentRoundCache + if analysisCache := GetAnalysisCache(d.parser.GetSelfSeat()); analysisCache != nil { + currentRoundCache = analysisCache.WholeGameCache[d.roundNumber][d.benNumber] + } + + d.doraIndicators = doraIndicators + for _, dora := range doraIndicators { + d.descLeftCounts(dora) + } + for _, tile := range hands { + d.counts[tile]++ + d.descLeftCounts(tile) + } + d.numRedFives = numRedFives + + playerInfo := d.newModelPlayerInfo() + + // 牌谱分析模式下,记录舍牌推荐 + if d.GameMode == GameModeRecordCache && len(hands) == 14 { + currentRoundCache.AddAIDiscardTileWhenDrawTile(simpleBestDiscardTile(playerInfo), -1, 0, 0) + } + + if d.SkipOutput { + return nil + } + + // 牌谱模式下,打印舍牌推荐 + if d.GameMode == gameModeRecord { + currentRoundCache.Print() + } + + color.New(color.FgHiGreen).Printf("%s", util.MahjongZH[d.roundWindTile]) + fmt.Printf("%d局开始,自风为", roundNumber%4+1) + color.New(color.FgHiGreen).Printf("%s", util.MahjongZH[d.players[0].selfWindTile]) + fmt.Println() + info := fmt.Sprintln(util.TilesToMahjongZHInterface(d.doraIndicators)...) + info = info[:len(info)-1] + color.HiYellow("宝牌指示牌是 " + info) + fmt.Println() + // TODO: 显示地和概率 + return analysisPlayerWithRisk(playerInfo, nil) + case d.parser.IsOpen(): + // 某家鳴牌(含暗杠、加杠) + who, meld, kanDoraIndicator := d.parser.ParseOpen() + meldType := meld.MeldType + meldTiles := meld.Tiles + calledTile := meld.CalledTile + + // 任何形式的鳴牌都能破除一发 + for _, player := range d.players { + player.canIppatsu = false + } + + // 杠宝牌指示牌 + if kanDoraIndicator != -1 { + d.newDora(kanDoraIndicator) + } + + player := d.players[who] + + // 不是暗杠则标记该玩家鳴牌了 + if meldType != meldTypeAnkan { + player.isNaki = true + } + + // 加杠单独处理 + if meldType == meldTypeKakan { + if who != 0 { + // (不是自家时)修改牌山剩余量 + d.descLeftCounts(calledTile) + } else { + // 自家加杠成功,修改手牌 + d.counts[calledTile]-- + // 由于均为自家操作,宝牌数是不变的 + + // 牌谱分析模式下,记录加杠操作 + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddKan(meldType) + } + } + // 修改原副露 + for _, _meld := range player.melds { + // 找到原有的碰副露 + if _meld.Tiles[0] == calledTile { + _meld.MeldType = meldTypeKakan + _meld.Tiles = append(_meld.Tiles, calledTile) + _meld.ContainRedFive = meld.ContainRedFive + break + } + } + + if DebugMode { + if who == 0 { + if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { + return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) + } + } + } + + break + } + + // 修改玩家副露数据 + d.players[who].melds = append(d.players[who].melds, meld) + + if who != 0 { + // (不是自家时)修改牌山剩余量 + // 先增后减 + if meldType != meldTypeAnkan { + d.leftCounts[calledTile]++ + } + for _, tile := range meldTiles { + d.descLeftCounts(tile) + } + } else { + // 自家,修改手牌 + if meldType == meldTypeAnkan { + d.counts[meldTiles[0]] = 0 + + // 牌谱分析模式下,记录暗杠操作 + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddKan(meldType) + } + } else { + d.counts[calledTile]++ + for _, tile := range meldTiles { + d.counts[tile]-- + } + if meld.RedFiveFromOthers { + tileType := meldTiles[0] / 9 + d.numRedFives[tileType]++ + } + + // 牌谱分析模式下,记录吃碰明杠操作 + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddChiPonKan(meldType) + } + } + + if DebugMode { + if meldType == meldTypeMinkan || meldType == meldTypeAnkan { + if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { + return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) + } + } else { + if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 2 { + return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) + } + } + } + } + case d.parser.IsReach(): + // 立直宣告 + // 如果是他家立直,进入攻守判断模式 + who := d.parser.ParseReach() + d.players[who].isReached = true + d.players[who].canIppatsu = true + //case "AGARI", "RYUUKYOKU": + // // 某人和牌或流局,round 结束 + //case "PROF": + // // 游戏结束 + //case "BYE": + // // 某人退出 + //case "REJOIN", "GO": + // // 重连 + case d.parser.IsFuriten(): + // 振听 + if d.SkipOutput { + return nil + } + color.HiYellow("振听") + //case "U", "V", "W": + // //(下家,对家,上家 不要其上家的牌)摸牌 + //case "HELO", "RANKING", "TAIKYOKU", "UN", "LN", "SAIKAI": + // // 其他 + case d.parser.IsSelfDraw(): + if !DebugMode && !d.SkipOutput { + Console.ClearScreen() + } + // 自家(从牌山 d.leftCounts)摸牌(至手牌 d.counts) + tile, isRedFive, kanDoraIndicator := d.parser.ParseSelfDraw() + d.descLeftCounts(tile) + d.counts[tile]++ + if isRedFive { + d.numRedFives[tile/9]++ + } + if kanDoraIndicator != -1 { + d.newDora(kanDoraIndicator) + } + + playerInfo := d.newModelPlayerInfo() + + // 安全度分析 + riskTables := d.analysisTilesRisk() + mixedRiskTable := riskTables.mixedRiskTable() + + // 牌谱分析模式下,记录舍牌推荐 + if d.GameMode == GameModeRecordCache { + bestAttackDiscardTile := simpleBestDiscardTile(playerInfo) + bestDefenceDiscardTile := mixedRiskTable.getBestDefenceTile(playerInfo.HandTiles34) + bestAttackDiscardTileRisk, bestDefenceDiscardTileRisk := 0.0, 0.0 + if bestDefenceDiscardTile >= 0 { + bestAttackDiscardTileRisk = mixedRiskTable[bestAttackDiscardTile] + bestDefenceDiscardTileRisk = mixedRiskTable[bestDefenceDiscardTile] + } + currentRoundCache.AddAIDiscardTileWhenDrawTile(bestAttackDiscardTile, bestDefenceDiscardTile, bestAttackDiscardTileRisk, bestDefenceDiscardTileRisk) + } + + if d.SkipOutput { + return nil + } + + // 牌谱模式下,打印舍牌推荐 + if d.GameMode == gameModeRecord { + currentRoundCache.Print() + } + + // 打印他家舍牌信息 + d.printDiscards() + fmt.Println() + + // 打印手牌对各家的安全度 + riskTables.printWithHands(d.counts, d.leftCounts) + + // 打印何切推荐 + // TODO: 根据是否听牌/一向听、打点、巡目、和率等进行攻守判断 + return analysisPlayerWithRisk(playerInfo, mixedRiskTable) + case d.parser.IsDiscard(): + who, discardTile, isRedFive, isTsumogiri, isReach, canBeMeld, kanDoraIndicator := d.parser.ParseDiscard() + + if kanDoraIndicator != -1 { + d.newDora(kanDoraIndicator) + } + + player := d.players[who] + if isReach { + player.isReached = true + player.canIppatsu = true + } + + if who == 0 { + // 特殊处理自家舍牌的情况 + riskTables := d.analysisTilesRisk() + mixedRiskTable := riskTables.mixedRiskTable() + + // 自家(从手牌 d.counts)舍牌(至牌河 d.globalDiscardTiles) + d.counts[discardTile]-- + + d.globalDiscardTiles = append(d.globalDiscardTiles, discardTile) + player.discardTiles = append(player.discardTiles, discardTile) + player.latestDiscardAtGlobal = len(d.globalDiscardTiles) - 1 + + if isRedFive { + d.numRedFives[discardTile/9]-- + } + + // 牌谱分析模式下,记录自家舍牌 + if d.GameMode == GameModeRecordCache { + currentRoundCache.AddSelfDiscardTile(discardTile, mixedRiskTable[discardTile], isReach) + } + + if DebugMode { + if handsCount := util.CountOfTiles34(d.counts); handsCount%3 != 1 { + return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, d.counts) + } + } + + return nil + } + + // 他家舍牌 + d.descLeftCounts(discardTile) + + _disTile := discardTile + if isTsumogiri { + _disTile = ^_disTile + } + d.globalDiscardTiles = append(d.globalDiscardTiles, _disTile) + player.discardTiles = append(player.discardTiles, _disTile) + player.latestDiscardAtGlobal = len(d.globalDiscardTiles) - 1 + + // 标记外侧牌 + if !player.isReached && len(player.discardTiles) <= 5 { + player.earlyOutsideTiles = append(player.earlyOutsideTiles, util.OutsideTiles(discardTile)...) + } + + if player.isReached && player.reachTileAtGlobal == -1 { + // 标记立直宣言牌 + player.reachTileAtGlobal = len(d.globalDiscardTiles) - 1 + player.reachTileAt = len(player.discardTiles) - 1 + // 若该玩家摸切立直,打印提示信息 + if isTsumogiri && !d.SkipOutput { + color.HiYellow("%s 摸切立直!", player.name) + } + } else if len(player.meldDiscardsAt) != len(player.melds) { + // 标记鳴牌的舍牌 + // 注意这里会标记到暗杠后的舍牌上 + // 注意对于连续开杠的情况,len(player.meldDiscardsAt) 和 len(player.melds) 是不等的 + player.meldDiscardsAt = append(player.meldDiscardsAt, len(player.discardTiles)-1) + player.meldDiscardsAtGlobal = append(player.meldDiscardsAtGlobal, len(d.globalDiscardTiles)-1) + } + + // 若玩家在立直后摸牌舍牌,则没有一发 + if player.reachTileAt < len(player.discardTiles)-1 { + player.canIppatsu = false + } + + playerInfo := d.newModelPlayerInfo() + + // 安全度分析 + riskTables := d.analysisTilesRisk() + mixedRiskTable := riskTables.mixedRiskTable() + + // 牌谱分析模式下,记录可能的鳴牌 + if d.GameMode == GameModeRecordCache { + allowChi := who == 3 + _, results14, incShantenResults14 := util.CalculateMeld(playerInfo, discardTile, isRedFive, allowChi) + bestAttackDiscardTile := -1 + if len(results14) > 0 { + bestAttackDiscardTile = results14[0].DiscardTile + } else if len(incShantenResults14) > 0 { + bestAttackDiscardTile = incShantenResults14[0].DiscardTile + } + if bestAttackDiscardTile != -1 { + bestDefenceDiscardTile := mixedRiskTable.getBestDefenceTile(playerInfo.HandTiles34) + bestAttackDiscardTileRisk := 0.0 + if bestDefenceDiscardTile >= 0 { + bestAttackDiscardTileRisk = mixedRiskTable[bestAttackDiscardTile] + } + currentRoundCache.AddPossibleChiPonKan(bestAttackDiscardTile, bestAttackDiscardTileRisk) + } + } + + if d.SkipOutput { + return nil + } + + // 上家舍牌时若无法鳴牌则跳过显示 + //if d.gameMode == gameModeMatch && who == 3 && !canBeMeld { + // return nil + //} + + if !DebugMode { + Console.ClearScreen() + } + + // 牌谱模式下,打印舍牌推荐 + if d.GameMode == gameModeRecord { + currentRoundCache.Print() + } + + // 打印他家舍牌信息 + d.printDiscards() + fmt.Println() + riskTables.printWithHands(d.counts, d.leftCounts) + + if d.GameMode == gameModeMatch && !canBeMeld { + return nil + } + + // 为了方便解析牌谱,这里尽可能地解析副露 + // TODO: 提醒: 消除海底/避免河底 + allowChi := d.playerNumber != 3 && who == 3 && playerInfo.LeftDrawTilesCount > 0 + return analysisMeld(playerInfo, discardTile, isRedFive, allowChi, mixedRiskTable) + case d.parser.IsRoundWin(): + // TODO: 解析天凤牌谱 - 注意 skipOutput + + if !DebugMode { + Console.ClearScreen() + } + fmt.Println("和牌,本局结束") + whos, points := d.parser.ParseRoundWin() + if len(whos) == 3 { + color.HiYellow("凤 凰 级 避 铳") + if d.parser.GetDataSourceType() == dataSourceTypeMajsoul { + color.HiYellow("(快醒醒,这是雀魂)") + } + } + for i, who := range whos { + fmt.Println(d.players[who].name, points[i]) + } + case d.parser.IsRyuukyoku(): + // TODO + d.parser.ParseRyuukyoku() + case d.parser.IsNukiDora(): + who, isTsumogiri := d.parser.ParseNukiDora() + player := d.players[who] + player.nukiDoraNum++ + if who != 0 { + // 减少北的数量 + d.descLeftCounts(30) + // TODO + _ = isTsumogiri + } else { + // 减少自己手牌中北的数量 + d.counts[30]-- + } + // 消除一发 + for _, player := range d.players { + player.canIppatsu = false + } + case d.parser.IsNewDora(): + // 杠宝牌 + // 1. 剩余牌减少 + // 2. 打点提高 + kanDoraIndicator := d.parser.ParseNewDora() + d.newDora(kanDoraIndicator) + default: + } + + return nil +} diff --git a/main.go b/main.go index 769c093..abf5ef2 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/EndlessCheng/mahjong-helper/Console" + "github.com/EndlessCheng/mahjong-helper/util" "github.com/EndlessCheng/mahjong-helper/util/model" "github.com/fatih/color" @@ -14,7 +16,7 @@ import ( // Enum const ( - Tenhou int = iota + TenHou int = iota MahJongSoul ) @@ -51,14 +53,14 @@ var ( Type: []string{ "Web", "4K"}, - Code: Tenhou, + Code: TenHou, }, { Name: "雀魂", Type: []string{ "國際中文服", "日服", - "国际服"}, + "國際服"}, Code: MahJongSoul, }, } @@ -67,6 +69,8 @@ var ( func init() { rand.Seed(time.Now().UnixNano()) + // this program can run different mode by command-line with argument "old, majsoul, tenhou, analysis, interactive, + // i detail agari, a, score, s, yaku, y, dora, d, port and p" to use different mode" flag.BoolVar(&ConsiderOldYaku, "old", false, "允许古役") flag.BoolVar(&IsMajsoul, "majsoul", false, "雀魂助手") flag.BoolVar(&IsTenhou, "tenhou", false, "天凤助手") @@ -86,30 +90,29 @@ func init() { flag.IntVar(&Port, "p", 12121, "同 -port") } -const readmeURL = "https://github.com/EndlessCheng/mahjong-helper/blob/master/README.md" -const issueURL = "https://github.com/EndlessCheng/mahjong-helper/issues" -const issueCommonQuestions = "https://github.com/EndlessCheng/mahjong-helper/issues/104" -const qqGroupNum = "375865038" - +// Print description of README, question issues, and community and +// let player can choose witch platform and reminder func welcome() int { - fmt.Println("使用说明:" + readmeURL) - fmt.Println("问题反馈:" + issueURL) - fmt.Println("吐槽群:" + qqGroupNum) + fmt.Println("使用说明:" + "https://github.com/EndlessCheng/mahjong-helper/blob/master/README.md") + fmt.Println("问题反馈:" + "https://github.com/EndlessCheng/mahjong-helper/issues") + fmt.Println("吐槽群:" + "375865038") fmt.Println() RenterPlatform: // wrong enter goto label // print platforms for _, element := range Platforms { - fmt.Printf("%d - %s %v\n", element.Code, element.Name, element.Type) + + fmt.Printf("%d - %s %v\n\n", element.Code, element.Name, "[" + strings.Join(element.Type,`,` ) +"]") } fmt.Print("請選擇對應的網站(0或1),如未選擇則預設雀魂(1): ") // set default value to int MahJongSoul(1) can exclude not int type - choose := MahJongSoul + choose := TenHou + // choose := MahJongSoul fmt.Scanln(&choose) - ClearConsole() - if choose == Tenhou { // choose TenHou + Console.ClearScreen() + if choose == TenHou { // choose TenHou color.HiGreen("已選擇 - %s", Platforms[0].Name) } else if choose == MahJongSoul { // choose MahJongSoul color.HiGreen("已選擇 - %s", Platforms[1].Name) @@ -119,7 +122,7 @@ RenterPlatform: // wrong enter goto label 该步骤用于获取您的账号 ID,便于在游戏开始时获取自风,否则程序将无法解析后续数据。 若助手无响应,请确认您已按步骤安装完成。 -相关链接 ` + issueCommonQuestions) +相关链接 https://github.com/EndlessCheng/mahjong-helper/issues/104`) } } else { // the choice not in selection fmt.Printf("輸入錯誤,請重新輸入選擇\n\n") @@ -130,13 +133,16 @@ RenterPlatform: // wrong enter goto label } func main() { + Console.ClearScreen() flag.Parse() - color.HiGreen("日本麻将助手 %s (by EndlessCheng)", Version) + // print text with green color + color.HiGreen("日本麻将助手 ver.%s (by EndlessCheng)", Version) if Version != VersionDev { go CheckNewVersion(Version) } + // set consider old yaku to false util.SetConsiderOldYaku(ConsiderOldYaku) HumanTiles := strings.Join(flag.Args(), " ") @@ -146,6 +152,7 @@ func main() { } var err error + // switch to different mode by command-line argument switch { case IsMajsoul: err = RunServer(true, Port) diff --git a/majsoul.go b/majsoul.go index 6d8b3ae..978d636 100644 --- a/majsoul.go +++ b/majsoul.go @@ -453,7 +453,7 @@ func (d *MahJongSoulRoundData) ParseOpen() (who int, meld *model.Meld, kanDoraIn } else if len(meldTiles) == 4 { meldType = meldTypeMinkan // 大明杠 } else { - panic("鸣牌数据解析失败!") + panic("鳴牌数据解析失败!") } meld = &model.Meld{ MeldType: meldType, diff --git a/majsoul_record.go b/majsoul_record.go index 1b34f87..9771020 100644 --- a/majsoul_record.go +++ b/majsoul_record.go @@ -33,13 +33,13 @@ func (i *MahJongSoulRecordBaseInfo) sort() { }) } -var seatNameZH = []string{"东", "南", "西", "北"} +var seatNameZH = []string{"東", "南", "西", "北"} func (i *MahJongSoulRecordBaseInfo) String() string { i.sort() const timeFormat = "2006-01-02 15:04:05" - output := fmt.Sprintf("%s\n从 %s\n到 %s\n\n", i.UUID, time.Unix(i.StartTime, 0).Format(timeFormat), time.Unix(i.EndTime, 0).Format(timeFormat)) + output := fmt.Sprintf("%s\n從 %s\n到 %s\n\n", i.UUID, time.Unix(i.StartTime, 0).Format(timeFormat), time.Unix(i.EndTime, 0).Format(timeFormat)) maxAccountID := 0 for _, account := range i.Accounts { diff --git a/platform/majsoul/api/liqi_test.go b/platform/majsoul/api/liqi_test.go index 1216e00..2ff787b 100644 --- a/platform/majsoul/api/liqi_test.go +++ b/platform/majsoul/api/liqi_test.go @@ -102,7 +102,7 @@ func TestLogin(t *testing.T) { if err != nil { t.Skip("登录失败:", err) } - t.Log("登录成功:", respLogin) + t.Log("登入成功:", respLogin) t.Log(respLogin.AccessToken) time.Sleep(time.Second) @@ -150,7 +150,7 @@ func TestReLogin(t *testing.T) { if err != nil { t.Skip("登录失败:", err) } - t.Log("登录成功:", respLogin) + t.Log("登入成功:", respLogin) t.Log(respLogin.AccessToken) time.Sleep(time.Second) diff --git a/platform/majsoul/proto/lq/helper.go b/platform/majsoul/proto/lq/helper.go index 08c9180..1cfa4df 100644 --- a/platform/majsoul/proto/lq/helper.go +++ b/platform/majsoul/proto/lq/helper.go @@ -50,7 +50,7 @@ func (m *Friend) CLIString() string { type FriendList []*Friend func (l FriendList) String() string { - out := "好友账号ID 好友上次登录时间 好友上次登出时间 好友昵称\n" + out := "好友帳號ID 好友上次登入時間 好友上次登出时间 好友暱稱\n" for _, friend := range l { out += friend.CLIString() + "\n" } diff --git a/server.go b/server.go index bdd9123..12b03fb 100644 --- a/server.go +++ b/server.go @@ -13,6 +13,7 @@ import ( "strconv" "time" + "github.com/EndlessCheng/mahjong-helper/Console" "github.com/EndlessCheng/mahjong-helper/platform/tenhou" "github.com/EndlessCheng/mahjong-helper/util" "github.com/EndlessCheng/mahjong-helper/util/debug" @@ -288,8 +289,8 @@ func (handler *MahJongHandler) RunAnalysisMahJongSoulMessageTask() { handler.MahJongSoulRoundData.newGame() handler.MahJongSoulRoundData.SelfSeat = 0 // 观战进来后看的是东起的玩家 handler.MahJongSoulRoundData.GameMode = gameModeLive - ClearConsole() - fmt.Printf("正在载入对战:%s", d.LiveBaseInfo.String()) + Console.ClearScreen() + fmt.Printf("正在載入對戰:%s", d.LiveBaseInfo.String()) case d.LiveFastAction != nil: if err := handler.loadLiveAction(d.LiveFastAction, true); err != nil { handler.LogError(err) @@ -342,7 +343,7 @@ func (handler *MahJongHandler) loadMahJongSoulRecordBaseInfo(mahjongsoulRecordUU // 标记当前正在观看的牌谱 handler.MahJongSoulCurrentRecordUUID = mahjongsoulRecordUUID - ClearConsole() + Console.ClearScreen() fmt.Printf("正在解析雀魂牌谱:%s", baseInfo.String()) // 标记古役模式 @@ -350,7 +351,7 @@ func (handler *MahJongHandler) loadMahJongSoulRecordBaseInfo(mahjongsoulRecordUU util.SetConsiderOldYaku(isOldYaKuMode) if isOldYaKuMode { fmt.Println() - color.HiGreen("古役模式已开启") + color.HiGreen("古役模式已開啟") } return nil diff --git a/tenhou.go b/tenhou.go index 94440e9..534b1a1 100644 --- a/tenhou.go +++ b/tenhou.go @@ -430,7 +430,7 @@ func (d *TenHouRoundData) HandleLogin() { handler.LogError(err) } if username != gameConf.currentActiveTenhouUsername { - color.HiGreen("%s 登录成功", username) + color.HiGreen("%s 登入成功", username) gameConf.currentActiveTenhouUsername = username } } diff --git a/util/agari_test.go b/util/agari_test.go index 28869f4..5de92bf 100644 --- a/util/agari_test.go +++ b/util/agari_test.go @@ -14,8 +14,8 @@ func TestIsAgari(t *testing.T) { "22334455m 234s 234p", "111222333m 234s 11z", "112233m 112233p 11z", - "11223344556677z", // 七对子 - "1133556699m 1122s", // 七对子 + "11223344556677z", // 七對子 + "1133556699m 1122s", // 七對子 "11m 345p", "11m 112233p", "11m 123456789p", @@ -41,7 +41,7 @@ func TestIsAgari(t *testing.T) { func TestDivideTiles34(t *testing.T) { assert := assert.New(t) - const otherDivideResult = "国士 or 未和牌" + const otherDivideResult = "國士無雙 or 未和牌" divideTiles := func(humanTiles string) string { drs := DivideTiles34(MustStrToTiles34(humanTiles)) if len(drs) == 0 { @@ -54,20 +54,20 @@ func TestDivideTiles34(t *testing.T) { return strings.Join(results, ", ") } - assert.Equal("[七对子]", divideTiles("11223344556677z")) - assert.Equal("[七对子]", divideTiles("223344m 11335577s")) - assert.Equal("[99s 111s 123s 456s 789s][九莲宝灯][一气通贯]", divideTiles("11112345678999s")) - assert.Equal("[22s 111s 999s 345s 678s][九莲宝灯]", divideTiles("11122345678999s")) - assert.Equal("[11s 999s 123s 345s 678s][九莲宝灯]", divideTiles("11123345678999s")) - assert.Equal("[99s 111s 234s 456s 789s][九莲宝灯]", divideTiles("11123445678999s")) - assert.Equal("[55s 111s 999s 234s 678s][九莲宝灯]", divideTiles("11123455678999s")) - assert.Equal("[11s 999s 123s 456s 789s][九莲宝灯][一气通贯]", divideTiles("11123456789999s")) - assert.Equal("[44s 123m 456m 789m 123s][一气通贯]", divideTiles("123456789m 12344s")) - assert.Equal("[11m 123p 456p 789p][一气通贯]", divideTiles("11m 123456789p")) - assert.Equal("[11p 123p 456p 789p][一气通贯]", divideTiles("11123456789p")) - assert.Equal("[11z 123m 123m 123p 123p][两杯口]", divideTiles("112233m 112233p 11z")) - assert.Equal("[22m 345m 345m 234p 234s][一杯口], [55m 234m 234m 234p 234s][一杯口]", divideTiles("22334455m 234s 234p")) - assert.Equal("[11z 111m 222m 333m 234s], [11z 123m 123m 123m 234s][一杯口]", divideTiles("111222333m 234s 11z")) + assert.Equal("[七對子]", divideTiles("11223344556677z")) + assert.Equal("[七對子]", divideTiles("223344m 11335577s")) + assert.Equal("[99s 111s 123s 456s 789s][九蓮寶燈][一氣貫通]", divideTiles("11112345678999s")) + assert.Equal("[22s 111s 999s 345s 678s][九蓮寶燈]", divideTiles("11122345678999s")) + assert.Equal("[11s 999s 123s 345s 678s][九蓮寶燈]", divideTiles("11123345678999s")) + assert.Equal("[99s 111s 234s 456s 789s][九蓮寶燈]", divideTiles("11123445678999s")) + assert.Equal("[55s 111s 999s 234s 678s][九蓮寶燈]", divideTiles("11123455678999s")) + assert.Equal("[11s 999s 123s 456s 789s][九蓮寶燈][一氣貫通]", divideTiles("11123456789999s")) + assert.Equal("[44s 123m 456m 789m 123s][一氣貫通]", divideTiles("123456789m 12344s")) + assert.Equal("[11m 123p 456p 789p][一氣貫通]", divideTiles("11m 123456789p")) + assert.Equal("[11p 123p 456p 789p][一氣貫通]", divideTiles("11123456789p")) + assert.Equal("[11z 123m 123m 123p 123p][二盃口]", divideTiles("112233m 112233p 11z")) + assert.Equal("[22m 345m 345m 234p 234s][一盃口], [55m 234m 234m 234p 234s][一盃口]", divideTiles("22334455m 234s 234p")) + assert.Equal("[11z 111m 222m 333m 234s], [11z 123m 123m 123m 234s][一盃口]", divideTiles("111222333m 234s 11z")) assert.Equal("[11m 234m 234m], [44m 123m 123m]", divideTiles("11223344m")) assert.Equal("[22z 111m 111z 234m 678m]", divideTiles("111234678m 11122z")) assert.Equal("[11m 345p]", divideTiles("11m 345p")) diff --git a/util/model/meld.go b/util/model/meld.go index 89c0695..89a793c 100644 --- a/util/model/meld.go +++ b/util/model/meld.go @@ -9,11 +9,11 @@ const ( ) type Meld struct { - MeldType int // 鸣牌类型(吃、碰、暗杠、大明杠、加杠) + MeldType int // 鳴牌类型(吃、碰、暗杠、大明杠、加杠) // Tiles == sort(SelfTiles + CalledTile) Tiles []int // 副露的牌 - SelfTiles []int // 手牌中组成副露的牌(用于鸣牌分析) + SelfTiles []int // 手牌中组成副露的牌(用于鳴牌分析) CalledTile int // 被鸣的牌 // TODO: 重构 ContainRedFive RedFiveFromOthers diff --git a/util/model/player_info.go b/util/model/player_info.go index 567faa5..7ea354b 100644 --- a/util/model/player_info.go +++ b/util/model/player_info.go @@ -104,7 +104,7 @@ func (pi *PlayerInfo) CountDora() (count int) { // return float64(len(pi.DoraTiles)*sum) / float64(weight) //} -// 是否已鸣牌(暗杠不算) +// 是否已鳴牌(暗杠不算) // 可以用来判断该玩家能否立直,计算门清加符、役种番数等 func (pi *PlayerInfo) IsNaki() bool { for _, meld := range pi.Melds { @@ -165,7 +165,7 @@ func (pi *PlayerInfo) UndoDiscardTile(tile int, isRedFive bool) { //} func (pi *PlayerInfo) AddMeld(meld Meld) { - // 用手牌中的牌去鸣牌 + // 用手牌中的牌去鳴牌 // 原有的宝牌数量并未发生变化 for _, tile := range meld.SelfTiles { pi.HandTiles34[tile]-- @@ -178,7 +178,7 @@ func (pi *PlayerInfo) AddMeld(meld Meld) { } func (pi *PlayerInfo) UndoAddMeld() { - // 复原鸣牌动作 + // 复原鳴牌动作 latestMeld := pi.Melds[len(pi.Melds)-1] for _, tile := range latestMeld.SelfTiles { pi.HandTiles34[tile]++ diff --git a/util/point.go b/util/point.go index 70a9399..b85066a 100644 --- a/util/point.go +++ b/util/point.go @@ -146,7 +146,7 @@ func CalcPoint(playerInfo *model.PlayerInfo) (result *PointResult) { func CalcAvgPoint(playerInfo model.PlayerInfo, waits Waits) (avgPoint float64, pointResults []*PointResult) { isFuriten := playerInfo.IsFuriten(waits) if isFuriten { - // 振听只能自摸,但是振听立直时考虑了这一点,所以只在默听或鸣牌时考虑 + // 振听只能自摸,但是振听立直时考虑了这一点,所以只在默听或鳴牌时考虑 if !playerInfo.IsRiichi { playerInfo.IsTsumo = true } @@ -186,7 +186,7 @@ func CalcAvgPoint(playerInfo model.PlayerInfo, waits Waits) (avgPoint float64, p } // 计算立直时的平均点数(考虑自摸、一发和里宝)和各种侍牌下的对应点数 -// 已鸣牌时返回 0 +// 已鳴牌时返回 0 // TODO: 剩余不到 4 张无法立直 // TODO: 不足 1000 点无法立直 func CalcAvgRiichiPoint(playerInfo model.PlayerInfo, waits Waits) (avgRiichiPoint float64, pointResults []*PointResult) { diff --git a/util/shanten_improve.go b/util/shanten_improve.go index b32909d..b724fe2 100644 --- a/util/shanten_improve.go +++ b/util/shanten_improve.go @@ -2,9 +2,10 @@ package util import ( "fmt" - "github.com/EndlessCheng/mahjong-helper/util/model" "math" "sort" + + "github.com/EndlessCheng/mahjong-helper/util/model" ) // map[改良牌]进张(选择进张数最大的) @@ -18,7 +19,7 @@ type Hand13AnalysisResult struct { // 剩余牌 LeftTiles34 []int - // 是否已鸣牌(非门清状态) + // 是否已鳴牌(非门清状态) // 用于判断是否无役等 IsNaki bool @@ -33,7 +34,7 @@ type Hand13AnalysisResult struct { // 默听时的进张 DamaWaits Waits - // TODO: 鸣牌进张:他家打出这张牌,可以鸣牌,且能让向听数前进 + // TODO: 鳴牌进张:他家打出这张牌,可以鳴牌,且能让向听数前进 //MeldWaits Waits // map[进张牌]向听前进后的(最大)进张数 @@ -67,7 +68,7 @@ type Hand13AnalysisResult struct { // 役种 YakuTypes map[int]struct{} - // (鸣牌时)是否片听 + // (鳴牌时)是否片听 IsPartWait bool // 宝牌个数(手牌+副露) @@ -116,7 +117,7 @@ func (r *Hand13AnalysisResult) mixedRoundPoint() float64 { // 调试用 func (r *Hand13AnalysisResult) String() string { - s := fmt.Sprintf("%d 进张 %s\n%.2f 改良进张 [%d(%d) 种]", + s := fmt.Sprintf("%d 進張 %s\n%.2f 改良進張 [%d(%d) 種]", r.Waits.AllCount(), //r.Waits.AllCount()+r.MeldWaits.AllCount(), TilesToStrWithBracket(r.Waits.indexes()), @@ -125,14 +126,14 @@ func (r *Hand13AnalysisResult) String() string { r.ImproveWayCount, ) if len(r.DamaWaits) > 0 { - s += fmt.Sprintf("(默听进张 %s)", TilesToStrWithBracket(r.DamaWaits.indexes())) + s += fmt.Sprintf("(默聽進張 %s)", TilesToStrWithBracket(r.DamaWaits.indexes())) } if r.Shanten >= 1 { mixedScore := r.MixedWaitsScore //for i := 2; i <= r.Shanten; i++ { // mixedScore /= 4 //} - s += fmt.Sprintf(" %.2f %s进张(%.2f 综合分)", + s += fmt.Sprintf(" %.2f %s進張(%.2f 综合分)", r.AvgNextShantenWaitsCount, NumberToChineseShanten(r.Shanten-1), mixedScore, @@ -145,7 +146,7 @@ func (r *Hand13AnalysisResult) String() string { s += fmt.Sprintf(" [局收支%d]", int(math.Round(r.MixedRoundPoint))) } if r.DamaPoint > 0 { - s += fmt.Sprintf("[默听%d]", int(math.Round(r.DamaPoint))) + s += fmt.Sprintf("[默聽%d]", int(math.Round(r.DamaPoint))) } if r.RiichiPoint > 0 { s += fmt.Sprintf("[立直%d]", int(math.Round(r.RiichiPoint))) @@ -153,9 +154,9 @@ func (r *Hand13AnalysisResult) String() string { if r.Shanten >= 0 && r.Shanten <= 1 { if r.FuritenRate > 0 { if r.FuritenRate < 1 { - s += "[可能振听]" + s += "[可能振聽]" } else { - s += "[振听]" + s += "[振聽]" } } } @@ -822,7 +823,7 @@ func CalculateShantenWithImproves14(playerInfo *model.PlayerInfo) (shanten int, return } -// 计算最小向听数,鸣牌方式 +// 计算最小向听数,鳴牌方式 func calculateMeldShanten(tiles34 []int, calledTile int, isRedFive bool, allowChi bool) (minShanten int, meldCombinations []model.Meld) { // 是否能碰 if tiles34[calledTile] >= 2 { @@ -861,7 +862,7 @@ func calculateMeldShanten(tiles34 []int, calledTile int, isRedFive bool, allowCh } } - // 计算所有鸣牌下的最小向听数 + // 计算所有鳴牌下的最小向听数 minShanten = 99 for _, c := range meldCombinations { tiles34[c.SelfTiles[0]]-- @@ -874,11 +875,11 @@ func calculateMeldShanten(tiles34 []int, calledTile int, isRedFive bool, allowCh return } -// TODO 鸣牌的情况判断(待重构) +// TODO 鳴牌的情况判断(待重构) // 编程时注意他家切掉的这张牌是否算到剩余数中 //if isOpen { //if newShanten, combinations, shantens := calculateMeldShanten(tiles34, i, true); newShanten < shanten { -// // 向听前进了,说明鸣牌成功,则换的这张牌为鸣牌进张 +// // 向听前进了,说明鳴牌成功,则换的这张牌为鳴牌进张 // // 计算进张数:若能碰则 =剩余数*3,否则 =剩余数 // meldWaits[i] = leftTile - tiles34[i] // for i, comb := range combinations { @@ -890,7 +891,7 @@ func calculateMeldShanten(tiles34 []int, calledTile int, isRedFive bool, allowCh //} //} -// 计算鸣牌下的何切分析 +// 计算鳴牌下的何切分析 // calledTile 他家出的牌,尝试鸣这张牌 // isRedFive 这张牌是否为赤5 // allowChi 是否允许吃这张牌 diff --git a/util/shanten_improve_test.go b/util/shanten_improve_test.go index 3197daa..f402918 100644 --- a/util/shanten_improve_test.go +++ b/util/shanten_improve_test.go @@ -1,10 +1,11 @@ package util import ( + "strings" "testing" + "github.com/EndlessCheng/mahjong-helper/util/model" "github.com/stretchr/testify/assert" - "strings" ) func Test_calculateIsolatedTileValue(t *testing.T) { @@ -218,11 +219,11 @@ func TestCalculateMeld(t *testing.T) { tile = "3s" tile = "7z" shanten, results, incShantenResults := CalculateMeld(pi, MustStrToTile34(tile), false, true) - t.Log("鸣牌后" + NumberToChineseShanten(shanten)) + t.Log("鳴牌后" + NumberToChineseShanten(shanten)) for _, result := range results { t.Log(result) } - t.Log("鸣牌后" + NumberToChineseShanten(shanten+1)) + t.Log("鳴牌后" + NumberToChineseShanten(shanten+1)) for _, result := range incShantenResults { t.Log(result) } diff --git a/util/shanten_search_test.go b/util/shanten_search_test.go index 2960161..08c4e8f 100644 --- a/util/shanten_search_test.go +++ b/util/shanten_search_test.go @@ -26,7 +26,7 @@ func Test_searchShanten14(t *testing.T) { shanten := CalculateShanten(tiles34) fmt.Println(NumberToChineseShanten(shanten)) fmt.Print(searchShanten14(shanten, pi, -1)) - fmt.Println("倒退回" + NumberToChineseShanten(shanten+1)) + fmt.Println("向聽倒退回" + NumberToChineseShanten(shanten+1)) fmt.Print(searchShanten14(shanten+1, pi, -1)) } @@ -38,18 +38,18 @@ func TestCalculateShantenAndWaits13(t *testing.T) { } // closed - assert.Equal("听牌 3 进张 [7z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("1122334455667z"), nil))) - assert.Equal("听牌 4 进张 [4s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("123456789m 1135s"), nil))) - assert.Equal("听牌 8 进张 [25s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("123456789m 1134s"), nil))) - assert.Equal("一向听 61 进张 [12345678m 47p 12345678s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("3456m 3456s 44456p"), nil))) - assert.Equal("两向听 12 进张 [1234z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("123456789m 1234z"), nil))) - assert.Equal("三向听 32 进张 [46m 2468p 24s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("11357m 13579p 135s"), nil))) + assert.Equal("聽牌 3 進張 [7z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("1122334455667z"), nil))) + assert.Equal("聽牌 4 進張 [4s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("123456789m 1135s"), nil))) + assert.Equal("聽牌 8 進張 [25s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("123456789m 1134s"), nil))) + assert.Equal("一向聽 61 進張 [12345678m 47p 12345678s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("3456m 3456s 44456p"), nil))) + assert.Equal("兩向聽 12 進張 [1234z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("123456789m 1234z"), nil))) + assert.Equal("三向聽 32 進張 [46m 2468p 24s]", toString(CalculateShantenAndWaits13(MustStrToTiles34("11357m 13579p 135s"), nil))) // open - assert.Equal("听牌 3 进张 [5p]", toString(CalculateShantenAndWaits13(MustStrToTiles34("5p"), nil))) - assert.Equal("听牌 6 进张 [14p]", toString(CalculateShantenAndWaits13(MustStrToTiles34("1234p"), nil))) - assert.Equal("一向听 132 进张 [12346789m 123456789p 123456789s 1234567z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("5555m"), nil))) - assert.Equal("两向听 12 进张 [1234z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("1234z"), nil))) + assert.Equal("聽牌 3 進張 [5p]", toString(CalculateShantenAndWaits13(MustStrToTiles34("5p"), nil))) + assert.Equal("聽牌 6 進張 [14p]", toString(CalculateShantenAndWaits13(MustStrToTiles34("1234p"), nil))) + assert.Equal("一向聽 132 進張 [12346789m 123456789p 123456789s 1234567z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("5555m"), nil))) + assert.Equal("兩向聽 12 進張 [1234z]", toString(CalculateShantenAndWaits13(MustStrToTiles34("1234z"), nil))) } func BenchmarkSearchShanten0(b *testing.B) { diff --git a/util/tenpai_data.go b/util/tenpai_data.go index e9d31fd..6434c5e 100644 --- a/util/tenpai_data.go +++ b/util/tenpai_data.go @@ -1,6 +1,6 @@ package util -// [副露数][巡目][副露之后的手切数(手切数为0即鸣牌那一巡)] +// [副露数][巡目][副露之后的手切数(手切数为0即鳴牌那一巡)] var tenpaiRate = [][][]float64{ { // TODO: 默听 diff --git a/util/tile.go b/util/tile.go index eeb2d9a..fd88d01 100644 --- a/util/tile.go +++ b/util/tile.go @@ -2,8 +2,8 @@ package util import ( "fmt" - "sort" "math/rand" + "sort" ) var Mahjong = [...]string{ @@ -21,12 +21,26 @@ var MahjongU = [...]string{ } var MahjongZH = [...]string{ - "1万", "2万", "3万", "4万", "5万", "6万", "7万", "8万", "9万", - "1饼", "2饼", "3饼", "4饼", "5饼", "6饼", "7饼", "8饼", "9饼", - "1索", "2索", "3索", "4索", "5索", "6索", "7索", "8索", "9索", - "东", "南", "西", "北", "白", "发", "中", + "1萬", "2萬", "3萬", "4萬", "5萬", "6萬", "7萬", "8萬", "9萬", + "1餅", "2餅", "3餅", "4餅", "5餅", "6餅", "7餅", "8餅", "9餅", + "1條", "2條", "3條", "4條", "5條", "6條", "7條", "8條", "9條", + "東", "南", "西", "北", "白", "發", "中", } +// var MahjongZH = [...]string{ +// "一", "二", "三", "四", "五", "六", "七", "八", "九", +// "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", +// "1", "2", "3", "4", "5", "6", "7", "8", "9", +// "東", "南", "西", "北", "白", "發", "中", +// } +// var MahjongZH = [...]string{ +// "🀇", "🀈", "🀉", "🀊", "🀋", "🀌", "🀍", "🀎", "🀏", +// "🀙", "🀚", "🀛", "🀜", "🀝", "🀞", "🀟", "🀠", "🀡", +// "🀐", "🀑", "🀒", "🀒", "🀔", "🀕", "🀖", "🀗", "🀘", +// "🀀", "🀁", "🀂", "🀃", "🀆", "🀅", "🀄", +// } + + var YaochuTiles = [...]int{0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33} func TilesToMahjongZH(tiles []int) (words []string) { @@ -123,7 +137,7 @@ func (w Waits) tilesZH() []string { } func (w Waits) String() string { - return fmt.Sprintf("%d 进张 %s", w.AllCount(), TilesToStrWithBracket(w.indexes())) + return fmt.Sprintf("%d 進張 %s", w.AllCount(), TilesToStrWithBracket(w.indexes())) } func (w Waits) Equals(w1 Waits) bool { diff --git a/util/util.go b/util/util.go index 63d3ada..df5a2f1 100644 --- a/util/util.go +++ b/util/util.go @@ -51,7 +51,7 @@ func InStrings(e string, arr []string) bool { } // 258m 258p 258s 12345z 在不考虑国士无双和七对子时为八向听 -var chineseShanten = []string{"和了", "听牌", "一向听", "两向听", "三向听", "四向听", "五向听", "六向听", "七向听", "八向听"} +var chineseShanten = []string{"和了", "聽牌", "一向聽", "兩向聽", "三向聽", "四向聽", "五向聽", "六向聽", "七向聽", "八向聽"} // -1=和了,0=和牌,1=一向听,…… func NumberToChineseShanten(num int) string { diff --git a/util/yaku_data.go b/util/yaku_data.go index f6066e1..0ce5641 100644 --- a/util/yaku_data.go +++ b/util/yaku_data.go @@ -7,6 +7,7 @@ import ( var considerOldYaku bool +// encapsulation func SetConsiderOldYaku(b bool) { considerOldYaku = b } @@ -90,36 +91,36 @@ const ( var YakuNameMap = map[int]string{ // Special criteria YakuRiichi: "立直", - YakuChiitoi: "七对", + YakuChiitoi: "七對", // Yaku based on luck YakuTsumo: "自摸", - //YakuIppatsu: "一发", - //YakuHaitei: "海底", - //YakuHoutei: "河底", - //YakuRinshan: "岭上", - //YakuChankan: "抢杠", - YakuDaburii: "w立", + //YakuIppatsu: "一發", + //YakuHaitei: "海底撈月", + //YakuHoutei: "河底撈魚", + //YakuRinshan: "嶺上開花", + //YakuChankan: "搶槓", + YakuDaburii: "雙立直", // Yaku based on sequences YakuPinfu: "平和", - YakuRyanpeikou: "两杯口", - YakuIipeikou: "一杯口", - YakuSanshokuDoujun: "三色", - YakuIttsuu: "一通", // 一气 + YakuRyanpeikou: "二盃口", + YakuIipeikou: "一盃口", + YakuSanshokuDoujun: "三色同順", + YakuIttsuu: "一氣貫通", // 一气 // Yaku based on triplets and/or quads - YakuToitoi: "对对", + YakuToitoi: "對對和", YakuSanAnkou: "三暗刻", YakuSanshokuDoukou: "三色同刻", - YakuSanKantsu: "三杠子", + YakuSanKantsu: "三槓子", // Yaku based on terminal or honor tiles - YakuTanyao: "断幺", + YakuTanyao: "断么", YakuYakuhai: "役牌", - YakuChanta: "混全", - YakuJunchan: "纯全", - YakuHonroutou: "混老头", // 七对也算 + YakuChanta: "混全帶么九", + YakuJunchan: "纯全帶么九", + YakuHonroutou: "混老頭", // 七对也算 YakuShousangen: "小三元", // Yaku based on suits @@ -135,11 +136,11 @@ var YakuNameMap = map[int]string{ YakuShousuushii: "小四喜", YakuDaisuushii: "大四喜", YakuTsuuiisou: "字一色", - YakuChinroutou: "清老头", + YakuChinroutou: "清老頭", YakuRyuuiisou: "绿一色", - YakuChuuren: "九莲", - YakuChuuren9: "纯正九莲", - YakuSuuKantsu: "四杠子", + YakuChuuren: "九蓮寶燈", + YakuChuuren9: "纯正九蓮寶燈", + YakuSuuKantsu: "四槓子", //YakuTenhou: "天和", //YakuChiihou: "地和", } @@ -193,7 +194,7 @@ func YakuTypesWithDoraToStr(yakuTypes map[int]struct{}, numDora int) string { } // TODO: old yaku if numDora > 0 { - names = append(names, fmt.Sprintf("宝牌%d", numDora)) + names = append(names, fmt.Sprintf("寶牌%d", numDora)) } return fmt.Sprint(names) } diff --git a/utils.go b/utils.go index dca696b..856b7d3 100644 --- a/utils.go +++ b/utils.go @@ -57,20 +57,28 @@ func getOtherDiscardAlertColor(index int) color.Attribute { } } -// 铳率高低 -func getNumRiskColor(risk float64) color.Attribute { +/* 铳率高低 +以紅橙黃綠藍紫反方向做警示 +現物表示白色, +青色表示<3%, +藍色表示<5%, +綠色表示<10%, +黃色表示<15%, +紅色表示>15%。 +*/ +func GetNumRiskColor(risk float64) color.Attribute { switch { - //case risk < 3: - // return color.FgHiBlue - case risk < 5: + case risk < 3: return color.FgHiCyan + case risk < 5: + return color.FgHiBlue //case risk < 7.5: // return color.FgYellow case risk < 10: - return color.FgHiYellow + return color.FgHiGreen case risk < 15: - return color.FgHiRed + return color.FgHiYellow default: - return color.FgRed + return color.FgHiRed } } diff --git a/version.go b/version.go index 79215a5..30eae7d 100644 --- a/version.go +++ b/version.go @@ -9,47 +9,57 @@ import ( "github.com/fatih/color" ) -const VersionDev = "dev" +// declare const +const VersionDev string = "dev" // 编译时自动写入版本号 // go build -ldflags "-X main.Version=$(git describe --abbrev=0 --tags)" -o mahjong-helper -var Version = VersionDev +// declare Version +var Version string = VersionDev -func FetchLatestVersionTag() (latestVersionTag string, err error) { - const apiGetLatestRelease = "https://api.github.com/repos/EndlessCheng/mahjong-helper/releases/latest" - const timeout = 10 * time.Second +// if check github has new version and remind update +func CheckNewVersion(currentVersionTag string) { + // target to github + const latestReleasePage = "https://github.com/EndlessCheng/mahjong-helper/releases/latest" + + latestVersionTag, err := fetchLatestVersionTag() + if err != nil { + // 下次再说~ + return + } + + if latestVersionTag > currentVersionTag { + color.HiGreen("检测到新版本: %s!请前往 %s 下载", latestVersionTag, latestReleasePage) + } +} + +// fetch the lastest version +func fetchLatestVersionTag() (latestVersionTag string, err error) { + const apiGetLatestRelease string = "https://api.github.com/repos/EndlessCheng/mahjong-helper/releases/latest" + const timeout time.Duration = 10 * time.Second - c := &http.Client{Timeout: timeout} - resp, err := c.Get(apiGetLatestRelease) + // declare a client from http.Client and set Timeout to timeout + client := &http.Client{Timeout: timeout} + resp, err := client.Get(apiGetLatestRelease) + // if err unqual to nil mean get error if err != nil { return } + // deffered close defer resp.Body.Close() + // if response is not OK if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("[fetchLatestVersionTag] 返回 %s", resp.Status) } - d := struct { + //anonymous struct + data := struct { TagName string `json:"tag_name"` }{} - if err = json.NewDecoder(resp.Body).Decode(&d); err != nil { + if err = json.NewDecoder(resp.Body).Decode(&data); err != nil { return } - return d.TagName, nil -} - -func CheckNewVersion(currentVersionTag string) { - const latestReleasePage = "https://github.com/EndlessCheng/mahjong-helper/releases/latest" - - latestVersionTag, err := FetchLatestVersionTag() - if err != nil { - // 下次再说~ - return - } - - if latestVersionTag > currentVersionTag { - color.HiGreen("检测到新版本: %s!请前往 %s 下载", latestVersionTag, latestReleasePage) - } + return data.TagName, nil } diff --git a/version_test.go b/version_test.go index 0f26070..6eba8b9 100644 --- a/version_test.go +++ b/version_test.go @@ -7,7 +7,7 @@ import ( ) func Test_checkNewVersion(t *testing.T) { - latestVersionTag, err := FetchLatestVersionTag() + latestVersionTag, err := fetchLatestVersionTag() if err != nil { t.Fatal(err) }