diff --git a/analysis.go b/analysis.go
index 9e3c178..6633a39 100644
--- a/analysis.go
+++ b/analysis.go
@@ -3,6 +3,7 @@ package main
import (
"github.com/EndlessCheng/mahjong-helper/util"
"fmt"
+ "io"
"strings"
"github.com/fatih/color"
"github.com/EndlessCheng/mahjong-helper/util/model"
@@ -45,7 +46,7 @@ func humanHands(playerInfo *model.PlayerInfo) string {
return humanHands
}
-func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTable) error {
+func analysisPlayerWithRisk(writer io.Writer, playerInfo *model.PlayerInfo, mixedRiskTable riskTable) error {
// 手牌
humanTiles := humanHands(playerInfo)
fmt.Println(humanTiles)
@@ -61,7 +62,7 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab
result13: result,
mixedRiskTable: mixedRiskTable,
}
- r.printWaitsWithImproves13_oneRow()
+ r.printWaitsWithImproves13_oneRow(writer)
case 2:
// 分析手牌
shanten, results14, incShantenResults14 := util.CalculateShantenWithImproves14(playerInfo)
@@ -87,8 +88,8 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab
// TODO: 接近流局时提示河底是哪家
// 何切分析结果
- printResults14WithRisk(results14, mixedRiskTable)
- printResults14WithRisk(incShantenResults14, mixedRiskTable)
+ printResults14WithRisk(writer, results14, mixedRiskTable)
+ printResults14WithRisk(writer, incShantenResults14, mixedRiskTable)
default:
err := fmt.Errorf("参数错误: %d 张牌", countOfTiles)
if debugMode {
@@ -107,7 +108,7 @@ func analysisPlayerWithRisk(playerInfo *model.PlayerInfo, mixedRiskTable riskTab
// isRedFive: 此舍牌是否为赤5
// allowChi: 是否能吃
// mixedRiskTable: 危险度表
-func analysisMeld(playerInfo *model.PlayerInfo, targetTile34 int, isRedFive bool, allowChi bool, mixedRiskTable riskTable) error {
+func analysisMeld(writer io.Writer, playerInfo *model.PlayerInfo, targetTile34 int, isRedFive bool, allowChi bool, mixedRiskTable riskTable) error {
if handsCount := util.CountOfTiles34(playerInfo.HandTiles34); handsCount%3 != 1 {
return fmt.Errorf("手牌错误:%d 张牌 %v", handsCount, playerInfo.HandTiles34)
}
@@ -132,7 +133,7 @@ func analysisMeld(playerInfo *model.PlayerInfo, targetTile34 int, isRedFive bool
result13: result,
mixedRiskTable: mixedRiskTable,
}
- r.printWaitsWithImproves13_oneRow()
+ r.printWaitsWithImproves13_oneRow(writer)
// 提示信息
// TODO: 局收支相近时,提示:局收支相近,追求和率打xx,追求打点打xx
@@ -148,12 +149,12 @@ func analysisMeld(playerInfo *model.PlayerInfo, targetTile34 int, isRedFive bool
// TODO: 接近流局时提示河底是哪家
// 鸣牌何切分析结果
- printResults14WithRisk(results14, mixedRiskTable)
- printResults14WithRisk(incShantenResults14, mixedRiskTable)
+ printResults14WithRisk(writer, results14, mixedRiskTable)
+ printResults14WithRisk(writer, incShantenResults14, mixedRiskTable)
return nil
}
-func analysisHumanTiles(humanTilesInfo *model.HumanTilesInfo) (playerInfo *model.PlayerInfo, err error) {
+func analysisHumanTiles(writer io.Writer, humanTilesInfo *model.HumanTilesInfo) (playerInfo *model.PlayerInfo, err error) {
defer func() {
if er := recover(); er != nil {
err = er.(error)
@@ -231,13 +232,13 @@ func analysisHumanTiles(humanTilesInfo *model.HumanTilesInfo) (playerInfo *model
if er != nil {
return nil, er
}
- if er := analysisMeld(playerInfo, targetTile34, isRedFive, true, nil); er != nil {
+ if er := analysisMeld(writer, playerInfo, targetTile34, isRedFive, true, nil); er != nil {
return nil, er
}
return
}
playerInfo.IsTsumo = humanTilesInfo.IsTsumo
- err = analysisPlayerWithRisk(playerInfo, nil)
+ err = analysisPlayerWithRisk(writer, playerInfo, nil)
return
}
diff --git a/cli.go b/cli.go
index e334dc6..83e3da1 100644
--- a/cli.go
+++ b/cli.go
@@ -7,6 +7,7 @@ import (
"math"
"sort"
"strings"
+ "io"
)
func printAccountInfo(accountID int) {
@@ -464,7 +465,7 @@ type analysisResult struct {
*/
// 打印何切分析结果(单行)
-func (r *analysisResult) printWaitsWithImproves13_oneRow() {
+func (r *analysisResult) printWaitsWithImproves13_oneRow(writer io.Writer) {
discardTile34 := r.discardTile34
openTiles34 := r.openTiles34
result13 := r.result13
@@ -474,19 +475,19 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() {
// 进张数
waitsCount := result13.Waits.AllCount()
c := getWaitsCountColor(shanten, float64(waitsCount))
- color.New(c).Printf("%2d", waitsCount)
+ color.New(c).Fprintf(writer, "%2d", waitsCount)
// 改良进张均值
if len(result13.Improves) > 0 {
if r.highlightAvgImproveWaitsCount {
- color.New(color.FgHiWhite).Printf("[%5.2f]", result13.AvgImproveWaitsCount)
+ color.New(color.FgHiWhite).Fprintf(writer, "[%5.2f]", result13.AvgImproveWaitsCount)
} else {
- fmt.Printf("[%5.2f]", result13.AvgImproveWaitsCount)
+ fmt.Fprintf(writer, "[%5.2f]", result13.AvgImproveWaitsCount)
}
} else {
- fmt.Print(strings.Repeat(" ", 7))
+ fmt.Fprint(writer, strings.Repeat(" ", 7))
}
- fmt.Print(" ")
+ fmt.Fprint(writer, " ")
// 是否为3k+2张牌的何切分析
if discardTile34 != -1 {
@@ -496,14 +497,14 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() {
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)
+ color.New(color.FgHiWhite).Fprintf(writer, "%s%s", string([]rune(util.MahjongZH[openTiles34[0]])[:1]), util.MahjongZH[openTiles34[1]])
+ fmt.Fprintf(writer, "%s,", meldType)
}
// 舍牌
if r.isDiscardTileDora {
- color.New(color.FgHiWhite).Print("ド")
+ color.New(color.FgHiWhite).Fprintf(writer, "ド")
} else {
- fmt.Print("切")
+ fmt.Fprint(writer, "切")
}
tileZH := util.MahjongZH[discardTile34]
if discardTile34 >= 27 {
@@ -513,71 +514,71 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() {
// 若有实际危险度,则根据实际危险度来显示舍牌危险度
risk := r.mixedRiskTable[discardTile34]
if risk == 0 {
- fmt.Print(tileZH)
+ fmt.Fprint(writer, tileZH)
} else {
- color.New(getNumRiskColor(risk)).Print(tileZH)
+ color.New(getNumRiskColor(risk)).Fprint(writer, tileZH)
}
} else {
- fmt.Print(tileZH)
+ fmt.Fprint(writer, tileZH)
}
}
- fmt.Print(" => ")
+ fmt.Fprint(writer, " => ")
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))
+ color.New(c).Fprintf(writer, "%5.2f", result13.AvgNextShantenWaitsCount)
+ fmt.Fprintf(writer, "%s", util.NumberToChineseShanten(incShanten))
if incShanten >= 1 {
- //fmt.Printf("进张")
+ //fmt.Fprintf(writer, "进张")
} else { // incShanten == 0
- fmt.Printf("数")
+ fmt.Fprintf(writer, "数")
//if showAgariAboveShanten1 {
- // fmt.Printf("(%.2f%% 参考和率)", result13.AvgAgariRate)
+ // fmt.Fprintf(writer, "(%.2f%% 参考和率)", result13.AvgAgariRate)
//}
}
} else { // shanten == 0
// 前进后的和率
// 若振听或片听,则标红
if result13.FuritenRate == 1 || result13.IsPartWait {
- color.New(color.FgHiRed).Printf("%5.2f%% 参考和率", result13.AvgAgariRate)
+ color.New(color.FgHiRed).Fprintf(writer, "%5.2f%% 参考和率", result13.AvgAgariRate)
} else {
- fmt.Printf("%5.2f%% 参考和率", result13.AvgAgariRate)
+ fmt.Fprintf(writer, "%5.2f%% 参考和率", result13.AvgAgariRate)
}
}
// 手牌速度,用于快速过庄
if result13.MixedWaitsScore > 0 && shanten >= 1 && shanten <= 2 {
- fmt.Print(" ")
+ fmt.Fprint(writer, " ")
if r.highlightMixedScore {
- color.New(color.FgHiWhite).Printf("[%5.2f速度]", result13.MixedWaitsScore)
+ color.New(color.FgHiWhite).Fprintf(writer, "[%5.2f速度]", result13.MixedWaitsScore)
} else {
- fmt.Printf("[%5.2f速度]", result13.MixedWaitsScore)
+ fmt.Fprintf(writer, "[%5.2f速度]", result13.MixedWaitsScore)
}
}
// 局收支
if showScore && result13.MixedRoundPoint != 0.0 {
- fmt.Print(" ")
- color.New(color.FgHiGreen).Printf("[局收支%4d]", int(math.Round(result13.MixedRoundPoint)))
+ fmt.Fprint(writer, " ")
+ color.New(color.FgHiGreen).Fprintf(writer, "[局收支%4d]", int(math.Round(result13.MixedRoundPoint)))
}
// (默听)荣和点数
if result13.DamaPoint > 0 {
- fmt.Print(" ")
+ fmt.Fprint(writer, " ")
ronType := "荣和"
if !result13.IsNaki {
ronType = "默听"
}
- color.New(color.FgHiGreen).Printf("[%s%d]", ronType, int(math.Round(result13.DamaPoint)))
+ color.New(color.FgHiGreen).Fprintf(writer, "[%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)))
+ fmt.Fprint(writer, " ")
+ color.New(color.FgHiGreen).Fprintf(writer, "[立直%d]", int(math.Round(result13.RiichiPoint)))
}
if len(result13.YakuTypes) > 0 {
@@ -594,64 +595,64 @@ func (r *analysisResult) printWaitsWithImproves13_oneRow() {
}
if len(shownYakuTypes) > 0 {
sort.Ints(shownYakuTypes)
- fmt.Print(" ")
- color.New(color.FgHiGreen).Printf(util.YakuTypesToStr(shownYakuTypes))
+ fmt.Fprint(writer, " ")
+ color.New(color.FgHiGreen).Fprintf(writer, util.YakuTypesToStr(shownYakuTypes))
}
} else {
// debug
- fmt.Print(" ")
- color.New(color.FgHiGreen).Printf(util.YakuTypesWithDoraToStr(result13.YakuTypes, result13.DoraCount))
+ fmt.Fprint(writer, " ")
+ color.New(color.FgHiGreen).Fprintf(writer, util.YakuTypesWithDoraToStr(result13.YakuTypes, result13.DoraCount))
}
// 片听
if result13.IsPartWait {
- fmt.Print(" ")
- color.New(color.FgHiRed).Printf("[片听]")
+ fmt.Fprint(writer, " ")
+ color.New(color.FgHiRed).Fprintf(writer, "[片听]")
}
}
} else if result13.IsNaki && shanten >= 0 && shanten <= 2 {
// 鸣牌时的无役提示(从听牌到两向听)
- fmt.Print(" ")
- color.New(color.FgHiRed).Printf("[无役]")
+ fmt.Fprint(writer, " ")
+ color.New(color.FgHiRed).Fprintf(writer, "[无役]")
}
// 振听提示
if result13.FuritenRate > 0 {
- fmt.Print(" ")
+ fmt.Fprint(writer, " ")
if result13.FuritenRate < 1 {
- color.New(color.FgHiYellow).Printf("[可能振听]")
+ color.New(color.FgHiYellow).Fprintf(writer, "[可能振听]")
} else {
- color.New(color.FgHiRed).Printf("[振听]")
+ color.New(color.FgHiRed).Fprintf(writer, "[振听]")
}
}
// 改良数
if showScore {
- fmt.Print(" ")
+ fmt.Fprint(writer, " ")
if len(result13.Improves) > 0 {
- fmt.Printf("[%2d改良]", len(result13.Improves))
+ fmt.Fprintf(writer, "[%2d改良]", len(result13.Improves))
} else {
- fmt.Print(strings.Repeat(" ", 4))
- fmt.Print(strings.Repeat(" ", 2)) // 全角空格
+ fmt.Fprint(writer, strings.Repeat(" ", 4))
+ fmt.Fprint(writer, strings.Repeat(" ", 2)) // 全角空格
}
}
// 进张类型
- fmt.Print(" ")
+ fmt.Fprint(writer, " ")
waitTiles := result13.Waits.AvailableTiles()
- fmt.Print(util.TilesToStrWithBracket(waitTiles))
+ fmt.Fprint(writer, util.TilesToStrWithBracket(waitTiles))
//
- fmt.Println()
+ fmt.Fprintln(writer)
if showImproveDetail {
for tile, waits := range result13.Improves {
- fmt.Printf("摸 %s 改良成 %s\n", util.Mahjong[tile], waits.String())
+ fmt.Fprintf(writer, "摸 %s 改良成 %s\n", util.Mahjong[tile], waits.String())
}
}
}
-func printResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTable riskTable) {
+func printResults14WithRisk(writer io.Writer, results14 util.Hand14AnalysisResultList, mixedRiskTable riskTable) {
if len(results14) == 0 {
return
}
@@ -705,6 +706,6 @@ func printResults14WithRisk(results14 util.Hand14AnalysisResultList, mixedRiskTa
result.Result13.AvgImproveWaitsCount == maxAvgImproveWaitsCount,
result.Result13.MixedWaitsScore == maxMixedScore,
}
- r.printWaitsWithImproves13_oneRow()
+ r.printWaitsWithImproves13_oneRow(writer)
}
}
diff --git a/core.go b/core.go
index 3ec5f60..f25bd82 100644
--- a/core.go
+++ b/core.go
@@ -2,6 +2,7 @@ package main
import (
"fmt"
+ "github.com/EndlessCheng/mahjong-helper/webapi"
"github.com/EndlessCheng/mahjong-helper/util"
"github.com/EndlessCheng/mahjong-helper/util/model"
"github.com/fatih/color"
@@ -167,6 +168,8 @@ func (p *playerInfo) doraNum(doraList []int) (doraCount int) {
//
type roundData struct {
+ ApiData webapi.ApiData
+
parser DataParser
gameMode gameMode
@@ -470,6 +473,14 @@ func (d *roundData) newModelPlayerInfo() *model.PlayerInfo {
}
func (d *roundData) analysis() error {
+ d.ApiData.Init()
+ writer := webapi.ApiDataConvertor{&d.ApiData}
+
+ defer func() {
+ d.ApiData.GetOutput()
+ d.ApiData.Counts = d.counts
+ }()
+
if !debugMode {
defer func() {
if err := recover(); err != nil {
@@ -582,7 +593,7 @@ func (d *roundData) analysis() error {
color.HiYellow("宝牌指示牌是 " + info)
fmt.Println()
// TODO: 显示地和概率
- return analysisPlayerWithRisk(playerInfo, nil)
+ return analysisPlayerWithRisk(writer, playerInfo, nil)
case d.parser.IsOpen():
// 某家鸣牌(含暗杠、加杠)
who, meld, kanDoraIndicator := d.parser.ParseOpen()
@@ -768,7 +779,7 @@ func (d *roundData) analysis() error {
// 打印何切推荐
// TODO: 根据是否听牌/一向听、打点、巡目、和率等进行攻守判断
- return analysisPlayerWithRisk(playerInfo, mixedRiskTable)
+ return analysisPlayerWithRisk(writer, playerInfo, mixedRiskTable)
case d.parser.IsDiscard():
who, discardTile, isRedFive, isTsumogiri, isReach, canBeMeld, kanDoraIndicator := d.parser.ParseDiscard()
@@ -854,6 +865,7 @@ func (d *roundData) analysis() error {
// 安全度分析
riskTables := d.analysisTilesRisk()
mixedRiskTable := riskTables.mixedRiskTable()
+ d.ApiData.RiskTable = mixedRiskTable
// 牌谱分析模式下,记录可能的鸣牌
if d.gameMode == gameModeRecordCache {
@@ -905,7 +917,7 @@ func (d *roundData) analysis() error {
// 为了方便解析牌谱,这里尽可能地解析副露
// TODO: 提醒: 消除海底/避免河底
allowChi := d.playerNumber != 3 && who == 3 && playerInfo.LeftDrawTilesCount > 0
- return analysisMeld(playerInfo, discardTile, isRedFive, allowChi, mixedRiskTable)
+ return analysisMeld(writer, playerInfo, discardTile, isRedFive, allowChi, mixedRiskTable)
case d.parser.IsRoundWin():
// TODO: 解析天凤牌谱 - 注意 skipOutput
diff --git a/interact.go b/interact.go
index 012188c..f89de00 100644
--- a/interact.go
+++ b/interact.go
@@ -16,7 +16,7 @@ func interact(humanTilesInfo *model.HumanTilesInfo) error {
}()
}
- playerInfo, err := analysisHumanTiles(humanTilesInfo)
+ playerInfo, err := analysisHumanTiles(os.Stdout, humanTilesInfo)
if err != nil {
return err
}
@@ -67,7 +67,7 @@ func interact(humanTilesInfo *model.HumanTilesInfo) error {
tiles34[tile]--
playerInfo.DiscardTiles = append(playerInfo.DiscardTiles, tile) // 仅判断振听用
}
- if err := analysisPlayerWithRisk(playerInfo, nil); err != nil {
+ if err := analysisPlayerWithRisk(os.Stdout, playerInfo, nil); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
diff --git a/main.go b/main.go
index 548a055..cc03de2 100644
--- a/main.go
+++ b/main.go
@@ -9,6 +9,7 @@ import (
"math/rand"
"time"
"flag"
+ "os"
)
var (
@@ -131,7 +132,7 @@ func main() {
case isInteractive: // 交互模式
err = interact(humanTilesInfo)
case len(flag.Args()) > 0: // 静态分析
- _, err = analysisHumanTiles(humanTilesInfo)
+ _, err = analysisHumanTiles(os.Stdout, humanTilesInfo)
default: // 服务器模式
choose := welcome()
isHTTPS := choose == platformMajsoul
diff --git a/server.go b/server.go
index 432d113..59dc989 100644
--- a/server.go
+++ b/server.go
@@ -74,6 +74,17 @@ func (h *mjHandler) index(c echo.Context) error {
return c.String(http.StatusOK, time.Now().Format("2006-01-02 15:04:05"))
}
+// 以 JSON 格式返回游戏相关信息
+func (h *mjHandler) apiGetStatus(c echo.Context) error {
+ data := h.tenhouRoundData.ApiData
+ b, err := json.Marshal(data)
+ if err != nil {
+ h.logError(err)
+ return c.String(http.StatusBadRequest, err.Error())
+ }
+ return c.String(http.StatusOK, string(b[:]))
+}
+
// 打一摸一分析器
func (h *mjHandler) analysis(c echo.Context) error {
if h.analysing {
@@ -92,7 +103,7 @@ func (h *mjHandler) analysis(c echo.Context) error {
return c.String(http.StatusBadRequest, err.Error())
}
- if _, err := analysisHumanTiles(model.NewSimpleHumanTilesInfo(d.Tiles)); err != nil {
+ if _, err := analysisHumanTiles(os.Stdout, model.NewSimpleHumanTilesInfo(d.Tiles)); err != nil {
fmt.Println(err)
return c.String(http.StatusBadRequest, err.Error())
}
@@ -521,11 +532,14 @@ func runServer(isHTTPS bool, port int) (err error) {
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.GET("/", h.index)
+ e.GET("/api", h.apiGetStatus)
e.POST("/debug", h.index)
e.POST("/analysis", h.analysis)
e.POST("/tenhou", h.analysisTenhou)
e.POST("/majsoul", h.analysisMajsoul)
+ e.Static("/web", "webapi")
+
// code.js 也用的该端口
if port == 0 {
port = defaultPort
diff --git a/webapi/core.js b/webapi/core.js
new file mode 100644
index 0000000..f41bc7f
--- /dev/null
+++ b/webapi/core.js
@@ -0,0 +1,218 @@
+"use strict";
+
+let maj_risk = (function() {
+
+// https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
+
+/**
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes h, s, and l are contained in the set [0, 1] and
+ * returns r, g, and b in the set [0, 255].
+ *
+ * @param {number} h The hue
+ * @param {number} s The saturation
+ * @param {number} l The lightness
+ * @return {Array} The RGB representation
+ */
+function hslToRgb(h, s, l){
+ var r, g, b;
+
+ if(s == 0){
+ r = g = b = l; // achromatic
+ }else{
+ var hue2rgb = function hue2rgb(p, q, t){
+ if(t < 0) t += 1;
+ if(t > 1) t -= 1;
+ if(t < 1/6) return p + (q - p) * 6 * t;
+ if(t < 1/2) return q;
+ if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+ return p;
+ }
+
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1/3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1/3);
+ }
+
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
+}
+
+/**
+ * Converts an RGB color value to HSL. Conversion formula
+ * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes r, g, and b are contained in the set [0, 255] and
+ * returns h, s, and l in the set [0, 1].
+ *
+ * @param {number} r The red color value
+ * @param {number} g The green color value
+ * @param {number} b The blue color value
+ * @return {Array} The HSL representation
+ */
+function rgbToHsl(r, g, b){
+ r /= 255, g /= 255, b /= 255;
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
+ var h, s, l = (max + min) / 2;
+
+ if(max == min){
+ h = s = 0; // achromatic
+ }else{
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch(max){
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+
+ return [h, s, l];
+}
+
+let gradient = [
+ { risk: 0.0 , color: [255, 255, 255] }, // 绝安,白色
+ { risk: 1e-5 , color: [131, 179, 17] }, // 只要有一点风险,鸭绿色
+ { risk: 8 , color: [250, 195, 0] }, // 还可以冲一冲, 橙黄色
+ { risk: 16 , color: [253, 99, 0] }, // 相当危险,粉色
+ { risk: 25.0, color: [253, 0, 135] }, // 绝对危险, 红色
+];
+
+// https://stackoverflow.com/questions/4856717/javascript-equivalent-of-pythons-zip-function
+let zip = rows=>rows[0].map((_,c)=>rows.map(row=>row[c]));
+
+return {
+ get_rist_color(risk) {
+ risk = Math.min(risk, gradient[gradient.length-1].risk);
+
+ for (let i = 0; i + 1 < gradient.length; i++) {
+ if (risk <= gradient[i + 1].risk) {
+ // let left = rgbToHsl.apply(null, gradient[i].color);
+ // let right = rgbToHsl.apply(null, gradient[i + 1].color);
+ let left = gradient[i].color;
+ let right = gradient[i+1].color;
+ // interpolation in RGB color space
+ // HSL 效果似乎并不如人意
+ let ratio = (risk - gradient[i].risk) / (gradient[i + 1].risk - gradient[i].risk);
+ let result = zip([left, right]).map(([x, y]) => x + (y - x) * ratio);
+
+ // let rgba = hslToRgb.apply(null, (result)).join(',');
+ let rgba = result;
+ return `rgba(${rgba})`;
+ }
+ }
+
+ throw "can not find a color for the risk " + risk;
+ }
+};
+
+})(); // closure for maj_risk
+
+function tile_img_url(index) {
+ function label(index) {
+ let offset = [0, 9, 18, 27, 34];
+ let l = ["m", "p", "s", "z"];
+
+ for (let i = 0; i < l.length; i++) {
+ if (index < offset[i+1]) {
+ return (index - offset[i] + 1) + l[i];
+ }
+ }
+ throw new Error("invalid index");
+ }
+
+ let l = label(index);
+ return `img/${l}.png`;
+}
+
+function show_tiles(data) {
+ if (data["counts"] === null) {
+ return;
+ }
+
+ let container = document.getElementById("my-tiles");
+ container.innerHTML = '';
+
+ for (let i = 0; i < 34; i++) {
+ var risk = data["risk"] === null ? 0 : data["risk"][i];
+
+ let count = data["counts"][i];
+ for (let c = 0; c < count; c++) {
+ let img = document.createElement("img");
+ img.src = tile_img_url(i);
+ img.classList.add("tile");
+ img.style.border = `solid 3px ${maj_risk.get_rist_color(risk)}`;
+ container.appendChild(img);
+ }
+ }
+}
+
+function inline_img(line) {
+ function img_tag(index) {
+ return ``;
+ }
+
+ let z_tiles = "东南西北白发中";
+
+ let offset = {
+ 'm': 0,
+ 'p': 9,
+ 's': 18,
+ 'z': 27,
+ '万': 0,
+ '饼': 9,
+ '索': 18,
+ };
+
+ var keys = "";
+ for (const [key, _] of Object.entries(offset)) {
+ keys = keys + key;
+ }
+
+ let reg = new RegExp(`(\\d+)([${keys}])| ([${z_tiles}])`, 'g');
+
+ return line.replace(reg, function(_, s, t, z) {
+ if (z) {
+ return img_tag(27 + z_tiles.indexOf(z));
+ }
+
+ var result = "";
+ for (var i = 0; i < s.length; i++) {
+ result += img_tag(parseInt(s[i]) - 1 + offset[t]);
+ }
+ return result;
+ });
+}
+
+function show_outputs(outputs) {
+ let lines = outputs.replace(/\u001b\[\d*m/g, '').split('\n')
+ let result = lines.map(function(line) {
+ return `
${ inline_img(line) }
`; + }).join(''); + document.getElementById("outputs").innerHTML = result; +} + +// show_outputs("\u001b[96m39\u001b[0m\u001b[97m[47.04]\u001b[0m 切 南 => \u001b[96m22.95\u001b[0m两向听 [12345789m 7p 8s 37z]\n\u001b[96m39\u001b[0m\u001b[97m[47.04]\u001b[0m 切 西 => \u001b[96m22.95\u001b[0m两向听 [12345789m 7p 8s 27z]\n\u001b[96m31\u001b[0m[40.21] 切9万 => \u001b[96m20.10\u001b[0m两向听 [12345m 7p 8s 237z]\n\u001b[96m39\u001b[0m\u001b[97m[47.04]\u001b[0m 切 中 => \u001b[96m22.95\u001b[0m两向听 [12345789m 7p 8s 23z]\n\u001b[96m33\u001b[0m[39.99] 切3万 => \u001b[96m18.33\u001b[0m两向听 [14789m 7p 8s 237z]\n\u001b[96m28\u001b[0m[37.66] 切2万 => \u001b[96m14.00\u001b[0m两向听 [3789m 7p 8s 237z]\n\u001b[96m65\u001b[0m\u001b[97m[68.44]\u001b[0m 切9索 => \u001b[96m45.00\u001b[0m三向听 [12345789m 2347p 56789s 237z]\n\u001b[96m58\u001b[0m[63.13] 切7饼 => \u001b[96m44.83\u001b[0m三向听 [12345789m 56789p 8s 237z]\n\u001b[96m57\u001b[0m[61.50] 切7索 => \u001b[96m39.95\u001b[0m三向听 [12345789m 2347p 789s 237z]\n\u001b[96m61\u001b[0m[64.97] 切4饼 => \u001b[96m34.84\u001b[0m三向听 [12345789m 12347p 789s 237z]\n\u001b[96m61\u001b[0m[64.44] 切2饼 => \u001b[96m34.84\u001b[0m三向听 [12345789m 23457p 789s 237z]\n\u001b[96m57\u001b[0m[61.50] 切3饼 => \u001b[96m31.60\u001b[0m三向听 [12345789m 2347p 789s 237z]\n"); + +let interval = 1000; +var backoff = interval; + +function update_tile() { + $.getJSON("/api") + .done(function(data) { + show_tiles(data); + show_outputs(data["outputs"]); + backoff = interval; + setTimeout(update_tile, interval); + }) + .fail(function( jqxhr, textStatus, error ) { + var err = textStatus + ", " + error; + console.log("Request Failed: " + err); + backoff = backoff * 2; + setTimeout(update_tile, backoff); + }); +} + +setTimeout(update_tile, interval); diff --git a/webapi/data.go b/webapi/data.go new file mode 100644 index 0000000..aa2c24f --- /dev/null +++ b/webapi/data.go @@ -0,0 +1,51 @@ +package webapi + +import ( + "os" + "io" + "bytes" +) + +type ApiData struct { + // 数据更新时间戳 + Timestamp int `json:"timestamp"` + + // 自家手牌 一个长度为 34 的整数数组 + Counts []int `json:"counts"` + + // 手牌危险度 一个长度为 34 的浮点数组 + RiskTable []float64 `json:"risk"` + + // 显示终端结果 + Outputs string `json:"outputs"` + + output_buffer bytes.Buffer +} + +func (data *ApiData) Init() { + data.output_buffer.Reset() +} + +func (data *ApiData) GetOutput() { + s := data.output_buffer.String() + if len(s) > 0 { + data.Outputs = s + } +} + +// implement the io.Writer interface +var _ io.Writer = (*ApiDataConvertor)(nil) +type ApiDataConvertor struct { + *ApiData +} + +func (writer ApiDataConvertor) Write(p []byte) (n int, err error) { + n, e := writer.output_buffer.Write(p) + if e != nil { + return n, e + } + return os.Stdout.Write(p) +} + + + diff --git a/webapi/img/0m.png b/webapi/img/0m.png new file mode 100755 index 0000000..3a28ee4 Binary files /dev/null and b/webapi/img/0m.png differ diff --git a/webapi/img/0p.png b/webapi/img/0p.png new file mode 100755 index 0000000..d5a0476 Binary files /dev/null and b/webapi/img/0p.png differ diff --git a/webapi/img/0s.png b/webapi/img/0s.png new file mode 100755 index 0000000..6739013 Binary files /dev/null and b/webapi/img/0s.png differ diff --git a/webapi/img/1m.png b/webapi/img/1m.png new file mode 100755 index 0000000..6512d8f Binary files /dev/null and b/webapi/img/1m.png differ diff --git a/webapi/img/1p.png b/webapi/img/1p.png new file mode 100755 index 0000000..783ca19 Binary files /dev/null and b/webapi/img/1p.png differ diff --git a/webapi/img/1s.png b/webapi/img/1s.png new file mode 100755 index 0000000..831f984 Binary files /dev/null and b/webapi/img/1s.png differ diff --git a/webapi/img/1z.png b/webapi/img/1z.png new file mode 100755 index 0000000..2e852b9 Binary files /dev/null and b/webapi/img/1z.png differ diff --git a/webapi/img/2m.png b/webapi/img/2m.png new file mode 100755 index 0000000..45002bd Binary files /dev/null and b/webapi/img/2m.png differ diff --git a/webapi/img/2p.png b/webapi/img/2p.png new file mode 100755 index 0000000..22d9612 Binary files /dev/null and b/webapi/img/2p.png differ diff --git a/webapi/img/2s.png b/webapi/img/2s.png new file mode 100755 index 0000000..f0c679d Binary files /dev/null and b/webapi/img/2s.png differ diff --git a/webapi/img/2z.png b/webapi/img/2z.png new file mode 100755 index 0000000..85b0e71 Binary files /dev/null and b/webapi/img/2z.png differ diff --git a/webapi/img/3m.png b/webapi/img/3m.png new file mode 100755 index 0000000..4851f24 Binary files /dev/null and b/webapi/img/3m.png differ diff --git a/webapi/img/3p.png b/webapi/img/3p.png new file mode 100755 index 0000000..d977dbd Binary files /dev/null and b/webapi/img/3p.png differ diff --git a/webapi/img/3s.png b/webapi/img/3s.png new file mode 100755 index 0000000..4ec0ff7 Binary files /dev/null and b/webapi/img/3s.png differ diff --git a/webapi/img/3z.png b/webapi/img/3z.png new file mode 100755 index 0000000..38b486e Binary files /dev/null and b/webapi/img/3z.png differ diff --git a/webapi/img/4m.png b/webapi/img/4m.png new file mode 100755 index 0000000..7de24a7 Binary files /dev/null and b/webapi/img/4m.png differ diff --git a/webapi/img/4p.png b/webapi/img/4p.png new file mode 100755 index 0000000..8c55a3e Binary files /dev/null and b/webapi/img/4p.png differ diff --git a/webapi/img/4s.png b/webapi/img/4s.png new file mode 100755 index 0000000..a6c5395 Binary files /dev/null and b/webapi/img/4s.png differ diff --git a/webapi/img/4z.png b/webapi/img/4z.png new file mode 100755 index 0000000..c21b667 Binary files /dev/null and b/webapi/img/4z.png differ diff --git a/webapi/img/5m.png b/webapi/img/5m.png new file mode 100755 index 0000000..207179d Binary files /dev/null and b/webapi/img/5m.png differ diff --git a/webapi/img/5p.png b/webapi/img/5p.png new file mode 100755 index 0000000..e5ad873 Binary files /dev/null and b/webapi/img/5p.png differ diff --git a/webapi/img/5s.png b/webapi/img/5s.png new file mode 100755 index 0000000..f0ba7bf Binary files /dev/null and b/webapi/img/5s.png differ diff --git a/webapi/img/5z.png b/webapi/img/5z.png new file mode 100755 index 0000000..5a2234b Binary files /dev/null and b/webapi/img/5z.png differ diff --git a/webapi/img/6m.png b/webapi/img/6m.png new file mode 100755 index 0000000..7f0c830 Binary files /dev/null and b/webapi/img/6m.png differ diff --git a/webapi/img/6p.png b/webapi/img/6p.png new file mode 100755 index 0000000..9d20f32 Binary files /dev/null and b/webapi/img/6p.png differ diff --git a/webapi/img/6s.png b/webapi/img/6s.png new file mode 100755 index 0000000..e3a01ee Binary files /dev/null and b/webapi/img/6s.png differ diff --git a/webapi/img/6z.png b/webapi/img/6z.png new file mode 100755 index 0000000..37ff7dd Binary files /dev/null and b/webapi/img/6z.png differ diff --git a/webapi/img/7m.png b/webapi/img/7m.png new file mode 100755 index 0000000..17f6b94 Binary files /dev/null and b/webapi/img/7m.png differ diff --git a/webapi/img/7p.png b/webapi/img/7p.png new file mode 100755 index 0000000..108bef0 Binary files /dev/null and b/webapi/img/7p.png differ diff --git a/webapi/img/7s.png b/webapi/img/7s.png new file mode 100755 index 0000000..21fae0e Binary files /dev/null and b/webapi/img/7s.png differ diff --git a/webapi/img/7z.png b/webapi/img/7z.png new file mode 100755 index 0000000..1909906 Binary files /dev/null and b/webapi/img/7z.png differ diff --git a/webapi/img/8m.png b/webapi/img/8m.png new file mode 100755 index 0000000..7c5087b Binary files /dev/null and b/webapi/img/8m.png differ diff --git a/webapi/img/8p.png b/webapi/img/8p.png new file mode 100755 index 0000000..11dc85b Binary files /dev/null and b/webapi/img/8p.png differ diff --git a/webapi/img/8s.png b/webapi/img/8s.png new file mode 100755 index 0000000..49d6c95 Binary files /dev/null and b/webapi/img/8s.png differ diff --git a/webapi/img/9m.png b/webapi/img/9m.png new file mode 100755 index 0000000..dd24e3a Binary files /dev/null and b/webapi/img/9m.png differ diff --git a/webapi/img/9p.png b/webapi/img/9p.png new file mode 100755 index 0000000..8ba09b6 Binary files /dev/null and b/webapi/img/9p.png differ diff --git a/webapi/img/9s.png b/webapi/img/9s.png new file mode 100755 index 0000000..9307540 Binary files /dev/null and b/webapi/img/9s.png differ diff --git a/webapi/index.html b/webapi/index.html new file mode 100644 index 0000000..2b0d181 --- /dev/null +++ b/webapi/index.html @@ -0,0 +1,28 @@ + + + +