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.
Related
I'm currently learning BackTracking algorithms with Python and the first question everyone typically starts with is NQueens. NQueens is where you take a board of size N x N and you have to determine where to place N queens, in such an order they are not attacked by any other queen.
Example:
N = 5
['Q', 0, 0, 0, 0]
[0, 0, 'Q', 0, 0]
[0, 0, 0, 0, 'Q']
[0, 'Q', 0, 0, 0]
[0, 0, 0, 'Q', 0]
Currently, my algorithm returns ALL Possible Solutions. How do I produce ONE outcome. For instance, when N = 8, there are 92 optimal outcomes, how do I just return One Outcome instead of printing 92 separate Outcomes.
#This is our main recursive function that will determine optimal outcome
def NQueens(row,Current,N):
#this tells us when to stop
if row == N:
print("\n")
return printBoard(Current)
for choice in range(0,N):
if isValid(row,choice,Current,N):
#Place Queen in appropriate spot
Current[row][choice] = "Q"
#Go to next row
NQueens(row+1,Current,N)
Current[row][choice] = 0
return "All Solutions Found"
#This function determines whether we can put a Queen in a certain orientation on the board
def isValid(row,col,Current,N):
#check whether current state of game has queen in row/column
for i in range(0,N):
#checks column/row and sees whether a queen exists already
if Current[row][i] == "Q" or Current[i][col] == "Q":
return False
# Check upper diagonal on right side
i = row-1
j = col + 1
#Do while row is greater than 0 and column is less than N
while i >= 0 and j < N:
if Current[i][j] == "Q":
return False
i -= 1
j += 1
# Check upper diagonal on left side
#Do while row is greater than 0 and column is greater than N
i = row-1
j = col - 1
while i >= 0 and j >= 0:
if Current[i][j] == "Q":
return False
i -= 1
j -= 1
#If we pass the diagonal/row/column tests, we can then determine this is a valid move
return True
###############################################################################################################
#These functions deal with board creation and printing them in a proper format
def generateBoard(N):
#generate board based on User Input N in Main()
Board = [[0 for i in range(0,N)] for j in range(0,N)]
return Board
def printBoard(arr):
#Loop through each row to print an organized board
for row in arr:
print(row)
def main():
#ask number from user
print("What sized board would you like?"
" Please input a number higher that 3: ")
#user input used to determine size of board
N = int(input())
#generate Board that will be used
Board = generateBoard(N)
#this is the current status of the board
printBoard(Board)
print("\n")
#Runs Algorithm
print(NQueens(0,Board,N))
if __name__ == "__main__":
main()
NQueens needs to communicate to its caller that a solution has been found; a simple return True will do if that's the case. I made the following changes to your code (your previous lines are commented):
def NQueens(row,Current,N):
#this tells us when to stop
if row == N:
print("\n")
#return printBoard(Current)
return True # found a solution, say so
for choice in range(0,N):
if isValid(row,choice,Current,N):
#Place Queen in appropriate spot
Current[row][choice] = "Q"
#Go to next row
# NQueens(row+1,Current,N)
if NQueens(row+1,Current,N): # recursive call found a solution, let's stop
return True
Current[row][choice] = 0
#return "All Solutions Found"
And in main():
# ...
#Runs Algorithm
#print(NQueens(0,Board,N))
if NQueens(0,Board,N):
printBoard(Board) # found a solution, print it
I have fixed your code to have well-perfoming:
I merge this code https://www.geeksforgeeks.org/n-queen-problem-backtracking-3/ and yours to get it.
count = 1
def printOut(arrayMap):
global count
print('{}: -'.format(count))
count += 1
for i in range(0, len(arrayMap)):
print(arrayMap[i])
# This is our main recursive function that will determine optimal outcome
def NQueens(currentRow, arrayMap, matrixSize):
# this tells us when to stop
if currentRow == matrixSize:
print()
printOut(arrayMap)
return True # found a solution, say so
res = False
for col in range(0, matrixSize):
if isValid(currentRow, col, arrayMap, matrixSize):
# Place Queen in appropriate spot
arrayMap[currentRow][col] = 0
# Go to next row
# NQueens(row+1,Current,N)
res = NQueens(currentRow + 1, arrayMap, matrixSize) or res # recursive call found a solution, let's stop
arrayMap[currentRow][col] = 1
# return "All Solutions Found"
return res
# This function determines whether we can put a Queen in a certain orientation on the board
def isValid(row, col, arrayMap, matrixSize):
# check whether current state of game has queen in row/column
for i in range(0, matrixSize):
# checks column/row and sees whether a queen exists already
if arrayMap[row][i] == 0 or arrayMap[i][col] == 0:
return False
# Check upper diagonal on right side
i = row - 1
j = col + 1
# Do while row is greater than 0 and column is less than N
while i >= 0 and j < matrixSize:
if arrayMap[i][j] == 0:
return False
i -= 1
j += 1
# Check upper diagonal on left side
# Do while row is greater than 0 and column is greater than N
i = row - 1
j = col - 1
while i >= 0 and j >= 0:
if arrayMap[i][j] == 0:
return False
i -= 1
j -= 1
# If we pass the diagonal/row/column tests, we can then determine this is a valid move
return True
###############################################################################################################
if __name__ == "__main__":
# ask number from user
print("What sized board would you like?")
N = int(input('Choose your size: '))
# generate Board that will be used
Board = [[1 for i in range(0, N)] for j in range(0, N)]
count = 0
NQueens(0, Board, N)
I've started solving the 8 queens problem with backtracking in Python. Everything is nice & fine. It even printed out the first answer. However, it stuck itself on its first backtracking try.
The task sounded in that way:
Implement a Python function that solves the 8 queens puzzle. The 8 queen puzzle consists of placing 8 queens on a chess board, so that, none of the queens could capture any other. Note that queens can move orthogonally or diagonally in any direction.
You should implement a function solve() that when called, it prints the first solution of the puzzle and then it awaits for input. Once the user presses ‘enter’, the next solution is printed, and so on.
- Your program should be able to find all the solutions for the puzzle and each solution only once. '
- It should be easy to modify your program, so that, it works for different board sizes. Hints:
- In any row, there is exactly one queen. Hence, all you need to compute is the column in which each of the 8 queens can be placed.
- You should implement a recursive function solve(n) that finds a place for nth+1 the queen and then calls itself recursively for the n+1 queen (unless all the queens have been placed). It should systematically explore all the possibilities using backtracking.
- You are allowed (and encouraged) to define extra functions (other than solve() ) to improve the quality of your code if necessary.
import numpy as np
grid = np.zeros((8, 8), dtype = int)
def possible(y, n):
global solved
global grid
for i in range(0, 8):
if grid[y][i] == n:
return False
try:
for item in solved[str(y)]:
if grid[y].all() == item.all():
return False
except KeyError:
return True
return True
max_y = 7
max_x = 7
def print_grid():
global grid
for line in grid:
for square in line:
if square == 0:
print(".", end = " ")
else :
print("Q", end = " ")
print()
solved = {}
def prefilled_solved():
global solved
for i in range(0, len(grid[0])):
solved[f"{str(i)}"] = []
def solve(y=0):
global grid
global solved
while y < 8:
for x in range(0, 8):
if grid[y][x] == 0:
if possible(x, 1):
grid[y][x] = 1
solved[f"{str(y)}"].append(grid[y])
y += 1
solve(y)
#y -= 1 or y = 0 or y -=2
# backtracking - bad choice
# grid[y][x] = 0
print_grid()
print(grid)
return
input("More?")
if __name__ == '__main__':
prefilled_solved()
solve()
I've followed the #mkam advice, Now I've got the random constellation of Queen but I've got rid of recursion altogether.
```import numpy as np
grid = np.zeros((8, 8), dtype = int)
from random import randint, shuffle, choice
from itertools import permutations
constellations_drawn = []
def print_grid():
global grid
for line in grid:
for square in line:
if square == 0:
print(".", end = " ")
else :
print("Q", end = " ")
print()
solved = []
def prefilled_solved():
global solved
new_board = ['1', '2', '3', '4', '5', '6', '7', '8']
new_board_i = ''.join(new_board)
solved = permutations(new_board_i, 8)
def solve(y=0):
global grid
global solved
global constellations_drawn
list_solved = list(solved)
len_solved = len(list_solved)
board_drawn = list_solved[randint(0, len_solved-1)]
board_drawn_str = ''.join(board_drawn)
while board_drawn_str in constellations_drawn:
board_drawn = list_solved[randint(0, len_solved - 1)]
new_board_list = [int(item) for item in board_drawn]
for i, x in enumerate(new_board_list):
if grid[i-1][x-1] == 0:
grid[i-1][x-1] = 1
#y += 1
#solve(y)
#y -= 1 or y = 0 or y -=2
# backtracking - bad choice
# grid[y][x] = 0
constellations_drawn.append(board_drawn_str)
print_grid()
print(grid)
return
input("More?")
if __name__ == '__main__':
prefilled_solved()
solve()
I've merged the code of #mkam and mine. And it works. I still use numpy ndarray.
import numpy as np
from numpy.core._multiarray_umath import ndarray
def print_grid(solutions_found, board) -> None:
line: ndarray
len_board = len(board)
grid: ndarray = np.zeros((len_board, len_board), dtype=int)
for i, number in enumerate(board):
grid[i - 1][number - 1] = 1
for line in grid:
for square in line:
if square == 0:
print(".", end=" ")
else:
print("Q", end=" ")
print()
print(f'Solution - {solutions_found}')
def solve(boardsize, board=[], solutions_found=0):
if len(board) == boardsize:
solutions_found += 1
print_grid(solutions_found, board)
else:
for q in [col for col in range(1, boardsize + 1) if col not in board]:
if is_safe(q, board):
solutions_found = solve(boardsize, board + [q], solutions_found)
return solutions_found
def is_safe(q, board, x=1):
if not board:
return True
if board[-1] in [q + x, q - x]:
return False
return is_safe(q, board[:-1], x + 1)
if __name__ == '__main__':
solve(8)
This is an example of how the 8-Queens problem can be solved recursively, using a simple list to represent the board. A list such as [8, 4, 1, 3, 6, 2, 7, 5] represents the 8 rows of a chessboard from top to bottom, with a Q in the 8th column of the top row, the 4th column of the 7th row, the 1st column of the 6th row ... and the 5th column of the bottom row.
A solution is built starting with an empty board [] by placing a Q in the next row in a column position where it cannot be taken. Possible positions are columns which have not already been taken earlier (this is the for loop in function solve). For each of these possible column positions, function issafe checks whether the position is safe from being taken diagonally by the Qs already on the board. If the position is safe, the solution board is extended by another row and the solution recurses until the board is filled (len(board) == boardsize), at which point the solution count is incremented and the board is displayed.
Note that the function solve works for any size of square chessboard - the desired size is passed as a parameter to solve, and the function returns the total number of solutions found.
Hope this helps explain how the 8-Queens problem can be solved recursively WITHOUT numpy.
def display(solution_number, board):
row = '| ' * len(board) + '|'
hr = '+---' * len(board) + '+'
for col in board:
print(hr)
print(row[:col*4-3],'Q',row[col*4:])
print(f'{hr}\n{board}\nSolution - {solution_number}\n')
def issafe(q, board, x=1):
if not board: return True
if board[-1] in [q+x,q-x]: return False
return issafe(q, board[:-1], x+1)
def solve(boardsize, board=[], solutions_found=0):
if len(board) == boardsize:
solutions_found += 1
display(solutions_found, board)
else:
for q in [col for col in range(1,boardsize+1) if col not in board]:
if issafe(q,board):
solutions_found = solve(boardsize, board + [q], solutions_found)
return solutions_found
if __name__ == '__main__':
solutions = solve(8)
print(f'{solutions} solutions found')
You mention using yield - this is also possible, and will transform solve into a generator, producing one solution at a time. Your program can then use a for loop to receive each solution in turn and process it as required. The following yield solution works with Python v.3.3 onwards because it uses yield from:
def display(solution_number, board):
row = '| ' * len(board) + '|'
hr = '+---' * len(board) + '+'
for col in board:
print(hr)
print(row[:col*4-3],'Q',row[col*4:])
print(f'{hr}\n{board}\nSolution - {solution_number}\n')
def issafe(q, board, x=1):
if not board: return True
if board[-1] in [q+x,q-x]: return False
return issafe(q, board[:-1], x+1)
def solve(boardsize, board=[]):
if len(board) == boardsize:
yield board
else:
for q in [col for col in range(1,boardsize+1) if col not in board]:
if issafe(q,board):
yield from solve(boardsize, board + [q])
if __name__ == '__main__':
for solutionnumber, solution in enumerate(solve(8)):
display(solutionnumber+1, solution)
If the recursive function issafe appears confusing, here is a non-recursive version:
def issafe(q, board):
x = len(board)
for col in board:
if col in [q+x,q-x]: return False
x -= 1
return True
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.
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)
Here is my very rough implementation of Conway's Game of Life simulation.
LIVE = 1
DEAD = 0
def board(canvas, width, height, n):
for row in range(n+1):
for col in range(n+1):
canvas.create_rectangle(row*height/n,col*width/n,(row+1)*height/n,(col+1)*width/n,width=1,fill='black',outline='green')
n = int(raw_input("Enter the dimensions of the board: "))
width = n*25
height = n*25
from Tkinter import *
import math
window=Tk()
window.title('Game of Life')
canvas=Canvas(window,width=width,height=height,highlightthickness=0)
canvas.grid(row=0,column=0,columnspan=5)
board = [[DEAD for row in range(n)] for col in range(n)]
rect = [[None for row in range(n)] for col in range(n)]
for row in range(n):
for col in range(n):
rect[row][col] = canvas.create_rectangle(row*height/n,col*width/n,(row+1)*height/n,(col+1)*width/n,width=1,fill='black',outline='green')
#canvas.itemconfigure(rect[2][3], fill='red') #rect[2][3] is rectangle ID
#print rect
f = open('filename','r') #filename is whatever configuration file is chosen that gives the step() function to work off of for the first time
for line in f:
parsed = line.split()
print parsed
if len(parsed)>1:
row = int(parsed[0].strip())
col = int(parsed[1].strip())
board[row][col] = LIVE
board[row][col] = canvas.itemconfigure(rlist[row][col], fill='red')
def surrounding(row,col):
count = 0
if board[(row-1) % n][(col-1) % n] == LIVE:
count += 1
if board[(row-1) % n][col % n] == LIVE:
count += 1
if board[(row-1) % n][(col+1) % n] == LIVE:
count += 1
if board[row % n][(col-1) % n] == LIVE:
count += 1
if board[row % n][(col+1) % n] == LIVE:
count += 1
if board[(row+1) % n][(col-1) % n] == LIVE:
count +=1
if board[(row+1) % n ][col % n] == LIVE:
count += 1
if board[(row+1) % n][(col+1) % n] == LIVE:
count += 1
print count
return count
surrounding(1,1)
def round():
board_copy = board
for row in range(n):
for col in range(n):
if surrounding(row,col) == 3:
board_copy[row][col] = LIVE
board_copy[row][col] = canvas.itemconfigure(rect[row][col],fill='red')
elif surrounding(row,col) > 3 or getNeighbors(row,col) < 2:
board_copy[row][col] = DEAD
board_copy[row][col] = canvas.itemconfigure(rect[row][col],fill='black')
board = board_copy
def start():
global alarm
alarm = window.after(500,round)
def stop():
window.after.cancel(alarm)
So I have a function to count how many surrounding squares around a certain square had the value LIVE (where LIVE = 1).
Originally, all squares were initialized to DEAD. The configuration file determines which squares are assigned the value LIVE. Made a configuration file so that the 8 squares surrounding the square at 1,1 would return a value of 8, but I am getting back a value of 0 consistently, no matter the configuration or the tested square.
UPDATE: Changed the counter to begin at 1 and the return value was 1. I'm guessing my function isn't actually counting (or reading the values of the surrounding squares on the board), but I can't see why it would not.
Also, for my step() function, I need a function that makes a copy of the board and then changes the fill on the copy depending on a count being performed on the original board. If a rectangle on the original board has 3 rectangles with the values of LIVE, then the rectangle with the same indices on the board copy will have a LIVE value and turn red. Same for DEAD, except for different case. At the end the copied board (with changes) replaces the original board so that when we run the function again, it will be based off the copy.
I'm assuming board_copy = board is sufficient in making the copy; what do I write so that on the Tkinter window, after the function is performed, the board_copy is what appears?
The format of the config file is
rownum colnum
rownum colnum
So the config file I used if I wanted to get 8 in countNeighbors function (if it worked) for the first round would be:
0 0
0 1
0 2
1 0
1 2
2 0
2 1
2 2
I'm assuming board_copy = board is sufficient in making the copy;
No, board_copy = board is not sufficient. This will just bind the name board_copy to the exact same list of list as board, i.e. everything you change in board_copy is also changed in board.
Instead, you could use the deepcopy function from the copy module:
import copy
board_copy = copy.deepcopy(board)
Or use a list-comprehension to create a deep copy of the nested list:
board_copy = [[x for x in row] for row in board]
The use of board = board_copy at the end of the method is okay, since here you just want to bind the new board to board. Note, however, that you will also need to add global board at the top of the method to access the globally defined board.
Also, note that you define both a function board (def board(...): and a global variable board (board = [...]). The latter will shadow the first, i.e. you will not be able to use the board function. You should rename the function to e.g. def draw_board(...).
There might be more problems with your code, but I can not execute it sinceit seems to be incomplete (where is start called?) and I do not know the content of 'filename'.