A-star algorithm results in weird, slow and inefficient choices - python

Today I attempted making the A-STAR pathfinding algorithm, with visualization in PyGame. The code is:
import pygame, time
pygame.init()
pygame.font.init()
#----------[0,1,2]
#GRID[x][y][g/h/f]
#g_cost: distance from starting to current node
#h_cost: distance from ending to current node
#f_cost: sum of previous two costs. That way, the lower
#the f cost, the more likely a node is to be on the path
WIDTH = 20
HEIGHT = 15
XSTART = 5
YSTART = 5
XEND = 15
YEND = 10
TICK = 10000000
CLOCK = pygame.time.Clock()
OPEN_NODES = []
CLOSED_NODES = []
BEACON = 0
SQUARE = 50
GEN = 0
WHITE = (255,255,255)
GRID = []
#func to find distance between 2 nodes
def find_dist(coor1,coor2):
x1,y1 = coor1
x2,y2 = coor2
x_diff = abs(x1-x2)
y_diff = abs(y1-y2)
smaller = min([x_diff,y_diff])
#diagonal multiplied by sqrt(2) approximately
dist = round(smaller*1.4+(max([x_diff,y_diff])-smaller))*10
return dist
#super stupid way to get all node neighbors
def get_neighbors(coors):
x,y = coors
neighbors = []
if x-1 > -1:
neighbors.append(GRID[x-1][y])
if x-1 > -1 and y+1 < HEIGHT:
neighbors.append(GRID[x-1][y+1])
if y+1 < HEIGHT:
neighbors.append(GRID[x][y+1])
if y+1 < HEIGHT and x+1 < WIDTH:
neighbors.append(GRID[x+1][y+1])
if x+1 < WIDTH:
neighbors.append(GRID[x+1][y])
if x+1 < WIDTH and y-1 > -1:
neighbors.append(GRID[x+1][y-1])
if y-1 > -1:
neighbors.append(GRID[x][y-1])
if y-1 > -1 and x-1 > -1:
neighbors.append(GRID[x-1][y-1])
return neighbors
#returns list without duplicates
def elim_duplicates(listin):
for element in listin:
if listin.count(element) > 1:
listin.pop(listin.index(element))
return listin
#creates main grid
for x in range(WIDTH):
COLUMN = []
for y in range(HEIGHT):
COLUMN.append([-1,-1])
GRID.append(COLUMN)
GRID[XSTART][YSTART] = [-2,-2]
GRID[XEND][YEND] = [-3,-3]
#adds costs to each node
for x in range(WIDTH):
for y in range(HEIGHT):
g_cost = find_dist([x,y],[XSTART,YSTART])
h_cost = find_dist([x,y],[XEND,YEND])
f_cost = g_cost + h_cost
GRID[x][y] = [g_cost,h_cost,f_cost,x,y]
#adds the starting node to open nodes,
#which is a list containing all nodes
#that will be checked for the lowest f cost
OPEN_NODES.append(GRID[XSTART][YSTART])
WINDOW = pygame.display.set_mode((WIDTH*SQUARE, HEIGHT*SQUARE))
pygame.display.set_caption('A* Algorithm')
RUN = True
PATH_NODES = []
while RUN:
GEN += 1
CLOCK.tick(TICK)
#cycle control
for event in pygame.event.get():
if event.type == pygame.QUIT:
RUN = False
#updates costs
for x in range(WIDTH):
for y in range(HEIGHT):
g_cost = find_dist([x,y],[XSTART,YSTART])
h_cost = find_dist([x,y],[XEND,YEND])
f_cost = g_cost + h_cost
GRID[x][y] = [g_cost,h_cost,f_cost,x,y]
#creates a list with only f costs
f_cost_list = []
for i in OPEN_NODES:
f_cost_list.append(i[2])
#finds the node with the lowest f cost
CURRENT_NODE = OPEN_NODES[f_cost_list.index(min(f_cost_list))]
#adds all neighbors of the current node and removes duplicates
OPEN_NODES.extend(get_neighbors(CURRENT_NODE[3:]))
OPEN_NODES = elim_duplicates(OPEN_NODES)
#adds current node to possible path nodes
PATH_NODES.append(CURRENT_NODE)
#removes current node from open nodes
OPEN_NODES.remove(CURRENT_NODE)
#removes all possible duplicates from lists
PATH_NODES = elim_duplicates(PATH_NODES)
OPEN_NODES = elim_duplicates(OPEN_NODES)
#checks if path has been found
if CURRENT_NODE[3] == XEND and CURRENT_NODE[4] == YEND:
pygame.draw.rect(WINDOW,(0,255,255), (490, 490, SQUARE, SQUARE))
print('DONE')
time.sleep(3)
pygame.quit()
#draws all open nodes
for i in OPEN_NODES:
pygame.draw.rect(WINDOW,(0,0,255),(i[3]*SQUARE,i[4]*SQUARE,SQUARE,SQUARE))
#draws all possible path nodes
for i in PATH_NODES:
pygame.draw.rect(WINDOW,(255,0,0),(i[3]*SQUARE,i[4]*SQUARE,SQUARE,SQUARE))
#draws start and end nodes
pygame.draw.rect(WINDOW,(0,255,0), (XSTART*SQUARE, YSTART*SQUARE, SQUARE, SQUARE))
pygame.draw.rect(WINDOW,(0,255,0), (XEND*SQUARE,YEND*SQUARE,SQUARE,SQUARE))
#shows generation
font = pygame.font.Font(None, 100)
text = font.render(str(GEN), 1, WHITE)
WINDOW.blit(text, (10,10))
#updates screen
pygame.display.update()
WINDOW.fill((0, 0, 0))
pygame.quit()
Whenever I run the algorithm with the starting and ending coordinates as shown, this is the weird result that shows:
It doesn't yet show the exact path, but the errors in the algorithm are clearly visible: holes, weird edges etc.
What could be causing this?

Related

python need help seeing where my set is being changed during iteration

I have built a minesweeper algorithm for a cs50 assignment. I have identidied issue with my code. (Inside the minesweeper class)
The program needs both minesweeper.py and runner.py and is executed with "python runner.py"
Upon running it with python runner.py it gives me a RuntimeError:
Traceback (most recent call last):
File "C:\Users\seng\Downloads\minesweeper\minesweeper\runner.py", line 220, in <module>
ai.add_knowledge(move, nearby)
File "C:\Users\seng\Downloads\minesweeper\minesweeper\minesweeper.py", line 230, in add_knowledge
self.updateknowledge()
File "C:\Users\seng\Downloads\minesweeper\minesweeper\minesweeper.py", line 274, in updateknowledge
for safe in safes:
RuntimeError: Set changed size during iteration
Here is the code.
minesweeper.py
import itertools
import random
import copy
class Minesweeper():
"""
Minesweeper game representation
"""
def __init__(self, height=8, width=8, mines=8):
# Set initial width, height, and number of mines
self.height = height
self.width = width
self.mines = set()
# Initialize an empty field with no mines
self.board = []
for i in range(self.height):
row = []
for j in range(self.width):
row.append(False)
self.board.append(row)
# Add mines randomly
while len(self.mines) != mines:
i = random.randrange(height)
j = random.randrange(width)
if not self.board[i][j]:
self.mines.add((i, j))
self.board[i][j] = True
# At first, player has found no mines
self.mines_found = set()
def print(self):
"""
Prints a text-based representation
of where mines are located.
"""
for i in range(self.height):
print("--" * self.width + "-")
for j in range(self.width):
if self.board[i][j]:
print("|X", end="")
else:
print("| ", end="")
print("|")
print("--" * self.width + "-")
def is_mine(self, cell):
i, j = cell
return self.board[i][j]
def nearby_mines(self, cell):
"""
Returns the number of mines that are
within one row and column of a given cell,
not including the cell itself.
"""
# Keep count of nearby mines
count = 0
# Loop over all cells within one row and column
for i in range(cell[0] - 1, cell[0] + 2):
for j in range(cell[1] - 1, cell[1] + 2):
# Ignore the cell itself
if (i, j) == cell:
continue
# Update count if cell in bounds and is mine
if 0 <= i < self.height and 0 <= j < self.width:
if self.board[i][j]:
count += 1
return count
def won(self):
"""
Checks if all mines have been flagged.
"""
return self.mines_found == self.mines
class Sentence():
"""
Logical statement about a Minesweeper game
A sentence consists of a set of board cells,
and a count of the number of those cells which are mines.
"""
def __init__(self, cells, count):
self.cells = set(cells)
self.count = count
def __eq__(self, other):
return self.cells == other.cells and self.count == other.count
def __str__(self):
return f"{self.cells} = {self.count}"
def known_mines(self):
"""
Returns the set of all cells in self.cells known to be mines.
"""
if len(self.cells) == self.count:
return self.cells
def known_safes(self):
"""
Returns the set of all cells in self.cells known to be safe.
"""
if 0 == self.count:
return self.cells
def mark_mine(self, cell):
"""
Updates internal knowledge representation given the fact that
a cell is known to be a mine.
"""
if cell in self.cells:
self.cells.remove(cell)
self.count -= 1
def mark_safe(self, cell):
"""
Updates internal knowledge representation given the fact that
a cell is known to be safe.
"""
if cell in self.cells:
self.cells.remove(cell)
class MinesweeperAI():
"""
Minesweeper game player
"""
def __init__(self, height=8, width=8):
# Set initial height and width
self.height = height
self.width = width
# Keep track of which cells have been clicked on
self.moves_made = set()
# Keep track of cells known to be safe or mines
self.mines = set()
self.safes = set()
# List of sentences about the game known to be true
self.knowledge = []
def mark_mine(self, cell):
"""
Marks a cell as a mine, and updates all knowledge
to mark that cell as a mine as well.
"""
self.mines.add(cell)
for sentence in self.knowledge:
sentence.mark_mine(cell)
def mark_safe(self, cell):
"""
Marks a cell as safe, and updates all knowledge
to mark that cell as safe as well.
"""
self.safes.add(cell)
for sentence in self.knowledge:
sentence.mark_safe(cell)
def add_knowledge(self, cell, count):
"""
Called when the Minesweeper board tells us, for a given
safe cell, how many neighboring cells have mines in them.
This function should:
1) mark the cell as a move that has been made
2) mark the cell as safe
3) add a new sentence to the AI's knowledge base
based on the value of `cell` and `count`
4) mark any additional cells as safe or as mines
if it can be concluded based on the AI's knowledge base
5) add any new sentences to the AI's knowledge base
if they can be inferred from existing knowledge
"""
#1
self.moves_made.add(cell)
#2
self.mark_safe(cell)
#3
i, j = cell
removecell = []
addcell = [(i-1, j-1), (i-1, j), (i-1, j+1),
(i, j-1), (i, j+1),
(i+1, j-1), (i+1, j), (i+1, j + 1),]
for c in addcell:
if c[0] < 0 or c[0] > 7 or c[1] < 0 or c[1] > 7:
removecell.append(c)
for c in removecell:
addcell.remove(c)
removecell = []
for c in addcell:
if c in self.mines:
removecell.append(c)
count -= len(removecell)
for c in removecell:
addcell.remove(c)
removecell = []
for c in addcell:
if c in self.safes:
removecell.append(c)
for c in removecell:
addcell.remove(c)
#need filter for empty
newsentence = Sentence(addcell, count)
if len(newsentence.cells) > 0:
self.knowledge.append(newsentence)
print("dfs")
self.updateknowledge()
print("2")
self.inference()
print("3")
def inference(self):
for sentence1 in self.knowledge:
for sentence2 in self.knowledge:
if sentence1.cells.issubset(sentence2.cells):
new_cells = sentence2.cells - sentence1.cells
new_count = sentence2.count - sentence1.count
new_sentence = Sentence(new_cells, new_count)
if new_sentence not in self.knowledge:
self.knowledge.append(new_sentence)
self.updateknowledge()
def updateknowledge(self):
keepgoing = True
while keepgoing:
keepgoing = False
for sentence in self.knowledge:
mines = sentence.known_mines()
if mines:
keepgoing = True
for mine in mines:
self.mark_mine(mine)
safes = sentence.known_safes()
if safes:
keepgoing = True
for safe in safes:
self.mark_safe(safe)
def make_safe_move(self):
"""
Returns a safe cell to choose on the Minesweeper board.
The move must be known to be safe, and not already a move
that has been made.
This function may use the knowledge in self.mines, self.safes
and self.moves_made, but should not modify any of those values.
"""
for safe in self.safes:
if safe not in self.moves_made:
return safe
return None
def make_random_move(self):
"""
Returns a move to make on the Minesweeper board.
Should choose randomly among cells that:
1) have not already been chosen, and
2) are not known to be mines
"""
moves = len(self.moves_made) + len(self.mines)
if moves == 64:
return None
while True:
i = random.randrange(self.height)
j = random.randrange(self.height)
if (i, j) not in self.moves_made and (i, j) not in self.mines:
return (i, j)
runner.py
import pygame
import sys
import time
from minesweeper import Minesweeper, MinesweeperAI
HEIGHT = 8
WIDTH = 8
MINES = 8
# Colors
BLACK = (0, 0, 0)
GRAY = (180, 180, 180)
WHITE = (255, 255, 255)
# Create game
pygame.init()
size = width, height = 600, 400
screen = pygame.display.set_mode(size)
# Fonts
OPEN_SANS = "assets/fonts/OpenSans-Regular.ttf"
smallFont = pygame.font.Font(OPEN_SANS, 20)
mediumFont = pygame.font.Font(OPEN_SANS, 28)
largeFont = pygame.font.Font(OPEN_SANS, 40)
# Compute board size
BOARD_PADDING = 20
board_width = ((2 / 3) * width) - (BOARD_PADDING * 2)
board_height = height - (BOARD_PADDING * 2)
cell_size = int(min(board_width / WIDTH, board_height / HEIGHT))
board_origin = (BOARD_PADDING, BOARD_PADDING)
# Add images
flag = pygame.image.load("assets/images/flag.png")
flag = pygame.transform.scale(flag, (cell_size, cell_size))
mine = pygame.image.load("assets/images/mine.png")
mine = pygame.transform.scale(mine, (cell_size, cell_size))
# Create game and AI agent
game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES)
ai = MinesweeperAI(height=HEIGHT, width=WIDTH)
# Keep track of revealed cells, flagged cells, and if a mine was hit
revealed = set()
flags = set()
lost = False
# Show instructions initially
instructions = True
while True:
# Check if game quit
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.fill(BLACK)
# Show game instructions
if instructions:
# Title
title = largeFont.render("Play Minesweeper", True, WHITE)
titleRect = title.get_rect()
titleRect.center = ((width / 2), 50)
screen.blit(title, titleRect)
# Rules
rules = [
"Click a cell to reveal it.",
"Right-click a cell to mark it as a mine.",
"Mark all mines successfully to win!"
]
for i, rule in enumerate(rules):
line = smallFont.render(rule, True, WHITE)
lineRect = line.get_rect()
lineRect.center = ((width / 2), 150 + 30 * i)
screen.blit(line, lineRect)
# Play game button
buttonRect = pygame.Rect((width / 4), (3 / 4) * height, width / 2, 50)
buttonText = mediumFont.render("Play Game", True, BLACK)
buttonTextRect = buttonText.get_rect()
buttonTextRect.center = buttonRect.center
pygame.draw.rect(screen, WHITE, buttonRect)
screen.blit(buttonText, buttonTextRect)
# Check if play button clicked
click, _, _ = pygame.mouse.get_pressed()
if click == 1:
mouse = pygame.mouse.get_pos()
if buttonRect.collidepoint(mouse):
instructions = False
time.sleep(0.3)
pygame.display.flip()
continue
# Draw board
cells = []
for i in range(HEIGHT):
row = []
for j in range(WIDTH):
# Draw rectangle for cell
rect = pygame.Rect(
board_origin[0] + j * cell_size,
board_origin[1] + i * cell_size,
cell_size, cell_size
)
pygame.draw.rect(screen, GRAY, rect)
pygame.draw.rect(screen, WHITE, rect, 3)
# Add a mine, flag, or number if needed
if game.is_mine((i, j)) and lost:
screen.blit(mine, rect)
elif (i, j) in flags:
screen.blit(flag, rect)
elif (i, j) in revealed:
neighbors = smallFont.render(
str(game.nearby_mines((i, j))),
True, BLACK
)
neighborsTextRect = neighbors.get_rect()
neighborsTextRect.center = rect.center
screen.blit(neighbors, neighborsTextRect)
row.append(rect)
cells.append(row)
# AI Move button
aiButton = pygame.Rect(
(2 / 3) * width + BOARD_PADDING, (1 / 3) * height - 50,
(width / 3) - BOARD_PADDING * 2, 50
)
buttonText = mediumFont.render("AI Move", True, BLACK)
buttonRect = buttonText.get_rect()
buttonRect.center = aiButton.center
pygame.draw.rect(screen, WHITE, aiButton)
screen.blit(buttonText, buttonRect)
# Reset button
resetButton = pygame.Rect(
(2 / 3) * width + BOARD_PADDING, (1 / 3) * height + 20,
(width / 3) - BOARD_PADDING * 2, 50
)
buttonText = mediumFont.render("Reset", True, BLACK)
buttonRect = buttonText.get_rect()
buttonRect.center = resetButton.center
pygame.draw.rect(screen, WHITE, resetButton)
screen.blit(buttonText, buttonRect)
# Display text
text = "Lost" if lost else "Won" if game.mines == flags else ""
text = mediumFont.render(text, True, WHITE)
textRect = text.get_rect()
textRect.center = ((5 / 6) * width, (2 / 3) * height)
screen.blit(text, textRect)
move = None
left, _, right = pygame.mouse.get_pressed()
# Check for a right-click to toggle flagging
if right == 1 and not lost:
mouse = pygame.mouse.get_pos()
for i in range(HEIGHT):
for j in range(WIDTH):
if cells[i][j].collidepoint(mouse) and (i, j) not in revealed:
if (i, j) in flags:
flags.remove((i, j))
else:
flags.add((i, j))
time.sleep(0.2)
elif left == 1:
mouse = pygame.mouse.get_pos()
# If AI button clicked, make an AI move
if aiButton.collidepoint(mouse) and not lost:
move = ai.make_safe_move()
if move is None:
move = ai.make_random_move()
if move is None:
flags = ai.mines.copy()
print("No moves left to make.")
else:
print("No known safe moves, AI making random move.")
else:
print("AI making safe move.")
time.sleep(0.2)
# Reset game state
elif resetButton.collidepoint(mouse):
game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES)
ai = MinesweeperAI(height=HEIGHT, width=WIDTH)
revealed = set()
flags = set()
lost = False
continue
# User-made move
elif not lost:
for i in range(HEIGHT):
for j in range(WIDTH):
if (cells[i][j].collidepoint(mouse)
and (i, j) not in flags
and (i, j) not in revealed):
move = (i, j)
# Make move and update AI knowledge
if move:
if game.is_mine(move):
lost = True
else:
nearby = game.nearby_mines(move)
revealed.add(move)
ai.add_knowledge(move, nearby)
pygame.display.flip()
I understand that this is where the error occurs, and replacing my iterated object with a copy fixes the issue. Problem is i fail to see where i am editing the set in the first place.
class MinesweeperAI:
def updateknowledge(self):
...
safes = sentence.known_safes()
if safes:
keepgoing = True
for safe in safes.copy():
self.mark_safe(safe)
self.mark_safe(safe) leads to (in minesweeper class)
self.safes.add(cell)
for sentence in self.knowledge:
sentence.mark_safe(cell)
and sentence.mark_safe(cell) leads to
if cell in self.cells:
self.cells.remove(cell)
So how is this affecting the set of safes which I am iterating over? Would appreciate advice
The problem is that the code is "marking safes" while looping over "safes".
Try changing:
for safe in self.safes:
to:
for safe in list(self.safes):
That will make a copy of the "safes" to loop over, leaving the underlying set available for updates.

How can I fix my neighbor counting for my maze generation?

My issue is with my def countNeighbours(self, grid) function. In the grid, every cell is surround by four walls making each of them rectangles. I decided to use the wrap around method where the an edge cell'
s neighbor is on the other side. This way none of the indices will be out of bounds. However now I've run into the problem where the maze pathing goes straight down and breaks. I can't comprehend why exactly.
Screenshot of the bug.
The goal was for the maze pathing to randomly visited a couple cells and destroy the walls before stopping. Then I'd move on the the backtracking step.
Here is the entire code:
import pygame
import random
pygame.init()
screenWidth = 604
screenHeight = 604
FPS = 60
# Colors
Red = (255, 0, 0)
Blue = (0, 100, 200, 50)
Green = (0, 200, 100, 50)
White = (255, 255, 255)
Black = (0, 0, 0)
# TODO: FIX THE COUNTING FUNCTION- MESSES UP THE PATHING
class Cell:
def __init__(self, i, j):
self.i = i # Row
self.j = j # Column
self.visited = False
# Checks to see if wall(s) of the cell exists
# Order: Top, Right, Bottom, Left
self.wall = [True, True, True, True]
def draw(self, surface):
x = self.i * length
y = self.j * length
# Our Path in Green
if self.visited:
pygame.draw.rect(displayWindow, Green, (x, y, length, length))
# Top Line Start Pos, End Pos
if self.wall[0]:
pygame.draw.line(displayWindow, White, (x, y), (x + length, y), 1)
# Right Line
if self.wall[1]:
pygame.draw.line(displayWindow, White, (x + length, y), (x + length, y + length), 1)
# Bottom
if self.wall[2]:
pygame.draw.line(displayWindow, White, (x + length, y + length), (x, y + length), 1)
# Left
if self.wall[3]:
pygame.draw.line(displayWindow, White, (x, y + length), (x, y), 1)
self.changed = False
# Function adds the unvisited to the cell's neighbor array and returns the next cell
def countNeighbors(self, grid):
global x, y
self.neighbors = []
top = grid[(((self.i + 1) + x) % x)][self.j]
right = grid[self.i][(((self.j + 1) + y) % y)]
bottom = grid[(((self.i - 1) + x) % x)][self.j]
left = grid[self.i][(((self.j - 1) + y) % y)]
# If the neighbor cell is unvisited add to array
if not top.visited:
self.neighbors.append(top)
if not right.visited:
self.neighbors.append(right)
if not bottom.visited:
self.neighbors.append(bottom)
if not left.visited:
self.neighbors.append(left)
# pick random unvisted cell as our next
if len(self.neighbors) > 0:
p = random.randrange(0, len(self.neighbors))
return self.neighbors[p]
# Highlights the current cell
def marker(self):
x = self.i * length
y = self.j * length
pygame.draw.rect(displayWindow, Blue, (x, y, length, length))
# Rows and Columns, length will be our width of each cell
length = 40
rows = screenWidth // length
cols = screenHeight // length
print(rows, cols)
# store cells
grid = [[]*cols]*rows
for i in range(rows):
for j in range(cols):
cell = Cell(i, j)
grid[i].append(cell)
current = grid[0][0]
x = len(grid)
y = len(grid[i])
def display(surface):
global current
for i in range(len(grid)):
for j in range(len(grid[i])):
grid[i][j].draw(surface)
# Mark first cell as visited
current.visited = True
current.marker()
# Check neighbors of current cell
nextCell = current.countNeighbors(grid)
if nextCell:
nextCell.visited = True
deleteWall(current, nextCell)
current = nextCell
def deleteWall(a, b):
# Right and Left Walls
p = a.i - b.i
if p == 1: # neighbor to the left
a.wall[3] = False
b.wall[1] = False
elif p == -1: # neighbor to the right
a.wall[1] = False
b.wall[3] = False
# Top and Bottom Walls
q = a.j - b.j
if q == 1: # neighbor above
a.wall[0] = False
b.wall[2] = False
elif q == -1: # neighbor below
a.wall[2] = False
b.wall[0] = False
displayWindow = pygame.display.set_mode((screenWidth, screenHeight))
pygame.display.set_caption("Maze Generator")
def main():
global FPS
FPSclock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
FPSclock.tick(FPS)
display(displayWindow)
pygame.display.update()
if __name__ == '__main__':
main()
Found the bug:
grid = [[]*cols]*rows
And the correct functions were:
def countNeighbors(self, grid):
global x, y
self.neighbors = []
# Edge Cases
# Top Cell
if self.j == 0:
self.neighbors.append(None)
else:
self.neighbors.append(grid[self.i][self.j - 1])
# Right
if self.i == x - 1:
self.neighbors.append(None)
else:
self.neighbors.append(grid[self.i + 1][self.j])
# Bottom
if self.j == y - 1:
self.neighbors.append(None)
else:
self.neighbors.append(grid[self.i][self.j + 1])
# Left
if self.i == 0:
self.neighbors.append(None)
else:
self.neighbors.append(grid[self.i - 1][self.j])
grid = []
for i in range(rows):
column = []
for j in range(cols):
cell = Cell(i, j)
column.append(cell)
grid.append(column)
def display(surface):
global current
for i in range(len(grid)):
for j in range(len(grid[i])):
grid[i][j].draw(surface)
grid[i][j].countNeighbors(grid)
current.visited = True
current.marker()
next = random.choice(current.neighbors)
if next and not next.visited:
next.visited = True
deleteWall(current, next)
current = next

Problem with animating a sprite in pygame

i have a problem with this code, i am a new person with programming and been using the book "how to think like a computer scientist 3rd edition" and he did not solve exercise 2 of chapter 17 this given: "he deliberately left a mistake in the code to animate Duke. If you click on one of the checkerboard squares to the right of Duke, he salutes anyway. Why? Find a one-line solution to the error ", I've tried many forms but I have not succeeded, I leave you all the code and the images that I have used
PS: images must have the name: ball.png and duke_spritesheet.png
import pygame
gravity = 0.025
my_clock = pygame.time.Clock()
class QueenSprite:
def __init__(self, img, target_posn):
self.image = img
self.target_posn = target_posn
(x, y) = target_posn
self.posn = (x, 0) # Start ball at top of its column
self.y_velocity = 0 # with zero initial velocity
def update(self):
self.y_velocity += gravity
(x, y) = self.posn
new_y_pos = y + self.y_velocity
(target_x, target_y) = self.target_posn # Unpack the position
dist_to_go = target_y - new_y_pos # How far to our floor?
if dist_to_go < 0: # Are we under floor?
self.y_velocity = -0.65 * self.y_velocity # Bounce
new_y_pos = target_y + dist_to_go # Move back above floor
self.posn = (x, new_y_pos) # Set our new position.
def draw(self, target_surface): # Same as before.
target_surface.blit(self.image, self.posn)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains point pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
def handle_click(self):
self.y_velocity += -2 # Kick it up
class DukeSprite:
def __init__(self, img, target_posn):
self.image = img
self.posn = target_posn
self.anim_frame_count = 0
self.curr_patch_num = 0
def update(self):
if self.anim_frame_count > 0:
self.anim_frame_count = (self.anim_frame_count + 1 ) % 60
self.curr_patch_num = self.anim_frame_count // 6
def draw(self, target_surface):
patch_rect = (self.curr_patch_num * 50, 0,
50, self.image.get_width())
target_surface.blit(self.image, self.posn, patch_rect)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
def handle_click(self):
if self.anim_frame_count == 0:
self.anim_frame_count = 5
def draw_board(the_board):
""" Draw a chess board with queens, as determined by the the_board. """
pygame.init()
colors = [(255,0,0), (0,0,0)] # Set up colors [red, black]
n = len(the_board) # This is an NxN chess board.
surface_sz = 480 # Proposed physical surface size.
sq_sz = surface_sz // n # sq_sz is length of a square.
surface_sz = n * sq_sz # Adjust to exactly fit n squares.
# Create the surface of (width, height), and its window.
surface = pygame.display.set_mode((surface_sz, surface_sz))
ball = pygame.image.load("ball.png")
# Use an extra offset to centre the ball in its square.
# If the square is too small, offset becomes negative,
# but it will still be centered :-)
ball_offset = (sq_sz-ball.get_width()) // 2
all_sprites = [] # Keep a list of all sprites in the game
# Create a sprite object for each queen, and populate our list.
for (col, row) in enumerate(the_board):
a_queen = QueenSprite(ball,
(col*sq_sz+ball_offset, row*sq_sz+ball_offset))
all_sprites.append(a_queen)
# Load the sprite sheet
duke_sprite_sheet = pygame.image.load("duke_spritesheet.png")
# Instantiate two duke instances, put them on the chessboard
duke1 = DukeSprite(duke_sprite_sheet,(sq_sz*2, 0))
duke2 = DukeSprite(duke_sprite_sheet,(sq_sz*5, sq_sz))
# Add them to the list of sprites which our game loop manages
all_sprites.append(duke1)
all_sprites.append(duke2)
while True:
# Look for an event from keyboard, mouse, etc.
ev = pygame.event.poll()
if ev.type == pygame.QUIT:
break;
if ev.type == pygame.KEYDOWN:
key = ev.dict["key"]
if key == 27: # On Escape key ...
break # leave the game loop.
if key == ord("r"):
colors[0] = (255, 0, 0) # Change to red + black.
elif key == ord("g"):
colors[0] = (0, 255, 0) # Change to green + black.
elif key == ord("b"):
colors[0] = (0, 0, 255) # Change to blue + black.
if ev.type == pygame.MOUSEBUTTONDOWN: # Mouse gone down?
posn_of_click = ev.dict["pos"] # Get the coordinates.
for sprite in all_sprites:
if sprite.contains_point(posn_of_click):
sprite.handle_click()
break
for sprite in all_sprites:
sprite.update()
# Draw a fresh background (a blank chess board)
for row in range(n): # Draw each row of the board.
c_indx = row % 2 # Alternate starting color
for col in range(n): # Run through cols drawing squares
the_square = (col*sq_sz, row*sq_sz, sq_sz, sq_sz)
surface.fill(colors[c_indx], the_square)
# Now flip the color index for the next square
c_indx = (c_indx + 1) % 2
# Ask every sprite to draw itself.
for sprite in all_sprites:
sprite.draw(surface)
my_clock.tick(60) # Waste time so that frame rate becomes 60 fps
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
draw_board([0, 5, 3, 1, 6, 4, 2]) # 7 x 7 to test window size
PS: I think the error is here but it did not succeed
return ( x >= my_x and x + my_width and y >= my_y and y < my_y + my_height)
The issue is caused by the face, that "duke_spritesheet.png" is a sprite sheet. When you define the rectangular region which is covered by the object, then you have to use the width of a single image, rather than the width of the entire sprite sheet:
my_width = self.image.get_width()
my_width = 50
Change this in the method contains_point of the class DukeSprite:
class DukeSprite:
# [...]
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
(my_x, my_y) = self.posn
my_width = 50
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)

Python: PSO clumping <s>and code working only when mouse moves</s>

I'm trying to make a simple PSO (particle-swarm optimization) program.
Below is my current code, and I've been tweaking the weights only to find the "optimization" not working.
import random as ran
import math
# import threading as thr
import pygame as gm
# import pycuda as cuda
# import matplotlib.pyplot as plt
p_max = 10 # Maximum number of particles
rand_max = 100 # Maximum random vector value
# history = 10 # Number of iterations to record as history (including most current)
width = 600
height = 600
func_dict = {
"sphere" : ( lambda x, y: ( (x-(width/2))**2 + (y-(height/2))**2 ) ),
"booth" : ( lambda x, y: (((x-(width/2)) + 2*(y-(height/2)) - 7) ** 2) + ((2*(x-(width/2)) + (y-(height/2)) - 5) ** 2) ),
"matyas" : ( lambda x, y: (0.26 * ((x-(width/2))**2 + (y-(height/2))**2) - 0.48*(x-(width/2))*(y-(height/2))) )
} # (x-(width/2)) and (y-(height/2)) to shift the Zero to the display center
func = "sphere" # Choose function from func_dict
# Weights (0<w<1)
wLocal = 0.4 # Explore weight
wGlobal = 0.8 # Exploit weight
wRandom = 0.02 # Weight of random vector
global_best = [None, None, None] # Initial blank
class particle: # particles
global global_best
def __init__(self):
global global_best
global width, height
global func_dict, func
self.vel_x = 0
self.vel_y = 0
self.pos_x = ran.randint(0, width)
self.pos_y = ran.randint(0, height)
self.pos_z = func_dict[func](self.pos_x, self.pos_y)
self.local_best = [self.pos_x, self.pos_y, self.pos_z]
if (global_best[0] == None) or (global_best[1] == None) or (global_best[2] == None): # Is 1st particle
global_best = self.local_best
def update(self): # Update vectors
global width, height
global rand_max
global wGlobal, wLocal, wRandom
global global_best
self.vel_x = (wGlobal * (global_best[0] - self.pos_x)) + (wLocal * (self.local_best[0] - self.pos_x)) + (wRandom * ran.randint(-rand_max, rand_max))
self.vel_y = (wGlobal * (global_best[1] - self.pos_y)) + (wLocal * (self.local_best[1] - self.pos_y)) + (wRandom * ran.randint(-rand_max, rand_max))
# self.pos_x = (self.pos_x + self.vel_x) % width
# self.pos_y = (self.pos_y + self.vel_y) % height
self.pos_x += self.vel_x
self.pos_y += self.vel_y
if self.pos_x < 0:
self.pos_x = 0
if self.pos_y < 0:
self.pos_y = 0
if self.pos_x > width:
self.pos_x = width
if self.pos_y > height:
self.pos_y = height
self.pos_z = func_dict[func](self.pos_x, self.pos_y)
if self.pos_z < global_best[2]:
global_best = [self.pos_x, self.pos_y, self.pos_z]
particles = [None for _ in range(p_max)]
def initialize():
global_best = [None, None, None]
for foo in range(p_max):
particles[foo] = particle() # create new particles
# def dist(p1, p2): # distance
# return(math.sqrt( ( (p1.pos_x - p2.pos_y)**2) + ((p1.pos_y - p2.pos_y)**2) ) )
# def update(this): # this = particle
# this.vel_x = (wGlobal * (global_best[0] - this.pos_x)) + (wLocal * (this.local_best[0] - this.pos_x)) + (wRandom * ran.randint(0, rand_max))
# this.vel_y = (wGlobal * (global_best[1] - this.pos_y)) + (wLocal * (this.local_best[1] - this.pos_y)) + (wRandom * ran.randint(0, rand_max))
# # this.pos_x = (this.pos_x + this.vel_x) % width
# # this.pos_y = (this.pos_y + this.vel_y) % height
# this.pos_x += this.vel_x
# this.pos_y += this.vel_y
# if this.pos_x < 0:
# this.pos_x = 0
# if this.pos_y < 0:
# this.pos_y = 0
# if this.pos_x > width:
# this.pos_x = width
# if this.pos_y > height:
# this.pos_y = height
# # return this
# def update_multi(things): # things = list() of particles
# these = things
# for item in these:
# item = update(item)
# return these
gm.init()
main = gm.display.set_mode((width, height))
end_program = False
initialize()
main.fill((255, 255, 255))
while end_program == False:
# main.fill((255, 255, 255)) #Comment/Uncomment to leave trace
# plt.plot() # Plot functions
for event in gm.event.get():
if event.type == gm.QUIT:
end_program = True
for foo in range(len(particles)):
particles[foo].update()
gm.draw.circle(main, (0, 0, 0), (int(particles[foo].pos_x), int(particles[foo].pos_y)), 5, 0)
gm.display.flip()
Problem 1: Program only runs when mouse is moving
I'm not sure why but the program only runs fairly quickly for a few iterations but seems to just stop afterwards, but continues when the mouse is moved.
Problem 2: Particles appear to stay local
As I move the mouse around, it kinda runs. What emerges is clumps of traces left when the 2nd # main.fill((255, 255, 255)) is uncommented. The 1st initial traces from before the program stops without the mouse's movement seem more spread out and I'm not sure if that's the global variables or the randoms at work.
Edit: I've fixed the problem where the program only runs when the mouse moves by unindenting:
for foo in range(len(particles)):
particles[foo].update()
gm.draw.circle(main, (0, 0, 0), (int(particles[foo].pos_x), int(particles[foo].pos_y)), 5, 0)
However, the particles still seem to hardly move from their own positions, oscilating locally.
Problem 1 was solved thanks to Marius. I fixed it by simply putting update outside the event loop.
Problem 2 was fixed by adding the local update that I forgot.
if self.pos_z < global_best[2]: # This was not forgotten
global_best = [self.pos_x, self.pos_y, self.pos_z]
if self.pos_z < local_best[2]: # This was forgotten
local_best = [self.pos_x, self.pos_y, self.pos_z]

Converting joystick axis values to hex triplet codes

Using a PS4 controller in pygame, I already figured out how to capture axis rotation which can vary to a -1 or 1, but I don't know how to convert those numbers to a color ring like scale in order to turn it into a hex triplet number.
The values mimicking that of a color ring is more important than anything as I don't want the joystick capturing a color while it's not in motion. Picture
( Since that was a bit confusing, essentially I want to be able to move my joystick around and capture an accurate hex triplet number based on where it has moved )
This is my code so far:
import pygame
# Define some colors
BLACK = ( 0, 0, 0)
WHITE = ( 62, 210, 255)
# This is a simple class that will help us print to the screen
# It has nothing to do with the joysticks, just outputting the
# information.
class TextPrint:
def __init__(self):
self.reset()
self.font = pygame.font.Font(None, 25)
def print(self, screen, textString):
textBitmap = self.font.render(textString, True, BLACK)
screen.blit(textBitmap, [self.x, self.y])
self.y += self.line_height
def reset(self):
self.x = 25
self.y = 25
self.line_height = 30
def indent(self):
self.x += 10
def unindent(self):
self.x -= 10
pygame.init()
# Set the width and height of the screen [width,height]
size = [900, 1080]
screen = pygame.display.set_mode(size)
pygame.display.set_caption("PS4Testing")
#Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
# Initialize the joysticks
pygame.joystick.init()
# Get ready to print
textPrint = TextPrint()
# -------- Main Program Loop -----------
while done==False:
# EVENT PROCESSING STEP
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done=True # Flag that we are done so we exit this loop
screen.fill(WHITE)
textPrint.reset()
# Get count of joysticks
joystick_count = pygame.joystick.get_count()
# For each joystick:
for i in range(joystick_count):
joystick = pygame.joystick.Joystick(i)
joystick.init()
# Usually axis run in pairs, up/down for one, and left/right for
# the other.
axes = joystick.get_numaxes()
for i in range( axes ):
axis = joystick.get_axis( i )
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
textPrint.unindent()
# ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT
# Go ahead and update the screen with what we've drawn.
pygame.display.flip()
# Limit to 20 frames per second
clock.tick(20)
# Close the window and quit.
# If you forget this line, the program will 'hang'
# on exit if running from IDLE.
pygame.quit ()
Modified code from official pygame documentation
Any help would be greatly appreciated
My plan:
Find the angle of stick on joystick
Get RGB value using HSV and stick's angle
Convert to HEX
Finding the angle of joystick's stick
First, we need to find the joystick's angle. We can do this using the law of cosines and axis statements as lengths of the sides of a triangle (because they are from one point/center).
Store axis statements in this block:
for i in range( axes ):
axis = joystick.get_axis( i )
#Storing axis statement
if i == 0:
Xaxis = axis
elif i == 1:
Yaxis = axis
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
We store them because in for loop we can take only one statement at a time.
Then define a function that will return the angle of the stick. We need to use math module for pi and acos.
def get_angle(Xaxis,Yaxis):
#To avoid ZeroDivisionError
#P.S. - you can improve it a bit.
if Xaxis == 0:
Xaxis = 0.001
if Yaxis == 0:
Yaxis = 0.001
#defining the third side of a triangle using the Pythagorean theorem
b = ((Xaxis)**2 + (Yaxis)**2)**0.5
c = Xaxis
a = Yaxis
#Using law of cosines we'll find angle using arccos of cos
#math.acos returns angles in radians, so we need to multiply it by 180/math.pi
angle = math.acos((b**2 + c**2 - a**2) / (2*b*c)) * 180/math.pi
#It'll fix angles to be in range of 0 to 360
if Yaxis > 0:
angle = 360 - angle
return angle
Now get_angle(Xaxis,Yaxis) will return stick's angle. East is 0°, north is 90°, west is 180°, south is 270°.
Getting HSV and converting to RGB
We have stick's angle and we can use it to make an HSV color and then convert it to RGB, because Color Wheel is based on hue of the colors.
Hue has the same range as the stick's angle, so we can use stick's angle as hue.
Using the formulas from Wikipedia, define a function that converts HSV to RGB.
def hsv_to_rgb(H,S,V):
#0 <= H <= 360
#0 <= S <= 1
#0 <= V <= 1
C = V * S
h = H/60
X = C * (1 - abs(h % 2 -1))
#Yes, Python can compare like "0 <= 2 > 1"
if 0 <= h <= 1:
r = C; g = X; b = 0
elif 1 <= h <= 2:
r = X; g = C; b = 0
elif 2 <= h <= 3:
r = 0; g = C; b = X
elif 3 <= h <= 4:
r = 0; g = X; b = C
elif 4 <= h <= 5:
r = X; g = 0; b = C
elif 5 <= h < 6:
r = C; g = 0; b = X
m = V - C
#Final computing and converting from 0 - 1 to 0 - 255
R = int((r+m)*255)
G = int((g+m)*255)
B = int((b+m)*255)
return [R,G,B]
Now hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1) will return list of red, green and blue in range of 0 - 255 (for an easier conversion in hex). Saturation and Value are not necessary in this case.
Conversion to HEX
The easiest part. We need to get the list of RGB and convert values to hex.
colors = hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Converting to hex
lst = list(map(hex,colors))
#Cutting the "0x" part
for i in range(len(lst)):
lst[i] = lst[i][2:]
#If one of the colors has only one digit, extra 0 will be added for a better look
if len(lst[i]) == 1:
lst[i] = "0"+str(lst[i])
print(get_angle(Xaxis,Yaxis))
Something like #ff0000, #00ff00, and #0000ff will be printed.
You also can make your program to show color change in real time, simply add WHITE = colors in this block. DO NOT use it if you have photosensitive epilepsy.
Merging everything
Place both functions from first and second points somewhere at the beginning.
Add the storing from the first point to the block
for i in range( axes ):
axis = joystick.get_axis( i )
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
Add the conversion from the third point after the block. I recommend to make a death zone feature.
death_zone = 0.1
if abs(Xaxis) > death_zone or abs(Yaxis) > death_zone:
#If you prefer HSV color wheel, use hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Else if you prefer RGB color wheel, use hsv_to_rgb(360-get_angle(Xaxis,Yaxis),1,1)
colors = hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Converting to hex
lst = list(map(hex,colors))
#Cutting the "0x" part
for i in range(len(lst)):
lst[i] = lst[i][2:]
#If one of the colors has only one digit, extra 0 will be added for a better look
if len(lst[i]) == 1:
lst[i] = "0"+str(lst[i])
print("#"+"".join(lst))
That's how your code may look after all:
P.S. - you will probably have to change some code, because I thing my joystick's axis weren't properly captured.
import pygame
import math
# Define some colors
BLACK = ( 0, 0, 0)
WHITE = ( 62, 210, 255)
def hsv_to_rgb(H,S,V):
#Accirding to https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSV
#0 <= H <= 360
#0 <= S <= 1
#0 <= V <= 1
C = V * S
h = H/60
X = C * (1 - abs(h % 2 -1))
#Yes, Python can compare like "0 <= 2 > 1"
if 0 <= h <= 1:
r = C; g = X; b = 0
elif 1 <= h <= 2:
r = X; g = C; b = 0
elif 2 <= h <= 3:
r = 0; g = C; b = X
elif 3 <= h <= 4:
r = 0; g = X; b = C
elif 4 <= h <= 5:
r = X; g = 0; b = C
elif 5 <= h < 6:
r = C; g = 0; b = X
m = V - C
#Final computing and converting from 0 - 1 to 0 - 255
R = int((r+m)*255)
G = int((g+m)*255)
B = int((b+m)*255)
return [R,G,B]
def get_angle(Xaxis,Yaxis):
#To avoid ZeroDivisionError
#P.S. - you can improve it a bit.
if Xaxis == 0:
Xaxis = 0.001
if Yaxis == 0:
Yaxis = 0.001
#defining the third side of a triangle using the Pythagorean theorem
b = ((Xaxis)**2 + (Yaxis)**2)**0.5
c = Xaxis
a = Yaxis
#Using law of cosines we'll fing angle using arccos of cos
#math.acos returns angles in radians, so we need to multiply it by 180/math.pi
angle = math.acos((b**2 + c**2 - a**2) / (2*b*c)) * 180/math.pi
#It'll fix angles to be in range of 0 to 360
if Yaxis > 0:
angle = 360 - angle
return angle
# This is a simple class that will help us print to the screen
# It has nothing to do with the joysticks, just outputting the
# information.
class TextPrint:
def __init__(self):
self.reset()
self.font = pygame.font.Font(None, 25)
def print(self, screen, textString):
textBitmap = self.font.render(textString, True, BLACK)
screen.blit(textBitmap, [self.x, self.y])
self.y += self.line_height
def reset(self):
self.x = 25
self.y = 25
self.line_height = 30
def indent(self):
self.x += 10
def unindent(self):
self.x -= 10
pygame.init()
# Set the width and height of the screen [width,height]
size = [900, 1080]
screen = pygame.display.set_mode(size)
pygame.display.set_caption("PS4Testing")
#Loop until the user clicks the close button.
done = False
# Used to manage how fast the screen updates
clock = pygame.time.Clock()
# Initialize the joysticks
pygame.joystick.init()
# Get ready to print
textPrint = TextPrint()
# -------- Main Program Loop -----------
while done==False:
# EVENT PROCESSING STEP
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT: # If user clicked close
done=True # Flag that we are done so we exit this loop
screen.fill(WHITE)
textPrint.reset()
# Get count of joysticks
joystick_count = pygame.joystick.get_count()
# For each joystick:
for i in range(joystick_count):
joystick = pygame.joystick.Joystick(i)
joystick.init()
# Usually axis run in pairs, up/down for one, and left/right for
# the other.
axes = joystick.get_numaxes()
for i in range( axes ):
axis = joystick.get_axis( i )
#Storing axis statement
if i == 0:
Xaxis = axis
elif i == 1:
Yaxis = axis
textPrint.print(screen, "Axis {} value: {:>6.3f}".format(i, axis) )
textPrint.unindent()
#If joystick is not in the center
#Death zone is used to not capture joystick if it's very close to the center
death_zone = 0.1
if abs(Xaxis) > death_zone or abs(Yaxis) > death_zone:
#If you prefer HSV color wheel, use hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Else if you prefer RGB color wheel, use hsv_to_rgb(360-get_angle(Xaxis,Yaxis),1,1)
colors = hsv_to_rgb(get_angle(Xaxis,Yaxis),1,1)
#Converting to hex
lst = list(map(hex,colors))
#Cutting the "0x" part
for i in range(len(lst)):
lst[i] = lst[i][2:]
#If one of the colors has only one digit, extra 0 will be added for a better look
if len(lst[i]) == 1:
lst[i] = "0"+str(lst[i])
print("#"+"".join(lst))
#You can use it to see color change in real time.
#But I don't recomend to use it if you have photosensitive epilepsy.
#WHITE = colors
# ALL CODE TO DRAW SHOULD GO ABOVE THIS COMMENT
# Go ahead and update the screen with what we've drawn.
pygame.display.flip()
# Limit to 20 frames per second
clock.tick(20)
# Close the window and quit.
# If you forget this line, the program will 'hang'
# on exit if running from IDLE.
pygame.quit ()

Categories

Resources