Related
I'm trying to make a tic tac toe game for 2 players - user and computer using minimax algorythm. My problem is that the code has an error and it doesn't work properly, but I'm not sure why. More explanation about code you can see in comments for code.
I have a TypeError: cannot unpack non-iterable Non-Type object for line 258 in main row, col = comp.evaluation(board)
Here is the code and comments:
file ttt1 with variables assignment:
import random
WIDTH = 600
HEIGHT = 600
ROWS = 3 #rows of the grid
COLS = 3 #cols of the grid
SQUARE_SIZE = WIDTH // COLS #size of one square of the grid - can be height devided by rows
LINE_WIDTH = 15 #width of the lines of the grid
#circle
CIRCLE_WIDTH = 20
RADIUS = SQUARE_SIZE // 3
#cross
CROSS_WIDTH = 20
OFFSET = 50
#define colors
BACKCOLOR = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))
LINE_COLOR = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))
CIRCLE = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))
CROSS = ((random.randint(0,255)), (random.randint(0, 255)), (random.randint(0,255)))
file with classes and etc:
import sys #for quitting the application
import copy
import random
import pygame
import numpy as np
from ttt1 import * #importing things from ttt1 file
#pygamecode and set up
pygame.init() #initializig pygame module
screen = pygame.display.set_mode((WIDTH, HEIGHT)) #setting up the screen - width and height
pygame.display.set_caption("Tic Tac Toe game with minimax")
screen.fill(BACKCOLOR) #filling in the screen with a background colors
class Board:
def __init__(self): #2 dimensional array of 0-s
self.squares = np.zeros((ROWS, COLS)) #parameters as tuple with 0-s, making empty squares
#self.mark_squares(1, 2, 3) #for testing
#print(self.squares) #return 0-s in terminal
self.empty_square = self.squares #list of squares
self.marked_square = 0 #state of a square
def final_condition(self, show=False):
#returning 0 if there is no win till now(not a draw condition), if player 1 wins - then returns 1; if player 2 wins returns 2
#vertical winning situation
for col in range(COLS):
if self.squares[0][col] == self.squares[1][col] == self.squares[2][col] != 0:
if show:
color = CIRCLE if self.squares[0][col] == 2 else CROSS
start_pos = (col * SQUARE_SIZE + SQUARE_SIZE // 2, 20)
final_pos = (col * SQUARE_SIZE + SQUARE_SIZE // 2, HEIGHT - 20)
pygame.draw.line(screen, color, start_pos, final_pos, LINE_WIDTH)
return self.squares[0][col] #return any of column from previous if stat
#horizontal winning situation
for row in range(ROWS):
if self.squares[row][0] == self.squares[row][1] == self.squares[row][2] != 0:
if show:
color = CIRCLE if self.squares[row][0] == 2 else CROSS
start_pos = (20, row * SQUARE_SIZE + SQUARE_SIZE // 2)
final_pos = (WIDTH - 20, row * SQUARE_SIZE + SQUARE_SIZE // 2)
pygame.draw.line(screen, color, start_pos, final_pos, LINE_WIDTH)
return self.squares[row][0]
#going down diagonal winning situation
if self.squares[0][0] == self.squares[1][1] == self.squares[2][2] != 0:
if show:
color = CIRCLE if self.squares[1][1] == 2 else CROSS
start_pos = (20, 20)
final_pos = (WIDTH - 20, HEIGHT - 20)
pygame.draw.line(screen, color, start_pos, final_pos, CROSS_WIDTH)
return self.squares[1][1] #11 is a common square between 2 diagonals so it's returning this one
#going up diagonal winning situation
if self.squares[2][0] == self.squares[1][1] == self.squares[0][2] != 0:
if show:
color = CIRCLE if self.squares[1][1] == 2 else CROSS
start_pos = (20, HEIGHT - 20)
final_pos = (WIDTH - 20, 20)
pygame.draw.line(screen, color, start_pos, final_pos, CROSS_WIDTH)
return self.squares[1][1]
return 0 #no winning yet
def mark_squares(self, row, col, player): #for 2 players, one will be marked and zeros in tuple will be replaced in a square that was marked by a player with a player
self.squares[row][col] = player
#ach time square is marked it's gonna be increased by one
self.marked_square += 1
def empty_square(self, row, col): #checking if the square is empy or not
return self.squares[row][col] == 0 #if return True than it's empty, if False than bot empty
def return_empty(self):
#square that is needed to be deleted from the empty squares
empty = []
for row in range(ROWS):
for col in range(COLS): #2 dimentional array(matrix)
if self.empty_square[row, col]:
empty.append((row, col))
return empty
def isfull(self):
return self.marked_square == 9 #max amount of squares when the board is full
def isempty(self):
return self.marked_square == 0 #return if the square is marked
class MinMax:
def __init__(self, level=1, player=2):
self.level = level #level of game
self.player = player #there are 2 players
def random_choice(self, board): #random choice function
empty = board.return_empty()
index = random.randrange(0, len(empty))
return empty[index] #row and col
def minmax(self, board, max):
#checking terminal case
case = board.final_condition() #case
#player 1 wins
if case == 1:
return 1, None #evaluation, move
#player 2 wins
if case == 2:
return -1, None
#draw
elif board.isfull():
return 0, None
#coding the algorythm
if max: #if player is maximizing
max_eval = -100 #minimal eva;uation that player gets from a specific board
best_move = None
empty_square = board.return_empty()
#loop each square inside of the list of empty squares
for(row, col) in empty_square:
temp_board = copy.deepcopy(board) #copying the board for testing on other boards
temp_board.mark_squares(row, col, 1)#marking the square to a copy
eval = self.minmax(temp_board, False)[0] #it's true because of changing the player, first position is an evaluation
if eval > max_eval:
max_eval = eval #save eval into max eval
best_move = (row, col)
return max_eval, best_move
elif not max: #if player is minimizing
min_eval = 100 #minimal eva;uation that player gets from a specific board
best_move = None
empty_square = board.return_empty()
#loop each square inside of the list of empty squares
for(row, col) in empty_square:
temp_board = copy.deepcopy(board) #copying the board for testing on other boards - a copy of the object is copied into another object
temp_board.mark_squares(row, col, self.player)#marking the square to a copy
eval = self.minmax(temp_board, True)[0] #it's true because of changing the player, first position is an evaluation
if eval < min_eval:
min_eval = eval
best_move = (row, col)
return min_eval, best_move #returning the min eval with respect to best move
def evaluation(self, main_board):
if self.level == 0:
#random choice
eval = 'random'
move = self.random_choice(main_board)
else:
#minmax alg choice
eval, move = self.minmax(main_board, False)
print(f'Minimax is chosen for marking the square in position {move} with an evaluation {eval}')
return move #move is the row and the col
#drowing lines for game - lines of the grid
class TicTac:
def __init__(self): #init method for the new game objects
self.board = Board() #creating a new board class
self.comp = MinMax()
self.player = 2 #who is the next player to mark in the squares #player 1 is crosses and player 2 is circles
self.gamemode = 'computer' #by human, or by computer (ai)
self.run = True
self.lines() #colling the method for showing the lines
def make_move(self, row, col):
self.board.mark_squares(row, col, self.player) #the last parameter changes from 1 to tictac.player
#print(board.squares) #testing, return 0 and 1 in console whether it's empty or not
self.draw_figure(row, col) #graphic displaying of the information
self.another_player()
#print(board.squares) #checking the another player works or not, result in console
def reset(self):
self.__init__() #restarting all attributes to default values
def lines(self): #defining the creating the grid function
#background color after reseting
screen.fill(BACKCOLOR)
#vertical lines
pygame.draw.line(screen, LINE_COLOR,(SQUARE_SIZE, 0), (SQUARE_SIZE, HEIGHT), LINE_WIDTH) #first line
pygame.draw.line(screen, LINE_COLOR,(WIDTH - SQUARE_SIZE, 0), (WIDTH - SQUARE_SIZE, HEIGHT), LINE_WIDTH) #y axis is 0, x axis is first; second line
#horizontal lines
pygame.draw.line(screen, LINE_COLOR,(0,SQUARE_SIZE), (WIDTH, SQUARE_SIZE), LINE_WIDTH) #first line
pygame.draw.line(screen, LINE_COLOR,(0, HEIGHT - SQUARE_SIZE), (WIDTH, HEIGHT - SQUARE_SIZE), LINE_WIDTH) #second line
def draw_figure(self, row, col):
if self.player == 1:
#drawing the cross
start_down_line = (col * SQUARE_SIZE + OFFSET, row * SQUARE_SIZE + OFFSET) #adjusting the down going line of the cross
end_down_line = (col * SQUARE_SIZE + SQUARE_SIZE - OFFSET, row * SQUARE_SIZE + SQUARE_SIZE - OFFSET)
pygame.draw.line(screen, CROSS, start_down_line, end_down_line, CROSS_WIDTH) #drawing the cross
#going up line
start_up_line = (col * SQUARE_SIZE + OFFSET, row * SQUARE_SIZE + SQUARE_SIZE - OFFSET)
end_up_line = (col * SQUARE_SIZE + SQUARE_SIZE - OFFSET, row * SQUARE_SIZE + OFFSET)
pygame.draw.line(screen, CROSS, start_up_line, end_up_line, CROSS_WIDTH) #drawing the line
elif self.player == 2:
#drawing the circle
center = (col * SQUARE_SIZE + SQUARE_SIZE // 2, row * SQUARE_SIZE + SQUARE_SIZE // 2) #center position
pygame.draw.circle(screen, CIRCLE, center, RADIUS, CIRCLE_WIDTH)
def another_player(self): #chenging the player that is marking the square to the next one
self.player = self.player % 2 + 1 #first operation will return the remainder ((model)%2) if the player is 1, than it adds 1 and there is o a remainder so the player is changing
def change_gamemode(self):
#self.gamemode = 'computer' if self.gamemode == 'user' else 'user'
if self.gamemode == 'user':
self.gamemode = 'computer'
else:
self.gamemode = 'user'
def isover(self):
return self.board.final_condition(show=True) != 0 or self.board.isfull()
def main(): #main function where the code will be executing
#game object
tictac = TicTac() #object and the class
board = tictac.board
comp = tictac.comp
#mainloop
while True:
#pygame events
for event in pygame.event.get():
if event.type == pygame.QUIT: #event is any actin happening in the game(pressing the key etc)
pygame.quit()
sys.exit() #for exiting the game
if event.type == pygame.KEYDOWN: #keydown event
#g-gamemode
if event.key == pygame.K_g:
tictac.change_gamemode()
# r = restart
if event.key == pygame.K_r:
tictac.reset()
board = tictac.board #board and ai will be reseted again and start from the initializing condition
comp = tictac.comp
# 0 -random computer alg
if event.key == pygame.K_0:
comp.level = 0
# 1 - random
if event.key == pygame.K_1:
comp.level = 1
if event.type == pygame.MOUSEBUTTONDOWN: #position of coursor in the pixels
pos = event.pos #position of pixels
row = pos[1]//SQUARE_SIZE #represents y axis of the board
col = pos[0]//SQUARE_SIZE #position 0 of x axis
#print(row, col) #testing
if board.empty_square[row,col] and tictac.run:
tictac.make_move(row, col)
if tictac.isover():
tictac.run = False
#board.mark_squares(row, col, 1) #in a position row col player number 1
#print(tictac.board.squares) #testing the board, returns information to console
if tictac.gamemode == 'computer' and tictac.player == comp.player and tictac.run:
#update the screen
pygame.display.update()
#computer methods
row, col = comp.evaluation(board)
#board.mark_squares(row, col, comp.player)
tictac.make_move(row, col)
if tictac.isover():
tictac.run = False
pygame.display.update() #updating the screen
#minimax alg =
""" terminal case and the base case - terminal case is when the game is over
3 situations: 1) player 1 wins, 2) player 2 wins, 3) draw //terminal cases
if __name__ == "__main__":
main()
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.
I' am trying to make a program using pygame. The program involves two tiles that switch colours and turn back into their original color once the two tiles have been clicked and after a 1-second delay. My problem is that whenever I tried to implement the pygame.time.delay , it delays the whole system and also affects the scoring mechanism of the program. I tried solving this problem by writing the codes found in handle_color_change and update methods in the game class
Any suggestions to fix this problem is greatly appreciated
import pygame,time,random
# User-defined functions
def main():
# for initializing all pygame modules
pygame.init()
# this creates the pygame display window
surface_width = 500
surface_height = 400
surface = pygame.display.set_mode((surface_width,surface_height))
# this sets the caption of the window to 'Pong'
pygame.display.set_caption('Painting')
# creates a game object
game = Game(surface, surface_width, surface_height)
# this starts the game loop by calling the play method found in the game object
game.play()
# quits pygame and cleans up the pygame window
pygame.quit()
# User-defined classes
class Game:
# an object in this class represents the complete game
def __init__(self,surface,surface_width,surface_height):
# # Initialize a Game.
# - self is the Game to initialize
# - surface is the display window surface object
# - surface_width is the display width size
# - surface_height is the display height size
# attributes that are needed to run any game
self.surface = surface
self.surface_width = surface_width
self.surface_height = surface_height
self.close_clicked = False
self.surface_color = pygame.Color('black')
# attributes that are needed to run this specific game
self.FPS = 60
self.game_clock = pygame.time.Clock()
self._continue = True
self.score = [0,0]
self.max_mismatch = 5
# Game specific objects
self.default_color = 'white'
self.color_options = ('blue' , 'red', 'yellow', 'green')
self.tile_width = 50
self.tile_height = 150
self.tile_left = Tile( self.default_color, (self.surface_width/3) - self.tile_width, (self.surface_height/2)/ 2 , self.tile_width, self.tile_height , self.surface)
self.tile_right = Tile(self.default_color, self.surface_width/2 + self.tile_width, (self.surface_height/2)/ 2
,self.tile_width, self.tile_height , self.surface)
def play(self):
# this is main game loop
# plays the game until the players has closed the window or the score of a players equals the max score
# - self is the game that should be continued or not
while not self.close_clicked:
self.main_handle_events()
self.draw()
self.update()
self.game_clock.tick(self.FPS)
def draw(self):
# this draws the circle and the rectangles that are needed for this specific game
# -self is the Game to draw
self.surface.fill(self.surface_color)
self.tile_left.draw()
self.tile_right.draw()
self.display_score_match()
self.display_score_mismatch(self.surface_width)
pygame.display.update() # makes the updated surface appear on the display
def update(self):
events = pygame.event.get()
if self.handle_color_change(events):
pygame.time.delay(1000)
self.tile_left.set_color(self.default_color)
self.tile_right.set_color(self.default_color)
self.update_score()
def main_handle_events(self):
# handles each user events by changing the game state appropriately
# -self is the Game of whose events are handled
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
self.close_clicked = True
if event.type == pygame.MOUSEBUTTONDOWN:
self.handle_color_change(event)
#self.update_score()
#self.handle_color_change(event)
def display_score_match(self):
text_string = 'Match: ' + str(self.score[0])
text_colour = pygame.Color('white')
text_font = pygame.font.SysFont('Times New Roman',25)
text_image = text_font.render(text_string, True, text_colour)
text_pos = [0,0]
self.surface.blit(text_image, text_pos)
def display_score_mismatch(self, surface_width):
text_string = 'Mismatch: ' + str(self.score[1])
text_colour = pygame.Color('white')
text_font = pygame.font.SysFont('Times New Roman',25)
text_image = text_font.render(text_string, True, text_colour)
text_pos = [(surface_width - text_image.get_width()), 0]
self.surface.blit(text_image, text_pos)
def handle_color_change(self, event):
tile_clicked = 0
change_white = False
if event.button == 1 and self.tile_left.inside_tile(event.pos) == True:
self.tile_left.set_color(random.choice(self.color_options))
tile_clicked += 1
if event.button == 1 and self.tile_right.inside_tile(event.pos) == True:
self.tile_right.set_color(random.choice(self.color_options))
tile_clicked +=1
if tile_clicked == 2:
change_white = True
tile_clicked = 0
return change_white
def update_score(self):
if self.tile_left.color_match(self.tile_right) == True:
self.score[0] = self.score[0] + 1
else:
self.score[1] = self.score[1] + 1
class Tile:
def __init__(self, rect_color, rect_left, rect_top, rect_width, rect_height,surface):
# Initialize a rectabgle which is used as a paintbrush.
# - self is the rectangle to initialize
# - rect_color is the pygame.Color of the dot
# - rect_height is the int length of the rectangle in the y axis
# - rect_width is the int width of the rectangle in the x axis
# - rect_left is the int coordinate position of the rectangle in the x axis
# - rect_top is the int coordinate position of the rectangle in the y axis
# - rect_velocity is a list of x and y components and the speed of which the rectangles can move
self.rect_colour = pygame.Color(rect_color)
self.rect_height = rect_height
self.rect_width = rect_width
self.rect_left = rect_left
self.rect_top = rect_top
self.surface = surface
self.rect_parameters = pygame.Rect(rect_left, rect_top, rect_width, rect_height)
def draw(self):
# draws the rectangle on the surface
# - self is the rectangle
pygame.draw.rect(self.surface, self.rect_colour, self.rect_parameters)
def inside_tile(self, position):
inside = False
if self.rect_parameters.collidepoint(position):
inside = True
return inside
def set_color(self, color):
self.rect_colour = pygame.Color(color)
def color_match(self, other_tile):
match = False
if self.rect_colour == other_tile.rect_colour:
match = True
return match
main()
Never use a delay in your application loop. Use the application loop. Compute the point in time when the rectangles have to change color back. Change the color after the current time is greater than the calculated point of time.
In pygame the system time can be obtained by calling pygame.time.get_ticks(), which returns the number of milliseconds since pygame.init() was called. See pygame.time module.
Add 2 attributes self.tile_clicked = 0 and self.turn_white_time = 0 to the class Game:
class Game:
def __init__(self,surface,surface_width,surface_height):
# [...]
self.tile_clicked = []
self.turn_white_time = 0
Compute the the point in time when the rectangles have to change color back after the 2nd rectangle was clicked:
class Game:
# [...]
def handle_color_change(self, event):
if len(self.tile_clicked) < 2:
if 1 not in self.tile_clicked:
if event.button == 1 and self.tile_left.inside_tile(event.pos) == True:
self.tile_left.set_color(random.choice(self.color_options))
self.tile_clicked.append(1)
if 2 not in self.tile_clicked:
if event.button == 1 and self.tile_right.inside_tile(event.pos) == True:
self.tile_right.set_color(random.choice(self.color_options))
self.tile_clicked.append(2)
if len(self.tile_clicked) == 2:
delay_time = 1000 # 1000 milliseconds == 1 second
self.turn_white_time = pygame.time.get_ticks() + delay_time
get_ticks() returns the current time. A time is just a number. get_ticks() + delay_time is a time in the future. When the program is running, the current time is continuously retrieved and compared with turn_white_time. At some point the current time is greater than turn_white_time and the color of the rectangles is changed.
Change back to the white color after the current time is greater than the calculated point of time in update:
class Game:
# [...]
def update(self):
current_time = pygame.time.get_ticks()
if len(self.tile_clicked) == 2 and current_time > self.turn_white_time:
self.tile_left.set_color(self.default_color)
self.tile_right.set_color(self.default_color)
self.tile_clicked = []
Complete example:
import pygame,time,random
# User-defined functions
def main():
# for initializing all pygame modules
pygame.init()
# this creates the pygame display window
surface_width = 500
surface_height = 400
surface = pygame.display.set_mode((surface_width,surface_height))
# this sets the caption of the window to 'Pong'
pygame.display.set_caption('Painting')
# creates a game object
game = Game(surface, surface_width, surface_height)
# this starts the game loop by calling the play method found in the game object
game.play()
# quits pygame and cleans up the pygame window
pygame.quit()
# User-defined classes
class Game:
# an object in this class represents the complete game
def __init__(self,surface,surface_width,surface_height):
# # Initialize a Game.
# - self is the Game to initialize
# - surface is the display window surface object
# - surface_width is the display width size
# - surface_height is the display height size
# attributes that are needed to run any game
self.surface = surface
self.surface_width = surface_width
self.surface_height = surface_height
self.close_clicked = False
self.surface_color = pygame.Color('black')
# attributes that are needed to run this specific game
self.FPS = 60
self.game_clock = pygame.time.Clock()
self._continue = True
self.score = [0,0]
self.max_mismatch = 5
# Game specific objects
self.default_color = 'white'
self.color_options = ('blue' , 'red', 'yellow', 'green')
self.tile_width = 50
self.tile_height = 150
self.tile_left = Tile( self.default_color, (self.surface_width/3) - self.tile_width, (self.surface_height/2)/ 2 , self.tile_width, self.tile_height , self.surface)
self.tile_right = Tile(self.default_color, self.surface_width/2 + self.tile_width, (self.surface_height/2)/ 2
,self.tile_width, self.tile_height , self.surface)
self.tile_clicked = []
self.turn_white_time = 0
def play(self):
# this is main game loop
# plays the game until the players has closed the window or the score of a players equals the max score
# - self is the game that should be continued or not
while not self.close_clicked:
self.main_handle_events()
self.draw()
self.update()
self.game_clock.tick(self.FPS)
def draw(self):
# this draws the circle and the rectangles that are needed for this specific game
# -self is the Game to draw
self.surface.fill(self.surface_color)
self.tile_left.draw()
self.tile_right.draw()
self.display_score_match()
self.display_score_mismatch(self.surface_width)
pygame.display.update() # makes the updated surface appear on the display
def update(self):
current_time = pygame.time.get_ticks()
if len(self.tile_clicked) == 2 and current_time > self.turn_white_time:
self.tile_left.set_color(self.default_color)
self.tile_right.set_color(self.default_color)
self.tile_clicked = []
def main_handle_events(self):
# handles each user events by changing the game state appropriately
# -self is the Game of whose events are handled
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
self.close_clicked = True
if event.type == pygame.MOUSEBUTTONDOWN:
self.handle_color_change(event)
#self.update_score()
#self.handle_color_change(event)
def display_score_match(self):
text_string = 'Match: ' + str(self.score[0])
text_colour = pygame.Color('white')
text_font = pygame.font.SysFont('Times New Roman',25)
text_image = text_font.render(text_string, True, text_colour)
text_pos = [0,0]
self.surface.blit(text_image, text_pos)
def display_score_mismatch(self, surface_width):
text_string = 'Mismatch: ' + str(self.score[1])
text_colour = pygame.Color('white')
text_font = pygame.font.SysFont('Times New Roman',25)
text_image = text_font.render(text_string, True, text_colour)
text_pos = [(surface_width - text_image.get_width()), 0]
self.surface.blit(text_image, text_pos)
def handle_color_change(self, event):
if len(self.tile_clicked) < 2:
if 1 not in self.tile_clicked:
if event.button == 1 and self.tile_left.inside_tile(event.pos) == True:
self.tile_left.set_color(random.choice(self.color_options))
self.tile_clicked.append(1)
if 2 not in self.tile_clicked:
if event.button == 1 and self.tile_right.inside_tile(event.pos) == True:
self.tile_right.set_color(random.choice(self.color_options))
self.tile_clicked.append(2)
if len(self.tile_clicked) == 2:
delay_time = 1000 # 1000 milliseconds == 1 second
self.turn_white_time = pygame.time.get_ticks() + delay_time
def update_score(self):
if self.tile_left.color_match(self.tile_right) == True:
self.score[0] = self.score[0] + 1
else:
self.score[1] = self.score[1] + 1
class Tile:
def __init__(self, rect_color, rect_left, rect_top, rect_width, rect_height,surface):
# Initialize a rectabgle which is used as a paintbrush.
# - self is the rectangle to initialize
# - rect_color is the pygame.Color of the dot
# - rect_height is the int length of the rectangle in the y axis
# - rect_width is the int width of the rectangle in the x axis
# - rect_left is the int coordinate position of the rectangle in the x axis
# - rect_top is the int coordinate position of the rectangle in the y axis
# - rect_velocity is a list of x and y components and the speed of which the rectangles can move
self.rect_colour = pygame.Color(rect_color)
self.rect_height = rect_height
self.rect_width = rect_width
self.rect_left = rect_left
self.rect_top = rect_top
self.surface = surface
self.rect_parameters = pygame.Rect(rect_left, rect_top, rect_width, rect_height)
def draw(self):
# draws the rectangle on the surface
# - self is the rectangle
pygame.draw.rect(self.surface, self.rect_colour, self.rect_parameters)
def inside_tile(self, position):
inside = False
if self.rect_parameters.collidepoint(position):
inside = True
return inside
def set_color(self, color):
self.rect_colour = pygame.Color(color)
def color_match(self, other_tile):
match = False
if self.rect_colour == other_tile.rect_colour:
match = True
return match
main()
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)
I am creating a battleship-type game. I am using .blit to display images that I load using the function pygame.image.load. I was wondering, is it possible to make images like this appear/disappear at different points?
My code is as follows:
import random, sys, pygame
from pygame.locals import *
# Set variables, like screen width and height
# globals
FPS = 60 #Determines the number of frames per second
REVEALSPEED = 2 #Determines the speed at which the squares reveals after being clicked
WINDOWWIDTH = 800 #Width of game window
WINDOWHEIGHT = 600 #Height of game window
TILESIZE = 40 #Size of the squares in each grid(tile)
MARKERSIZE = 40 #Size of the box which contatins the number that indicates how many ships in this row/col
BUTTONHEIGHT = 20 #Height of a standard button
BUTTONWIDTH = 40 #Width of a standard button
TEXT_HEIGHT = 25 #Size of the text
TEXT_LEFT_POSN = 10 #Where the text will be positioned
BOARDWIDTH = 6 #Number of grids horizontally
BOARDHEIGHT = 6 #Number of grids vertically
DISPLAYWIDTH = 200 #Width of the game board
EXPLOSIONSPEED = 10 #How fast the explosion graphics will play
XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * TILESIZE) - DISPLAYWIDTH - MARKERSIZE) / 2) #x-position of the top left corner of board
YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * TILESIZE) - MARKERSIZE) / 2) #y-position of the top left corner of board
#Colours which will be used by the game
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
GREEN = ( 0, 204, 0)
GRAY = ( 60, 60, 60)
BLUE = ( 0, 50, 255)
YELLOW = (255, 255, 0)
DARKGRAY =( 40, 40, 40)
transparent = (0, 0, 0, 0)
#Determine what to colour each element of the game
BGCOLOR = GRAY
BUTTONCOLOR = GREEN
TEXTCOLOR = WHITE
TILECOLOR = GREEN
BORDERCOLOR = BLUE
TEXTSHADOWCOLOR = BLUE
SHIPCOLOR = YELLOW
HIGHLIGHTCOLOR = BLUE
def main():
"""
The main function intializes the variables which will be used by the game.
"""
global DISPLAYSURF, FPSCLOCK, BASICFONT, HELP_SURF, HELP_RECT, NEW_SURF, \
NEW_RECT, SHOTS_SURF, SHOTS_RECT, BIGFONT, COUNTER_SURF, \
COUNTER_RECT, HBUTTON_SURF, EXPLOSION_IMAGES
pygame.init()
FPSCLOCK = pygame.time.Clock()
#Fonts used by the game
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
BASICFONT = pygame.font.Font('freesansbold.ttf', 20)
BIGFONT = pygame.font.Font('freesansbold.ttf', 50)
# Create and label the buttons
HELP_SURF = BASICFONT.render("HELP", True, WHITE)
HELP_RECT = HELP_SURF.get_rect()
HELP_RECT.topleft = (WINDOWWIDTH - 180, WINDOWHEIGHT - 350)
NEW_SURF = BASICFONT.render("NEW GAME", True, WHITE)
NEW_RECT = NEW_SURF.get_rect()
NEW_RECT.topleft = (WINDOWWIDTH - 200, WINDOWHEIGHT - 200)
# The 'Shots:' label at the top
SHOTS_SURF = BASICFONT.render("Shots: ", True, WHITE)
SHOTS_RECT = SHOTS_SURF.get_rect()
SHOTS_RECT.topleft = (WINDOWWIDTH - 750, WINDOWHEIGHT - 570)
# Load the explosion graphics from the /img folder
EXPLOSION_IMAGES = [
pygame.image.load("blowup1.png"), pygame.image.load("blowup2.png"),
pygame.image.load("blowup3.png"),pygame.image.load("blowup4.png"),
pygame.image.load("blowup5.png"),pygame.image.load("blowup6.png")]
# Set the title in the menu bar to 'Battleship'
pygame.display.set_caption('Battleship')
# Keep the game running at all times
while True:
shots_taken = run_game() #Run the game until it stops and save the result in shots_taken
show_gameover_screen(shots_taken) #Display a gameover screen by passing in shots_taken
def run_game():
greenButton = pygame.image.load('green-button-icon-png-13.png')
greenButton = pygame.transform.scale(greenButton, (75,75))
rect = greenButton.get_rect()
rect = rect.move((150, 475))
redButton = pygame.image.load('red-button-1426817_960_720.png')
redButton = pygame.transform.scale(redButton, (85,85))
rect2 = redButton.get_rect()
rect2 = rect2.move((400, 475))
"""
Function is executed while a game is running.
returns the amount of shots taken
"""
revealed_tiles = generate_default_tiles(False) #Contains the list of the tiles revealed by user
# main board object,
main_board = generate_default_tiles(None) #Contains the list of the ships which exists on board
ship_objs = ['raft'] # List of the ships available
main_board = add_ships_to_board(main_board, ship_objs) #call add_ships_to_board to add the list of ships to the main_board
mousex, mousey = 0, 0 #location of mouse
counter = [] #counter to track number of shots fired
while True:
# counter display (it needs to be here in order to refresh it)
COUNTER_SURF = BASICFONT.render(str(len(counter)), True, WHITE)
COUNTER_RECT = SHOTS_SURF.get_rect()
COUNTER_RECT.topleft = (WINDOWWIDTH - 680, WINDOWHEIGHT - 570)
# Fill background
DISPLAYSURF.fill(BGCOLOR)
# draw the buttons
DISPLAYSURF.blit(HELP_SURF, HELP_RECT)
DISPLAYSURF.blit(NEW_SURF, NEW_RECT)
DISPLAYSURF.blit(SHOTS_SURF, SHOTS_RECT)
DISPLAYSURF.blit(COUNTER_SURF, COUNTER_RECT)
DISPLAYSURF.blit(greenButton, rect)
DISPLAYSURF.blit(redButton, rect2)
greenButton.fill(transparent)
DISPLAYSURF.blit(greenButton, rect)
# Draw the tiles onto the board and their respective markers
draw_board(main_board, revealed_tiles)
mouse_clicked = False
check_for_quit()
#Check for pygame events
for event in pygame.event.get():
if event.type == MOUSEBUTTONUP:
if HELP_RECT.collidepoint(event.pos): #if the help button is clicked on
DISPLAYSURF.fill(BGCOLOR)
show_help_screen() #Show the help screen
elif NEW_RECT.collidepoint(event.pos): #if the new game button is clicked on
main() #goto main, which resets the game
else: #otherwise
mousex, mousey = event.pos #set mouse positions to the new position
mouse_clicked = True #mouse is clicked but not on a button
elif event.type == MOUSEMOTION: #Detected mouse motion
mousex, mousey = event.pos #set mouse positions to the new position
#Check if the mouse is clicked at a position with a ship piece
tilex, tiley = get_tile_at_pixel(mousex, mousey)
if tilex != None and tiley != None:
if not revealed_tiles[tilex][tiley]: #if the tile the mouse is on is not revealed
draw_highlight_tile(tilex, tiley) # draws the hovering highlight over the tile
if not revealed_tiles[tilex][tiley] and mouse_clicked: #if the mouse is clicked on the not revealed tile
reveal_tile_animation(main_board, [(tilex, tiley)])
revealed_tiles[tilex][tiley] = True #set the tile to now be revealed
if check_revealed_tile(main_board, [(tilex, tiley)]): # if the clicked position contains a ship piece
left, top = left_top_coords_tile(tilex, tiley)
blowup_animation((left, top))
if check_for_win(main_board, revealed_tiles): # check for a win
counter.append((tilex, tiley))
return len(counter) # return the amount of shots taken
counter.append((tilex, tiley))
pygame.display.update()
FPSCLOCK.tick(FPS)
def generate_default_tiles(default_value):
"""
Function generates a list of 10 x 10 tiles. The list will contain tuples
('shipName', boolShot) set to their (default_value).
default_value -> boolean which tells what the value to set to
returns the list of tuples
"""
default_tiles = [[default_value]*BOARDHEIGHT for i in range(BOARDWIDTH)]
return default_tiles
def blowup_animation(coord):
"""
Function creates the explosition played if a ship is shot.
coord -> tuple of tile coords to apply the blowup animation
"""
for image in EXPLOSION_IMAGES: # go through the list of images in the list of pictures and play them in sequence
#Determine the location and size to display the image
image = pygame.transform.scale(image, (TILESIZE+10, TILESIZE+10))
DISPLAYSURF.blit(image, coord)
pygame.display.flip()
FPSCLOCK.tick(EXPLOSIONSPEED) #Determine the delay to play the image with
def check_revealed_tile(board, tile):
"""
Function checks if a tile location contains a ship piece.
board -> the tiled board either a ship piece or none
tile -> location of tile
returns True if ship piece exists at tile location
"""
return board[tile[0][0]][tile[0][1]] != None
def reveal_tile_animation(board, tile_to_reveal):
"""
Function creates an animation which plays when the mouse is clicked on a tile, and whatever is
behind the tile needs to be revealed.
board -> list of board tile tuples ('shipName', boolShot)
tile_to_reveal -> tuple of tile coords to apply the reveal animation to
"""
for coverage in range(TILESIZE, (-REVEALSPEED) - 1, -REVEALSPEED): #Plays animation based on reveal speed
draw_tile_covers(board, tile_to_reveal, coverage)
def draw_tile_covers(board, tile, coverage):
"""
Function draws the tiles according to a set of variables.
board -> list; of board tiles
tile -> tuple; of tile coords to reveal
coverage -> int; amount of the tile that is covered
"""
left, top = left_top_coords_tile(tile[0][0], tile[0][1])
if check_revealed_tile(board, tile):
pygame.draw.rect(DISPLAYSURF, SHIPCOLOR, (left, top, TILESIZE,
TILESIZE))
else:
pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, TILESIZE,
TILESIZE))
if coverage > 0:
pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left, top, coverage,
TILESIZE))
pygame.display.update()
FPSCLOCK.tick(FPS)
def check_for_quit():
"""
Function checks if the user has attempted to quit the game.
"""
for event in pygame.event.get(QUIT):
pygame.quit()
sys.exit()
def check_for_win(board, revealed):
"""
Function checks if the current board state is a winning state.
board -> the board which contains the ship pieces
revealed -> list of revealed tiles
returns True if all the ships are revealed
"""
for tilex in range(BOARDWIDTH):
for tiley in range(BOARDHEIGHT):
if board[tilex][tiley] != None and not revealed[tilex][tiley]: # check if every board with a ship is revealed, return false if not
return False
return True
def draw_board(board, revealed):
"""
Function draws the game board.
board -> list of board tiles
revealed -> list of revealed tiles
"""
#draws the grids depending on its state
for tilex in range(BOARDWIDTH):
for tiley in range(BOARDHEIGHT):
left, top = left_top_coords_tile(tilex, tiley)
if not revealed[tilex][tiley]:
pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left, top, TILESIZE,
TILESIZE))
else:
if board[tilex][tiley] != None:
pygame.draw.rect(DISPLAYSURF, SHIPCOLOR, (left, top,
TILESIZE, TILESIZE))
else:
pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top,
TILESIZE, TILESIZE))
#draws the horizontal lines
for x in range(0, (BOARDWIDTH + 1) * TILESIZE, TILESIZE):
pygame.draw.line(DISPLAYSURF, DARKGRAY, (x + XMARGIN + MARKERSIZE,
YMARGIN + MARKERSIZE), (x + XMARGIN + MARKERSIZE,
WINDOWHEIGHT - YMARGIN))
#draws the vertical lines
for y in range(0, (BOARDHEIGHT + 1) * TILESIZE, TILESIZE):
pygame.draw.line(DISPLAYSURF, DARKGRAY, (XMARGIN + MARKERSIZE, y +
YMARGIN + MARKERSIZE), (WINDOWWIDTH - (DISPLAYWIDTH + MARKERSIZE *
2), y + YMARGIN + MARKERSIZE))
def add_ships_to_board(board, ships):
"""
Function goes through a list of ships and add them randomly into a board.
board -> list of board tiles
ships -> list of ships to place on board
returns list of board tiles with ships placed on certain tiles
"""
new_board = board[:]
ship_length = 0
for ship in ships: #go through each ship declared in the list
#Randomly find a valid position that fits the ship
valid_ship_position = False
while not valid_ship_position:
xStartpos = random.randint(0, (BOARDHEIGHT-1))
yStartpos = random.randint(0, (BOARDHEIGHT-1))
isHorizontal = random.randint(0, 1) #vertical or horizontal positioning
#Type of ship and their respective length
if 'battleship' in ship:
ship_length = 5
elif 'destroyer' in ship:
ship_length = 4
elif 'cruiser'in ship:
ship_length = 3
elif 'submarine' in ship:
ship_length = 2
elif 'raft' in ship:
ship_length = 1
#check if position is valid
valid_ship_position, ship_coords = make_ship_position(new_board,
xStartpos, yStartpos, isHorizontal, ship_length, ship)
#add the ship if it is valid
if valid_ship_position:
for coord in ship_coords:
new_board[coord[0]][coord[1]] = ship
return new_board
def make_ship_position(board, xPos, yPos, isHorizontal, length, ship):
"""
Function makes a ship on a board given a set of variables
board -> list of board tiles
xPos -> x-coordinate of first ship piece
yPos -> y-coordinate of first ship piece
isHorizontal -> True if ship is horizontal
length -> length of ship
returns tuple: True if ship position is valid and list ship coordinates
"""
ship_coordinates = [] #the coordinates the ship will occupy
if isHorizontal:
for i in range(length):
if (i+xPos > (BOARDHEIGHT-1)) or (board[i+xPos][yPos] != None) or \
hasAdjacent(board, i+xPos, yPos, ship): #if the ship goes out of bound, hits another ship, or is adjacent to another ship
return (False, ship_coordinates) #then return false
else:
ship_coordinates.append((i+xPos, yPos))
else:
for i in range(length):
if (i+yPos > (BOARDHEIGHT-1)) or (board[xPos][i+yPos] != None) or \
hasAdjacent(board, xPos, i+yPos, ship): #if the ship goes out of bound, hits another ship, or is adjacent to another ship
return (False, ship_coordinates) #then return false
else:
ship_coordinates.append((xPos, i+yPos))
return (True, ship_coordinates) #ship is successfully added
def hasAdjacent(board, xPos, yPos, ship):
"""
Funtion checks if a ship has adjacent ships
board -> list of board tiles
xPos -> x-coordinate of first ship piece
yPos -> y-coordinate of first ship piece
ship -> the ship being checked for adjacency
returns true if there are adjacent ships and false if there are no adjacent ships
"""
for x in range(xPos-1,xPos+2):
for y in range(yPos-1,yPos+2):
if (x in range (BOARDHEIGHT)) and (y in range (BOARDHEIGHT)) and \
(board[x][y] not in (ship, None)):
return True
return False
def left_top_coords_tile(tilex, tiley):
"""
Function calculates and returns the pixel of the tile in the top left corner
tilex -> int; x position of tile
tiley -> int; y position of tile
returns tuple (int, int) which indicates top-left pixel coordinates of tile
"""
left = tilex * TILESIZE + XMARGIN + MARKERSIZE
top = tiley * TILESIZE + YMARGIN + MARKERSIZE
return (left, top)
def get_tile_at_pixel(x, y):
"""
Function finds the corresponding tile coordinates of pixel at top left, defaults to (None, None) given a coordinate.
x -> int; x position of pixel
y -> int; y position of pixel
returns tuple (tilex, tiley)
"""
for tilex in range(BOARDWIDTH):
for tiley in range(BOARDHEIGHT):
left, top = left_top_coords_tile(tilex, tiley)
tile_rect = pygame.Rect(left, top, TILESIZE, TILESIZE)
if tile_rect.collidepoint(x, y):
return (tilex, tiley)
return (None, None)
def draw_highlight_tile(tilex, tiley):
"""
Function draws the hovering highlight over the tile.
tilex -> int; x position of tile
tiley -> int; y position of tile
"""
left, top = left_top_coords_tile(tilex, tiley)
pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR,
(left, top, TILESIZE, TILESIZE), 4)
def show_help_screen():
"""
Function display a help screen until any button is pressed.
"""
line1_surf, line1_rect = make_text_objs('Press a key to return to the game',
BASICFONT, TEXTCOLOR)
line1_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT)
DISPLAYSURF.blit(line1_surf, line1_rect)
line2_surf, line2_rect = make_text_objs(
'This is a battleship puzzle game. Your objective is ' \
'to sink all the ships in as few', BASICFONT, TEXTCOLOR)
line2_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 3)
DISPLAYSURF.blit(line2_surf, line2_rect)
line3_surf, line3_rect = make_text_objs('shots as possible. The markers on'\
' the edges of the game board tell you how', BASICFONT, TEXTCOLOR)
line3_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 4)
DISPLAYSURF.blit(line3_surf, line3_rect)
line4_surf, line4_rect = make_text_objs('many ship pieces are in each'\
' column and row. To reset your game click on', BASICFONT, TEXTCOLOR)
line4_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 5)
DISPLAYSURF.blit(line4_surf, line4_rect)
line5_surf, line5_rect = make_text_objs('the "New Game" button.',
BASICFONT, TEXTCOLOR)
line5_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 6)
DISPLAYSURF.blit(line5_surf, line5_rect)
while check_for_keypress() == None: #Check if the user has pressed keys, if so go back to the game
pygame.display.update()
FPSCLOCK.tick()
def check_for_keypress():
"""
Function checks for any key presses by pulling out all KEYDOWN and KEYUP events from queue.
returns any KEYUP events, otherwise return None
"""
for event in pygame.event.get([KEYDOWN, KEYUP, MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION]):
if event.type in (KEYDOWN, MOUSEBUTTONUP, MOUSEBUTTONDOWN, MOUSEMOTION):
continue
return event.key
return None
def make_text_objs(text, font, color):
"""
Function creates a text.
text -> string; content of text
font -> Font object; face of font
color -> tuple of color (red, green blue); colour of text
returns the surface object, rectangle object
"""
surf = font.render(text, True, color)
return surf, surf.get_rect()
def show_gameover_screen(shots_fired):
"""
Function display a gameover screen when the user has successfully shot at every ship pieces.
shots_fired -> the number of shots taken before game is over
"""
DISPLAYSURF.fill(BGCOLOR)
titleSurf, titleRect = make_text_objs('Congrats! Puzzle solved in:',
BIGFONT, TEXTSHADOWCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
DISPLAYSURF.blit(titleSurf, titleRect)
titleSurf, titleRect = make_text_objs('Congrats! Puzzle solved in:',
BIGFONT, TEXTCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)
DISPLAYSURF.blit(titleSurf, titleRect)
titleSurf, titleRect = make_text_objs(str(shots_fired) + ' shots',
BIGFONT, TEXTSHADOWCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2 + 50))
DISPLAYSURF.blit(titleSurf, titleRect)
titleSurf, titleRect = make_text_objs(str(shots_fired) + ' shots',
BIGFONT, TEXTCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2 + 50) - 3)
DISPLAYSURF.blit(titleSurf, titleRect)
pressKeySurf, pressKeyRect = make_text_objs(
'Press a key to try to beat that score.', BASICFONT, TEXTCOLOR)
pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)
DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
while check_for_keypress() == None: #Check if the user has pressed keys, if so start a new game
pygame.display.update()
FPSCLOCK.tick()
if __name__ == "__main__": #This calls the game loop
main()
Generally there's two ways of doing this.
The more common way is to re-paint the entire screen on each iteration of the main loop.
For example:
### Main Loop
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.MOUSEBUTTONUP ):
mouse_position = pygame.mouse.get_pos() # Location of mouse-click
player_moves.append ( PlayerMove( mouse_position ) ) # Make a new move
# Re-paint the screen
window.fill( OCEAN_BLUE_COLOUR ) # clear the screen
# Paint each of the players turns
for m in player_moves:
m.draw( window ) # paints a hit or miss icon
pygame.display.flip()
Alternatively, instead of re-painting everything, only change the items that have updated, or when events happen. This is close to the "dirty-rectangles" method of updating.
# Initially paint the screen
window.fill( OCEAN_BLUE_COLOUR ) # clear the screen
### Main Loop
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.MOUSEBUTTONUP ):
mouse_position = pygame.mouse.get_pos() # Location of mouse-click
move = playerMove( mouse_position )
move.draw( window )
pygame.display.flip()
The difficulty of the second method, is that the program needs to clean up after the movement of on-screen images (otherwise they will leave a trail). Obviously in a battleship game, no on-screen elements move - but things like re-drawing scores or starting a new game will need to somehow erase the background. I'm not sure if this will also re-paint the window after it has been occluded by another window.
If you are a beginner programmer, I would use the first method. It's much simpler, and a lot of games are written this way.