Related
I am trying to implement the BFS algorithm but python is giving me an error that the ellipsis object is not sub scriptable.
I am unsure what these means because as far as I am aware this type should not be Ellipsis?
TypeError: 'ellipsis' object is not subscriptable
Causing error:
visited[starting_row][starting_col] = True
Function:
def findRoute(self, x1, y1, x2, y2):
grid = self.grid
print(grid)
starting_row, starting_col = x1, y1
# Creating 2 seperate queues for X and Y.
x_queue, y_queue = deque(), deque()
number_of_moves = 0
number_of_nodes_in_current_layer = 1
number_of_nodes_in_next_layer = 0
end_reached = False
# Up/Down/Right/Left directions
direction_row = [-1, 1, 0, 0]
direction_col = [0, 0, 1, -1]
visited = ...
x_queue.append(starting_row)
y_queue.append(starting_col)
visited[starting_row][starting_col] = True
while len(x_queue) > 0:
x = x_queue.dequeue()
y = y_queue.dequeue()
if x == x2 & y == y2:
end_reached = True
break
# for(i = 0; i < 4; i++):
# Loop through direction.
for i in range(0, 4):
new_row = x + direction_row[i]
new_col = x + direction_col[i]
#Validate position
# Skip locations not in grid.
if new_row < 0 or new_col < 0 or new_row >= self.height or new_col >= self.width:
continue
# Skip locations already visited / cells blocked by walls.
if visited[new_row][new_col] or grid[new_row][new_col]:
continue
x_queue.enqueue(new_row)
y_queue.enqueue(new_col)
visited[new_row][new_col] = True
number_of_nodes_in_next_layer += 1
if number_of_nodes_in_current_layer == 0:
number_of_nodes_in_current_layer = number_of_nodes_in_next_layer
number_of_nodes_in_next_layer = 0
number_of_moves += 1
if end_reached:
return number_of_moves
return -1
return grid[1][2]
Any help would be appreciated, thanks.
Your code has this line:
visited = ...
This ... is not commonly used, but it is a native object. The documentation on Ellipsis has:
The same as the ellipsis literal “...”. Special value used mostly in conjunction with extended slicing syntax for user-defined container data types. Ellipsis is the sole instance of the types.EllipsisType type.
As the error message states, this object is not subscriptable, yet that is exactly what you tried to do with:
visited[starting_row][starting_col] = True
I suppose you didn't really intend to use visited = ..., and that you were planning to complete this statement later and then forgot about it. It should be:
visited = [[False] * len(row) for row in grid]
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.
I'm working on a challenge to count the number of moves it takes to get from point A to point B on a grid which is set out like a chessboard and the moves you can make are that of the Knight so 2 in any direction and 1 perpendicular.
I've gotten most of it worked out but for some reason, my counter is not returning the number of moves between the two points. Below is what I have regarding the counting.
You'll notice I'm using a dict called position and the reason for this is so that I can store an int representing the no of moves that particular position is from the destination.
I thought at the end I should be incrementing the move value after a move is deemed valid but I'm still failing to get the right number.
def solution(src, dest):
# Chessboard made using nested lists. The indexes will act as coordinates.
chessboard = [
[0,1,2,3,4,5,6,7],
[8,9,10,11,12,13,14,15],
[16,17,18,19,20,21,22,23],
[24,25,26,27,28,29,30,31],
[32,33,34,35,36,37,38,39],
[40,41,42,43,44,45,46,47],
[48,49,50,51,52,53,54,55],
[56,57,58,59,60,61,62,63]
]
# Find index values of src and dest
for row in chessboard:
if src in row:
srcX = chessboard.index(row)
srcY = row.index(src)
if dest in row:
destX = chessboard.index(row)
destY = row.index(dest)
# Position dict to store indexes and no of mvoes when using bfs
position = {
'x': 0,
'y': 0,
'moves': 0,
}
position['x'] = srcX
position['y'] = srcY
# Below represents the knights moves to be applied to the index of position
row = [-2,-2,-1,1,2,2,1,-1]
col = [-1,1,2,2,-1,1,-2,-2]
# We use an if-statement to check for valid moves
def isValid(x, y):
return not (x < 0 or y < 0 or x >=8 or y >=8)
q = []
q.append(position)
# Record spaces visited already
isVisited = []
while len(q)>0:
space = q.pop()
x = space['x']
y = space['y']
moves = space['moves']
# if the position matches the destination, return no.moves
# I'm just using print to see the result in the terminal
if x == destX and y == destY:
print(moves)
if (x,y) not in isVisited:
isVisited.append((x,y))
# Loop over possible moves
for i in range(len(row)):
newX = x + row[i]
newY = y + col[i]
if isValid(newX, newY):
position['x'] = newX
position['y'] = newY
position['moves'] = moves+1
q.append(position)
This is a common Python issue. You create a dictionary called "position" and add it to the queue. Then, any time you have a new move, you MODIFY that same dictionary and add it to the queue again, but that's not creating a NEW dictionary. What you will end up with is a queue that full of references to the exact same dictionary. Every time you change "position", you're changing every dict in the queue. You need to create a new object each time.
This seems to work.
# We use an if-statement to check for valid moves
def isValid(x, y):
return (0 <= x <= 7) and (0 <= y <= 7)
def solution(src, dest):
# Chessboard made using nested lists. The indexes will act as coordinates.
chessboard = [
[0,1,2,3,4,5,6,7],
[8,9,10,11,12,13,14,15],
[16,17,18,19,20,21,22,23],
[24,25,26,27,28,29,30,31],
[32,33,34,35,36,37,38,39],
[40,41,42,43,44,45,46,47],
[48,49,50,51,52,53,54,55],
[56,57,58,59,60,61,62,63]
]
# Find index values of src and dest
srcY = src % 8
srcX = src // 8
destY = dest % 8
destX = dest // 8
# Position dict to store indexes and no of mvoes when using bfs
position = {
'x': srcX,
'y': srcY,
'moves': 0,
}
# Below represents the knights moves to be applied to the index of position
row = [ -2, -2, -1, -1, 1, 1, 2, 2]
col = [ -1, 1, 2, -2, -2, 2, -1, 1]
q = []
q.append(position)
# Record spaces visited already
isVisited = []
while q:
space = q.pop()
print( "Checking", space )
x = space['x']
y = space['y']
moves = space['moves']
# if the position matches the destination, return no.moves
# I'm just using print to see the result in the terminal
if x == destX and y == destY:
print(f"{moves}!!!")
return moves
if (x,y) not in isVisited:
isVisited.append((x,y))
# Loop over possible moves
for dx,dy in zip(row,col):
newX = x + dx
newY = y + dy
if isValid(newX, newY):
position = {
'x': newX,
'y': newY,
'moves': moves+1
}
q.append(position)
print( solution( 3, 61 ) )
I have a dataframe with thousands rows. One column consists of only 3 values: -1, 0, 1. I would like to count in rolling window (let's say 100) how many times a specific value (let's say 0) occurs.
How can I do it? I do not see such a method related to the object Rolling and I don't know how to do it by apply.
It's pretty simple, I coded up a quick demo. You should get the idea.
Example
# Parameters
# iterable - column
# size - window size (100)
def window(iterable, size=2):
i = iter(iterable)
win = []
for e in range(0, size):
win.append(next(i))
yield win
for e in i:
win = win[1:] + [e]
yield win
# Sample data
a = [1, 0, 0, 0, 1, 1]
from collections import Counter
result = []
value = 1 # Value to keep count (-1, 0, 1)
for i in window(a, 2):
count = Counter(i)[value]
result.append(count)
# Sample output
print(result)
[1, 0, 0, 1, 2]
I guess this will help. I tested this, It works
def cnt(x):
prev_count = 0
for i in x:
if i == 0:
prev_count+=1
return prev_count
df['col'].rolling(100,min_periods=1).apply(cnt)
I'm working on a tic-tac-toe game with a M x N board in Python. I'm trying to find an efficient way to determine if a player has won (3 in a row either vertical, horizontal, or diagonal direction.) Most 3x3 implementations of the game just check for all possible winning combinations after each turn. This seems a little extreme with a massive board.
4x4 example: (using 1s and 2s instead of Xs and Os)
board = ([1,0,2,1], [0,0,0,1], [2,2,0,0], [1,0,0,1])
for row in board:
print row
Thanks-
Jonathan
Although this approach has a certain appeal, it's probably not especially fast.
# A bogus game with wins in several directions.
board = (
[1,1,2,1],
[0,2,1,1],
[2,2,2,1],
[1,0,0,1],
)
# A few convenience variables.
n_rows = len(board)
lft = [ [0] * i for i in range(n_rows) ] # [[], [0], [0, 0], [0, 0, 0]]
rgt = list(reversed(lft))
# Create transpositions of the board to check for wins in various directions.
transpositions = {
'horizontal' : board,
'vertical' : zip(*board),
'diag_forw' : zip(* [lft[i] + board[i] + rgt[i] for i in range(n_rows)] ),
'diag_back' : zip(* [rgt[i] + board[i] + lft[i] for i in range(n_rows)] ),
}
# Apply Jonathan's horizontal-win check to all of the transpositions.
for direction, transp in transpositions.iteritems():
for row in transp:
s = ''.join( map(str, row) )
for player in range(1,3):
if s.find(str(player) * 3) >= 0:
print 'player={0} direction={1}'.format(player, direction)
Output:
player=1 direction=diag_back
player=2 direction=diag_forw
player=2 direction=horizontal
player=1 direction=vertical
The idea behind the diagonal transpositions is to shift the rows, using lft and rgt for left and right padding. For example, the diag_forw list looks like this after the padding is added (pad characters are shown as periods, even though zeroes are used in the actual code).
1 1 2 1 . . .
. 0 2 1 1 . .
. . 2 2 2 1 .
. . . 1 0 0 1
Then we simply transpose that array, using zip(*foo), which allows us to use Jonathan's good idea for finding horizontal wins.
You can look if the player's move closed the game (looking on that row, that column and the 2 diagonals if they ar x checks consecutively), it's o(x) complexity. Let's say you're looking o that row to see if he won. Look to the left how many consecutively checks are and to the right. If the sum of them excedes x he won. You'll do the same on the columns and on the diagonals.
Check for horizontal win
for row in board:
rowString = ''.join(row)
if(rowString.count('111') > 2 or rowString.count('222') > 2):
print "Somebody won"
Check for vertical win
for col in xrange(len(board[0])):
colString = ""
for row in board:
colString = colString.append(row[col])
if(colString.count('111') > 2 or colString.count('222') > 2):
print "Somebody won"
Still stumped on diagonals...
If you have a board set up as follows:
board =
([1,0,2,0],
[0,1,2,0],
[0,0,0,0],
[0,0,0,0])
You can imagine it as x and y coordinates, starting from the upper left hand corner, having downward movement as positive y and rightward movement as positive x. A move at board[3][3] by either player would be a winning move. Using Teodor Pripoae process, we can construct the horizontal, vertical and diagonals around the last move. The horizontal case is easy.
def horizontal(board, y_coord):
return board[y_coord]
The vertical case requires us to select the x_coord from each row:
def vertical(board, x_coord):
return [row[x_coord] for row in board]
The diagonal case is a bit trickier. For this first function, it's computing the diagonal that goes from left to right as it goes top to bottom. Distance basically represents the horizontal distance from zero, when y is equal to zero.
def diagonal1(board, x_coord, y_coord):
length = len(board[0])
distance = x_coord - y_coord
if distance >= 0:
return [y[x] for x, y in enumerate(board) if distance + x <= length]
else:
return [y[x] for x, y in enumerate(board) if x - distance >= 0 and x - distance <= length]
This second function computes the diagonal that goes right to left as it goes top to bottom. In this function distance represents the vertical distance from zero as the horizontal distance is at zero.
def diagonal2(board, x_coord, y_coord):
length = len(board[0])
distance = y_coord + x_coord
return [y[distance - x] for x, y in enumerate(board) if distance - x <= length]
Once you have these defined, you just need a way to check if a player has won. Something like this might do:
def game_over(direction, number_to_win, player_number):
count = 0
for i in direction:
if i == player_number:
count += 1
if count = number_to_win:
return True
else:
count = 0
return False
Having written all of this, it seems like this is overkill, unless you have quite large M and N. While it may be more efficient than checking every victory condition, it does construct the entire horizontal, vertical and diagonal directions, rather than just those coordinates surrounding the last move, it isn't as efficient as it could be.
Maybe this is helpful, but it seems like Brian's suggestion to simply remove x's might be better.
I've been using a variant of this question in software developer interviews, so I've thought about the problem a fair bit. Here's a better answer: it handles any number of players, any square tic-tac-toe grid, and any "run size". The approach is fairly simple, provides info about all of the sequences found, and is O(N) where N is the number of cells.
# Given a square tic-tac-toe grid of any size, with any number of players, find
# all sequences (horizontal, vertical, diagonal) of some minimum size.
def main():
raw_grid = [
[1, 1, 2, 1, 0], # Zero means open spot.
[0, 2, 1, 1, 1],
[2, 2, 2, 1, 2],
[1, 0, 1, 1, 2],
[1, 0, 0, 0, 2],
]
for run in get_runs(raw_grid, 3):
print run
def get_runs(raw_grid, run_size):
# Offsets to find the previous cell in all four directions.
offsets = {
'h' : ( 0, -1), # _
'v' : (-1, 0), # |
'f' : (-1, 1), # /
'b' : (-1, -1), # \
}
# Helpers to check for valid array bounds and to return a new cell dict.
size = len(raw_grid)
in_bounds = lambda r, c: r >= 0 and c >= 0 and r < size and c < size
new_cell = lambda i, j, p: dict(h=1, v=1, f=1, b=1, i=i, j=j, player=p)
# Use the raw grid to create a grid of cell dicts.
grid = []
for i, row in enumerate(raw_grid):
grid.append([])
for j, player in enumerate(row):
# Add a cell dict to the grid (or None for empty spots).
cell = new_cell(i, j, player) if player else None
grid[i].append(cell)
if not cell: continue
# For each direction, look to the previous cell. If it matches the
# current player, we can extend the run in that direction.
for d, offset in offsets.iteritems():
r, c = (i + offset[0], j + offset[1])
if in_bounds(r, c):
prev = grid[r][c]
if prev and prev['player'] == cell['player']:
# We have a match, so the run size is one bigger,
# and we will track that run in the current cell,
# not the previous one.
cell[d] = prev[d] + 1
prev[d] = None
# For all non-None cells, yield run info for any runs that are big enough.
for cell in (c for row in grid for c in row if c):
for d in offsets:
if cell[d] and cell[d] >= run_size:
yield dict(
player = cell['player'],
endpoint = (cell['i'], cell['j']),
direction = d,
run_size = cell[d],
)
main()
Output:
{'player': 1, 'direction': 'h', 'endpoint': (1, 4), 'run_size': 3}
{'player': 2, 'direction': 'f', 'endpoint': (2, 0), 'run_size': 3}
{'player': 2, 'direction': 'h', 'endpoint': (2, 2), 'run_size': 3}
{'player': 1, 'direction': 'b', 'endpoint': (2, 3), 'run_size': 3}
{'player': 1, 'direction': 'f', 'endpoint': (3, 2), 'run_size': 3}
{'player': 1, 'direction': 'v', 'endpoint': (3, 3), 'run_size': 4}
{'player': 2, 'direction': 'v', 'endpoint': (4, 4), 'run_size': 3}