Skip to content

Commit 92fd28e

Browse files
committed
performance optimazations by better killer move heuristic, null move skip and ordering of moves
1 parent f03f9be commit 92fd28e

File tree

1 file changed

+94
-37
lines changed

1 file changed

+94
-37
lines changed

cmd/engine/alpha_beta_engine.go

Lines changed: 94 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package engine
22

3-
import "github.com/RubikNube/GoInGo/cmd/game"
3+
import (
4+
"sort"
5+
6+
"github.com/RubikNube/GoInGo/cmd/game"
7+
)
48

59
// AlphaBetaEngine implements Engine using alpha-beta pruning with killer move heuristic.
610
type AlphaBetaEngine struct {
@@ -85,6 +89,14 @@ func (e *AlphaBetaEngine) alphaBeta(board game.Board, player, opp game.FieldStat
8589
}
8690
foundMove := false
8791

92+
// Null Move Pruning: try skipping a move (pass) if depth is sufficient
93+
if depth >= 2 {
94+
passScore := -e.alphaBeta(board, opp, player, ko, depth-2, -beta, -beta+1)
95+
if passScore >= beta {
96+
return passScore
97+
}
98+
}
99+
88100
// Try killer move first if available
89101
if killer, ok := e.killerMoves[depth]; ok && killer != nil && board[killer.Row][killer.Col] == game.Empty {
90102
pt := *killer
@@ -118,55 +130,100 @@ func (e *AlphaBetaEngine) alphaBeta(board game.Board, player, opp game.FieldStat
118130
}
119131
}
120132

121-
for i := 0; i < 9; i++ {
122-
for j := 0; j < 9; j++ {
123-
if board[i][j] != game.Empty {
124-
continue
133+
for _, pt := range orderedMoves(board, player) {
134+
if board[pt.Row][pt.Col] != game.Empty {
135+
continue
136+
}
137+
if ko != nil && pt.Row == ko.Row && pt.Col == ko.Col {
138+
continue
139+
}
140+
// Skip killer move (already tried)
141+
if killer, ok := e.killerMoves[depth]; ok && killer != nil && pt.Row == killer.Row && pt.Col == killer.Col {
142+
continue
143+
}
144+
var nextBoard game.Board
145+
copy(nextBoard[:], board[:])
146+
nextBoard[pt.Row][pt.Col] = player
147+
for _, n := range game.Neighbors(pt) {
148+
if nextBoard[n.Row][n.Col] == opp {
149+
group, libs := game.Group(nextBoard, n)
150+
if len(libs) == 0 {
151+
for stonePt := range group {
152+
nextBoard[stonePt.Row][stonePt.Col] = game.Empty
153+
}
154+
}
125155
}
126-
pt := game.Point{Row: int8(i), Col: int8(j)}
127-
if ko != nil && pt.Row == ko.Row && pt.Col == ko.Col {
128-
continue
156+
}
157+
_, libs := game.Group(nextBoard, pt)
158+
if len(libs) == 0 {
159+
continue
160+
}
161+
foundMove = true
162+
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -beta, -alpha)
163+
if score > alpha {
164+
alpha = score
165+
// Update killer move if this move caused a beta cutoff
166+
if alpha >= beta {
167+
move := pt
168+
e.killerMoves[depth] = &move
169+
return alpha
129170
}
130-
// Skip killer move (already tried)
131-
if killer, ok := e.killerMoves[depth]; ok && killer != nil && pt.Row == killer.Row && pt.Col == killer.Col {
171+
}
172+
}
173+
// Consider passing if no move found or passing is better
174+
passScore := -e.alphaBeta(board, opp, player, ko, depth-1, -beta, -alpha)
175+
if !foundMove || passScore > alpha {
176+
alpha = passScore
177+
}
178+
return alpha
179+
}
180+
181+
// orderedMoves returns a list of all empty points, ordered by proximity to existing stones and capture potential.
182+
func orderedMoves(board game.Board, player game.FieldState) []game.Point {
183+
type moveScore struct {
184+
pt game.Point
185+
score int
186+
}
187+
var moves []moveScore
188+
// Find all empty points and score them
189+
for i := int8(0); i < 9; i++ {
190+
for j := int8(0); j < 9; j++ {
191+
if board[i][j] != game.Empty {
132192
continue
133193
}
134-
var nextBoard game.Board
135-
copy(nextBoard[:], board[:])
136-
nextBoard[pt.Row][pt.Col] = player
194+
pt := game.Point{Row: i, Col: j}
195+
score := 0
196+
// Proximity: +1 for each neighbor that is not empty
137197
for _, n := range game.Neighbors(pt) {
138-
if nextBoard[n.Row][n.Col] == opp {
139-
group, libs := game.Group(nextBoard, n)
140-
if len(libs) == 0 {
141-
for stonePt := range group {
142-
nextBoard[stonePt.Row][stonePt.Col] = game.Empty
143-
}
144-
}
198+
if board[n.Row][n.Col] != game.Empty {
199+
score += 2
145200
}
146201
}
147-
_, libs := game.Group(nextBoard, pt)
148-
if len(libs) == 0 {
149-
continue
202+
// Capture potential: +5 for each neighbor group with 1 liberty
203+
opp := game.Black
204+
if player == game.Black {
205+
opp = game.White
150206
}
151-
foundMove = true
152-
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -beta, -alpha)
153-
if score > alpha {
154-
alpha = score
155-
// Update killer move if this move caused a beta cutoff
156-
if alpha >= beta {
157-
move := pt
158-
e.killerMoves[depth] = &move
159-
return alpha
207+
for _, n := range game.Neighbors(pt) {
208+
if board[n.Row][n.Col] == opp {
209+
_, libs := game.Group(board, n)
210+
if len(libs) == 1 {
211+
score += 5
212+
}
160213
}
161214
}
215+
moves = append(moves, moveScore{pt, score})
162216
}
163217
}
164-
// Consider passing if no move found or passing is better
165-
passScore := -e.alphaBeta(board, opp, player, ko, depth-1, -beta, -alpha)
166-
if !foundMove || passScore > alpha {
167-
alpha = passScore
218+
// Sort moves by descending score
219+
sort.Slice(moves, func(i, j int) bool {
220+
return moves[i].score > moves[j].score
221+
})
222+
result := make([]game.Point, len(moves))
223+
for i, m := range moves {
224+
result[i] = m.pt
168225
}
169-
return alpha
226+
return result
170227
}
171228

172229
// evaluate is a sophisticated evaluation function considering liberties, groups, and captures.

0 commit comments

Comments
 (0)