I've created a full Tic tac toe game in Tkinter that has single and multiplayer modes. The single player mode then has a hard and easy difficulty. The easy difficulty is just choosing a random open spot and the hard difficulty uses the minimax algorithm. On top of all that, the grid size for the game is adjustable, but only for odd numbers (so that diagonal wins are always possible). This is my first time creating tic tac toe and one of my first Tkinter projects, so I am not sure if my code is efficient or that organized. Any tips or feedback about how I could improve my code would be great. Here's my code:
Main.py
import gui
if (__name__ == "__main__"):
controller = gui.GUIController()
gui.window.option_add( "*font", "roboto 22")
controller.disHome()
Gui.py
from tkinter import *
from tkinter import font
from main import *
import sys
import TicTacToe
import Minimax
import copy
window = Tk()
window.config(padx=15, pady=15)
class GUIController:
def __init__(self):
self.backgroundColor = "#FA867A"
self.titleColor = "#000000"
self.frameColor = "#FFE6E8"
self.buttonTextColor = "#0A241C"
self.buttonColor = "#FA867A"
self.buttonPressedColor = "#fc4430"
self.buttonHovorColor = "#fc7365"
# Single = 0, multiplayer = 1, easy = 2, impossible = 3
self.buttonList = [[None, self.buttonColor], [None, self.buttonColor], [None, self.buttonColor], [None, self.buttonColor]]
self.turnLblX = None
self.turnLblO = None
self.tictactoe = None
self.minimax = None
self.gameMode = None
self.diff = None
self.sliderValue = 3
self.gridSlider = 3
def resetVariables(self):
# Single = 0, multiplayer = 1, easy = 2, impossible = 3
self.buttonList = [[None, self.buttonColor], [None, self.buttonColor], [None, self.buttonColor], [None, self.buttonColor]]
self.turnLblX = None
self.turnLblO = None
self.tictactoe = None
self.gameMode = None
self.diff = None
self.sliderValue = 3
self.gridSlider = 3
#Displaying the home window, is the first window called
def disHome(self):
# Is called at the beginning of each window function to clear the window before we draw on it again
self.clearWindow()
# Reset all values
self.resetVariables()
# Creating the frames
topFrame = Frame(window)
middleFrames = Frame(window, pady=10, relief=RIDGE, bd=2)
middleFrameTop = Frame(middleFrames, padx=15, pady=0)
middleFrameMid = Frame(middleFrames, padx=15, pady=0)
middleFrameBot = Frame(middleFrames, padx=15, pady=0)
bottomFrame = Frame(window)
# Display the title
Label(topFrame, text="Tic Tac Toe", bg=self.backgroundColor, fg=self.titleColor, font=("Roboto", 35)).grid(column=0, row=0, pady=15)
# Displaying Single Player Button
self.buttonList[0][0] = Button(middleFrameTop, text="Single Player", bg=self.buttonList[0][1], fg=self.buttonTextColor,
activebackground=self.buttonPressedColor, padx=10, pady=10, width=10, relief=GROOVE,
command=lambda x="s": self.setGameMode(x))
self.buttonList[0][0].grid(column=0, row=0, padx=2, pady=2)
# Displaying Multiplayer Button
self.buttonList[1][0] = Button(middleFrameTop, text="Multiplayer", bg=self.buttonColor, fg=self.buttonTextColor,
activebackground=self.buttonPressedColor, padx=10, pady=10, width=10, relief=GROOVE,
command=lambda x="m": self.setGameMode(x))
self.buttonList[1][0].grid(column=0, row=1, padx=2, pady=2)
# Displaying Easy Button
self.buttonList[2][0] = Button(middleFrameMid, text="Easy", bg=self.buttonColor, fg=self.buttonTextColor,
activebackground=self.buttonPressedColor, padx=10, pady=10, width=10, relief=GROOVE,
state=DISABLED if self.gameMode==None else NORMAL, command=lambda x=2: self.activateDiffBtns(x))
self.buttonList[2][0].grid(column=0, row=0, padx=2, pady=2)
# Displaying Impossible Button
self.buttonList[3][0] = Button(middleFrameMid, text="Hard", bg=self.buttonColor, fg=self.buttonTextColor,
activebackground=self.buttonPressedColor, padx=10, pady=10, width=10, relief=GROOVE,
state=DISABLED if self.gameMode==None else NORMAL, command=lambda x=3: self.activateDiffBtns(x))
self.buttonList[3][0].grid(column=1, row=0, padx=2, pady=2)
# Changing the button colors when the mouse is hovored over
for button in self.buttonList:
self.changeOnHover(button[0], self.buttonHovorColor, button[1])
# Displaying the slider to change the game grid size
Label(middleFrameBot, text="Enter Board Size:", bg=self.frameColor, fg=self.titleColor,).grid(column=0, row=0)
self.gridSlider = Scale(middleFrameBot, from_=3, to=21, orient=HORIZONTAL, bg=self.frameColor, fg=self.buttonTextColor,
activebackground=self.backgroundColor, troughcolor=self.buttonColor,
variable=self.sliderValue, command=self.updateSliderValue)
self.gridSlider.grid(column=0, row=1, sticky="nsew", padx=20, pady=2)
self.gridSlider.config()
# Display the exit button
Button(bottomFrame, text="Exit", bg="red", fg=self.buttonTextColor, width=10, command=lambda: sys.exit(), relief=GROOVE).grid(column=0, row=0, padx=5, pady=15, sticky="nsew")
# Displaying the start button
sB = Button(bottomFrame, text="Start", bg="#4ABF36", activebackground="#62FA47", fg=self.buttonTextColor,
width=10, relief=GROOVE, command=lambda: self.disGameWindow((self.gridSlider.get(), True)))
sB.grid(column=1, row=0, padx=5, pady=15, sticky="nsew")
self.changeOnHover(sB, "#56F222", "#4ABF36")
# Displaying the frames
topFrame.grid(column=0, row=0)
middleFrames.grid(column=0, row=1)
middleFrameTop.grid(column=0, row=0)
middleFrameMid.grid(column=0, row=1)
middleFrameBot.grid(column=0, row=2)
bottomFrame.grid(column=0, row=2)
# Setting the background colors
topFrame.configure(bg=self.backgroundColor)
middleFrames.configure(bg=self.frameColor)
middleFrameTop.configure(bg=self.frameColor)
middleFrameMid.configure(bg=self.frameColor)
middleFrameBot.configure(bg=self.frameColor)
bottomFrame.configure(bg=self.backgroundColor)
window.configure(bg=self.backgroundColor)
window.mainloop()
# Displaying the game window with proper grid size
def disGameWindow(self, p):
gridSize = p[0]
initial = p[1]
if (initial):
if (self.gameMode == None):
messagebox.showinfo("Error", "Please select a gamemode.")
self.disHome()
elif (self.gameMode == "s" and self.diff == None):
messagebox.showinfo("Error", "Please select a game difficulty when playing single player.")
self.disHome()
if (self.diff == 3 and gridSize > 3):
messagebox.showinfo("Error", "Impossible difficulty not avaliable for game sizes above 3 :(")
self.disHome()
self.tictactoe = TicTacToe.ttt(gridSize)
self.minimax = Minimax.Minimax(gridSize, self.tictactoe)
# Clear the current window
self.clearWindow()
topFrame = Frame(window)
gameFrame = Frame(window, padx=15, pady=15)
gridBackgroundFrame = Frame(gameFrame)
bottomFrame = Frame(window)
frames = []
self.buttonList = []
for i in range(gridSize):
(i)
frames.append([])
self.buttonList.append([])
for j in range(gridSize):
px = ((j != 0) * 2, (j != gridSize-1) * 2)
py = ((i != 0) * 2, (i != gridSize-1) * 2)
frames[i].append(Frame(gridBackgroundFrame, bg=self.frameColor,
width=int(400/gridSize), height=int(400/gridSize)))
frames[i][j].pack_propagate(False)
frames[i][j].grid(row=i, column=j, padx=px, pady=py)
self.buttonList[i].append(Button(frames[i][j], text=" ",
bg=self.frameColor, relief=FLAT,
command=lambda p=(i, j): self.buttonClicked(p)))
self.buttonList[i][-1].pack(expand=True, fill=BOTH)
self.buttonList[i][j]['font'] = self.findFontSize(self.tictactoe.gridSize)
for i in range(self.tictactoe.gridSize):
for j in range(self.tictactoe.gridSize):
self.buttonList[i][j]['text'] = self.displayLetter(i, j)
# Displaying the score and who's turn it is
self.turnLblX = Label(topFrame, text="Player" if self.gameMode=="s" else "Player 1", bg=str(self.buttonHovorColor) if self.tictactoe.turn==1 else str(self.frameColor), fg=self.titleColor, padx=5)
self.turnLblX.grid(column=0, row=0, sticky="nsew")
Label(topFrame, text="", bg=self.backgroundColor, padx=10).grid(column=1, row=0, sticky="nsew")
self.turnLblO = Label(topFrame, text="Computer" if self.gameMode=="s" else "Player 2", bg=str(self.buttonHovorColor) if self.tictactoe.turn==-1 else str(self.frameColor), fg=self.titleColor, padx=5)
self.turnLblO.grid(column=2, row=0, sticky="nsew")
# Displaying the exit button
Button(bottomFrame, text="Exit", bg="red", fg=self.buttonTextColor, width=10,
command=lambda: sys.exit(), relief=GROOVE).grid(column=0, row=0, padx=5, pady=15, sticky="nsew")
# Displaying the home button
sB = Button(bottomFrame, text="Home", bg="#4ABF36", activebackground="#62FA47", fg=self.buttonTextColor,
width=10, relief=GROOVE, command=self.disHome)
sB.grid(column=1, row=0, padx=5, pady=15, sticky="nsew")
self.changeOnHover(sB, "#56F222", "#4ABF36")
topFrame.grid(column=0, row=0, pady=15)
gameFrame.grid(column=0, row=1)
gridBackgroundFrame.grid(column=0, row=0)
bottomFrame.grid(column=0, row=2, pady=5, sticky="n")
# Drawing Background
topFrame.configure(bg=self.backgroundColor)
gameFrame.configure(bg=self.frameColor)
gridBackgroundFrame.configure(bg=self.buttonColor)
bottomFrame.configure(bg=self.backgroundColor)
window.mainloop()
# Called when the user clicks a grid in the game board
# Called when a grid button is clickedSB1206
def buttonClicked(self, index):
self.tictactoe.updateGameGrid(index)
self.buttonList[index[0]][index[1]]['text'] = ("X" if self.tictactoe.grid[index[0]][index[1]]==1 else "O")
if (self.gameMode == "s"):
if (self.tictactoe.winner != None):
self.disWinMessage(self.tictactoe.winner)
if (self.diff == 2 and self.tictactoe.turn == -1):
# Easy Difficulty, random choice for computer
self.buttonClicked(self.tictactoe.randomIndex())
elif (self.diff == 3 and self.tictactoe.turn == -1):
winner, nextMove = self.minimax.minimax(copy.deepcopy(self.tictactoe.grid), (-1, -1), 15, self.tictactoe.turn)
self.tictactoe.winner = None
self.buttonClicked(nextMove)
elif (self.gameMode == "m"):
if (self.tictactoe.winner != None):
self.disWinMessage(self.tictactoe.winner)
self.turnLblX['bg'] = (self.buttonHovorColor if self.tictactoe.turn==1 else self.frameColor)
self.turnLblO['bg'] = (self.buttonHovorColor if self.tictactoe.turn==-1 else self.frameColor)
# Called when there is a winner
def disWinMessage(self, winner):
if (self.gameMode == "s"):
if (winner == 1):
messagebox.showinfo("Winner", "Yayayayay YOU WON!!!")
elif (self.tictactoe.winner == -1):
messagebox.showinfo("Loser", "The computer took the victory this time")
elif (self.tictactoe.winner == 0):
messagebox.showinfo("Tie", "This match has resulted in a tie")
self.disHome()
else:
if (winner == 1):
messagebox.showinfo("Winner", "Player 1 has annihilated player 2!!")
elif (self.tictactoe.winner == -1):
messagebox.showinfo("Loser", "Player 2 has defeated player 1!!")
elif (self.tictactoe.winner == 0):
messagebox.showinfo("Tie", "This match has resulted in a tie")
self.disHome()
# Function to change properties of button on hover
def changeOnHover(self, button, colorOnHover, colorOnLeave):
# Adjusting backgroung of the widget
# Background on entering widget
button.bind("<Enter>", func=lambda e: button.config(
background=colorOnHover))
# Background color on leving widget
button.bind("<Leave>", func=lambda e: button.config(
background=colorOnLeave))
# Clears the entire window
def clearWindow(self):
for widget in window.winfo_children():
widget.destroy()
def findFontSize(self, size):
s = 22
if (size==3): s = 80
elif (5 <= size <= 9): s = 40
elif (11 <= size <= 15): s = 25
else: s = 18
f = font.Font(family="Roboto", size=s)
return f
def updateSliderValue(self, variable):
temp = int(variable)
if (temp%2==0):
self.gridSlider.set(temp+1)
def activateDiffBtns(self, d):
d = int(d)
if (self.buttonList[d][0]['state'] != DISABLED):
if (self.diff != None and (self.diff != d)):
i = int(d+1 if d==2 else d-1)
self.buttonList[i][1] = self.buttonColor
self.buttonList[i][0].configure(bg=self.buttonList[i][1])
self.changeOnHover(self.buttonList[i][0], self.buttonHovorColor, self.buttonList[i][1])
self.buttonList[d][1] = self.buttonPressedColor
self.buttonList[d][0].configure(bg=self.buttonList[d][1])
self.changeOnHover(self.buttonList[d][0], self.buttonHovorColor, self.buttonList[d][1])
self.diff = d
def setGameMode(self, m):
if (m == "s"):
self.diffButtonState(NORMAL)
if (m == "s"):
if (self.gameMode == None):
self.buttonList[0][1] = self.buttonPressedColor
self.gameMode = m
self.buttonList[1][0].configure(state=DISABLED)
elif (self.gameMode == "s"):
self.buttonList[0][1] = self.buttonColor
self.gameMode = None
self.diffButtonState(DISABLED)
self.buttonList[1][0].configure(state=NORMAL)
self.buttonList[0][0].configure(bg=self.buttonList[0][1])
self.changeOnHover(self.buttonList[0][0], self.buttonHovorColor, self.buttonList[0][1])
else:
if (self.gameMode == None):
self.buttonList[1][1] = self.buttonPressedColor
self.gameMode = m
self.buttonList[0][0].configure(state=DISABLED)
elif (self.gameMode == "m"):
self.buttonList[1][1] = self.buttonColor
self.gameMode = None
self.buttonList[0][0].configure(state=NORMAL)
self.buttonList[1][0].configure(bg=self.buttonList[1][1])
self.changeOnHover(self.buttonList[1][0], self.buttonHovorColor, self.buttonList[1][1])
def diffButtonState(self, state):
self.buttonList[2][0]['state'] = state
self.buttonList[3][0]['state'] = state
if (state == DISABLED):
self.buttonList[3][1] = self.buttonColor
self.buttonList[3][0].configure(bg=self.buttonList[3][1])
self.changeOnHover(self.buttonList[3][0], self.buttonHovorColor, self.buttonList[3][1])
self.buttonList[2][1] = self.buttonColor
self.buttonList[2][0].configure(bg=self.buttonList[2][1])
self.changeOnHover(self.buttonList[2][0], self.buttonHovorColor, self.buttonList[2][1])
def displayLetter(self, i, j):
if (self.tictactoe.grid[i][j] == 1):
return "X"
elif (self.tictactoe.grid[i][j] == -1):
return "O"
else:
return " "
Tictactoe.py
from random import randint
class ttt:
def __init__(self, size):
self.gridSize = size
self.grid = self.createGrid()
# If using minimax algorithm, user is maximizer(1) and computer is minimizer(-1)
# If single player, then user is 1, computer is -1
# If multiplayer, user1 is 1, user2 = -1
self.turn = 1
self.winner = None
def createGrid(self):
grid = []
for i in range(self.gridSize):
grid.append([])
for j in range(self.gridSize):
grid[i].append(0)
# grid = [[-1, 1, 0], [0, -1, 0], [0, 0, 0]]
return grid
def updateGameGrid(self, index):
if (self.grid[index[0]][index[1]] != 0):
return
self.grid[index[0]][index[1]] = self.turn
winner = self.findWinner(index, self.grid)
self.turn = -self.turn
def randomIndex(self):
x = randint(0, self.gridSize-1)
y = randint(0, self.gridSize-1)
while (self.grid[x][y] != 0):
x = randint(0, self.gridSize-1)
y = randint(0, self.gridSize-1)
return (x, y)
def findWinner(self, index, grid):
# Row
found = True
for j in range(self.gridSize-1):
if (grid[index[0]][j] != grid[index[0]][j+1] or grid[index[0]][j] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
# Column
found = True
for i in range(self.gridSize-1):
if (grid[i][index[1]] != grid[i+1][index[1]] or grid[i][index[1]] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
# Top Left to Bottom Right Diagonal
if (index[0] == index[1]):
found = True
for i in range(self.gridSize-1):
if (grid[i][i] != grid[i+1][i+1] or grid[i][i] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
# Top Right to Bottom Left Diagonal
if (index[0] + index[1] == self.gridSize-1):
found = True
for i in range(self.gridSize-1):
if (grid[self.gridSize-i-1][i] != grid[self.gridSize-i-2][i+1] or grid[self.gridSize-i-1][i] == 0):
found = False
break
if (found):
self.winner = self.turn
return self.turn
tie = True
for i in range(self.gridSize):
for j in range(self.gridSize):
if (grid[i][j] == 0):
tie = False
if (tie):
self.winner = 0
return 0
return None
Minimax.py
import copy
class Minimax:
def __init__(self, gs, t):
self.gridSize = gs
self.ttt = t
def minimax(self, state, currIndex, depth, turn):
if (currIndex[0] != -1 and currIndex[1] != -1):
winner = self.ttt.findWinner(currIndex, state)
if (winner == -1):
return winner - depth, currIndex
elif (winner == -1):
return winner + depth, currIndex
elif (winner == 0):
return 0, currIndex
if (depth==0 and winner==None):
return 0, currIndex
evalLimit = -turn * 1000
bestIndex = None
for i in range(self.gridSize):
for j in range(self.gridSize):
if (state[i][j] == 0):
state[i][j] = turn
eval, newIndex = self.minimax(state, (i, j), depth-1, -turn)
state[i][j] = 0
if (turn > 0 and eval > evalLimit):
bestIndex = newIndex
evalLimit = eval
elif (turn < 0 and eval < evalLimit):
bestIndex = newIndex
evalLimit = eval
return evalLimit, bestIndex