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
+
## 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