Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Count all blunders #255

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 85 additions & 32 deletions katago/kataprob/kataprob.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,117 @@
package kataprob

import (
"math"

"github.com/golang/glog"
"github.com/otrego/clamshell/go/color"
"github.com/otrego/clamshell/go/movetree"
"github.com/otrego/clamshell/katago"
)

// FindBlunderOptions contains options for blunder detection
type FindBlunderOptions struct {
// Sets the minimum point delta beyond which a move is considered a blunder
PointThreshold float64
// Determines which color's blunders we are searching. Can be Empty as well
Color color.Color
}

// FindBlunders finds positions (paths) that result from big swings in points.
func FindBlunders(g *movetree.MoveTree) ([]movetree.Path, error) {
blunderAmt := 3.0
return findBlunders(g, &FindBlunderOptions{PointThreshold: 3.0, Color: color.Empty})
}

func FindBlundersWithOptions(g *movetree.MoveTree, opt *FindBlunderOptions) ([]movetree.Path, error) {
return findBlunders(g, opt)
}

func findBlunders(g *movetree.MoveTree, opt *FindBlunderOptions) ([]movetree.Path, error) {
glog.V(3).Infof("Finding blunders with options: %v:\n", opt)
var cur movetree.Path
var found []movetree.Path
if g.Root == nil {
return found, nil
}

var prevLead float64

for n := g.Root; n != nil; n = n.Next(0) {
glog.V(3).Infof("VarNum %v\n", n.VarNum())
glog.V(3).Infof("MoveNum %v\n", n.MoveNum())

// We assume alternating moves. Lead is always presented as
pl := prevLead
cur = append(cur, n.VarNum())
glog.V(3).Infof("PrevLead %v\n", prevLead)

d := n.AnalysisData()
if d == nil {
glog.Infof("nil analysis data")
continue
if n.Move != nil {
glog.V(3).Info(n.Move.GoString())
}

katad, ok := d.(*katago.AnalysisResult)
delta, ok := computeDelta(n, n.Parent)
glog.V(3).Infof("Delta: %f\n", delta)
if !ok {
glog.V(2).Infof("not analysisResult")
continue
}
if katad.RootInfo == nil {
// This
glog.Errorf("no RootInfo for at move %v", n.MoveNum())
glog.V(3).Info("No ScoreLead for current node, skipping")
continue
}
cur = append(cur, n.VarNum())

lead := katad.RootInfo.ScoreLead
nextLead := -1 * lead
glog.V(3).Infof("Next ScoreLead: %v:", nextLead)
delta := nextLead - pl
glog.V(3).Infof("Delta: %v:", delta)

if delta >= math.Abs(blunderAmt) {
if delta <= -opt.PointThreshold {
found = append(found, cur.Clone())
}

// prevLead is always with respect to current player
prevLead = nextLead
}

if opt.Color != color.Empty {
return filterColor(g, &found, opt.Color), nil
}
return found, nil
}

func getScoreLead(n *movetree.Node) (float64, bool) {
if n == nil {
glog.V(2).Info("nil node")
return 0, false
}

d := n.AnalysisData()
if d == nil {
glog.V(2).Info("nil analysis data")
return 0, false
}

nData, ok := d.(*katago.AnalysisResult)
if !ok {
glog.V(2).Info("not analysisResult")
return 0, false
}

if nData.RootInfo == nil {
glog.Errorf("no RootInfo for at move %v", n.MoveNum())
return 0, false
}

return nData.RootInfo.ScoreLead, true
}

func computeDelta(n, p *movetree.Node) (float64, bool) {
previousLead, ok := getScoreLead(p)
if !ok {
glog.V(3).Info("Defaulting score to 0")
}
glog.V(3).Infof("Previous Lead %v\n", previousLead)

currentLead, ok := getScoreLead(n)
if !ok {
return 0, false
}
glog.V(3).Infof("Current Lead %v\n", currentLead)

// A positive ScoreLead means black is winning. Negative means white is winning.
delta := currentLead - previousLead
if n.Move.Color() == color.White {
delta *= -1
}
glog.V(3).Infof("Delta: %v:", delta)
return delta, true
}

func filterColor(g *movetree.MoveTree, paths *[]movetree.Path, c color.Color) []movetree.Path {
var filtered []movetree.Path
for _, p := range *paths {
if p.Apply(g.Root).Move.Color() == c {
filtered = append(filtered, p)
}
}
return filtered
}
141 changes: 141 additions & 0 deletions katago/kataprob/kataprob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package kataprob

import (
"encoding/json"
"os"
"testing"

"github.com/otrego/clamshell/go/color"
"github.com/otrego/clamshell/go/movetree"
"github.com/otrego/clamshell/go/sgf"
"github.com/otrego/clamshell/katago"
)

func TestBlunderComprehensiveness(t *testing.T) {
game, err := makeGameWithAnalysis(t)
if err != nil {
t.Fatal(err)
}

paths, err := FindBlunders(game)
if err != nil {
t.Fatal(err)
}

expectedBlunders := []int{2, 3, 5, 6}
for i, expected := range expectedBlunders {
actual := paths[i].Apply(game.Root).MoveNum()
if actual != expected {
t.Errorf("Expected to find blunder at move %d, but found move %d", expected, actual)
}
}
}

func TestBlunderPointThreshold(t *testing.T) {
game, err := makeGameWithAnalysis(t)
if err != nil {
t.Fatal(err)
}

threshold := 8.0
paths, err := FindBlundersWithOptions(game, &FindBlunderOptions{PointThreshold: threshold})
if err != nil {
t.Fatal(err)
}

expectedBlunders := []int{2, 3, 6}
for i, expected := range expectedBlunders {
actual := paths[i].Apply(game.Root).MoveNum()
if actual != expected {
t.Errorf("Expected to find blunder at move %d, but found move %d", expected, actual)
}
}
}

func TestBlunderBlackColorFilter(t *testing.T) {
expectedColor := color.Black
game, err := makeGameWithAnalysis(t)
if err != nil {
t.Fatal(err)
}

threshold := 3.0
paths, err := FindBlundersWithOptions(
game,
&FindBlunderOptions{PointThreshold: threshold, Color: expectedColor},
)
if err != nil {
t.Fatal(err)
}

expectedBlunders := []int{3, 5}
for i, expected := range expectedBlunders {
root := paths[i].Apply(game.Root)
actual := root.MoveNum()
if actual != expected {
t.Errorf("Expected to find blunder at move %d, but found move %d", expected, actual)
}
actualColor := root.Move.Color()
if actualColor != expectedColor {
t.Errorf("Found color %#v for move %d, expected %#v", actualColor, root.MoveNum(), expectedColor)
}
}
}

func TestBlunderWhiteColorFilter(t *testing.T) {
expectedColor := color.White
game, err := makeGameWithAnalysis(t)
if err != nil {
t.Fatal(err)
}

threshold := 3.0
paths, err := FindBlundersWithOptions(
game,
&FindBlunderOptions{PointThreshold: threshold, Color: expectedColor},
)
if err != nil {
t.Fatal(err)
}

expectedBlunders := []int{2, 6}
for i, expected := range expectedBlunders {
root := paths[i].Apply(game.Root)
actual := root.MoveNum()
if actual != expected {
t.Errorf("Expected to find blunder at move %d, but found move %d", expected, actual)
}
actualColor := root.Move.Color()
if actualColor != expectedColor {
t.Errorf("Found color %#v for move %d, expected %#v", actualColor, root.MoveNum(), expectedColor)
}
}
}

func makeGameWithAnalysis(t *testing.T) (*movetree.MoveTree, error) {
content, err := os.ReadFile("../../test-database/blunders.sgf")
if err != nil {
t.Fatal(err)
}

game, err := sgf.FromString(string(content)).Parse()
if err != nil {
t.Fatal(err)
}

analysisBytes, err := os.ReadFile("../testdata/blunder-analysis.json")
if err != nil {
t.Fatal(err)
}

analysis := &katago.AnalysisList{}
if err := json.Unmarshal(analysisBytes, analysis); err != nil {
t.Fatal(err)
}

if err := analysis.AddToGame(game); err != nil {
t.Fatal(err)
}

return game, nil
}
Loading