Skip to content

Commit b649546

Browse files
committed
added old alpha beta engine
1 parent b34180f commit b649546

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package old
2+
3+
import (
4+
"sort"
5+
6+
"github.com/RubikNube/GoInGo/pkg/game"
7+
)
8+
9+
// AlphaBetaEngine implements Engine using alpha-beta pruning with killer move heuristic, transposition table, and history heuristic.
10+
type AlphaBetaEngine struct {
11+
killerMoves map[int]*game.Point // depth -> killer move
12+
transpositionTable map[uint64]int // board hash -> score
13+
historyHeuristic map[game.Point]int // move -> score for ordering
14+
}
15+
16+
func NewAlphaBetaEngine() *AlphaBetaEngine {
17+
return &AlphaBetaEngine{
18+
killerMoves: make(map[int]*game.Point),
19+
transpositionTable: make(map[uint64]int),
20+
historyHeuristic: make(map[game.Point]int),
21+
}
22+
}
23+
24+
// Move in AlphaBetaEngine uses alpha-beta pruning to select the best move or pass if no beneficial move exists.
25+
func (e *AlphaBetaEngine) Move(board game.Board, player game.FieldState, ko *game.Point) *game.Point {
26+
bestScore := -1 << 30
27+
var bestMove *game.Point
28+
depth := 4 // Shallow for performance; increase for stronger player
29+
moveFound := false
30+
31+
// Ensure killerMoves map is initialized
32+
if e.killerMoves == nil {
33+
e.killerMoves = make(map[int]*game.Point)
34+
}
35+
if e.transpositionTable == nil {
36+
e.transpositionTable = make(map[uint64]int)
37+
}
38+
if e.historyHeuristic == nil {
39+
e.historyHeuristic = make(map[game.Point]int)
40+
}
41+
42+
for i := int8(0); i < 9; i++ {
43+
for j := int8(0); j < 9; j++ {
44+
if board[i][j] != game.Empty {
45+
continue
46+
}
47+
pt := game.Point{Row: i, Col: j}
48+
if ko != nil && pt.Row == ko.Row && pt.Col == ko.Col {
49+
continue
50+
}
51+
var nextBoard game.Board
52+
copy(nextBoard[:], board[:])
53+
nextBoard[pt.Row][pt.Col] = player
54+
opp := game.Black
55+
if player == game.Black {
56+
opp = game.White
57+
}
58+
for _, n := range game.Neighbors(pt) {
59+
if nextBoard[n.Row][n.Col] == opp {
60+
group, libs := game.Group(nextBoard, n)
61+
if len(libs) == 0 {
62+
for stonePt := range group {
63+
nextBoard[stonePt.Row][stonePt.Col] = game.Empty
64+
}
65+
}
66+
}
67+
}
68+
_, libs := game.Group(nextBoard, pt)
69+
if len(libs) == 0 {
70+
continue
71+
}
72+
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -1<<30, 1<<30)
73+
moveFound = true
74+
if score > bestScore {
75+
bestScore = score
76+
move := pt
77+
bestMove = &move
78+
}
79+
}
80+
}
81+
// Pass if no move found or if passing is as good or better than any move
82+
passScore := -e.alphaBeta(board, opponent(player), player, ko, depth-1, -1<<30, 1<<30)
83+
if !moveFound || passScore >= bestScore {
84+
return nil // pass
85+
}
86+
return bestMove
87+
}
88+
89+
// opponent returns the opposite FieldState (Black <-> White).
90+
func opponent(player game.FieldState) game.FieldState {
91+
if player == game.Black {
92+
return game.White
93+
}
94+
return game.Black
95+
}
96+
97+
// alphaBeta is a minimax search with alpha-beta pruning, killer move heuristic, transposition table, and history heuristic.
98+
func (e *AlphaBetaEngine) alphaBeta(board game.Board, player, opp game.FieldState, ko *game.Point, depth, alpha, beta int) int {
99+
if depth == 0 {
100+
return evaluate(board, player, opp)
101+
}
102+
foundMove := false
103+
104+
// Transposition table lookup
105+
boardHash := boardHash(board, player)
106+
if val, ok := e.transpositionTable[boardHash]; ok {
107+
return val
108+
}
109+
110+
// Null Move Pruning: try skipping a move (pass) if depth is sufficient
111+
if depth >= 2 {
112+
passScore := -e.alphaBeta(board, opp, player, ko, depth-2, -beta, -beta+1)
113+
if passScore >= beta {
114+
e.transpositionTable[boardHash] = passScore
115+
return passScore
116+
}
117+
}
118+
119+
// Try killer move first if available
120+
if killer, ok := e.killerMoves[depth]; ok && killer != nil && board[killer.Row][killer.Col] == game.Empty {
121+
pt := *killer
122+
if ko == nil || pt.Row != ko.Row || pt.Col != ko.Col {
123+
var nextBoard game.Board
124+
copy(nextBoard[:], board[:])
125+
nextBoard[pt.Row][pt.Col] = player
126+
for _, n := range game.Neighbors(pt) {
127+
if nextBoard[n.Row][n.Col] == opp {
128+
group, libs := game.Group(nextBoard, n)
129+
if len(libs) == 0 {
130+
for stonePt := range group {
131+
nextBoard[stonePt.Row][stonePt.Col] = game.Empty
132+
}
133+
}
134+
}
135+
}
136+
_, libs := game.Group(nextBoard, pt)
137+
if len(libs) != 0 {
138+
foundMove = true
139+
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -beta, -alpha)
140+
// History heuristic update
141+
e.historyHeuristic[pt] += 1 << uint(depth)
142+
if score > alpha {
143+
alpha = score
144+
// Update killer move if this move caused a beta cutoff
145+
if alpha >= beta {
146+
e.killerMoves[depth] = &pt
147+
e.transpositionTable[boardHash] = alpha
148+
return alpha
149+
}
150+
}
151+
}
152+
}
153+
}
154+
155+
for _, pt := range e.orderedMoves(board, player, depth) {
156+
if board[pt.Row][pt.Col] != game.Empty {
157+
continue
158+
}
159+
if ko != nil && pt.Row == ko.Row && pt.Col == ko.Col {
160+
continue
161+
}
162+
// Skip killer move (already tried)
163+
if killer, ok := e.killerMoves[depth]; ok && killer != nil && pt.Row == killer.Row && pt.Col == killer.Col {
164+
continue
165+
}
166+
var nextBoard game.Board
167+
copy(nextBoard[:], board[:])
168+
nextBoard[pt.Row][pt.Col] = player
169+
for _, n := range game.Neighbors(pt) {
170+
if nextBoard[n.Row][n.Col] == opp {
171+
group, libs := game.Group(nextBoard, n)
172+
if len(libs) == 0 {
173+
for stonePt := range group {
174+
nextBoard[stonePt.Row][stonePt.Col] = game.Empty
175+
}
176+
}
177+
}
178+
}
179+
_, libs := game.Group(nextBoard, pt)
180+
if len(libs) == 0 {
181+
continue
182+
}
183+
foundMove = true
184+
score := -e.alphaBeta(nextBoard, opp, player, ko, depth-1, -beta, -alpha)
185+
// History heuristic update
186+
e.historyHeuristic[pt] += 1 << uint(depth)
187+
if score > alpha {
188+
alpha = score
189+
// Update killer move if this move caused a beta cutoff
190+
if alpha >= beta {
191+
move := pt
192+
e.killerMoves[depth] = &move
193+
e.transpositionTable[boardHash] = alpha
194+
return alpha
195+
}
196+
}
197+
}
198+
// Consider passing if no move found or passing is better
199+
passScore := -e.alphaBeta(board, opp, player, ko, depth-1, -beta, -alpha)
200+
if !foundMove || passScore > alpha {
201+
alpha = passScore
202+
}
203+
e.transpositionTable[boardHash] = alpha
204+
return alpha
205+
}
206+
207+
// orderedMoves returns a list of all empty points, ordered by killer move, history heuristic, proximity, and capture potential.
208+
func (e *AlphaBetaEngine) orderedMoves(board game.Board, player game.FieldState, depth int) []game.Point {
209+
type moveScore struct {
210+
pt game.Point
211+
score int
212+
}
213+
var moves []moveScore
214+
killer, hasKiller := e.killerMoves[depth]
215+
for i := int8(0); i < 9; i++ {
216+
for j := int8(0); j < 9; j++ {
217+
if board[i][j] != game.Empty {
218+
continue
219+
}
220+
pt := game.Point{Row: i, Col: j}
221+
score := 0
222+
// Killer move gets highest priority
223+
if hasKiller && killer != nil && pt.Row == killer.Row && pt.Col == killer.Col {
224+
score += 10000
225+
}
226+
// History heuristic
227+
score += e.historyHeuristic[pt] * 10
228+
// Proximity: +1 for each neighbor that is not empty
229+
for _, n := range game.Neighbors(pt) {
230+
if board[n.Row][n.Col] != game.Empty {
231+
score += 2
232+
}
233+
}
234+
// Capture potential: +5 for each neighbor group with 1 liberty
235+
opp := game.Black
236+
if player == game.Black {
237+
opp = game.White
238+
}
239+
for _, n := range game.Neighbors(pt) {
240+
if board[n.Row][n.Col] == opp {
241+
_, libs := game.Group(board, n)
242+
if len(libs) == 1 {
243+
score += 5
244+
}
245+
}
246+
}
247+
moves = append(moves, moveScore{pt, score})
248+
}
249+
}
250+
// Sort moves by descending score
251+
sort.Slice(moves, func(i, j int) bool {
252+
return moves[i].score > moves[j].score
253+
})
254+
result := make([]game.Point, len(moves))
255+
for i, m := range moves {
256+
result[i] = m.pt
257+
}
258+
return result
259+
}
260+
261+
// evaluate is a sophisticated evaluation function considering liberties, groups, and captures.
262+
func evaluate(board game.Board, player, opp game.FieldState) int {
263+
playerStones, oppStones := 0, 0
264+
playerLibs, oppLibs := 0, 0
265+
playerGroups, oppGroups := 0, 0
266+
playerCapturable, oppCapturable := 0, 0
267+
268+
visited := make(map[game.Point]bool)
269+
for i := 0; i < 9; i++ {
270+
for j := 0; j < 9; j++ {
271+
pt := game.Point{Row: int8(i), Col: int8(j)}
272+
if visited[pt] || board[i][j] == game.Empty {
273+
continue
274+
}
275+
group, libs := game.Group(board, pt)
276+
for stone := range group {
277+
visited[stone] = true
278+
}
279+
if board[i][j] == player {
280+
playerStones += len(group)
281+
playerLibs += len(libs)
282+
playerGroups++
283+
if len(libs) == 1 {
284+
playerCapturable += len(group)
285+
}
286+
} else if board[i][j] == opp {
287+
oppStones += len(group)
288+
oppLibs += len(libs)
289+
oppGroups++
290+
if len(libs) == 1 {
291+
oppCapturable += len(group)
292+
}
293+
}
294+
}
295+
}
296+
// Weighted sum: stones, liberties, groups, capturability
297+
return (playerStones-oppStones)*10 +
298+
(playerLibs-oppLibs)*2 +
299+
(oppCapturable-playerCapturable)*8 +
300+
(playerGroups - oppGroups)
301+
}
302+
303+
// boardHash returns a simple hash for the board and player.
304+
// You may want to replace this with Zobrist hashing for better collision resistance.
305+
func boardHash(board game.Board, player game.FieldState) uint64 {
306+
var h uint64
307+
for i := 0; i < 9; i++ {
308+
for j := 0; j < 9; j++ {
309+
h = h*3 + uint64(board[i][j])
310+
}
311+
}
312+
h = h*3 + uint64(player)
313+
return h
314+
}

0 commit comments

Comments
 (0)