#Joseph Taylor
#15/06/17 - Initial commit.
#01/10/17 - Code completed.
#An AI that always wins or draws at 'Naughts and Crosses'.
import tkinter as tk
import random as rand
import time
whoGoesFirst = rand.randint(0,1)
toggle = rand.randint(0,1)
loopnum = 0
window = tk.Tk() #Main window now called 'window' as variable name
window.title("Naughts and Crosses") #Give window a name
window.attributes('-fullscreen',True) #Make auto-fullscreen
window.configure(background='floral white')
window.update() #Updating the window refreshes it so changes are visualised. Without this the window will never, visually, change.
height = window.winfo_height() #Get height of monitor being used.
width = window.winfo_width() #Get width of monitor being used.
framed = round(height / 5) ##Making some monitor-specific
gap1 = round(framed / 5) ##variables so that the layout
gap2 = round((width - (3*(framed)) - (2*(gap1)))/2) ##is scaled and works perfectly
gap3 = round((height - (3*(framed)) - (2*(gap1)))/2) ##on every monitor.
def fill(ButNum): #Function that gets called when buttons are pressed by user.
global toggle1 #toggle1 tries to be local if this line not included.
global whoGoesFirst #whoGoesFirst tries to be local of this line not included.
if toggle1 == 0 and toggle2 == 0: #The point of toggle1: Buttons will not do anything when toggle1 is 0
if ButNum['font'] == ("{Times New Roman} 50"): #Test if button is empty. Button fonts are only changed to size 50 when nought or cross is placed.
return #In this case, button isn't empty, so don't try to change it. toggle1 is still 0 so PC cannot play.
else: #If the button IS empty...
ButNum['text'] = letter2 #Change button text to user's symbol, either a nought or a cross due to random generation.
ButNum['font'] = ("Times New Roman", 50) #Change font size to 50 so that nought/cross takes up entire button.
whoGoesFirst = 0 #Toggling this means the computer can play.
return #End function
def smartPlay(): #Function that gets called on the computers 2nd, 3rd, and all subsequent turns.
global gridRay ##These variables
global gridNo ##try to be local if these lines
global whoGoesFirst ##aren't included.
array = [0,0,0,0,0,0,0,0,0] #initialising an array of length 9, one for each button.
for x in range(0,9): ##This section of the code essentially
if gridRay[x]['text'] == '': ##allocates a number to each button.
array[x] = 0 ##Any button that is empty is assigned
elif gridRay[x]['text'] == letter: ##the value '0', and button that contains
array[x] = 1 ##the symbol the computer is playing is
elif gridRay[x]['text'] == letter2: ##assigned the value '1', and any button
array[x] = 5 ##that contains the symbol the player is using is assinged the value '5'.
topRow = array[0] + array[1] + array[2] ##Now we calculate the value
midRow = array[3] + array[4] + array[5] ##of each row, column and the
botRow = array[6] + array[7] + array[8] ##diagonals based in the above
leftCol = array[0] + array[3] + array[6] ##allocations of 0, 1 and 5.
midCol = array[1] + array[4] + array[7] ##This allows us to refer to an
rightCol = array[2] + array[5] + array[8] ##entire row as being equal to, for
diag1 = array[0] + array[4] + array[8] ##examlple, 10, which must mean it
diag2 = array[2] + array[4] + array[6] ##contains 2 of the players symbols, and a blank space.
if topRow == 2: ##A very repetitive section of the code.
for x in range(0,3): ##This part of 'smartPlay' is checking
if gridRay[x]['text'] == "": ##each row, column and diagonal to see
gridRay[x]['text'] = letter ##if any are equal to '2', which would
gridRay[x]['font'] = ("Times New Roman", 50) ##represent a blank space, and 2 of the
elif midRow == 2: ##computer's symbols. If any are equal to
for x in range(3,6): ##2, the computer can win by then checking
if gridRay[x]['text'] == "": ##each button in that row/column/diagonal to
gridRay[x]['text'] = letter ##see which one is empty, and will play
gridRay[x]['font'] = ("Times New Roman", 50) ##in that space.
elif botRow == 2:
for x in range(6,9):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif leftCol == 2:
for x in range(0,7,3):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif midCol == 2:
for x in range(1,8,3):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif rightCol == 2:
for x in range(2,9,3):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif diag1 == 2:
for x in range(0,9,4):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif diag2 == 2:
for x in range(2,7,2):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif topRow == 10: ##In this section the value chages to 10.
for x in range(0,3): ##(if topRow == 10, instead of == 2.)
if gridRay[x]['text'] == "": ##At this stage, the computer has checked
gridRay[x]['text'] = letter ##and found that it can't win yet, so it
gridRay[x]['font'] = ("Times New Roman", 50) ##now checks to see if the player can win.
elif midRow == 10: ##If they can, the value of the row/column/
for x in range(3,6): ##diagonal will be 10, as remember, the player's
if gridRay[x]['text'] == "": ##symbol is represented by a value of 5.
gridRay[x]['text'] = letter ##Once again, if it finds that the player
gridRay[x]['font'] = ("Times New Roman", 50) ##can win, it will stop them from winning
elif botRow == 10: ##by finding the blank space, and playing in
for x in range(6,9): ##it.
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif leftCol == 10:
for x in range(0,7,3):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif midCol == 10:
for x in range(1,8,3):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif rightCol == 10:
for x in range(2,9,3):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif diag1 == 10:
for x in range(0,9,4):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif diag2 == 10:
for x in range(2,7,2):
if gridRay[x]['text'] == "":
gridRay[x]['text'] = letter
gridRay[x]['font'] = ("Times New Roman", 50)
elif (gridRay[0]['text'] or gridRay[1]['text'] or gridRay[2]['text'] or gridRay[3]['text'] or gridRay[5]['text'] or gridRay[6]['text'] or gridRay[7]['text'] or gridRay[8]['text']) == letter2 and gridRay[4]['text'] == '':
gridRay[4]['text'] = letter
gridRay[4]['font'] = ("Times New Roman", 50)
elif diag1 == 11 and (topRow == 5 and botRow == 5 and leftCol == 5 and rightCol == 5): ##This section of code blocks a very specific
side = True ##scenario where the player can win. In the PCs
while side == True: ##Initial turn, if the player goes first and plays
gridNo = rand.choice([1, 3, 5, 7]) ##anywhere but the middle, the computer will play in
#Chooses one of the 4 sides to play ##in the middle. The player can utilise this by playing
but = gridRay[gridNo] ##in 2 opposite corners, either side of the middle,
#'but' is the name of the button currently being changed ##to which the computer would play in one of the other
if but['font'] == ("{Times New Roman} 50"): ##2 corners. This section of code stops the computer
side = True ##playing in the corners in that specific scenario.
else:
but['text'] = letter
but['font'] = ("Times New Roman", 50)
side = False
elif diag1 == 11 and (midRow == 5 and midCol == 5 and diag2 == 5): ##This section of code blocks a very specific
side = True ##scenario where the player can win. In the PCs
while side == True: ##Initial turn, if the player goes first and plays
gridNo = rand.choice([0, 2, 6, 8]) ##in the middle, the computer will play in
#Chooses one of the 4 sides to play ##in a random corner. The player can utilise this by playing
but = gridRay[gridNo] ##in the opposite corner, and the PC would sometimes
#'but' is the name of the button currently being changed ##proceed to play on one of the edges. If this happened,
if but['font'] == ("{Times New Roman} 50"): ##the player could win. If the PC played in the corner
side = True ##instead, it would end in a draw. This makes it play
else: ##in the corner every time.
but['text'] = letter
but['font'] = ("Times New Roman", 50)
side = False
elif diag2 == 11 and (topRow == 5 and botRow == 5 and leftCol == 5 and rightCol == 5):
side = True
while side == True:
gridNo = rand.choice([1, 3, 5, 7])
#Chooses one of the 4 sides to play
but = gridRay[gridNo]
#'but' is the name of the button currently being changed
if but['font'] == ("{Times New Roman} 50"):
side = True
else:
but['text'] = letter
but['font'] = ("Times New Roman", 50)
side = False
elif diag2 == 11 and (midRow == 5 and midCol == 5 and diag1 == 5):
side = True
while side == True:
gridNo = rand.choice([0, 2, 6, 8])
#Chooses one of the 4 sides to play
but = gridRay[gridNo]
#'but' is the name of the button currently being changed
if but['font'] == ("{Times New Roman} 50"):
side = True
else:
but['text'] = letter
but['font'] = ("Times New Roman", 50)
side = False
else: ##The computer's default move. If the computer doesn't
y = 0 ##win, or block the player, and does not detect the
for x in range(0,9): ##situation explained in the previous paragraph, it does
if gridRay[x]['text'] != '': ##this by default: Picks a corner at random, and plays in it.
y = y + 1 ##If there are no corners to play in, it will detect this, and
if y == 9: ##play on the side or in the middle instead.
return() ##The for loop at the start of this section is to detect if the
else: ##board is actually already full, so as not get caught in a
corn = True ##loop forever. The 'corn' variable means 'corners', as that's
x1 = 0 ##the default playing position. x1, x2, x3 and x4 are to test
x2 = 0 ##for if the computer has tried, and failed, to play a corner.
x3 = 0 ##once all these variables are equal to 1 and not 0, the computer
x4 = 0 ##will start to try playing in the sides and middle instead. But
while corn == True: ##while any of them are still 0, it will keep testing the corners.
if (x1 == 0 or x2 == 0 or x3 == 0 or x4 == 0):
#In other words, if not all of the corners have been tested yet
gridNo = rand.choice([0, 2, 6, 8])
#Choose one of the 4 corners to play
but = gridRay[gridNo]
#'but' is the name of the button currently being changed
if gridNo == 0:
x1 = 1 #Changes if the top left corner has been tested.
elif gridNo == 2:
x2 = 1 #Changes if the top right corner has been tested.
elif gridNo == 6:
x3 = 1 #Changes if the bottom left corner has been tested.
else:
x4 = 1 #Changes if the bottom right corner has been tested.
else: #If all the corners have been tested, and failed
gridNo = rand.choice([1, 3, 4, 5, 7])
#Choose anything but one of the 4 corners to play
but = gridRay[gridNo]
#'but' is the name of the button currently being changed
if but['font'] == ("{Times New Roman} 50"):
corn = True
#If the button already has a symbol in it, go round again and generate a different button
else:
but['text'] = letter ##Changes the button text to the value the
but['font'] = ("Times New Roman", 50) ##computer is using.
corn = False
whoGoesFirst = 1 ##Here at the very end of the function, whoGoesFirst is set to 1 so that
toggle1 = 0 ##the computer can't play twice in a row. This gets set back to 0 after
return() ##the user clicks on any empty button.
def hasWon(): ##Function that tests if all the symbols in a given row, column or diagonal
global gridRay ##are the same. Called once just before the computer plays, and again just
global Title ##after the computer plays.
global toggle2
for x in range(0,9,3):
if (gridRay[x]['text'] == letter and gridRay[x+1]['text'] == letter and gridRay[x+2]['text'] == letter): ##in this first for loop, x takes on the values
Title['text'] = ("Computer wins!") ##of 0, 3 and 6, which are the numerical equivalent
Reset['text'] = ("Play again?") ##values of the first buttons of each row. Then, the
toggle1 = 1 ##buttons of value x, x+1 and x+2 (being the 3 buttons
toggle2 = 1 ##
return(1) ##in a given row) will be tested, and if they all match
elif (gridRay[x]['text'] == letter2 and gridRay[x+1]['text'] == letter2 and gridRay[x+2]['text'] == letter2): ##then the game will end. Returning a value of 1
Title['text'] = ("Player wins!") ##breaks main program's while loop, and setting
Reset['text'] = ("Play again?") ##toggle1 equal to 1 will stop the user from clicking
toggle1 = 1 ##any more buttons.
toggle2 = 1
return(1)
for x in range(0,3):
if (gridRay[x]['text'] == letter and gridRay[x+3]['text'] == letter and gridRay[x+6]['text'] == letter): ##The maths and methods for these other 2 loops are
Title['text'] = ("Computer wins!") ##very similar and hopefully can be figured out from the
Reset['text'] = ("Play again?") ##context of the first paragraph, some maths skills and
toggle1 = 1 ##some common sense.
toggle2 = 1
return(1)
elif (gridRay[x]['text'] == letter2 and gridRay[x+3]['text'] == letter2 and gridRay[x+6]['text'] == letter2):
Title['text'] = ("Player wins!")
Reset['text'] = ("Play again?")
toggle1 = 1
toggle2 = 1
return(1)
for x in range(0,3,2):
if (gridRay[x]['text'] == letter and gridRay[4]['text'] == letter and gridRay[8-x]['text'] == letter):
Title['text'] = ("Computer wins!")
Reset['text'] = ("Play again?")
toggle1 = 1
toggle2 = 1
return(1)
elif (gridRay[x]['text'] == letter2 and gridRay[4]['text'] == letter2 and gridRay[8-x]['text'] == letter2):
Title['text'] = ("Player wins!")
Reset['text'] = ("Play again?")
toggle1 = 1
toggle2 = 1
return(1)
return(0)
def reset():
global gridRay
global toggle1
global toggle2
global whoGoesFirst
for x in range(0,9):
gridRay[x]['text'] = ''
gridRay[x]['font'] = ("{Times New Roman} 15")
Title['text'] = "Naughts and Crosses"
Reset['text'] = "Reset"
whoGoesFirst = rand.randint(0,1)
if whoGoesFirst == 1:
toggle1 = 0
toggle2 = 0
def quitGame():
window.destroy()
exit()
f = tk.Frame(window, height=framed, width=framed) #Create a frame to put a button in
f.pack_propagate(0) #The frame will try to fit to the smallest space possible, unless this line is involved.
f.grid(row=1, column=1, padx=(gap2,0), pady=(0,0)) #Position in the grid. First row, first column, for this is button number 1.
f2 = tk.Frame(window, height=framed, width=framed)
f2.pack_propagate(0)
f2.grid(row=1, column=2, padx=(gap1,0), pady=(0,0))
f3 = tk.Frame(window, height=framed, width=framed)
f3.pack_propagate(0)
f3.grid(row=1, column=3, padx=(gap1,0), pady=(0,0))
f4 = tk.Frame(window, height=framed, width=framed)
f4.pack_propagate(0)
f4.grid(row=2, column=1, padx=(gap2,0), pady=(gap1,0))
f5 = tk.Frame(window, height=framed, width=framed)
f5.pack_propagate(0)
f5.grid(row=2, column=2, padx=(gap1,0), pady=(gap1,0))
f6 = tk.Frame(window, height=framed, width=framed)
f6.pack_propagate(0)
f6.grid(row=2, column=3, padx=(gap1,0), pady=(gap1,0))
f7 = tk.Frame(window, height=framed, width=framed)
f7.pack_propagate(0)
f7.grid(row=3, column=1, padx=(gap2,0), pady=(gap1,0))
f8 = tk.Frame(window, height=framed, width=framed)
f8.pack_propagate(0)
f8.grid(row=3, column=2, padx=(gap1,0), pady=(gap1,0))
f9 = tk.Frame(window, height=framed, width=framed)
f9.pack_propagate(0)
f9.grid(row=3, column=3, padx=(gap1,0), pady=(gap1,0))
fTitle = tk.Frame(window, height=(gap3), width=(gap3 + gap1)) ##Those monitor-specific variables initialised at the start of
fTitle.pack_propagate(0) ##the program are used for determining the height, width and
fTitle.grid(row=0, column=2, columnspan=1, padx=(gap1,0), pady=(0,0)) ##padding on the buttons. This is one of the more complicated
##buttons.
fReset = tk.Frame(window, height=(gap3 - (2*gap1)), width=(gap3 - gap1))
fReset.pack_propagate(0)
fReset.grid(row=4, column=1, columnspan=1, padx=(gap2,0), pady=(gap1,0))
fQuit = tk.Frame(window, height=(gap3 - (2*gap1)), width=(gap3 - gap1))
fQuit.pack_propagate(0)
fQuit.grid(row=4, column=3, columnspan=1, padx=(gap1,0), pady=(gap1,0))
Grid1 = tk.Button(f, command=lambda: fill(Grid1), bg='light cyan') #Create a plain, blank button and place it in the specified frame (in this case, frame 'f')
Grid1.pack(fill=tk.BOTH, expand=1) #This line means the button fills the whole of the space of the frame it is in.
Grid2 = tk.Button(f2, command=lambda: fill(Grid2), bg='light cyan') ##Command=lambda: fill(Grid2) means that when the button is pressed, call a function, and that
Grid2.pack(fill=tk.BOTH, expand=1) ##function is 'fill' from the top of the document. Each button passes itself into the function.
Grid3 = tk.Button(f3, command=lambda: fill(Grid3), bg='light cyan')
Grid3.pack(fill=tk.BOTH, expand=1)
Grid4 = tk.Button(f4, command=lambda: fill(Grid4), bg='light cyan')
Grid4.pack(fill=tk.BOTH, expand=1)
Grid5 = tk.Button(f5, command=lambda: fill(Grid5), bg='light cyan')
Grid5.pack(fill=tk.BOTH, expand=1)
Grid6 = tk.Button(f6, command=lambda: fill(Grid6), bg='light cyan')
Grid6.pack(fill=tk.BOTH, expand=1)
Grid7 = tk.Button(f7, command=lambda: fill(Grid7), bg='light cyan')
Grid7.pack(fill=tk.BOTH, expand=1)
Grid8 = tk.Button(f8, command=lambda: fill(Grid8), bg='light cyan')
Grid8.pack(fill=tk.BOTH, expand=1)
Grid9 = tk.Button(f9, command=lambda: fill(Grid9), bg='light cyan')
Grid9.pack(fill=tk.BOTH, expand=1)
Title = tk.Label(fTitle, text="Naughts and Crosses", font=("Times new roman", 15),bg='floral white') ##The title and reset buttons some unique feature like text and font
Title.pack(fill=tk.BOTH, expand=1) ##size, and the lack of a command, so that clicking the title does
##nothing, as intended.
Reset = tk.Button(fReset, command=lambda: reset(), text="Reset", font=("Times new roman", 15), bg='light goldenrod')
Reset.pack(fill=tk.BOTH, expand=1)
Quit = tk.Button(fQuit, command=lambda: quitGame(), text="Quit", font=("Times new roman", 15), bg='light goldenrod')
Quit.pack(fill=tk.BOTH, expand=1)
if toggle == 1: ##Random generation.
letter = 'X' ##The user can be
letter2 = 'O' ##noughts or crosses,
else: ##as with the computer,
letter = 'O' ##and it's random as
letter2 = 'X' ##to which thakes the
##first go.
if whoGoesFirst == 0:
toggle1 = 1
else:
toggle1 = 0
toggle2 = 0
while True:
#Play the game until the loop is broken (which will happen when the computer detects either a winner or a draw)
while True:
gridRay = [Grid1, Grid2, Grid3, Grid4, Grid5, Grid6, Grid7, Grid8, Grid9]
#Array can be referenced to refer to any button
gridNo = 0
#Initialising Array index
window.update()
#Window must be updated every loop
time.sleep(0.05)
#Creating a small delay
breakOrNot = hasWon()
if breakOrNot == 1:
break
if whoGoesFirst == 0 and toggle2 == 0:
loopnum = loopnum + 1
#Determines how many times this part of the code has been executed
if loopnum == 1:
if (gridRay[0]['text'] or gridRay[1]['text'] or gridRay[2]['text'] or gridRay[3]['text'] or gridRay[5]['text'] or gridRay[6]['text'] or gridRay[7]['text'] or gridRay[8]['text']) == letter2:
gridRay[4]['text'] = letter
gridRay[4]['font'] = ("Times New Roman", 50)
whoGoesFirst = 1
toggle1 = 0
else:
gridNo = rand.choice([0, 2, 6, 8])
#Chooses one of the 4 corners as first play
but = gridRay[gridNo]
#'but' is the name of the button currently being changed
if but['font'] == ("{Times New Roman} 50"):
whoGoesFirst = 0
#Loop again if space is taken already, and play in a different corner
else:
but['text'] = letter
but['font'] = ("Times New Roman", 50)
whoGoesFirst = 1
toggle1 = 0
elif loopnum >= 1:
smartPlay()
breakOrNot = hasWon()
if breakOrNot == 1:
break
y = 0
for x in range(0,9):
if gridRay[x]['text'] != '':
y = y + 1
if y == 9:
Title['text'] = ("Draw!")
Reset['text'] = ("Play again?")
break
window.mainloop()
The setup.py is as follows, I don't entirely know what I'm doing when it comes to it so this is based off a few examples I found on the internet.
from cx_Freeze import setup, Executable
import os
import sys
base = None
if sys.platform == "win32":
base = "Win32GUI"
executables = [Executable("Naughts and Crosses AI.py", base=base)]
packages = ["os", "tkinter"]
build_exe_options = {"packages": packages, "includes": ["tkinter"]}
os.environ['TCL_LIBRARY'] = r'C:\Users\Joseph Taylor\AppData\Local\Programs\Python\Python35-32\tcl\tcl8.6'
os.environ['TK_LIBRARY'] = r'C:\Users\Joseph Taylor\AppData\Local\Programs\Python\Python35-32\tcl\tk8.6'
setup(
name = "Naughts and Crosses",
options = {"build_exe": build_exe_options},
version = "3.5.2",
description = 'Please work',
executables = executables
)