Python Forum
Adding a single player mode to my wxOthello game
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Adding a single player mode to my wxOthello game
#11
I think you are at a point that separating the game logic from the GUI would be a good idea, then you can work on the GUI and the logic of the game separately.
I made a GameBoard class that makes use of your priority map idea, but using higher values for more desired moves.
A score for each player is calculated based on the priority map for a board state.
This could be used to get a score for each valid move, a list of valid moves to scores could be chosen from,
the highest difficulty uses the highest score move from the list,
the lowest difficulty uses the lowest score move from the list.
Middle difficulty uses a middle of the list move.
Do you think this would be any good.

B_EMPTY = 0
B_PLAYER1 = 1
B_PLAYER2 = 2

BOARD_PRIORITY_MAP = [[100, 50, 90, 80, 80, 90, 50, 100],
                      [50, 40, 60, 70, 70, 60, 40, 50],
                      [90, 60, 100, 90, 90, 100, 60, 90],
                      [80, 70, 90, 10, 10, 90, 70, 80],
                      [80, 70, 90, 10, 10, 90, 70, 80],
                      [90, 60, 100, 90, 90, 100, 60, 90],
                      [50, 40, 60, 70, 70, 60, 40, 50],
                      [100, 50, 90, 80, 80, 90, 50, 100]]


class GameBoard(object):

    def __init__(self):
        self.board = []
        self.player1_score = 0
        self.player2_score = 0
        self.set_start_condition()
        self.calculate_board_score()
    
    def clear_board(self):
        new_board = []
        for _ in range(8):
            new_row = []
            for _ in range(8):
                new_row.append(B_EMPTY)
            new_board.append(new_row)
        self.board = new_board
        
    def set_start_condition(self):
        self.clear_board()
        self.board[3][3] = B_PLAYER2
        self.board[3][4] = B_PLAYER1
        self.board[4][3] = B_PLAYER1
        self.board[4][4] = B_PLAYER2
        
    def calculate_board_score(self):
        self.player1_score = 0
        self.player2_score = 0

        for row_index, row in enumerate(self.board):
            for col_index, col in enumerate(row):
                score = BOARD_PRIORITY_MAP[row_index][col_index]
                    
                if col == B_PLAYER1:
                    self.player1_score += score
                    
                elif col == B_PLAYER2:
                    self.player2_score += score
        
    def __repr__(self):
        text = ''
        for row in self.board:
            text += f'{row}\n'
        text = f'{text}Player1 Score = {self.player1_score}'
        text = f'{text}\nPlayer2 Score = {self.player2_score}'
        return text
                

if __name__ == '__main__':
    game_board = GameBoard()
    print(game_board)
    game_board.board[0][0] = B_PLAYER1  # give player 1 the top left corner
    game_board.calculate_board_score()
    print(game_board)
Output:
[0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 2, 1, 0, 0, 0] [0, 0, 0, 1, 2, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] Player1 Score = 20 Player2 Score = 20 [1, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 2, 1, 0, 0, 0] [0, 0, 0, 1, 2, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] Player1 Score = 120 Player2 Score = 20
Reply
#12
I put the capture counting in after selecting out the moves with the greatest strategic value and this code is the result:
class PlayerBot:

    """
    Instances of this class play as player 2 in single player mode.
    class PlayerBot:
        __init__(self, parent, difficulty)
        parent - and OthelloGameFrame instance
        difficulty - an integer between 0 and 100
    """
    # These nested lists map various levels of desirability to different areas of the gameboard
    # where 0 is most desirable, 1 is more desirable than 0 and so on. The nines in the center are
    # already occupied at the start of the game and their values aren't relevant here. The center
    # spaces (early game) are marked with desirability values 0 and 1. Midgame is 2, 3 and 4 with
    # the approches to the corners set to 1 to make the bot take those areas if at all possible
    # regardless of circumstances, and the corners being most desirable are marked with 0, with
    # 5 and 6 used for the buffers with the edge buffer given a value of 5 and the middle buffer
    # given a value of 6. This is actually the map that beat me.
    priorityMap = [[0,5,1,2,2,1,5,0],
                   [5,6,4,3,3,4,6,5],
                   [1,4,0,1,1,0,4,1],
                   [2,3,1,9,9,1,3,2],
                   [2,3,1,9,9,1,3,2],
                   [1,4,0,1,1,0,4,1],
                   [5,6,4,3,3,4,6,5],
                   [0,5,1,2,2,1,5,0]]

    def __init__ (self, parent, difficulty):
        self.parent = parent
        self.difficulty = difficulty

    def makeMove(self):
        """
        PlayerBot.makeMove(self)
        This method tries to make its best to make thepossible move given the current state
        of the game. It can be a bit of a numty sometimes, but I still love it :)
        """
        # Create a list of all currently valid moves as a list of tuples with the row at index 0
        # and the column at index 1 in each tuple.
        validMoves = []
        for row in range(8):
            for col in range(8):
                if self.parent.isValidMove(row, col, self.parent.player2Color, self.parent.player1Color)[0]:
                    validMoves.append( (row, col) )

        # If no valid move is available to the bot, give the next move to player 1.
        if validMoves == []:
            self.parent.player1ButtonClicked()
            return

        # The difficulty setting varies the probability of the bot making a 'mistake' That is,
        # making a purely random move from the set of all possible moves. Here, we just make a
        # 'mistake' if random.randint(0, 100) returns a number greater than the set difficulty.
        if random.randint(0, 100) < self.difficulty:
            # This code is pretty ugly. If anybody write a more elegant solution to the problem this code solves,
            # I would greatly appreciate it. Here, we get only the most desirable move from the set of all possible
            # moves and select randomly from them.
            highestPriorityMoves = [validMoves[0]]
            for i in validMoves:
                if self.priorityMap[i[0]][i[1]] < self.priorityMap[highestPriorityMoves[0][0]][highestPriorityMoves[0][1]]:
                    highestPriorityMoves = [i]
                elif self.priorityMap[i[0]][i[1]] == self.priorityMap[highestPriorityMoves[0][0]][highestPriorityMoves[0][1]]:
                    highestPriorityMoves.append(i)
            # Select a move from the moves with the highest strategic priority that will produce the most captures.
            # If more than one move will produce the same number of captures, select randomly from among them.
            bestMoves = [highestPriorityMoves[0]]
            for i in highestPriorityMoves:
                if self.countCaptures(i) > self.countCaptures(bestMoves[0]):
                    bestMoves = [i]
                elif self.countCaptures(i) == self.countCaptures(bestMoves[0]):
                    bestMoves.append(i)
            randomMove = bestMoves[random.randint(0, len(bestMoves) - 1)]
            self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])
        
        else:
            # Make a purely random move; a 'mistake.'
            randomMove = validMoves[random.randint(0, len(validMoves) - 1)]
            self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])

    def countCaptures (self, possibleMove):
        """
        PlayerBot.countCaptures(self, possibleMove):
            Where possibleMove is an integer tuple of length 2, this method returns an integer indicating
            the number of spaces the move would capture.
        """
        numCaptures = 1 # We already know that the gamepiece we put down will be an additional gamepiece of our color
        scanningDirections = ((-1, 0), (0, 1), (1, 0), (0, -1),     # We use the same scanning vectors used in OthelloGameFrame.makeMove
                              (-1, -1), (-1, 1), (1, 1), (1, -1))   # to look at all the gamepieces that would be flipped and count them up.
        for SDRow, SDCol in scanningDirections:
            canCountPieces = False
            sawOpponent = False
            currentRow = possibleMove[0] + SDRow
            currentCol = possibleMove[1] + SDCol
            while currentRow in range(0, 8) and currentCol in range(0, 8):
                if self.parent.gameboard[currentRow][currentCol].GetBackgroundColour() == self.parent.bgAndBoardColor: break
                if self.parent.gameboard[currentRow][currentCol].GetBackgroundColour() == self.parent.player1Color: sawOpponent = True
                if self.parent.gameboard[currentRow][currentCol].GetBackgroundColour() == self.parent.player2Color and sawOpponent:
                    canCountPieces = True
                if self.parent.gameboard[currentRow][currentCol].GetBackgroundColour() == self.parent.player2Color and not sawOpponent: break
                currentRow += SDRow
                currentCol += SDCol

            currentRow = possibleMove[0] + SDRow
            currentCol = possibleMove[1] + SDCol
            while canCountPieces and currentRow in range(0, 8) and currentCol in range(0, 8):
                if self.parent.gameboard[currentRow][currentCol].GetBackgroundColour() == self.parent.player1Color: numCaptures += 1
                if self.parent.gameboard[currentRow][currentCol].GetBackgroundColour() == self.parent.player2Color: break
                currentRow += SDRow
                currentCol += SDCol

        return numCaptures
It's noticeably better; definitely a formidable opponent to a novice but it seems all too willing to let me into the corners...

It definitely appears to have the intended aversion to putting pieces anywhere that would let me into the corners but it doesn't know how to stop me from forcing it to do that.

Welcome to the game dev forum, Mr. Yoriz. I agree that I'm at the point of separating game logic from GUI. That's why I wrote the PlayerBot class. The next logical step for improving this bot is determining how much player 2 stands to gain, both strategically and in the quantity of pieces he could capture after the bot makes its move and then selecting the most strategically effective move in terms of strategic and quantitative gain, and minimizing strategic and quantitative gain to the opponent.

It would take some serious refactoring of the PlayerBot class and implementation of an internally simulated gameboard like what you proposed, but I think this could be done with recursion. I'll share what I come up with.
Reply
#13
I've implemented some player action prediction in the bot and 2 algorithms designed to limit the players options to only the worst available to them. The problem is the bot is completely unable to set traps in the corners and create safe entrances to the corners for itself. I'm somewhat at a loss as to how to make it do these things algorithmically. The situation basically amounts to the bot sucks at endgame strategies. Any advice?

This is my PlayerBot class as it is currently. I think this is the final hurdle. The class now contains a suite of methods for getting information about the game state.
class PlayerBot:

    """
    Instances of this class play as player 2 in single player mode.
    class PlayerBot:
        __init__(self, parent, difficulty)
        parent - an OthelloGameFrame instance
        difficulty - an integer between 0 and 100
    """
    # These nested lists map various levels of desirability to different areas of the gameboard
    # where positive numbers indicate higher levels of desirability and negative numbers indicate
    # varying levels of undesirability. The larger the numbers absolute value, the more desirable/
    # undesirable that area of the gameboard.
    priorityMap = [[200, -200, 99, 80, 80, 99, -200, 200],
                   [-200, -300, -10, -5, -5, -10, -300, -200],
                   [99, -10, 200, 150, 150, 200, -10, 99],
                   [80, -5, 150, 100, 100, 150, -5, 80],
                   [80, -5, 150, 100, 100, 150, -5, 80],
                   [99, -10, 200, 150, 150, 200, -10, 99],
                   [-200, -300, -10, -5, -5, -10, -300, -200],
                   [200, -200, 99, 80, 80, 99, -200, 200]]

    bdCode = 0 # Constants for encoding empty board spaces, player 1 and player 2 in an internal
    p1Code = 1 # representation of the current game state.
    p2Code = 2

    def __init__ (self, parent, difficulty):
        self.parent = parent
        self.difficulty = difficulty
        self.gameState = self.getInitialGameState()

    def getInitialGameState (self):
        """
        PlayerBot.getInitialGameState (self):
        Returns 8 integer lists nested inside a list representing the initial state of a game
        of Othello, where 0 is an empty space, 1 is player 1, and 2 is player 2.
        """
        gameState = []
        for row in range(8):
            currentRow = []
            for col in range(8):
                currentRow.append(self.bdCode)
            gameState.append(currentRow)
            currentRow = []
        gameState[2][2] = self.p2Code
        gameState[2][3] = self.p1Code
        gameState[3][2] = self.p1Code
        gameState[3][3] = self.p2Code

        return gameState

    def makeMove(self):
        """
        PlayerBot.makeMove (self)
        This method makes a move in its parent as player2 and surrenders its move to player 1 if there are no
        moves available to it.
        """
        self.updateGameState() # Update the objects internal representation of the current state of the game.
        
        validMoves = [] # Create a list of all valid moves available to the bot.
        for row in range(8):
            for col in range(8):
                if self.isValidMove(row, col, self.p2Code, self.p1Code, self.gameState):
                    validMoves.append( (row, col) )

        if validMoves == []: # If there are no moves available to the bot, give player 1 its move.
            self.parent.player1ButtonClicked()
            return

        if random.randint(0, 100) < self.difficulty: # This outer if statement is used to make the occasional 'mistake'; a move
                                                     # selected randomly from the set of all valid moves. The frequency of these
                                                     # 'mistakes' is determined by the difficulty setting.
            
            strategicMoves = [ validMoves[0] ]
            for move in validMoves:
                if self.priorityMap[move[0]][move[1]] > self.priorityMap[strategicMoves[0][0]][strategicMoves[0][1]]:
                    strategicMoves = [ move ]
                elif self.priorityMap[move[0]][move[1]] == self.priorityMap[strategicMoves[0][0]][strategicMoves[0][1]]:
                    strategicMoves.append(move)

            profitableMoves = [strategicMoves[0]]
            player2Gain, gameStateAfterMove = self.scoreMove(strategicMoves[0], self.gameState, self.p2Code, self.p1Code)
            player1Gain = self.getMaxPlayer1Gain(gameStateAfterMove)
            currentBestKnownNetGain = player2Gain - player1Gain
            # Create a list of all the moves which produce the largest net gain assuming player 1 makes a move that captures the
            # most pieces possible during the next move.
            for move in strategicMoves:
                player2Gain, gameStateAfterMove = self.scoreMove(move, self.gameState, self.p2Code, self.p1Code)
                player1Gain = self.getMaxPlayer1Gain(gameStateAfterMove)
                if player2Gain - player1Gain > currentBestKnownNetGain:
                    profitableMoves = [ move ]
                elif player2Gain - player1Gain == currentBestKnownNetGain:
                    profitableMoves.append(move)

            # Find moves which limit or completely eliminate player 1 options on the next move. Add them to bestMoves and make any
            # move that completely eliminates player 1 options.
            movesLimitingP1Opt = []
            for move in profitableMoves:
                _, gameStateAfterMove = self.scoreMove(move, self.gameState, self.p2Code, self.p1Code)
                if self.countPlayer1Options(gameStateAfterMove) == 2:
                    movesLimitingP1Opt.append(move)
                elif self.countPlayer1Options(gameStateAfterMove) <= 1:
                    self.parent.gameboardButtonClicked(row = move[0], col = move[1])
                    return
                
            # Try to make a move that eliminates options available to player 1:
            if len(movesLimitingP1Opt) != 0:
                randomMove = movesLimitingP1Opt[random.randint(0, len(movesLimitingP1Opt) - 1)]
                self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])
                return


            # Create a list of all the moves which produce a larger strategic gain for player 2 than for player 1 from all the moves
            # in profitableMoves.
            bestMoves = []
            for move in profitableMoves:
                player2Gain = self.priorityMap[move[0]][move[1]]
                _, gameStateAfterMove = self.scoreMove(move, self.gameState, self.p2Code, self.p1Code)
                player1Gain = self.getMaxPlayer1StrategicGain(gameStateAfterMove)
                if player2Gain > player1Gain:
                    bestMoves.append(move)

            # Select randomly from the best moves available and make that move.
            if len(bestMoves) != 0:
                # In this case, at least one move that produces a strategic gain for the bot is available.
                randomMove = bestMoves[random.randint(0, len(bestMoves) - 1)]
                self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])
                # In this case, no move that produces a strategic gain for the bot is available.
            else:
                randomMove = strategicMoves[random.randint(0, len(strategicMoves) - 1)]
                self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])

        else:
            # Make a 'mistake'.
            randomMove = validMoves[random.randint(0, len(validMoves) - 1)]
            self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])

        self.updateGameState()

    def getMaxPlayer1StrategicGain (self, gameState):
        """
        PlayerBot.getMaxPlayer1StrategicGain(self, gameState)
        Given the state passed as gameState, returns the maximum strategic gain,
        accurding to PlayerBot.priorityMap available to player 1 if they should
        make a move.
        """
        maxStrategicGain = -300
        for row in range(8):
            for col in range(8):
                if self.isValidMove(row, col, self.p1Code, self.p2Code, gameState):
                    if self.priorityMap[row][col] > maxStrategicGain:
                        maxStrategicGain = self.priorityMap[row][col]
        return maxStrategicGain

    def countPlayer1Options (self, gameState):
        validMoveCount = 0
        for row in range(8):
            for col in range(8):
                if self.isValidMove(row, col, self.p1Code, self.p2Code, gameState): validMoveCount += 1

        return validMoveCount

    def isValidMove (self, row, col, me, opponent, gameState):
        """
        PlayerBot.isValidMove(self, row, col, me, opponent, gamestate)
        Returns True if the given move is valid for the given gamestate and
        False if the given move is invalid for the given gamestate.
        """
        if gameState[row][col] != self.bdCode:
            return False # If the space where we're trying to move isn't empty, we already know this move is invalid.

        scanningDirections = ((-1, 0), (0, 1), (1, 0), (0, -1), # A series of scanning vectors.
                              (-1, -1), (-1, 1), (1, 1), (1, -1))
        for SDRow, SDCol in scanningDirections: # Iterate over the scanning vectors.
            currentRow = row + SDRow # Start scanning at a position offset from the move by one
            currentCol = col + SDCol # along the current scanning vector.
            sawOpponent = False      # The opponents gamepieces haven't yet been seen on this vector.
            while currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == self.bdCode: break # If the gamespace is empty, we know there are no pieces to flip on this vector.
                if gameState[currentRow][currentCol] == opponent: sawOpponent = True # The opponents gamepieces have been seen.
                if gameState[currentRow][currentCol] == me and sawOpponent:
                    return True # There are at least pieceses on this vector that can be flipped. The move is valid.
                if gameState[currentRow][currentCol] == me and not sawOpponent: break # There are no pieces to flip along this vector. Proceed to the next.
                
                currentRow += SDRow # Proceed to the next gamespace in the current vector.
                currentCol += SDCol

        return False # If we've fallen out of the vector scanning loop, we know the move is invalid.

    def updateGameState (self):
        """
        PlayerBot.updateGameState(self)
        Synchronizes the objects gameState attribute with the current state of parent.gameboard.
        """
        for row in range(8):
            for col in range(8): # Iterate over the parents gameboard and insert integer values into self.gameState
                                 # corresponding to black pieces, white pieces and empty spaces.
                if self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.bgAndBoardColor:
                    self.gameState[row][col] = self.bdCode
                elif self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.player1Color:
                    self.gameState[row][col] = self.p1Code
                elif self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.player2Color:
                    self.gameState[row][col] = self.p2Code

    def scoreMove (self, possibleMove, gameState, me, opponent):
        """
        PlayerBot.scoreMove (self, possibleMove, gameState, me, opponent)
        Calculate the number of pieces captured by a given move in a given game state
        and return a tuple containing the number of captures at index 0 and the state
        of the game after the move at index 1
        """
        gameState = gameState.copy() # We wouldn't want to alter the value of self.gameState now, would we?
        row, col = possibleMove # Unpack the move parameter to make the code more readable.
        moveScore = 1 # We already know that we at least have the grid space where we placed our piece.
        scanningDirections = ((-1, 0), (0, 1), (1, 0), (0, -1), # A series of scanning vectors
                              (-1, -1), (-1, 1), (1, 1), (1, -1))
        for SDRow, SDCol in scanningDirections: # Scann along all 8 vectors.
            currentRow = row + SDRow # Start at a position offset from the position of the move along the current
            currentCol = col + SDCol # scanning vector.
            sawOpponent = False # None of the opponents gamepieces have been seen on the current scanning vector at this time.
            canCountPieces = False # No row of my opponents pieces with another of my pieces at the other end has been seen on
                                   # on this scanning vector at this time.
            while currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == self.bdCode: break # If we see an empty space, we know we can't flip pieces on this vector.
                if gameState[currentRow][currentCol] == self.p1Code: sawOpponent = True
                if gameState[currentRow][currentCol] == me and sawOpponent:
                    canCountPieces = True # We now know we can flip pieces on this vector.
                    break # There is no need to continue scanning this vector.
                if gameState[currentRow][currentCol] == me and not sawOpponent: break # If I see another of my pieces without seeing an opponents piece,
                                                                                      # there are no pieces to flip on this vector.
                currentRow += SDRow
                currentCol += SDCol

            currentRow = row + SDRow
            currentCol = col + SDCol
            while canCountPieces and currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == opponent:
                    gameState[currentRow][currentCol] = me # Flip the pieces on this vector and increment the move score.
                    moveScore += 1
                elif gameState[currentRow][currentCol] == me:
                    break
                
                currentRow += SDRow
                currentCol += SDCol

        return moveScore, gameState # Return the tuple

    def getMaxPlayer1Gain (self, gameState):
        """
        PlayerBot.getMaxPlayer1Gain(self, gameState)
        Returns an integer corresponding to the maximum number of captures player 1 can achieve if they make a move
        in the given gamestate.
        """
        validMoves = [] # Identify all the valid moves and store them in a list of tuples.
        for row in range(8):
            for col in range(8):
                if self.isValidMove(row, col, self.p1Code, self.p2Code, gameState):
                    validMoves.append( (row, col) )

        if validMoves == []: # If there are no moves available to player 1, player 1 cannot capture any pieces.
            return 0

        strategicMoves = [ validMoves[0] ] # Narrow the list of possible moves to those of strategic value. Player 1 isn't likely to
        for move in validMoves:            # deliberately throw the game and the bot does stupid things when it considers the possibility
                                           # that they would.
            if self.priorityMap[move[0]][move[1]] > self.priorityMap[strategicMoves[0][0]][strategicMoves[0][1]]:
                strategicMoves = [ move ]
            elif self.priorityMap[move[0]][move[1]] == self.priorityMap[strategicMoves[0][0]][strategicMoves[0][1]]:
                strategicMoves.append(move)

        maxGain = 0
        for move in strategicMoves: # Identify the move that captures the most pieces and return the number of pieces captured.
            if self.scoreMove(move, gameState, self.p1Code, self.p2Code)[0] > maxGain: maxGain = self.scoreMove(move, gameState, self.p1Code, self.p2Code)[0]

        return maxGain
If we can perfect the bots endgame, we may be able to create a bot that plays a perfect game at max difficulty; one guaranteed to end either in a draw or a win for the bot. As always, any help will be greatly appreciated.
Reply
#14
I don't know if you're still improving the game but import time and add a time.sleep after the play does a turn rather than - Platy does turn, computer does turn in less than a second. Add something like this -
import time
import random
time.sleep(random.randint(1, 3))
Reply
#15
Warning ! Python time.sleep is a show stopper. In all GUI tool kits.
This will effect mainloops. Can cause flicker. Can make program unresponsive.

WxPython has wx.Timer and wx.Callafter. wx.Timer can be use as a oneshot.
99 percent of computer problems exists between chair and keyboard.
Reply
#16
I guess you're right about that but you know what I mean.
Reply
#17
I've been doing research while I was away and have begun attempting to implement minMax. Right now, I'm a little stuck on my scoring heuristic. It lacks the necessary aversion to going into the buffers around the corners; even without minMax. I'll document my code and do my best to remove any methods that are no longer used, then I'll post it. It's a very different beast from what I've posted previously. The real challenge is getting all these numbers in the scoring heuristic balanced just right.

To those suggesting I add a time delay between the player making their move and the bot making its move, minMax is a recursive algorithm which will generate and analyze thousands of possible game states. With the recursion depth set just right, the bot will take time to make its move without introducing an artificial delay.
Reply
#18
This is my PlayerBot class as it is since I began trying to implement minMax. As it is, it's just picking the move that yields the highest board score when the board state is passed to scoreGameState, my scoring heuristic method. Even as it is, it should still be able to avoid simple mistakes like diving into the corner buffers at the earliest opportunity. You'll notice that the priorityMap class variable is gone. The algorithm that I'm trying to implement demands that the state of the entire board be evaluated, Assigning arbitrary values to each move based on its position just won't do.
class PlayerBot:

    """
    Instances of this class play as player 2 in single player mode.
    class PlayerBot:
        __init__(self, parent, difficulty)
        parent - an OthelloGameFrame instance
        difficulty - an integer between 0 and 100
    """
    bdCode = 0 # Constants for encoding empty board spaces, player 1 and player 2 in an internal
    p1Code = 1 # representation of the current game state.
    p2Code = 2

    def __init__ (self, parent, difficulty):
        self.parent = parent
        self.difficulty = difficulty
        self.gameState = self.getInitialGameState()
        # A list of moves potentially detrimental to the bots midgame.
        self.movesDetrimentalToMidgame = []

    def getInitialGameState (self):
        """
        PlayerBot.getInitialGameState (self):
        Returns 8 integer lists nested inside a list representing the initial state of a game
        of Othello, where 0 is an empty space, 1 is player 1, and 2 is player 2.
        """
        gameState = []
        for row in range(8):
            currentRow = []
            for col in range(8):
                currentRow.append(self.bdCode)
            gameState.append(currentRow)
            currentRow = []
        gameState[2][2] = self.p2Code
        gameState[2][3] = self.p1Code
        gameState[3][2] = self.p1Code
        gameState[3][3] = self.p2Code

        return gameState

    def makeMove(self):
        """
        PlayerBot.makeMove (self)
        This method makes a move in its parent as player2 and surrenders its move to player 1 if there are no
        moves available to it.
        """
        self.updateGameState() # Update the objects internal representation of the current state of the game.
        
        validMoves = self.getAllValidMoves(self.gameState, self.p2Code, self.p1Code)

        if validMoves == []: # If there are no moves available to the bot, give player 1 its move.
            self.parent.player1ButtonClicked()
            return

        if random.randint(0, 100) <= self.difficulty: # This outer if statement is used to make the occasional 'mistake'; a move
                                                      # selected randomly from the set of all valid moves. The frequency of these
                                                      # 'mistakes' is determined by the difficulty setting.
            bestMove = validMoves[0]
            bestScore = -inf
            for move in validMoves:
                _, gameStateAfterMove = self.scoreMove(move, self.gameState, self.p2Code, self.p1Code)
                moveScore = self.scoreGameState(gameStateAfterMove)
                if moveScore > bestScore:
                    bestMove = move
                    bestScore = moveScore

            self.parent.gameboardButtonClicked(row = bestMove[0], col = bestMove[1])
            return

        else:
            # Make a 'mistake'.
            randomMove = validMoves[random.randint(0, len(validMoves) - 1)]
            self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])

        self.updateGameState()

    def getAllValidMoves (self, gameState, me, opponent):
        """
        PlayerBot.getAllValidMoves(self, gameState, me, opponent)
        Return a list of all moves which are valid for the player 'me'
        in the given game state.
        """
        validMoves = []
        for row in range(8):
            for col in range(8):
                if self.isValidMove(row, col, me, opponent, gameState): validMoves.append( (row, col) )

        return validMoves

    def scoreGameState (self, gameState):
        """
        PlayerBot.scoreGameState(self, gameState)
        Used by the minMax algorithm to score a game state using a series of heuristics,
        returning positive score values to game states which are favorable to the bot and
        negative score values to game states which are unfavorable.
        """
        score = 0 # Initialize score to zero.
        me = self.p2Code        # I use the terms 'me' and 'opponent' in this function
        opponent = self.p1Code  # for code clarity. These variables serve no other purpose.
        validMoves = self.getAllValidMoves(gameState, me, opponent)
        # Penalize actions by the bot which result in its options being limited.
        if len(validMoves) <= 3: score -= 300_000
        # If I don't have any valid moves:
        if validMoves == []:
            # Check whether the game has reached a terminal state:
            if self.getAllValidMoves(gameState, opponent, me) == []:
                myScore = 0     # Find out who won.
                oppScore = 0
                for row in range(8):
                    for col in range(8):
                        if gameState[row][col] == me: myScore += 1
                        if gameState[row][col] == opponent: oppScore += 1
                if myScore > oppScore: return 1_000_000_000_000     # If the bot won in this scenario:
                if myScore == oppScore: return 0                    # If the game was a draw:
                if myScore < oppScore: return -1_000_000_000_000    # If the bot lost in this scenario:

            # If the player has options and the bot has none:
            else:
                # Give a score penalty for losing a turn to the player. The score value of other strategic gains is also cut in half.
                score -= 999_999_999
                # Give a score penalty for each move available to the player.
                score -= len(self.getAllValidMoves(gameState, opponent, me)) * 10
                # Check for traps set by the bot and add 1500 to score for each one.
                if gameState[2][0] == me and gameState[5][0] == me and (gameState[3][0] == opponent or gameState[4][0] == opponent):
                    score += 1500
                if gameState[2][7] == me and gameState[5][7] == me and (gameState[3][7] == opponent or gameState[4][7] == opponent):
                    score += 1500
                if gameState[0][2] == me and gameState[0][5] == me and (gameState[0][3] == opponent or gameState[0][4] == opponent):
                    score += 1500
                if gameState[7][2] == me and gameState[7][5] == me and (gameState[7][3] == opponent or gameState[7][4] == opponent):
                    score += 1500

                # Check for traps set by the player and subtract 3000 from the score for each one.
                if gameState[2][0] == opponent and gameState[5][0] == opponent and (gameState[3][0] == me or gameState[4][0] == me):
                    score -= 3000
                if gameState[2][7] == opponent and gameState[5][7] == opponent and (gameState[3][7] == me or gameState[4][7] == me):
                    score -= 3000
                if gameState[0][2] == opponent and gameState[0][5] == opponent and (gameState[0][3] == me or gameState[0][4] == me):
                    score -= 3000
                if gameState[7][2] == opponent and gameState[7][5] == opponent and (gameState[7][3] == me or gameState[7][4] == me):
                    score -= 3000

                # Check for corners filled by the bot and add 70000 to score for each one.
                if gameState[0][0] == me: score += 70000
                if gameState[7][0] == me: score += 70000
                if gameState[0][7] == me: score += 70000
                if gameState[7][7] == me: score += 70000

                # Check for corners filled by the player and subtract 140000 from the score for each one.
                if gameState[0][0] == opponent: score -= 140000
                if gameState[7][0] == opponent: score -= 140000
                if gameState[0][7] == opponent: score -= 140000
                if gameState[7][7] == opponent: score -= 140000

                # Check for edges filled by the bot and add 2500 to the score for each edge filled.
                if gameState[0][2] == me and gameState[0][3] == me and gameState[0][4] == me and gameState[0][5] == me:
                    score += 2500
                if gameState[7][2] == me and gameState[7][3] == me and gameState[7][4] == me and gameState[7][5] == me:
                    score += 2500
                if gameState[2][0] == me and gameState[3][0] == me and gameState[4][0] == me and gameState[5][0] == me:
                    score += 2500
                if gameState[2][7] == me and gameState[3][7] == me and gameState[4][7] == me and gameState[5][7] == me:
                    score += 2500

                # Check whether the player has filled any edges and subtract 5000 from score for each edge filled.
                if gameState[0][2] == opponent and gameState[0][3] == opponent and gameState[0][4] == opponent and gameState[0][5] == opponent:
                    score -= 5000
                    if self.isValidMove(0, 1, opponent, me, gameState): score -= 2000 # Check whether the filled edge is an entrance.
                    if self.isValidMove(0, 6, opponent, me, gameState): score -= 2000
                if gameState[7][2] == opponent and gameState[7][3] == opponent and gameState[7][4] == opponent and gameState[7][5] == opponent:
                    score -= 5000
                    if self.isValidMove(7, 1, opponent, me, gameState): score -= 2000
                    if self.isValidMove(7, 6, opponent, me, gameState): score -= 2000
                if gameState[2][0] == opponent and gameState[3][0] == opponent and gameState[4][0] == opponent and gameState[5][0] == opponent:
                    score -= 5000
                    if self.isValidMove(1, 0, opponent, me, gameState): score -= 2000
                    if self.isValidMove(6, 0, opponent, me, gameState): score -= 2000
                if gameState[2][7] == opponent and gameState[3][7] == opponent and gameState[4][7] == opponent and gameState[5][7] == opponent:
                    score -= 5000
                    if self.isValidMove(1, 7, opponent, me, gameState): score -= 2000
                    if self.isValidMove(6, 7, opponent, me, gameState): score -= 2000

                # Check whether any of the corners have been captured and add 140000 to the score for each corner captured.
                if gameState[0][0] == me: score += 140000
                if gameState[0][7] == me: score += 140000
                if gameState[7][0] == me: score += 140000
                if gameState[7][7] == me: score += 140000

                # Check whether the player can capture any of the corners and subtract 1000000 from the score for each potential corner.
                if self.isValidMove(0, 0, opponent, me, gameState): score -= 1400000
                if self.isValidMove(0, 7, opponent, me, gameState): score -= 1400000
                if self.isValidMove(7, 0, opponent, me, gameState): score -= 1400000
                if self.isValidMove(7, 7, opponent, me, gameState): score -= 1400000
                # Check whether any of the corners are captured and subtract 2700000 from the score for each one.
                if gameState[0][0] == opponent: score -= 2700000
                if gameState[0][7] == opponent: score -= 2700000
                if gameState[7][0] == opponent: score -= 2700000
                if gameState[7][7] == opponent: score -= 2700000

                # Check the inside corners and add 3500 to the score for each one that is filled by the bot.
                if gameState[2][2] == me: score += 3500
                if gameState[2][5] == me: score += 3500
                if gameState[5][2] == me: score += 3500
                if gameState[5][5] == me: score += 3500

                # Subtract 7000 for each inside corner filled by the player.
                if gameState[2][2] == opponent: score -= 7000
                if gameState[2][5] == opponent: score -= 7000
                if gameState[5][2] == opponent: score -= 7000
                if gameState[5][5] == opponent: score -= 7000

                return score

        # If the bot has moves available:
        # Add to or subtract from the bots score based on the number of moves available to the bot and the player.
        score += (len(self.getAllValidMoves(gameState, me, opponent)) - len(self.getAllValidMoves(gameState, me, opponent))) * 10
        # Check for traps set by the bot and add 3000 to score for each one.
        if gameState[2][0] == me and gameState[5][0] == me and (gameState[3][0] == opponent or gameState[4][0] == opponent):
            score += 3000
        if gameState[2][7] == me and gameState[5][7] == me and (gameState[3][7] == opponent or gameState[4][7] == opponent):
            score += 3000
        if gameState[0][2] == me and gameState[0][5] == me and (gameState[0][3] == opponent or gameState[0][4] == opponent):
            score += 3000
        if gameState[7][2] == me and gameState[7][5] == me and (gameState[7][3] == opponent or gameState[7][4] == opponent):
            score += 3000

        # Check for traps set by the player and subtract 3000 from the score for each one.
        if gameState[2][0] == opponent and gameState[5][0] == opponent and (gameState[3][0] == me or gameState[4][0] == me):
            score -= 3000
        if gameState[2][7] == opponent and gameState[5][7] == opponent and (gameState[3][7] == me or gameState[4][7] == me):
            score -= 3000
        if gameState[0][2] == opponent and gameState[0][5] == opponent and (gameState[0][3] == me or gameState[0][4] == me):
            score -= 3000
        if gameState[7][2] == opponent and gameState[7][5] == opponent and (gameState[7][3] == me or gameState[7][4] == me):
            score -= 3000

        # Check for corners filled by the bot and add 140000 to score for each one.
        if gameState[0][0] == me: score += 140000
        if gameState[7][0] == me: score += 140000
        if gameState[0][7] == me: score += 140000
        if gameState[7][7] == me: score += 140000

        # Check for corners filled by the player and subtract 140000 from the score for each one.
        if gameState[0][0] == opponent: score -= 140000
        if gameState[7][0] == opponent: score -= 140000
        if gameState[0][7] == opponent: score -= 140000
        if gameState[7][7] == opponent: score -= 140000

        # Check for edges filled by the bot and add 5000 to the score for each edge filled.
        if gameState[0][2] == me and gameState[0][3] == me and gameState[0][4] == me and gameState[0][5] == me:
            score += 5000
            if self.isValidMove(0, 1, me, opponent, gameState): score += 2000 # Check whether the filled edge is an entrance.
            if self.isValidMove(0, 6, me, opponent, gameState): score += 2000
        if gameState[7][2] == me and gameState[7][3] == me and gameState[7][4] == me and gameState[7][5] == me:
            score += 5000
            if self.isValidMove(7, 1, me, opponent, gameState): score += 2000
            if self.isValidMove(7, 6, me, opponent, gameState): score += 2000
        if gameState[2][0] == me and gameState[3][0] == me and gameState[4][0] == me and gameState[5][0] == me:
            score += 5000
            if self.isValidMove(1, 0, me, opponent, gameState): score += 2000
            if self.isValidMove(6, 0, me, opponent, gameState): score += 2000
        if gameState[2][7] == me and gameState[3][7] == me and gameState[4][7] == me and gameState[5][7] == me:
            score += 5000
            if self.isValidMove(1, 7, me, opponent, gameState): score += 2000
            if self.isValidMove(6, 7, me, opponent, gameState): score += 2000

        # Check whether the player has filled any edges and subtract 5000 from score for each edge filled.
        if gameState[0][2] == opponent and gameState[0][3] == opponent and gameState[0][4] == opponent and gameState[0][5] == opponent:
            score -= 5000
            if self.isValidMove(0, 1, opponent, me, gameState): score -= 2000 # Check whether the filled edge is an entrance.
            if self.isValidMove(0, 6, opponent, me, gameState): score -= 2000
        if gameState[7][2] == opponent and gameState[7][3] == opponent and gameState[7][4] == opponent and gameState[7][5] == opponent:
            score -= 5000
            if self.isValidMove(7, 1, opponent, me, gameState): score -= 2000
            if self.isValidMove(7, 6, opponent, me, gameState): score -= 2000
        if gameState[2][0] == opponent and gameState[3][0] == opponent and gameState[4][0] == opponent and gameState[5][0] == opponent:
            score -= 5000
            if self.isValidMove(1, 0, opponent, me, gameState): score -= 2000
            if self.isValidMove(6, 0, opponent, me, gameState): score -= 2000
        if gameState[2][7] == opponent and gameState[3][7] == opponent and gameState[4][7] == opponent and gameState[5][7] == opponent:
            score -= 5000
            if self.isValidMove(1, 7, opponent, me, gameState): score -= 2000
            if self.isValidMove(6, 7, opponent, me, gameState): score -= 2000

        # Check whether the bot can capture any of the corners and add 140000 to score for each corner that can be captured.
        if self.isValidMove(0, 0, me, opponent, gameState): score += 140000
        if self.isValidMove(0, 7, me, opponent, gameState): score += 140000
        if self.isValidMove(7, 0, me, opponent, gameState): score += 140000
        if self.isValidMove(7, 7, me, opponent, gameState): score += 140000
        # Check whether any of the corners have been captured and add 2700000 to the score for each corner captured.
        if gameState[0][0] == me: score += 2700000
        if gameState[0][7] == me: score += 2700000
        if gameState[7][0] == me: score += 2700000
        if gameState[7][7] == me: score += 2700000

        # Check whether the player can capture any of the corners and subtract 140000 from the score for each potential corner.
        if self.isValidMove(0, 0, opponent, me, gameState): score -= 140000
        if self.isValidMove(0, 7, opponent, me, gameState): score -= 140000
        if self.isValidMove(7, 0, opponent, me, gameState): score -= 140000
        if self.isValidMove(7, 7, opponent, me, gameState): score -= 140000
        # Check whether any of the corners are captured and subtract 2700000 from the score for each one.
        if gameState[0][0] == opponent: score -= 2700000
        if gameState[0][7] == opponent: score -= 2700000
        if gameState[7][0] == opponent: score -= 2700000
        if gameState[7][7] == opponent: score -= 2700000

        # Check the inside corners to and add 7000 to the score for each one that is filled by the bot.
        if gameState[2][2] == me: score += 7000
        if gameState[2][5] == me: score += 7000
        if gameState[5][2] == me: score += 7000
        if gameState[5][5] == me: score += 7000

        # Subtract 7000 for each inside corner filled by the player.
        if gameState[2][2] == opponent: score -= 7000
        if gameState[2][5] == opponent: score -= 7000
        if gameState[5][2] == opponent: score -= 7000
        if gameState[5][5] == opponent: score -= 7000

        # Check whether pieces have been placed inside the buffers unnecessarily.
        # This algorithm isn't very airtight, but it's better than nothing.
        canPlaceOutsideBuffers = False
        for move in validMoves:
            if move not in ((0, 1), (1, 0), (1, 1), (0, 6), (1, 6), (1, 7),
                            (6, 0), (6, 1), (7, 1), (6, 6), (6, 7), (7, 6)):
                canPlaceOutsideBuffers = True
                break

        buffersOccupiedUnnecessarily = False
        if canPlaceOutsideBuffers:
            if gameState[0][1] == me or gameState[1][0] == me or gameState[1][1] == me: buffersOccupiedUnnecessarily = True
            if gameState[0][6] == me or gameState[1][6] == me or gameState[1][7] == me: buffersOccupiedUnnecessarily = True
            if gameState[6][0] == me or gameState[6][1] == me or gameState[7][1] == me: buffersOccupiedUnnecessarily = True
            if gameState[6][6] == me or gameState[6][7] == me or gameState[7][6] == me: buffersOccupiedUnnecessarily = True

        if buffersOccupiedUnnecessarily: score -= 275_000_000_000_000_000_000_000_275_000_000_000_000_000_000_000
        
        return score

    def isValidMove (self, row, col, me, opponent, gameState):
        """
        PlayerBot.isValidMove(self, row, col, me, opponent, gamestate)
        Returns True if the given move is valid for the given gamestate and
        False if the given move is invalid for the given gamestate.
        """
        if gameState[row][col] != self.bdCode:
            return False # If the space where we're trying to move isn't empty, we already know this move is invalid.

        scanningDirections = ((-1, 0), (0, 1), (1, 0), (0, -1), # A series of scanning vectors.
                              (-1, -1), (-1, 1), (1, 1), (1, -1))
        for SDRow, SDCol in scanningDirections: # Iterate over the scanning vectors.
            currentRow = row + SDRow # Start scanning at a position offset from the move by one
            currentCol = col + SDCol # along the current scanning vector.
            sawOpponent = False      # The opponents gamepieces haven't yet been seen on this vector.
            while currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == self.bdCode: break # If the gamespace is empty, we know there are no pieces to flip on this vector.
                if gameState[currentRow][currentCol] == opponent: sawOpponent = True # The opponents gamepieces have been seen.
                if gameState[currentRow][currentCol] == me and sawOpponent:
                    return True # There are at least pieceses on this vector that can be flipped. The move is valid.
                if gameState[currentRow][currentCol] == me and not sawOpponent: break # There are no pieces to flip along this vector. Proceed to the next.
                
                currentRow += SDRow # Proceed to the next gamespace in the current vector.
                currentCol += SDCol

        return False # If we've fallen out of the vector scanning loop, we know the move is invalid.

    def updateGameState (self):
        """
        PlayerBot.updateGameState(self)
        Synchronizes the objects gameState attribute with the current state of parent.gameboard.
        """
        for row in range(8):
            for col in range(8): # Iterate over the parents gameboard and insert integer values into self.gameState
                                 # corresponding to black pieces, white pieces and empty spaces.
                if self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.bgAndBoardColor:
                    self.gameState[row][col] = self.bdCode
                elif self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.player1Color:
                    self.gameState[row][col] = self.p1Code
                elif self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.player2Color:
                    self.gameState[row][col] = self.p2Code

    def scoreMove (self, possibleMove, gameState, me, opponent):
        """
        PlayerBot.scoreMove (self, possibleMove, gameState, me, opponent)
        Calculate the number of pieces captured by a given move in a given game state
        and return a tuple containing the number of captures at index 0 and the state
        of the game after the move at index 1
        """
        gameState = gameState.copy() # We wouldn't want to alter the value of self.gameState now, would we?
        row, col = possibleMove # Unpack the move parameter to make the code more readable.
        moveScore = 1 # We already know that we at least have the grid space where we placed our piece.
        scanningDirections = ((-1, 0), (0, 1), (1, 0), (0, -1), # A series of scanning vectors
                              (-1, -1), (-1, 1), (1, 1), (1, -1))
        for SDRow, SDCol in scanningDirections: # Scann along all 8 vectors.
            currentRow = row + SDRow # Start at a position offset from the position of the move along the current
            currentCol = col + SDCol # scanning vector.
            sawOpponent = False # None of the opponents gamepieces have been seen on the current scanning vector at this time.
            canCountPieces = False # No row of my opponents pieces with another of my pieces at the other end has been seen on
                                   # on this scanning vector at this time.
            while currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == self.bdCode: break # If we see an empty space, we know we can't flip pieces on this vector.
                if gameState[currentRow][currentCol] == self.p1Code: sawOpponent = True
                if gameState[currentRow][currentCol] == me and sawOpponent:
                    canCountPieces = True # We now know we can flip pieces on this vector.
                    break # There is no need to continue scanning this vector.
                if gameState[currentRow][currentCol] == me and not sawOpponent: break # If I see another of my pieces without seeing an opponents piece,
                                                                                      # there are no pieces to flip on this vector.
                currentRow += SDRow
                currentCol += SDCol

            currentRow = row + SDRow
            currentCol = col + SDCol
            while canCountPieces and currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == opponent:
                    gameState[currentRow][currentCol] = me # Flip the pieces on this vector and increment the move score.
                    moveScore += 1
                elif gameState[currentRow][currentCol] == me:
                    break
                
                currentRow += SDRow
                currentCol += SDCol

        return moveScore, gameState # Return the tuple
Maybe it dives into corners at the first opportunity, but it does have a talent for stealing options away from you such that even while it does something blatantly stupid, you can't do anything to stop it; at least for a couple moves. Once I get a good scoring heuristic in here, it should begin doing something at least a little bit sensible, even without minMax.
Reply
#19
It seems all I've managed to do was make the code look nicer and get rid of some repetition. Many of the same problems still exist. I suspected that checks and score changes designed to help the bot at earlier stages of the game were interfering with later gameplay and vice versa. Basically, the score value that I was getting from the previous version of the scoreGameState method was garbage but this time, it seems like the bot just calls assessMidGame and assessLateGame methods when it's still early game. I have absolutely no idea how getStage could be screwing this up. It's simple. If there are no valid moves in zone 1, tell the caller it's time to move on the zone 2, and if there are no valid moves in zone 2, tell the caller it's time to move on to zone 3. I know I still haven't implemented minMax yet, but I should at least start to see the bot making somewhat sensible moves. It still runs out of the center when it's still early game and dives into the corners at the first opportunity.

here's my code:
class PlayerBot:

    """
    Instances of this class play as player 2 in single player mode.
    class PlayerBot:
        __init__(self, parent, difficulty)
        parent - an OthelloGameFrame instance
        difficulty - an integer between 0 and 100
    """
    bdCode = 0 # Constants for encoding empty board spaces, player 1 and player 2 in an internal
    p1Code = 1 # representation of the current game state.
    p2Code = 2
    # Constants for early game, mid game and late game used by the scoring heuristic.
    ST_EARLY = 3
    ST_MID = 4
    ST_LATE = 5
    # Constants for top, left, right, and bottom used by the trap and entrance detection and assessment
    # methods.
    TOP = (0, 2)
    LEFT = (2, 0)
    RIGHT = (2, 6)
    BOTTOM = (6, 2)

    def __init__ (self, parent, difficulty):
        self.parent = parent
        self.difficulty = difficulty
        self.gameState = self.getInitialGameState()
        # A list of moves potentially detrimental to the bots midgame.
        self.movesDetrimentalToMidgame = []

    def getInitialGameState (self):
        """
        PlayerBot.getInitialGameState (self):
        Returns 8 integer lists nested inside a list representing the initial state of a game
        of Othello, where 0 is an empty space, 1 is player 1, and 2 is player 2.
        """
        gameState = []
        for row in range(8):
            currentRow = []
            for col in range(8):
                currentRow.append(self.bdCode)
            gameState.append(currentRow)
            currentRow = []
        gameState[2][2] = self.p2Code
        gameState[2][3] = self.p1Code
        gameState[3][2] = self.p1Code
        gameState[3][3] = self.p2Code

        return gameState

    def makeMove(self):
        """
        PlayerBot.makeMove (self)
        This method makes a move in its parent as player2 and surrenders its move to player 1 if there are no
        moves available to it.
        """
        self.updateGameState() # Update the objects internal representation of the current state of the game.
        
        validMoves = self.getAllValidMoves(self.gameState, self.p2Code, self.p1Code)

        if validMoves == []: # If there are no moves available to the bot, give player 1 its move.
            self.parent.player1ButtonClicked()
            return

        if random.randint(0, 100) <= self.difficulty: # This outer if statement is used to make the occasional 'mistake'; a move
                                                      # selected randomly from the set of all valid moves. The frequency of these
                                                      # 'mistakes' is determined by the difficulty setting.
            bestMove = validMoves[0]
            bestScore = -inf
            for move in validMoves:
                _, gameStateAfterMove = self.scoreMove(move, self.gameState, self.p2Code, self.p1Code)
                moveScore = self.scoreGameState(gameStateAfterMove)
                if moveScore > bestScore:
                    bestMove = move
                    bestScore = moveScore
                    print(bestScore)
            print("")

            self.parent.gameboardButtonClicked(row = bestMove[0], col = bestMove[1])
            return

        else:
            # Make a 'mistake'.
            randomMove = validMoves[random.randint(0, len(validMoves) - 1)]
            self.parent.gameboardButtonClicked(row = randomMove[0], col = randomMove[1])

        self.updateGameState()

    def getAllValidMoves (self, gameState, me, opponent):
        """
        PlayerBot.getAllValidMoves(self, gameState, me, opponent)
        Return a list of all moves which are valid for the player 'me'
        in the given game state.
        """
        validMoves = []
        for row in range(8):
            for col in range(8):
                if self.isValidMove(row, col, me, opponent, gameState): validMoves.append( (row, col) )

        return validMoves

    def scoreGameState (self, gameState):
        """
        PlayerBot.scoreGameState(self, gameState)
        Used by the minMax algorithm to score a game state using a series of heuristics,
        returning positive score values to game states which are favorable to the bot and
        negative score values for game states which are unfavorable.
        """
        score = 0 # Initialize score to zero.
        me = self.p2Code        # I use the terms 'me' and 'opponent' in this function
        opponent = self.p1Code  # for code clarity. These variables serve no other purpose.
        validMoves = self.getAllValidMoves(gameState, me, opponent)
        gameStage = self.getStage(gameState)
        # Penalize actions by the bot which result in its options being limited.
        if len(validMoves) <= 2: score -= 300_000
        # If I don't have any valid moves:
        if len(validMoves) == 0:
            # Check whether the game has reached a terminal state:
            if len(self.getAllValidMoves(gameState, opponent, me)) == 0:
                myScore = 0     # Find out who won.
                oppScore = 0
                for row in range(8):
                    for col in range(8):
                        if gameState[row][col] == me: myScore += 1
                        if gameState[row][col] == opponent: oppScore += 1
                if myScore > oppScore: return 1_000_000_000_000     # If the bot won in this scenario:
                if myScore == oppScore: return 0                    # If the game was a draw:
                if myScore < oppScore: return -1_000_000_000_000    # If the bot lost in this scenario:

            # If the player has options and the bot has none:
            else:
                score -= 999_999
                
                if gameStage == self.ST_EARLY:
                    score += self.assessEarlyGame(gameState)

                elif gameStage == self.ST_MID:
                    score += self.assessMidGame(gameState)

                elif gameStage == self.ST_LATE:
                    score += self.assessLateGame(gameState)

                return score

        # Determine whether the game state is early game, midgame, or late game.
        
        if gameStage == self.ST_EARLY:
            score += self.assessEarlyGame(gameState)

        elif gameStage == self.ST_MID:
            score += self.assessMidGame(gameState)

        elif gameStage == self.ST_LATE:
            score += self.assessLateGame(gameState)

        return score

    def assessEarlyGame (self, gameState):
        """
        PlayerBot.assessEarlyGame(self, gameState)
        This method is used by scoreGameState to compute a score for early game.
        """
        # Scores range from 100_000 to 150_000
        score = 0
        me = self.p2Code
        opponent = self.p1Code
        # Check whether the bot placed/was forced to place any pieces outside the center of the board.
        for row in range(8):
            for col in range(8):
                if row not in range(2, 6) and col not in range(2, 6):
                    if gameState[row][col] == me: score -= 150_000
                    # If the bot forced the player to place outside the center of the board, that's a good
                    # thing.
                    if gameState[row][col] == opponent: score += 150_000

        # Check whether the bot placed any pieces in the inside corners.
        if gameState[2][2] == me: score += 150_000
        if gameState[2][5] == me: score += 150_000
        if gameState[5][2] == me: score += 150_000
        if gameState[5][5] == me: score += 150_000

        # Check whether the player placed any pieces in the inside corners.
        if gameState[2][2] == opponent: score -= 150_000
        if gameState[2][5] == opponent: score -= 150_000
        if gameState[5][2] == opponent: score -= 150_000
        if gameState[5][5] == opponent: score -= 150_000

        # Check whether the bot can place any pieces in the inside corners.
        if self.isValidMove(2, 2, me, opponent, gameState): score += 110_000
        if self.isValidMove(2, 5, me, opponent, gameState): score += 110_000
        if self.isValidMove(5, 2, me, opponent, gameState): score += 110_000
        if self.isValidMove(5, 5, me, opponent, gameState): score += 110_000

        # Check whether the player can place any pieces in the inside corners.
        if self.isValidMove(2, 2, opponent, me, gameState): score -= 110_000
        if self.isValidMove(2, 5, opponent, me, gameState): score -= 110_000
        if self.isValidMove(5, 2, opponent, me, gameState): score -= 110_000
        if self.isValidMove(5, 5, opponent, me, gameState): score -= 110_000

        score += self.evaluateCornerAreas(gameState)

        return score

    def assessMidGame (self, gameState):
        """
        PlayerBot.assessMidGame(self, gameState)
        This method is used by scoreGameState to evaluate games which have progressed to the middle stage.
        """
        # Scores range from 50_000 to 100_000.
        score = 0
        me = self.p2Code
        opponent = self.p1Code
        # Check whether the bot set any traps and add a score appropriate to the quality of the trap.
        if self.isTrap(self.TOP, me, opponent, gameState):
            score += self.evaluateTrap(self.TOP, me, opponent, gameState)
            
        if self.isTrap(self.LEFT, me, opponent, gameState):
            score += self.evaluateTrap(self.LEFT, me, opponent, gameState)
            
        if self.isTrap(self.RIGHT, me, opponent, gameState):
            score += self.evaluateTrap(self.RIGHT, me, opponent, gameState)
            
        if self.isTrap(self.BOTTOM, me, opponent, gameState):
            score += self.evaluateTrap(self.BOTTOM, me, opponent, gameState)

        # Check whether the player set any traps and subtract a score appropriate to the quality of the trap.
        if self.isTrap(self.TOP, opponent, me, gameState):
            score -= self.evaluateTrap(self.TOP, opponent, me, gameState)
            
        if self.isTrap(self.LEFT, opponent, me, gameState):
            score -= self.evaluateTrap(self.LEFT, opponent, me, gameState)
            
        if self.isTrap(self.RIGHT, opponent, me, gameState):
            score -= self.evaluateTrap(self.RIGHT, opponent, me, gameState)
            
        if self.isTrap(self.BOTTOM, opponent, me, gameState):
            score -= self.evaluateTrap(self.BOTTOM, opponent, me, gameState)

        # Check whether the bot created any entrances and add a value appropriate to the quality of the entrance.
        # An entrance is a way to enter the buffer around the corner without fear of the player taking the corner.
        # Filling an edge is always beneficial whether it's a proper entrance or not, so a lesser value is added
        # to score if the edge is filled.
        if self.isEntrance(self.TOP, me, opponent, gameState):
            score += self.evaluateEntrance(self.TOP, me, opponent, gameState)
        elif gameState[0][2] == me and gameState[0][3] == me and gameState[0][4] == me and gameState[0][5] == me:
            score += 50_000

        if self.isEntrance(self.RIGHT, me, opponent, gameState):
            score += self.evaluateEntrance(self.LEFT, me, opponent, gameState)
        elif gameState[0][2] == me and gameState[0][3] == me and gameState[0][4] == me and gameState[0][5] == me:
            score += 50_000

        if self.isEntrance(self.LEFT, me, opponent, gameState):
            score += self.evaluateEntrance(self.RIGHT, me, opponent, gameState)
        elif gameState[0][2] == me and gameState[0][3] == me and gameState[0][4] == me and gameState[0][5] == me:
            score += 50_000

        if self.isEntrance(self.BOTTOM, me, opponent, gameState):
            score += self.evaluateEntrance(self.BOTTOM, me, opponent, gameState)
        elif gameState[0][2] == me and gameState[0][3] == me and gameState[0][4] == me and gameState[0][5] == me:
            score += 50_000

        # Check whether the player created any entrances or filled any edges and subtract value from score accordingly.
        if self.isEntrance(self.TOP, opponent, me, gameState):
            score -= self.evaluateEntrance(self.TOP, opponent, me, gameState)
        elif gameState[0][2] == opponent and gameState[0][3] == opponent and gameState[0][4] == opponent and gameState[0][5] == opponent:
            score -= 50_000

        if self.isEntrance(self.LEFT, opponent, me, gameState):
            score -= self.evaluateEntrance(self.LEFT, opponent, me, gameState)
        elif gameState[2][0] == opponent and gameState[3][0] == opponent and gameState[4][0] == opponent and gameState[5][0] == opponent:
            score -= 50_000

        if self.isEntrance(self.RIGHT, opponent, me, gameState):
            score -= self.evaluateEntrance(self.RIGHT, opponent, me, gameState)
        elif gameState[2][7] == opponent and gameState[3][7] == opponent and gameState[4][7] == opponent and gameState[5][7] == opponent:
            score -= 50_000

        if self.isEntrance(self.BOTTOM, opponent, me, gameState):
            score -= self.evaluateEntrance(self.BOTTOM, opponent, me, gameState)
        elif gameState[7][2] == opponent and gameState[7][3] == opponent and gameState[7][4] == opponent and gameState[7][5] == opponent:
            score -= 50_000

        score += self.evaluateCornerAreas(gameState)

        return score
    def evaluateCornerAreas (self, gameState):
        """
        PlayerBot.evaluateCornerAreas(self, gameState)
        Used by game state scoring methods to assess the state of the corners and the buffers around them.
        """
        score = 0
        me = self.p2Code
        opponent = self.p1Code
        # Dole out massive rewards for capturing the corners and massive penalties for giving the corners to the opponent.

        # Check whether the bot filled any of the corners and add 999_999 to score for each corner it's taken.
        if gameState[0][0] == me: score += 999_999
        if gameState[0][7] == me: score += 999_999
        if gameState[7][0] == me: score += 999_999
        if gameState[7][7] == me: score += 999_999

        # Check whether the player filled any of the corners and subtract 999_999 from score for each corner they've taken.
        if gameState[0][0] == opponent: score -= 999_999
        if gameState[0][7] == opponent: score -= 999_999
        if gameState[7][0] == opponent: score -= 999_999
        if gameState[7][7] == opponent: score -= 999_999

        # Check whether the bot put/was forced to put pieces in any of the corner buffers and subtract 999_999 for each
        # buffer position filled starting with the top left.
        if gameState[0][1] == me: score -= 999_999
        if gameState[1][0] == me: score -= 999_999
        if gameState[1][1] == me: score -= 999_999
        # Top Right:
        if gameState[0][6] == me: score -= 999_999
        if gameState[1][6] == me: score -= 999_999
        if gameState[6][1] == me: score -= 999_999
        # Bottom Left:
        if gameState[6][0] == me: score -= 999_999
        if gameState[6][1] == me: score -= 999_999
        if gameState[7][1] == me: score -= 999_999
        # Bottom Right:
        if gameState[6][6] == me: score -= 999_999
        if gameState[6][7] == me: score -= 999_999
        if gameState[7][6] == me: score -= 999_999

        # Check whether the player put/was forced to put pieces in any of the corner buffers and add 40000 for each buffer
        # position filled starting with the top left.
        if gameState[0][1] == opponent: score += 999_999
        if gameState[1][0] == opponent: score += 999_999
        if gameState[1][1] == opponent: score += 999_999
        # Top Right:
        if gameState[0][6] == opponent: score += 999_999
        if gameState[1][6] == opponent: score += 999_999
        if gameState[6][1] == opponent: score += 999_999
        # Bottom Left:
        if gameState[6][0] == opponent: score += 999_999
        if gameState[6][1] == opponent: score += 999_999
        if gameState[7][1] == opponent: score += 999_999
        # Bottom Right:
        if gameState[6][6] == opponent: score += 999_999
        if gameState[6][7] == opponent: score += 999_999
        if gameState[7][6] == opponent: score += 999_999

        return score

    def isEntrance (self, position, me, opponent, gameState):
        """
        PlayerBot.isEntrance(self, position, me, opponent, gameState)
        This is a boolean method used to determine whether an entrance was created
        at the specified position.
        """
        # How to assess whether a pattern is an entrance is dependent on the position and orientation of the supposed entrance.
        if position == self.TOP:
            if gameState[0][2] == me and gameState[0][3] == me and gameState[0][4] == me and gameState[0][5] == me:
                if self.isValidMove(0, 1, me, opponent, gameState) or self.isValidMove(0, 6, me, opponent, gameState):
                    return True

        elif position == self.LEFT:
            if gameState[2][0] == me and gameState[3][0] == me and gameState[4][0] == me and gameState[5][0] == me:
                if self.isValidMove(1, 0, me, opponent, gameState) or self.isValidMove(6, 0, me, opponent, gameState):
                    return True

        elif position == self.RIGHT:
            if gameState[2][7] == me and gameState[3][7] == me and gameState[4][7] == me and gameState[5][7] == me:
                if self.isValidMove(1, 7, me, opponent, gameState) or self.isValidMove(6, 7, me, opponent, gameState):
                    return True

        elif position == self.BOTTOM:
            if gameState[7][2] == me and gameState[7][3] == me and gameState[7][4] == me and gameState[7][5] == me:
                if self.isValidMove(7, 1, me, opponent, gameState) or self.isValidMove(7, 6, me, opponent, gameState):
                    return True

        else:
            raise ValueError("The position parameter of isEntrance must be a tuple with the values: (0, 2), (2, 0), (2, 6) or (6, 2) or the class constants TOP, LEFT, RIGHT, or BOTTOM!")

        return False

    def evaluateEntrance (self, position, me, opponent, gameState):
        """
        PlayerBot.evaluateEntrance(self, position, me, opponent, gameState)
        This method is used by assessMidGame to evaluate the quality of
        the entrance and return a score indicative of the quality of the same.
        """
        score = 50_000
        # The way the quality of an entrance is evaluated is dependent on its position and orientation.
        if position == self.TOP:
            # We already know by checking whether this is an entrance that it performs the basic function of an entrance,
            # however, it is also possible for an entrance to serve as a trap, provided that the opponent can make a
            # move at either end of the entrance, giving me access to a corner. Here, we check whether that is the case.
            if self.isValidMove(0, 1, opponent, me, gameState) and not self.isValidMove(0, 6, opponent, me, gameState):
                score += 4950
            elif self.isValidMove(0, 6, opponent, me, gameState) and not self.isValidMove(0, 1, opponent, me, gameState):
                score += 4950

        elif position == self.LEFT:
            if self.isValidMove(1, 0, opponent, me, gameState) and not self.isValidMove(6, 0, opponent, me, gameState):
                score += 4950
            elif self.isValidMove(6, 0, opponent, me, gameState) and not self.isValidMove(1, 0, opponent, me, gameState):
                score += 4950

        elif position == self.RIGHT:
            if self.isValidMove(1, 7, opponent, me, gameState) and not self.isValidMove(6, 7, opponent, me, gameState):
                score += 4950
            elif self.isValidMove(6, 7, opponent, me, gameState) and not self.isValidMove(1, 7, opponent, me, gameState):
                score += 4950

        elif position == self.BOTTOM:
            if self.isValidMove(7, 1, opponent, me, gameState) and not self.isValidMove(7, 6, opponent, me, gameState):
                score += 4950
            elif self.isValidMove(7, 6, opponent, me, gameState) and not self.isValidMove(7, 1, opponent, me, gameState):
                score += 4950

        else:
            raise ValueError("The position parameter of evaluateEntrance must be a tuple with the values: (0, 2), (2, 0), (2, 6) or (6, 2) or the class constants TOP, LEFT, RIGHT, or BOTTOM!")

        return score

    def isTrap (self, position, me, opponent, gameState):
        """
        PlayerBot.isTrap(self, position, me, opponent, gameState)
        This is a boolean method used to assess whether there is a trap at the specified position.
        """
        # How to assess whether a pattern of pieces is a trap is dependent on the position and orientation of the supposed trap.
        if position == self.TOP:
            if gameState[0][2] == me and gameState[0][5] == me and (gameState[0][3] == opponent or gameState[0][4] == opponent): return True

        elif position == self.LEFT:
            if gameState[2][0] == me and gameState[5][0] == me and (gameState[3][0] == opponent or gameState[4][0] == opponent): return True

        elif position == self.RIGHT:
            if gameState[2][7] == me and gameState[5][7] == me and (gameState[3][7] == opponent or gameState[4][7] == opponent): return True

        elif position == self.BOTTOM:
            if gameState[7][2] == me and gameState[7][5] == me and (gameState[7][3] == opponent or gameState[7][4] == opponent): return True
        else:
            raise ValueError("The position parameter of isTrap must be a tuple with the values: (0, 2), (2, 0), (2, 6) or (6, 2) or the class constants TOP, LEFT, RIGHT, or BOTTOM!")

        return False

    def evaluateTrap(self, position, me, opponent, gameState):
        """
        PlayerBot.evaluateTrap(self, position, me, opponent, gameState)
        This method is used by assessMidGame to assess the quality of a trap at a
        given position.
        """
        score = 50_000

        # As in the isTrap method, in order to evaluate the quality of the trap, we must
        # know the position of the trap.
        if position == self.TOP:
            # Check whether the trap is complete. An incomplete trap is ineffective but still has
            # the potential to be completed as gameplay progresses.
            if gameState[0][3] != self.bdCode and gameState[0][4] != self.bdCode: score += 2000
            # Check whether the trap is failure protected. If the bot renders it impossible to place
            # a piece in the buffer around the trap, the player cannot force it to do so.
            if gameState[1][2] == me: score += 2500
            if gameState[1][5] == me: score += 2500

        elif position == self.LEFT:
            if gameState[3][0] != self.bdCode and gameState[4][0] != self.bdCode: score += 2000
            if gameState[2][1] == me: score += 2500
            if gameState[5][1] == me: score += 2500

        elif position == self.RIGHT:
            if gameState[3][7] != self.bdCode and gameState[4][7] != self.bdCode: score += 2000
            if gameState[2][6] == me: score += 2500
            if gameState[5][6] == me: score += 2500

        elif position == self.BOTTOM:
            if gameState[7][3] != self.bdCode and gameState[7][4] != self.bdCode: score += 2000
            if gameState[6][2] == me: score += 2500
            if gameState[6][5] == me: score += 2500

        else:
            raise ValueError("The position parameter of evaluateTrap must be a tuple with the values: (0, 2), (2, 0), (2, 6) or (6, 2) or the class constants TOP, LEFT, RIGHT, or BOTTOM!")

        return score

    def assessLateGame (self, gameState):
        """
        PlayerBot.assessLateGame(self, gameState)
        This method is used by scoreGameState to evaluate games which have progressed to late stage.
        """
        return self.evaluateCornerAreas(gameState)

    def getStage (self, gameState):

        for row in range(2, 5):
            for col in range(2, 5):
                if self.isValidMove(row, col, self.p2Code, self.p1Code, gameState): return self.ST_EARLY
                
        for row in range(2, 5):
            for col in range(0, 7):
                if col in range(0, 2) or col in range(6, 8):
                    if self.isValidMove(row, col, self.p2Code, self.p1Code, gameState): return self.ST_MID
        for row in range(0, 7):
            for col in range(2, 5):
                if row in range(0, 2) or row in range(6, 8):
                    if self.isValidMove(row, col, self.p2Code, self.p1Code, gameState): return self.ST_MID

        return self.ST_LATE
                    

    def isValidMove (self, row, col, me, opponent, gameState):
        """
        PlayerBot.isValidMove(self, row, col, me, opponent, gamestate)
        Returns True if the given move is valid for the given gamestate and
        False if the given move is invalid for the given gamestate.
        """
        if gameState[row][col] != self.bdCode:
            return False # If the space where we're trying to move isn't empty, we already know this move is invalid.

        scanningDirections = ((-1, 0), (0, 1), (1, 0), (0, -1), # A series of scanning vectors.
                              (-1, -1), (-1, 1), (1, 1), (1, -1))
        for SDRow, SDCol in scanningDirections: # Iterate over the scanning vectors.
            currentRow = row + SDRow # Start scanning at a position offset from the move by one
            currentCol = col + SDCol # along the current scanning vector.
            sawOpponent = False      # The opponents gamepieces haven't yet been seen on this vector.
            while currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == self.bdCode: break # If the gamespace is empty, we know there are no pieces to flip on this vector.
                if gameState[currentRow][currentCol] == opponent: sawOpponent = True # The opponents gamepieces have been seen.
                if gameState[currentRow][currentCol] == me and sawOpponent:
                    return True # There are at least pieceses on this vector that can be flipped. The move is valid.
                if gameState[currentRow][currentCol] == me and not sawOpponent: break # There are no pieces to flip along this vector. Proceed to the next.
                
                currentRow += SDRow # Proceed to the next gamespace in the current vector.
                currentCol += SDCol

        return False # If we've fallen out of the vector scanning loop, we know the move is invalid.

    def updateGameState (self):
        """
        PlayerBot.updateGameState(self)
        Synchronizes the objects gameState attribute with the current state of parent.gameboard.
        """
        for row in range(8):
            for col in range(8): # Iterate over the parents gameboard and insert integer values into self.gameState
                                 # corresponding to black pieces, white pieces and empty spaces.
                if self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.bgAndBoardColor:
                    self.gameState[row][col] = self.bdCode
                elif self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.player1Color:
                    self.gameState[row][col] = self.p1Code
                elif self.parent.gameboard[row][col].GetBackgroundColour() == self.parent.player2Color:
                    self.gameState[row][col] = self.p2Code

    def scoreMove (self, possibleMove, gameState, me, opponent):
        """
        PlayerBot.scoreMove (self, possibleMove, gameState, me, opponent)
        Calculate the number of pieces captured by a given move in a given game state
        and return a tuple containing the number of captures at index 0 and the state
        of the game after the move at index 1
        """
        gameState = gameState.copy() # We wouldn't want to alter the value of self.gameState now, would we?
        row, col = possibleMove # Unpack the move parameter to make the code more readable.
        moveScore = 1 # We already know that we at least have the grid space where we placed our piece.
        scanningDirections = ((-1, 0), (0, 1), (1, 0), (0, -1), # A series of scanning vectors
                              (-1, -1), (-1, 1), (1, 1), (1, -1))
        for SDRow, SDCol in scanningDirections: # Scann along all 8 vectors.
            currentRow = row + SDRow # Start at a position offset from the position of the move along the current
            currentCol = col + SDCol # scanning vector.
            sawOpponent = False # None of the opponents gamepieces have been seen on the current scanning vector at this time.
            canCountPieces = False # No row of my opponents pieces with another of my pieces at the other end has been seen on
                                   # on this scanning vector at this time.
            while currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == self.bdCode: break # If we see an empty space, we know we can't flip pieces on this vector.
                if gameState[currentRow][currentCol] == self.p1Code: sawOpponent = True
                if gameState[currentRow][currentCol] == me and sawOpponent:
                    canCountPieces = True # We now know we can flip pieces on this vector.
                    break # There is no need to continue scanning this vector.
                if gameState[currentRow][currentCol] == me and not sawOpponent: break # If I see another of my pieces without seeing an opponents piece,
                                                                                      # there are no pieces to flip on this vector.
                currentRow += SDRow
                currentCol += SDCol

            currentRow = row + SDRow
            currentCol = col + SDCol
            while canCountPieces and currentRow in range(0, 8) and currentCol in range(0, 8):
                if gameState[currentRow][currentCol] == opponent:
                    gameState[currentRow][currentCol] = me # Flip the pieces on this vector and increment the move score.
                    moveScore += 1
                elif gameState[currentRow][currentCol] == me:
                    break
                
                currentRow += SDRow
                currentCol += SDCol

        return moveScore, gameState # Return the tuple
I'm going to try to fix the getStage method and adding rewards for moves which can help the bot toward strategic goals, but any advice would be greatly appreciated.
Reply
#20
I've been attempting to get my getStage method to work and its malfunctions continue to defy all logic. I've removed the now obviously invalid logic in the previous version posted above, but it still insists that games are mid game or late game when they are obviously early game. Here is the method as it is now:
class PlayerBot:
    .
    .
    .
    def getStage (self, gameState):

        for row in range(2, 6):
            for col in range(2, 6):
                if self.isValidMove(row, col, self.p2Code, self.p1Code, gameState):
                    print("I decided it's early game.")
                    return self.ST_EARLY

        # Scan everything but the center and the corner areas for a valid move and return self.ST_MID if a valid move is found.
        # Start with the top and bottom center edges.
        for row in range(0, 8):
            for col in range(2, 6):
                if row not in range(2, 6):
                    if self.isValidMove(row, col, self.p2Code, self.p1Code, gameState):
                        print("I decided it's mid game.")
                        return self.ST_MID
        # Do the left and right center edges.
        for row in range(2, 6):
            for col in range(0, 8):
                if col not in range(2, 6):
                    if self.isValidMove(row, col, self.p2Code, self.p1Code, gameState):
                        print("I decided it's mid game.")
                        return self.ST_MID

        print("I decided it's late game.")
        return self.ST_LATE
    .
    .
    .
Using dots to represent areas each for loop isn't supposed to scan and octothorpes for areas which are supposed to be scanned, here's what each for loop is supposed to scan:
Output:
The first for loop: . . . . . . . . . . . . . . . . . . # # # # . . . . # # # # . . . . # # # # . . . . # # # # . . . . . . . . . . . . . . . . . . The second for loop: . . # # # # . . . . # # # # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . # # # # . . . . # # # # . . The third for loop: . . . . . . . . . . . . . . . . # # . . . . # # # # . . . . # # # # . . . . # # # # . . . . # # . . . . . . . . . . . . . . . .
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyGame] adding mouse control to game flash77 2 205 Apr-08-2024, 06:52 PM
Last Post: flash77
  Creating a “Player” class, and then importing it into game onizuka 4 3,039 Sep-01-2020, 06:06 PM
Last Post: onizuka
  Adding an inventory and a combat system to a text based adventure game detkitten 2 6,872 Dec-17-2019, 03:40 AM
Last Post: detkitten
  Adding persistent multiple levels to game michael1789 2 2,407 Nov-16-2019, 01:15 AM
Last Post: michael1789
  Can a player play game created with Python without installing Python? hsunteik 3 5,289 Feb-23-2017, 10:44 AM
Last Post: Larz60+

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020