@@ -192,19 +192,14 @@ enum Piece: String {
|
192 | 192 | }
|
193 | 193 | }
|
194 | 194 |
|
195 |
| -// a move is an integer 0-9 indicating a place to put a piece |
| 195 | +// a move is an integer 0-8 indicating a place to put a piece |
196 | 196 | typealias Move = Int
|
197 | 197 |
|
198 | 198 | struct Board {
|
199 | 199 | let position: [Piece]
|
200 | 200 | let turn: Piece
|
201 | 201 | let lastMove: Move
|
202 | 202 |
|
203 |
| -// the legal moves in a position are all of the empty squares |
204 |
| -var legalMoves: [Move] { |
205 |
| -return position.indices.filter { position[$0] == .E } |
206 |
| -} |
207 |
| - |
208 | 203 | // by default the board is empty and X goes first
|
209 | 204 | // lastMove being -1 is a marker of a start position
|
210 | 205 | init(position: [Piece] = [.E, .E, .E, .E, .E, .E, .E, .E, .E], turn: Piece = .X, lastMove: Int = -1) {
|
@@ -221,16 +216,21 @@ struct Board {
|
221 | 216 | return Board(position: tempPosition, turn: turn.opposite, lastMove: location)
|
222 | 217 | }
|
223 | 218 |
|
| 219 | +// the legal moves in a position are all of the empty squares |
| 220 | +var legalMoves: [Move] { |
| 221 | +return position.indices.filter { position[$0] == .E } |
| 222 | +} |
| 223 | + |
224 | 224 | var isWin: Bool {
|
225 | 225 | return
|
226 |
| -position[0] == position[1] && position [0] == position[2] && position[0] != .E || // row 0 |
227 |
| -position[3] == position[4] && position[3] == position [5] && position[3] != .E || // row 1 |
228 |
| -position[6] == position[7] && position[6] == position[8] && position[6] != .E || // row 2 |
229 |
| -position[0] == position[3] && position[0] == position[6] && position[0] != .E || // col 0 |
230 |
| -position[1] == position[4] && position[1] == position[7] && position[1] != .E || // col 1 |
231 |
| -position[2] == position[5] && position[2] == position[8] && position[2] != .E || // col 2 |
232 |
| -position[0] == position[4] && position[0] == position[8] && position[0] != .E || // diag 0 |
233 |
| -position[2] == position[4] && position[2] == position[6] && position[2] != .E // diag 1 |
| 226 | +position[0] == position[1] && position[0] == position[2] && position[0] != .E || // row 0 |
| 227 | +position[3] == position[4] && position[3] == position[5] && position[3] != .E || // row 1 |
| 228 | +position[6] == position[7] && position[6] == position[8] && position[6] != .E || // row 2 |
| 229 | +position[0] == position[3] && position[0] == position[6] && position[0] != .E || // col 0 |
| 230 | +position[1] == position[4] && position[1] == position[7] && position[1] != .E || // col 1 |
| 231 | +position[2] == position[5] && position[2] == position[8] && position[2] != .E || // col 2 |
| 232 | +position[0] == position[4] && position[0] == position[8] && position[0] != .E || // diag 0 |
| 233 | +position[2] == position[4] && position[2] == position[6] && position[2] != .E // diag 1 |
234 | 234 |
|
235 | 235 | }
|
236 | 236 |
|
@@ -239,49 +239,67 @@ struct Board {
|
239 | 239 | }
|
240 | 240 | }
|
241 | 241 |
|
242 |
| -func minimax(_ board: Board, maximizing: Bool) -> (eval: Int, bestMove: Move) { |
243 |
| -if board.isWin { return (1, board.lastMove) } |
244 |
| -else if board.isDraw { return (0, board.lastMove) } |
| 242 | +// Find the best possible outcome for originalPlayer |
| 243 | +func minimax(_ board: Board, maximizing: Bool, originalPlayer: Piece) -> Int { |
| 244 | +// Base case — evaluate the position if it is a win or a draw |
| 245 | +if board.isWin && originalPlayer == board.turn.opposite { return 1 } // win |
| 246 | +else if board.isWin && originalPlayer != board.turn.opposite { return -1 } // loss |
| 247 | +else if board.isDraw { return 0 } // draw |
245 | 248 |
|
| 249 | +// Recursive case — maximize your gains or minimize the opponent's gains |
246 | 250 | if maximizing {
|
247 |
| -var bestEval: (eval: Int, bestMove: Move) = (Int.min, -1) |
248 |
| -for move in board.legalMoves { |
249 |
| -let result = minimax(board.move(move), maximizing: false) |
250 |
| -if result.eval > bestEval.eval { bestEval = result } |
| 251 | +var bestEval = Int.min |
| 252 | +for move in board.legalMoves { // find the move with the highest evaluation |
| 253 | +let result = minimax(board.move(move), maximizing: false, originalPlayer: originalPlayer) |
| 254 | +bestEval = max(result, bestEval) |
251 | 255 | }
|
252 | 256 | return bestEval
|
253 | 257 | } else { // minimizing
|
254 |
| -var worstEval: (eval: Int, bestMove: Move) = (Int.max, -1) |
| 258 | +var worstEval = Int.max |
255 | 259 | for move in board.legalMoves {
|
256 |
| -let result = minimax(board.move(move), maximizing: true) |
257 |
| -if result.eval < worstEval.eval { worstEval = result } |
| 260 | +let result = minimax(board.move(move), maximizing: true, originalPlayer: originalPlayer) |
| 261 | +worstEval = min(result, worstEval) |
258 | 262 | }
|
259 | 263 | return worstEval
|
260 | 264 | }
|
261 | 265 | }
|
262 | 266 |
|
| 267 | +// Run minimax on every possible move to find the best one |
| 268 | +func findBestMove(_ board: Board) -> Move { |
| 269 | +var bestEval = Int.min |
| 270 | +var bestMove = -1 |
| 271 | +for move in board.legalMoves { |
| 272 | +let result = minimax(board.move(move), maximizing: false, originalPlayer: board.turn) |
| 273 | +if result > bestEval { |
| 274 | +bestEval = result |
| 275 | +bestMove = move |
| 276 | +} |
| 277 | +} |
| 278 | +return bestMove |
| 279 | +} |
| 280 | + |
263 | 281 | // win in 1 move
|
264 | 282 | let toWinEasyPosition: [Piece] = [.X, .O, .X,
|
265 | 283 | .X, .E, .O,
|
266 | 284 | .E, .E, .O]
|
267 | 285 | let testBoard1: Board = Board(position: toWinEasyPosition, turn: .X, lastMove: 8)
|
268 |
| -let answer1 = minimax(testBoard1, maximizing: true) |
269 |
| -print(answer1.bestMove) |
| 286 | +let answer1 = findBestMove(testBoard1) |
| 287 | +print(answer1) |
270 | 288 |
|
271 | 289 | // must block O's win
|
272 | 290 | let toBlockPosition: [Piece] = [.X, .E, .E,
|
273 | 291 | .E, .E, .O,
|
274 | 292 | .E, .X, .O]
|
275 | 293 | let testBoard2: Board = Board(position: toBlockPosition, turn: .X, lastMove: 8)
|
276 |
| -let answer2 = minimax(testBoard2, maximizing: true) |
277 |
| -print(answer2.bestMove) |
| 294 | +let answer2 = findBestMove(testBoard2) |
| 295 | +print(answer2) |
278 | 296 |
|
279 | 297 | // find the best move to win in 2 moves
|
280 | 298 | let toWinHardPosition: [Piece] = [.X, .E, .E,
|
281 | 299 | .E, .E, .O,
|
282 | 300 | .O, .X, .E]
|
283 | 301 | let testBoard3: Board = Board(position: toWinHardPosition, turn: .X, lastMove: 6)
|
284 |
| -let answer3 = minimax(testBoard3, maximizing: true) |
285 |
| -print(answer3.bestMove) |
| 302 | +let answer3 = findBestMove(testBoard3) |
| 303 | +print(answer3) |
286 | 304 | //: [Next](@next)
|
287 | 305 |
|
0 commit comments