dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

commit 491df5e2ebcee425110fee9b04dfbb47c2338367
parent f1f1e712778c710085ac3e5ad1bd5c35b9edcea4
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Wed, 14 Jun 2023 06:07:13 -0700

add chess game graph stats

Diffstat:
Acmd/dkf/migrations/143.sql | 4++++
Mpkg/database/tableChessGames.go | 1+
Mpkg/web/handlers/chess.go | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mpkg/web/handlers/interceptors/chess.go | 95++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 189 insertions(+), 12 deletions(-)

diff --git a/cmd/dkf/migrations/143.sql b/cmd/dkf/migrations/143.sql @@ -0,0 +1,4 @@ +-- +migrate Up +ALTER TABLE chess_games ADD COLUMN stats BLOB NULL; + +-- +migrate Down diff --git a/pkg/database/tableChessGames.go b/pkg/database/tableChessGames.go @@ -14,6 +14,7 @@ type ChessGame struct { Outcome string AccuracyWhite float64 AccuracyBlack float64 + Stats []byte CreatedAt time.Time UpdatedAt time.Time } diff --git a/pkg/web/handlers/chess.go b/pkg/web/handlers/chess.go @@ -15,6 +15,7 @@ import ( "github.com/notnil/chess" "github.com/sirupsen/logrus" "html/template" + "math" "net/http" "strings" "time" @@ -91,13 +92,100 @@ var cssReset = `<style> </style>` func ChessAnalyseHandler(c echo.Context) error { - pgn := `1. d4 Nf6 2. Nf3 e6 3. Bf4 { A46 Indian Defense: London System } c5 4. e3 d5 5. c3 Bd6 6. Bg3 O-O 7. Bd3 Qc7 8. Bxd6 Qxd6 9. O-O Nbd7 10. Nbd2 e5 11. dxe5 Nxe5 12. Nxe5 Qxe5 13. Nf3 Qe7 14. b3 Rd8 15. Rc1 Bg4 16. Be2 Bf5 17. Bd3 Ne4 18. Qc2 Rd6 19. Rfd1 Re8 20. Bb5?! { (-0.28 → -0.96) Inaccuracy. Bxe4 was best. } (20. Bxe4) 20... Rc8 21. Bd3 Rcd8 22. Nd2 h6 23. Nxe4 dxe4 24. Bc4 b6 25. Rxd6 Qxd6 26. h3 a5 27. Qe2 Qd2 28. Qxd2 Rxd2 29. a4?! { (-0.06 → -0.70) Inaccuracy. b4 was best. } (29. b4 Be6 30. Bxe6 fxe6 31. bxc5 bxc5 32. Rb1 Rxa2 33. Rb5 a4 34. Rxc5 Kf7 35. Rc6 Ke7) 29... Be6?! { (-0.70 → -0.13) Inaccuracy. Kf8 was best. } (29... Kf8 30. b4 Ke7 31. bxc5 bxc5 32. Rb1 Be6 33. Bxe6 Kxe6 34. g4 Rc2 35. Rb6+ Kd5 36. Rb5) 30. Bxe6 fxe6 31. Rb1 Kf7 32. Kf1 Rc2 33. c4 h5 34. g4 hxg4 35. hxg4 Kf6 36. Kg2 Ke5 37. Kg3 Rd2 38. f3?? { (0.00 → -5.42) Blunder. Kg2 was best. } (38. Kg2 Kf6 39. Kg3 Kg5 40. Rh1 Rb2 41. Rh8 Rxb3 42. Re8 Rb4 43. Rxe6 Rxa4 44. Rxb6 Rxc4) 38... Re2 39. f4+ Kf6 40. g5+ Kf5 41. Rh1 Rxe3+ 42. Kh4 Rxb3 43. g6 Kxf4 44. Kh5?! { (-12.25 → Mate in 15) Checkmate is now unavoidable. Rg1 was best. } (44. Rg1 e3 45. Rg4+ Kf3 46. Rg3+ Ke2 47. Kg4 Kd2 48. Kf4 Rd3 49. Ke4 Rd4+ 50. Ke5 e2) 44... Ra3?! { (Mate in 15 → -55.05) Lost forced checkmate sequence. Rf3 was best. } (44... Rf3 45. Rh4+ Kf5 46. Rh2 e3 47. Kh4 Rf2 48. Rh1 Kf4 49. Kh3 e2 50. Re1 Kf3 51. Kh4) 45. Rf1+?! { (-55.05 → Mate in 11) Checkmate is now unavoidable. Kh4 was best. } (45. Kh4 Rf3 46. Rg1 e3 47. Rg4+ Kf5 48. Rg2 Ke4 49. Ra2 Kf4 50. Rg2 Rf2 51. Rg4+ Kf5) 45... Rf3 46. Rxf3+ exf3 47. Kh4 f2 48. Kh3 Kf3 49. Kh2 f1=R 50. Kh3` //Rh1# { Black wins by checkmate. } 0-1` - res, err := interceptors.AnalyseGame(pgn, 60) - if err != nil { - return c.String(http.StatusOK, err.Error()) + //pgn := `1. d4 Nf6 2. Nf3 e6 3. Bf4 { A46 Indian Defense: London System } c5 4. e3 d5 5. c3 Bd6 6. Bg3 O-O 7. Bd3 Qc7 8. Bxd6 Qxd6 9. O-O Nbd7 10. Nbd2 e5 11. dxe5 Nxe5 12. Nxe5 Qxe5 13. Nf3 Qe7 14. b3 Rd8 15. Rc1 Bg4 16. Be2 Bf5 17. Bd3 Ne4 18. Qc2 Rd6 19. Rfd1 Re8 20. Bb5?! { (-0.28 → -0.96) Inaccuracy. Bxe4 was best. } (20. Bxe4) 20... Rc8 21. Bd3 Rcd8 22. Nd2 h6 23. Nxe4 dxe4 24. Bc4 b6 25. Rxd6 Qxd6 26. h3 a5 27. Qe2 Qd2 28. Qxd2 Rxd2 29. a4?! { (-0.06 → -0.70) Inaccuracy. b4 was best. } (29. b4 Be6 30. Bxe6 fxe6 31. bxc5 bxc5 32. Rb1 Rxa2 33. Rb5 a4 34. Rxc5 Kf7 35. Rc6 Ke7) 29... Be6?! { (-0.70 → -0.13) Inaccuracy. Kf8 was best. } (29... Kf8 30. b4 Ke7 31. bxc5 bxc5 32. Rb1 Be6 33. Bxe6 Kxe6 34. g4 Rc2 35. Rb6+ Kd5 36. Rb5) 30. Bxe6 fxe6 31. Rb1 Kf7 32. Kf1 Rc2 33. c4 h5 34. g4 hxg4 35. hxg4 Kf6 36. Kg2 Ke5 37. Kg3 Rd2 38. f3?? { (0.00 → -5.42) Blunder. Kg2 was best. } (38. Kg2 Kf6 39. Kg3 Kg5 40. Rh1 Rb2 41. Rh8 Rxb3 42. Re8 Rb4 43. Rxe6 Rxa4 44. Rxb6 Rxc4) 38... Re2 39. f4+ Kf6 40. g5+ Kf5 41. Rh1 Rxe3+ 42. Kh4 Rxb3 43. g6 Kxf4 44. Kh5?! { (-12.25 → Mate in 15) Checkmate is now unavoidable. Rg1 was best. } (44. Rg1 e3 45. Rg4+ Kf3 46. Rg3+ Ke2 47. Kg4 Kd2 48. Kf4 Rd3 49. Ke4 Rd4+ 50. Ke5 e2) 44... Ra3?! { (Mate in 15 → -55.05) Lost forced checkmate sequence. Rf3 was best. } (44... Rf3 45. Rh4+ Kf5 46. Rh2 e3 47. Kh4 Rf2 48. Rh1 Kf4 49. Kh3 e2 50. Re1 Kf3 51. Kh4) 45. Rf1+?! { (-55.05 → Mate in 11) Checkmate is now unavoidable. Kh4 was best. } (45. Kh4 Rf3 46. Rg1 e3 47. Rg4+ Kf5 48. Rg2 Ke4 49. Ra2 Kf4 50. Rg2 Rf2 51. Rg4+ Kf5) 45... Rf3 46. Rxf3+ exf3 47. Kh4 f2 48. Kh3 Kf3 49. Kh2 f1=R 50. Kh3` //Rh1# { Black wins by checkmate. } 0-1` + //res, err := interceptors.AnalyseGame(pgn, 15) + //if err != nil { + // return c.String(http.StatusOK, err.Error()) + //} + //by, _ := json.Marshal(res) + res := `{"WhiteAccuracy":91.33574827591818,"BlackAccuracy":98.17676103042548,"Scores":[{"CP":46,"Mate":0},{"CP":35,"Mate":0},{"CP":46,"Mate":0},{"CP":26,"Mate":0},{"CP":9,"Mate":0},{"CP":21,"Mate":0},{"CP":31,"Mate":0},{"CP":11,"Mate":0},{"CP":13,"Mate":0},{"CP":7,"Mate":0},{"CP":0,"Mate":0},{"CP":0,"Mate":0},{"CP":0,"Mate":0},{"CP":-12,"Mate":0},{"CP":-8,"Mate":0},{"CP":-19,"Mate":0},{"CP":0,"Mate":0},{"CP":-11,"Mate":0},{"CP":-7,"Mate":0},{"CP":-6,"Mate":0},{"CP":-19,"Mate":0},{"CP":-14,"Mate":0},{"CP":-17,"Mate":0},{"CP":-15,"Mate":0},{"CP":-11,"Mate":0},{"CP":-23,"Mate":0},{"CP":-57,"Mate":0},{"CP":-41,"Mate":0},{"CP":-79,"Mate":0},{"CP":-80,"Mate":0},{"CP":-92,"Mate":0},{"CP":-54,"Mate":0},{"CP":-100,"Mate":0},{"CP":-77,"Mate":0},{"CP":-71,"Mate":0},{"CP":-40,"Mate":0},{"CP":-39,"Mate":0},{"CP":-78,"Mate":0},{"CP":-124,"Mate":0},{"CP":-68,"Mate":0},{"CP":-63,"Mate":0},{"CP":-50,"Mate":0},{"CP":-44,"Mate":0},{"CP":-11,"Mate":0},{"CP":-13,"Mate":0},{"CP":-11,"Mate":0},{"CP":-13,"Mate":0},{"CP":0,"Mate":0},{"CP":-9,"Mate":0},{"CP":-2,"Mate":0},{"CP":0,"Mate":0},{"CP":-19,"Mate":0},{"CP":-26,"Mate":0},{"CP":-40,"Mate":0},{"CP":-43,"Mate":0},{"CP":-40,"Mate":0},{"CP":-96,"Mate":0},{"CP":-66,"Mate":0},{"CP":-35,"Mate":0},{"CP":-52,"Mate":0},{"CP":-43,"Mate":0},{"CP":-68,"Mate":0},{"CP":-65,"Mate":0},{"CP":-58,"Mate":0},{"CP":-60,"Mate":0},{"CP":-33,"Mate":0},{"CP":-17,"Mate":0},{"CP":-18,"Mate":0},{"CP":-25,"Mate":0},{"CP":-12,"Mate":0},{"CP":-10,"Mate":0},{"CP":-25,"Mate":0},{"CP":-4,"Mate":0},{"CP":-14,"Mate":0},{"CP":-460,"Mate":0},{"CP":-510,"Mate":0},{"CP":-600,"Mate":0},{"CP":-603,"Mate":0},{"CP":-818,"Mate":0},{"CP":-826,"Mate":0},{"CP":-815,"Mate":0},{"CP":-804,"Mate":0},{"CP":-959,"Mate":0},{"CP":-946,"Mate":0},{"CP":-1009,"Mate":0},{"CP":-984,"Mate":0},{"CP":-1101,"Mate":0},{"CP":-1059,"Mate":0},{"CP":0,"Mate":12},{"CP":0,"Mate":10},{"CP":0,"Mate":6},{"CP":0,"Mate":5},{"CP":0,"Mate":5},{"CP":0,"Mate":4},{"CP":0,"Mate":4},{"CP":0,"Mate":3},{"CP":0,"Mate":2},{"CP":0,"Mate":1},{"CP":0,"Mate":1}]}` + var r interceptors.AnalyseResult + _ = json.Unmarshal([]byte(res), &r) + + htmlTmpl := cssReset + htmlTmpl += ` +<style> +html, body { background-color: #222; } +.graph { + border: 3px solid #000; + background-color: #666; +} +.graph .first-row { height: 120px; } +.graph .second-row { height: 120px; } +.graph td { + border-right: 0px solid #555; +} +.graph .first-row td { + vertical-align: bottom; +} +.graph .second-row td { + vertical-align: top; +} +.graph .column { + width: {{ .ColumnWidth }}px; + box-sizing: border-box; + border-right: 1px solid #555; +} +</style> +<div style="margin: 20px"> + <table class="graph"> + <tr class="first-row"> + {{ range $idx, $el := .Data.Scores }} + <td title="Move: {{ $idx }} | Advantage: {{ if not $el.Mate }}{{ $el.CP | cp }}{{ else }}#{{ $el.Mate }}{{ end }}"> + {{ if ge .CP 0 }} + <div class="column" style="height: {{ $el | renderCP "white" }}px; background-color: #eee;"></div> + {{ end }} + </td> + {{ end }} + </tr> + <tr class="second-row"> + {{ range $idx, $el := .Data.Scores }} + <td title="Move: {{ $idx }} | Advantage: {{ if not $el.Mate }}{{ $el.CP | cp }}{{ else }}#{{ $el.Mate }}{{ end }}"> + {{ if le .CP 0 }} + <div class="column" style="height: {{ $el | renderCP "black" }}px; background-color: #111;"></div> + {{ end }} + </td> + {{ end }} + </tr> + </table> +</div>` + + const graphWidth = 800 + + columnWidth := graphWidth / len(r.Scores) + + data := map[string]any{ + "Data": r, + "ColumnWidth": columnWidth, + } + + fns := template.FuncMap{ + "css": func(s string) template.CSS { return template.CSS(s) }, + "abs": func(v int) int { return int(math.Abs(float64(v))) }, + "cp": func(v int) string { + return fmt.Sprintf("%.2f", float64(v)/100) + }, + "renderCP": func(color string, v interceptors.Score) int { + const maxH = 120 // Max graph height + const maxV = 1200 // Max cp value. Anything bigger should take 100% of graph height space. + absV := int(math.Abs(float64(v.CP))) + absV = utils.MinInt(absV, maxV) + absV = absV * maxH / maxV + if v.CP == 0 && v.Mate != 0 { + if (color == "white" && v.Mate < 0) || (color == "black" && v.Mate > 0) { + absV = maxH + } + } + return absV + }, } - by, _ := json.Marshal(res) - return c.String(http.StatusOK, string(by)) + + var buf bytes.Buffer + if err := utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf, data); err != nil { + logrus.Error(err) + } + + return c.HTML(http.StatusOK, buf.String()) } func ChessHandler(c echo.Context) error { @@ -147,6 +235,7 @@ func ChessGameAnalyseHandler(c echo.Context) error { if err != nil { return c.String(http.StatusOK, err.Error()) } + g.DbChessGame.Stats, _ = json.Marshal(res) g.DbChessGame.AccuracyWhite = res.WhiteAccuracy g.DbChessGame.AccuracyBlack = res.BlackAccuracy g.DbChessGame.DoSave(db) diff --git a/pkg/web/handlers/interceptors/chess.go b/pkg/web/handlers/interceptors/chess.go @@ -8,6 +8,7 @@ import ( "dkforest/pkg/pubsub" "dkforest/pkg/utils" "encoding/base64" + "encoding/json" "errors" "fmt" "github.com/fogleman/gg" @@ -331,6 +332,27 @@ func (g *ChessGame) drawPlayerCard(key string, isBlack, isSpectator, isYourTurn, #black-advantage .score:after { content: "{{ .BlackScore }}"; } #outcome:after { content: "{{ .Outcome }}"; } .score { font-size: 11px; } + +.graph { + border: 3px solid #000; + background-color: #666; +} +.graph .first-row { height: 120px; } +.graph .second-row { height: 120px; } +.graph td { + border-right: 0px solid #555; +} +.graph .first-row td { + vertical-align: bottom; +} +.graph .second-row td { + vertical-align: top; +} +.graph .column { + width: {{ .ColumnWidth }}px; + box-sizing: border-box; + border-right: 1px solid #555; +} </style> <table style="width: 100%; height: 100%;"> <tr> @@ -390,15 +412,37 @@ func (g *ChessGame) drawPlayerCard(key string, isBlack, isSpectator, isYourTurn, <tr style="height: 100%;"> <td colspan="2"> <div style="color: #eee;">Outcome: <span id="outcome"></span></div> - {{ if .IsAnalysed }} - <div style="color: #eee;">White accuracy: <span id="white-accuracy">{{ .WhiteAccuracy }}</span></div> - <div style="color: #eee;">Black accuracy: <span id="black-accuracy">{{ .BlackAccuracy }}</span></div> - {{ end }} </td> </tr> - {{ if .GameOver }}<tr><td colspan="2"><div><textarea>{{ .PGN }}</textarea></div></td></tr>{{ end }} </table> + + {{ if .Stats }} + {{ if .IsAnalysed }} + <div style="color: #eee;">White accuracy: <span id="white-accuracy">{{ .WhiteAccuracy }}</span></div> + <div style="color: #eee;">Black accuracy: <span id="black-accuracy">{{ .BlackAccuracy }}</span></div> + {{ end }} + <table class="graph"> + <tr class="first-row"> + {{ range $idx, $el := .Stats.Scores }} + <td title="Move: {{ $idx }} | Advantage: {{ if not $el.Mate }}{{ $el.CP | cp }}{{ else }}#{{ $el.Mate }}{{ end }}"> + {{ if ge .CP 0 }} + <div class="column" style="height: {{ $el | renderCP "white" }}px; background-color: #eee;"></div> + {{ end }} + </td> + {{ end }} + </tr> + <tr class="second-row"> + {{ range $idx, $el := .Stats.Scores }} + <td title="Move: {{ $idx }} | Advantage: {{ if not $el.Mate }}{{ $el.CP | cp }}{{ else }}#{{ $el.Mate }}{{ end }}"> + {{ if le .CP 0 }} + <div class="column" style="height: {{ $el | renderCP "black" }}px; background-color: #111;"></div> + {{ end }} + </td> + {{ end }} + </tr> + </table> + {{ end }} </td> </tr> </table> @@ -411,6 +455,14 @@ func (g *ChessGame) drawPlayerCard(key string, isBlack, isSpectator, isYourTurn, imgB64 := g.renderBoardB64(isBlack) whiteAdvantage, whiteScore, blackAdvantage, blackScore := CalcAdvantage(game.Position()) + const graphWidth = 800 + var columnWidth = 0 + var stats *AnalyseResult + _ = json.Unmarshal(g.DbChessGame.Stats, &stats) + if stats != nil { + columnWidth = graphWidth / len(stats.Scores) + } + data := map[string]any{ "SoundsEnabled": soundsEnabled, "Key": key, @@ -431,16 +483,37 @@ func (g *ChessGame) drawPlayerCard(key string, isBlack, isSpectator, isYourTurn, "IsAnalysed": g.DbChessGame.AccuracyWhite != 0 && g.DbChessGame.AccuracyBlack != 0, "WhiteAccuracy": g.DbChessGame.AccuracyWhite, "BlackAccuracy": g.DbChessGame.AccuracyBlack, + "Stats": stats, + "ColumnWidth": columnWidth, } fns := template.FuncMap{ "attr": func(s string) template.HTMLAttr { return template.HTMLAttr(s) }, + "abs": func(v int) int { return int(math.Abs(float64(v))) }, + "cp": func(v int) string { + return fmt.Sprintf("%.2f", float64(v)/100) + }, + "renderCP": func(color string, v Score) int { + const maxH = 120 // Max graph height + const maxV = 1200 // Max cp value. Anything bigger should take 100% of graph height space. + absV := int(math.Abs(float64(v.CP))) + absV = utils.MinInt(absV, maxV) + absV = absV * maxH / maxV + if v.CP == 0 && v.Mate != 0 { + if (color == "white" && v.Mate < 0) || (color == "black" && v.Mate > 0) { + absV = maxH + } + } + return absV + }, } var buf1 bytes.Buffer - _ = utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf1, data) + if err := utils.Must(template.New("").Funcs(fns).Parse(htmlTmpl)).Execute(&buf1, data); err != nil { + logrus.Error(err) + } return buf1.String() } @@ -753,9 +826,15 @@ func CalcAdvantage(position *chess.Position) (string, string, string, string) { return whiteAdvantage, whiteScoreLbl, blackAdvantage, blackScoreLbl } +type Score struct { + CP int + Mate int +} + type AnalyseResult struct { WhiteAccuracy float64 BlackAccuracy float64 + Scores []Score } func AnalyseGame(pgn string, t int64) (out AnalyseResult, err error) { @@ -787,6 +866,7 @@ func AnalyseGame(pgn string, t int64) (out AnalyseResult, err error) { } defer eng.Close() + scores := make([]Score, 0) cps := make([]int, 0) t = utils.Clamp(t, 1, 60) @@ -804,10 +884,12 @@ func AnalyseGame(pgn string, t int64) (out AnalyseResult, err error) { } res := eng.SearchResults() cp := res.Info.Score.CP + mate := res.Info.Score.Mate if idx%2 != 0 { cp *= -1 } cps = append(cps, cp) + scores = append(scores, Score{CP: cp, Mate: mate}) //fmt.Printf("%d: %d/%d %d %d\n", idx/2, idx, len(positions), idx%2, cp) } @@ -820,6 +902,7 @@ func AnalyseGame(pgn string, t int64) (out AnalyseResult, err error) { wa, ba := gameAccuracy(cps) return AnalyseResult{ + Scores: scores, WhiteAccuracy: wa, BlackAccuracy: ba, }, nil