Skip to content

Commit 8661135

Browse files
committed
Add more evaluation
1 parent 55d0b12 commit 8661135

File tree

8 files changed

+191
-46
lines changed

8 files changed

+191
-46
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Go Build & Test
22
on:
33
push:
44
branches:
5-
- 'main'
5+
- 'master'
66
pull_request:
77
branches:
88
- '*'

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ implemented.
2929

3030
* Written in golang
3131
* Basic UCI interface
32+
* Simple timemanagement
3233
* Simple evaluation function using
3334
* Gamephase detection of mid- and endgame
3435
* Piece value
3536
* Piece square tables
37+
* Tempo evaluation
38+
* Pair bonus
39+
* Rook on (half-)open files bonus
3640
* Simple search algorithm using
3741
* Negamax with alpha-beta-pruning
3842
* Quiescence search
@@ -43,13 +47,9 @@ implemented.
4347
## Features ideas
4448

4549
* Evaluation improvements
46-
* Tempo evaluation
47-
* Pair bonus
48-
* Rook on (half-)open files bonus
4950
* Pawnshield bonus
5051
* Blocked piece penalty
5152
* Passed pawn bonus
52-
* Simple timemanagement
5353
* Search improvements
5454
* Killer move heuristics
5555
* Multiprocessing (Lazy SMP?)

evaluation/evaluation.go

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ type EvaluationPart struct {
1212
PieceScore int
1313
PieceSquareScoreMidgame int
1414
PieceSquareScoreEndgame int
15+
PairModifier int
16+
TempoModifier int
17+
RookFileModifier int
18+
BlockedPiecesModifier int
19+
KingShieldModifier int
20+
PassedPawnModifier int
1521
}
1622

1723
type Evaluation struct {
@@ -23,6 +29,26 @@ type Evaluation struct {
2329
TotalScorePerspective int
2430
}
2531

32+
func (e *Evaluation) CalculateEvaluation(g *game.Game) int {
33+
e.Game = g
34+
e.GameOver = e.Game.Result != game.GameNotOver
35+
36+
e.White = EvaluationPart{}
37+
e.Black = EvaluationPart{}
38+
39+
if !e.GameOver {
40+
e.White = calculateEvaluationPart(g, game.White)
41+
e.Black = calculateEvaluationPart(g, game.Black)
42+
}
43+
44+
e.updateTotal()
45+
return e.TotalScorePerspective
46+
}
47+
48+
func (e *Evaluation) GetPieceTypeValue(pieceType dragontoothmg.Piece) int {
49+
return weights[game.White].Midgame.Material[pieceType]
50+
}
51+
2652
func (e *Evaluation) updateTotal() {
2753
if e.GameOver {
2854
if e.Game.Result == game.WhiteWon {
@@ -48,35 +74,35 @@ func (e *Evaluation) updateTotal() {
4874
e.TotalScore += (gamePhase*e.White.PieceSquareScoreMidgame + (24-gamePhase)*e.White.PieceSquareScoreEndgame) / 24
4975
e.TotalScore -= (gamePhase*e.Black.PieceSquareScoreMidgame + (24-gamePhase)*e.Black.PieceSquareScoreEndgame) / 24
5076

77+
e.TotalScore += e.White.PairModifier
78+
e.TotalScore -= e.Black.PairModifier
79+
e.TotalScore += e.White.TempoModifier
80+
e.TotalScore -= e.Black.TempoModifier
81+
e.TotalScore += e.White.RookFileModifier
82+
e.TotalScore -= e.Black.RookFileModifier
83+
e.TotalScore += e.White.BlockedPiecesModifier
84+
e.TotalScore -= e.Black.BlockedPiecesModifier
85+
e.TotalScore += e.White.KingShieldModifier
86+
e.TotalScore -= e.Black.KingShieldModifier
87+
e.TotalScore += e.White.PassedPawnModifier
88+
e.TotalScore -= e.Black.PassedPawnModifier
89+
5190
e.TotalScorePerspective = e.TotalScore
5291
if !e.Game.Position.Wtomove {
5392
e.TotalScorePerspective = -e.TotalScore
5493
}
5594
}
5695

57-
func (e *Evaluation) CalculateEvaluation(g *game.Game) int {
58-
e.Game = g
59-
e.GameOver = e.Game.Result != game.GameNotOver
60-
61-
e.White = EvaluationPart{}
62-
e.Black = EvaluationPart{}
63-
64-
if !e.GameOver {
65-
e.White = calculateEvaluationPart(g, game.White)
66-
e.Black = calculateEvaluationPart(g, game.Black)
67-
}
68-
69-
e.updateTotal()
70-
return e.TotalScorePerspective
71-
}
72-
7396
func calculateEvaluationPart(g *game.Game, color game.PlayerColor) EvaluationPart {
7497
ps, pstMid, pstEnd := calculateMaterialScore(g, color)
7598
evalPart := EvaluationPart{
7699
GamePhase: calculateGamephase(g, color),
77100
PieceScore: ps,
78101
PieceSquareScoreMidgame: pstMid,
79102
PieceSquareScoreEndgame: pstEnd,
103+
PairModifier: calculatePairModifier(g, color),
104+
TempoModifier: calculateTempoModifier(g, color),
105+
RookFileModifier: calculateRookModifier(g, color),
80106
}
81107
return evalPart
82108
}
@@ -143,6 +169,61 @@ func calculateMaterialScoreForPieceType(g *game.Game, color game.PlayerColor, pi
143169
return ps, pstMid, pstEnd
144170
}
145171

146-
func (e *Evaluation) GetPieceTypeValue(pieceType dragontoothmg.Piece) int {
147-
return weights[game.White].Midgame.Material[pieceType]
172+
func calculatePairModifier(g *game.Game, color game.PlayerColor) (result int) {
173+
bboards := g.Position.White
174+
if color == game.Black {
175+
bboards = g.Position.Black
176+
}
177+
178+
if bits.OnesCount64(bboards.Bishops) >= 2 {
179+
result += weights[color].AdditionalModifier.BishopPairModifier
180+
}
181+
if bits.OnesCount64(bboards.Knights) >= 2 {
182+
result += weights[color].AdditionalModifier.KnightPairModifier
183+
}
184+
if bits.OnesCount64(bboards.Rooks) >= 2 {
185+
result += weights[color].AdditionalModifier.RookPairModifier
186+
}
187+
188+
return
189+
}
190+
191+
func calculateTempoModifier(g *game.Game, color game.PlayerColor) (result int) {
192+
if g.Position.Wtomove == bool(color) {
193+
result += weights[color].AdditionalModifier.TempoModifier
194+
}
195+
return
196+
}
197+
198+
func calculateRookModifier(g *game.Game, color game.PlayerColor) (result int) {
199+
bboardsOwn := g.Position.White
200+
bboardsOther := g.Position.Black
201+
if color == game.Black {
202+
bboardsOwn = g.Position.Black
203+
bboardsOther = g.Position.White
204+
}
205+
206+
pawnFillOwn := calculatePawnFileFill(bboardsOwn.Pawns)
207+
pawnFillOther := calculatePawnFileFill(bboardsOther.Pawns)
208+
209+
openFiles := ^pawnFillOwn & ^pawnFillOther
210+
halfOpenFiles := ^pawnFillOwn ^ openFiles
211+
212+
rooksOnOpenFiles := bits.OnesCount64(bboardsOwn.Rooks & openFiles)
213+
rooksOnHalfOpenFiles := bits.OnesCount64(bboardsOwn.Rooks & halfOpenFiles)
214+
215+
return rooksOnOpenFiles*weights[color].AdditionalModifier.OpenRookModifier + rooksOnHalfOpenFiles*weights[color].AdditionalModifier.HalfRookModifier
216+
}
217+
218+
func calculatePawnFileFill(pawnBitboard uint64) uint64 {
219+
// Northfill
220+
pawnBitboard |= (pawnBitboard << 8)
221+
pawnBitboard |= (pawnBitboard << 16)
222+
pawnBitboard |= (pawnBitboard << 32)
223+
// Southfill
224+
pawnBitboard |= (pawnBitboard >> 8)
225+
pawnBitboard |= (pawnBitboard >> 16)
226+
pawnBitboard |= (pawnBitboard >> 32)
227+
228+
return pawnBitboard
148229
}

evaluation/evaluation_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func TestCalculateEvaluation(t *testing.T) {
205205
{
206206
"GameStart",
207207
game_gamestart,
208-
0,
208+
10,
209209
},
210210
}
211211
for _, tt := range tests {

evaluation/weights.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,41 @@ import (
55
"go.janniklasrichter.de/axwchessbot/game"
66
)
77

8+
type AdditionalModifier struct {
9+
BishopPairModifier int
10+
KnightPairModifier int
11+
RookPairModifier int
12+
OpenRookModifier int
13+
HalfRookModifier int
14+
TempoModifier int
15+
KingShieldRank2Modifier int
16+
KingShieldRank3Modifier int
17+
RookBlockedByKingModifier int
18+
}
19+
820
type GamephaseWeights struct {
921
Material map[dragontoothmg.Piece]int
1022
PieceSquareTables map[dragontoothmg.Piece][64]int
1123
}
1224

1325
type Weights struct {
14-
Midgame GamephaseWeights
15-
Endgame GamephaseWeights
26+
Midgame GamephaseWeights
27+
Endgame GamephaseWeights
28+
AdditionalModifier AdditionalModifier
1629
}
1730

1831
var (
32+
additionalModifiers = AdditionalModifier{
33+
BishopPairModifier: 30,
34+
KnightPairModifier: -8,
35+
RookPairModifier: -16,
36+
OpenRookModifier: 10,
37+
HalfRookModifier: 5,
38+
TempoModifier: 10,
39+
KingShieldRank2Modifier: 10,
40+
KingShieldRank3Modifier: 5,
41+
RookBlockedByKingModifier: 24,
42+
}
1943
weightsForAllPhases = GamephaseWeights{
2044
Material: map[dragontoothmg.Piece]int{
2145
dragontoothmg.Pawn: 100,
@@ -130,8 +154,9 @@ func flipPstArrayVertically(pst [64]int) [64]int {
130154
var (
131155
weights = map[game.PlayerColor]Weights{
132156
game.White: Weights{
133-
Midgame: midgameWeights,
134-
Endgame: endgameWeights,
157+
Midgame: midgameWeights,
158+
Endgame: endgameWeights,
159+
AdditionalModifier: additionalModifiers,
135160
},
136161
game.Black: Weights{
137162
Midgame: GamephaseWeights{
@@ -156,6 +181,7 @@ var (
156181
dragontoothmg.King: flipPstArrayVertically(endgameWeights.PieceSquareTables[dragontoothmg.King]),
157182
},
158183
},
184+
AdditionalModifier: additionalModifiers,
159185
},
160186
}
161187
)

search/search.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ func (s *Search) alphaBeta(ctx context.Context, depthLeft, alpha, beta int, move
9696
var cancelled bool = false
9797

9898
if depthLeft <= 0 || s.Game.Result != game.GameNotOver {
99-
score := s.quiescenceSearch(alpha, beta, int(s.MaximumDepthQuiescence)-1)
99+
// add leaf to nodecount, but do not count it in qnodecount (prevent overlap in both counts)
100+
s.SearchInfo.NodesTraversed++
101+
s.SearchInfo.QNodesTraversed--
102+
score := s.quiescenceSearch(alpha, beta, int(s.MaximumDepthQuiescence))
100103
if s.Game.Result == game.BlackWon || s.Game.Result == game.WhiteWon {
101104
if score > 0 {
102105
score -= int(s.MaximumDepthAlphaBeta) - depthLeft // game won, minimize path to victory
@@ -189,7 +192,6 @@ moveIterator:
189192
}
190193

191194
func (s *Search) quiescenceSearch(alpha, beta, depthLeft int) int {
192-
s.SearchInfo.NodesTraversed++
193195
s.SearchInfo.QNodesTraversed++
194196

195197
standPat := s.evaluationProvider.CalculateEvaluation(s.Game)
@@ -200,7 +202,7 @@ func (s *Search) quiescenceSearch(alpha, beta, depthLeft int) int {
200202
alpha = standPat
201203
}
202204

203-
if depthLeft <= 0 || s.Game.Result != game.GameNotOver {
205+
if depthLeft > 0 && s.Game.Result == game.GameNotOver {
204206
for _, move := range s.getCapturesInOrder() {
205207
s.Game.PushMove(move)
206208
score := -s.quiescenceSearch(-beta, -alpha, depthLeft-1)
@@ -243,29 +245,31 @@ func (s *Search) getCapturesInOrder() []dragontoothmg.Move {
243245
legalMoves := s.Game.Position.GenerateLegalMoves()
244246
var captures []dragontoothmg.Move = make([]dragontoothmg.Move, 0, len(legalMoves))
245247

246-
bitboards := s.Game.Position.White
248+
bitboardsOwn := s.Game.Position.White
249+
bitboardsOpponent := s.Game.Position.Black
247250
if !s.Game.Position.Wtomove {
248-
bitboards = s.Game.Position.Black
251+
bitboardsOwn = s.Game.Position.Black
252+
bitboardsOpponent = s.Game.Position.White
249253
}
250254

251255
for _, move := range legalMoves {
252-
if bitboards.All&(1<<move.To()) > 0 {
256+
if bitboardsOpponent.All&(1<<move.To()) > 0 {
253257
captures = append(captures, move)
254258
}
255259
}
256260

257261
sort.Slice(captures, func(i, j int) bool {
258-
return s.getCaptureMVVLVA(captures[i], bitboards) < s.getCaptureMVVLVA(captures[j], bitboards)
262+
return s.getCaptureMVVLVA(captures[i], bitboardsOwn, bitboardsOpponent) < s.getCaptureMVVLVA(captures[j], bitboardsOwn, bitboardsOpponent)
259263
})
260264

261265
return captures
262266
}
263267

264-
func (s *Search) getCaptureMVVLVA(move dragontoothmg.Move, bitboards dragontoothmg.Bitboards) (score int) {
265-
pieceTypeFrom, _ := s.getPieceTypeAtPosition(move.From(), bitboards)
266-
pieceTypeTo, _ := s.getPieceTypeAtPosition(move.To(), bitboards)
268+
func (s *Search) getCaptureMVVLVA(move dragontoothmg.Move, bitboardsOwn dragontoothmg.Bitboards, bitboardsOpponent dragontoothmg.Bitboards) (score int) {
269+
pieceTypeFrom, _ := s.getPieceTypeAtPosition(move.From(), bitboardsOwn)
270+
pieceTypeTo, _ := s.getPieceTypeAtPosition(move.To(), bitboardsOpponent)
267271

268-
return s.evaluationProvider.GetPieceTypeValue(pieceTypeTo) + (10 - int(pieceTypeFrom))
272+
return (1200 - s.evaluationProvider.GetPieceTypeValue(pieceTypeTo)) + int(pieceTypeFrom)
269273
}
270274

271275
func (s *Search) getPieceTypeAtPosition(position uint8, bitboards dragontoothmg.Bitboards) (pieceType dragontoothmg.Piece, occupied bool) {

search/search_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func benchmarkSearchEvaluation(evaluator evaluation_provider.EvaluationProvider,
2525

2626
for n := 0; n < b.N; n++ {
2727
start = time.Now()
28-
searchObj := New(game.New(), logger, transpositionTable, evaluator, abdepth, 4)
28+
searchObj := New(game.New(), logger, transpositionTable, evaluator, abdepth, 1)
2929
searchObj.SearchBestMove(ctx)
3030
nps += float64(searchObj.SearchInfo.NodesTraversed) / float64(time.Since(start).Seconds())
3131
}
@@ -137,3 +137,34 @@ func TestProblematicGames(t *testing.T) {
137137
})
138138
}
139139
}
140+
141+
func TestSearch_getCapturesInOrder(t *testing.T) {
142+
type fields struct {
143+
Game *game.Game
144+
}
145+
tests := []struct {
146+
name string
147+
fields fields
148+
want []dragontoothmg.Move
149+
}{
150+
{"No Captures in Starting Position", fields{game.New()}, []dragontoothmg.Move{}},
151+
{"One Capture in Scandinavian Defense", fields{game.NewFromFen("rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")}, []dragontoothmg.Move{1827}},
152+
{"Multiple Captures on Enemy Queen", fields{game.NewFromFen("rn2kb1r/ppp1pppp/8/2bpq2n/3P4/5NQ1/PPP1PPPP/RNB1KB1R w KQkq - 0 1")}, []dragontoothmg.Move{1764, 1380, 1444, 1762, 1462}},
153+
}
154+
for _, tt := range tests {
155+
t.Run(tt.name, func(t *testing.T) {
156+
logger := log.New(os.Stdout, "", log.LstdFlags)
157+
evaluator := evaluation.Evaluation{}
158+
transpositionTable := NewTranspositionTable(1000000)
159+
s := &Search{
160+
Game: tt.fields.Game,
161+
logger: logger,
162+
evaluationProvider: &evaluator,
163+
transpositionTable: transpositionTable,
164+
}
165+
if got := s.getCapturesInOrder(); !reflect.DeepEqual(got, tt.want) {
166+
t.Errorf("Search.getCapturesInOrder() = %v, want %v", got, tt.want)
167+
}
168+
})
169+
}
170+
}

0 commit comments

Comments
 (0)