Skip to content

Commit baf5662

Browse files
committed
add killer move heuristics to engine
1 parent 3530d48 commit baf5662

File tree

2 files changed

+60
-13
lines changed

2 files changed

+60
-13
lines changed

cmd/engine/alpha_beta_engine.go

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ package engine
22

33
import "github.com/RubikNube/GoInGo/cmd/game"
44

5-
// AlphaBetaEngine implements Engine using an evaluation function and alpha-beta pruning.
6-
type AlphaBetaEngine struct{}
5+
// AlphaBetaEngine implements Engine using alpha-beta pruning with killer move heuristic.
6+
type AlphaBetaEngine struct {
7+
killerMoves map[int]*game.Point // depth -> killer move
8+
}
9+
10+
func NewAlphaBetaEngine() *AlphaBetaEngine {
11+
return &AlphaBetaEngine{killerMoves: make(map[int]*game.Point)}
12+
}
713

814
// Move in AlphaBetaEngine uses alpha-beta pruning to select the best move or pass if no beneficial move exists.
915
func (e *AlphaBetaEngine) Move(board game.Board, player game.FieldState, ko *game.Point) *game.Point {
1016
bestScore := -1 << 30
1117
var bestMove *game.Point
12-
depth := 3 // Shallow for performance; increase for stronger play
18+
depth := 4 // Shallow for performance; increase for stronger play
1319
moveFound := false
1420

1521
for i := 0; i < 9; i++ {
@@ -42,7 +48,7 @@ func (e *AlphaBetaEngine) Move(board game.Board, player game.FieldState, ko *gam
4248
if len(libs) == 0 {
4349
continue
4450
}
45-
score := -alphaBeta(nextBoard, opp, player, ko, depth-1, -1<<30, 1<<30)
51+
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -1<<30, 1<<30)
4652
moveFound = true
4753
if score > bestScore {
4854
bestScore = score
@@ -52,7 +58,7 @@ func (e *AlphaBetaEngine) Move(board game.Board, player game.FieldState, ko *gam
5258
}
5359
}
5460
// Pass if no move found or if passing is as good or better than any move
55-
passScore := -alphaBeta(board, opponent(player), player, ko, depth-1, -1<<30, 1<<30)
61+
passScore := -e.alphaBeta(board, opponent(player), player, ko, depth-1, -1<<30, 1<<30)
5662
if !moveFound || passScore >= bestScore {
5763
return nil // pass
5864
}
@@ -67,12 +73,46 @@ func opponent(player game.FieldState) game.FieldState {
6773
return game.Black
6874
}
6975

70-
// alphaBeta is a minimax search with alpha-beta pruning and pass support.
71-
func alphaBeta(board game.Board, player, opp game.FieldState, ko *game.Point, depth, alpha, beta int) int {
76+
// alphaBeta is a minimax search with alpha-beta pruning and killer move heuristic.
77+
func (e *AlphaBetaEngine) alphaBeta(board game.Board, player, opp game.FieldState, ko *game.Point, depth, alpha, beta int) int {
7278
if depth == 0 {
7379
return evaluate(board, player, opp)
7480
}
7581
foundMove := false
82+
83+
// Try killer move first if available
84+
if killer, ok := e.killerMoves[depth]; ok && killer != nil && board[killer.Row][killer.Col] == game.Empty {
85+
pt := *killer
86+
if ko == nil || pt.Row != ko.Row || pt.Col != ko.Col {
87+
var nextBoard game.Board
88+
copy(nextBoard[:], board[:])
89+
nextBoard[pt.Row][pt.Col] = player
90+
for _, n := range game.Neighbors(pt) {
91+
if nextBoard[n.Row][n.Col] == opp {
92+
group, libs := game.Group(nextBoard, n)
93+
if len(libs) == 0 {
94+
for stonePt := range group {
95+
nextBoard[stonePt.Row][stonePt.Col] = game.Empty
96+
}
97+
}
98+
}
99+
}
100+
_, libs := game.Group(nextBoard, pt)
101+
if len(libs) != 0 {
102+
foundMove = true
103+
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -beta, -alpha)
104+
if score > alpha {
105+
alpha = score
106+
// Update killer move if this move caused a beta cutoff
107+
if alpha >= beta {
108+
e.killerMoves[depth] = &pt
109+
return alpha
110+
}
111+
}
112+
}
113+
}
114+
}
115+
76116
for i := 0; i < 9; i++ {
77117
for j := 0; j < 9; j++ {
78118
if board[i][j] != game.Empty {
@@ -82,6 +122,10 @@ func alphaBeta(board game.Board, player, opp game.FieldState, ko *game.Point, de
82122
if ko != nil && pt.Row == ko.Row && pt.Col == ko.Col {
83123
continue
84124
}
125+
// Skip killer move (already tried)
126+
if killer, ok := e.killerMoves[depth]; ok && killer != nil && pt.Row == killer.Row && pt.Col == killer.Col {
127+
continue
128+
}
85129
var nextBoard game.Board
86130
copy(nextBoard[:], board[:])
87131
nextBoard[pt.Row][pt.Col] = player
@@ -100,17 +144,20 @@ func alphaBeta(board game.Board, player, opp game.FieldState, ko *game.Point, de
100144
continue
101145
}
102146
foundMove = true
103-
score := -alphaBeta(nextBoard, opp, player, ko, depth-1, -beta, -alpha)
147+
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -beta, -alpha)
104148
if score > alpha {
105149
alpha = score
106-
}
107-
if alpha >= beta {
108-
return alpha
150+
// Update killer move if this move caused a beta cutoff
151+
if alpha >= beta {
152+
move := pt
153+
e.killerMoves[depth] = &move
154+
return alpha
155+
}
109156
}
110157
}
111158
}
112159
// Consider passing if no move found or passing is better
113-
passScore := -alphaBeta(board, opp, player, ko, depth-1, -beta, -alpha)
160+
passScore := -e.alphaBeta(board, opp, player, ko, depth-1, -beta, -alpha)
114161
if !foundMove || passScore > alpha {
115162
alpha = passScore
116163
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ func main() {
363363
keybindings = cfg.Keybindings
364364

365365
// selectedEngine = &engine.RandomEngine{}
366-
selectedEngine = &engine.AlphaBetaEngine{}
366+
selectedEngine = engine.NewAlphaBetaEngine()
367367
engineEnabled = true // Enable engine by default
368368

369369
defer g.Close()

0 commit comments

Comments
 (0)