-
Notifications
You must be signed in to change notification settings - Fork 0
/
minimax.go
341 lines (303 loc) · 7.73 KB
/
minimax.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
package ghess
import (
"errors"
"fmt"
"strings"
)
// Principal Variation Search
var pvHash map[[120]byte]int = make(map[[120]byte]int)
var pvMap map[Board][2]int = make(map[Board][2]int)
/*
MiniMax implementation ###########################################
*/
// State struct holds a board position,
// the move that got there, and the evaluation.
// Init is the move which began a certain branch of the tree.
type State struct {
board *Board // the Board object
eval int // score
Init [2]int // the moves which got to that position at root
isMax bool // is White Player
alpha int
beta int
parent *State
}
// String returns some basic info of a State.
func (s State) String() string {
return fmt.Sprintf("\nScore: %d\nFrom Move: %d, %d", s.eval, s.Init[0], s.Init[1])
}
// States are a slice of State structs.
type States []State
// Sort functionality depricated.
func (s States) Len() int { return len(s) }
func (s States) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s States) Less(i, j int) bool { return s[i].eval < s[j].eval }
// GetState turns a Board into a copy and it's state.
// The Init value is nil.
func GetState(b *Board) State {
c := *b // dereference the pointer
s := State{board: &c, eval: c.Evaluate()}
return s
}
// TryState takes in a *Board and valid move and returns
// a State struct.
func TryState(b *Board, o, d int) (State, error) {
state := State{}
possible := CopyBoard(b)
err := possible.Move(o, d)
if err != nil {
//fmt.Println(err, o, d)
//fmt.Println(b.StringWhite())
//fmt.Println(b.toMove)
return state, err
}
state.board = possible
state.eval = possible.Evaluate()
return state, nil
}
// GetPossibleStates returns a slice of State structs
// Each with a score and the move that got there.
func GetPossibleStates(state State) (States, error) {
states := make(States, 0)
origs, dests := state.board.SearchValid()
for i := 0; i < len(origs); i++ {
s, err := TryState(state.board, origs[i], dests[i])
if err != nil {
return states, err
}
if state.Init[0] == 0 {
s.Init[0], s.Init[1] = origs[i], dests[i]
} else {
s.Init[0], s.Init[1] =
state.Init[0], state.Init[1]
}
s.isMax = state.isMax // Basically is White
// Add parent state?
states = append(states, s)
}
return states, nil
}
// DictionaryAttack looks up common openings
// for less stupid opening moves.
func DictionaryAttack(s State) (State, error) {
position := s.board.Position()
// I don't need castling empassant or move number
posits := strings.Split(position, " ")
key := posits[0] + " " + posits[1]
// Check if opening exists
if val, ok := dict[key]; ok {
state := State{Init: val}
return state, nil
}
return s, errors.New("No Dictionary Attack Found")
}
// The Max and Mini functions are O(n)
// Max returns the state from States with
// highest evaluation.
func Max(states States) State {
var maxIdx int
var maxVal int = -1000
for idx, state := range states {
if state.eval > maxVal {
maxVal = state.eval
maxIdx = idx
}
}
return states[maxIdx]
}
// Min returns the state from States with
// Lowest evaluation.
func Min(states States) State {
var minIdx int
var minVal int = 1000
for idx, state := range states {
if state.eval < minVal {
minVal = state.eval
minIdx = idx
}
}
return states[minIdx]
}
// MiniMax with Alpha Beta Pruning:
// Return the position where, assuming your opponent picks its
// *best* moves, they have the minimum advantage
// Minimizing Maximum Loss
//
// Param:
// state, current depth and terminal depth.
// Depth is always 0 when passed in initially.
//
// Minimax:
// This is like a Depth First Search algorithm.
// Speed increase from Pruning largely depends on Move Ordering
func MiniMaxPruning(depth, terminal int, s State) (State, error) {
if depth == 0 {
// At first depth set Alpha and Beta values
s.alpha = -1000000000
s.beta = 1000000000
// Search for Max or Min Player?
if s.board.toMove == "w" {
s.isMax = true
} else {
s.isMax = false
}
// At first depth check for Opening in Dictionary
openState, err := DictionaryAttack(s)
if err == nil {
return openState, nil
}
}
if depth == terminal {
// The final state will pass up the
// call stack:
// the Score of terminal position
// the Original move to get there
return s, nil
}
// Determine if Max or Min node
// by height of tree
even := (depth % 2) == 0
maxNode := even == s.isMax
states, err := GetPossibleStates(s)
if err != nil {
return s, err
}
// Recursively call MiniMax on all Possible States
var bestState State
var bestStates States
for _, state := range states {
state.alpha = s.alpha
state.beta = s.beta
// Increment Depth when calling MiniMax
bestState, err = MiniMaxPruning(depth+1, terminal, state)
if err != nil {
return bestState, err
}
/* Alpha Beta Pruning
* The trick is to update the root node's
* (for this branch) beta or alpha
* This will prune further branches from root
* If we are considering a Max Node,
* and state's score/evaluation >= beta, then PRUNE/return
* otherwise, set alpha = Max(alpha, state's value)
* If we are considering a Min Node,
* and state's score/evaluation <= alpha, then PRUNE/return
* otherwise, set beta = Min(beta, state's value)
* Pruning effectively breaks out of this loop
* which call MiniMax on all Possible States
* in a certain branch of the tree
*/
if maxNode {
if bestState.eval > s.beta {
return bestState, nil
} else {
bestState.beta = s.beta
s.alpha = max(s.alpha, bestState.eval)
}
}
if !maxNode {
if bestState.eval < s.alpha {
return bestState, nil
} else {
bestState.alpha = s.alpha
s.beta = min(s.beta, bestState.eval)
}
}
// if there is no Pruning, add the returned
// bestStates from MiniMax calls to slice of bestStates
bestStates = append(bestStates, bestState)
}
// If say there is stalemate/checkmate no best-states
if len(bestStates) < 1 {
return s, nil
}
if maxNode { // if height == Max nodes
return Max(bestStates), nil
} else { // if height == Min nodes
return Min(bestStates), nil
}
}
// small min, doesn't take state,
// but it takes numbers
func min(a, b int) int {
if a < b {
return a
} else {
return b
}
}
func max(a, b int) int {
if a > b {
return a
} else {
return b
}
}
// DEPRECATED
// MiniMax is Deprecated in favor of
// AlphaBeta Pruning Minimax
// Use for Testing
func MiniMax(depth, terminal int, s State) (State, error) {
if depth == 0 {
// set the Min or Max
if s.board.toMove == "w" {
s.isMax = true
} else {
s.isMax = false
}
openState, err := DictionaryAttack(s)
if err == nil {
return openState, nil
}
}
if depth == terminal { // that is, 2 ply
//fmt.Println("Depth ", depth, s)
return s, nil
}
// Determine which node this is
// TODO: Why is this so complicated?
// Because when minimax from perspective black,
// things are totally different
even := (depth % 2) == 0
var maxNode bool
if even {
// If White Player Return Maximum
if s.isMax {
maxNode = true
//return Max(bestStates), nil
} else {
maxNode = false
//return Min(bestStates), nil
}
} else { // Otherwise Return Minimum... Yup that's the idea.
if s.isMax {
maxNode = false
//return Min(bestStates), nil
} else {
maxNode = true
//return Max(bestStates), nil
}
}
states, err := GetPossibleStates(s)
if err != nil {
return s, err
}
// Recursive Call
var bestState State
var bestStates States
for _, state := range states {
bestState, err = MiniMax(depth+1, terminal, state)
if err != nil {
return bestState, err
}
bestStates = append(bestStates, bestState)
}
if len(bestStates) < 1 {
return s, nil
}
if maxNode {
return Max(bestStates), nil
} else {
return Min(bestStates), nil
}
}