└── Tic-Tac-Toe with AI └── task └── tictactoe.py /Tic-Tac-Toe with AI/task/tictactoe.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | 4 | 5 | class TicTacToeGame: 6 | def __init__(self, board_state=None): 7 | if board_state is None: 8 | board_state = "_________" 9 | self.board_state = list(board_state) 10 | self.game_state = 0 11 | self.mode = ["user", "easy", "medium", "hard"] 12 | self.menu = ["start", "exit"] 13 | self.parameter = "" 14 | self.users = [] 15 | self.range = [1, 2, 3] 16 | self.depth = 0 17 | # player and opponent variable were introduced for minimax function where user1's opponent is user2 and vice versa 18 | self.player = "" 19 | self.opponent = "" 20 | self.board_rows = {"row1": {0: None, 1: None, 2: None}, 21 | "row2": {3: None, 4: None, 5: None}, 22 | "row3": {6: None, 7: None, 8: None}, 23 | "column1": {0: None, 3: None, 6: None}, 24 | "column2": {1: None, 4: None, 7: None}, 25 | "column3": {2: None, 5: None, 8: None}, 26 | "diagonal1": {0: None, 4: None, 8: None}, 27 | "diagonal2": {2: None, 4: None, 6: None}} 28 | 29 | def game_state_analysis(self): 30 | tictactoe_matrix = [list(self.board_state[i:i + 3]) for i in range(0, len(self.board_state), 3)] 31 | tictactoe_matrix_columns = [[self.board_state[j] for j in range(i, len(self.board_state), 3)] for i in range(3)] 32 | x_win, o_win, empty_cell = 0, 0, 0 33 | diagonal1, diagonal2 = [], [] 34 | # checking rows 35 | for row in tictactoe_matrix: 36 | if row.count("X") == 3: 37 | x_win = 1 38 | elif row.count("O") == 3: 39 | o_win = 1 40 | elif row.count("_"): 41 | empty_cell = 1 42 | # checking columns 43 | for row in tictactoe_matrix_columns: 44 | if row.count("X") == 3: 45 | x_win = 1 46 | elif row.count("O") == 3: 47 | o_win = 1 48 | elif row.count("_"): 49 | empty_cell = 1 50 | # checking diagonals 51 | for i, elem in enumerate(tictactoe_matrix): 52 | diagonal1.append(elem[i]) 53 | diagonal2.append(elem[-(i + 1)]) 54 | 55 | if diagonal1.count("X") == 3 or diagonal2.count("X") == 3: 56 | x_win = 1 57 | elif diagonal1.count("O") == 3 or diagonal2.count("O") == 3: 58 | o_win = 1 59 | 60 | if x_win and not o_win: 61 | return f"X wins" 62 | elif o_win and not x_win: 63 | return f"O wins" 64 | elif not x_win and not o_win and not empty_cell: 65 | return f"Draw" 66 | 67 | def game_state_analysis_minimax(self, order): 68 | tictactoe_matrix = [list(self.board_state[i:i + 3]) for i in range(0, len(self.board_state), 3)] 69 | tictactoe_matrix_columns = [[self.board_state[j] for j in range(i, len(self.board_state), 3)] for i in range(3)] 70 | x_win, o_win, empty_cell = 0, 0, 0 71 | diagonal1, diagonal2 = [], [] 72 | # checking rows 73 | for row in tictactoe_matrix: 74 | if row.count(order) == 3: 75 | return True 76 | for row in tictactoe_matrix_columns: 77 | if row.count(order) == 3: 78 | return True 79 | for i, elem in enumerate(tictactoe_matrix): 80 | diagonal1.append(elem[i]) 81 | diagonal2.append(elem[-(i + 1)]) 82 | 83 | if diagonal1.count(order) == 3 or diagonal2.count(order) == 3: 84 | return True 85 | 86 | return False 87 | 88 | def draw_field(self, print_answer=1): 89 | print("---------") 90 | for i, elem in enumerate(self.board_state): 91 | if not i % 3: 92 | print(f"| {elem}", end="") 93 | elif (i + 1) % 3: 94 | print(f" {elem}", end="") 95 | else: 96 | print(f" {elem} |") 97 | print("---------") 98 | 99 | if self.game_state_analysis() and print_answer: 100 | print(self.game_state_analysis()) 101 | # reset board 102 | self.board_state = list("_________") 103 | return 0 104 | else: 105 | return 1 106 | 107 | def order(self): 108 | if self.board_state.count('X') == self.board_state.count("O"): 109 | return "X" 110 | return "O" 111 | 112 | def next_order(self): 113 | if self.board_state.count('X') == self.board_state.count("O"): 114 | return "O" 115 | return "X" 116 | 117 | def ai_move_easy(self, level="easy"): 118 | # time.sleep(2) 119 | free_indexes = [i for i, el in enumerate(self.board_state) if el == "_"] 120 | ai_coordinates_cell = random.choice(free_indexes) 121 | print(f"Making move level \"{level}\"") 122 | return ai_coordinates_cell 123 | 124 | def ai_move_medium(self, level="medium"): 125 | # time.sleep(2) 126 | for key, value in self.board_rows.items(): 127 | for k, v in value.items(): 128 | value[k] = self.board_state[k] 129 | # attack 130 | order = self.order() 131 | for key, value in self.board_rows.items(): 132 | count = 0 133 | for k, v in value.items(): 134 | if v == order: 135 | count += 1 136 | if count == 2: 137 | for k, v in value.items(): 138 | if v == "_": 139 | print(f"Making move level \"{level}\"") 140 | return k 141 | # defend 142 | next_order = self.next_order() 143 | for key, value in self.board_rows.items(): 144 | count = 0 145 | for k, v in value.items(): 146 | if v == next_order: 147 | count += 1 148 | if count == 2: 149 | for k, v in value.items(): 150 | if v == "_": 151 | print(f"Making move level \"{level}\"") 152 | return k 153 | return self.ai_move_easy(level="medium") 154 | 155 | # This is the minimax function. It considers all the possible ways the game can go and returns the value of the board 156 | def minimax(self, depth, is_max): 157 | # If Maximizer has won the game return his/her evaluated score 158 | if self.game_state_analysis_minimax(self.player): 159 | # self.depth = depth 160 | return 10 - depth 161 | # If Minimizer has won the game return his evaluated score 162 | elif self.game_state_analysis_minimax(self.opponent): 163 | # self.depth = depth 164 | return -10 + depth 165 | # If there are no more moves and no winner then it is a tie 166 | elif self.game_state_analysis() == "Draw": 167 | # self.depth = depth 168 | return 0 169 | # If this maximizer's move 170 | if is_max: 171 | best_value = -1000 172 | # Traverse all cells 173 | for i, el in enumerate(self.board_state): 174 | # Check if cell is empty 175 | if el == "_": 176 | # Make the move 177 | self.board_state[i] = self.player 178 | # Call minimax recursively and choose the maximum value 179 | best_value = max(best_value, self.minimax(depth + 1, not is_max)) 180 | # Undo the move 181 | self.board_state[i] = "_" 182 | return best_value 183 | # If this minimizer's move 184 | else: 185 | best_value = 1000 186 | # Traverse all cells 187 | for i, el in enumerate(self.board_state): 188 | # Check if cell is empty 189 | if el == "_": 190 | # Make the move 191 | self.board_state[i] = self.opponent 192 | # Call minimax recursively and choose the maximum value 193 | best_value = min(best_value, self.minimax(depth + 1, not is_max)) 194 | # Undo the move 195 | self.board_state[i] = "_" 196 | return best_value 197 | 198 | # This will return the best possible move for the player 199 | def ai_move_hard(self, level="hard"): 200 | best_value = -1000 201 | best_move = 0 202 | best_depth = 0 203 | self.player = self.order() 204 | self.opponent = self.next_order() 205 | # Traverse all cells, evaluate minimax function for all empty cells. And return the cell with optimal value. 206 | for i, el in enumerate(self.board_state): 207 | # Check if cell is empty 208 | if el == "_": 209 | # Make the move 210 | self.board_state[i] = self.order() 211 | # compute evaluation function for this move. 212 | current_value = self.minimax(0, False) 213 | # Undo the move 214 | self.board_state[i] = "_" 215 | # If the value of the current move is more than the best value, then update best 216 | if current_value > best_value: 217 | best_value = current_value 218 | best_depth = self.depth 219 | best_move = i 220 | # print(f"depth is {best_depth}") 221 | print(f"Making move level \"{level}\"") 222 | return best_move 223 | 224 | def player_move(self): 225 | while True: 226 | try: 227 | user_coordinates_row, user_coordinates_column = input("Enter the coordinates: ").split() 228 | user_coordinates_row = int(user_coordinates_row) 229 | user_coordinates_column = int(user_coordinates_column) 230 | except ValueError: 231 | print("You should enter numbers!") 232 | continue 233 | 234 | if user_coordinates_row not in self.range or user_coordinates_column not in self.range: 235 | print("Coordinates should be from 1 to 3!") 236 | continue 237 | 238 | if user_coordinates_row == 1: 239 | cell_state = self.cell_analysis(self.board_state[user_coordinates_column - 1]) 240 | if not cell_state: 241 | index = user_coordinates_row * (user_coordinates_column - 1) 242 | return index 243 | else: 244 | continue 245 | else: 246 | cell_state = self.cell_analysis(self.board_state[(user_coordinates_row - 1) * 3 + user_coordinates_column - 1]) 247 | if not cell_state: 248 | index = (user_coordinates_row - 1) * 3 + user_coordinates_column - 1 249 | return index 250 | else: 251 | continue 252 | 253 | @staticmethod 254 | def cell_analysis(cell, print_answer=1): 255 | if cell == "_": 256 | _state = 0 257 | else: 258 | if print_answer: 259 | print("This cell is occupied! Choose another one!") 260 | _state = 1 261 | return _state 262 | 263 | def game_menu(self): 264 | while True: 265 | self.parameter, *self.users = input("Input command: ").split() 266 | if self.parameter.lower() == "exit": 267 | return False 268 | elif (self.parameter.lower() not in self.menu) or (self.parameter.lower() == "exit" and len(self.users) != 0): 269 | print("Bad parameters!") 270 | continue 271 | elif (len(self.users) != 2) or not all(user in self.mode for user in self.users): 272 | print("Bad parameters!") 273 | continue 274 | elif self.parameter.lower() == "start": 275 | return True 276 | 277 | def game(self): 278 | self.game_state = self.draw_field(print_answer=0) 279 | user1, user2 = None, None 280 | if self.users[0] == "user": 281 | user1 = self.player_move 282 | elif self.users[0] == "easy": 283 | user1 = self.ai_move_easy 284 | elif self.users[0] == "medium": 285 | user1 = self.ai_move_medium 286 | elif self.users[0] == "hard": 287 | user1 = self.ai_move_hard 288 | 289 | if self.users[1] == "user": 290 | user2 = self.player_move 291 | elif self.users[1] == "easy": 292 | user2 = self.ai_move_easy 293 | elif self.users[1] == "medium": 294 | user2 = self.ai_move_medium 295 | elif self.users[1] == "hard": 296 | user2 = self.ai_move_hard 297 | 298 | while self.game_state: 299 | if self.order() == "X": 300 | self.board_state[user1()] = self.order() 301 | else: 302 | self.board_state[user2()] = self.order() 303 | 304 | self.game_state = self.draw_field() 305 | 306 | 307 | def main(): 308 | game = TicTacToeGame() 309 | while game.game_menu(): 310 | game.game() 311 | 312 | 313 | if __name__ == "__main__": 314 | main() 315 | --------------------------------------------------------------------------------