python depth-first search recursion - python

i am trying to make a python dfs connecting island with recursion...
the program works fine, however on some cases there are logical error in which the output is incorrect
For example
o o o
o x x
o o o the output is 1 which is correct.
However , on other cases
o x o
o x o
o o o the output is 2 which is incorrect.
Here is my full code that includes dfs function
row = int(input("Enter Row : "))
col = int(input("Enter Col : "))
# declare array baru namanya peta
peta = []
# array 2 dimensi
# Masukkin smua input ke array petas
for i in range(0,row):
line = input()
peta.append(line)
store = []
# declare array baru nama visited
visited = []
for i in range(0,row):
visited.append([])
# buat column di row i false smua
for j in range(0,col):
visited[i].append(False)
def dfs(i,j):
visited[i][j] = True
a = row-1
b = col-1
#peta[i][j] = store[a][b]
for i in range(i,row):
for j in range(j,col):
if(visited[i][j] == True):
return 1
else:
if(peta[i][j] == 'x' and visited[i][j] == False ):
#top left array
if(i == 0 or j == 0):
dfs(i+1,j+1)
dfs(i+1,j)
dfs(i,j+1)
#bottom left array
elif(i == a and j == 0):
dfs(i-1,j)
dfs(i-1,j+1)
dfs(i,j+1)
#top right array
elif(i == 0 and j == b):
dfs(i,j-1)
dfs(i+1,j-1)
dfs(i+1,j)
#bottom right array
elif(i == a and j == b):
dfs(i,j-1)
dfs(i-1,j-1)
dfs(i-1,j)
#west array
elif(i >= 1 and j == 0):
dfs(i-1,j)
dfs(i-1,j+1)
dfs(i+1,j)
dfs(i,j+1)
dfs(i+1,j+1)
#north array
elif(i==0 and j>=1):
dfs(i,j-1)
dfs(i+1,j-1)
dfs(i+1,j)
dfs(i,j+1)
dfs(i+1,j+1)
#east array
elif(i>=1 and j==b):
dfs(i-1,j)
dfs(i-1,j-1)
dfs(i,j-1)
dfs(i+1,j-1)
dfs(i+1,j)
#south array
elif(i==a and j>=1):
dfs(i,j-1)
dfs(i-1,j-1)
dfs(i-1,j)
dfs(i-1,j+1)
dfs(i,j+1)
#middle array
else:
dfs(i-1,j-1)
dfs(i-1,j)
dfs(i-1,j+1)
dfs(i,j-1)
dfs(i,j+1)
dfs(i+1,j-1)
dfs(i+1,j)
dfs(i+1,j+1)
else:
#peta[i][j] = 0
return 0
numberofisland = 0
for i in range(0,row):
for j in range(0,col):
if((peta[i][j] == 'x' and visited[i][j] == False)):
dfs(i,j)
numberofisland+=1
print(numberofisland)
in my opinion, my logical error is i visit the visited node twice , however there seems no error in my arrays. Can you give some suggestion about where my mistake is?
Thank you very much for your time, cheers
edit : i have updated into full code version as community requested ( how to call the function, global variable, etc )

Some things in your code do not make sense:
1) If you want to return a value from the dfs function, this value should have some meaning and it should be used. If you only call a function for its side-effects, then you can just return with no value. In this case, it looks like to me as the purpose of the dfs function is to update the visited array, so you do not need to return 1 or 0 or anything.
2) When you do a depth-first search in a graph, you start at a node, and you visit its connected nodes recursively. If you have a for-loop inside the dfs function which visits a big part of the graph, ignoring connections, then you are not doing DFS. Generally, you only need to recursively call the dfs function on the connected nodes.
3) The way your function looks right now, it seems that it will always return 1 before making any recursive calls.
Also take note of the following good practices for Python code:
1) Avoid constructs like if expression == True:. Instead use if expression:. Also instead of if expression == False, use if not expression.
2) Avoid using parentheses around the conditional expression in if and elif clauses, it is not neccessary, unlike C or Java. For example, instead of elif (a == b): use elif a == b.
3) Add a docstring at the top of a function, to describe what the function does (see my code below for an example).
From what I understand, you want for each call of the dfs function to visit all connected xs that form an island. You can do this with the following code:
def dfs(i,j):
'''
This function starts from the square with coordinates (i,j).
The square (i,j) should be an 'x', and also unvisited.
This function will keep visiting all adjacent 'x' squares, using a
depth first traversal, and marking these squares as visited in the
#visited array.
The squares considered adjacent are the 8 surrounding squares:
(up, up-right, right, down-right, down, down-left, left, up-left).
'''
# The range checks have been moved to the beginning of the function, after each call.
# This makes the code much shorter.
if i < 0 or j < 0 or i >= row or j >= col:
return
# If we reached a non-x square, or an already visited square, stop the recursion.
if peta[i][j] != 'x' or visited[i][j]:
# Notice that since we don't do something with the return value,
# there is no point in returning a value here.
return
# Mark this square as visited.
visited[i][j] = True
# Visit all adjacent squares (up to 8 squares).
# Don't worry about some index falling out of range,
# this will be checked right after the function call.
dfs(i-1,j-1)
dfs(i-1,j)
dfs(i-1,j+1)
dfs(i,j-1)
dfs(i,j+1)
dfs(i+1,j-1)
dfs(i+1,j)
dfs(i+1,j+1)

Related

Cant properly update cell status in my Game Of Life

cells not "dying" in may logocal loop. New cells geretion is normal, i think..
so cells not die
all calculations are into one numpy array, i dont have "second/old/new etc" arrays
sory for my bad English
def updateCells(grid: np.ndarray):
for i in range(GRID_WIDTH):
for j in range(GRID_HEIGHT):
#neighbours list contains 0, 1
neighbours = getNeighbours(i, j, grid)
#sum of alive neighbours
count = sum(neighbours)
if grid[i][j] == 1:
if count < 2:
grid[i][j] = 0
continue
if count == 2 or count == 3:
grid[i][j] = 1
continue
if count > 3:
grid[i][j] = 0
continue
if grid[i][j] == 0:
if count == 3:
grid[i][j] = 1
continue
function for getting neighbours of one cell:
def getNeighbours(i, j, arr):
ax_0_y = np.size(arr, 1)
ax_1_x = np.size(arr, 0)
n = []
ofssets = [-1, 0, 1]
for x in ofssets:
for y in ofssets:
if x == 0 and y == 0: continue
if y+j < 0 or y+j == ax_0_y: continue
if x+i < 0 or x+i == ax_1_x: continue
n.append(arr[x+i][y+j])
return n
main loop:
def main():
grid = np.zeros((GRID_WIDTH, GRID_HEIGHT), dtype=numpy.short)
....
drawGrid(display) #drawing cells line separators
randomise(grid) #random gereation alive cells
while True:
for i in pg.event.get():
if i.type == pg.QUIT:
quit()
updateCells(grid)
drawCells(display, grid) #drawing squares
pg.display.update()
fpsClock.tick(UPDATE_TIME)
if __name__ == "__main__":
main()
You can’t update the grid as you go, because then cells you get to later in the scan will have some neighbors from the current generation and some from the new one and will get the wrong neighbor count.
You have to do the whole grid as if it all updates at the same time, which means either having a second grid to build the new generation in or creating a list of changes to make to the grid at the end of the scan.
Some of the cells might not die due to you changing the field before the checks, like mentioned in the previous answer. You also seem to have quiet a bit of unneccessary code, like
if count == 2 or count == 3:
grid[i][j] = 1
continue
For example.
If there is no dying cells at all, i would like you to show us the code that calls your function.

Infinite recursive call minimax algorithm

I have recently implemented the code for a 4X4 tic tac toe game which is using minimax algorithm.
However, my minimax function is calling itself recursively infinite no. of times.
Initial Board (4X4) tic tac toe ->
board = np.array([['_','_','_', '_'],['_','_','_', '_'],['_','_','_', '_'],['_','_','_', '_']])
Code for the computer's turn ->
import math
from utility import *
def minimax(board, spotsLeft, maximizing):
bestIdx = (0,0)
p1 = 'X'
p2 = 'O'
res, stat = checkGameOver(board, spotsLeft)
if res==True:
if stat == 'X': return 17-spotsLeft, bestIdx
elif stat == 'O': return -17+spotsLeft, bestIdx
else: return 0, bestIdx
if maximizing:
# return max score
bestMove = -math.inf
for i in range(4):
for j in range(4):
if board[i][j] == '_':
board[i][j] = p1
score, idx = minimax(board, spotsLeft-1, False)
print(score, idx)
board[i][j] = '_'
if bestMove<score:
bestMove = score
bestIdx = (i,j)
return bestMove, bestIdx
else:
# return min score
bestMove = math.inf
for i in range(4):
for j in range(4):
if board[i][j] == '_':
board[i][j] = p2
score, idx = minimax(board, spotsLeft-1, True)
print(score, idx)
board[i][j] = '_'
if bestMove>score:
bestMove = score
bestIdx = (i,j)
return bestMove, bestIdx
def ai(board):
spotsLeft = 16
p1 = 'X' # Computer
p2 = 'O' # Player
turn = p1
while True:
res, stat = checkGameOver(board, spotsLeft)
if res==False:
if turn == p1:
# AI
boardCopy = board[:]
score, idx = minimax(boardCopy, spotsLeft, True)
board[idx[0]][idx[1]] = turn
turn = p2
spotsLeft-=1
else:
# Human
inp = int(input(f"Your turn: "))
i, j = spotToIdx(inp)
if board[i][j]=='_':
board[i][j] = turn
turn = p1
spotsLeft-=1
else: return stat
printBoard(board)
In the above code
spotsLeft is the empty places on board,
checkGameOver returns "X" (if Player X wins), returns "O" (if Player O wins) & returns "T" (if game is tied)
checkGameOver function ->
def checkWin(board):
# Check Row
for i in board:
if len(set(i)) == 1 and i[0]!='_':
return i[0]
# Check Column
for i in range(4):
if (board[0][i] == board[1][i] == board[2][i] == board[3][i]) and board[0][i]!='_':
return board[0][i]
# Check Diagonals
if (board[0][0]==board[1][1]==board[2][2]==board[3][3]) and board[0][0]!='_':
return board[0][0]
if (board[0][3]==board[1][2]==board[2][1]==board[3][0]) and board[0][3]!='_':
return board[0][3]
# No One Won Yet
return -1
def checkGameOver(board, spotsLeft):
# t -> Game Tied
# x -> Player X won
# o -> Player O won
# if tied - all spots filled
if spotsLeft == 0:
return True, 'T'
# if any player won the game
result = checkWin(board)
if result!=-1:
return True, result
return False, -1
I think your code is fine, and doing what you want it to do. The issue is most likely due to the complexity of the problem, and for a lower dimension tic tac toe it would work fine.
Let's first simplify and look at a 2x2 case. For the first turn, you call minimax from the ai function, which will at the first level call itself 4 times. At the next level, each one of those calls will also call minimax, but one time less than at the previous level. To put it as a list:
level 0 (ai function): 1 call to minimax
level 1: 4 calls
level 2: 4x3 calls (each of the 4 calls above make 3 new calls)
level 3: 4x3x2 calls
level 4: 4x3x2x1 calls
Now using n-factorial notation as n!, we can compute the total number of calls as:
4!/4! + 4!/(4-1)! + 4!/(4-2)! + 4!/(4-3)! + 4!/(4-4!)
Which is the sum of n!/(n-k)! for k between 0 and n (included), n begin the number of cells on your board. The result here is 65 calls to minimax for a 2x2 board.
Put into a python code:
def factorial(n):
if n > 1: return n*factorial(n-1)
else: return 1
def tictactoeComplexity(gridsize):
return sum([factorial(gridsize)/factorial(gridsize-k) for k in range(gridsize+1)])
print(tictactoeComplexity(2*2)) # result is 65
Now let's check for a 3*3 board:
print(tictactoeComplexity(3*3)) # result is 986,410
Just going from 2x2 to 3x3, we go from around 100 to around 1,000,000. You can guess the result for a 4x4 board:
print(tictactoeComplexity(4*4)) # result is 56,874,039,553,217
So your program does what you are asking it to do, but you're asking quite a lot.
As Jenny has very well explained, the search tree is too large. Even if you would make improvements to the data structure and move-mechanics to make them more efficient and reduce the memory footprint, it will still be a challenge to have this search finish within an acceptable time.
In general you would go for the following measures to cut down on the search tree:
Stop at a certain search depth (like 4) and perform a static evaluation of the board instead. This evaluation will be a heuristic. For instance it will give a high value to a 3-in-a-row with a free cell available to complete it to a win. It would also give a significant value to a pair on a line that is not blocked by the opponent, ...etc.
Use alpha-beta pruning to avoid searching in branches that could never lead to a better choice at an earlier stage in the search tree.
Use killer moves: a move that was found good after a certain opponent move, could also turn out to be good in reply to another opponent move. And trying that one first may work well in combination with alpha-beta pruning.
Memoize positions that were already evaluated (by swapping of moves), and mirror positions to equivalent positions to reduce the size of that dictionary.
But to be honest, 4x4 Tic Tac Toe is quite boring: it is very easy to play a draw, and it requires really inferior moves for a player to give the game away to the other. For instance, neither player can make a bad first move. Whatever they play on their first move, it will still be a draw with correct play.
So... I would propose to only use a heuristic evaluation function, and not search at all. Or, perform a shallow minimax, and use such a heuristic evaluation at that fixed depth. But even replacing the minimax algorithm with just an evaluation function works quite well.
What to change
To implement that idea, proceed as follows:
Replace the AI part with this:
if turn == p1:
# AI -- a heuristic based approach
bestScore = -math.inf
bestMove = None
for i in range(4):
for j in range(4):
if board[i][j] == '_':
board[i][j] = turn
score = evaluate(board, turn)
if score > bestScore:
bestScore = score
bestMove = (i, j)
board[i][j] = '_'
i, j = bestMove
board[i][j] = turn
turn = p2
spotsLeft-=1
This calls evaluate which will give a numeric score that is higher the better it is for the given player (argument).
Add the definition for evaluate:
# winning lines
lines = [
[(i, j) for i in range(4)] for j in range(4)
] + [
[(j, i) for i in range(4)] for j in range(4)
] + [
[(i, i) for i in range(4)],
[(3-i, i) for i in range(4)]
]
def evaluate(board, player):
score = 0
for line in lines:
discs = [board[i][j] for i, j in line]
# The more discs in one line the better
value = [1000, 10, 6, 1, 0][sum(ch == "_" for ch in discs)]
if "X" in discs:
if not "O" in discs: # X could still win in this line
score += value
elif "O" in discs: # O could still win in this line
score -= value
# Change the sign depending on which player has just played
return score if player == "X" else -score
That's it! Optionally you can use the evaluate function to simplify the checkWin function:
def checkWin(board):
score = evaluate(board, "X")
if abs(score) > 500: # Take into account there could be some reduction
return "X" if score > 0 else "O"
# No One Won Yet
return -1
With this implementation you don't need the minimax function anymore, but you might want to keep it, and limit the search depth. When that depth is reached, make the call to evaluate, ...etc. Still I found the above implementation to play fine. You can't win a game against it.

Problem trying to implement 'Game of Life' in python as a beginner

So I am an absolute beginner in python and tried my hand at implementing Conway's game of life.
I am not using any libraries at all, my grid is just a 50x50 matrix of 1s and 0s.
The next_gen output I get is not matching the expected output but I couldn't figure out why, any help would be appreciated.
Here's my code:
def alive_neighbours(grid, r, c):
count = 0
if grid[r-1][c-1] == 1:
count += 1
if grid[r-1][c] == 1:
count += 1
if grid[r-1][c+1] == 1:
count += 1
if grid[r][c-1] == 1:
count += 1
if grid[r][c+1] == 1:
count += 1
if grid[r+1][c-1] == 1:
count += 1
if grid[r+1][c] == 1:
count += 1
if grid[r+1][c+1] == 1:
count += 1
return count
grid = [[0 for i in range(50)] for j in range(50)]
grid[25][25] = 1
grid[26][26] = 1
grid[27][24] = 1
grid[27][25] = 1
grid[27][26] = 1
grid[49][49] = 1
def next_gen(grid):
new_grid = grid[:]
for r in range(1, 49):
for c in range(1, 49):
neighbour = alive_neighbours(grid, r, c)
if (r == 0 or c == 0) or (r == 49 or c == 49):
pass # I am yet to define edge case
else:
if grid[r][c] == 1 and (neighbour > 3 or neighbour < 2):
new_grid[r][c] = 0
continue
elif grid[r][c] == 1 and (neighbour == 2 or 3):
continue
elif grid[r][c] == 0 and neighbour == 3:
new_grid[r][c] = 1
continue
else:
continue
grid = new_grid[:]
def printf(grid):
for r in range(50):
for c in range(50):
if grid[r][c] == 1:
print("*", end=" ")
else:
print(" ", end=" ")
print("")
x = 0
while x != '-1':
x = (input("x: "))
printf(grid)
next_gen(grid)
I also tried rewriting my next_gen function, but using that there is absolutely no change in the matrix
next_gen:
def next_gen(grid):
new_grid = grid[:]
for r in range(1, 49):
for c in range(1, 49):
neighbour = alive_neighbours(grid, r, c)
if (r == 0 or c == 0) or (r == 49 or c == 49):
pass
else:
if grid[r][c] == 1 and neighbour == 2 or 3:
continue
if grid[r][c] == 0 and neighbour == 3:
new_grid[r][c] = 1
continue
if grid[r][c] == 1:
new_grid[r][c] = 0
continue
grid = new_grid[:]
As bruno said in his answer there are a few issues in your code, He already told about your issue with grid and how allocating to it in the function actually points the local scope version to the new grid and not the global scope one. He also covers how to resolve this.
The other issue you will have is that you have undertood that just doing new_grid = grid will mean that new_grid and grid point at the same list. So to prevent this you have correctly done new_grid = grid[:] as this will create a new list in memory and copy the data from the grid list. However thats a shallow copy, so you will create a new list object but copy all the list references inside your list. we can demonstrate this by doing a shallow copy of a list and then changing a value in the new list.
grid_size = 2
grid = [[0 for i in range(grid_size)] for j in range(grid_size)]
new_grid = grid[:]
new_grid[1][1] = "R"
print("grid:", grid)
print("newg:", new_grid)
#output#
grid: [[0, 0], [0, 'R']]
newg: [[0, 0], [0, 'R']]
So you can see that changing the inner list in one will change the inner list in the other. so you need to do a deep copy of the list so that your not changing the original grid as you go. Since conways states are based on the original grid state and squares changing shouldnt impact the others. I think your already aware of this concept.
I also made a change to the alive neighbours to simplyfy it. Below is a quick draft adaptation. when you run it you should see your glider heading off to the bottom right corner
from copy import deepcopy
def alive_neighbours(grid, r, c):
differences = (0, -1, +1)
cells_in_square = [(r + a, c + b) for a in differences for b in differences]
total = 0
for x,y in cells_in_square[1:]:
try:
if x >=0 and y>=0:
total += grid[x][y]
except IndexError as ie:
pass #ignore index errors as at the edge of the grid
return total
def next_gen(grid):
new_grid = deepcopy(grid)
for r in range(len(grid)):
for c in range(len(grid)):
neighbour = alive_neighbours(grid, r, c)
if grid[r][c] == 1 and (neighbour > 3 or neighbour < 2):
new_grid[r][c] = 0
elif grid[r][c] == 0 and neighbour == 3:
new_grid[r][c] = 1
return new_grid
def printf(grid):
for r in grid:
for c in r:
if c == 1:
print("*", end=" ")
else:
print(" ", end=" ")
print("")
grid_size = 50
grid = [[0 for i in range(grid_size)] for j in range(grid_size)]
grid[25][25] = 1
grid[26][26] = 1
grid[27][24] = 1
grid[27][25] = 1
grid[27][26] = 1
grid[49][49] = 1
while True:
x = (input("press enter to see next grid: "))
if x:
break
printf(grid)
grid = next_gen(grid)
UPDATE
other then the glider you started with the below is a nice start for a cool exploder
grid_size = 50
grid = [[0 for i in range(grid_size)] for j in range(grid_size)]
grid[25][25] = 1
grid[26][24] = 1
grid[26][25] = 1
grid[26][26] = 1
grid[27][24] = 1
grid[27][26] = 1
grid[28][25] = 1
There are actually quite a few issues with your code, but the first and main problem is with your updated grid never being returned to the caller.
Here:
def next_gen(grid):
new_grid = grid[:]
# ...
# code modifying new_grid
# ...
grid = new_grid[:]
within the function, grid is a local name. Rebinding this name at the end of the function only affects the local name, it doesn't do anything to the global one. you should read this reference article for more in-depth explanations.
What you want is to return the grid to the caller instead:
def next_gen(grid):
new_grid = grid[:]
# ...
# code modifying new_grid
# ...
# return the new grid to the caller
return new_grid
x = 0
while x != '-1':
x = (input("x: "))
printf(grid)
# replace previous grid with the new one
grid = next_gen(grid)
For some other issues, this:
if grid[r][c] == 1 and neighbour == 2 or 3:
doesn't do what you think it does.
The neighbour == 2 or 3 part is actually executed as (neighbour == 2) or 3. Now the or operator returns either the first of it's operands that is not false, or the last of it's operand. Note that "that is not false" means "that does not have a false value in a boolean context" (all Python objects have a "truth" value, and for numbers, all numbers are true expect for zeros). So in the end, if the neighbours is different from 2, the value of neighbour == 2 or 3is3, whatever the value ofneighbour`:
>>> foo
1
>>> foo == 1 or 3
True
>>> foo == 2 or 3
3
>>>
And since 3 is true, the expression will have a true value even if neighbours is actually 1 or 4 or 5 or etc...
TL;DR: you want either:
`neighbour == 2 or neighbour == 3`
or more simply:
`neighbour in (2, 3)`

Function to find winning line with NxN board and M pieces in a row in Python 3

I am trying to create a function that finds if a move is winning on an NxN board where the winning condition is M pieces in a row in Python 3.
I am pretty new to programming and in my specific case I am creating a Gomoku game (15x15 board with 5 pieces in a row to win). To get it working I created 6 for loops to check vertical, horizontal and 4 diagonals. See the code below for examples on the 2 options for left to right digonals. This takes way too long though when I need to loop through it many times (8) for computer to find if I can win or if it has a winning move.
end_row = 15
for j in range(11):
end_row -= 1
counter = 0
for i in range(end_row):
if board[i+j][i] == board[i+1+j][i+1] and board[i+j][i] != ' ':
counter += 1
if counter == 4:
winning_line = [(i+j-3, i-3), (i+j-2, i-2), (i+j-1, i-1), (i+j, i), (i+1+j, i+1)]
winner = True
break
else:
counter = 0
# Top left to bottom right, lower side
end_row = 15
for j in range(11):
end_row -= 1
counter = 0
for i in range(end_row):
if board[i][i+j] == board[i+1][i+1+j] and board[i][i+j] != ' ':
counter += 1
if counter == 4:
winning_line = [(i-3, i+j-3), (i-2, i+j-2), (i-1, i+j-1), (i, i+j), (i+1, i+1+j)]
winner = True
break
else:
counter = 0
# What I want to do instead, where x and y are coordinates of last move:
# Horizontal
counter = 0
for i = x - (n - 1) to x + (n - 1):
if board[i][y] == board[x][y] :
counter++
else :
counter = 0
if counter == n:
return true
The problem with the lower part of the code is that if I place a piece on e.g. position (0, 0) the program will complain when trying to reach board[-4][0] in the first looping. I will have to place lots of if statements when I get close to the edge, which is not an elegant solution.
I thought of making a 3*15 x 3*15 board instead, where the actual board is the inner 15x15 part and the rest just contains placeholders:
15x15 || 15x15 || 15x15
15x15 || board || 15x15
15x15 || 15x15 || 15x15
This to avoid getting outside of my list of lists when looping through. Not an elegant solution either, but takes less space in the code.
Any suggestions on how to solve this problem? Thank you in advance from a beginner programmer!
As #MePsyDuck mentioned in comments, you can use min and max functions to limit the range to only reference valid squares in the board matrix.
Furthermore, you could make a generic function that does the count-job on any given list of values. Then you can call that generic function four times: once for every direction (horizontal, vertical, diagonal \ and diagonal /)
Here is how that could work:
def is_win(board, n, x, y):
end_row = len(board)
color = board[x][y]
def check(values):
counter = 0
for value in values:
if value == color:
counter += 1
else:
counter = 0
if counter == n:
return True
return False
return (check([board[i][y] for i in range(max(0, x - n + 1), min(end_row, x + n))])
or check([board[x][i] for i in range(max(0, y - n + 1), min(end_row, y + n))])
or check([board[x+i][y+i] for i in range(max(-x, -y, 1 - n), min(end_row - x, end_row - y, n))])
or check([board[x+i][y-i] for i in range(max(-x, y - end_row + 1, 1 - n), min(end_row - x, y + 1, n))]))
Instead of looping from 0 to 14, just loop from 0 to (board_size - winning_length).
Here's an example for a 1-dimensional board:
BOARD_SIZE = 15
WINNING_LENGTH = 5
for x in range(BOARD_SIZE - WINNING_LENGTH):
players_here = set()
for pos in range(x, x + WINNING_LENGTH):
players_here.add(board[pos])
if len(players_here) == 1:
# Exactly 1 player occupies every position in this line, so they win

How to fix cellular automata/spatial prisoners dilemma that is not replicating properly

I am trying to replicate the results of a paper (if you are interested, its Nowak & May, 1992: Evolutionary Games and Spatial Chaos) that create a set of fractals by running a prisoners dilemma on an n x n grid (for example,
https://www.researchgate.net/figure/Spatial-version-of-the-Prisoners-Dilemma-for-symmetrical-initial-conditions-Nowak_fig3_277476479), but my results are not what they should be.
The idea is that the grid is populated entirely by Cooperators, except for a single Defector object that is placed in the center of the grid. Different interactions yield different payoffs: mutual defectors yield a payoff of 0, mutual cooperators a payoff of 1 each, and a defector against a cooperator yields a payoff of b for the defector and 0 for the cooperator, where b > 1. All objects in the grid play against each other and receive a score according to the above payoff structure. After each generation, each object on a node is replaced by the neighbor with the highest score. Since the defector strategy is the superior strategy, it should invade the Cooperator population and produce said fractal images, as a cellular automata would.
The main way I have tried doing this (also the main area I have had trouble with) is through the replace_pop function shown below. After each round, the program loops through the grid and replaces any object on a node with a neighbour object that has a higher score. I thought that this would have been sufficient but as one can see after even a few generations, there is some form of replication but just not in the way it should happen, making it difficult to pinpoint what exactly is going wrong. At N = 1 (N is the number of generations) the result seems correct, as the neighbouring (neighbours are left, right, above and below) Cooperators become Defectors, but as N grows larger the image just goes astray.
I also reinitialized each objects score to 0 after each generation to ensure that proper replication can take place. When this is not done however, the population evolves in the same fashion as the N = 1 case above but for all subsequent generations, which is peculiar because there should be defectors that have higher scores than surrounding Cooperators. I am not sure where I am going wrong? My code is below (sorry for including all of it but I do not know where exactly is the problem). I am pretty new to Python and Stack so any help would be appreciated.
import random
import matplotlib.pyplot as plt
row = 99
col = 99
class Cooperator:
def __init__(self):
self.score = 0
self.id = 'C'
class Defector:
def __init__(self):
self.score = 0
self.id = 'D'
class Grid:
def __init__(self, rowsize, colsize):
self.rowsize = rowsize
self.colsize = colsize
def make_grid(self):
n = self.rowsize
m = self.colsize
arr = [[0 for j in range(m)] for i in range(n)]
return arr
def populate_grid(self):
empty_grid = self.make_grid()
for i in range(self.rowsize):
for j in range(self.colsize):
empty_grid[i][j] = Cooperator()
empty_grid[i//2][j//2] = Defector()
return empty_grid
def shuffle_population(self):
populated_grid = self.populate_grid()
for i in range(self.rowsize):
random.shuffle(populated_grid[i])
return populated_grid
def von_neumann_neighbourhood(array, row, col, wrapped=True):
"""gets von neumann neighbours for a specfic point on grid with or without wrapping"""
neighbours = []
#conditions for in bound points
if row + 1 <= len(array) - 1:
neighbours.append(array[row + 1][col])
if row - 1 >= 0:
neighbours.append(array[row - 1][col])
if col + 1 <= len(array[0]) - 1:
neighbours.append(array[row][col + 1])
if col - 1 >= 0:
neighbours.append(array[row][col - 1])
#if wrapped is on, conditions for out of bound points
if row - 1 < 0 and wrapped == True:
neighbours.append(array[-1][col])
if col - 1 < 0 and wrapped == True:
neighbours.append(array[row][-1])
if row + 1 > len(array) - 1 and wrapped == True:
neighbours.append(array[0][col])
if col + 1 > len(array[0]) - 1 and wrapped == True:
neighbours.append(array[row][0])
return neighbours
def play_round(array, row, col):
b = 1.70
player = array[row][col]
neighbours = von_neumann_neighbourhood(array, row, col)
for neighbour in neighbours:
if player.id == 'C' and neighbour.id == 'C':
player.score += 1
neighbour.score += 1
if player.id == 'D' and neighbour.id == 'D':
player.score += 0
neighbour.score += 0
if player.id == 'D' and neighbour.id == 'C':
player.score += b
neighbour.score += 0
if player.id == 'C' and neighbour.id == 'D':
player.score += 0
neighbour.score += b
def replace_pop(array, row, col):
neighbour_score = 0
type_neighbour = ""
neighbours = von_neumann_neighbourhood(array, row, col)
player_score = array[row][col].score
for neighbour in neighbours:
if neighbour.score > neighbour_score:
neighbour_score = neighbour.score
type_neighbour = neighbour.id
if player_score < neighbour_score:
if type_neighbour == "C":
array[row][col] = Cooperator()
if type_neighbour == "D":
array[row][col] = Defector()
N = 1
last_gen = []
def generations(N, row, col, array):
for gen in range(N):
for z in range(row):
for x in range(col):
play_round(array, z, x)
for r in range(row):
last_gen.append([])
for c in range(col):
last_gen[r].append(lattice[r][c].id)
replace_pop(array, r, c)
for obj in lattice:
for ob in obj:
ob.score = 0
lattice = Grid(row, col).populate_grid()
generations(N, row, col, lattice)
heatmap_stuff = []
for z in range(row):
heatmap_stuff.append([])
for v in range(col):
if lattice[z][v].id == 'C' and last_gen[z][v] == 'C':
heatmap_stuff[z].append(1)
if lattice[z][v].id == 'D' and last_gen[z][v] == 'D':
heatmap_stuff[z].append(0)
if lattice[z][v].id == 'C' and last_gen[z][v] == 'D':
heatmap_stuff[z].append(3)
if lattice[z][v].id == 'D' and last_gen[z][v] == 'C':
heatmap_stuff[z].append(4)
plt.imshow(heatmap_stuff, interpolation='nearest')
plt.colorbar()
plt.show()
Edit: I have updated the code in line with Ilmari's suggestions. Although the results look better, as well as returning an actual fractal in real-time, the results are still not optimal, leading me to think there might be a bug elsewhere since the cells seem to be updating correctly. Below is the updated code I have added/replaced to the previous code.
def get_moore_neighbours(grid, row, col):
neighbours = []
for x, y in (
(row - 1, col), (row + 1, col), (row, col - 1),
(row, col + 1), (row - 1, col - 1), (row - 1, col + 1),
(row + 1, col - 1), (row + 1, col + 1)):
if not (0 <= x < len(grid) and 0 <= y < len(grid[x])):
# out of bounds
continue
else:
neighbours.append(grid[x][y])
return neighbours
def calculate_score(grid, row, col):
b = 1.85
player = grid[row][col]
neighbours = get_moore_neighbours(grid, row, col)
for neighbour in neighbours:
if player.id == 'C' and neighbour.id == 'C':
player.score += 1
neighbour.score += 1
if player.id == 'D' and neighbour.id == 'D':
player.score += 0
neighbour.score += 0
if player.id == 'D' and neighbour.id == 'C':
player.score += b
neighbour.score += 0
if player.id == 'C' and neighbour.id == 'D':
player.score += 0
neighbour.score += b
return player.score
def best_neighbor_type(grid, row, col):
neighbour_score = 0
type_neighbour = ""
neighbours = get_moore_neighbours(grid, row, col)
player_score = grid[row][col].score
for neighbour in neighbours:
if neighbour.score > neighbour_score:
neighbour_score = neighbour.score
type_neighbour = neighbour.id
if player_score < neighbour_score:
if type_neighbour == "C":
return 'C'
if type_neighbour == "D":
return 'D'
if player_score >= neighbour_score:
return grid[row][col].id
N = 15
heatmap_data = Grid(row, col).make_grid()
lattice = Grid(row, col).populate_grid()
dbl_buf = Grid(row, col).populate_grid()
for gen in range(N):
for r in range(row):
for c in range(col):
lattice[r][c].score = calculate_score(lattice, r, c)
for r in range(row):
for c in range(col):
dbl_buf[r][c].id = best_neighbor_type(lattice, r, c)
for r in range(row):
for c in range(col):
if lattice[r][c].id == 'C' and dbl_buf[r][c].id == 'C':
heatmap_data[r][c] = 1
if lattice[r][c].id == 'D' and dbl_buf[r][c].id == 'D':
heatmap_data[r][c] = 2
if lattice[r][c].id == 'C' and dbl_buf[r][c].id == 'D':
heatmap_data[r][c] = 3
if lattice[r][c].id == 'D' and dbl_buf[r][c].id == 'C':
heatmap_data[r][c] = 4
plt.imshow(heatmap_data, interpolation='nearest')
plt.pause(0.01)
(lattice, dbl_buf) = (dbl_buf, lattice)
plt.show()
Looking at your code, a few issues jump out:
You never reset the last_gen array between generations, so you're constantly appending new (empty) rows to it and making the first row rows longer and longer. This is almost certainly a bug.
You also never use the last_gen array for anything except generating the heat map. In particular, your replace_pop() function is modifying the same array (creatively named array) that it reads the neighbor states from.
The second issue means that the behavior of your code will depend on the order in which you loop over the cells to call replace_pop() in each generation, since replacing one cell with a different neighbor will affect the neighborhood of all of its neighbors that haven't yet been updated in this generation.
In a cellular automaton like described in the paper you cite, all the cells are supposed to update their state effectively simultaneously, such that changes to each cell's state won't become visible to its neighbors until the next generation.
In practice, the simplest way to implement this kind of "simultaneous" updating is to use double buffering, where you first copy the state of all the cells into a second array, and then update the first array based on the copy you just made. Or, more efficiently, just swap the (references to) the arrays instead of copying one into the other. The code would look something like this:
lattice = Grid(row, col).populate_grid()
dbl_buf = Grid(row, col)
for gen in range(N):
for r in range(row):
for c in range(col):
lattice[r][c].score = calculate_score(lattice, r, c)
# This is probably the best spot for generating output, since both
# buffers contain consistent and up-to-date IDs and scores here.
for r in range(row):
for c in range(col):
dbl_buf[r][c].id = best_neighbor_type(lattice, r, c)
(lattice, dbl_buf) = (dbl_buf, lattice)
where the calculate_score() function returns the score of the given cell on the lattice based on the types of its neighbors, and the best_neighbor_id() function returns the type ID of the highest-scoring neighbor of the cell on the lattice.
Addendum: Your implementation of calculate_score() in your updated code has some bugs:
you start the calculations from the previous score value (which is actually from two generations back due to the double buffering),
you're redundantly writing the score directly to the grid inside the function, rather than just returning the score to the caller, and
you're also redundantly updating the scores of the cell's neighbors, leading to some interactions being effective counted twice.
However, the real reason why you're getting different results than in the Nowak & May paper is because of a conceptual difference: the paper assumes that cells also play the game with themselves, effectively giving cooperators a one point score boost. Your implementation doesn't include that, leading to different dynamics for the same parameter values.
Anyway, here's how I'd rewrite the function:
def calculate_score(grid, row, col):
neighbours = get_moore_neighbours(grid, row, col)
player = grid[row][col]
score = 0
if player.id == 'C': score += 1 # self-interaction!
for neighbour in neighbours:
if player.id == 'C' and neighbour.id == 'C':
score += 1
if player.id == 'D' and neighbour.id == 'C':
score += b
return score
With that change, your code produces very similar patterns as in the Nowak & May paper:
BTW, I'm not sure how Nowak & May handle the edges of the lattice, which might cause the patterns to diverge once they hit the edge. Your implementation effectively excludes any neighbors outside the edges from the score calculation, as if the lattice was surrounded by non-spreading defectors.

Categories

Resources