def getAction(self, number: int) -> "Action Object": while self.__moveCount < 5: action = AI.Action(random.randrange(1, len(AI.Action))) x = random.randrange(self.__colDimension) y = random.randrange(self.__rowDimension) self.__moveCount += 1 return Action(action, x, y) action = AI.Action(random.randrange(len(AI.Action))) x = random.randrange(self.__colDimension) y = random.randrange(self.__rowDimension) return Action(action, x, y)
def __updateFirstMove(self) -> None: """ update the first move in KB""" c = self.startX r = self.startY self.unknown.remove((c, r)) # self.safeQ.append([c, r]) self.cLast = c self.rLast = r # update the 4 instances for this coordinates if necessary self.board[c][r].covered = False self.board[c][r].nubmer = 0 # update lastTile instance self.lastTile = self.board[c][r] self.lastAction = AI.Action(1)
def getAction(self, number: int) -> "Action Object": ######################################################################## # YOUR CODE BEGINS # ######################################################################## #Record the previous turn, update flag neighbors' effective labels self.moveCount += 1 self.recordTileLabel(number) #If out of moves OR we believe we won the game, LEAVE if self.moveCount >= self.moveCountMax: #print("---Leaving... because out of moves - 'AI lost?'---") return Action(AI.Action(0)) elif self.isBoardComplete(): #print("---Leaving... because the game board was completed - 'AI won?'---") return Action(AI.Action(0)) #print('\nResulting Board:') #self.printAIBoard() #Check every numbered tile for rule-of-thumb: "EffectiveLabel == NumOfUnmarkedNeighbors -> neighbors are bombs" #print('Numbered Tiles =', self.numberedTiles) for tile in self.numberedTiles: unmarkedNeighbors = set() x = tile[0] y = tile[1] neighbors = [(x, y + 1), (x - 1, y + 1), (x - 1, y), (x - 1, y - 1), (x, y - 1), (x + 1, y - 1), (x + 1, y), (x + 1, y + 1)] for n in neighbors: if self.isInBounds(n[0], n[1]): if self.board[n[0]][n[1]][0] == '?': unmarkedNeighbors.add(n) if self.board[x][y][1] == len(unmarkedNeighbors): for n2 in list(unmarkedNeighbors): if n2 not in self.flaggingQueue and n2 not in list( self.flags.keys()): self.flaggingQueue.append(n2) #Pop through frontier until coords are a valid tile to UNCOVER #print() while True: #print('iterating frontier... =', self.frontier) #Flagging Queue Case: Flag any guaranteed flags found by rule-of-thumb #print('\nFlagging Queue =', self.flaggingQueue) if len(self.flaggingQueue) != 0: #Update every non-zero and non-flag tile for "EffectiveLabel == NumOfCoveredNeighbors" #If true, all covered neighbors of tile are bombs. if len(self.flaggingQueue) > 0: while True: flag = self.flaggingQueue.pop(0) if flag not in list(self.flags.keys()): break #print('Flagging now:', flag) self.effectiveLabeling(flag[0], flag[1]) self.finishFunction(number, 2, flag[0], flag[1]) return Action(AI.Action(2), flag[0], flag[1]) if len(self.frontier) != 0: coords = self.frontier.pop(0) #Normal case: Found tile to uncover that is valid and not a bomb if coords not in self.uncovered and self.isInBounds( coords[0], coords[1]): #print('Breaking out of loop, found valid tile in frontier...') break #Flagging Case: Flag possible bomb using right-angle method elif len(self.frontier) == 0: if len(self.flags) != self.totalMines and len( self.numberedTiles) >= 3: #print('\nFlagging Corner bomb...') flag = None for i in self.numberedTiles: flag = self.flagBombs() if flag == None: continue else: break #If did not find valid flag, do random UNCOVER instead #print('Flag =', flag) if flag != None: if type(flag) == list: #Sometimes returns 2 flags, separate them here self.flaggingQueue.append(flag[0]) flag = flag[1] self.effectiveLabeling(flag[0], flag[1]) self.finishFunction(number, 2, flag[0], flag[1]) return Action(AI.Action(2), flag[0], flag[1]) else: #print('\nCorner and Effective Label Flags not found, trying random coords...') coords = self.chooseRandomTile() break #Cleanup Case: Bombs have been flagged, UNCOVER all remaining tiles that are not flagged elif len(self.flags) == self.totalMines: #print('\nAll bombs flagged, moving remaining tiles to frontier...') self.frontier = self.remaining self.remaining = [] coords = self.frontier.pop(0) break #Last Resort Case: Try random coords if no other options else: #print('\nEverything else failed, trying random coords...') coords = self.chooseRandomTile() break #print() self.finishFunction(number, 1, coords[0], coords[1]) return Action(AI.Action(1), coords[0], coords[1])
def getAction(self, number: int) -> "Action Object": """ input: an integer number as a perceptnumber from World.py act as a hint output: action number, rowCoordinate x, columnCoordinate y Total of 5 parts: I: Patient 0 - Uncover all the surrounding tiles if the center tile is 0 II: Basic 3x3 deductions - If the number of covered surrending tiles is qual to the percept number for the center tile, then the covered tiles are mines III: Multisquare algorithm - Looking at each single tile in the frontier (tiles that are unexplored by adjcent to a percept number), expand the deduction method from part II to multiple surrounding neighbors that have multural affecting tiles. IV: Linear algebra - Looking at the entier frontier and simplify it into a matrix. Use the uncovered neighbor tiles' percept number as constraints. Each constraint tells the max possible number of mines among its surrounding tiles. Also, use the entire board's unexplored Tiles and mines left as the final equation in the matrix. Solve or reduce the matrix into row echelon form. Since each tile can either be a mine or not, further deduction can be made. V: Guesses - Assign each unexplored tiles a probability based on the current knowledge base (number of minesleft and surrouding constraints) One exception - guess corners first since the probability that a corner being a mines is low. """ # in the beginning, update KB with the received percept number self.logLastMovePercept(number) """ Part I - Patient 0 """ # if the number is 0, then the last tile's surrounding is safe if (number == 0): """ if in bounds, not the last move, not already in safeQ, and covered""" for col in range(self.cLast - 1, self.cLast + 2): for row in range(self.rLast - 1, self.rLast + 2): if (self.__isInBounds(col, row) and \ not (col == self.cLast and row == self.rLast)) and \ ((col, row) not in self.safeQ) and \ self.board[col][row].covered == True: # add to the safeQ self.safeQ.append((col, row)) # uncover all the safe ones while (self.safeQ != deque([])): # get the left most tile in the safe queue cr = self.safeQ.popleft() # update the knowledge base self.logMove(AI.Action(1), cr[0], cr[1]) # return the action to the World return Action(AI.Action(1), cr[0], cr[1]) """ Part II - Basic 3x3 deductions """ # locate the mine for col in range(0, self.col): for row in range(0, self.row): """ If the total number of covered surrending the tile is the percept number for the tile, then the covered tile is the mine """ if (self.board[col][row].covered == False and \ self.board[col][row].number != 0 and \ self.board[col][row].number == \ self.surCovered(col, row)[0]): # flag those as mines mines = self.surCovered(col, row)[1] for _ in mines: self.markMines(_) for col in range(0, self.col): for row in range(0, self.row): # percept == known mines # surrounding unexplored - known mines > 0 if ((self.board[col][row].number == \ self.surMines(col, row)[0]) and \ (self.surCovered(col, row)[0] - \ self.surMines(col, row)[0] > 0)): # get the unexplored tiles and known mines covered = self.surCovered(col, row)[1] mines = self.surMines(col, row)[1] for _ in covered: # if the mines are all discovered, the rest of the tiles # must be safe if (_ not in mines) and (_ not in self.safeQ): self.safeQ.append(_) # uncover all the safe ones while (self.safeQ != deque([])): # get the left most tile in the safe queue cr = self.safeQ.popleft() # update the knowledge base self.logMove(AI.Action(1), cr[0], cr[1]) # return the action to the World return Action(AI.Action(1), cr[0], cr[1]) """ Part III: neighbor_test (multisquare algorithm) """ for col in range(self.col): for row in range(self.row): if self.board[col][row].number > 0 and \ self.surUnknown(col, row)[0] > 0: neigh = self.neighbor_test(col, row) if neigh is not None and neigh != []: for _ in neigh: if _ in self.unknown and _ not in self.safeQ: self.safeQ.append(_) # uncover all the safe ones while (self.safeQ != deque([])): # get the left most tile in the safe queue cr = self.safeQ.popleft() # update the knowledge base self.logMove(AI.Action(1), cr[0], cr[1]) # return the action to the World return Action(AI.Action(1), cr[0], cr[1]) """ Part IV: linear algebra """ # initialize contraints, frontier, and frontier encoding for the matrix constraints = [] frontier = [] frontierMap = dict() unknown = self.unknown totalMinesLeft = self.minesLeft # get the current contraints constraints = self.constraints() constraintsCount = len(constraints) # get the current frontier frontier = self.frontier() frontierCount = len(frontier) # each row is a contraint # the additional constraint is the entire unexplored tiles rowCount = constraintsCount + 1 # each column is an explored tile on the board columnCount = len(unknown) + 1 # if there are constraints and the variables, construct the matrix if columnCount != 1 and rowCount != 1: # create a list of column code for each variable tile plus the # general rule (unexplored tiles and total mines left) columnHeader = [x for x in range(columnCount)] frontierHeader = columnHeader[:-1] # encode each tile into a dictionary col_to_tile = dict(zip(frontierHeader, unknown)) tile_to_col = dict(zip(unknown, frontierHeader)) # initialize the matrix matrix = [[0 for i in range(columnCount)] for j in range(rowCount)] # construct the matrix row = 0 for constraint in constraints: # list out the tiles to be explored for each constraint sub_frontier = self.surUnknown(constraint[0], constraint[1])[1] # mark the coordinates for tile in sub_frontier: # encode each column into tile coordinates col = tile_to_col.get(tile) # update the matrix coordinate value matrix[row][col] = 1 # update the last column with the actual number of mines # which is (percept - number of mines already discovered) minesCount = self.board[constraint[0]][constraint[1]].number - \ self.surMines(constraint[0], constraint[1])[0] # update the last number as the effective percept number matrix[row][-1] = minesCount # move on to the next row row += 1 # update the last row as the general rule for i in range(columnCount): matrix[row][i] = 1 matrix[-1][-1] = totalMinesLeft # reduce to row echelon form, where the magic happens self.reduceRowEchelon(matrix) """ Since each tile is either a mine or not, its value is binary. this is useful to solve the matrix or at least some tile's value. """ safe = [] mines = [] for row in matrix: last = row[-1] onesCount = self.countMix(row[:-1])[0] onesList = self.countMix(row[:-1])[1] neg_onesCount = self.countMix(row[:-1])[2] negList = self.countMix(row[:-1])[3] # case when the total number of mines is 0 if last == 0: # case when there are only possitive coefficients on the left if onesCount > 0 and neg_onesCount == 0: for col in onesList: tile = col_to_tile.get(col) # they are safe if tile not in safe: safe.append(tile) # case when there are only negative coefficients on the left if neg_onesCount > 0 and onesCount == 0: for col in negList: tile = col_to_tile.get(col) # they are mines if tile not in mines: mines.append(tile) # case when the total number of mines is possitive if last > 0: # ignore the negative coefficients if onesCount == last: for col in onesList: tile = col_to_tile.get(col) if tile not in safe: mines.append(tile) for col in negList: tile = col_to_tile.get(col) if tile not in mines: safe.append(tile) # case when the total number of mines is negative if last < 0: # ignore the possitive coefficients if neg_onesCount == last: for col in onesList: tile = col_to_tile.get(col) if tile not in safe: safe.append(tile) for col in negList: tile = col_to_tile.get(col) if tile not in mines: mines.append(tile) # update the knowledge base if mines != []: for _ in mines: self.markMines(_) # update the knowledge base and append to the safe queue if safe != []: for _ in safe: if _ in self.unknown and _ not in self.safeQ: self.safeQ.append(_) # uncover all the safe ones while (self.safeQ != deque([])): # get the left most tile in the safe queue cr = self.safeQ.popleft() # update the knowledge base self.logMove(AI.Action(1), cr[0], cr[1]) # return the action to the World return Action(AI.Action(1), cr[0], cr[1]) """ Part V: Guesses """ # assign heuristics to each tile: number of mines / number of unexplored if self.unknown != []: keys = self.unknown values = [self.minesLeft / len(self.unknown)] * len(self.unknown) self.prob = dict(zip(keys, values)) for col in range(0, self.col): for row in range(0, self.row): percept = self.board[col][row].number num_mines = self.surMines(col, row)[0] num_covered = self.surCovered(col, row)[0] if ((percept > 0) and \ (num_covered - num_mines > 0)): mines = self.surMines(col, row)[1] covered = self.surCovered(col, row)[1] for _ in covered: if (_ not in mines) and (_ not in self.safeQ): # only get the maximum probability of being a mine self.prob[_] = max( ( percept-num_mines ) / \ num_covered,\ self.prob[_]) # get the corners first corners = [(self.col-1, self.row-1), \ (0, 0), \ (self.col-1, 0), \ (0, self.row-1)] for _ in corners: if _ in self.unknown: self.prob[_] = self.prob[_] - 1 if (self.unknown != []): # only uncover the least possible mines minList = self.minList(self.prob) self.safeQ.append(random.choice(minList)) if (self.minesLeft == 0): return Action(AI.Action(0)) # uncover all the safe ones while (self.safeQ != deque([])): # get the left most tile in the safe queue cr = self.safeQ.popleft() # update the knowledge base self.logMove(AI.Action(1), cr[0], cr[1]) # return the action to the World return Action(AI.Action(1), cr[0], cr[1]) if (self.minesLeft == 0): return Action(AI.Action(0))
def logLastMovePercept(self, number): """ log the feedback percept number from the world """ if self.lastAction == AI.Action(1): self.lastTile.covered = False self.lastTile.number = number