def valueOfMove(self, board, move):
     board = self.game.preRotate(move, board)
     testGame = GameState()
     testGame.setBoard(board)
     testGame.setBoard(testGame.copyArr())
     value = 0
     
     # store previous information
     oldScore = testGame.getScore()
     oldMaxTile = testGame.getMaxTile()
     
     testGame.executeMove(Move.down)
     newScore = testGame.getScore()
     value += newScore - oldScore
     
     # check if the largest tile is in a corner after the move
     newMaxTile = testGame.getMaxTile()
     for corner in self.fourCorners:
         if testGame.gameArray[corner[0]][corner[1]] == newMaxTile:
             value *= self.maxInCornerMultiplier
             value += self.cornerBonusScaledByMax * newMaxTile
             value += self.enterCorner
     
     # penalty for moving down
     if move == Move.down:
         value -= self.moveDownPenalty * newMaxTile
     
     board = self.game.postRotate(move, board)
     return value
Ejemplo n.º 2
0
class RandomSolver(object):
    '''
    classdocs
    '''

    def __init__(self):
        '''
        Constructor
        '''
        self.game = GameState()
        self.numMoves = 0
        pass
    
    
    def playGame(self):
        while (self.game.isGoing()):
            'pick a move'
            move = randint(1, 4)
            'execute move'
            if (self.game.isValid(Move(move))):
                self.game.takeMove(Move(move))
                self.numMoves += 1
        pass
    
    def getScore(self):
        return self.game.getScore()
    
    def getMaxTile(self):
        return self.game.getMaxTile()
    
    def getMoves(self):
        return self.numMoves
    
    def printGame(self):
        self.game.printState()
        pass
class SearchRandomComparison(object):
    '''
    classdocs
    '''


    def __init__(self, inDepth, zeroes):
        '''
        Constructor
        '''
        self.game = GameState()
        self.numMoves = 0
        self.depth = inDepth
        self.zeroes = zeroes
        self.disagreements = 0
        self.comparisons = 0
        pass
    
    
    'keeps searching for moves until the game is complete'
    def playGame(self):
        self.game.printState(self.game.gameArray)
        
        count = 0
        disagreements = 0
        comparisons = 0
        
        while (self.game.isGoing()):
            print("\nStarting move: " + datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S'))
            
            bestMove = self.searchRandom(self.game.copyArr(), self.depth)
            print(bestMove[0])
            
            if self.enoughZeroes():
                self.comparisons += 1
                noRandomMove = self.searchNoRandom(self.game.copyArr(), self.depth)
                if not noRandomMove == bestMove:
                    self.disagreements += 1
            
            
            # when at the end, all decisions might lead to an inevitable failure
            if (not self.game.isValid(bestMove)):
                pass
            
            #self.game.printState(self.game.gameArray)
            self.game.takeMove(bestMove[0])
            self.game.printState(self.game.gameArray)
            self.numMoves = self.numMoves + 1
        
        print(self.numMoves)
        pass
    
    
    'determines whether or not a comparison should occur based on how full the board is'
    'number of required 0s is determined at solver creation'
    def enoughZeroes(self):
        numZeroes = 0
        
        # count number of 0-tiles
        for x in range (0, 4):
            for y in range (0, 4):
                if self.game.gameArray[x][y] == 0:
                    numZeroes += 1
        
        if numZeroes >= self.zeroes:
            return True
        return False
    
    
    'returns best move and the value of that move factoring in random tiles'
    'best move is only useful for the top-level call'
    def searchRandom(self, board, depth):
        if (depth == 0):
            return (Move.up, 0)
        
        bestMove = Move.up
        bestValue = -1
        
        move = Move.up
        moveValue = self.searchDirectionRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        move = Move.left
        moveValue = self.searchDirectionRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
            
        move = Move.right
        moveValue = self.searchDirectionRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        move = Move.down
        moveValue = self.searchDirectionRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        return (bestMove, bestValue)
    
    
    'returns the number of matches that a given move would make'
    'this only determines value of one move and no further searching'
    def valueOfMove(self, board, move):
        return value(self.game.preRotate(move, board), self.game, move)
    
    
    'returns the expected value of a given move searching with the given depth factoring in random tiles'
    def searchDirectionRandom(self, board, depth, move):
        testGame = GameState()
        testGame.setBoard(board)
        testGame.setBoard(testGame.copyArr())
        
        # if the move isn't valid, don't consider it
        if (not testGame.isValid(move)):
            return -1
        
        #determine the value for making the move at this level
        ourValue = self.valueOfMove(testGame.gameArray, move)
        
        #'using that as the starting board, check a lot of possibilities'
        afterMove = testGame.executeMove(move)
        testGame.setBoard(afterMove)
        ev2 = [[0 for x in range(4)] for x in range(4)]
        ev4 = [[0 for x in range(4)] for x in range(4)]
        
        options = 0
        searchValue = 0
        
        # determine the value of each cell
        for x in range (0, 4):
            for y in range (0, 4):
                trialBoard = testGame.copyArr()
                if (trialBoard[x][y] == 0):
                    options += 1
                    
                    trialBoard[x][y] = 2
                    ev2[x][y] = self.searchRandom(trialBoard, depth - 1)[1]
                    trialBoard[x][y] = 0
                    
                    trialBoard[x][y] = 4
                    ev4[x][y] = self.searchRandom(trialBoard, depth - 1)[1]
                    trialBoard[x][y] = 0
        
        
        # adjust those cells for their likelihood
        for x in range (0, 4):
            for y in range (0, 4):
                searchValue += (ev2[x][y] * 0.9) / options
                searchValue += (ev4[x][y] * 0.1) / options
        
        return ourValue + searchValue
    
    
    'returns best move and the value of that move'
    'best move is only useful for the top-level call'
    def searchNoRandom(self, board, depth):    
        if (depth == 0):
            return (Move.up, 0)
        
        bestMove = Move.up
        bestValue = -1
        
        move = Move.up
        moveValue = self.searchDirectionNoRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        move = Move.left
        moveValue = self.searchDirectionNoRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
            
        move = Move.right
        moveValue = self.searchDirectionNoRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        move = Move.down
        moveValue = self.searchDirectionNoRandom(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        return (bestMove, bestValue)


    'returns the expected value of a given move searching with the given depth'
    'this ignores the new tiles appearing, which saves tons on complexity'
    def searchDirectionNoRandom(self, board, depth, move):
        testGame = GameState()
        testGame.setBoard(board)
        testGame.setBoard(testGame.copyArr())
        
        # if the move isn't valid, don't consider it
        if (not testGame.isValid(move)):
            return -1
        
        # determine the value for making the move at this level
        ourValue = self.valueOfMove(testGame.gameArray, move)
        
        # using that as the starting board, check the child's options
        afterMove = testGame.executeMove(move)
        #testGame.setBoard(afterMove)
        
        #trialBoard = testGame.copyArr()
        searchValue = self.searchNoRandom(afterMove, depth - 1)[1]
        
        return ourValue + searchValue


    'class specific stats'
    def getComparisons(self):
        return self.comparisons
    
    def getDisagreements(self):
        return self.disagreements

    'generic methods of every solver'
    def getScore(self):
        return self.game.getScore()
    
    def getMaxTile(self):
        return self.game.getMaxTile()
    
    def getMoves(self):
        return self.numMoves
    
    def printGame(self):
        self.game.printState()
        pass
class GreedySearchPercentageDepth(object):
    minimumDepth = 2
    
    fourCorners = {(0, 0), (0, 3), (3, 0), (3, 3)}
    options = {Move.down, Move.left, Move.up, Move.right}
    
    enterCorner = 0
    maxInCornerMultiplier = 1.0
    cornerBonusScaledByMax = 0.8
    moveDownPenalty = 0.0



    def __init__(self, threshold):
        self.game = GameState()
        self.numMoves = 0
        self.threshold = threshold
        pass
    
    
    'keeps searching for moves until the game is complete'
    def playGame(self):
        self.game.printState(self.game.gameArray)
        
        count = 0
        
        while (self.game.isGoing()):
            self.possibilities = 0
            moveStart = time.time()
            print("Starting move: " + datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S'))
            testBoard = self.game.copyArr()
            
            bestMove = self.search(testBoard, 1, 0)
            moveEnd = time.time()
            totalTime = moveEnd - moveStart
            print(bestMove[0])
            print("time to search " + str(self.possibilities) + " possibilities moves: " + str(totalTime))
            print("time per possibility: " + str(totalTime / self.possibilities))
            
            
            # when at the end, all decisions might lead to an inevitable failure
            if (not self.game.isValid(bestMove)):
                pass
            
            #self.game.printState(self.game.gameArray)
            self.game.takeMove(bestMove[0])
            self.game.printState(self.game.gameArray)
            self.numMoves = self.numMoves + 1
        
        print(self.numMoves)
        pass
    
    
    'returns best move and the value of that move'
    'best move is only useful for the top-level call'
    def search(self, board, likelihood, depth):
        if (depth >= self.minimumDepth and likelihood < self.threshold):
            return (Move.up, 0)
        self.possibilities += 1
        
        bestMove = Move.up
        bestValue = -1
        
        for move in self.options:
            moveValue = self.searchDirection(board, likelihood, move, depth + 1)
            if (moveValue > bestValue):
                bestMove = move
                bestValue = moveValue
        
        return (bestMove, bestValue)
    
    
    'returns the number of matches that a given move would make'
    'this only determines value of one move and no further searching'
    def valueOfMove(self, board, move):
        board = self.game.preRotate(move, board)
        testGame = GameState()
        testGame.setBoard(board)
        testGame.setBoard(testGame.copyArr())
        value = 0
        
        # store previous information
        oldScore = testGame.getScore()
        oldMaxTile = testGame.getMaxTile()
        
        testGame.executeMove(Move.down)
        newScore = testGame.getScore()
        value += newScore - oldScore
        
        # check if the largest tile is in a corner after the move
        newMaxTile = testGame.getMaxTile()
        for corner in self.fourCorners:
            if testGame.gameArray[corner[0]][corner[1]] == newMaxTile:
                value *= self.maxInCornerMultiplier
                value += self.cornerBonusScaledByMax * newMaxTile
                value += self.enterCorner
        
        # penalty for moving down
        if move == Move.down:
            value -= self.moveDownPenalty * newMaxTile
        
        board = self.game.postRotate(move, board)
        return value
    
    
    'returns the expected value of a given move searching with the given likelihood'
    def searchDirection(self, board, likelihood, move, depth):
        testGame = GameState()
        testGame.setBoard(board)
        testGame.setBoard(testGame.copyArr())
        
        # if the move isn't valid, don't consider it
        if (not testGame.isValid(move)):
            return -1
        
        #determine the value for making the move at this level
        ourValue = self.valueOfMove(testGame.gameArray, move)
        
        #'using that as the starting board, check a lot of possibilities'
        afterMove = testGame.executeMove(move)
        testGame.setBoard(afterMove)
        ev2 = [[0 for x in range(4)] for x in range(4)]
        ev4 = [[0 for x in range(4)] for x in range(4)]
        
        options = 0
        searchValue = 0
        
        # determine which cells can have a new tile
        trialBoard = testGame.copyArr()
        for x in range (0, 4):
            for y in range (0, 4):
                if (trialBoard[x][y] == 0):
                    options += 1
        
        # determine the value of each cell
        for x in range (0, 4):
            for y in range (0, 4):
                trialBoard = testGame.copyArr()
                if (trialBoard[x][y] == 0):
                    cellChance = likelihood / options
                    
                    trialBoard[x][y] = 2
                    ev2[x][y] = cellChance * 0.9 * self.search(trialBoard, likelihood * cellChance * 0.9, depth)[1]
                    trialBoard[x][y] = 0
                    
                    trialBoard[x][y] = 4
                    ev4[x][y] = cellChance * 0.9 * self.search(trialBoard, likelihood * cellChance * 0.1, depth)[1]
                    trialBoard[x][y] = 0
        
        return ourValue + searchValue
    
    
    'generic methods of every solver'
    def getScore(self):
        return self.game.getScore()
    
    def getMaxTile(self):
        return self.game.getMaxTile()
    
    def getMoves(self):
        return self.numMoves
    
    def printGame(self):
        self.game.printState()
        pass
Ejemplo n.º 5
0
class RegionSearch(object):
    """
    classdocs
    """

    def __init__(self, inDepth):
        """
        Constructor
        """
        self.game = GameState()
        self.numMoves = 0
        self.depth = inDepth
        pass

    "keeps searching for moves until the game is complete"

    def playGame(self):
        self.game.printState(self.game.gameArray)

        count = 0

        while self.game.isGoing():
            print("\nStarting move: " + datetime.datetime.fromtimestamp(time.time()).strftime("%Y-%m-%d %H:%M:%S"))
            testBoard = self.game.copyArr()

            bestMove = self.search(testBoard, self.depth)
            print(bestMove[0])

            # when at the end, all decisions might lead to an inevitable failure
            if not self.game.isValid(bestMove):
                pass

            # self.game.printState(self.game.gameArray)
            self.game.takeMove(bestMove[0])
            self.game.printState(self.game.gameArray)
            self.numMoves = self.numMoves + 1

        print(self.numMoves)
        pass

    "returns best move and the value of that move"
    "best move is only useful for the top-level call"

    def search(self, board, depth):
        if depth == 0:
            return (Move.up, 0)

        bestMove = Move.up
        bestValue = -1

        move = Move.up
        moveValue = self.searchDirection(board, depth, move)
        if moveValue > bestValue:
            bestMove = move
            bestValue = moveValue

        move = Move.left
        moveValue = self.searchDirection(board, depth, move)
        if moveValue > bestValue:
            bestMove = move
            bestValue = moveValue

        move = Move.right
        moveValue = self.searchDirection(board, depth, move)
        if moveValue > bestValue:
            bestMove = move
            bestValue = moveValue

        move = Move.down
        moveValue = self.searchDirection(board, depth, move)
        if moveValue > bestValue:
            bestMove = move
            bestValue = moveValue

        return (bestMove, bestValue)

    "returns the number of matches that a given move would make"
    "this only determines value of one move and no further searching"

    def valueOfMove(self, board, move):
        board = self.game.preRotate(move, board)

        value = 0
        for x in range(0, 4):
            value += self.game.countSlideDownMatches(x, board)

        board = self.game.postRotate(move, board)
        return value

    "returns the expected value of a given move searching with the given depth"

    def searchDirection(self, board, depth, move):
        testGame = GameState()
        testGame.setBoard(board)
        testGame.setBoard(testGame.copyArr())

        # if the move isn't valid, don't consider it
        if not testGame.isValid(move):
            return -1

        # determine the value for making the move at this level
        ourValue = self.valueOfMove(testGame.gameArray, move)

        #'using that as the starting board, check a lot of possibilities'
        afterMove = testGame.executeMove(move)
        testGame.setBoard(afterMove)

        options = 0
        searchValue = 0

        # arrays
        ev2 = [[0 for x in range(2)] for x in range(2)]
        ev4 = [[0 for x in range(2)] for x in range(2)]
        optionsInRegion = [[0 for x in range(2)] for x in range(2)]

        # determine value of the region
        for x in range(0, 2):  # location of region
            for y in range(0, 2):
                trialBoard = testGame.copyArr()
                validLocs = []
                for rx in range(0, 2):  # location within region
                    for ry in range(0, 2):
                        locX = (x * 2) + rx
                        locY = (y * 2) + ry

                        # take all 0s into account for likelihood
                        if trialBoard[locX][locY] == 0:
                            optionsInRegion[x][y] += 1
                            options += 1
                            validLocs += [(locX, locY)]

                # find a cell in the region to test
                possibilities = optionsInRegion[x][y]
                for loc in validLocs:
                    rand = randrange(0, 100)

                    # randomly select one cell in the region to care about
                    if rand < ((1 / possibilities) * 100):  # test whether or not this is the cell we care about
                        trialBoard[loc[0]][loc[1]] = 2
                        ev2[x][y] = self.search(trialBoard, depth - 1)[1]

                        trialBoard[loc[0]][loc[1]] = 4
                        ev4[x][y] = self.search(trialBoard, depth - 1)[1]
                        trialBoard[loc[0]][loc[1]] = 0
                        break
                    possibilities -= 1

                # reset our locations for the next region
                validLocs.clear()

        # adjust those region evs for their likelihood
        for x in range(0, 2):
            for y in range(0, 2):
                searchValue += (ev2[x][y] * 0.9) * (optionsInRegion[x][y] / options)
                searchValue += (ev4[x][y] * 0.1) * (optionsInRegion[x][y] / options)

        return ourValue + searchValue

    "generic methods of every solver"

    def getScore(self):
        return self.game.getScore()

    def getMaxTile(self):
        return self.game.getMaxTile()

    def getMoves(self):
        return self.numMoves

    def printGame(self):
        self.game.printState()
        pass
Ejemplo n.º 6
0
class SearchSolver(object):
    '''
    classdocs
    '''


    def __init__(self, inDepth):
        '''
        Constructor
        '''
        self.depth = inDepth
        self.game = GameState()
        self.numMoves = 0
        pass
    
    
    def playGame(self):
        while (self.game.isGoing()):
            self.startSearch(self.game.copyArr(), self.depth)
            break
        pass
    
    
    # determines the move with the highest ev with the given search depth
    def startSearch(self, board, depth):
        bestOption = None
        bestVal = -1
        
        # base case, depth == 0
        # in this case, estimate rest of the way greedy?
        if (depth == 0):
            return (Move.up, 0)
        
        upVal = self.estimateValue(depth - 1, board, Move.up)
        print(upVal)
        if (upVal > bestVal):
            bestOption = Move.up
        
        rightVal = self.estimateValue(depth - 1, board, Move.right)
        if (rightVal > bestVal):
            bestOption = Move.right
        
        return (bestOption, bestVal)
    
    
    def estimateValue(self, depth, board, move):
        ev2 = [[0 for x in range(4)] for x in range(4)]
        ev4 = [[0 for x in range(4)] for x in range(4)]
        ev = 0
        numOptions = 0
        
        # go through all options
        if(self.game.isValid(move)): 
            baseGameUp = GameState()
            baseGameUp.setBoard(board)
            baseGameUp.executeMove(move)
            
            for x in range (0, 4):
                for y in range (0, 4):
                    if(baseGameUp.gameArray[x][y] == 0):
                        numOptions += 2
                        
                        # per cell ev expecting 4
                        pretendBoard2 = baseGameUp.copyArr()
                        pretendGame2 = GameState()
                        pretendBoard2[x][y] = 2
                        pretendGame2.setBoard(pretendBoard2)
                        searchEv = self.startSearch(pretendGame2.copyArr(), depth - 1)
                        ev2[x][y] = searchEv[1]
                        pretendGame4 = None
                    
                        # per cell ev expecting 4
                        pretendBoard4 = baseGameUp.copyArr()
                        pretendGame4 = GameState()
                        pretendBoard4[x][y] = 4
                        pretendGame4.setBoard(pretendBoard4)
                        searchEv = self.startSearch(pretendGame4.copyArr(), depth - 1)
                        ev4[x][y] = searchEv[1]
                        pretendGame4 = None
                    
                        pass
        return ev
    
    
    def getScore(self):
        return self.game.getScore()
    
    def getMaxTile(self):
        return self.game.getMaxTile()
    
    def getMoves(self):
        return self.numMoves
    
    def printGame(self):
        self.game.printState()
        pass
Ejemplo n.º 7
0
class GreedySearch(object):
    fourCorners = {(0, 0), (0, 3), (3, 0), (3, 3)}
    possibilities = {Move.down, Move.left, Move.up, Move.right}
    
    enterCorner = 0
    maxInCornerMultiplier = 1.0
    cornerBonusScaledByMax = 0.8
    moveDownPenalty = 0.0

    def __init__(self, inDepth):
        '''
        Constructor
        '''
        self.game = GameState()
        self.numMoves = 0
        self.depth = inDepth
        pass
    
    
    'keeps searching for moves until the game is complete'
    def playGame(self):
        count = 0
        
        while (self.game.isGoing()):
            print("\nStarting move: " + datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S'))
            testBoard = self.game.copyArr()
            
            bestMove = self.search(testBoard, self.depth)
            print(bestMove[0])
            
            self.game.printState(self.game.gameArray)
            wasSuccessful = self.game.takeMove(bestMove[0])
            self.numMoves = self.numMoves + 1
            if not wasSuccessful:
                break
        
        self.game.printState(self.game.gameArray)
        print("number of moves in game: " + str(self.numMoves))
        
        pass
    
    
    'returns best move and the value of that move'
    'best move is only useful for the top-level call'
    def search(self, board, depth):
        bestMove = Move.up
        bestValue = -1
        
        for move in self.possibilities:
            moveValue = self.searchDirection(board, depth, move)
            if (moveValue > bestValue):
                bestMove = move
                bestValue = moveValue
        
        return (bestMove, bestValue)
    
    
    'returns the number of matches that a given move would make'
    'this only determines value of one move and no further searching'
    def valueOfMove(self, board, move):
        return value(self.game.preRotate(move, board), self.game, move)
    
    'returns the expected value of a given move searching with the given depth'
    def searchDirection(self, board, depth, move):
        testGame = GameState()
        testGame.setBoard(board)
        testGame.setBoard(testGame.copyArr())
        
        # if the move isn't valid, don't consider it
        if (not testGame.isValid(move)):
            return -1
        
        # determine the value for making the move at this level
        ourValue = self.valueOfMove(testGame.gameArray, move)
        
        # if we have reached bottom depth, stop searching and return the heuristic value
        if depth == 1:
            return ourValue
        
        # using that as the starting board, check a lot of possibilities
        afterMove = testGame.executeMove(move)
        testGame.setBoard(afterMove)
        ev2 = [[0 for x in range(4)] for x in range(4)]
        ev4 = [[0 for x in range(4)] for x in range(4)]
        
        options = 0
        searchValue = 0
        
        # determine the value of each cell
        for x in range (0, 4):
            for y in range (0, 4):
                trialBoard = testGame.copyArr()
                if (trialBoard[x][y] == 0):
                    options += 1
                    
                    trialBoard[x][y] = 2
                    ev2[x][y] = self.search(trialBoard, depth - 1)[1]
                    
                    trialBoard[x][y] = 4
                    ev4[x][y] = self.search(trialBoard, depth - 1)[1]
                    trialBoard[x][y] = 0
        
        
        # adjust those cells for their likelihood
        for x in range (0, 4):
            for y in range (0, 4):
                searchValue += (ev2[x][y] * 0.9) / options
                searchValue += (ev4[x][y] * 0.1) / options
        
        return ourValue + searchValue
    
    
    'generic methods of every solver'
    def getScore(self):
        return self.game.getScore()
    
    def getMaxTile(self):
        return self.game.getMaxTile()
    
    def getMoves(self):
        return self.numMoves
    
    def printGame(self):
        self.game.printState()
        pass
class GreedySearchNoRandom(object):

    def __init__(self, inDepth):
        self.game = GameState()
        self.numMoves = 0
        self.depth = inDepth
        pass
    
    
    'keeps searching for moves until the game is complete'
    def playGame(self):
        self.game.printState(self.game.gameArray)
        
        count = 0
        
        while (self.game.isGoing()):
            testBoard = self.game.copyArr()
            
            bestMove = self.search(testBoard, self.depth)
            print(bestMove[0])
            
            # when at the end, all decisions might lead to an inevitable failure
            if (not self.game.isValid(bestMove)):
                pass
            
            #self.game.printState(self.game.gameArray)
            self.game.takeMove(bestMove[0])
            self.game.printState(self.game.gameArray)
        pass
    
    
    'returns best move and the value of that move'
    'best move is only useful for the top-level call'
    def search(self, board, depth):    
        if (depth == 0):
            return (Move.up, 0)
        
        bestMove = Move.up
        bestValue = -1
        
        move = Move.up
        moveValue = self.searchDirection(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        move = Move.left
        moveValue = self.searchDirection(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
            
        move = Move.right
        moveValue = self.searchDirection(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        move = Move.down
        moveValue = self.searchDirection(board, depth, move)
        if (moveValue > bestValue):
            bestMove = move
            bestValue = moveValue
        
        return (bestMove, bestValue)
    
    
    'returns the number of matches that a given move would make'
    'this only determines value of one move and no further searching'
    def valueOfMove(self, board, move):
        return value(self.game.preRotate(move, board), self.game, move)
    
    
    'returns the expected value of a given move searching with the given depth'
    'this ignores the new tiles appearing, which saves tons on complexity'
    def searchDirection(self, board, depth, move):
        testGame = GameState()
        testGame.setBoard(board)
        testGame.setBoard(testGame.copyArr())
        
        # if the move isn't valid, don't consider it
        if (not testGame.isValid(move)):
            return -1
        
        # determine the value for making the move at this level
        ourValue = self.valueOfMove(testGame.gameArray, move)
        
        # using that as the starting board, check the child's options
        afterMove = testGame.executeMove(move)
        searchValue = self.search(afterMove, depth - 1)[1]
        
        return ourValue + searchValue
    
    
    'generic methods of every solver'
    def getScore(self):
        return self.game.getScore()
    
    def getMaxTile(self):
        return self.game.getMaxTile()
    
    def getMoves(self):
        return self.numMoves
    
    def printGame(self):
        self.game.printState()
        pass