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 @@ + + + + 日本麻将助手 + + + +
+ Waiting +
+ +
+
+ + + + + + +