├── LICENSE ├── README.md ├── images └── ticky.gif └── ticky.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Guodong Xu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ticky 2 | 3 | A Tic Tac Toe game, implemented in python, pygame. It includes a unbeatable computer AI. Have a Fun! 4 | 5 | **If you like it, PLEASE give it a star, Thanks!** 6 | 7 | ### Require 8 | 9 | You should have `pygame` module installed 10 | 11 | ### Usage 12 | 13 | ```bash 14 | git clone https://github.com/memoiry/Ticky 15 | cd Ticky 16 | python ticky.py 17 | ``` 18 | 19 | ### Demo 20 | 21 | 22 | 23 | ### Minimax Algorithm 24 | 25 | The minimax algorithm is a decision rule used for two-player game. The concept of minimax algorithm is to simulate all posible move of both players and then make the best decision. 26 | 27 | ### Reference 28 | 29 | [Tic Tac Toe: Understanding The Minimax Algorithm](http://neverstopbuilding.com/minimax) 30 | 31 | [Minimax with Alpha Beta Pruning](http://web.cs.ucla.edu/~rosen/161/notes/alphabeta.html) 32 | 33 | -------------------------------------------------------------------------------- /images/ticky.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memoiry/Ticky/7d49116be8415cc096c07ec77f6d409bd9317c34/images/ticky.gif -------------------------------------------------------------------------------- /ticky.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame.locals import * 3 | 4 | 5 | # Create the constants (go ahead and experiment with different values) 6 | BOARDWIDTH = 3 # number of columns in the board 7 | BOARDHEIGHT = 3 # number of rows in the board 8 | TILESIZE = 100 9 | WINDOWWIDTH = 480 10 | WINDOWHEIGHT = 480 11 | FPS = 30 12 | BLANK = None 13 | 14 | # R G B 15 | BLACK = ( 0, 0, 0) 16 | WHITE = (255, 255, 255) 17 | BRIGHTBLUE = ( 0, 50, 255) 18 | DARKTURQUOISE = ( 3, 54, 73) 19 | GREEN = ( 0, 204, 0) 20 | 21 | BGCOLOR = DARKTURQUOISE 22 | TILECOLOR = GREEN 23 | TEXTCOLOR = WHITE 24 | BORDERCOLOR = BRIGHTBLUE 25 | BASICFONTSIZE = 20 26 | 27 | BUTTONCOLOR = WHITE 28 | BUTTONTEXTCOLOR = BLACK 29 | MESSAGECOLOR = WHITE 30 | 31 | BLANK = 10 32 | PLAYER_O = 11 33 | PLAYER_X = 21 34 | 35 | 36 | PLAYER_O_WIN = PLAYER_O * 3 37 | PLAYER_X_WIN = PLAYER_X * 3 38 | 39 | CONT_GAME = 10 40 | DRAW_GAME = 20 41 | QUIT_GAME = 30 42 | 43 | XMARGIN = int((WINDOWWIDTH - (TILESIZE * BOARDWIDTH + (BOARDWIDTH - 1))) / 2) 44 | YMARGIN = int((WINDOWHEIGHT - (TILESIZE * BOARDHEIGHT + (BOARDHEIGHT - 1))) / 2) 45 | 46 | choice = 0 47 | 48 | 49 | def check_win_game(board): 50 | def check_draw_game(): 51 | return sum(board)%10 == 9 52 | 53 | def check_horizontal(player): 54 | for i in [0, 3, 6]: 55 | if sum(board[i:i+3]) == 3 * player: 56 | return player 57 | 58 | def check_vertical(player): 59 | for i in range(3): 60 | if sum(board[i::3]) == 3 * player: 61 | return player 62 | 63 | def check_diagonals(player): 64 | if (sum(board[0::4]) == 3 * player) or (sum(board[2:7:2]) == 3 * player): 65 | return player 66 | 67 | for player in [PLAYER_X, PLAYER_O]: 68 | if any([check_horizontal(player), check_vertical(player), check_diagonals(player)]): 69 | return player 70 | 71 | return DRAW_GAME if check_draw_game() else CONT_GAME 72 | 73 | 74 | def unit_score(winner, depth): 75 | if winner == DRAW_GAME: 76 | return 0 77 | else: 78 | return 10 - depth if winner == PLAYER_X else depth - 10 79 | 80 | 81 | def get_available_step(board): 82 | return [i for i in range(9) if board[i] == BLANK] 83 | 84 | 85 | def minmax(board, depth): 86 | global choice 87 | result = check_win_game(board) 88 | if result != CONT_GAME: 89 | return unit_score(result, depth) 90 | 91 | depth += 1 92 | scores = [] 93 | steps = [] 94 | 95 | for step in get_available_step(board): 96 | score = minmax(update_state(board, step, depth), depth) 97 | scores.append(score) 98 | steps.append(step) 99 | 100 | if depth % 2 == 1: 101 | max_value_index = scores.index(max(scores)) 102 | choice = steps[max_value_index] 103 | return max(scores) 104 | else: 105 | min_value_index = scores.index(min(scores)) 106 | choice = steps[min_value_index] 107 | return min(scores) 108 | 109 | 110 | def update_state(board, step, depth): 111 | board = list(board) 112 | board[step] = PLAYER_X if depth % 2 else PLAYER_O 113 | return board 114 | 115 | 116 | def update_board(board, step, player): 117 | board[step] = player 118 | 119 | 120 | def change_to_player(player): 121 | if player == PLAYER_O: 122 | return 'O' 123 | elif player == PLAYER_X: 124 | return 'X' 125 | elif player == BLANK: 126 | return '-' 127 | 128 | 129 | def drawBoard(board, message): 130 | DISPLAYSURF.fill(BGCOLOR) 131 | if message: 132 | textSurf, textRect = makeText(message, MESSAGECOLOR, BGCOLOR, 5, 5) 133 | DISPLAYSURF.blit(textSurf, textRect) 134 | 135 | for tilex in range(3): 136 | for tiley in range(3): 137 | if board[tilex*3+tiley] != BLANK: 138 | drawTile(tilex, tiley, board[tilex*3+tiley]) 139 | 140 | left, top = getLeftTopOfTile(0, 0) 141 | width = BOARDWIDTH * TILESIZE 142 | height = BOARDHEIGHT * TILESIZE 143 | pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (left - 5, top - 5, width + 11, height + 11), 4) 144 | 145 | DISPLAYSURF.blit(NEW_SURF, NEW_RECT) 146 | DISPLAYSURF.blit(NEW_SURF2, NEW_RECT2) 147 | 148 | 149 | def getLeftTopOfTile(tileX, tileY): 150 | left = XMARGIN + (tileX * TILESIZE) + (tileX - 1) 151 | top = YMARGIN + (tileY * TILESIZE) + (tileY - 1) 152 | return (left, top) 153 | 154 | 155 | def makeText(text, color, bgcolor, top, left): 156 | '''Create the Surface and Rect objects for some text.''' 157 | textSurf = BASICFONT.render(text, True, color, bgcolor) 158 | textRect = textSurf.get_rect() 159 | textRect.topleft = (top, left) 160 | return (textSurf, textRect) 161 | 162 | 163 | def drawTile(tilex, tiley, symbol, adjx=0, adjy=0): 164 | ''' 165 | Draw a tile at board coordinates tilex and tiley, optionally a few 166 | pixels over (determined by adjx and adjy). 167 | ''' 168 | left, top = getLeftTopOfTile(tilex, tiley) 169 | pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left + adjx, top + adjy, TILESIZE, TILESIZE)) 170 | textSurf = BASICFONT.render(symbol_to_str(symbol), True, TEXTCOLOR) 171 | textRect = textSurf.get_rect() 172 | textRect.center = left + int(TILESIZE / 2) + adjx, top + int(TILESIZE / 2) + adjy 173 | DISPLAYSURF.blit(textSurf, textRect) 174 | 175 | 176 | def symbol_to_str(symbol): 177 | if symbol == PLAYER_O: 178 | return 'O' 179 | elif symbol == PLAYER_X: 180 | return 'X' 181 | 182 | 183 | def getSpotClicked(x, y): 184 | '''From the x & y pixel coordinates, get the x & y board coordinates.''' 185 | for tileX in range(3): 186 | for tileY in range(3): 187 | left, top = getLeftTopOfTile(tileX, tileY) 188 | tileRect = pygame.Rect(left, top, TILESIZE, TILESIZE) 189 | if tileRect.collidepoint(x, y): 190 | return (tileX, tileY) 191 | return None 192 | 193 | 194 | def board_to_step(spotx, spoty): 195 | return spotx * 3 + spoty 196 | 197 | 198 | def check_move_legal(coords, board): 199 | step = board_to_step(*coords) 200 | return board[step] == BLANK 201 | 202 | def main(): 203 | global FPSCLOCK, DISPLAYSURF, BASICFONT, NEW_SURF, NEW_RECT, NEW_SURF2, NEW_RECT2 204 | two_player = False #by default false 205 | pygame.init() 206 | FPSCLOCK = pygame.time.Clock() 207 | DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT)) 208 | pygame.display.set_caption('Ticky') 209 | BASICFONT = pygame.font.Font('freesansbold.ttf', BASICFONTSIZE) 210 | NEW_SURF, NEW_RECT = makeText('vs AI', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 120, WINDOWHEIGHT - 60) 211 | NEW_SURF2, NEW_RECT2 = makeText('vs Human', TEXTCOLOR, TILECOLOR, WINDOWWIDTH - 240, WINDOWHEIGHT - 60) 212 | board = [BLANK] * 9 213 | game_over = False 214 | x_turn = True 215 | msg = "Ticky - Unbeatable Tic Tac Toe AI" 216 | drawBoard(board, msg) 217 | pygame.display.update() 218 | 219 | while True: 220 | coords = None 221 | for event in pygame.event.get(): 222 | if event.type == MOUSEBUTTONUP: 223 | coords = getSpotClicked(event.pos[0], event.pos[1]) 224 | if not coords and NEW_RECT.collidepoint(event.pos): 225 | board = [BLANK] * 9 226 | game_over = False 227 | msg = "Ticky - Unbeatable Tic Tac Toe AI" 228 | drawBoard(board, msg) 229 | pygame.display.update() 230 | two_player = False 231 | if not coords and NEW_RECT2.collidepoint(event.pos): 232 | board = [BLANK] * 9 233 | game_over = False 234 | msg = "Ticky - Unbeatable Tic Tac Toe AI" 235 | drawBoard(board, msg) 236 | pygame.display.update() 237 | two_player = True 238 | if coords and check_move_legal(coords, board) and not game_over: 239 | if two_player: 240 | next_step = board_to_step(*coords) 241 | if x_turn: 242 | update_board(board, next_step, PLAYER_X) 243 | x_turn = False 244 | else: 245 | update_board(board, next_step, PLAYER_O) 246 | x_turn = True 247 | drawBoard(board, msg) 248 | pygame.display.update() 249 | 250 | if not two_player: 251 | next_step = board_to_step(*coords) 252 | update_board(board, next_step, PLAYER_X) 253 | drawBoard(board, msg) 254 | pygame.display.update() 255 | minmax(board, 0) 256 | update_board(board, choice, PLAYER_O) 257 | 258 | result = check_win_game(board) 259 | game_over = (result != CONT_GAME) 260 | 261 | if result == PLAYER_X: 262 | msg = "X wins!" 263 | elif result == PLAYER_O: 264 | msg = "O wins!" 265 | elif result == DRAW_GAME: 266 | msg = "Draw game" 267 | 268 | drawBoard(board, msg) 269 | pygame.display.update() 270 | 271 | 272 | if __name__ == '__main__': 273 | main() 274 | --------------------------------------------------------------------------------