Exemplo n.º 1
0
class NeuralNetSolver:
    def __init__(self, rows, cols, bombs):
        self.board = Board(rows, cols, bombs)
        self.unmarkedBombs = bombs
        if torch.cuda.is_available():
            self.net = miniNet().cuda()
        else:
            self.net = miniNet()
            
        self.optimizer = torch.optim.Adam(self.net.parameters(), lr=LR, weight_decay=REG)
        self.net.load(self.optimizer)
    
    def firstMove(self):
        self.board.explore(0,0)
        return self.board
    
    def getAllSurroundingTiles(self, tile):
        # TODO: might need just a shared functions file for this nonsense
        surrounding = []

        row = tile.row
        col = tile.col

        surrounding.append(self.board.left(row, col))
        surrounding.append(self.board.right(row, col))
        surrounding.append(self.board.up(row, col))
        surrounding.append(self.board.down(row, col))
        surrounding.append(self.board.upLeft(row, col))
        surrounding.append(self.board.upRight(row, col))
        surrounding.append(self.board.downLeft(row, col))
        surrounding.append(self.board.downRight(row, col))
        
        return surrounding
    
    def mark(self, row, col):
        """ Marks the tile at the row and column as a bomb
        and updates the remaining value of surrounding tiles """
        self.board.mark(row, col)
        self.unmarkedBombs -= 1
        tilesToReduce = self.getAllSurroundingTiles(Tile(0, row, col))
        for t in tilesToReduce:
            if t is not None and t.remainingValue > 0 and t.value != BOMB:
                self.board.board[t.row][t.col].remainingValue -= 1
    
    def move(self):
        """
        Performs the optimal move, if the move was successful returns the
        current board, else returns None to signify that a mine was triggered
        """
        # TODO: clean/reafactor
        validTiles = transformBoard(self.board)
        nnInput = [x["nn"] for x in validTiles]
        tiles = [x["tile"] for x in validTiles]
        probs = self.determineProbs(torch.Tensor(nnInput)).cpu().numpy()
        validTiles = validTiles[::6] #Get rid of duplicates since they're already extracted
        avgProbs = np.mean(probs.reshape(-1, 6, 2), axis=1)

        confidence = [max(x) for x in avgProbs]
        for i in range(len(confidence)):
            validTiles[i]["confidence"] = confidence[i]

        isExplore = lambda x: x[0] > x[1]
        explores = np.array([isExplore(x) for x in avgProbs])
        marks = ~explores
        explores = validTiles[explores]
        marks = validTiles[marks]

        # TESTING
        # print("EXPLORES:")
        # for e in explores:
        #     print(e["tile"].row, e["tile"].col, "certainty:", e["confidence"])
        # print("MARKS:")
        # for e in marks:
        #     print(e["tile"].row, e["tile"].col, "certainty:", e["confidence"])
        # TESTING
        
        exploreMove = None
        markMove = None
        dictToList = lambda d, key: [x[key] for x in d]
        if len(explores) > 0:
            exploreMove = explores[np.argmin(dictToList(explores, "confidence"))]

        if len(marks) > 0:
            # TESTING: I'm not sure maybe this is cheating, this is waht it was before:
            # markMove = marks[np.argmax(dictToList(marks, "confidence"))]
            valid = False
            while not valid and len(marks) > 0:
                index = np.argmax(dictToList(marks, "confidence"))
                markMove = marks[index]
                surroundingTiles = self.getAllSurroundingTiles(markMove["tile"])
                surroundingValues = [t.remainingValue for t in surroundingTiles if t is not None and t.explored]
                if 0 in surroundingValues:
                    marks = np.delete(marks, index)
                    # print("-"*16, "Tried to make a stupid move :(", "-"*16, ":", "MARK:", markMove["tile"].row, markMove["tile"].col)
                    # print("Instead I will", "Explore:", markMove["tile"].row, markMove["tile"].col)
                    self.board.explore(markMove["tile"].row, markMove["tile"].col)
                    return self.board
                else:
                    valid = True

        exploredProportion = 1 #EXPLORE_COEFF * self.getExploredProportion() # TODO: Maybe more like how explored is an area?

        if exploreMove is not None and exploreMove["confidence"] > MOVE_CERTAINTY_THRESHOLD * exploredProportion: # TODO: explore more in the early game, mark late game
            move = 0
            tile = exploreMove["tile"]
        elif markMove is not None and markMove["confidence"] > MARK_CERTAINTY_THRESHOLD / exploredProportion:
            move = 1
            tile = markMove["tile"]
        else:
            # TODO: consider unexplored tiles adjacent to validTiles ?
            # print("-"*16, "I AM UNCERTAIN ABOUT THIS MOVE!", "-"*16)
            farTiles = []
            for r in self.board.board:
                for tile in r:
                    if not tile.explored and tile not in dictToList(validTiles, "tile"):
                        farTiles.append(tile)
            move = 0
            if len(farTiles) > 0:
                tile = np.random.choice(farTiles)
            else:
                tile = exploreMove["tile"]

        # print("Chosen tile:", tile.row, tile.col)
        # print("Chosen move:", "Mark" if move == 1 else "Explore")
        # print("Certainty:", markMove["confidence"] if move == 1 else exploreMove["confidence"])
        # print("Thresholds: Explore", MOVE_CERTAINTY_THRESHOLD * exploredProportion, "Mark", MARK_CERTAINTY_THRESHOLD / exploredProportion)
        if move == 1:
            self.mark(tile.row, tile.col)
        else:
            if self.board.explore(tile.row, tile.col):
                return None # Game exploded
        
        return self.board

    def getExploredProportion(self):
        numExplored = 0
        numUnexplored = 0
        for r in self.board.board:
            for t in r:
                if t.explored or t.marked:
                    numExplored += 1
                else:
                    numUnexplored += 1
        return numExplored / (numUnexplored + numExplored)

    def determineProbs(self, tiles):
        """ Returns the output of the nn on the given tiles 
            As well as a mask for the explores and marks
        """
        if MODEL == "2dnn":
            input = 12
        if MODEL == "2dnnNEW":
            input = 11
        
        if torch.cuda.is_available():
            tiles = tiles.view((-1, input, 5, 5)).cuda()
        else:
            tiles = tiles.view((-1, input, 5, 5))

        probs = self.net.classifyTiles(tiles)
        return probs
    
    @profile
    def play(self, verbose=True):
        """
        Plays the game, returns stats about the game played
        """
        self.firstMove()
        if verbose:
            print(self.board)
        totalMoves = 1
        moveTimes = []

        while not self.board.isSolved():
            t0 = perf_counter()
            boardState = self.move()
            t1 = perf_counter()
            timeToMove = t1 - t0
            totalMoves += 1
            moveTimes += [timeToMove]

            if boardState:
                if verbose:
                    print(str(boardState))
            else:
                # Game is lost
                return {
                    "win": False,
                    "explored": self.getExploredProportion(),
                    "numMoves": totalMoves,
                    "moveTimes": moveTimes,
                }
        return {
            "win": True,
            "explored": 1,
            "numMoves": totalMoves,
            "moveTimes": moveTimes,
        }
Exemplo n.º 2
0
class BruteForceSolver:
    def __init__(self, rows, cols, bombs):
        # # TESTING
        # self.board = Board(rows, cols, bombs)
        # self.board.board = [[Tile(0, r, c) for c in range(cols)] for r in range(rows)]
        # for c in range(0, cols, 2):
        #     self.board.board[1][c] = Tile(BOMB, 1, c)

        # for r in range(cols):
        #     for c in range(rows):
        #         self.board.updateCounts(r, c)

        # for c in range(cols):
        #     self.board.explore(0, c)

        self.rows = rows
        self.cols = cols
        self.board = Board(rows, cols, bombs)
        self.unmarkedBombs = bombs

    def firstMove(self):
        # http://www.minesweeper.info/wiki/Strategy#First_Click
        self.board.explore(0, 0)

    def mark(self, row, col):
        """ Marks the tile at the row and column as a bomb and updates the remaining value of surrounding tiles """
        self.board.mark(row, col)
        self.unmarkedBombs -= 1
        tilesToReduce = self.getAllSurroundingTiles(Tile(0, row, col))
        for t in tilesToReduce:
            if t is not None and t.remainingValue != 0 and t.value != BOMB:
                self.board.board[t.row][t.col].remainingValue -= 1

    def move(self):
        """
        Performs the optimal move, if the move was successful returns the
        current board, else returns None to signify that a mine was triggered.
        The second return value is the actual number of moves made.
        """
        # print("Number of bombs left:", self.unmarkedBombs)
        tilesToConsider = self.getTilesAdjacentToExploredTiles()
        probabilityBoard = self.calculateProbabilities(tilesToConsider)
        completedMove = False  # Make all the certain moves at once
        numMoves = 0

        # Check if there is any certain bombs
        for i, row in enumerate(probabilityBoard):
            for j, pBomb in enumerate(row):
                if pBomb == 1 and not self.board.board[i][j].marked:
                    # print("MOVE: mark", i, j)
                    self.mark(i, j)
                    numMoves += 1
                    completedMove = True

        # Explore any certain safe spaces
        for i, row in enumerate(probabilityBoard):
            for j, pBomb in enumerate(row):
                if pBomb == 0:
                    # print("MOVE: explore1", i, j)
                    numMoves += 1
                    if self.board.explore(i, j):
                        return None, numMoves  # Game exploded
                    completedMove = True

        if completedMove:
            return self.board, numMoves

        # Explore the most likely safe space
        # TODO: speed me up scotty
        minimum = 1
        mini = -1
        minj = -1
        for i, row in enumerate(probabilityBoard):
            for j, pBomb in enumerate(row):
                if pBomb < minimum:
                    minimum = pBomb
                    mini = i
                    minj = j
        # print("MOVE: explore2", i, j)
        numMoves += 1
        if self.board.explore(mini, minj):
            return None, numMoves  # Game exploded

        return self.board, numMoves

    # @profile
    def calculateProbabilities(self, tilesToConsider):
        """ Calculates the probability of each tile being a bomb TODO: too long func """
        numUnexplored = self.countUnexploredTiles()
        noInfoTiles = numUnexplored - len(tilesToConsider)

        # print("Number of tiles to consider:", len(tilesToConsider))
        # print("Total unexplored tiles:", numUnexplored)
        # print("Tiles with no info:", noInfoTiles)

        possibleBombs = self.permuteBombsInTiles(tilesToConsider)
        exploredTiles = [
            tile for row in self.board.board for tile in row if tile.explored
        ]

        # Format: [permutation[tile and isBomb]]
        validBombs = [
            x for x in possibleBombs
            if self.isPermutationValid(x, exploredTiles, noInfoTiles)
        ]

        # Calculate probability of having that many bombs in tilesToConsider
        bombCounts = []  # bombCounts[i] = the number of bombs in validBombs[i]
        for v in validBombs:
            bombCounts.append(
                len([isBomb for isBomb in v if isBomb['isBomb'] == BOMB]))

        # The number of bomb permutations given bombs in tilesToConsider
        permutationsOfOtherTiles = [
            self.countPermutations(noInfoTiles, self.unmarkedBombs - bc)
            for bc in bombCounts
        ]
        # print("Permutations of other tiles", permutationsOfOtherTiles) # TODO: testme?

        # print("Number of bombs in each permutation:", bombCounts)

        isBombCount = [0] * len(tilesToConsider)
        for i, tile in enumerate(tilesToConsider):
            for j, permutation in enumerate(validBombs):
                for p in permutation:

                    # TESTING
                    # if i == 0:
                    #     print(p['tile'].row, p['tile'].col, p['isBomb'])

                    if p['tile'] == tile and p['isBomb']:
                        isBombCount[i] += permutationsOfOtherTiles[j]
                # TESTING:
                # if i == 0:
                #     print('-'*32)
        # print("Number of times this tile was a bomb:", isBombCount)
        if sum(permutationsOfOtherTiles) != 0:
            isBombProbability = [
                count / sum(permutationsOfOtherTiles) for count in isBombCount
            ]
        else:
            isBombProbability = [0] * len(isBombCount)
        # print("Probability of this tile being a bomb:", isBombProbability)

        if noInfoTiles != 0:
            if len(bombCounts) != 0:
                noInfoBombChance = (self.unmarkedBombs - sum(bombCounts) /
                                    len(bombCounts)) / noInfoTiles
            else:
                noInfoBombChance = self.unmarkedBombs / noInfoTiles
        else:
            noInfoBombChance = 0
        # print("Chance of having a bomb in no info tile:", noInfoBombChance)

        tilesAndProbability = zip(tilesToConsider, isBombProbability)

        # Build the probability board
        probabilityBoard = [[None for i in range(self.cols)]
                            for j in range(self.rows)]

        for tp in tilesAndProbability:
            probabilityBoard[tp[0].row][tp[0].col] = tp[1]

        for tile in exploredTiles:
            probabilityBoard[tile.row][
                tile.col] = 2  # don't click me, I', explored

        for i, row in enumerate(probabilityBoard):
            for j, tile in enumerate(row):
                if tile is None:
                    probabilityBoard[i][j] = noInfoBombChance
                if self.board.board[i][j].marked:
                    probabilityBoard[i][j] = 3  # don't click me, I'm marked

        # TESTING
        # print("\n Probability board:")
        # for q in probabilityBoard:
        #     for p in q:
        #         print("{0:.2f}".format(p), end=" ")
        #     print()

        return probabilityBoard

    def countPermutations(self, n, r):
        # HACK: r should never be > n but sometimes it is :(
        if n - r < 0:
            return 0
        else:
            return factorial(n) / (factorial(n - r))

    def isPermutationValid(self, permutation, explored, noInfoTiles):
        """ Returns true if the given permutation of bombs is valid given the explored tiles """
        # If the number of bombs for outside the permutation > noInfoTiles then invalid
        if self.unmarkedBombs + sum([p['isBomb']
                                     for p in permutation]) > noInfoTiles:
            return False

        for e in explored:
            adjacentBombs = [
                p for p in permutation
                if self.isAdjacent(e, p['tile']) and p['isBomb'] == BOMB
            ]
            # if len(adjacentBombs) > 0 and len(adjacentBombs) ==  e.value - 1:
            #     print("ERROR?:", len(adjacentBombs), e.row, e.col, e.value)
            if len(adjacentBombs) != e.remainingValue:
                return False

        return True

    def isAdjacent(self, a, b):
        """ Returns true if the given tiles are adjacent TODO: maybe smartify? """
        if a.row == b.row - 1:
            if a.col == b.col - 1 or a.col == b.col or a.col == b.col + 1:
                return True
            return False
        if a.row == b.row:
            if a.col == b.col - 1 or a.col == b.col + 1:
                return True
            return False
        if a.row == b.row + 1:
            if a.col == b.col - 1 or a.col == b.col or a.col == b.col + 1:
                return True
            return False

    # @profile
    def permuteBombsInTiles(self, tiles):
        """ 
        Set up all the possible (valid and invalid) permutations of bombs in the given tiles 
        Returns a list of dicts with {tile: $tile, isBomb: $boolean}
        """
        possiblePermutations = []
        # FIXME: this is also slow
        # for numBombs in range(min(self.unmarkedBombs + 1, len(tiles))):
        # bombArrangement = [BOMB] * numBombs + [0] * (len(tiles) - numBombs)
        # possiblePermutations += list(multiset_permutations(bombArrangement))
        possiblePermutations = [
            x for x in list(product([0, BOMB], repeat=len(tiles)))
            if sum(x) >= -1 * min(self.unmarkedBombs + 1, len(tiles))
        ]

        # Format: [bomb arrangement[isBomb]]
        possiblePermutations = np.array(possiblePermutations, dtype=int)

        tiles = np.array(list(tiles))  #.reshape(1, -1) #TESTING

        # FIXME: I'm STILL slow and I eat memory like popcorn

        # mapBombsToTiles = lambda bombs, tile: {"tile": tile, "isBomb": bombs}
        def mapBombsToTiles(perms, tiles):
            for perm in perms:
                mapped = [None] * len(tiles)
                for i in range(len(perm)):
                    mapped[i] = {"tile": tiles[i], "isBomb": perm[i]}
                yield mapped

        # mapBombsToTiles = np.vectorize(mapBombsToTiles)

        mappedPermutations = mapBombsToTiles(
            possiblePermutations,
            tiles)  #FIXME: this is where memory dies! Maybe generator it

        return mappedPermutations

    def getTilesAdjacentToExploredTiles(self):
        """ Returns all unique unexplored tiles that are next to an explored tile """
        exploredTiles = [
            tile for row in self.board.board for tile in row if tile.explored
        ]
        tilesToConsider = [
            self.getSurroundingTiles(tile) for tile in exploredTiles
        ]
        tilesToConsider = set(chain.from_iterable(tilesToConsider))

        # TESTING
        # for t in tilesToConsider:
        #     print("Considering: ", t.row, t.col)

        return tilesToConsider

    def getAllSurroundingTiles(self, tile):
        surrounding = []

        row = tile.row
        col = tile.col

        surrounding.append(self.board.left(row, col))
        surrounding.append(self.board.right(row, col))
        surrounding.append(self.board.up(row, col))
        surrounding.append(self.board.down(row, col))
        surrounding.append(self.board.upLeft(row, col))
        surrounding.append(self.board.upRight(row, col))
        surrounding.append(self.board.downLeft(row, col))
        surrounding.append(self.board.downRight(row, col))

        return surrounding

    def getSurroundingTiles(self, tile, explored=False):
        surrounding = self.getAllSurroundingTiles(tile)

        if not explored:
            filteredTiles = lambda x: not x.explored and not x.marked if x is not None else False
        else:
            filteredTiles = lambda x: x.explored if x is not None else False

        surrounding = list(filter(filteredTiles, surrounding))

        return surrounding

    def countUnexploredTiles(self):
        # TODO: speed me up scotty?
        count = 0
        for row in self.board.board:
            for tile in row:
                if not tile.explored and not tile.marked:
                    count += 1

        return count

    # @profile
    def play(self, verbose=True):
        # TODO: omg please test me, this must be 100% right ....Seems like it?
        """
        Plays the game, returns stats about the game played
        """
        self.firstMove()
        if verbose:
            print(self.board)
        totalMoves = 1
        moveTimes = []

        while not self.board.isSolved():
            t0 = perf_counter()
            boardState, numMoves = self.move()
            t1 = perf_counter()
            timeToMove = (t1 - t0) / numMoves
            totalMoves += numMoves
            moveTimes += [timeToMove] * numMoves

            if boardState:
                if verbose:
                    print(str(boardState))
            else:
                # Game is lost
                return {
                    "win":
                    False,
                    "explored":
                    1 - self.countUnexploredTiles() / (self.rows * self.cols),
                    "numMoves":
                    totalMoves,
                    "moveTimes":
                    moveTimes,
                }
        return {
            "win": True,
            "explored": 1,
            "numMoves": totalMoves,
            "moveTimes": moveTimes,
        }