dkforest

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

commit 7ea50e51c76aa8dc9820569d4b104bb6e33348a2
parent 1d997a4501521a73405ffb99416e5f855db6bf6a
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Tue, 13 Jun 2023 12:55:30 -0700

testing accuracy algo

Diffstat:
Mpkg/web/handlers/interceptors/chess.go | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apkg/web/handlers/interceptors/chess_test.go | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 285 insertions(+), 0 deletions(-)

diff --git a/pkg/web/handlers/interceptors/chess.go b/pkg/web/handlers/interceptors/chess.go @@ -14,10 +14,13 @@ import ( "github.com/google/uuid" "github.com/labstack/echo" "github.com/notnil/chess" + "github.com/notnil/chess/uci" + "github.com/sirupsen/logrus" "html/template" "image" "image/color" "image/png" + "math" "sort" "strconv" "strings" @@ -717,3 +720,201 @@ func CalcAdvantage(position *chess.Position) (string, string, string, string) { } return whiteAdvantage, whiteScoreLbl, blackAdvantage, blackScoreLbl } + +func Test() { + // lichess: 86% 97% (game against stockfish) + // Lila: 26.036024039104852 25.007112871751964 + // Ours: 28.07242196565701 25.629909658576725 + // 35, -29, 37, -42, 31, -15, 28, -16, 16, -4, 30, -24, 13, -14, 0, -17, 9, 6, -17, 8, -3, -5, -1, 5, -13, 20, -21, 90, -59, 77, -55, 70, -84, 116, -97, 68, -50, 46, -54, 93, -53, 57, -62, 61, -18, 11, -17, 41, -15, 13, -19, 8, 0, 22, -76, 44, -46, 87, -62, 62, -56, 59, -28, 69, -50, 70, -63, 63, -27, 30, -53, 14, -65, 43, -17, 398, -430, 565, -597, 793, -802, 837, -821, 908, -915, 957, -984, 1073, -981, 1285 + 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 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 Be6 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 Re2 39. f4+ Kf6 " + + "40. g5+ Kf5 41. Rh1 Rxe3+ 42. Kh4 Rxb3 43. g6 Kxf4 44. Kh5 Ra3 45. Rf1+ Rf3 46. Rxf3+ exf3 47. Kh4 f2 " + + "48. Kh3 Kf3 49. Kh2 f1=R 50. Kh3 Rh1# { Black wins by checkmate. } 0-1" + pgnOpt, _ := chess.PGN(strings.NewReader(pgn)) + g := chess.NewGame(pgnOpt) + positions := g.Positions() + + eng, err := uci.New("stockfish") + if err != nil { + logrus.Error(err) + return + } + if err := eng.Run(uci.CmdUCI, uci.CmdIsReady, uci.CmdUCINewGame); err != nil { + logrus.Error(err) + return + } + defer eng.Close() + + cps := make([]int, 0) + + for idx, position := range positions { + cmdPos := uci.CmdPosition{Position: position} + cmdGo := uci.CmdGo{MoveTime: time.Second / 20, Depth: 22} + if err := eng.Run(cmdPos, cmdGo); err != nil { + logrus.Fatal(err) + } + res := eng.SearchResults() + cp := res.Info.Score.CP + cps = append(cps, cp) + fmt.Printf("%d/%d %d %d\n", idx, len(positions), idx%2, cp) + } + + s := make([]string, 0) + for _, c := range cps { + s = append(s, strconv.Itoa(c)) + } + fmt.Println(strings.Join(s, ", ")) + fmt.Println(gameAccuracy(cps)) +} + +func standardDeviation(num []WinPercent) float64 { + nb := len(num) + var sum, mean, sd float64 + for i := 0; i < nb; i++ { + sum += float64(num[i]) + } + mean = sum / float64(nb) + for j := 0; j < nb; j++ { + sd += math.Pow(float64(num[j])-mean, 2) + } + sd = math.Sqrt(sd) / float64(nb) + return sd +} + +// https://www.scribbr.com/statistics/standard-deviation/ +//// using population variance +//def standardDeviation(a: Iterable[Double]): Option[Double] = +//mean(a) map { mean => +//Math.sqrt: +//a.foldLeft(0d) { (sum, x) => +//sum + Math.pow(x - mean, 2) +//} / a.size +//} + +type WinPercent float64 + +type Cp int + +func fromCentiPawns(cp Cp) WinPercent { + return 50 + 50*winningChances(cp) +} + +func winningChances(cp Cp) WinPercent { + const MULTIPLIER = -0.00368208 // https://github.com/lichess-org/lila/pull/11148 + res := 2/(1+math.Exp(MULTIPLIER*float64(cp))) - 1 + out := WinPercent(math.Max(math.Min(res, 1), -1)) + return out +} + +func fromWinPercents(before, after float64) (accuracy float64) { + if after >= before { + return 100 + } + winDiff := before - after + raw := 103.1668100711649*math.Exp(-0.04354415386753951*winDiff) + -3.166924740191411 + raw += 1 + return math.Min(math.Max(raw, 0), 100) +} + +func calcWindows(allWinPercents []WinPercent, windowSize int) (out [][]WinPercent) { + start := allWinPercents[:windowSize] + tmp := windowSize + if tmp > len(allWinPercents) { + tmp = len(allWinPercents) + } + for i := 0; i < tmp-2; i++ { + out = append(out, start) + } + + for i := 0; i < len(allWinPercents)-(windowSize-1); i++ { + curr := make([]WinPercent, 0) + for j := 0; j < windowSize; j++ { + curr = append(curr, allWinPercents[i+j]) + } + out = append(out, curr) + } + return +} + +func calcWeights(windows [][]WinPercent) (out []float64) { + for _, w := range windows { + out = append(out, math.Min(math.Max(standardDeviation(w), 0.5), 12)) + } + return +} + +func calcWeightedAccuracies(allWinPercents []WinPercent, weights []float64) (float64, float64) { + sw := calcWindows(allWinPercents, 2) + whites := make([][2]float64, 0) + blacks := make([][2]float64, 0) + for i := 0; i < len(sw); i++ { + prev, next := sw[i][0], sw[i][1] + acc := prev + acc1 := next + if i%2 != 0 { + acc, acc1 = acc1, acc + } + accuracy := fromWinPercents(float64(acc), float64(acc1)) + + if i%2 == 0 { + whites = append(whites, [2]float64{accuracy, weights[i]}) + } else { + blacks = append(blacks, [2]float64{accuracy, weights[i]}) + } + } + + www1 := weightedMean(whites) + www2 := harmonicMean(whites) + bbb1 := weightedMean(blacks) + bbb2 := harmonicMean(blacks) + return (www1 + www2) / 2, (bbb1 + bbb2) / 2 +} + +func harmonicMean(arr [][2]float64) float64 { + vs := make([]float64, 0) + for _, v := range arr { + vs = append(vs, v[0]) + } + sm := 0.0 + for i := 0; i < len(vs); i++ { + sm = sm + 1/vs[i] + } + return float64(len(vs)) / sm +} + +func weightedMean(a [][2]float64) float64 { + vs := make([]float64, 0) + ws := make([]float64, 0) + + for _, v := range a { + vs = append(vs, v[0]) + ws = append(ws, v[1]) + } + + sumWeight, avg := 0.0, 0.0 + for i, v := range vs { + if v == 0 { + continue + } + sumWeight += ws[i] + avg += float64(v) * ws[i] + } + avg /= sumWeight + return avg +} + +func gameAccuracy(cps []int) (float64, float64) { + cps = append([]int{15}, cps...) + var allWinPercents []WinPercent + for _, cp := range cps { + allWinPercents = append(allWinPercents, fromCentiPawns(Cp(cp))) + } + windowSize := int(math.Min(math.Max(float64(len(cps)/10), 2), 8)) + windows := calcWindows(allWinPercents, windowSize) + weights := calcWeights(windows) + wa, ba := calcWeightedAccuracies(allWinPercents, weights) + return wa, ba +} diff --git a/pkg/web/handlers/interceptors/chess_test.go b/pkg/web/handlers/interceptors/chess_test.go @@ -0,0 +1,84 @@ +package interceptors + +import ( + "github.com/stretchr/testify/assert" + "math" + "testing" +) + +func isCloseTo(a, b, delta float64) bool { + return math.Abs(a-b) <= delta +} + +func fill(n, v int) []int { + out := make([]int, n) + for i := 0; i < n; i++ { + out[i] = v + } + return out +} + +func fill1(n int, v []int) []int { + out := make([]int, n*len(v)) + for i := 0; i < n*len(v); i += len(v) { + for j := range v { + out[i+j] = v[j] + } + } + return out +} + +func Test_gameAccuracy(t *testing.T) { + // two good moves + w, b := gameAccuracy([]int{15, 15}) + assert.True(t, isCloseTo(w, 100, 1)) + assert.True(t, isCloseTo(b, 100, 1)) + // white blunders on first move + w, b = gameAccuracy([]int{-900, -900}) + assert.True(t, isCloseTo(w, 10, 5)) + assert.True(t, isCloseTo(b, 100, 1)) + // black blunders on first move + w, b = gameAccuracy([]int{15, 900}) + assert.True(t, isCloseTo(w, 100, 1)) + assert.True(t, isCloseTo(b, 10, 5)) + // both blunder on first move + w, b = gameAccuracy([]int{-900, 0}) + assert.True(t, isCloseTo(w, 10, 5)) + assert.True(t, isCloseTo(b, 10, 5)) + // 20 perfect moves + w, b = gameAccuracy(fill(20, 15)) + assert.True(t, isCloseTo(w, 100, 1)) + assert.True(t, isCloseTo(b, 100, 1)) + // 20 perfect moves and a white blunder + cps := fill(20, 15) + cps = append(cps, -900) + w, b = gameAccuracy(cps) + assert.True(t, isCloseTo(w, 50, 5)) + assert.True(t, isCloseTo(b, 100, 1)) + // 21 perfect moves and a black blunder + cps = fill(21, 15) + cps = append(cps, 900) + w, b = gameAccuracy(cps) + assert.True(t, isCloseTo(w, 100, 1)) + assert.True(t, isCloseTo(b, 50, 5)) + // 5 average moves (65 cpl) on each side + cps = []int{-50, 15, -50, 15, -50, 15, -50, 15, -50, 15} + w, b = gameAccuracy(cps) + assert.True(t, isCloseTo(w, 76, 8)) + assert.True(t, isCloseTo(b, 76, 8)) + // 50 average moves (65 cpl) on each side + cps = fill1(50, []int{-50, 15}) + w, b = gameAccuracy(cps) + assert.True(t, isCloseTo(w, 76, 8)) + assert.True(t, isCloseTo(b, 76, 8)) + // 50 mediocre moves (150 cpl) on each side + cps = fill1(50, []int{-135, 15}) + w, b = gameAccuracy(cps) + assert.True(t, isCloseTo(w, 54, 8)) + assert.True(t, isCloseTo(b, 54, 8)) + // 50 terrible moves (500 cpl) on each side + cps = fill1(50, []int{-435, 15}) + w, b = gameAccuracy(cps) + assert.True(t, isCloseTo(w, 20, 8)) + assert.True(t, isCloseTo(b, 20, 8)) +}