~melmon/c4ai

419f72e7eccc153cf516f5e7f0cfbe84fb8311f5 — Melmon 4 years ago 4f4b5be
Added code files
3 files changed, 943 insertions(+), 0 deletions(-)

A controller.py
A model.py
A view.py
A controller.py => controller.py +81 -0
@@ 0,0 1,81 @@
"""Controller file. This file acts as a communication between the Model and the View."""

import model

def getLocStatus(x, y):
    """Fetches the ID of an individual space within the board"""
    if model.theBoard.grid[y][x] == 0:
        return 0
    elif model.theBoard.grid[y][x] == 1:
        return 1
    elif model.theBoard.grid[y][x] == 2:
        return 2
    else:
        raise Exception("Foreign variable found within board")

def getColStatus(x):
    """Returns the availability of a column by checking whether the top space of a column is free"""
    if model.theBoard.grid[0][x] == 0:
        return True
    else:
        return False

def resetBoard():
    """Resets the board to a clean state"""
    for y in range(6):
        for x in range(7):
            model.theBoard.grid[y][x] = 0
            model.theBoard.currentplayer = 1
    for z in range(len(model.AILIST)):
        model.AILIST[z].forgetboard()

def fetchBoard():
    """Fetches the board from the model"""
    return model.theBoard.grid

def winCheck():
    """Returns whether a win has been achieved in the board"""
    return model.theBoard.winCheck()

def dropPiece(pos):
    """Sends a piece to the model for dropping"""
    model.theBoard.addPiece(pos, True)

def getAIDecision(index):
    """Gets the AI's decision for dropping a piece"""
    return model.AILIST[index].chooseDropPos()

def saveAI():
    """Saves the current AI profiles"""
    for x in model.AILIST:
        x.saveToFile(x.name)

def setGameType(gameType):
    """Sets game type"""
    model.theBoard.gameType = gameType

def sendAIpositions(pos1, pos2):
    """Sends the positions of the AI to the Model for use in AI vs AI"""
    model.gameAIPos = [pos1, pos2]

def getAIPlayerPos(name):
    """Returns the position of any AI during game play"""
    # 0: Miyagi
    # 1: Chizuru
    nameindex = 0
    if name == "Chizuru":
        nameindex = 1
    if model.theBoard.gameType == 0:
        return None
    elif model.theBoard.gameType == 1:
        return 2
    else:
        if model.gameAIPos[0] == nameindex:
            return 1
        elif model.gameAIPos[1] == nameindex:
            return 2
        else:
            return None

if __name__ == "__main__":
    print("Please run view.py to play")

A model.py => model.py +390 -0
@@ 0,0 1,390 @@
"""Model file. This file contains all of the functions that work with board management, and AI management"""

import pickle as pk
import random as rd
from copy import deepcopy
import controller
import math as ma

class Board:
    """Board class"""
    def __init__(self):
        """Constructor"""
        # 0. Empty Space
        # 1. P1 Piece
        # 2. P2 Piece
        self.grid = [[0 for _ in range(7)] for _ in range(6)]
        self.currentplayer = 1
        self.previouspos = 0
        # For AI purposes, shows what y pos the previous token was placed in
        self.gameType = 0
        # AI purposes
    
    def addPiece(self, pos: int, toRemember: bool):
        """Drops a piece in the selected column to the lowest available free space"""
        dropped = False
        ypos = 5
        while not dropped:
            if self.grid[ypos][pos] == 0:
                self.grid[ypos][pos] = self.currentplayer
                dropped = True
            else:
                ypos -= 1
            if ypos < 0:
                raise Exception("Column full; no error checking for full columns")
        if self.currentplayer == 1:
            self.currentplayer = 2
        else:
            self.currentplayer = 1
        self.previouspos = pos
        if toRemember:
            for x in range(len(AILIST)):
                AILIST[x].rememberCurrentBoard(self.clone())
    
    def winCheck(self):
        """Checks the board for any four adjacent identical pieces"""
        # Returns:
        # 0. None
        # 1. Player 1
        # 2. Player 2
        # 3. Draw
        draw = True
        for checkdraw in range(7):
            if not self.colFullCheck(checkdraw):
                draw = False
        if draw:
            return 3
        for y in range(6):
            for x in range(7):
                if self.grid[y][x] != 0:
                    # Horz victory
                    try:
                        if self.grid[y][x+1] == self.grid[y][x] and self.grid[y][x+2] == self.grid[y][x]\
                        and self.grid[y][x+3] == self.grid[y][x]:
                            return self.grid[y][x]
                    except:
                        pass
                    # Vert victory
                    try:
                        if self.grid[y+1][x] == self.grid[y][x] and self.grid[y+2][x] == self.grid[y][x]\
                        and self.grid[y+3][x] == self.grid[y][x]:
                            return self.grid[y][x]
                    except:
                        pass
                    # \ Diag victory
                    try:
                        if self.grid[y+1][x+1] == self.grid[y][x] and self.grid[y+2][x+2] == self.grid[y][x]\
                        and self.grid[y+3][x+3] == self.grid[y][x]:
                            return self.grid[y][x]
                    except:
                        pass
                    # / Diag victory
                    try:
                        if self.grid[nonNegative(y-1)][x+1] == self.grid[y][x]\
                        and self.grid[nonNegative(y-2)][x+2] == self.grid[y][x]\
                        and self.grid[nonNegative(y-3)][x+3] == self.grid[y][x]:
                            return self.grid[y][x]
                    except:
                        pass
        return 0

    def colFullCheck(self, pos):
        """Checks if any column is full. Returns false if not full, returns true if full."""
        if self.grid[0][pos] == 0:
            return False
        else:
            return True

    def clone(self):
        """Clones the current state of the game."""
        new = Board()
        new.grid = deepcopy(self.grid)
        new.currentplayer = deepcopy(self.currentplayer)
        new.previouspos = deepcopy(self.previouspos)
        return new

    def __repr__(self):
        """repr function"""
        ret = ''
        for y in range(6):
            for x in range(7):
                ret += str(self.grid[y][x]) + ' '
            ret += '\n'
        ret += "WIN: " + str(self.winCheck())
        return ret

# noinspection PyTypeChecker
class Node:
    """Node class which will be used in the brain"""
    def __init__(self, parent=None, state: Board=Board()):
        """Constructor"""
        # where state must be the Board class and referenced when theBoard clones itself
        self.state = state
        self.children = [None for _ in range(7)]
        self.parent = parent
        self.visits = 0
        self.wins = 0
        self.payoff = 10 + rd.uniform(0, 0.15)

    def backprop(self, win=False):
        """Updates node and parents with new information"""
        self.visits += 1
        if win:
            self.wins += 1
        # This backpropagation is ignored if the node has no parent. Used for the root node
        if self.parent is not None:
            self.parent.backprop(win)
        self.updatePayoff()

    def updatePayoff(self):
        """Updates the payoff attribute of a single node"""
        if self.visits == 0:
            self.payoff = 10
        elif self.parent is not None:
            self.payoff = (self.wins / self.visits) + (ma.sqrt(2) * ma.sqrt(ma.log(self.parent.visits)/self.visits))
        if self.state.winCheck() == 3:
            self.payoff = 0

    def expand(self, pos: int, toRemember: bool):
        """Adds a child node"""
        temp = self.state.clone()
        temp.addPiece(pos, toRemember)
        self.children[pos] = Node(parent=self, state=deepcopy(temp))

    def getChild(self, pos):
        """Returns the child node found in a specific position"""
        try:
            return self.children[pos]
        except:
            return None

    def __repr__(self):
        return "V: " + str(self.visits) + " W: " + str(self.wins) + " P: " + str(self.payoff)

# noinspection PyTypeChecker
class Brain:
    """AI Brain class"""
    def __init__(self, name):
        """Constructor"""
        self.root = Node()
        self.playinstance = [] # for recording the current play instance history to aid the tree search.
                               # each input is a dropped token position.
        self.name = name

    def selection(self, anynode: Node, route):
        """Selects a route to take within the tree to reach a leaf"""
        # Going through a known route
        # checking for wins
        if anynode.state.winCheck() == 1:
            if controller.getAIPlayerPos(self.name) == 1:
                self.backpropagation(anynode, True)
            else:
                self.backpropagation(anynode, False)
        elif anynode.state.winCheck() == 2:
            if controller.getAIPlayerPos(self.name) == 2:
                self.backpropagation(anynode, True)
            else:
                self.backpropagation(anynode, False)
        elif anynode.state.winCheck() == 3:
            self.backpropagation(anynode, False)
        else:
            if len(route) != 0:
                if anynode.getChild(head(route)) is not None:
                    self.selection(anynode.getChild(head(route)), tail(route))
                else:
                    anynode.expand(head(route), False)
                    self.selection(anynode.getChild(head(route)), tail(route))
            # Going through an unknown route until a None is hit
            else:
                childsuccessrates = [None for _ in range(7)]
                for x in range(7):
                    try:
                        if anynode.state.colFullCheck(x):
                            childsuccessrates[x] = -1
                        else: # where t is an instance of a child
                            t = anynode.getChild(x)
                            t.updatePayoff()
                            childsuccessrates[x] = t.payoff
                    except:
                        childsuccessrates[x] = 10 + rd.uniform(0, 0.15)
                complete = False
                while not complete:
                    goto = highestPosInArray(childsuccessrates)
                    if anynode.getChild(goto) is not None:
                        if goto == -1:
                            raise Exception
                        self.selection(anynode.getChild(goto), [])
                        complete = True
                    else:
                        self.expansion(anynode, goto)
                        complete = True

    def expansion(self, currentnode: Node, pos: int):
        """Add a new child node for either a new learned state or a potential state and leads to simulation"""
        currentnode.expand(pos, False)
        self.simulation(currentnode.getChild(pos))

    def simulation(self, anynode: Node):
        """Runs simulations of a hypothetical occurance and backpropagates results"""
        playerID = controller.getAIPlayerPos(self.name)
        nodestateclone = anynode.state.clone()
        complete = False
        # Random playout
        if playerID is not None:
            while not complete:
                if nodestateclone.winCheck() != 0:
                    if nodestateclone.winCheck() == playerID:
                        # Win
                        complete = True
                        self.backpropagation(anynode, True)
                    else:
                        # Lose
                        complete = True
                        self.backpropagation(anynode, False)
                else:
                    try:
                        nodestateclone.addPiece(rd.randint(0, 6), False)
                    except:
                        pass
        else:
            raise Exception("playerID is None")

    @staticmethod
    def backpropagation(anynode: Node, victory):
        """Backpropagates the reults of one simulation to all the parent nodes where anynode is a leaf"""
        if anynode is not None:
            anynode.backprop(victory)
        else:
            return None

    def getStatePossibilities(self, anynode: Node, route):
        """Gets the current state's known children and their status"""
        if route:
            return self.getStatePossibilities(anynode.getChild(head(route)), tail(route))
        else:
            childsuccessrates = [10 for _ in range(7)]
            for x in range(7):
                if anynode.getChild(x) is not None:
                    t = anynode.getChild(x)
                    t.updatePayoff()
                    childsuccessrates[x] = t.payoff
            return childsuccessrates

    def MCTS(self):
        """Runs a lot of instances of the MCTS algorithm"""
        scalar = rd.uniform(0.7, 1)
        for run in range(rd.randint(420, ma.floor(720 * scalar))):
            self.selection(self.root, deepcopy(self.playinstance))

    def addHistory(self, instance):
        """Adds history"""
        self.playinstance.append(instance.previouspos)

    def resetHistory(self):
        """Resets history"""
        self.playinstance = []

class AI:
    """AI index class"""
    def __init__(self, name: str):
        """Constructor"""
        self.name = name
        self.brain = Brain(name)

    def forgetboard(self):
        """Forgets the board history"""
        self.brain.resetHistory()

    # def randomDropPos(self):
    #     """Randomly picks a dropping position"""
    #     drop = rd.randint(0, 6)
    #     self.brain.playinstance.append(drop)
    #     return drop

    def chooseDropPos(self):
        """Returns where the AI wishes to drop a piece."""
        self.brain.MCTS()
        childsuccessrates = self.brain.getStatePossibilities(self.brain.root, self.brain.playinstance)
        a = False
        while not a:
            if not theBoard.colFullCheck(highestPosInArray(childsuccessrates)):
                drop = highestPosInArray(childsuccessrates)
                return drop
            else:
                childsuccessrates[highestPosInArray(childsuccessrates)] = -1

    def rememberCurrentBoard(self, state: Board):
        """Adds any state's recently dropped piece to the brain's playinstance"""
        self.brain.playinstance.append(state.previouspos)

    def saveToFile(self, filename):
        """Saves the AI to an external file"""
        saver = open(str("ai/" + filename + ".c4ai"), "wb")
        pk.dump(self, saver)

def loadFromFile(filename):
    """Loads an AI from an external file"""
    loader = open(str("ai/" + filename + ".c4ai"), "rb")
    toreturn = pk.load(loader)
    return toreturn

def nonNegative(check):
    """Prevents negative indexing"""
    if check >= 0:
        return check
    else:
        raise Exception

def head(anything):
    """Returns the head of something"""
    if anything:
        return anything[0]
    else:
        return None

def tail(anything):
    """Returns the tail of something"""
    if len(anything) > 1:
        return anything[1:]
    else:
        return []

def highestPosInArray(anything):
    """Returns the respective position of the highest thing in an array"""
    if len(anything) == 1:
        return [0]
    elif not anything:
        raise Exception("Empty array inputted in the highestInArray() function.")
    else:
        highest = -1
        pos = 0
        for x in range(len(anything)):
            if anything[x] > highest:
                highest = anything[x]
                pos = x
        if highest == -1:
            return 0
        return pos

def posInArray(anything, tofind):
    """Returns the position of something in an array if applicable"""
    for x in range(len(anything)):
        if anything[x] == tofind:
            return x

theBoard = Board()
AILIST = []
gameAIPos = []

try:
    AILIST.append(loadFromFile("Miyagi"))
except:
    AILIST.append(AI("Miyagi"))

try:
    AILIST.append(loadFromFile("Chizuru"))
except:
    AILIST.append(AI("Chizuru"))

if __name__ == "__main__":
    print("Please run view.py to play")

A view.py => view.py +472 -0
@@ 0,0 1,472 @@
"""View file. This creates a GUI so that the user can interact with the game. It sends instrunctions
to the controller based on what the user inputs and updates based on what it receives from the Controller"""

import tkinter as tk
import tkinter.colorchooser as tkCol
import controller
import atexit

class MainMenu(tk.Frame):
    """Main menu class"""
    def __init__(self, *args, **kwargs):
        """Constructor"""
        tk.Frame.__init__(self, *args, **kwargs)
        lb1 = tk.Label(self, text="Connect Four")
        lb1.grid(row=0, padx=180, sticky='N')
        lb2 = tk.Label(self, text="Please pick an option")
        lb2.grid(row=1, sticky='N')
        # bt1, 2, 3 - gameplay buttons
        # bt4 - options button
        # bt5 - quit button
        bt1 = tk.Button(self, text="Human vs Human", command=lambda: FRAME2.initialiseGame(0)) # Game type HvH
        bt1.grid(row=4, ipadx=10, ipady=10, pady=5, sticky='N')
        bt2 = tk.Button(self, text="Human vs AI", command=lambda: FRAME2.initialiseGame(1)) # Game type HvA
        bt2.grid(row=5, ipadx=10, ipady=10, pady=5, sticky='N')
        bt3 = tk.Button(self, text="AI vs AI", command=lambda: FRAME2.initialiseGame(2)) # Game type AvA
        bt3.grid(row=6, ipadx=10, ipady=10, pady=5, sticky='N')
        bt4 = tk.Button(self, text="Options", command=lambda: FRAME3.show()) # Options menu
        bt4.grid(row=7, ipadx=3, ipady=3, pady=5, sticky='N')
        bt5 = tk.Button(self, text="Quit", command=lambda: MainMenu.quitting()) # Exit game
        bt5.grid(row=8, ipadx=3, ipady=3, pady=5, sticky='N')
    
    def show(self):
        """Lifts the Main Menu to the front"""
        self.lift()

    @staticmethod
    def quitting():
        """Saves all new data of the AI to their respective files then quits the game"""
        controller.saveAI()
        exit()

class GameBoard(tk.Frame):
    """Game Board and interactions class"""
    def __init__(self, *args, **kwargs):
        """Constructor"""
        tk.Frame.__init__(self, *args, **kwargs)
        self.board = []
        self.gameOver = False

        # storing names in a list for easy access
        self.names = [tk.StringVar(), tk.StringVar()]
        # humannames are for replacing AI names in the names array when accesing gametype 0 or 1
        self.humannames = [tk.StringVar(), tk.StringVar()]
        self.humannames[0].set("Human 1")
        self.humannames[1].set("Human 2")
        self.gameType = 0
        self.names[0].set("Human 1")
        self.names[1].set("Human 2")
        self.pl1score = tk.IntVar()
        self.pl1score.set(0)
        self.pl2score = tk.IntVar()
        self.pl2score.set(0)
        self.currentturn = tk.IntVar()
        self.currentturn.set(0)
        self.turnmsg = tk.StringVar()
        self.turnmsg.set("It is now " + self.names[0].get() + "'s turn...")

        # score display player 1
        sc1disp = tk.Label(self, textvariable=self.pl1score, bg='#FFFFFF', borderwidth=2, relief='ridge')
        sc1disp.grid(row=0, column=0, pady=7, ipadx=9, ipady=7)

        # name display player 1
        nam1disp = tk.Label(self, textvariable=self.names[0], bg='#FFFFFF', borderwidth=2,
                            relief='ridge', wraplength=52)
        nam1disp.grid(row=0, column=1, pady=7, ipadx=7, ipady=7, columnspan=2)

        # VS text display
        vs = tk.Label(self, text="VS")
        vs.grid(row=0, column=3)

        #score display player 2
        sc2disp = tk.Label(self, textvariable=self.pl2score, bg='#FFFFFF', borderwidth=2, relief='ridge')
        sc2disp.grid(row=0, column=6, pady=7, ipadx=9, ipady=7)

        # name display player 2
        nam2disp = tk.Label(self, textvariable=self.names[1], bg='#FFFFFF', borderwidth=2,
                            relief='ridge', wraplength=52)
        nam2disp.grid(row=0, column=4, pady=7, ipadx=7, ipady=7, columnspan=2)

        # turn display
        turndisp = tk.Label(self, textvariable=self.turnmsg, wraplength=170)
        turndisp.grid(row=1, ipadx=5, ipady=3, columnspan=7)

        # grid cells
        for y in range(6):
            self.board.append([])
            for x in range(7):
                self.board[y].append(tk.Label(self, text='    ', bg='#FFFFFF', borderwidth=2, relief='ridge'))
                self.board[y][x].grid(row=y+2, column=x, ipadx=10, ipady=10)

        # drop button row
        self.buttonarray = []
        self.buttonarray.append(tk.Button(self, text='V', command=lambda: self.sendPieceToDrop(0)))
        self.buttonarray.append(tk.Button(self, text='V', command=lambda: self.sendPieceToDrop(1)))
        self.buttonarray.append(tk.Button(self, text='V', command=lambda: self.sendPieceToDrop(2)))
        self.buttonarray.append(tk.Button(self, text='V', command=lambda: self.sendPieceToDrop(3)))
        self.buttonarray.append(tk.Button(self, text='V', command=lambda: self.sendPieceToDrop(4)))
        self.buttonarray.append(tk.Button(self, text='V', command=lambda: self.sendPieceToDrop(5)))
        self.buttonarray.append(tk.Button(self, text='V', command=lambda: self.sendPieceToDrop(6)))
        for bt in range(7):
            self.buttonarray[bt].grid(row=9, column=bt, ipadx=7, ipady=3)

        # other buttons
        retirebt = tk.Button(self, text="Retire", command=lambda: FRAME1.show())
        self.playagainbt = tk.Button(self, text="Play Again", command=lambda: self.playAgain())
        self.playagainbt.config(state='disabled')
        self.playagainbt.grid(row=10, column=2, columnspan=2, pady=15)
        retirebt.grid(row=10, column=0, columnspan=2, pady=15)

    def show(self):
        """Lifts the Game Board to the front"""
        self.lift()
    
    def initialiseGame(self, gameType):
        """Initialises game, including resetting the board, setting up any AI, etc."""
        # Game Types:
        # 0. Human vs Human
        # 1. Human vs AI
        # 2. AI vs AI
        self.gameOver = False
        self.gameType = gameType
        controller.setGameType(gameType)
        # Name setup
        self.names[0].set(self.humannames[0].get())
        self.names[1].set(self.humannames[1].get())
        if gameType == 1 or gameType == 2:
            self.names[1].set(controller.model.AILIST[AI1].name)
        if gameType == 2:
            self.names[0].set(controller.model.AILIST[AI1].name)
            self.names[1].set(controller.model.AILIST[AI2].name)

        controller.resetBoard()
        self.currentturn.set(0)
        self.pl1score.set(0)
        self.pl2score.set(0)
        self.buttonEnableDisable(0)
        self.playagainbt.config(state='disabled')        
        self.updateBoard()
        self.show()
        if gameType == 2:
            controller.sendAIpositions(AI1, AI2)
            self.buttonEnableDisable(1)
            self.gameOver = True
            self.playagainbt.config(state='active')
            self.turnmsg.set("Press 'Play Again' to start!")
            self.update_idletasks()
            self.AIprocess()
    
    def buttonEnableDisable(self, enabledisable):
        """Enables or disables the dropper buttons for when an AI is playing to prevent player interference"""
        # 0. Enable
        # 1. Disable
        for bt in range(7):
            if enabledisable == 0:
                self.buttonarray[bt].config(state='active')
            else:
                self.buttonarray[bt].config(state='disabled')
    
    def sendPieceToDrop(self, pos):
        """Sends a piece to drop to the Controller"""
        if controller.getColStatus(pos):
            # Column not full
            controller.dropPiece(pos)
            self.switchTurns()
            self.updateBoard()
            return True
        else:
            # Column full
            self.turnmsg.set("Cannot drop a piece there!")
            self.update_idletasks()
            # Runs another AI process if the AI tries to drop at a full column
            if self.gameType == 1:
                if self.currentturn == 1:
                    self.AIprocess()
            elif self.gameType == 2:
                self.AIprocess()
            return False
    
    def switchTurns(self):
        """Switches the player turns from player 1 to player 2 and vice versa and checks for a win"""
        if self.currentturn.get() == 0:
            self.currentturn.set(1)
            if self.gameType == 1 or self.gameType == 2:
                self.buttonEnableDisable(1)
                self.wincheck()
                self.updateBoard()
                self.AIprocess()
            else:
                self.wincheck()
        else:
            self.currentturn.set(0)
            self.buttonEnableDisable(0)
            if self.gameType == 2:
                self.buttonEnableDisable(1)
                self.wincheck()
                self.updateBoard()
                self.AIprocess()
            else:
                self.wincheck()

    def AIprocess(self):
        """The AI process, involving thinking about where to drop pieces"""
        if not self.gameOver:
            dropped = False
            while not dropped:
                dec = -1
                if self.gameType == 1:
                    dec = controller.getAIDecision(AI1)
                elif self.gameType == 2:
                    if self.currentturn.get() == 0:
                        dec = controller.getAIDecision(AI1)
                    elif self.currentturn.get() == 1:
                        dec = controller.getAIDecision(AI2)
                    else:
                        raise Exception("Piece is not dropped")
                else:
                    pass
                if dec != -1:
                    dropped = self.sendPieceToDrop(dec)
                else:
                    raise Exception("dec not reassigned")

    def updateBoard(self):
        """Fetches the board fron the Model via Controller and updates the visible board with the new information"""
        modelboard = controller.fetchBoard()
        for y in range(6):
            for x in range(7):
                if modelboard[y][x] == 0:
                    self.board[y][x].config(bg='#FFFFFF')
                elif modelboard[y][x] == 1:
                    self.board[y][x].config(bg=pl1col)
                elif modelboard[y][x] == 2:
                    self.board[y][x].config(bg=pl2col)
                else:
                    raise Exception("Foreign variable within the modelboard")
                if not self.gameOver:
                    self.turnmsg.set("It is now " + self.names[self.currentturn.get()].get() + "'s turn...")
                self.update_idletasks()
    
    def wincheck(self):
        """Checks for a win from the model board and parses the win procedure if a win is attained"""
        winningplayer = controller.winCheck()
        if winningplayer == 0:
            return False
        elif winningplayer == 3:
            self.draw()
            return True
        else:
            self.win(winningplayer-1)
            return True
    
    def win(self, player):
        """Winning procedure, disabling piece dropping, displaying a win message,
        adding score and enabling the play again button"""
        self.buttonEnableDisable(1)
        self.gameOver = True
        self.turnmsg.set(self.names[player].get() + " has won!")
        if player == 0:
            self.pl1score.set(self.pl1score.get() + 1)
        else:
            self.pl2score.set(self.pl2score.get() + 1)
        self.playagainbt.config(state='active')
        self.update_idletasks()
    
    def draw(self):
        """Draw procedure. Almost identical to the win procedure, except the different message,
        and no player earns score"""
        self.gameOver = True
        self.buttonEnableDisable(1)
        self.turnmsg.set("It's a draw!")
        self.playagainbt.config(state='active')
        self.update_idletasks()
    
    def playAgain(self):
        """Procedure for when the Play Again button is pressed"""
        controller.resetBoard()
        self.gameOver = False
        self.updateBoard()
        if self.gameType != 2:
            self.buttonEnableDisable(0)
        self.playagainbt.config(state='disabled')
        self.currentturn.set(0)
        self.turnmsg.set("It is now " + self.names[0].get() + "'s turn...")
        self.update_idletasks()
        if self.gameType == 2:
            self.AIprocess()

class OptionsMenu(tk.Frame):
    """Options menu class"""
    def __init__(self, *args, **kwargs):
        """Constructor"""
        tk.Frame.__init__(self, *args, **kwargs)
        somelabel = tk.Label(self, text="Options menu")
        somelabel.grid(row=0, padx=5, pady=5, sticky='NW')
        # P1 Name
        # p1setprompt - prompt
        p1setprompt = tk.Label(self, text="Human 1 Name:")
        p1setprompt.grid(row=1, column=0, sticky='W')
        # p1set - text entry
        p1set = tk.Entry(self)
        # p1setbt - button for saving name
        p1setbt = tk.Button(self, text="SET", command=lambda: self.changeName(0, p1set.get()))
        # p1disp - name preview
        self.p1disp = tk.StringVar()
        self.p1disp.set("Current: " + FRAME2.names[0].get())
        p1displab = tk.Label(self, textvariable=self.p1disp)
        p1set.grid(row=2, column=0)
        p1setbt.grid(row=2, column=1, ipadx=5)
        p1displab.grid(row=2, column=2, sticky='W')
        # P2 Name
        # p2setprompt - prompt
        p2setprompt = tk.Label(self, text="Human 2 Name:")
        p2setprompt.grid(row=3, column=0, sticky='W')
        # p2set - text entry
        p2set = tk.Entry(self)
        # p2setbt - button for saving name
        p2setbt = tk.Button(self, text="SET", command=lambda: self.changeName(1, p2set.get()))
        # p2disp - name preview
        self.p2disp = tk.StringVar()
        self.p2disp.set("Current: " + FRAME2.names[1].get())
        p2displab = tk.Label(self, textvariable=self.p2disp)
        p2set.grid(row=4, column=0)
        p2setbt.grid(row=4, column=1, ipadx=5)
        p2displab.grid(row=4, column=2, sticky='W')  
        # Col 1
        # lab3 - prompt
        lab3 = tk.Label(self, text="Player 1 Colour:")
        lab3.grid(row=5, column=0, sticky='W', pady=2)
        # bt1label - text within the button
        self.bt1label = tk.StringVar()
        self.bt1label.set(pl1col)
        # ex1 - example colour
        self.ex1 = tk.Label(self, text='     ', bg=pl1col, borderwidth=2, relief="ridge")
        bt1 = tk.Button(self, textvariable=self.bt1label, command=lambda: self.pickColour(0))
        bt1.grid(row=5, column=1, ipadx=5, sticky='E')
        self.ex1.grid(row=5, column=2, sticky='W')
        bt1.grid_columnconfigure(1, weight=2)
        # Col 2
        # lab4 - prompt
        lab4 = tk.Label(self, text="Player 2 Colour:")
        lab4.grid(row=6, column=0, sticky='W', pady=2)
        # bt2label - text within the button
        self.bt2label = tk.StringVar()
        self.bt2label.set(pl2col)
        # ex2 - example colour
        self.ex2 = tk.Label(self, text='     ', bg=pl2col, borderwidth=2, relief="ridge")
        bt2 = tk.Button(self, textvariable=self.bt2label, command=lambda: self.pickColour(1))
        bt2.grid(row=6, column=1, ipadx=5, sticky='E')
        self.ex2.grid(row=6, column=2, sticky='W')
        bt2.grid_columnconfigure(1, weight=2)
        # AI 1
        # lab1 - prompt
        lab1 = tk.Label(self, text="First AI to use:")
        lab1.grid(row=7, column=0, sticky='W', pady=2)
        # self.disp1 - text in dropdown
        self.disp1 = tk.StringVar(self)
        self.disp1.set("Miyagi")
        # dd1 - dropdown
        dd1 = tk.OptionMenu(self, self.disp1, "Miyagi", "Chizuru")
        dd1.grid(row=7, column=1, sticky='E')
        dd1.grid_columnconfigure(1, weight=2)
        # AI 2
        # lab2 - prompt
        lab2 = tk.Label(self, text="Second AI to use:")
        lab2.grid(row=8, column=0, sticky='W', pady=2)
        # self.disp2 - text in dropdown
        self.disp2 = tk.StringVar(self)
        self.disp2.set("Chizuru")
        # dd2 - dropdown
        dd2 = tk.OptionMenu(self, self.disp2, "Chizuru", "Miyagi")
        dd2.grid(row=8, column=1, sticky='E')
        dd2.grid_columnconfigure(1, weight=2)
        # Dropdown option menus based on a script from http://effbot.org/tkinterbook/optionmenu.htm
        # Go Back button
        btgoback = tk.Button(self, text="Go Back", command=lambda: self.setAI())
        btgoback.grid(row=9, columnspan=2, ipadx=8)
        btgoback.grid_columnconfigure(0, weight=3)
        lab5 = tk.Label(self, text="Changes are automatically saved.")
        lab5.grid(row=10, columnspan=2)
    
    def setAI(self):
        """Sets the AI based on user choice"""
        global AI1
        global AI2
        FRAME1.show()
        AI1 = self.disp1.get()
        if AI1 == 'Miyagi':
            AI1 = 0
        elif AI1 == 'Chizuru':
            AI1 = 1
        AI2 = self.disp2.get()
        if AI2 == 'Miyagi':
            AI2 = 0
        elif AI2 == 'Chizuru':
            AI2 = 1

    def pickColour(self, playerIndex):
        """Lets the user pick a colour via a pop up GUI"""
        colour = tkCol.askcolor()
        if playerIndex == 0:
            global pl1col
            pl1col = colour[1]
            self.bt1label.set(pl1col)
            self.ex1.config(bg=pl1col)
            self.update_idletasks()
        else:
            global pl2col
            pl2col = colour[1]
            self.bt2label.set(pl2col)
            self.ex2.config(bg=pl2col)
            self.update_idletasks()
    
    def changeName(self, nameindex, newname):
        """Changes the name of a human player"""
        FRAME2.names[nameindex].set(newname)
        FRAME2.humannames[nameindex].set(newname)
        self.p1disp.set("Current: " + FRAME2.humannames[0].get())
        self.p2disp.set("Current: " + FRAME2.humannames[1].get())
        self.update_idletasks()
    
    def show(self):
        """Lifts the Options menu to the front"""
        self.lift()

class Window(tk.Frame):
    """Window that keeps all of the pages"""
    def __init__(self, *args, **kwargs):
        """Constructor"""
        tk.Frame.__init__(self, *args, **kwargs)
        global FRAME1
        global FRAME2
        global FRAME3
        FRAME1 = MainMenu(self)
        FRAME2 = GameBoard(self)
        FRAME3 = OptionsMenu(self)
        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        FRAME1.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        FRAME2.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        FRAME3.place(in_=container, x=0, y=0, relwidth=1, relheight=1)
        FRAME1.show()

if __name__ == "__main__":
    pl1col = '#ff0000'
    pl2col = '#ffff00'
    AI1 = 0
    AI2 = 1
    # 0: Miyagi
    # 1: Chizuru
    Root = tk.Tk()
    mainWindow = Window(Root)
    mainWindow.pack(side="top", fill="both", expand=True)
    Root.wm_geometry("450x550")
    Root.title("Connect Four")
    Root.resizable(width=False, height=False)
    # Root.iconbitmap()
    Root.mainloop()
    atexit.register(MainMenu.quitting())

# Tkinter multi-page code structure taken from
# https://stackoverflow.com/questions/14817210/using-buttons-in-tkinter-to-navigate-to-different-pages-of-the-application