├── README.md ├── tic_tac_toe.ipynb └── tictactoe_minimax_helper.py /README.md: -------------------------------------------------------------------------------- 1 | # Tic Tac Toe Python Notebook game 2 | Example of minimax algorithm applied to tic tac toe. 3 | -------------------------------------------------------------------------------- /tic_tac_toe.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 11, 6 | "metadata": { 7 | "collapsed": true, 8 | "deletable": true, 9 | "editable": true 10 | }, 11 | "outputs": [], 12 | "source": [ 13 | "from ipywidgets import widgets, HBox, VBox, Layout\n", 14 | "from IPython.display import display\n", 15 | "from functools import partial\n", 16 | "import numpy as np\n", 17 | "#import tictactoe\n", 18 | "import tictactoe_minimax_helper as minimax_helper" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "# tic tac toe game using minimax algorithm" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": { 31 | "deletable": true, 32 | "editable": true 33 | }, 34 | "source": [ 35 | "References:\n", 36 | "http://neverstopbuilding.com/minimax" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 4, 42 | "metadata": { 43 | "collapsed": false, 44 | "deletable": true, 45 | "editable": true 46 | }, 47 | "outputs": [], 48 | "source": [ 49 | "class General_functions(object):\n", 50 | " def __init__(self, matrix, actual_turn):\n", 51 | " self.N = 3\n", 52 | " self.button_list = None\n", 53 | " self.text_box = None\n", 54 | " self.matrix = matrix\n", 55 | " self.game_finished = False\n", 56 | " self.actual_turn = actual_turn\n", 57 | " \n", 58 | " def display_matrix(self):\n", 59 | " N = self.N\n", 60 | " childs = []\n", 61 | " for i in range(N):\n", 62 | " for j in range(N):\n", 63 | " if self.matrix[i,j]==1:\n", 64 | " self.button_list[i*N + j].description = 'o'\n", 65 | " if self.matrix[i,j]==-1:\n", 66 | " self.button_list[i*N + j].description = 'x'\n", 67 | "\n", 68 | " def on_button_clicked(self, index, button):\n", 69 | " N = self.N \n", 70 | "\n", 71 | " if self.game_finished:\n", 72 | " return\n", 73 | "\n", 74 | " y = index%N\n", 75 | " x = int(index/N)\n", 76 | " if self.matrix[x,y]!=0:\n", 77 | " self.text_box.value = 'No se puede ahi!'\n", 78 | " return\n", 79 | " button.description = self.actual_turn[0]\n", 80 | "\n", 81 | " if self.actual_turn == 'o':\n", 82 | " self.matrix[x,y] = 1\n", 83 | " self.game_finished, status = minimax_helper.game_over(self.matrix)\n", 84 | " if self.game_finished:\n", 85 | " if (status!=0):\n", 86 | " self.text_box.value = 'o wins'\n", 87 | " else: \n", 88 | " self.text_box.value = 'draw'\n", 89 | " else:\n", 90 | " self.actual_turn = 'x'\n", 91 | " self.text_box.value = 'Juega '+self.actual_turn\n", 92 | " else:\n", 93 | " self.matrix[x,y] = -1\n", 94 | " self.game_finished, status = minimax_helper.game_over(self.matrix)\n", 95 | " if self.game_finished:\n", 96 | " if (status!=0):\n", 97 | " self.text_box.value = 'x wins'\n", 98 | " else: \n", 99 | " self.text_box.value = 'draw'\n", 100 | " else:\n", 101 | " self.actual_turn = 'o'\n", 102 | " self.text_box.value = 'Juega '+self.actual_turn\n", 103 | " self.computer_play()\n", 104 | " \n", 105 | " def draw_board(self):\n", 106 | " self.text_box = widgets.Text(value = 'Juega '+self.actual_turn, layout=Layout(width='129px', height='40px'))\n", 107 | " self.button_list = []\n", 108 | " for i in range(9):\n", 109 | " button = widgets.Button(description='',\n", 110 | " disabled=False,\n", 111 | " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", 112 | " tooltip='Click me',\n", 113 | " icon='',\n", 114 | " layout=Layout(width='40px', height='40px'))\n", 115 | " self.button_list.append(button)\n", 116 | " button.on_click(partial(self.on_button_clicked, i))\n", 117 | " tic_tac_toe_board = VBox([HBox([self.button_list[0],self.button_list[1],self.button_list[2]]),\n", 118 | " HBox([self.button_list[3],self.button_list[4],self.button_list[5]]),\n", 119 | " HBox([self.button_list[6],self.button_list[7],self.button_list[8]])])\n", 120 | " display(VBox([self.text_box, tic_tac_toe_board]))\n", 121 | " return\n", 122 | "\n", 123 | " def computer_play(self):\n", 124 | "\n", 125 | " if self.game_finished:\n", 126 | " return\n", 127 | " \n", 128 | " if self.actual_turn=='x':\n", 129 | " turn = -1\n", 130 | " next_turn = 'o'\n", 131 | " if self.actual_turn=='o':\n", 132 | " turn = 1\n", 133 | " next_turn = 'x'\n", 134 | " self.matrix = self.get_best_play(turn)\n", 135 | " self.display_matrix()\n", 136 | " self.actual_turn = next_turn\n", 137 | " self.text_box.value = 'Juega '+self.actual_turn\n", 138 | " self.game_finished, status = minimax_helper.game_over(self.matrix)\n", 139 | " if self.game_finished:\n", 140 | " if (status!=0):\n", 141 | " self.text_box.value = 'computer wins'\n", 142 | " else: \n", 143 | " self.text_box.value = 'draw'\n", 144 | "\n", 145 | " def get_best_play(self, turn):\n", 146 | " # 1000 is an infinite value compared with the highest cost of 10 that we can get\n", 147 | "\n", 148 | " choice, points, nodes_visited = minimax_helper.minimax(self.matrix, turn)\n", 149 | " print('points:',points)\n", 150 | " print('nodes_visited:',nodes_visited)\n", 151 | " return choice" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 7, 157 | "metadata": { 158 | "collapsed": false, 159 | "deletable": true, 160 | "editable": true 161 | }, 162 | "outputs": [], 163 | "source": [ 164 | "def start_game(computer_starts = True, user_icon='x', start_mode = 'center'):\n", 165 | " matrix = np.zeros((3,3))\n", 166 | " \n", 167 | " if user_icon=='x':\n", 168 | " computer_icon_representation = 1\n", 169 | " else:\n", 170 | " computer_icon_representation = -1\n", 171 | "\n", 172 | " GF = General_functions(matrix, user_icon)\n", 173 | " GF.draw_board()\n", 174 | "\n", 175 | " if computer_starts:\n", 176 | " if start_mode == 'center':\n", 177 | " matrix[1,1] = computer_icon_representation\n", 178 | " elif start_mode == 'minimax':\n", 179 | " GF.computer_play()\n", 180 | " elif start_mode == 'random':\n", 181 | " x = np.random.randint(3)\n", 182 | " y = np.random.randint(3)\n", 183 | " matrix[x,y] = computer_icon_representation\n", 184 | "\n", 185 | " GF.display_matrix()" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 10, 191 | "metadata": { 192 | "collapsed": false 193 | }, 194 | "outputs": [ 195 | { 196 | "data": { 197 | "application/vnd.jupyter.widget-view+json": { 198 | "model_id": "524a1ed3446c4915ae69e4b58dfe986e" 199 | } 200 | }, 201 | "metadata": {}, 202 | "output_type": "display_data" 203 | }, 204 | { 205 | "name": "stdout", 206 | "output_type": "stream", 207 | "text": [ 208 | "points: 0\n", 209 | "nodes_visited: 831\n", 210 | "points: 0\n", 211 | "nodes_visited: 104\n", 212 | "points: 0\n", 213 | "nodes_visited: 11\n", 214 | "points: 9\n", 215 | "nodes_visited: 1\n" 216 | ] 217 | } 218 | ], 219 | "source": [ 220 | "# start_mode:\n", 221 | "# 'minimax': Uses minimax to select the first move\n", 222 | "# 'center': Starts on the center\n", 223 | "# 'random': Starts on a random position\n", 224 | "# user_icon:\n", 225 | "# 'x': user is x\n", 226 | "# 'o': user is o\n", 227 | "# computer_starts: True or False\n", 228 | "\n", 229 | "start_game(computer_starts = True, user_icon = 'x', start_mode = 'random')" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "metadata": { 236 | "collapsed": true, 237 | "deletable": true, 238 | "editable": true 239 | }, 240 | "outputs": [], 241 | "source": [] 242 | } 243 | ], 244 | "metadata": { 245 | "anaconda-cloud": {}, 246 | "kernelspec": { 247 | "display_name": "Python 3", 248 | "language": "python", 249 | "name": "python3" 250 | }, 251 | "language_info": { 252 | "codemirror_mode": { 253 | "name": "ipython", 254 | "version": 3 255 | }, 256 | "file_extension": ".py", 257 | "mimetype": "text/x-python", 258 | "name": "python", 259 | "nbconvert_exporter": "python", 260 | "pygments_lexer": "ipython3", 261 | "version": "3.6.0" 262 | }, 263 | "toc": { 264 | "colors": { 265 | "hover_highlight": "#DAA520", 266 | "navigate_num": "#000000", 267 | "navigate_text": "#333333", 268 | "running_highlight": "#FF0000", 269 | "selected_highlight": "#FFD700", 270 | "sidebar_border": "#EEEEEE", 271 | "wrapper_background": "#FFFFFF" 272 | }, 273 | "moveMenuLeft": true, 274 | "nav_menu": { 275 | "height": "30px", 276 | "width": "252px" 277 | }, 278 | "navigate_menu": true, 279 | "number_sections": true, 280 | "sideBar": true, 281 | "threshold": 4, 282 | "toc_cell": false, 283 | "toc_section_display": "block", 284 | "toc_window_display": false, 285 | "widenNotebook": false 286 | }, 287 | "widgets": { 288 | "state": { 289 | "0688572cdfda44de9cc3994ffe0faf33": { 290 | "views": [ 291 | { 292 | "cell_index": 3 293 | } 294 | ] 295 | } 296 | }, 297 | "version": "1.2.0" 298 | } 299 | }, 300 | "nbformat": 4, 301 | "nbformat_minor": 1 302 | } 303 | -------------------------------------------------------------------------------- /tictactoe_minimax_helper.py: -------------------------------------------------------------------------------- 1 | def get_next_turn(active_turn): 2 | # Change player 3 | # 1 -> o 4 | #-1 -> x 5 | if active_turn == 1: 6 | next_turn = -1 7 | else: 8 | next_turn = 1 9 | return next_turn 10 | 11 | def score(matrix, i_am, depth): 12 | # Returns +-10 points -+ depth 13 | # It is the score for user i_am depending on matrix 14 | # If matrix correspond to a win for user i_am, get 10 points - depth 15 | # If matrix correspond to a win for the other user, return depth - 10 16 | # If matrix if not a winning condition, then returns 0 17 | # The maximum depth is 8 so it seems natural that the max points are 10 18 | points = 10 19 | status = game_status(matrix) 20 | if status==0: 21 | return 0 22 | if status==i_am: 23 | return points - depth 24 | if status!=i_am: 25 | return depth - points 26 | 27 | def get_childs(matrix, turn): 28 | # turn is 1 or -1 29 | # 1 -> o 30 | #-1 -> x 31 | # 0 -> Free space 32 | # return all posible plays for user 'turn' ('x' or 'o') 33 | N = 3 34 | childs = [] 35 | for i in range(N): 36 | for j in range(N): 37 | if matrix[i,j]==0: 38 | child = matrix.copy() 39 | child[i,j] = turn 40 | childs.append(child) 41 | return childs 42 | 43 | def game_status(matrix): 44 | # Returns 1 if 'o' win, -1 if 'x' win, 0 if draw 45 | points = 1 46 | if (matrix[0,:].sum() == 3)|(matrix[1,:].sum() == 3)|(matrix[2,:].sum() == 3)|(matrix[:,0].sum() == 3)|(matrix[:,1].sum() == 3)|(matrix[:,2].sum() == 3): 47 | return points 48 | if (matrix[0,0]==matrix[1,1])&(matrix[2,2]==matrix[1,1])&(matrix[0,0]==1): 49 | return points 50 | if (matrix[0,2]==matrix[1,1])&(matrix[2,0]==matrix[1,1])&(matrix[2,0]==1): 51 | return points 52 | if (matrix[0,:].sum() == -3)|(matrix[1,:].sum() == -3)|(matrix[2,:].sum() == -3)|(matrix[:,0].sum() == -3)|(matrix[:,1].sum() == -3)|(matrix[:,2].sum() == -3): 53 | return -points 54 | if (matrix[0,0]==matrix[1,1])&(matrix[2,2]==matrix[1,1])&(matrix[0,0]==-1): 55 | return -points 56 | if (matrix[0,2]==matrix[1,1])&(matrix[2,0]==matrix[1,1])&(matrix[2,0]==-1): 57 | return -points 58 | return 0 59 | 60 | def game_over(matrix): 61 | # status <- Returns 1 if 'o' win, -1 if 'x' win, 0 if draw 62 | # game_finished: true is game is over 63 | game_finished = False 64 | status = game_status(matrix) 65 | if status!=0: 66 | #Game finishes, someone won 67 | game_finished = True 68 | if abs(matrix).sum() == 9: 69 | #No more moves 70 | game_finished = True 71 | return game_finished, status 72 | 73 | def maximize(matrix, active_turn, player, depth, alpha, beta, nodes_visited): 74 | game_finished,_ = game_over(matrix) 75 | if game_finished: 76 | return None, score(matrix, player, depth), nodes_visited 77 | depth += 1 78 | 79 | infinite_number = 100000 80 | maxUtility = -infinite_number 81 | choice = None 82 | 83 | childs = get_childs(matrix, active_turn) 84 | for child in childs: 85 | nodes_visited = nodes_visited + 1 86 | _, utility, nodes_visited = minimize(child, get_next_turn(active_turn), player, depth, alpha, beta, nodes_visited) 87 | 88 | if utility > maxUtility: 89 | choice = child 90 | maxUtility = utility 91 | 92 | if maxUtility >= beta: 93 | break 94 | if maxUtility > alpha: 95 | alpha = maxUtility 96 | return choice, maxUtility, nodes_visited 97 | 98 | def minimize(matrix, active_turn, player, depth, alpha, beta, nodes_visited): 99 | game_finished,_ = game_over(matrix) 100 | if game_finished: 101 | return None, score(matrix, player, depth), nodes_visited 102 | depth += 1 103 | infinite_number = 100000 104 | minUtility = infinite_number 105 | choice = None 106 | 107 | childs = get_childs(matrix, active_turn) 108 | for child in childs: 109 | nodes_visited = nodes_visited + 1 110 | _, utility, nodes_visited = maximize(child, get_next_turn(active_turn), player, depth, alpha, beta, nodes_visited) 111 | 112 | if utility < minUtility: 113 | choice = child 114 | minUtility = utility 115 | 116 | if minUtility <= alpha: 117 | break 118 | if minUtility < beta: 119 | beta = minUtility 120 | return choice, minUtility, nodes_visited 121 | 122 | def minimax(matrix, player): 123 | infinite_number = 1000 124 | alpha = -infinite_number 125 | beta = infinite_number 126 | choice, score, nodes_visited = maximize(matrix, player, player, 0, alpha, beta, 0) 127 | return choice, score, nodes_visited --------------------------------------------------------------------------------