Minesweeper AI labelling mines as safe spots - python

Background:
I have been working on the Minesweeper ai project for the HarvardX CS50AI online course for a few days. The goal is to implement AI for the minesweeper game. The problem set can be accessed here: https://cs50.harvard.edu/ai/2020/projects/1/minesweeper/
Implementation:
My task is to implement two classes, MinesweeperAI and the Sentence. Sentence class is a logical statement about a Minesweeper game that consists of a set of board cells and a count of the number of those cells which are mines. MinesweeperAI class is a main handler of AI.
Issue:
Although the program is running without any errors, the AI is making bad decisions, and thus, it is unable to complete the Minesweeper game successfully. From my observations, the AI is labelling potential mines as a safe space and thus, making suicidal runes.
Debugging
I have tried classical debugging, printing, even talking to myself about the code. For some reason, the AI is labelling statements that are mines as safe spaces - I can not detect the reason behind it. I have documented the code with comments, and I can not see any breakdown in implemented logic. However, there must be one - I am inserting the code below with some additional materials.
Sentence class, the logical representation of in-game knowledge:
class Sentence():
"""
Logical statement about a Minesweeper game
A sentence consists of a set of board cells,
and a count of the number of those cells which are mines.
"""
def __init__(self, cells, count):
self.cells = set(cells)
self.count = count
def __eq__(self, other):
return self.cells == other.cells and self.count == other.count
def __str__(self):
return f"{self.cells} = {self.count}"
def known_mines(self):
"""
Returns the set of all cells in self.cells known to be mines.
"""
# Because we are eliminating safe cells from the the statement, we are looking for statements
# that would contain number of cells that is equal (or smaller) than number of mines.
# Upon fulfilment of such condition, evaluated cells are known to be mines.
if len(self.cells) <= self.count:
return self.cells
else:
return None
def known_safes(self):
"""
Returns the set of all cells in self.cells known to be safe.
"""
# There is only one case when the cells are known to be "safes" - when the number of count is 0.
if self.count == 0:
return self.cells
else:
return None
def mark_mine(self, cell):
"""
Updates internal knowledge representation given the fact that
a cell is known to be a mine.
"""
# Marking mine implies two logical consequences:
# a) the number of counts must decrease by one (n - 1);
# b) the cell marked as mine must be discarded from the sentence (we keep track,
# only of the cells that are still unknown to be mines or "safes".
if cell in self.cells:
self.cells.discard(cell)
self.count -= 1
if self.count < 0: # this is a safeguard from any improper inference set forth.
self.count = 0
else:
pass
def mark_safe(self, cell):
"""
Updates internal knowledge representation given the fact that
a cell is known to be safe.
"""
# Marking "safe" implies one logical consequence:
# a) the cell marked as safe must be discarded from the sentence.
if cell in self.cells:
self.cells.discard(cell)
else:
pass
MinesweeperAI class, the primary AI module:
class MinesweeperAI():
"""
Minesweeper game player
"""
def __init__(self, height=8, width=8):
# Set initial height and width
self.height = height
self.width = width
# Keep track of which cells have been clicked on
self.moves_made = set()
# Keep track of cells known to be safe or mines
self.mines = set()
self.safes = set()
# List of sentences about the game known to be true
self.knowledge = []
def mark_mine(self, cell):
"""
Marks a cell as a mine, and updates all knowledge
to mark that cell as a mine as well.
"""
self.mines.add(cell)
for sentence in self.knowledge:
sentence.mark_mine(cell)
def mark_safe(self, cell):
"""
Marks a cell as safe, and updates all knowledge
to mark that cell as safe as well.
"""
self.safes.add(cell)
for sentence in self.knowledge:
sentence.mark_safe(cell)
def add_knowledge(self, cell, count):
"""
Called when the Minesweeper board tells us, for a given
safe cell, how many neighboring cells have mines in them.
This function should:
1) mark the cell as a move that has been made
2) mark the cell as safe
3) add a new sentence to the AI's knowledge base
based on the value of `cell` and `count`
4) mark any additional cells as safe or as mines
if it can be concluded based on the AI's knowledge base
5) add any new sentences to the AI's knowledge base
if they can be inferred from existing knowledge
"""
# 1) mark the cell as a move that has been made.
self.moves_made.add(cell)
# 2) mark the cell as safe. By this we are also updating our internal knowledge base.
self.mark_safe(cell)
# 3) add a new sentence to the AI's knowledge base based on the value of `cell` and `count`
sentence_prep = set()
# Sentence must include all the adjacent tiles, but do not include:
# a) the revealed cell itself;
# b) the cells that are known to be mines;
# c) the cell that are known to be safe.
for i in range(cell[0] - 1, cell[0] + 2):
for j in range(cell[1] - 1, cell[1] + 2): # Those two cover all the adjacent tiles.
if (i, j) != cell:
if (i, j) not in self.moves_made and (i, j) not in self.mines and (i, j) not in self.safes:
if 0 <= i < self.height and 0 <= j < self.width: # The cell must be within the game frame.
sentence_prep.add((i, j))
new_knowledge = Sentence(sentence_prep, count) # Adding newly formed knowledge to the KB.
self.knowledge.append(new_knowledge)
# 4) mark any additional cells as safe or as mines,
# if it can be concluded based on the AI's knowledge base
# 5) add any new sentences to the AI's knowledge base
# if they can be inferred from existing knowledge.
while True: # iterating knowledge base in search for new conclusions on safes or mines.
amended = False # flag indicates that we have made changes to the knowledge, new run required.
knowledge_copy = copy.deepcopy(self.knowledge) # creating copy of the database.
for sentence in knowledge_copy: # cleaning empty sets from the database.
if len(sentence.cells) == 0:
self.knowledge.remove(sentence)
knowledge_copy = copy.deepcopy(self.knowledge) # creating copy once again, without empty sets().
for sentence in knowledge_copy:
mines_check = sentence.known_mines() # this should return: a set of mines that are known mines or None.
safes_check = sentence.known_safes() # this should return: a set of safes that are known safes or None
if mines_check is not None:
for cell in mines_check:
self.mark_mine(cell) # marking cell as a mine, and updating internal knowledge.
amended = True # raising flag.
if safes_check is not None:
for cell in safes_check:
self.mark_safe(cell) # marking cell as a safe, and updating internal knowledge.
amended = True # raising flag.
# the algorithm should infer new knowledge,
# basing on reasoning: (A.cells - B.cells) = (A.count - B.count), if
# B is the subset of A.
knowledge_copy = copy.deepcopy(self.knowledge) # creating copy once again, updated checks.
for sentence_one in knowledge_copy:
for sentence_two in knowledge_copy:
if len(sentence_one.cells) != 0 and len(sentence_two.cells) != 0: # In case of the empty set
if sentence_one.cells != sentence_two.cells: # Comparing sentences (if not the same).
if sentence_one.cells.issubset(sentence_two.cells): # If sentence one is subset of sen_two.
new_set = sentence_two.cells.difference(sentence_one.cells)
if len(new_set) != 0: # if new set is not empty (in case of bug).
new_counts = sentence_two.count - sentence_one.count
if new_counts >= 0: # if the counts are equal or bigger than 0 (in case of bug).
new_sentence = Sentence(new_set, new_counts)
if new_sentence not in self.knowledge: # if the sentence is not already in
# the KB.
self.knowledge.append(new_sentence)
amended = True # raising flag.
if not amended:
break # If the run resulted in no amendments, then we can not make any additional amendments,
# to our KB.
def make_safe_move(self):
"""
Returns a safe cell to choose on the Minesweeper board.
The move must be known to be safe, and not already a move
that has been made.
This function may use the knowledge in self.mines, self.safes
and self.moves_made, but should not modify any of those values.
"""
for cell in self.safes:
if cell not in self.moves_made:
return cell
return None
def make_random_move(self):
"""
Returns a move to make on the Minesweeper board.
Should choose randomly among cells that:
1) have not already been chosen, and
2) are not known to be mines
"""
for i in range(self.height):
for j in range(self.width):
cell = (i, j)
if cell not in self.moves_made and cell not in self.mines:
return cell
return None
Documentation of the issue:
Documentation of the issue - the AI is making a safe move that it should now have labelled as the safe
Some comments:
Generally speaking, the cell is known to be safe when the sentence.count is zero (it means, that all the cells in the sentence are known to be "safes"). On the other hand, the cell is known as a mine, if the (len) of cells is equal to the sentence.count. The logic behind it is rather straightforward, still, I am missing something big when it comes to the implementation.
Thank you for all your help. Please do not be too harsh on my code - I am still learning, and to be honest, it's the first time when I am struggling hard with a piece of code that I have prepared. It's giving me little rest because I just can not crack down on what I am doing wrong. If there is something that I could provide (any more additional data) - please, just let me know!

Ok, after a lot debugging I found the root of the issue: When new knowledge is added via add_knowledge, the AI does only half account for cells it knows to be mines: It does not added those to the new Sentence, but one also needs to reduce the count by one for each already known cell:
for i in range(cell[0] - 1, cell[0] + 2):
for j in range(cell[1] - 1, cell[1] + 2): # Those two cover all the adjacent tiles.
if (i, j) != cell:
if (i, j) not in self.moves_made and (i, j) not in self.mines and (i, j) not in self.safes:
if 0 <= i < self.height and 0 <= j < self.width: # The cell must be within the game frame.
sentence_prep.add((i, j))
elif (i, j) in self.mines: # One of the neighbors is a known mine. Reduce the count.
count -= 1
new_knowledge = Sentence(sentence_prep, count) # Adding newly formed knowledge to the KB.
self.knowledge.append(new_knowledge)
This should now work (Unless there is another edge case somewhere)
Here a bit about my journey. I wrote these Tools to help with debugging:
def get_neighbours(size, x, y):
for i in range(x - 1, x + 2):
for j in range(y - 1, y + 2): # Those two cover all the adjacent tiles.
if (i, j) != (x, y):
if 0 <= i < size[0] and 0 <= j < size[1]:
yield i, j
class SimpleBoard:
def __init__(self, size, grid):
self.size = size
self.grid = grid
self.calc()
def calc(self):
for x in range(self.size[0]):
for y in range(self.size[1]):
if self.grid[x][y] != 9:
self.grid[x][y] = sum(1 for i, j in get_neighbours(self.size, x, y) if self.grid[i][j] == 9)
#classmethod
def random(cls, size, count):
self = cls(size, [[0] * size[1] for _ in range(size[0])])
options = list(product(range(size[0]), range(size[1])))
shuffle(options)
mines = options[:count]
for x, y in mines:
self.grid[x][y] = 9
self.calc()
return self
def build_ai_view(ai: MinesweeperAI, board: SimpleBoard):
out = []
for x in range(ai.height):
out.append(l :=[])
for y in range(ai.width):
cell = x,y
if cell in ai.mines:
assert cell not in ai.safes
l.append("X" if board.grid[x][y] == 9 else "%")
elif cell in ai.safes:
l.append(str(board.grid[x][y]) if cell in ai.moves_made else "_")
else:
l.append("?")
cells_to_sentence = defaultdict(list)
for i, sentence in enumerate(ai.knowledge):
for c in sentence.cells:
cells_to_sentence[c].append(sentence)
unique_groups = []
for c, ss in cells_to_sentence.items():
if ss not in unique_groups:
unique_groups.append(ss)
labels = "abcdefghijklmnopqrstuvxyz"
for (x, y), ss in cells_to_sentence.items():
i = unique_groups.index(ss)
l = labels[i]
assert out[x][y] == "?"
out[x][y] = l
for i, ss in enumerate(unique_groups):
out.append(l := [labels[i]])
if len(ss) > 1:
l.append("overlap of")
for s in ss:
if [s] not in unique_groups:
unique_groups.append([s])
l.append(labels[unique_groups.index([s])])
# l.extend(labels[unique_groups.index([s])] for s in ss)
else:
l.append(str(ss[0].count))
out.append([repr(ai)])
return "\n".join(map(str, out))
They might not be pretty code, but they work and display all relevant information from the perspective of the AI. I then used this together with the given failing case:
board = SimpleBoard((8, 8), [
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 9, 0, 0, 0, 9, 0, 0],
[0, 0, 0, 9, 0, 0, 0, 0],
[0, 0, 0, 9, 0, 0, 0, 0],
[0, 9, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 9, 0, 9, 0, 9, 0, 0],
])
and this simple loop:
pprint(board.grid)
start = next((x, y) for x in range(board.size[0]) for y in range(board.size[1]) if board.grid[x][y] == 0)
ai = MinesweeperAI(*board.size)
ai.add_knowledge(start, 0)
print(build_ai_view(ai, board))
while True:
target = ai.make_safe_move()
print(target)
x, y = target
if board.grid[x][y] == 9:
print("FOUND MINE", x, y)
break
else:
ai.add_knowledge((x, y), board.grid[x][y])
print(build_ai_view(ai, board))
to be able to backwards figure out at which point the AI starts to make false assumptions.
This came in multiple steps: figure out when the first % (e.g. wrongly marked mine) appears, figure out which Sentences lead to that conclusion, figure out which of those is wrong and finally figure out why that assumption is made.

Related

Why does utility computation in is_terminal include the # of blanks left in a terminal state? What does mean with performance measure for the agent?

Consider:
import numpy as np
def actions(state):
# Returns the indices of all blank spaces on the board (represented by '.')
return [i for i,s in np.ndenumerate(state) if s=='.']
def result(state, action, player):
# Returns a new state with the 'action' space taken by 'player'
new_state = state.copy() # Don't modify the passed-in array!
new_state[action] = player
return new_state
def is_terminal(state, k):
# Test whether 'state' is a terminal state or not
# Also return the final game score if yes
num_blanks = np.count_nonzero(state=='.')
# If X has k-in-a-row from any position, this is a terminal state
X_indices = [i for i,s in np.ndenumerate(state) if s=='X']
if has_k_in_a_row(X_indices, k):
return True, 1+num_blanks
# If O has k-in-a-row from any position, this is a terminal state
O_indices = [i for i,s in np.ndenumerate(state) if s=='O']
if has_k_in_a_row(O_indices, k):
return True, -(1+num_blanks)
# If there are no blanks left, the game ends with a tie
if num_blanks == 0:
return True, 0
# Otherwise, the game is not over
return False, None
#--------------------------------------------
# Helper functions used by is_terminal() (above)
def has_k_in_a_row(indices, k):
# Test whether there are k consecutive indices in a row in the given list of indices
# Get the indices as a set, for efficient subset testing
index_set = set(indices)
# For each starting position...
for start_pos in indices:
# Determine the length-k sequence of indices (starting at the current position)
# in each of four possible directions
winning_sequences = sequences(start_pos, k)
# If we have any of these sequences covered, we have 'k in a row'
if any([seq.issubset(index_set) for seq in winning_sequences]):
return True
# If we get here, we don't have 'k in a row'
return False
def sequences(start_pos, k):
# Return the 4 sets of k indices 'in a row' starting from index start_pos
# A win can be down, across, diagonally down, or diagonally up
across = set([(start_pos[0], start_pos[1]+j) for j in range(k)])
down = set([(start_pos[0]+j, start_pos[1]) for j in range(k)])
diagdown = set([(start_pos[0]+j, start_pos[1]+j) for j in range(k)])
diagup = set([(start_pos[0]+j, start_pos[1]-j) for j in range(k)])
# Return all 4 sets of indices
return across, down, diagdown, diagup
Review the code above, and make sure you understand it, especially actions, result, and is_terminal.
(Note: we could have gone with the standard ±1 utility for an X win/loss, but the extra "bonus" provides some useful information.)

How do I find shortest path in maze with BFS?

I am trying to find a way to solve a maze. My teacher said I have to use BFS as a way to learn. So I made the algorithm itself, but I don't understand how to get the shortest path out of it. I have looked at others their codes and they said that backtracking is the way to do it. How does this backtracking work and what do you backtrack?
I will give my code just because I like some feedback to it and maybe I made some mistake:
def main(self, r, c):
running = True
self.queue.append((r, c))
while running:
if len(self.queue) > 0:
self.current = self.queue[0]
if self.maze[self.current[0] - 1][self.current[1]] == ' ' and not (self.current[0] - 1, self.current[1])\
in self.visited and not (self.current[0] - 1, self.current[1]) in self.queue:
self.queue.append((self.current[0] - 1, self.current[1]))
elif self.maze[self.current[0] - 1][self.current[1]] == 'G':
return self.path
if self.maze[self.current[0]][self.current[1] + 1] == ' ' and not (self.current[0], self.current[1] + 1) in self.visited\
and not (self.current[0], self.current[1] + 1) in self.queue:
self.queue.append((self.current[0], self.current[1] + 1))
elif self.maze[self.current[0]][self.current[1] + 1] == 'G':
return self.path
if self.maze[self.current[0] + 1][self.current[1]] == ' ' and not (self.current[0] + 1, self.current[1]) in self.visited\
and not (self.current[0] + 1, self.current[1]) in self.queue:
self.queue.append((self.current[0] + 1, self.current[1]))
elif self.maze[self.current[0] + 1][self.current[1]] == 'G':
return self.path
if self.maze[self.current[0]][self.current[1] - 1] == ' ' and not (self.current[0], self.current[1] - 1) in self.visited\
and not (self.current[0], self.current[1] - 1) in self.queue:
self.queue.append((self.current[0], self.current[1] - 1))
elif self.maze[self.current[0]][self.current[1] - 1] == 'G':
return self.path
self.visited.append((self.current[0], self.current[1]))
del self.queue[0]
self.path.append(self.queue[0])
As maze I use something like this:
############
# S #
##### ######
# #
######## ###
# #
## ##### ###
# G#
############
Which is stored in a matrix
What I eventually want is just the shortest path inside a list as output.
Since this is a coding assignment I'll leave the code to you and simply explain the general algorithm here.
You have a n by m grid. I am assuming this is is provided to you. You can store this in a two dimensional array.
Step 1) Create a new two dimensional array the same size as the grid and populate each entry with an invalid coordinate (up to you, maybe use None or another value you can use to indicate that a path to that coordinate has not yet been discovered). I will refer to this two dimensional array as your path matrix and the maze as your grid.
Step 2) Enqueue the starting coordinate and update the path matrix at that position (for example, update matrix[1,1] if coordinate (1,1) is your starting position).
Step 3) If not at the final coordinate, dequeue an element from the queue. For each possible direction from the dequeued coordinate, check if it is valid (no walls AND the coordinate does not exist in the matrix yet), and enqueue all valid coordinates.
Step 4) Repeat Step 3.
If there is a path to your final coordinate, you will not only find it with this algorithm but it will also be a shortest path. To backtrack, check your matrix at the location of your final coordinate. This should lead you to another coordinate. Continue this process and backtrack until you arrive at the starting coordinate. If you store this list of backtracked coordinates then you will have a path in reverse.
The main problem in your code is this line:
self.path.append(self.queue[0])
This will just keep adding to the path while you go in all possible directions in a BFS way. This path will end up getting all coordinates that you visit, which is not really a "path", because with BFS you continually switch to a different branch in the search, and so you end up collecting positions that are quite unrelated.
You need to build the path in a different way. A memory efficient way of doing this is to track where you come from when visiting a node. You can use the visited variable for that, but then make it a dictionary, which for each r,c pair stores the r,c pair from which the cell was visited. It is like building a linked list. From each newly visited cell you'll be able to find back where you came from, all the way back to the starting cell. So when you find the target, you can build the path from this linked list.
Some other less important problems in your code:
You don't check whether a coordinate is valid. If the grid is bounded completely by # characters, this is not really a problem, but if you would have a gap at the border, you'd get an exception
There is code repetition for each of the four directions. Try to avoid such repetition, and store recurrent expressions like self.current[1] - 1 in a variable, and create a loop over the four possible directions.
The variable running makes no sense: it never becomes False. Instead make your loop condition what currently is your next if condition. As long as the queue is not empty, continue. If the queue becomes empty then that means there is no path to the target.
You store every bit of information in self properties. You should only do that for information that is still relevant after the search. I would instead just create local variables for queue, visited, current, ...etc.
Here is how the code could look:
class Maze():
def __init__(self, str):
self.maze = str.splitlines()
def get_start(self):
row = next(i for i, line in enumerate(self.maze) if "S" in line)
col = self.maze[row].index("S")
return row, col
def main(self, r, c):
queue = [] # use a local variable, not a member
visited = {} # use a dict, key = coordinate-tuples, value = previous location
visited[(r, c)] = (-1, -1)
queue.append((r, c))
while len(queue) > 0: # don't use running as variable
# no need to use current; just reuse r and c:
r, c = queue.pop(0) # you can remove immediately from queue
if self.maze[r][c] == 'G':
# build path from walking backwards through the visited information
path = []
while r != -1:
path.append((r, c))
r, c = visited[(r, c)]
path.reverse()
return path
# avoid repetition of code: make a loop
for dx, dy in ((-1, 0), (0, -1), (1, 0), (0, 1)):
new_r = r + dy
new_c = c + dx
if (0 <= new_r < len(self.maze) and
0 <= new_c < len(self.maze[0]) and
not (new_r, new_c) in visited and
self.maze[new_r][new_c] != '#'):
visited[(new_r, new_c)] = (r, c)
queue.append((new_r, new_c))
maze = Maze("""############
# S #
##### ######
# #
######## ###
# #
## ##### ###
# G#
############""")
path = maze.main(*maze.get_start())
print(path)
See it run on repl.it

Creating an algorithm to solve a problem in python

Algorithm Objective:
link to the pictures i took while giving the amazon interview:
[https://boards.wetransfer.com/board/shl7w5z1e62os7nwv20190618224258/latest][pictures]
Eight houses, represented as cells, are arranged in a straight line. Each day every cell competes with its adjacent cells(neighbors). An integer value of 1 represents an active cell and a value of 0 represents an inactive cell. If the neighbors on both sides of a cell are either active or inactive, the cell becomes inactive on the next day, otherwise the cell becomes active. The two cell on each end have a single a single adjacent cell, so assume that the unoccupied space on the opposite side is an inactive cell. Even after updating the cell state, consider its previous state when updating the state of other cells. The state information of all cells should be updated simultaneously.
Create an algorithm to output the state of the cells after the given number of days.
Input:
The input to the function/method consists of two arguments:
states, a list of integers representing the current state of cells,
days,an integer representing the number of days.
Output:
Return a list of integers representing the state of the cells after the given number of days
Note:
The elements of the list states contains 0s and 1s only
TestCase 1:
Input: [1,0,0,0,0,1,0,0] , 1
Expected Return Value: 0 1 0 0 1 0 1 0
TestCase 2:
Input: [1,1,1,0,1,1,1,1] , 2
Expected Return Value: 0 0 0 0 0 1 1 0
What I Tried:
def cellCompete(states, days):
# WRITE YOUR CODE HERE
il = 0;
tl = len(states);
intialvalue = states
results = []
states = []
for i in range(days):
#first range
if(intialvalue[il] != intialvalue[il+1]):
print('value of index 0 is : ',reverse(intialvalue[il]))
results.append(reverse(intialvalue[il]))
else:
print('value of index 0 is :', intialvalue[il])
results.append(intialvalue[il])
print("-------------------")
#range middle
while il < tl-2:
if(intialvalue[il] != intialvalue[il+1] or intialvalue[il+1] != intialvalue[il+2]):
print('value of index',il+1,'is : ',reverse(intialvalue[il+1]))
results.append(reverse(intialvalue[il+1]))
else:
print('value of index', il+1,'is :', intialvalue[il+1])
results.append(intialvalue[il+1])
print("-------------------")
il += 1
#range last
if(intialvalue[tl-2] != intialvalue[tl-1]):
print('value of index',tl-1,'is : ',reverse(intialvalue[tl-1]))
results.append(reverse(intialvalue[tl-1]))
else:
print('value of index',tl-1,'is :', intialvalue[tl-1])
results.append(intialvalue[tl-1])
print("-------------------")
print('Input: ',intialvalue)
print('Results: ',results)
initialvalue = results
def reverse(val):
if(val == 0):
return 1
elif(val == 1):
return 0
print("-------------------------Test case 1--------------------")
cellCompete([1,0,0,0,0,1,0,0],1)
print("-------------------------Test case 2--------------------")
cellCompete([1,1,1,0,1,1,1,1],2)
I am relatively new to python and i could not complete this algorithm for the second case on this python
Here is a much shorter routine that solves your problem.
def cellCompete(states, days):
n = len(states)
for day in range(days):
houses = [0] + states + [0]
states = [houses[i-1] ^ houses[i+1] for i in range(1, n+1)]
return states
print(cellCompete([1,0,0,0,0,1,0,0] , 1))
print(cellCompete([1,1,1,0,1,1,1,1] , 2))
The printout from that is what you want (though with list brackets included):
[0, 1, 0, 0, 1, 0, 1, 0]
[0, 0, 0, 0, 0, 1, 1, 0]
This routine adds sentinel zeros to each end of the list of house states. It then uses a list comprehension to find the houses' new states. All this is repeated the proper number of times before the house states are returned.
The calculation of a new house state is houses[i-1] ^ houses[i+1]. That character ^ is bitwise exclusive-or. The value is 1 if the two values are different and 0 if the two values are the same. That is just what is needed in your problem.
Recursive version:
def cell_compete(states, days):
s = [0] + states + [0]
states = [i ^ j for i, j in zip(s[:-2], s[2:])] # Thanks #RoyDaulton
return cell_compete(states, days - 1) if days > 1 else states
A non-recursive version that also avoids extending the list by adding edge [0] elements would be:
def cell_compete(states, days):
for _ in range(days):
states = [states[1]] + [i ^ j for i, j in zip(states[:-2], states[2:])] + [states[-2]]
return states
Another possibility:
def cellCompete(states,days):
newstates = []
added_states = [0] + states + [0]
for counter,value in enumerate(states):
newstates.append(int((added_states[counter] != added_states[counter+2])))
if days > 1:
return cellCompete(newstates,days-1)
else:
return newstates
print(cellCompete([1,1,1,0,1,1,1,1],2))
Similar to Rory's using XOR but without the need for the internal comprehension. Bit shift the number by 2 and clip the extra bit from the left by taking the modulus:
def process(state, r):
n = int(''.join(map(str,state)), 2)
for i in range(r):
n = ((n ^ n << 2) >> 1) % 256
return list(map(int,format(n, "08b")))
process([1,1,1,0,1,1,1,1], 2)
# [0, 0, 0, 0, 0, 1, 1, 0]
process([1,0,0,0,0,1,0,0] , 1)
# [0, 1, 0, 0, 1, 0, 1, 0]
While everyone is trying to make the simplest version possible here's a more complex version. It's pretty similar to the previous answers except that instead of keeping the state in the function, this solution is formed of 2 two part. One is the utility function that we want to be able to call, the other is a generator that keep tracks of the states.
The main difference here is that the generator takes a comparator and an initial state that will be mutated. The generator can also be sent as a parameter so the generator can help divide the logic of how many state you want to generate and to have a way to mutate from an actual state indefinitely.
def mutator(state, comparator):
while True:
states = [0] + state + [0]
state = [
comparator(states[cellid-1], states[cellid+1])
for cellid in range(1, len(states)-1)
]
yield state
def cellCompete(states, days):
generator = mutator(states, lambda x, y: x ^ y)
for idx, states in enumerate(generator):
if idx+2 > days:
break
return states
print(cellCompete([1,0,0,0,0,1,0,0] , 1))
print(cellCompete([1,1,1,0,1,1,1,1] , 2))
Also, I added a comparator that allow us to have some kind of undefined operation on both elements. It can allow the code to be extended beyond the initial spec. It's obviously a superfluous implementation but as mentioned, it's supposed to be an interview answer and as much as I like to see a straight to the point answer, if someone can come up with a flexible answer in the same timeframe, then why not.

python game 2048 out of list index

I was working on the coursera python project 2048 using codesculpter.
The code works fine when I try 4 x 4 or 5 x 5, but it shows error when 4 x 5 or any other when height != width. I think I must have messed up somewhere in the __init__ or other places but I couldn't figure out.
Could someone give me some suggestions?
Here is what I have tried so far:
import poc_2048_gui
import random
# Directions, DO NOT MODIFY
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4
# Offsets for computing tile indices in each direction.
# DO NOT MODIFY this dictionary.
OFFSETS = {UP: (1, 0),
DOWN: (-1, 0),
LEFT: (0, 1),
RIGHT: (0, -1)}
def merge(line):
"""
Helper function that merges a single row or column in 2048
"""
# creat output list and remove 0
after_merge=[]
storage = []
for num_1 in range(len(line)):
after_merge.append(0)
if line[num_1] != 0 :
storage.append(line[num_1])
# sum number
for num_2 in range(len(storage)):
if num_2+2> len(storage):
break
elif storage[num_2]==storage[num_2+1]:
storage[num_2]*=2
storage.pop(num_2+1)
# replace 0 in after merge
for num in range(len(storage)):
after_merge[num]=storage[num]
return after_merge
class TwentyFortyEight:
"""
Class to run the game logic.
"""
def __init__(self, grid_height, grid_width):
self.grid_height = grid_height
self.grid_width = grid_width
self.cell=[]
self.indices = {}
self.indices[UP] = [[0,n] for n in range(grid_width)]
self.indices[LEFT] = [[n,0] for n in range(grid_height)]
self.indices[RIGHT] = [[n, grid_width - 1] for n in range(grid_height)]
self.indices[DOWN] = [[grid_height - 1, n]for n in range(grid_width)]
self.ranges = {}
self.ranges[UP] = grid_height
self.ranges[DOWN] = grid_height
self.ranges[LEFT] = grid_width
self.ranges[RIGHT] = grid_width
#self.reset()
def reset(self):
"""
Reset the game so the grid is empty except for two
initial tiles.
"""
self.cell = [[0*(col+row) for row in range(self.grid_height)] for col in range (self.grid_width)]
for count in range(2):
self.new_tile()
def __str__(self):
"""
Return a string representation of the grid for debugging.
"""
a_str = ""
for row in range(self.grid_height):
for col in range (self.grid_width):
a_str += ( str(self.cell[row][col]) + " " )
a_str += '\n'
return a_str
def get_grid_height(self):
"""
Get the height of the board.
"""
# replace with your code
return self.grid_height
def get_grid_width(self):
"""
Get the width of the board.
"""
# replace with your code
return self.grid_width
def move(self, direction):
"""
Move all tiles in the given direction and add
a new tile if any tiles moved.
"""
a_list = []
has_moved = False
for index in self.indices[direction]:
for step in range(self.ranges[direction]):
a_list.append(self.cell[index[0] + OFFSETS[direction][0] * step]
[index[1] + OFFSETS[direction][1] * step])
merged_list = merge(a_list)
if merged_list != a_list:
for step in range(self.ranges[direction]):
self.cell[index[0] + OFFSETS[direction][0] * step] \
[index[1] + OFFSETS[direction][1] * step] = merged_list[step]
has_moved = True
a_list = []
if has_moved:
self.new_tile()
def new_tile(self):
"""
Create a new tile in a randomly selected empty
square. The tile should be 2 90% of the time and
4 10% of the time.
"""
# replace with your code
row=0
col=0
available_positions = []
for row in range(self.grid_height):
for col in range(self.grid_width):
if self.cell[row][col] == 0:
available_positions.append([row, col])
if not available_positions:
print "There are no available positions."
random_pos=random.choice(available_positions)
rand_val=random.randint(1,10)
if rand_val>=9:
new_tile=4
else:
new_tile=2
self.set_tile(random_pos[0], random_pos[1], new_tile)
def set_tile(self, row, col, value):
"""
Set the tile at position row, col to have the given value.
"""
# replace with your code
self.cell[row][col] = value
def get_tile(self, row, col):
"""
Return the value of the tile at position row, col.
"""
# replace with your code
return self.cell[row][col]
poc_2048_gui.run_gui(TwentyFortyEight(4, 4))
Okay, I didn't debug all the way but here's what I found. cell should have dimension grid_height * grid_width. Correct?
However, right before this loop:
for row in range(self.grid_height):
for col in range(self.grid_width):
print(row," ",col);
if self.cell[row][col] == 0:
available_positions.append([row, col])
if not available_positions:
print "There are no available positions."
I found that the size of cell is reverse. That is grid_width * grid_height. Put these two lines before the nested loops to see for yourself.
print("cell size",len(self.cell)," ",len(self.cell[0]))
print("grid size",self.grid_height," ",self.grid_width)
This will cause IndexError in line if self.cell[row][col] == 0: when the dimensions are different. That being said, you should step through and see exactly how you fill in both the grid and the cell. Make sure they correspond correctly.
Hope that helps!
I did a quick debug and I was able to get a IndexError when calling move(). Looking through, you seem to expect self.cell to be populated, but it only is ever populated through your reset() function. You may not see this if your UI module calls reset when initializing...
There is a second IndexError then when the row and col are not the same number. This (as mentioned in the other answer) is because your 2D array representation is col * row, not row * col
below is the printout of (4, 6), which has 4 COLUMN and 6 ROW. You likely just need to swap the two in your representation:
[0, 0, 0, 0]
[2, 0, 2, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
A potential improvement to your syntax, but you can initiate your cells as such (test with your usage...)
self.cell = [[[0] * self.grid_width] for row in xrange(self.grid_height)]
Lastly, I believe you may get an IndexError in your new_tile because Python lists begin at the 0th element. You'll want to iter 0 through n-1:
for row in range(self.grid_height-1):
for col in range(self.grid_width-1):

Game Of Life : How to keep track of active cells

Now I have read the other stackoverflow Game of Life questions and also Googled voraciously.I know what to do for my Python implementation of the Game Of Life.I want to keep track of the active cells in the grid.The problem is I'm stuck at how should I code it.
Here's what I thought up but I was kinda at my wit's end beyond that:
Maintain a ActiveCell list consisting of cell co-ordinates tuples which are active
dead or alive.
When computing next generation , just iterate over the ActiveCell list,compute cell
state and check whether state changes or not.
If state changes , add all of the present cells neighbours to the list
If not , remove that cell from the list
Now the problem is : (" . "--> other cell)
B C D
. A .
. . .
If A satisfies 3) then it adds B,C,D
then if B also returns true for 3) ,which means it will add A,C again
(Duplication)
I considered using OrderedSet or something to take care of the order and avoid duplication.But still these I hit these issues.I just need a direction.
don't know if it will help you, but here's a quick sketch of Game of Life, with activecells dictionary:
from itertools import product
def show(board):
for row in board:
print " ".join(row)
def init(N):
board = []
for x in range(N):
board.append([])
for y in range(N):
board[x].append(".");
return board
def create_plane(board):
board[2][0] = "x"
board[2][1] = "x"
board[2][2] = "x"
board[1][2] = "x"
board[0][1] = "x"
def neighbors(i, j, N):
g1 = {x for x in product([1, 0, -1], repeat=2) if x != (0, 0)}
g2 = {(i + di, j + dj) for di, dj in g1}
return [(x, y) for x, y in g2 if x >= 0 and x < N and y >= 0 and y < N]
def live(board):
N = len(board)
acells = {}
for i in range(N):
for j in range(N):
if board[i][j] == "x":
for (x, y) in neighbors(i, j, N):
if (x, y) not in acells: acells[(x, y)] = board[x][y]
while True:
print "-" * 2 * N, len(acells), "cells to check"
show(board)
raw_input("Press any key...")
for c in acells.keys():
a = len([x for x in neighbors(c[0], c[1], N) if board[x[0]][x[1]] == "x"])
cur = board[c[0]][c[1]]
if a == 0:
del acells[c] # if no live cells around, remove from active
elif cur == "x" and a not in (2, 3):
acells[c] = "." # if alive and not 2 or 3 neighbors - dead
elif cur == "." and a == 3:
acells[c] = "x" # if dead and 3 neighbors - alive
for x in neighbors(c[0], c[1], N): # add all neighbors of new born
if x not in acells: acells[x] = board[x[0]][x[1]]
for c in acells:
board[c[0]][c[1]] = acells[c]
N = 7
board = init(N)
create_plane(board)
live(board)
You have two lists, I'll name them currentState, and newChanges. Here will be the workflow:
Iterate over currentState, figuring out which are newly born cells, and which ones are going to die. Do NOT add these changes to your currentState. If there is a cell to be born or a death, add it to the newChanges list. When you are finished with this step, currentState should look exactly the same as it did at the beginning.
Once you have finished all calculations in step 1 for every cell, then iterate over newChanges. For each pair in newChanges, change it in currentState from dead to alive or vice versa.
Example:
currentState has {0,0} {0,1} {0,2}. (Three dots in a line)
newChanges is calculated to be {0,0} {-1,1} {1,1} {0,2} (The two end dots die, and the spot above and below the middle are born)
currentState recieves the changes, and becomes {-1,1} {0,1} {1 ,1}, and newChanges is cleared.
Did you consider using an ordered dictionary and just set the values to None?
You didn't state that you have a restriction to implement the game in a specific way. So, the main question is: how big a grid do you want to be able to handle?
For example, if you are starting with a small fixed-size grid, the simplest representation is just a [[bool]] or [[int]] containing whether each cell is alive or dead. So, each round, you can make a new grid from the old one, e.g. assuming that all cells outside the grid are dead. Example:
[
[False, True, False],
[True, False, True],
[False, True, False],
]
If you want a very large dynamic-sized grid, there's the HashLife algorithm, which is much faster, but more complicated.
I implemented Game of Life in Python for fun and what I did was having board dict, with tuples of coordinates. Value is a state of cell. You can look at the code here https://raw.github.com/tdi/pycello/master/pycello.py. I know this is not very fast implementation and the project is abandoned due to lack of time.
board = {}
board[(x,y)] = value

Categories

Resources