diff --git a/README.md b/README.md index 036f112..daa6ad8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # tic-tac-toe-minimax An implementation of Minimax AI Algorithm on Tic-Tac-Toe (or Noughts and Crosses) game. Try it: [Tic-tac-toe - Minimax](https://cledersonbc.github.io/tic-tac-toe-minimax/) +Alternatively, if you have python interpreter, get the repo and run **gui.py** from the py_version directory +

@@ -150,6 +152,8 @@ def minimax(state, depth, player): return best ``` +## GUI Screenshot +![GUI Screenshot 2](preview/tkinter-gui2.png) ## Game Tree Below, the best move is on the middle because the max value is on 2nd node on left. diff --git a/preview/tkinter-gui1.png b/preview/tkinter-gui1.png new file mode 100644 index 0000000..53a7395 Binary files /dev/null and b/preview/tkinter-gui1.png differ diff --git a/preview/tkinter-gui2.png b/preview/tkinter-gui2.png new file mode 100644 index 0000000..8d4403a Binary files /dev/null and b/preview/tkinter-gui2.png differ diff --git a/py_version/compute.py b/py_version/compute.py new file mode 100644 index 0000000..f51b970 --- /dev/null +++ b/py_version/compute.py @@ -0,0 +1,256 @@ +# File Name: compute.py +# Description: Provides game algorithm for Cross & Nut game +# +# Written by Rutuparn Pawar (InputBlackBoxOutput) +# Created on 5 Sept 2019 +# Last modified on 2 Nov 2020 + +import random, time, minimax + +class Tic_Tac_Toe: + + def __init__(self): + # self.grid_map = [['O', 'X', 'X'], ['O', 'X', 'O'], ['O', 'O', 'O']] # Use for testing purpose + self.grid_map = [[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']] + self.cross_nut_map = {'cross': 'X', 'empty': ' ', 'nut': 'O'} + self.winner_string = None + + """Function to place cross/nut in grid if place is empty""" + + def place_cross_nut(self, x, y, place): + if place == 'cross': + if self.grid_map[x][y] == ' ': + self.grid_map[x][y] = self.cross_nut_map[place] + # Cross placed at ({x},{y}) + return True + else: + # Cross cannot be placed at ({x},{y}) since place is not empty + return False + elif place == 'nut': + if self.grid_map[x][y] == ' ': + self.grid_map[x][y] = self.cross_nut_map[place] + # Nut placed at ({x},{y}) + return True + else: + # Nut cannot be placed at ({x},{y}) since place is not empty + return False + else: + self.grid_map[x][y] = self.cross_nut_map['empty'] + + """Function to check if the player/computer has won""" + + def winner_check(self, player): + + # Check for 3 cross/nut in a row + for x in range(0, 3): + win_count = 0 + for y in range(0, 3): + if self.cross_nut_map[player] == self.grid_map[x][y]: + win_count = win_count + 1 + if win_count == 3: + # print('Complete Row', end='\n\n') + return 'Winner is ' + player + + # Check for 3 cross/nut in a column + for y in range(0, 3): + win_count = 0 + for x in range(0, 3): + if self.cross_nut_map[player] == self.grid_map[x][y]: + win_count = win_count + 1 + if win_count == 3: + # print('Complete column', end='\n\n') + return 'Winner is ' + player + + # Check for 3 cross/nut across diagonals + win_count = 0 + for i in range(0, 3): + if self.cross_nut_map[player] == self.grid_map[i][i]: + win_count = win_count + 1 + if win_count == 3: + # print('Complete Diagonal \\', end='\n\n') + return 'Winner is ' + player + + win_count = 0 + + if self.cross_nut_map[player] == self.grid_map[0][2]: + win_count = win_count + 1 + if self.cross_nut_map[player] == self.grid_map[1][1]: + win_count = win_count + 1 + if self.cross_nut_map[player] == self.grid_map[2][0]: + win_count = win_count + 1 + + if win_count == 3: + # print('Complete Diagonal /', end='\n\n') + return 'Winner is ' + player + + return None + + '''Function to check if ''' + + def game_tied(self): + if self.winner_string is None: + for row in range(0, 3): + if self.cross_nut_map['empty'] in self.grid_map[row]: + return False + return True + + '''Function to clear grid_map''' + + def clear_cross_nut(self): + for each_r in range(0, 3): + for each_c in range(0, 3): + self.grid_map[each_r][each_c] = self.cross_nut_map['empty'] + + """ Function to display grid on console for testing """ + + def display_for_testing(self): + index = [0, 1, 2] + for x in index: + for y in index: + print(self.grid_map[x][y], end=' ') + print() + + """ Function to run gameloop in console for testing """ + + def gameloop_for_testing(self): + # print("Tic Tac Toe") + # print('@Attention - !!!!! GRID MAY BEEN PREDEFINED !!!!!') + + while self.winner_string is None: + + self.display_for_testing() + # print("Player X") + x = input("Enter x coordinate :") + y = input("Enter y coordinate :") + self.place_cross_nut(int(x), int(y), 'cross') + self.winner_string = self.winner_check('cross') + + if self.winner_string is not None: + break + + self.display_for_testing() + + # print("Player O") + x = input("Enter x coordinate :") + y = input("Enter y coordinate :") + self.place_cross_nut(int(x), int(y), 'nut') + self.winner_string = self.winner_check('nut') + + self.display_for_testing() + # print(self.winner_string) + + return + + # Player is always cross hence begins the game + + """"Function to generate random unoccupied position""" + def random_position(self): + position = [-1, -1] + found_position = True + + retries = 0 + while found_position: + x = random.randint(0, 2) + y = random.randint(0, 2) + if self.grid_map[x][y] == self.cross_nut_map['empty']: + position[0] = x + position[1] = y + found_position = False + + retries = retries + 1 + if retries == 25: + print("Error: random_position tries exceeded") + break + + return position + + """Function to get vacant grid positions""" + def get_vacant_pos(self): + vacant = [] + for r in range(0,3): + for c in range(0,3): + if self.grid_map[r][c] == self.cross_nut_map['empty']: + vacant.append((r, c)) + return vacant + + """Function to get computer's move in a game vs computer""" + # Uses hand-crafted rules for AI + def bot_move(self): + if not self.game_tied() and self.winner_string is None: + # Wait for some time + time.sleep(0.1) + random.seed(random.randint(20, 120)) + # 75% chance of best move + chance = random.randint(0, 3) + + if chance > 0 : + best_move = False + # Check if winning move possible + for each in self.get_vacant_pos(): + self.grid_map[each[0]][each[1]] = self.cross_nut_map['nut'] + + if self.winner_check('nut') is None: + self.grid_map[each[0]][each[1]] = self.cross_nut_map['empty'] + else: + best_move = True + break + + if best_move: + return + + # Check if blocking move possible + for each in self.get_vacant_pos(): + self.grid_map[each[0]][each[1]] = self.cross_nut_map['cross'] + + if self.winner_check('cross') is None: + self.grid_map[each[0]][each[1]] = self.cross_nut_map['empty'] + else: + self.grid_map[each[0]][each[1]] = self.cross_nut_map['nut'] + best_move = True + break + if best_move: + return + else: + position = self.random_position() + self.place_cross_nut(position[0], position[1], 'nut') + + # Play random move + if chance == 0: + position = self.random_position() + self.place_cross_nut(position[0], position[1], 'nut') + + def bot_move_minimax(self): + for i in range(3): + for j in range(3): + if self.grid_map[i][j] == self.cross_nut_map['cross']: + minimax.board[i][j] = -1 + + elif self.grid_map[i][j] == self.cross_nut_map['nut']: + minimax.board[i][j] = 1 + + else: + minimax.board[i][j] = 0 + + + depth = len(minimax.empty_cells(minimax.board)) + if depth == 0 or minimax.game_over(minimax.board): + return + + if depth == 9: + x = minimax.choice([0, 1, 2]) + y = minimax.choice([0, 1, 2]) + else: + move = minimax.minimax(minimax.board, depth, minimax.COMP) + x, y = move[0], move[1] + + self.place_cross_nut(x, y, 'nut') + + +# For testing game vs person +# game=Tic_Tac_Toe() +# game.gameloop_for_testing() + +# For testing game vs computer +# game.display_for_testing() +# game.bot_move() +# game.display_for_testing() \ No newline at end of file diff --git a/py_version/gui.py b/py_version/gui.py new file mode 100644 index 0000000..99d8fe8 --- /dev/null +++ b/py_version/gui.py @@ -0,0 +1,307 @@ +# File Name:Tic_Tac_Toe.py +# Description:Provides graphical user interface (GUI) for Cross & Nut game. +# Built using python's inbuilt tkinter module +# +# Written by Rutuparn Pawar (InputBlackBoxOutput) +# Created on 29 Sept 2019 +# Last modified 1 Nov 2020 + +import os + +from tkinter import * +import tkinter.messagebox as msgbox + +from compute import * + +class GUI(Tk): + def __init__(self, background, font_size): + super().__init__() + self.board = Tic_Tac_Toe() + + self.title("Cross & Nut") + self.geometry("410x420") + self.wm_resizable(width=False, height=False) + # self.wm_iconbitmap('path') Add .ico file to path + + self.bkgnd = background + self.configure(bg=background) + self.font_size = font_size + + self.move = 'cross' # Cross plays first + self.bot = True # Default: Game vs computer + + # Binding functions for menu bar + def Vs_computer(self): + self.board.clear_cross_nut() + self.update_board() + self.remove_mark() + self.bot = True + self.move = 'cross' + self.board.winner_string = None + self.status.configure(text="Match versus computer") + # print('Beginning game vs computer') + + def Vs_player(self): + self.board.clear_cross_nut() + self.update_board() + self.remove_mark() + self.bot = False + self.board.winner_string = None + self.move = 'cross' + self.status.configure(text="") + # print('Beginning game vs person') + + def how_to_play(self): + try: + with open(os.path.join(sys.path[0], "help.txt"), "rt") as help_file: + msgbox.showinfo('HOW TO PLAY', help_file.read()) + except FileNotFoundError: + print("File not found!") + + def about(self): + try: + with open(os.path.join(sys.path[0], "about.txt"), "r") as about_file: + msgbox.showinfo('ABOUT', about_file.read()) + except FileNotFoundError: + print("File not found!") + + '''Fxn to build menu bar''' + def menu_bar(self): + self.menu = Menu(self) + self.menu.add_command(label='One player', command=self.Vs_computer) + self.menu.add_command(label='Two player', command=self.Vs_player) + self.menu.add_command(label='How to play', command=self.how_to_play) + self.menu.add_command(label='About', command=self.about) + self.menu.add_command(label='Close', command=quit) + self.config(menu=self.menu) + + '''Fxn to build label to display winner''' + def heading_label(self, padding): + self.win_label = Label(self, text="Let's play Tic Tac Toe!", font="lucida 16 bold", padx=padding, + pady=round(padding / 5)) + self.win_label.pack(side=TOP, fill=X) + Label(self, bg=self.bkgnd).pack(side=TOP) + + # Helper functions for playing grid + def update_board(self): + each_button = 0 + for each_r in range(0, 3): + for each_c in range(0, 3): + new_text = self.board.grid_map[each_r][each_c] + self.b_list[each_button].configure(text=new_text, font=f"calibri {self.font_size - 1} bold") + each_button = each_button + 1 + + ''' If there is no winner notify the user and begin the game again ''' + def no_winner(self): + if self.board.game_tied(): + msgbox.showinfo('No winner', "Looks like there is no winner.") + self.board.clear_cross_nut() + self.remove_mark() + self.update_board() + + + '''Helper function for button_pressed function''' + def toggle_move(self): + if self.move == 'cross': + self.move = 'nut' + else: + self.move = 'cross' + + def whose_move(self): + if self.move == 'cross': + return 'Nut' + else: + return'Cross' + + def button_pressed(self, button, x, y): + # Don't do anything if there is a winner + if self.board.winner_string is not None : + return + + # Do something if there is no winner + # print(f'Button {button} pressed') + + stat = self.board.place_cross_nut(x, y, self.move) + + if self.bot is False: + if stat: + self.update_board() + self.board.winner_string = self.board.winner_check(self.move) + self.status.configure(text=f"{self.whose_move()}'s turn ") + self.toggle_move() + + if self.board.winner_string is not None: + self.status.configure(text=self.board.winner_string) + self.mark() + return + self.no_winner() + else: + if stat: + self.board.bot_move_minimax() + self.update_board() + + self.board.winner_string = self.board.winner_check('nut') or self.board.winner_check('cross') + if self.board.winner_string is not None: + self.status.configure(text=self.board.winner_string) + self.mark() + + # Binding functions for playing grid + def b0(self): + self.button_pressed(0, 0, 0) + def b1(self): + self.button_pressed(1, 0, 1) + def b2(self): + self.button_pressed(2, 0, 2) + def b3(self): + self.button_pressed(3, 1, 0) + def b4(self): + self.button_pressed(4, 1, 1) + def b5(self): + self.button_pressed(5, 1, 2) + def b6(self): + self.button_pressed(6, 2, 0) + def b7(self): + self.button_pressed(7, 2, 1) + def b8(self): + self.button_pressed(8, 2, 2) + + '''Fxn to build playing grid''' + def play_grid(self, padding): + self.canvas = Canvas(self, bg=self.bkgnd) + self.grid_map = Frame(self.canvas, bg='grey') + self.canvas.pack(fill=BOTH) + + # Draw line after game has a winner (Not working!) + # Looks like drawing on canvas does not get superimposed on button widget + #self.canvas.create_line(0, 0, 1000, 1000, fill="red") + + # Generate 9 button widgets + self.b_list = [] + for each in range(0, 9): + self.b_list.append( + Button(self.grid_map, text=' ', font=f"calibri {self.font_size} bold", padx=padding, pady=padding)) + + # Place 9 button widgets in a grid + each = 0 + for r in range(0, 3): + for c in range(0, 3): + self.b_list[each].grid(row=r, column=c) + each = each + 1 + + # Mapping buttons in grid to functions + self.b_list[0].configure(command=self.b0) + self.b_list[1].configure(command=self.b1) + self.b_list[2].configure(command=self.b2) + + self.b_list[3].configure(command=self.b3) + self.b_list[4].configure(command=self.b4) + self.b_list[5].configure(command=self.b5) + + self.b_list[6].configure(command=self.b6) + self.b_list[7].configure(command=self.b7) + self.b_list[8].configure(command=self.b8) + + self.grid_map.pack(side=BOTTOM) + + self.update_board() + + '''Fxn to build status bar''' + def status_bar(self, padding): + self.status = Label(self, text="Developed by Rutuparn Pawar", font='calibri 12 normal', borderwidth=1, + relief=SUNKEN, anchor='s', pady=padding) + self.status.pack(side=BOTTOM, fill=X) + Label(self, bg=self.bkgnd).pack(side=BOTTOM) + + '''Fxn to remove mark row/column/diagonal''' + def remove_mark(self): + for each_button in self.b_list: + each_button.configure(bg='#F0FF0FF0F') + + '''Fxn to mark row/column/diagonal''' + def mark(self): + self.remove_mark() + + for player in ['cross', 'nut']: + # Check for 3 cross/nut in a row + for x in range(0, 3): + win_count = 0 + for y in range(0, 3): + if self.board.cross_nut_map[player] == self.board.grid_map[x][y]: + win_count = win_count + 1 + if win_count == 3: + if x == 0: + self.b_list[0].configure(bg='#AFFFAF') + self.b_list[1].configure(bg='#AFFFAF') + self.b_list[2].configure(bg='#AFFFAF') + break + elif x == 1: + self.b_list[3].configure(bg='#AFFFAF') + self.b_list[4].configure(bg='#AFFFAF') + self.b_list[5].configure(bg='#AFFFAF') + break + elif x == 2: + self.b_list[6].configure(bg='#AFFFAF') + self.b_list[7].configure(bg='#AFFFAF') + self.b_list[8].configure(bg='#AFFFAF') + break + + # Check for 3 cross/nut in a column + for y in range(0, 3): + win_count = 0 + for x in range(0, 3): + if self.board.cross_nut_map[player] == self.board.grid_map[x][y]: + win_count = win_count + 1 + if win_count == 3: + if y == 0: + self.b_list[0].configure(bg='#AFFFAF') + self.b_list[3].configure(bg='#AFFFAF') + self.b_list[6].configure(bg='#AFFFAF') + break + elif y == 1: + self.b_list[1].configure(bg='#AFFFAF') + self.b_list[4].configure(bg='#AFFFAF') + self.b_list[7].configure(bg='#AFFFAF') + break + elif y == 2: + self.b_list[2].configure(bg='#AFFFAF') + self.b_list[5].configure(bg='#AFFFAF') + self.b_list[8].configure(bg='#AFFFAF') + break + + # Check for 3 cross/nut across diagonals + win_count = 0 + for i in range(0, 3): + if self.board.cross_nut_map[player] == self.board.grid_map[i][i]: + win_count = win_count + 1 + if win_count == 3: + self.b_list[0].configure(bg='#AFFFAF') + self.b_list[4].configure(bg='#AFFFAF') + self.b_list[8].configure(bg='#AFFFAF') + return + + win_count = 0 + if self.board.cross_nut_map[player] == self.board.grid_map[0][2]: + win_count = win_count + 1 + if self.board.cross_nut_map[player] == self.board.grid_map[1][1]: + win_count = win_count + 1 + if self.board.cross_nut_map[player] == self.board.grid_map[2][0]: + win_count = win_count + 1 + + if win_count == 3: + self.b_list[2].configure(bg='#AFFFAF') + self.b_list[4].configure(bg='#AFFFAF') + self.b_list[6].configure(bg='#AFFFAF') + return + + + +# //////////////////////////////////////////////////////////////////////////////////////////// +if __name__ == "__main__": + print("Please minimize this window.") + window = GUI(background='grey', font_size=10) + window.menu_bar() + window.heading_label(padding=2) + window.status_bar(padding=2) + window.play_grid(padding=40) + window.remove_mark() + window.mainloop() \ No newline at end of file