From 5b30b34001f84c2406ac875fa419b6c1bf315d6a Mon Sep 17 00:00:00 2001 From: Jan Niklas Richter Date: Sun, 11 Apr 2021 02:00:51 +0200 Subject: [PATCH] Add tests to evaluation --- evaluation/evaluation.go | 7 +- evaluation/evaluation_test.go | 207 ++++++++++++++++++++++++++++++++++ evaluation/weights.go | 5 +- game/game_test.go | 104 ++++++++++++++++- 4 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 evaluation/evaluation_test.go diff --git a/evaluation/evaluation.go b/evaluation/evaluation.go index b4d67a1..97f106c 100644 --- a/evaluation/evaluation.go +++ b/evaluation/evaluation.go @@ -28,7 +28,7 @@ func (e *Evaluation) updateTotal() { if e.Game.Result == game.WhiteWon { e.TotalScore = int(^uint(0) >> 1) // MAX INT } else if e.Game.Result == game.BlackWon { - e.TotalScore = -e.TotalScore - 1 // MIN INT + e.TotalScore = -int(^uint(0)>>1) - 1 // MIN INT } else { e.TotalScore = 0 // DRAW } @@ -100,7 +100,7 @@ func calculatePieceScore(g *game.Game, color game.PlayerColor) map[dragontoothmg bboards = g.Position.Black } - for i := 1; i <= 5; i++ { + for i := 1; i <= 6; i++ { bitboard := getBitboardByPieceType(&bboards, dragontoothmg.Piece(i)) count := bits.OnesCount64(bitboard) ps[dragontoothmg.Piece(i)] += count * GetWeights().Midgame.Material[dragontoothmg.Piece(i)] @@ -121,10 +121,9 @@ func getBitboardByPieceType(bbs *dragontoothmg.Bitboards, pieceType dragontoothm return bbs.Rooks case dragontoothmg.Queen: return bbs.Queens - case dragontoothmg.King: + default: return bbs.Kings } - return bbs.All } func sumMapValues(mapToSum map[dragontoothmg.Piece]int) int { diff --git a/evaluation/evaluation_test.go b/evaluation/evaluation_test.go new file mode 100644 index 0000000..e160d2e --- /dev/null +++ b/evaluation/evaluation_test.go @@ -0,0 +1,207 @@ +package evaluation + +import ( + "reflect" + "testing" + + "github.com/dylhunn/dragontoothmg" + "go.janniklasrichter.de/axwchessbot/game" +) + +func Test_calculateGamephase(t *testing.T) { + type args struct { + g *game.Game + color game.PlayerColor + } + tests := []struct { + name string + args args + want uint8 + }{ + {"GameStart White", args{game.NewFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"), game.White}, 12}, + {"GameStart Black", args{game.NewFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"), game.Black}, 12}, + {"KvKQ White", args{game.NewFromFen("1q6/2k5/8/8/8/8/3K4/8 w - - 0 1"), game.White}, 0}, + {"KvKQ Black", args{game.NewFromFen("1q6/2k5/8/8/8/8/3K4/8 w - - 0 1"), game.Black}, 4}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculateGamephase(tt.args.g, tt.args.color); got != tt.want { + t.Errorf("calculateGamephase() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_calculatePieceScore(t *testing.T) { + type args struct { + g *game.Game + color game.PlayerColor + } + tests := []struct { + name string + args args + want map[dragontoothmg.Piece]int + }{ + {"GameStart White", args{game.NewFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"), game.White}, map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 800, dragontoothmg.Knight: 640, dragontoothmg.Bishop: 660, dragontoothmg.Rook: 1000, dragontoothmg.Queen: 900, dragontoothmg.King: 0}}, + {"GameStart Black", args{game.NewFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"), game.Black}, map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 800, dragontoothmg.Knight: 640, dragontoothmg.Bishop: 660, dragontoothmg.Rook: 1000, dragontoothmg.Queen: 900, dragontoothmg.King: 0}}, + {"KRNPvKQB White", args{game.NewFromFen("1q6/2k1b3/8/8/8/5P2/3KN3/7R w - - 0 1"), game.White}, map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 100, dragontoothmg.Knight: 320, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 500, dragontoothmg.Queen: 0, dragontoothmg.King: 0}}, + {"KRNPvKQB Black", args{game.NewFromFen("1q6/2k1b3/8/8/8/5P2/3KN3/7R w - - 0 1"), game.Black}, map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 330, dragontoothmg.Rook: 0, dragontoothmg.Queen: 900, dragontoothmg.King: 0}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := calculatePieceScore(tt.args.g, tt.args.color); !reflect.DeepEqual(got, tt.want) { + t.Errorf("calculatePieceScore() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEvaluation_updateTotal(t *testing.T) { + type fields struct { + Game *game.Game + White EvaluationPart + Black EvaluationPart + GameOver bool + TotalScore int + TotalScorePerspective int + } + tests := []struct { + name string + fields fields + want int + }{ + { + "GameStart", + fields{ + Game: game.NewFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"), + White: EvaluationPart{ + GamePhase: 12, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 800, dragontoothmg.Knight: 640, dragontoothmg.Bishop: 660, dragontoothmg.Rook: 1000, dragontoothmg.Queen: 900, dragontoothmg.King: 0}, + }, + Black: EvaluationPart{ + GamePhase: 12, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 800, dragontoothmg.Knight: 640, dragontoothmg.Bishop: 660, dragontoothmg.Rook: 1000, dragontoothmg.Queen: 900, dragontoothmg.King: 0}, + }, + }, + 0, + }, + { + "KRNPvKQB", + fields{ + Game: game.NewFromFen("1q6/2k1b3/8/8/8/5P2/3KN3/7R b - - 0 1"), + White: EvaluationPart{ + GamePhase: 12, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 100, dragontoothmg.Knight: 320, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 500, dragontoothmg.Queen: 0, dragontoothmg.King: 0}, + }, + Black: EvaluationPart{ + GamePhase: 12, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 330, dragontoothmg.Rook: 0, dragontoothmg.Queen: 900, dragontoothmg.King: 0}, + }, + }, + -310, + }, + { + "KQvK Checkmate White", + fields{ + Game: game.NewFromFen("7k/6Q1/5K2/8/8/8/8/8 b - - 0 1"), + White: EvaluationPart{ + GamePhase: 4, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 0, dragontoothmg.Queen: 900, dragontoothmg.King: 0}, + }, + Black: EvaluationPart{ + GamePhase: 0, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 0, dragontoothmg.Queen: 0, dragontoothmg.King: 0}, + }, + GameOver: true, + }, + int(^uint(0) >> 1), + }, + { + "KQvK Checkmate Black", + fields{ + Game: game.NewFromFen("8/8/8/8/8/2k5/1q6/K7 w - - 0 1"), + White: EvaluationPart{ + GamePhase: 0, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 0, dragontoothmg.Queen: 0, dragontoothmg.King: 0}, + }, + Black: EvaluationPart{ + GamePhase: 4, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 0, dragontoothmg.Queen: 900, dragontoothmg.King: 0}, + }, + GameOver: true, + }, + -int(^uint(0)>>1) - 1, + }, + { + "KvKR Stalemate", + fields{ + Game: game.NewFromFen("8/8/8/8/8/2k5/1r6/K7 w - - 0 1"), + White: EvaluationPart{ + GamePhase: 0, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 0, dragontoothmg.Queen: 0, dragontoothmg.King: 0}, + }, + Black: EvaluationPart{ + GamePhase: 2, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 0, dragontoothmg.Knight: 0, dragontoothmg.Bishop: 0, dragontoothmg.Rook: 500, dragontoothmg.Queen: 0, dragontoothmg.King: 0}, + }, + GameOver: true, + }, + 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Evaluation{ + Game: tt.fields.Game, + White: tt.fields.White, + Black: tt.fields.Black, + GameOver: tt.fields.GameOver, + TotalScore: tt.fields.TotalScore, + TotalScorePerspective: tt.fields.TotalScorePerspective, + } + e.updateTotal() + if !reflect.DeepEqual(e.TotalScore, tt.want) { + t.Errorf("Evaluation.updateTotal() = %v, want %v", e.TotalScore, tt.want) + } + }) + } +} + +func TestCalculateEvaluation(t *testing.T) { + game_gamestart := game.NewFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + tests := []struct { + name string + args *game.Game + want *Evaluation + }{ + { + "GameStart", + game_gamestart, + &Evaluation{ + Game: game_gamestart, + White: EvaluationPart{ + GamePhase: 12, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 800, dragontoothmg.Knight: 640, dragontoothmg.Bishop: 660, dragontoothmg.Rook: 1000, dragontoothmg.Queen: 900, dragontoothmg.King: 0}, + PieceSquareScoreMidgame: make(map[dragontoothmg.Piece]int), + PieceSquareScoreEndgame: make(map[dragontoothmg.Piece]int), + }, + Black: EvaluationPart{ + GamePhase: 12, + PieceScore: map[dragontoothmg.Piece]int{dragontoothmg.Pawn: 800, dragontoothmg.Knight: 640, dragontoothmg.Bishop: 660, dragontoothmg.Rook: 1000, dragontoothmg.Queen: 900, dragontoothmg.King: 0}, + PieceSquareScoreMidgame: make(map[dragontoothmg.Piece]int), + PieceSquareScoreEndgame: make(map[dragontoothmg.Piece]int), + }, + GameOver: false, + TotalScore: 0, + TotalScorePerspective: 0, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := CalculateEvaluation(tt.args); !reflect.DeepEqual(got, tt.want) { + t.Errorf("CalculateEvaluation() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/evaluation/weights.go b/evaluation/weights.go index a8a78b6..fbab052 100644 --- a/evaluation/weights.go +++ b/evaluation/weights.go @@ -16,10 +16,11 @@ func getWeightsForAllPhases() GamephaseWeights { return GamephaseWeights{ Material: map[dragontoothmg.Piece]int{ dragontoothmg.Pawn: 100, - dragontoothmg.Knight: 300, - dragontoothmg.Bishop: 300, + dragontoothmg.Knight: 320, + dragontoothmg.Bishop: 330, dragontoothmg.Rook: 500, dragontoothmg.Queen: 900, + dragontoothmg.King: 0, }, PieceSquareTables: map[dragontoothmg.Piece][64]int{ dragontoothmg.Pawn: [64]int{ diff --git a/game/game_test.go b/game/game_test.go index d685b92..d83ea23 100644 --- a/game/game_test.go +++ b/game/game_test.go @@ -1,14 +1,106 @@ package game import ( + "reflect" "testing" ) -func TestCheckmate(t *testing.T) { - fenStr := "rn1qkbnr/pbpp1ppp/1p6/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR w KQkq - 0 1" - game := NewFromFen(fenStr) - game.pushMove("f3f7") - if game.Result != WhiteWon { - t.Fatalf("expected result %d but got %d", WhiteWon, game.Result) +func TestGameOverDetection(t *testing.T) { + type args struct { + fen string + } + tests := []struct { + name string + args args + wantResult GameResult + wantReason DrawReason + }{ + {"GameNotOver", args{"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"}, GameNotOver, NoDraw}, + {"Checkmate White", args{"rn1qkbnr/pbpp1Qpp/1p6/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 1"}, WhiteWon, NoDraw}, + {"Checkmate Black", args{"rnb1k1nr/pppp1ppp/8/2b1p3/2B1P3/2N5/PPPP1qPP/R1BQK2R w KQkq - 0 1"}, BlackWon, NoDraw}, + {"Draw Stalemate", args{"7k/6R1/5K2/8/8/8/8/8 b - - 0 1"}, Draw, Stalemate}, + {"Draw InsufficientMaterial KBvKB Whitefields", args{"8/2kb4/8/8/8/3B4/4K3/8 w - - 0 1"}, Draw, InsufficientMaterial}, + {"Draw InsufficientMaterial KBvKB Blackfields", args{"8/2k1b3/8/8/5B2/5K2/8/8 w - - 0 1"}, Draw, InsufficientMaterial}, + {"Draw InsufficientMaterial KvK", args{"8/2k5/8/8/8/8/3K4/8 w - - 0 1"}, Draw, InsufficientMaterial}, + {"Draw InsufficientMaterial KNvK", args{"8/2k5/8/8/4N3/5K2/8/8 w - - 0 1"}, Draw, InsufficientMaterial}, + {"Draw InsufficientMaterial KvKB", args{"8/2k5/3b4/8/8/5K2/8/8 w - - 0 1"}, Draw, InsufficientMaterial}, + {"Draw FiftyMoveRule", args{"rNB1k2r/ppp2ppp/3p3B/3np2Q/3NP2q/3P3b/PPP2PPP/Rnb1K2R w - - 100 43"}, Draw, FiftyMoveRule}, + {"GameNotOver KBvKB", args{"8/2k1b3/8/8/4B3/5K2/8/8 w - - 0 1"}, GameNotOver, NoDraw}, + {"King Missing", args{"8/8/8/8/8/5K2/8/8 w - - 0 1"}, GameNotOver, NoDraw}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewFromFen(tt.args.fen) + if !reflect.DeepEqual(got.Result, tt.wantResult) { + t.Errorf("Game Over Detection, Result = %v, want %v", got.Result, tt.wantResult) + } + if !reflect.DeepEqual(got.DrawReason, tt.wantReason) { + t.Errorf("Game Over Detection, Reason = %v, want %v", got.DrawReason, tt.wantReason) + } + }) + } +} + +func TestDrawByRepetition(t *testing.T) { + game := NewFromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") + game.pushMove("e2e4") + game.pushMove("e7e5") + + game.pushMove("f1c4") + game.pushMove("f8c5") + game.pushMove("c4f1") + game.pushMove("c5f8") + + game.pushMove("f1c4") + game.pushMove("f8c5") + game.pushMove("c4f1") + game.pushMove("c5f8") + + game.pushMove("f1c4") + game.pushMove("f8c5") + game.pushMove("c4f1") + game.pushMove("c5f8") + + if game.Result != Draw { + t.Errorf("Draw by Repetition, Result = %v, want %v", game.Result, Draw) + } + if game.DrawReason != ThreefoldRepetition { + t.Errorf("Draw by Repetition, Result = %v, want %v", game.DrawReason, ThreefoldRepetition) + } +} + +func TestPushPopMove(t *testing.T) { + game := New() + unmodified_game := New() + game.pushMove("e2e4") + + if reflect.DeepEqual(game, unmodified_game) { + t.Errorf("PushMove = %v is wrongfully equal to %v", game, unmodified_game) + } + + game.popMove() + + if !reflect.DeepEqual(game, unmodified_game) { + t.Errorf("PushPopMove = %v, want %v", game, unmodified_game) + } +} + +func TestPushMoveError(t *testing.T) { + game := New() + + err := game.pushMove("e123e1") + + if err == nil { + t.Errorf("PushMoveError = %v is not an error", err) + } +} + +func TestPopNoMoves(t *testing.T) { + game := New() + + err := game.popMove() + + if err == nil { + t.Errorf("PopNoMoves = %v, is not an error", err) } }