Abstract Collision Detection - python

I've been struggling conceptually with how to implement simple square collision detection within a game I am writing while avoiding Pygame; I want to learn how to do it without cheating. The structure of the program as intended looks is this:
The game loads a text file containing a level. Each level consists of 25 rows of 25 digits (for a total of 625 digits). It is extracted into a 2D array to emulate a cartesian grid which will correspond with the screen. From there the program draws a 32x32 block at the proper place on the screen. For example, if the digit at location [2][5] is a 1, it will draw a white square at pixel coordinate (96,192) (the counting of the squares starts at zero since it is an array). It also generates a collision array consisting of True or False for each location corresponding to the original array.
I have a player object that moves freely along the grid, not confined to the 32x32 squares. My question is this: how would I implement square collision detection? I've tried a number of methods but I'm not quite sure where I'm getting stuck. I'll post my latest incarnation and the relevant code below.
Collision code:
def checkPlayerEnvCollision(self,player):
p = player
c = self.cLayer #this is the collision grid generated when loading the level
for row in range(25):
for col in range (25):
print("checkEnvCollision")
if c[row][col] != False:
tileleftx = row*32
tilerightx = tileleftx + 32
tilelefty = col*32
tilerighty = tilelefty+32
if (abs(tileleftx - p.x) * 2 < (tilerightx + (p.x + 32))) and (abs(tilelefty - p.y) * 2 < (tilerighty + (p.y + 32))):
print("OH NO, COLLISION")
The code that loads the tiles from the text file into the array:
def loadLevel(self, level):
print("Loading Level")
levelFile = open(level)
count=0
for line in levelFile:
tempArray = []
if line.startswith("|"):
dirs = line.split('|')
self.north = dirs[1]
self.south = dirs[2]
self.east = dirs[3]
self.west = dirs[4]
continue
for item in line:
if item in self.tileValues:
tempArray.append(int(item))
self.tileLayer[count] = tempArray
count+=1
for items in self.tileLayer:
if len(items) > 25:
print("Error: Loaded Level Too Large")
count = 0
for line in self.tileLayer:
tempArray = []
for item in line:
if self.tilePassableValues[item] == False:
tempArray.append(False)
else:
tempArray.append(True)
self.collisionLayer[count] = tempArray
count += 1
Not sure if this is useful, but here is a simple demonstration of the drawing method:
def levelTiles(self, level):
row = 0
for t in level:
col = 0
for r in t:
color = "white"
if r == 0:
col+=1
continue
elif r == 1:
color = "red"
elif r == 2:
color = "white"
elif r == 3:
color = "green"
self.Canvas.create_rectangle(row*32, col*32, row*32+32, col*32+32, fill=color, width=1,tags='block')
col += 1
row += 1
Lastly, here is the text file I have been testing it with:
1111111111111111111111111
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222233332222222222222221
1222233332222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222233332222222221
1222222222333332332222221
1222222222222222332222221
1222222222222222332222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1222222222222222222222221
1111111111111111111111111
|onescreen2|onescreen2|onescreen2|onescreen2
(The last line is what will load the map to the north, south, east and west when reaching the edge of the level; you can ignore it.)
Thanks for the help. It's a lot to ask, but I'm stuck on this one!

If the player is tied to the grid, why not just test the grid positions:
if grid[player.x][player.y] == some_collidable_thing:
# there was a collision
If not,
I also provided an answer to something almost identical in This question
def check_col(self, rect):
for row in self.cLayer:
for column in row:
grid_position = (row*element_size, column*element_width)
collide_x = False
collide_y = False
# check x axis for collision
if self.rect.x + self.rect.w > grid_position[0]:
collide_x = True
elif self.rect.x < grid_position[0] + element_width:
collide_x = True
# check y axis for collision
if self.rect.y < grid_position[1] + element_height:
collide_y = True
elif self.rect.y + self.rect.h > grid_position[1]:
collide_y = True
# act on a collision on both axis
if collide_x and collide_y:
# act on the collision
return True
else:
# act on no collision
return False

An easier way to do this would be to define vectors for the player's movement, and lines for the boundaries of the objects. Then you check to see if the vector collides with any line (there should not be many lines to check) as follows (I'm assuming that the player/object can be on the boundary of the other object):
Take the determinant of the triangle formed by the movement vector and endpoint of the line you're checking for a collision, and take its area via determinant. Compare its area to the area of the triangle formed with the other endpoint. If they are both positive/negative, then there is no intersection. If their signs are different, then there MIGHT be an intersection.
If their signs are different, do the same thing as above, except using the endpoints of the movement vector instead of the endpoints of the line. (And using the whole line instead of the movement vector).
If their signs are different, then there is definitely an intersection, and if they are the same, then there is no intersection.
I hope this helps (and just comment if it does not make sense).

Related

How to properly add gradually increasing/decreasing space between objects?

I've trying to implement transition from an amount of space to another which is similar to acceleration and deceleration, except i failed and the only thing that i got from this was this infinite stack of mess, here is a screenshot showing this in action:
you can see a very black circle here, which are in reality something like 100 or 200 circles stacked on top of each other
and i reached this result using this piece of code:
def Place_circles(curve, circle_space, cs, draw=True, screen=None):
curve_acceleration = []
if type(curve) == tuple:
curve_acceleration = curve[1][0]
curve_intensity = curve[1][1]
curve = curve[0]
#print(curve_intensity)
#print(curve_acceleration)
Circle_list = []
idx = [0,0]
for c in reversed(range(0,len(curve))):
for p in reversed(range(0,len(curve[c]))):
user_dist = circle_space[curve_intensity[c]] + curve_acceleration[c] * p
dist = math.sqrt(math.pow(curve[c][p][0] - curve[idx[0]][idx[1]][0],2)+math.pow(curve [c][p][1] - curve[idx[0]][idx[1]][1],2))
if dist > user_dist:
idx = [c,p]
Circle_list.append(circles.circles(round(curve[c][p][0]), round(curve[c][p][1]), cs, draw, screen))
This place circles depending on the intensity (a number between 0 and 2, random) of the current curve, which equal to an amount of space (let's say between 20 and 30 here, 20 being index 0, 30 being index 2 and a number between these 2 being index 1).
This create the stack you see above and isn't what i want, i also came to the conclusion that i cannot use acceleration since the amount of time to move between 2 points depend on the amount of circles i need to click on, knowing that there are multiple circles between each points, but not being able to determine how many lead to me being unable to the the classic acceleration formula.
So I'm running out of options here and ideas on how to transition from an amount of space to another.
any idea?
PS: i scrapped the idea above and switched back to my master branch but the code for this is still available in the branch i created here https://github.com/Mrcubix/Osu-StreamGenerator/tree/acceleration .
So now I'm back with my normal code that don't possess acceleration or deceleration.
TL:DR i can't use acceleration since i don't know the amount of circles that are going to be placed between the 2 points and make the time of travel vary (i need for exemple to click circles at 180 bpm of one circle every 0.333s) so I'm looking for another way to generate gradually changing space.
First, i took my function that was generating the intensity for each curves in [0 ; 2]
Then i scrapped the acceleration formula as it's unusable.
Now i'm using a basic algorithm to determine the maximum amount of circles i can place on a curve.
Now the way my script work is the following:
i first generate a stream (multiple circles that need to be clicked at high bpm)
this way i obtain the length of each curves (or segments) of the polyline.
i generate an intensity for each curve using the following function:
def generate_intensity(Circle_list: list = None, circle_space: int = None, Args: list = None):
curve_intensity = []
if not Args or Args[0] == "NewProfile":
prompt = True
while prompt:
max_duration_intensity = input("Choose the maximum amount of curve the change in intensity will occur for: ")
if max_duration_intensity.isdigit():
max_duration_intensity = int(max_duration_intensity)
prompt = False
prompt = True
while prompt:
intensity_change_odds = input("Choose the odds of occurence for changes in intensity (1-100): ")
if intensity_change_odds.isdigit():
intensity_change_odds = int(intensity_change_odds)
if 0 < intensity_change_odds <= 100:
prompt = False
prompt = True
while prompt:
min_intensity = input("Choose the lowest amount of spacing a circle will have: ")
if min_intensity.isdigit():
min_intensity = float(min_intensity)
if min_intensity < circle_space:
prompt = False
prompt = True
while prompt:
max_intensity = input("Choose the highest amount of spacing a circle will have: ")
if max_intensity.isdigit():
max_intensity = float(max_intensity)
if max_intensity > circle_space:
prompt = False
prompt = True
if Args:
if Args[0] == "NewProfile":
return [max_duration_intensity, intensity_change_odds, min_intensity, max_intensity]
elif Args[0] == "GenMap":
max_duration_intensity = Args[1]
intensity_change_odds = Args[2]
min_intensity = Args[3]
max_intensity = Args[4]
circle_space = ([min_intensity, circle_space, max_intensity] if not Args else [Args[0][3],circle_space,Args[0][4]])
count = 0
for idx, i in enumerate(Circle_list):
if idx == len(Circle_list) - 1:
if random.randint(0,100) < intensity_change_odds:
if random.randint(0,100) > 50:
curve_intensity.append(2)
else:
curve_intensity.append(0)
else:
curve_intensity.append(1)
if random.randint(0,100) < intensity_change_odds:
if random.randint(0,100) > 50:
curve_intensity.append(2)
count += 1
else:
curve_intensity.append(0)
count += 1
else:
if curve_intensity:
if curve_intensity[-1] == 2 and not count+1 > max_duration_intensity:
curve_intensity.append(2)
count += 1
continue
elif curve_intensity[-1] == 0 and not count+1 > max_duration_intensity:
curve_intensity.append(0)
count += 1
continue
elif count+1 > 2:
curve_intensity.append(1)
count = 0
continue
else:
curve_intensity.append(1)
else:
curve_intensity.append(1)
curve_intensity.reverse()
if curve_intensity.count(curve_intensity[0]) == len(curve_intensity):
print("Intensity didn't change")
return circle_space[1]
print("\n")
return [circle_space, curve_intensity]
with this, i obtain 2 list, one with the spacing i specified, and the second one is the list of randomly generated intensity.
from there i call another function taking into argument the polyline, the previously specified spacings and the generated intensity:
def acceleration_algorithm(polyline, circle_space, curve_intensity):
new_circle_spacing = []
for idx in range(len(polyline)): #repeat 4 times
spacing = []
Length = 0
best_spacing = 0
for p_idx in range(len(polyline[idx])-1): #repeat 1000 times / p_idx in [0 ; 1000]
# Create multiple list containing spacing going from circle_space[curve_intensity[idx-1]] to circle_space[curve_intensity[idx]]
spacing.append(np.linspace(circle_space[curve_intensity[idx]],circle_space[curve_intensity[idx+1]], p_idx).tolist())
# Sum distance to find length of curve
Length += abs(math.sqrt((polyline[idx][p_idx+1][0] - polyline[idx][p_idx][0]) ** 2 + (polyline [idx][p_idx+1][1] - polyline[idx][p_idx][1]) ** 2))
for s in range(len(spacing)): # probably has 1000 list in 1 list
length_left = Length # Make sure to reset length for each iteration
for dist in spacing[s]: # substract the specified int in spacing[s]
length_left -= dist
if length_left > 0:
best_spacing = s
else: # Since length < 0, use previous working index (best_spacing), could also jsut do `s-1`
if spacing[best_spacing] == []:
new_circle_spacing.append([circle_space[1]])
continue
new_circle_spacing.append(spacing[best_spacing])
break
return new_circle_spacing
with this, i obtain a list with the space between each circles that are going to be placed,
from there, i can Call Place_circles() again, and obtain the new stream:
def Place_circles(polyline, circle_space, cs, DoDrawCircle=True, surface=None):
Circle_list = []
curve = []
next_circle_space = None
dist = 0
for c in reversed(range(0, len(polyline))):
curve = []
if type(circle_space) == list:
iter_circle_space = iter(circle_space[c])
next_circle_space = next(iter_circle_space, circle_space[c][-1])
for p in reversed(range(len(polyline[c])-1)):
dist += math.sqrt((polyline[c][p+1][0] - polyline[c][p][0]) ** 2 + (polyline [c][p+1][1] - polyline[c][p][1]) ** 2)
if dist > (circle_space if type(circle_space) == int else next_circle_space):
dist = 0
curve.append(circles.circles(round(polyline[c][p][0]), round(polyline[c][p][1]), cs, DoDrawCircle, surface))
if type(circle_space) == list:
next_circle_space = next(iter_circle_space, circle_space[c][-1])
Circle_list.append(curve)
return Circle_list
the result is a stream with varying space between circles (so accelerating or decelerating), the only issue left to be fixed is pygame not updating the screen with the new set of circle after i call Place_circles(), but that's an issue i'm either going to try to fix myself or ask in another post
the final code for this feature can be found on my repo : https://github.com/Mrcubix/Osu-StreamGenerator/tree/Acceleration_v02

Python Negamax Algorithm

I have about as simple of a negamax algorithm as possible, for evaluating positions in Tic Tac Toe. The state of the game is stored as an array in numpy, with X's pieces represented by 1, and O's pieces represented by four.
I was testing this just now, and found:
a = np.zeros(9).reshape(3,3)
negaMax(a, 6, 1) # Returned zero as it should
negaMax(a, 7, 1) # Returns 100
Meaning that my algorithm thinks it has found a way for X to win in seven plies in a game of Tic Tac Toe, which is obviously impossible against decent play. I can't work out how to have it print the best moves it has found, so am having real trouble debugging this. What am I doing wrong?
def winCheck(state):
"""Takes a position, and returns the outcome of that game"""
# Sums which correspond to a line across a column
winNums = list(state.sum(axis=0))
# Sums which correspond to a line across a row
winNums.extend(list(state.sum(axis=1)))
# Sums which correspond to a line across the main diagonal
winNums.append(state.trace())
# Sums which correspond to a line across the off diagonal
winNums.append(np.flipud(state).trace())
if Square.m in winNums:
return 'X'
elif (Square.m**2 + Square.m) in winNums:
return 'O'
elif np.count_nonzero(state) == Square.m**2:
return 'D'
else:
return None
def moveFind(state):
"""Takes a position as an nparray and determines the legal moves"""
moveChoices = []
# Iterate over state, to determine which squares are empty
it = np.nditer(state, flags=['multi_index'])
while not it.finished:
if it[0] == 0:
moveChoices.append(it.multi_index)
it.iternext()
return moveChoices
def moveSim(state, move, player):
"""Create the state of the player having moved without interfering with the board"""
simState = state.copy()
if player == 1:
simState[move] = 1
else:
simState[move] = gamecfg.n + 1
return simState
def positionScore(state):
"""The game is either won or lost"""
if winCheck(state) == 'X':
return 100
elif winCheck(state) == 'O':
return -100
else:
return 0
def negaMax(state, depth, colour):
"""Recursively find the best move via a negamax search"""
if depth == 0:
return positionScore(state) * colour
highScore = -100
moveList = moveFind(state)
for move in moveList:
score = -negaMax(moveSim(state, move, colour), depth -1, colour * -1)
highScore = max(score, highScore)
return highScore
Your code does not consider the game to stop when a line of 3 symbols is made.
This means that it is playing a variant of tic-tac-toe where X wins if he makes a line of 3 even after O has made a line of 3.
For this variant, the program has correctly found that it is possible for X to always win!
(I came across the same situation with a chess program I made where the computer was happy to sacrifice its king if it would reach checkmate a little later...)

Efficient quadtree implementation in python

For a project I'm working on I'm trying to write some code to detect collisions between non-point particles in a 2D space. My goal is to try to detect collision for a few thousand particles at least a few times per time step which I know is a tall order for python. I've followed this blog post which implements a quadtree to significantly reduce the number pairwise checks I need to make. So where I believe I'm running into issues is this function:
def get_index(self, particle):
index = -1
bounds = particle.aabb
v_midpoint = self.bounds.x + self.bounds.width/2
h_midpoint = self.bounds.y + self.bounds.height/2
top_quad = bounds.y < h_midpoint and bounds.y + bounds.height < h_midpoint
bot_quad = bounds.y > h_midpoint
if bounds.x < v_midpoint and bounds.x + bounds.width < v_midpoint:
if top_quad:
index = 1
elif bot_quad:
index = 2
elif bounds.x > v_midpoint:
if top_quad:
index = 0
elif bot_quad:
index = 3
return index
This function from my initial profiling is the bottleneck and I need it to be blistering fast, because of its high call count. Originally I was just supplying an object axis-aligned bounding box which was working almost at the speed I needed, then realized I had no way of determining which particles may actually be colliding. So now I'm passing in a list of particles to my quadtree constructor and just using the class attribute aabb to get my bounds.
Is there someway I could pass something analogues to a object pointer instead of the whole object? Additionally are there other recommendation to optimize this above code?
Don't know if they'll help, but here are a few ideas:
v_midpoint and h_midpoint are re-calculated for every particle added to the quadtree. Instead, calculate them once when a Quad is initialized, then access them as attributes.
I don't think the and is needed in calculating top_quad. bounds.x + bounds.width < v_midpoint is sufficient. Same for left_quad.
Do the simpler checks first and only do the longer one if necessary: bounds.x > v_midpoint vs. bounds.x + bounds.width < v_midpoint
bounds.x + bounds.width is calculated multiple times for most particles. Maybe bounds.left and bounds.right can be calculated once as attributes of each particle.
No need to calculate bot_quad if top_quad is True. Or visa-versa.
Maybe like this:
def get_index(self, particle):
bounds = particle.aabb
# right
if bounds.x > self.v_midpoint:
# bottom
if bounds.y > self.h_midpoint:
return 3
# top
elif bounds.y + bounds.height < self.h_midpoint:
return 0
# left
elif bounds.x + bounds.width < self.v_midpoint:
# bottom
if bounds.y > self.h_midpoint:
return 2
# top
elif bounds.y + bounds.height < self.h_midpoint:
return 1
return -1

How to deal with very big Bitboards

I'm working on a 2-player board game (e.g. connect 4), with parametric board size h, w. I want to check for winning condition using hw-sized bitboards.
In game like chess, where board size is fixed, bitboards are usually represented with some sort of 64-bit integer. When h and w are not constant and maybe very big (let's suppose 30*30) are bitboards a good idea? If so, are the any data types in C/C++ to deal with big bitboards keeping their performances?
Since I'm currently working on python a solution in this language is appreciated too! :)
Thanks in advance
I wrote this code while ago just to play around with the game concept. There is no intelligence behaviour involve. just random moves to demonstrate the game. I guess this is not important for you since you are only looking for a fast check of winning conditions. This implementation is fast since I did my best to avoid for loops and use only built-in python/numpy functions (with some tricks).
import numpy as np
row_size = 6
col_size = 7
symbols = {1:'A', -1:'B', 0:' '}
def was_winning_move(S, P, current_row_idx,current_col_idx):
#****** Column Win ******
current_col = S[:,current_col_idx]
P_idx= np.where(current_col== P)[0]
#if the difference between indexes are one, that means they are consecutive.
#we need at least 4 consecutive index. So 3 Ture value
is_idx_consecutive = sum(np.diff(P_idx)==1)>=3
if is_idx_consecutive:
return True
#****** Column Win ******
current_row = S[current_row_idx,:]
P_idx= np.where(current_row== P)[0]
is_idx_consecutive = sum(np.diff(P_idx)==1)>=3
if is_idx_consecutive:
return True
#****** Diag Win ******
offeset_from_diag = current_col_idx - current_row_idx
current_diag = S.diagonal(offeset_from_diag)
P_idx= np.where(current_diag== P)[0]
is_idx_consecutive = sum(np.diff(P_idx)==1)>=3
if is_idx_consecutive:
return True
#****** off-Diag Win ******
#here 1) reverse rows, 2)find new index, 3)find offest and proceed as diag
reversed_rows = S[::-1,:] #1
new_row_idx = row_size - 1 - current_row_idx #2
offeset_from_diag = current_col_idx - new_row_idx #3
current_off_diag = reversed_rows.diagonal(offeset_from_diag)
P_idx= np.where(current_off_diag== P)[0]
is_idx_consecutive = sum(np.diff(P_idx)==1)>=3
if is_idx_consecutive:
return True
return False
def move_at_random(S,P):
selected_col_idx = np.random.permutation(range(col_size))[0]
#print selected_col_idx
#we should fill in matrix from bottom to top. So find the last filled row in col and fill the upper row
last_filled_row = np.where(S[:,selected_col_idx] != 0)[0]
#it is possible that there is no filled array. like the begining of the game
#in this case we start with last row e.g row : -1
if last_filled_row.size != 0:
current_row_idx = last_filled_row[0] - 1
else:
current_row_idx = -1
#print 'col[{0}], row[{1}]'.format(selected_col,current_row)
S[current_row_idx, selected_col_idx] = P
return (S,current_row_idx,selected_col_idx)
def move_still_possible(S):
return not (S[S==0].size == 0)
def print_game_state(S):
B = np.copy(S).astype(object)
for n in [-1, 0, 1]:
B[B==n] = symbols[n]
print B
def play_game():
#initiate game state
game_state = np.zeros((6,7),dtype=int)
player = 1
mvcntr = 1
no_winner_yet = True
while no_winner_yet and move_still_possible(game_state):
#get player symbol
name = symbols[player]
game_state, current_row, current_col = move_at_random(game_state, player)
#print '******',player,(current_row, current_col)
#print current game state
print_game_state(game_state)
#check if the move was a winning move
if was_winning_move(game_state,player,current_row, current_col):
print 'player %s wins after %d moves' % (name, mvcntr)
no_winner_yet = False
# switch player and increase move counter
player *= -1
mvcntr += 1
if no_winner_yet:
print 'game ended in a draw'
player = 0
return game_state,player,mvcntr
if __name__ == '__main__':
S, P, mvcntr = play_game()
let me know if you have any question
UPDATE: Explanation:
At each move, look at column, row, diagonal and secondary diagonal that goes through the current cell and find consecutive cells with the current symbol. avoid scanning the whole board.
extracting cells in each direction:
column:
current_col = S[:,current_col_idx]
row:
current_row = S[current_row_idx,:]
Diagonal:
Find the offset of the desired diagonal from the
main diagonal:
diag_offset = current_col_idx - current_row_idx
current_diag = S.diagonal(offset)
off-diagonal:
Reverse the rows of matrix:
S_reversed_rows = S[::-1,:]
Find the row index in the new matrix
new_row_idx = row_size - 1 - current_row_idx
current_offdiag = S.diagonal(offset)

Unexpected results in Conway's Game of Life

I have been trying to write my own version of Conway's Game of Life as practice for Python using Pygame. I first wrote the functions for initializing the game field, and then calculating the next generation. I verified it's functionality using the console to print the results and verify that they returned the expected results (just 2 generations deep by hand on a 5x5 grid).
An important note of how I am calculating the neighbors... Instead of doing a for loop through the entire array and doing for loops to count for each cell, I have implemented an array that holds the neighbor counts. Only making changes when a cells status is changed. This means I don't waste time calculating neighbors for cells that have not changed.
When it came time to use Pygame to display the array with rectangles, I wrote the following program. At first I was drawing the screen by filling in the entire screen white, and then drawing the live cells as black (this can be done by commenting out the else statement in update()). I expected this to work as normal, but when I ran the program all I ended up with was the screen filling in black.
I was perplexed by the result so i drew white rectangles for the unpopulated cells (using the else statement. And got a better looking result, but instead of the cells eventually all dying, they eventually multiplied across the whole screen. This is opposite of what I expected, as I was expecting it to eventually stabilize.
Anyone know what I am doing wrong? I know that this is not the best way of writing this program, I welcome comments of how I can make it better.
RETURN = run simulation
'R' = randomize
'T' = tick one generation
'C' = clear game field
'N' = display neighbor map
import pygame
from pygame.locals import *
import numpy as np
from random import *
import copy
fieldSize = [100,50]
cellSize = 10 # size of >10 is recommended to see neighbor count
windowSize = [fieldSize[0]*cellSize, fieldSize[1]*cellSize]
# calculate the last cell in each axis so it is not done repeatedly
lastCell = [(fieldSize[0]-1), (fieldSize[1]-1)]
dX = float(windowSize[0])/float(fieldSize[0])
dY = float(windowSize[1])/float(fieldSize[1])
colorAlive = [0,125,0]
colorDead = [0, 0, 0]
# todo list
# 1. make cLife take in the field size
# 2. convert random functions to numpy.random.randint
class cLife():
def randomize(self):
self.neighbors = np.zeros(fieldSize)
# fill in the game field with random numbers
for x in range(fieldSize[0]):
for y in range(fieldSize[1]):
if(randint(0,99)<20):
self.gameField[x][y] = 1
self.updateNeighbors([x,y], True)
else:
self.gameField[x][y] = 0
def displayNeighbors(self, surface):
self.drawField(surface)
for x in range(fieldSize[0]):
for y in range(fieldSize[1]):
neighborCount=font.render(str(int(self.neighbors[x][y])), 1,(200,200,200))
surface.blit(neighborCount, (x*dX+dX/3, y*dY+dY/3.5))
pygame.display.flip()
# This is the function to update the neighbor map, the game field is torroidal so the complexity is greatly
# increased. I have handcoded each instruction to avoid countless if statements and for loops.
# Hopefully, this has drastically improved the performance. Using this method also allows me to avoid calculating
# the neighbor map for every single cell because the neighbor map is updated only for the cells affected by a change.
def updateNeighbors(self, pos, status):
if(status == True):
change = 1
else:
change = -1
# testing for the cells in the center of the field (most cells are in the center so this is first)
# cells are filled in starting on the top-left corner going clockwise
if((pos[0]>0 and pos[0]<lastCell[0])and(pos[1]>0 and pos[1]<lastCell[1])):
self.neighbors[pos[0]-1][pos[1]-1] += change
self.neighbors[pos[0]][pos[1]-1] += change
self.neighbors[pos[0]+1][pos[1]-1] += change
self.neighbors[pos[0]+1][pos[1]] += change
self.neighbors[pos[0]+1][pos[1]+1] += change
self.neighbors[pos[0]][pos[1]+1] += change
self.neighbors[pos[0]-1][pos[1]+1] += change
self.neighbors[pos[0]-1][pos[1]] += change
elif(pos[0] == 0): # left edge
if(pos[1] == 0): # top left corner
self.neighbors[lastCell[0]][lastCell[1]] += change
self.neighbors[0][lastCell[1]] += change
self.neighbors[1][lastCell[1]] += change
self.neighbors[1][0] += change
self.neighbors[1][1] += change
self.neighbors[0][1] += change
self.neighbors[lastCell[0]][1] += change
self.neighbors[lastCell[0]][0] += change
elif(pos[1] == lastCell[1]): # bottom left corner
self.neighbors[lastCell[0]][pos[1]-1] += change
self.neighbors[0][pos[1]-1] += change
self.neighbors[1][pos[1]-1] += change
self.neighbors[1][pos[1]] += change
self.neighbors[1][0] += change
self.neighbors[0][0] += change
self.neighbors[lastCell[0]][0] += change
self.neighbors[lastCell[0]][pos[1]] += change
else: # everything else
self.neighbors[lastCell[0]][pos[1]-1] += change
self.neighbors[0][pos[1]-1] += change
self.neighbors[1][pos[1]-1] += change
self.neighbors[1][pos[1]] += change
self.neighbors[1][pos[1]+1] += change
self.neighbors[0][pos[1]+1] += change
self.neighbors[lastCell[0]][pos[1]+1] += change
self.neighbors[lastCell[0]][pos[1]] += change
elif(pos[0] == lastCell[0]): # right edge
if(pos[1] == 0): # top right corner
self.neighbors[pos[0]-1][lastCell[1]] += change
self.neighbors[pos[0]][lastCell[1]] += change
self.neighbors[0][lastCell[1]] += change
self.neighbors[0][0] += change
self.neighbors[0][1] += change
self.neighbors[pos[0]][1] += change
self.neighbors[pos[0]-1][1] += change
self.neighbors[pos[0]-1][0] += change
elif(pos[1] == lastCell[1]): # bottom right corner
self.neighbors[pos[0]-1][pos[1]-1] += change
self.neighbors[pos[0]][pos[1]-1] += change
self.neighbors[0][pos[1]-1] += change
self.neighbors[0][pos[1]] += change
self.neighbors[0][0] += change
self.neighbors[pos[0]][0] += change
self.neighbors[pos[0]-1][0] += change
self.neighbors[pos[0]-1][pos[1]] += change
else: # everything else
self.neighbors[pos[0]-1][pos[1]-1] += change
self.neighbors[pos[0]][pos[1]-1] += change
self.neighbors[0][pos[1]-1] += change
self.neighbors[0][pos[1]] += change
self.neighbors[0][pos[1]+1] += change
self.neighbors[pos[0]][pos[1]+1] += change
self.neighbors[pos[0]-1][pos[1]+1] += change
self.neighbors[pos[0]-1][pos[1]] += change
elif(pos[1] == 0): # top edge, corners already taken care of
self.neighbors[pos[0]-1][lastCell[1]] += change
self.neighbors[pos[0]][lastCell[1]] += change
self.neighbors[pos[0]+1][lastCell[1]] += change
self.neighbors[pos[0]+1][0] += change
self.neighbors[pos[0]+1][1] += change
self.neighbors[pos[0]][1] += change
self.neighbors[pos[0]-1][1] += change
self.neighbors[pos[0]-1][0] += change
elif(pos[1] == lastCell[1]): # bottom edge, corners already taken care of
self.neighbors[pos[0]-1][pos[1]-1] += change
self.neighbors[pos[0]][pos[1]-1] += change
self.neighbors[pos[0]+1][pos[1]-1] += change
self.neighbors[pos[0]+1][pos[1]] += change
self.neighbors[pos[0]+1][0] += change
self.neighbors[pos[0]][0] += change
self.neighbors[pos[0]-1][0] += change
self.neighbors[pos[0]-1][pos[1]] += change
def nextGeneration(self):
# copy the neighbor map, because changes will be made during the update
self.neighborsOld = copy.deepcopy(self.neighbors)
for x in range(fieldSize[0]):
for y in range(fieldSize[1]):
# Any live cell with fewer than two live neighbours dies, as if caused by under-population.
if(self.gameField[x][y] == 1 and self.neighborsOld[x][y] < 2):
self.gameField[x][y] = 0
self.updateNeighbors([x,y], False)
# Any live cell with more than three live neighbours dies, as if by overcrowding.
elif(self.gameField[x][y] == 1 and self.neighborsOld[x][y] >3):
self.gameField[x][y] = 0
self.updateNeighbors([x,y], False)
# Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
elif(self.gameField[x][y] == 0 and self.neighborsOld[x][y] == 3):
self.gameField[x][y] = 1
self.updateNeighbors([x,y], True)
def drawField(self, surface):
surface.fill(colorDead)
# loop through and draw each live cell
for x in range(fieldSize[0]):
for y in range(fieldSize[1]):
if(self.gameField[x][y] == 1):
pygame.draw.rect(surface, colorAlive, [dX*x, dY*y, dX, dY])
pygame.display.flip()
def __init__(self):
# initialize the game field and neighbor map with zeros
self.gameField = np.zeros(fieldSize)
self.neighbors = np.zeros(fieldSize)
# begining of the program
game = cLife()
pygame.init()
surface = pygame.display.set_mode(windowSize)
pygame.display.set_caption("Conway\'s Game of Life")
clock = pygame.time.Clock()
pygame.font.init()
font=pygame.font.Font(None,10)
surface.fill(colorDead)
game.randomize()
game.drawField(surface)
pygame.display.flip()
running = False
while True:
#clock.tick(60)
# handling events
for event in pygame.event.get():
if(event.type == pygame.MOUSEBUTTONDOWN):
mousePos = pygame.mouse.get_pos()
x = int(mousePos[0]/dX)
y = int(mousePos[1]/dY)
if(game.gameField[x][y] == 0):
game.gameField[x][y] = 1
game.updateNeighbors([x, y], True)
game.drawField(surface)
else:
game.gameField[x][y] = 0
game.updateNeighbors([x, y], False)
game.drawField(surface)
elif(event.type == pygame.QUIT):
pygame.quit()
elif(event.type == pygame.KEYDOWN):
# return key starts and stops the simulation
if(event.key == pygame.K_RETURN):
if(running == False):
running = True
else:
running = False
# 't' key ticks the simulation forward one generation
elif(event.key == pygame.K_t and running == False):
game.nextGeneration()
game.drawField(surface)
# 'r' randomizes the playfield
elif(event.key == pygame.K_r):
game.randomize()
game.drawField(surface)
# 'c' clears the game field
elif(event.key == pygame.K_c):
running = False
game.gameField = np.zeros(fieldSize)
game.neighbors = np.zeros(fieldSize)
game.drawField(surface)
# 'n' displays the neighbor map
elif(event.key == pygame.K_n):
game.displayNeighbors(surface)
if(running == True):
game.nextGeneration()
game.drawField(surface)
self.neighborsOld = self.neighbors does not copy the map, it only points to it.
See :
a = [[1,2],[3,4]]
b = a
b[0][0] = 9
>>> a
[[9, 2], [3, 4]]
You need to either make a copy (a[:]) for every row in a, or use the copy module and use deepcopy:
b = [x[:] for x in a]
or
import copy
b = copy.deepcopy(a)
Either way, it results in
b[0][0] = 9
>>> a
[[1, 2], [3, 4]]

Categories