2
\$\begingroup\$

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
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$
  • ttt is not a good class name and should probably be renamed to TicTacToe. Conversely, Tictactoe is not a good name for a module and should be lowercase.
  • Add PEP484 type hints to your signatures, such as size: int.
  • createGrid should be create_grid by PEP8; similar for your other functions and variables.
  • Do not add parens around your if predicates; this is not (Java/C/C++/etc).
  • The colour members on GUIController seem like they should be statics on the class, like
class GUIController:
    TITLE_COLOUR = '#000000'

rather than instance members, since they don't change.

  • What does this statement do? It should probably be deleted:
            (i)
  • Try to avoid window being a global; pass it into your classes.

In terms of your user interface: you currently follow a pattern that sets the user up for failure. "Start" is enabled, but clicking on it first is guaranteed to produce an error message because the user hasn't selected anything yet. Instead, either

  • Disable "Start" until a valid selection is made, or
  • Start with default selections that will not produce an error, and leave "Start" enabled.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.