I'm working on creating a Connect 4 game using Python turtle graphics. The main problem that has arisen is that when you use events like onkey or onclick, they are checked instantaneously followed by the remainder of the code. Is there a way to pause the code until an event happens, then continue on and, after looping, wait for another event to happen?
In the following code, the game gets setup and then the play() function is run. Within the play() function the program listens for onkey() events that allow the user to change which column it is over and drop the piece when ready. Then, it begins checking for 4 in a row either horizontally, vertically, or diagonally. It creates an error because the column lists are empty until I press the down key to drop a piece into a column of the board and append the piece to that column. I could create the columns already filled with None, '', or zeros but then I would have to change how my drop function works, as it currently bases the y value of where it is dropped to off of the number of items in the list. Is there a way to only run the check function once after each piece is dropped?
P.S. I'm relatively new to coding and this is my first time using this site. I have copy and pasted the code below:
import turtle
class Connect4:
"A connect 4 game"
def __init__(self):
self.pen = turtle.Turtle()
self.scr = turtle.Screen()
self.board = Connect4Board(self.pen, self.scr)
self.moves = 0
self.playing = True
self.piece = Connect4Piece(self.scr, self.board, self)
self.setup()
self.play()
def setup(self):
self.board.draw_board()
def play(self):
if self.moves == self.board.rows*self.board.columns:
game_over()
self.piece.st()
self.piece.goto(0, self.board.board_height/2)
while True:
self.scr.onkey(self.piece.prev_col, 'Left')
self.scr.onkey(self.piece.next_col, 'Right')
self.scr.onkey(self.piece.drop, 'Down')
self.scr.onkey(self.reset, 'r')
self.scr.listen()
self.check()
"Check if there is 4 pieces in a line horizontally, vertically or diagonally"
def check(self):
self.check_horizontal()
self.check_vertical()
self.check_diagonal()
def check_horizontal(self):
print("Checking horizontally")
for rows in range(self.board.rows):
for columns in range(self.board.columns - 3):
if self.board.squares[columns][rows] == 0:
continue
elif (self.board.squares[columns][rows] == self.board.squares[columns+1][rows] == self.board.squares[columns+2][rows] == self.board.squares[columns+3][rows]):
print(self.board.squares[columns][rows].color())
if self.board.squares[columns][rows].color() == ('red','red'):
print("Red wins!")
if self.board.squares[columns][rows].color() == ('black','black'):
print("Black wins!")
def check_vertical(self):
print("Checking vertically")
def check_diagonal(self):
print("Checking diagonally")
def reset(self):
self.board.reset()
self.piece.clear()
self.moves = 0
self.play()
def game_over(self):
self.pen.pu()
self.pen.goto(0, board_height/2 + 20)
self.pen.pd()
self.pen.write("Black wins!", align='center', font = ('Arial', 24, 'normal'))
self.pen.pu()
to.goto(0, board_height/2 + 10)
self.pen.write("Play Again?", align='center', font = ('Arial', 24, 'normal'))
self.playing = False
class Connect4Board:
def __init__(self, pen, screen):
#Used to create the board
self.square_size = 60
self.rows = 6
self.columns = 7
self.pen = pen
self.frame_color = 'blue'
self.board_length = self.square_size*self.columns
self.board_height = self.square_size*self.rows
self.squares = [[] for cols in range(self.columns)]
"""for cols in range(self.columns):
empty = []
self.squares.append(empty)"""
self.pen.speed(0)
self.pen.ht()
def _draw_square(self, x, y):
self.pen.pu()
self.pen.goto(x-self.square_size/2, y-self.square_size/2)
self.pen.pd()
self.pen.fillcolor(self.frame_color)
self.pen.begin_fill()
for sides in range(4):
self.pen.fd(self.square_size)
self.pen.left(90)
self.pen.end_fill()
def _draw_circle(self, x, y):
self.pen.pu()
self.pen.goto(x, y)
self.pen.pd()
self.pen.fillcolor('white')
self.pen.begin_fill()
self.pen.circle(self.square_size/2)
self.pen.end_fill()
def draw_board(self):
for row in range(self.rows):
for col in range(self.columns):
x = col*self.square_size - self.board_length/2 + self.square_size/2
y = row*self.square_size - self.board_length/2
self._draw_square(x, y)
self._draw_circle(x, y - self.square_size/2)
def reset(self):
self.squares = []
for cols in range(self.columns):
empty = []
self.squares.append(empty)
class Connect4Piece(turtle.Turtle):
def __init__(self, screen, board, game):
turtle.Turtle.__init__(self, screen)
self.board = board
self.speed(0)
self.pu()
self.shape('turtle')
self.cnum = 3
self.game = game
self.ht()
"Moves the piece to the left and updates it's column number"
def prev_col(self):
if self.xcor() - self.board.square_size > -self.board.board_length/2:
self.setx(self.xcor() - self.board.square_size)
self.cnum -= 1
"Moves the piece to the right and updates it's column number"
def next_col(self):
if self.xcor() + self.board.square_size < self.board.board_length/2:
self.setx(self.xcor() + self.board.square_size)
self.cnum += 1
def drop(self):
"Make sure the column isn't full. If it's not then move the turtle to the next available space in the row."
if len(self.board.squares[self.cnum]) != self.board.rows:
self.sety(len(self.board.squares[self.cnum]) *self.board.square_size - self.board.board_height/2 - self.board.square_size/2 )
"Stamp an image of the turtle to represent placing a piece"
self.stamp()
self.board.squares[self.cnum].append(self.color())
"Move the piece back above the middle column and set it's column back to 3"
self.goto(0, self.board.board_height/2)
self.cnum = 3
"Change the piece's color"
if self.color() == ('red','red'):
self.color('black')
else:
self.color('red')
self.game.moves += 1
print(self.game.moves, "moves")
game = Connect4()
Your program is structured incorrectly, as epitomized by this loop:
while True:
self.scr.onkey(self.piece.prev_col, 'Left')
self.scr.onkey(self.piece.next_col, 'Right')
self.scr.onkey(self.piece.drop, 'Down')
self.scr.onkey(self.reset, 'r')
self.scr.listen()
self.check()
First, while True: has no place in an event-driven environment like turtle. Second, these onkey calls only need to be done once during initialization -- they don't do anything at runtime. (Ditto listen())
I've restructured your code below to be event-based. You need to add (back) the checking code to determine if there's a winner or not:
from turtle import Turtle, Screen
class Connect4:
"A connect 4 game"
def __init__(self, screen):
self.pen = Turtle()
self.scr = screen
self.board = Connect4Board(self.pen)
self.moves = 0
self.playing = False
self.piece = Connect4Piece(self.board, self)
self.scr.tracer(False)
self.board.draw_board()
self.scr.tracer(True)
self.scr.onkey(self.piece.prev_col, 'Left')
self.scr.onkey(self.piece.next_col, 'Right')
self.scr.onkey(self.piece.drop, 'Down')
self.scr.onkey(self.reset, 'r')
self.scr.listen()
def play(self):
self.piece.showturtle()
self.piece.goto(0, self.board.board_height/2)
self.playing = True
def check(self):
"Check if there are 4 pieces in a line horizontally, vertically or diagonally"
if self.moves == self.board.rows * self.board.columns:
self.game_over()
if self.check_horizontal():
self.game_over()
if self.check_vertical():
self.game_over()
if self.check_diagonal():
self.game_over()
def check_horizontal(self):
print("Checking horizontally")
# implement this correctly
return False
def check_vertical(self):
print("Checking vertically")
# implement this
return False
def check_diagonal(self):
print("Checking diagonally")
# implement this
return False
def reset(self):
self.playing = False
self.board.reset()
self.piece.clear()
self.moves = 0
self.play()
def game_over(self):
self.playing = False
self.pen.penup()
self.pen.goto(0, self.board.board_height/2 + 20)
self.pen.pendown()
self.pen.write("Black wins!", align='center', font=('Arial', 24, 'normal'))
self.pen.penup()
self.pen.goto(0, self.board.board_height/2 + 10)
self.pen.write("Play Again?", align='center', font=('Arial', 24, 'normal'))
class Connect4Board:
def __init__(self, pen):
# Used to create the board
self.square_size = 60
self.rows = 6
self.columns = 7
self.pen = pen
self.frame_color = 'blue'
self.board_length = self.square_size * self.columns
self.board_height = self.square_size * self.rows
self.squares = [[] for _ in range(self.columns)]
self.pen.speed('fastest')
self.pen.hideturtle()
def _draw_square(self, x, y):
self.pen.penup()
self.pen.goto(x - self.square_size/2, y - self.square_size/2)
self.pen.pendown()
self.pen.fillcolor(self.frame_color)
self.pen.begin_fill()
for _ in range(4):
self.pen.forward(self.square_size)
self.pen.left(90)
self.pen.end_fill()
def _draw_circle(self, x, y):
self.pen.penup()
self.pen.goto(x, y)
self.pen.pendown()
self.pen.fillcolor('white')
self.pen.begin_fill()
self.pen.circle(self.square_size/2)
self.pen.end_fill()
def draw_board(self):
for row in range(self.rows):
for col in range(self.columns):
x = col * self.square_size - self.board_length/2 + self.square_size/2
y = row * self.square_size - self.board_length/2
self._draw_square(x, y)
self._draw_circle(x, y - self.square_size/2)
def reset(self):
self.squares = [[] for _ in range(self.columns)]
class Connect4Piece(Turtle):
def __init__(self, board, game):
super().__init__(shape='turtle', visible=False)
self.board = board
self.game = game
self.speed('fastest')
self.penup()
self.cnum = 3
def prev_col(self):
"Moves the piece to the left and updates it's column number"
if self.xcor() - self.board.square_size > -self.board.board_length/2:
self.setx(self.xcor() - self.board.square_size)
self.cnum -= 1
def next_col(self):
"Moves the piece to the right and updates it's column number"
if self.xcor() + self.board.square_size < self.board.board_length/2:
self.setx(self.xcor() + self.board.square_size)
self.cnum += 1
def drop(self):
"Make sure the column isn't full. If it's not then move the turtle to the next available space in the row."
if len(self.board.squares[self.cnum]) != self.board.rows:
self.sety(len(self.board.squares[self.cnum]) * self.board.square_size - self.board.board_height/2 - self.board.square_size/2)
# Stamp an image of the turtle to represent placing a piece
self.stamp()
self.board.squares[self.cnum].append(self.color())
# Move the piece back above the middle column and set it's column back to 3
self.goto(0, self.board.board_height/2)
self.cnum = 3
# Change the piece's color
self.color('black' if self.pencolor() == 'red' else 'red')
self.game.moves += 1
print(self.game.moves, "moves")
self.game.check()
screen = Screen()
game = Connect4(screen)
game.play()
screen.mainloop()
The graphics behave as expected but when you drop a turtle you'll see the stub checking functions get invoked and the player switches.
Also, read about Python comments vs. document strings -- you're mixing them up.
Related
I'm having an issue with my snakegame on the collision detection side. What I wrote originally to do so was this:
snakecollisionchecklist = len(snake.snake_coord_list)
snakecollisionchecklistset = len(set(snake.snake_coord_list))
if snakecollisionchecklist != snakecollisionchecklistset:
return
The idea being that if any segment position in the snake was equal to another segment position it would abort the program. The problem I found was that the .position() function I was using to find the position of each segment returned a 10 decimal float value, and for whatever reason this wasn't consistent even if the snake was sharing the same place. Converting it to int doesnt work either as that sometimes will make 60 59 etc. if the variance is high/low. To deal with this I put this together, but I feel like this isn't the most efficient way to handle this:
values_to_convert = segments.position()
xvaluestoconvert = round(values_to_convert[0],2)
yvaluestoconvert = round(values_to_convert[1],2)
self.snake_coord_list[segloc] = (xvaluestoconvert, yvaluestoconvert)
This just takes the individual pieces of the position() and forces it to be rounded. I also had a similar issue with trying to make food not spawn inside the snake, which ended up looking like this:
newloc= positionlist[0]
while newloc in positionlist:
randomx = round((random.randint(-7,7) * 20))
randomy = round((random.randint(-7,7) * 20))
newloc = [randomx, randomy]
self.goto(float(randomx),float(randomy))
print(f"{randomx} and {randomy}")
But then I still get overlap.
Is there a better way to do this? If you're curious about full code it's here:
gamesetup.py
import turtle as t
class FullSnake:
def __init__(self) -> None:
self.snake_part_list = []
self.snake_coord_list = [(-40,0),(-60,0), (-80,0)]
self.moment_prior_coord_list = [(-40,0),(-60,0), (-80,0)]
for nums in range(3):
new_segment = t.Turtle("circle")
new_segment.penup()
new_segment.color("white")
self.snake_part_list.append(new_segment)
new_segment.goto(self.snake_coord_list[nums])
self.snake_head = self.snake_part_list[0]
self.headingverification = 0
def add_segment(self,):
"""This adds a segment to the snake. the segment added should be initialized to be add the end of the list."""
new_segment = t.Turtle("circle")
new_segment.penup()
new_segment.color("white")
self.snake_part_list.append(new_segment)
#self.snake_coord_list.append((self.snake_part_list[0].xcor(),self.snake_part_list[0].ycor()))
#self.snake_coord_list.append(new_segment)
# current_final_seg = self.snake_part_list[-2]
current_final_seg_pos = self.moment_prior_coord_list[-1]
#self.move_snake()
new_segment.goto(current_final_seg_pos[0],current_final_seg_pos[1])
self.snake_coord_list.append(current_final_seg_pos)
def right(self):
if self.headingverification != 180:
self.snake_head.setheading(0)
# time.sleep(0.031)
# self.move_snake()
#ime.sleep(0.05)
def up(self):
if self.headingverification != 270:
self.snake_head.setheading(90)
# time.sleep(0.031)
#self.move_snake()
def left(self):
if self.headingverification != 0:
self.snake_head.setheading(180)
# time.sleep(0.031)
# self.move_snake()
def down(self):
if self.headingverification != 90:
self.snake_head.setheading(270)
#time.sleep(0.031)
# self.move_snake()
def move_snake(self):
"""moves snake. snake moves forward 20 units, and prior units get updated"""
self.moment_prior_coord_list = list(self.snake_coord_list)
for seg_num in range(len(self.snake_part_list)-1,0,-1):
new_x = round(self.snake_part_list[seg_num-1].xcor(),2)
new_y = round(self.snake_part_list[seg_num-1].ycor(),2)
self.snake_part_list[seg_num].goto(new_x, new_y)
self.snake_head.forward(20)
#print(self.snake_head.position())
for segments in self.snake_part_list:
segloc = self.snake_part_list.index(segments)
#for some reason segments.position() a varied float, so this just forces it to be samesies
values_to_convert = segments.position()
xvaluestoconvert = round(values_to_convert[0],2)
yvaluestoconvert = round(values_to_convert[1],2)
self.snake_coord_list[segloc] = (xvaluestoconvert, yvaluestoconvert)
print(self.snake_coord_list)
main.py:
import turtle as t
from gamesetup import FullSnake
import time
import food
import score_board as sb
screen = t.Screen()
screen.setup(food.screensize[0],food.screensize[1])
screen.bgcolor("Black")
screen.title("My Snake Game")
screen.tracer(0)
snakefood = food.Food()
snake = FullSnake()
scoreboard = sb.ScoreBoard()
screen.listen()
screen.onkey(snake.up,"Up")
screen.onkey(snake.down,"Down")
screen.onkey(snake.right,"Right")
screen.onkey(snake.left,"Left")
#game_is_on = True
#while game_is_on:
def snakemovefct():
snake.move_snake()
screen.update()
#what happens when you hit food, add to length of snake, increase score and move pellet to place that snake isnt
if snake.snake_head.distance(snakefood) <5:
snake.add_segment()
snakefood.refresh(snake.snake_coord_list)
scoreboard.score_event()
screen.update()
time.sleep(0.1)
#set gameover if you hit boundary
if snake.snake_head.xcor() > 150 or snake.snake_head.xcor() < -150 or snake.snake_head.ycor() > 150 or snake.snake_head.ycor() < -150:
scoreboard.game_over()
return
#check collision
snakecollisionchecklist = len(snake.snake_coord_list)
snakecollisionchecklistset = len(set(snake.snake_coord_list))
if snakecollisionchecklist != snakecollisionchecklistset:
scoreboard.game_over()
return
#this makes sure you cant press up and left when moving right to go left
snake.headingverification = snake.snake_head.heading()
#keep snake moving in loop, if no recursion it only moves once
screen.ontimer(snakemovefct,150)
screen.update()
screen.ontimer(snakemovefct,150)
screen.mainloop()
food.py
from turtle import Turtle
import random
screensize = (340, 340)
class Food(Turtle):
def __init__(self) -> None:
super().__init__()
self.shape("square")
self.penup()
#self.shapesize(stretch_len=0.5, stretch_wid=0.5)
self.color("red")
self.speed("fastest")
self.refresh([(20,0),(0,0), (-20,0)])
def refresh(self, positionlist):
newloc= positionlist[0]
while newloc in positionlist:
randomx = "{:.2f}".format(random.randint(-7,7) * 20)
randomy = "{:.2f}".format(random.randint(-7,7) * 20)
newloc = [randomx, randomy]
self.goto(float(randomx),float(randomy))
print(f"{randomx} and {randomy}")
scoreboard.py
import turtle as t
class ScoreBoard(t.Turtle):
def __init__(self) -> None:
super().__init__()
self.hideturtle()
self.penup()
self.pencolor("white")
self.speed("fastest")
self.score = 0
self.goto(0,120)
self.write(f"Current score: {self.score}", False, align="center")
def score_event(self):
self.score +=1
self.clear()
self.write(f"Current score: {self.score}", False, align="center")
def game_over(self):
self.goto(0,0)
self.write("GAME OVER", False, align="center")
im working on a game in python arcade, where the player runs around, shoots zombies, etc. very basic game. just recently, i started implementing the spawn over time part, but i get an error that just makes no sense:
AttributeError: 'MyGame' object has no attribute 'total_time'
it makes no sense because i have stated self.total_time in MyGame.
how do i fix this?
import arcade
import random
import math
import arcade.gui
import time
import timeit
SPRITE_SCALING = 0.35
SPRITE_SCALING_LASER = 0.8
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
SCREEN_TITLE = "zombier shooter"
ENEMY_COUNT = 20
BULLET_SPEED = 30
MOVEMENT_SPEED = 5
SPRITE_SPEED = 1
INDICATOR_BAR_OFFSET = 32
ENEMY_ATTACK_COOLDOWN = 1
PLAYER_HEALTH = 5
SCENE_MENU = 'SCENE_MENU'
SCENE_GAME = 'SCENE_GAME'
class QuitButton(arcade.gui.UIFlatButton):
def on_click(self, event: arcade.gui.UIOnClickEvent):
arcade.exit()
class Player(arcade.Sprite):
def update(self):
""" moves the player """
# move player.
self.center_x += self.change_x
self.center_y += self.change_y
# check for out of bounds
if self.left < 0:
self.left = 0
elif self.right > SCREEN_WIDTH - 1:
self.right = SCREEN_WIDTH - 1
if self.bottom < 0:
self.bottom = 0
elif self.top > SCREEN_HEIGHT - 1:
self.top = SCREEN_HEIGHT - 1
class Enemy(arcade.Sprite):
"""
This class represents the enemies on our screen.
"""
def follow_sprite(self, player_sprite):
"""
This function will move the current sprite towards whatever
other sprite is specified as a parameter.
"""
if self.center_y < player_sprite.center_y:
self.center_y += min(SPRITE_SPEED, player_sprite.center_y - self.center_y)
elif self.center_y > player_sprite.center_y:
self.center_y -= min(SPRITE_SPEED, self.center_y - player_sprite.center_y)
if self.center_x < player_sprite.center_x:
self.center_x += min(SPRITE_SPEED, player_sprite.center_x - self.center_x)
elif self.center_x > player_sprite.center_x:
self.center_x -= min(SPRITE_SPEED, self.center_x - player_sprite.center_x)
class MyGame(arcade.Window):
"""
main game class
"""
def __init__(self, width, height, title):
"""
initialises stuff
"""
# call the parent class initializer
super().__init__(width, height, title)
self.scene = SCENE_MENU
# variables that will hold sprite lists
self.player_list = None
# set up the player info
self.player_sprite = None
# track the current state of what key is pressed
self.left_pressed = False
self.right_pressed = False
self.up_pressed = False
self.down_pressed = False
# --- Required for all code that uses UI element,
# a UIManager to handle the UI.
self.manager = arcade.gui.UIManager()
self.manager.enable()
# Set background color
arcade.set_background_color(arcade.color.DARK_BLUE_GRAY)
# Create a vertical BoxGroup to align buttons
self.v_box = arcade.gui.UIBoxLayout()
# Create the buttons
start_button = arcade.gui.UIFlatButton(text="Start Game", width=200)
self.v_box.add(start_button.with_space_around(bottom=20))
settings_button = arcade.gui.UIFlatButton(text="Settings", width=200)
self.v_box.add(settings_button.with_space_around(bottom=20))
# Again, method 1. Use a child class to handle events.
quit_button = QuitButton(text="Quit", width=200)
self.v_box.add(quit_button)
# --- Method 2 for handling click events,
# assign self.on_click_start as callback
start_button.on_click = self.on_click_start
# --- Method 3 for handling click events,
# use a decorator to handle on_click events
#settings_button.event("on_click")
def on_click_settings(event):
print("Settings:", event)
# Create a widget to hold the v_box widget, that will center the buttons
self.manager.add(
arcade.gui.UIAnchorWidget(
anchor_x="center_x",
anchor_y="center_y",
child=self.v_box)
)
def setup(self):
""" Set up the game and initialize the variables. """
# sprite lists
self.player_list = arcade.SpriteList()
self.enemy_list = arcade.SpriteList()
self.bullet_list = arcade.SpriteList()
#setup timer
self.total_time = 0.0
# setup score
self.score = 0
self.score_text = None
# setup health info
self.health = 5
self.health_text = None
self.dead = None
# set up the player
self.player_sprite = Player(":resources:images/animated_characters/female_person/femalePerson_idle.png",
SPRITE_SCALING)
self.player_sprite.center_x = 50
self.player_sprite.center_y = 50
self.player_list.append(self.player_sprite)
def on_draw(self):
""" render the screen. """
# clear the screen
self.clear()
if self.scene == SCENE_MENU:
self.manager.draw()
elif self.scene == SCENE_GAME:
# draw all the sprites.
self.player_list.draw()
self.enemy_list.draw()
self.bullet_list.draw()
# put score text on the screen
output = f"Score: {self.score}"
arcade.draw_text(output, 10, 20, arcade.color.WHITE, 14)
# put helth text on the screen
output = f"Health: {self.health}"
arcade.draw_text(output, 10, 40, arcade.color.WHITE, 14)
if self.health <= 0:
self.player_sprite.remove_from_sprite_lists()
# put u died text on the screen
output = f"YOU DIED"
arcade.draw_text(output, 500, 400, arcade.color.RED, 50)
output = f"Click to Exit"
arcade.draw_text(output, 550, 300, arcade.color.BLACK, 30)
def on_click_start(self, event):
self.setup()
self.scene = SCENE_GAME
self.manager.disable()
print("Start:", event)
def on_mouse_press(self, x, y, button, modifiers):
""" Called whenever the mouse button is clicked. """
if self.health <= 0:
exit()
# create a bullet
bullet = arcade.Sprite(":resources:images/space_shooter/laserBlue01.png", SPRITE_SCALING_LASER)
# Position the bullet at the player's current location
start_x = self.player_sprite.center_x
start_y = self.player_sprite.center_y
bullet.center_x = start_x
bullet.center_y = start_y
# Get from the mouse the destination location for the bullet
# IMPORTANT! If you have a scrolling screen, you will also need
# to add in self.view_bottom and self.view_left.
dest_x = x
dest_y = y
# Do math to calculate how to get the bullet to the destination.
# Calculation the angle in radians between the start points
# and end points. This is the angle the bullet will travel.
x_diff = dest_x - start_x
y_diff = dest_y - start_y
angle = math.atan2(y_diff, x_diff)
# Angle the bullet sprite so it doesn't look like it is flying
# sideways.
bullet.angle = math.degrees(angle)
print(f"Bullet angle: {bullet.angle:.2f}")
# Taking into account the angle, calculate our change_x
# and change_y. Velocity is how fast the bullet travels.
bullet.change_x = math.cos(angle) * BULLET_SPEED
bullet.change_y = math.sin(angle) * BULLET_SPEED
# Add the bullet to the appropriate lists
self.bullet_list.append(bullet)
def update_player_speed(self):
# calculate speed based on the keys pressed
self.player_sprite.change_x = 0
self.player_sprite.change_y = 0
if self.up_pressed and not self.down_pressed:
self.player_sprite.change_y = MOVEMENT_SPEED
elif self.down_pressed and not self.up_pressed:
self.player_sprite.change_y = -MOVEMENT_SPEED
if self.left_pressed and not self.right_pressed:
self.player_sprite.change_x = -MOVEMENT_SPEED
elif self.right_pressed and not self.left_pressed:
self.player_sprite.change_x = MOVEMENT_SPEED
def on_update(self, delta_time):
""" updates values n stuff """
if self.scene == SCENE_GAME:
# call update to move the sprite
self.player_list.update()
# Call update on all sprites
self.bullet_list.update()
# go through each bullet
for bullet in self.bullet_list:
# check each bullet to see if it hit a zombie
hit_list = arcade.check_for_collision_with_list(bullet, self.enemy_list)
# if it did, remove the bullet
if len(hit_list) > 0:
bullet.remove_from_sprite_lists()
# for each enemy we hit with a bullet, remove enemy and add to the score
for enemy in hit_list:
enemy.remove_from_sprite_lists()
self.score += 1
# if bullet goes off screen, then remove it
if bullet.bottom > self.width or bullet.top < 0 or bullet.right < 0 or bullet.left > self.width:
bullet.remove_from_sprite_lists()
for enemy in self.enemy_list:
Enemy.follow_sprite(enemy, self.player_sprite)
# create a list of all sprites that had a collision with the player.
hit_list = arcade.check_for_collision_with_list(self.player_sprite, self.enemy_list)
# go through each sprite, if it got hit, then remove the sprite and lower score and health
for enemy in hit_list:
enemy.remove_from_sprite_lists()
self.score -= 1
self.health -= 1
# Accumulate the total time
self.total_time += delta_time
# Calculate minutes
minutes = int(self.total_time) // 60
# Calculate seconds by using a modulus (remainder)
seconds = int(self.total_time) % 60
# Calculate 100s of a second
seconds_100s = int((self.total_time - seconds) * 100)
if self.total_time > 5:
for i in range(5):
# enemy texture
enemy = arcade.Sprite(":resources:images/animated_characters/zombie/zombie_idle.png", SPRITE_SCALING)
enemy.center_x = random.randrange(SCREEN_WIDTH)
enemy.center_y = random.randrange(SCREEN_HEIGHT)
self.enemy_list.append(enemy)
self.total_time = 0.0
def on_key_press(self, key, modifiers):
"""called when user presses a key. """
if key == arcade.key.UP:
self.up_pressed = True
self.update_player_speed()
elif key == arcade.key.DOWN:
self.down_pressed = True
self.update_player_speed()
elif key == arcade.key.LEFT:
self.left_pressed = True
self.update_player_speed()
elif key == arcade.key.RIGHT:
self.right_pressed = True
self.update_player_speed()
def on_key_release(self, key, modifiers):
"""called when user releases a key. """
if key == arcade.key.UP:
self.up_pressed = False
self.update_player_speed()
elif key == arcade.key.DOWN:
self.down_pressed = False
self.update_player_speed()
elif key == arcade.key.LEFT:
self.left_pressed = False
self.update_player_speed()
elif key == arcade.key.RIGHT:
self.right_pressed = False
self.update_player_speed()
def main():
""" Main function """
MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
arcade.run()
if __name__ == "__main__":
main()
You defined self.total_time in setup() and not in __init__(), which should be a red-flag, because it could happen that self.total_time is actually accessed before setup() is called. And exactly this happened.
The error appears in on_update() when you try to run self.total_time += delta_time - in order for this line to work, self.total_time has to be initialised before - but setup() wasn't executed before, so self.total_time 'does not exist' yet.
So you can fix your code by moving the line self.total_time = 0.0 from setup() to __init__() - this way you make sure that you create this variable once the class gets initialised and before anything else gets executed.
If you plan on using variables in a class across different methods it's better to define them in __init__() to avoid such problems.
I am trying to create a simple game with turtle:
A turtle controlled by the user can shoot bullets when space is pressed
An enemy turtle is created at a random position
If the enemy is hit by the bullet, the enemy is destroyed and moves to a new position.
If the bullet leaves the screen, it disappears.
While the bullet is moving, the player should still be able to move.
If the enemy is not destroyed in 20 seconds, the player looses.
Therefore, I need some events to be controlled with the keyboard and others that get triggered after a certain time. I cannot think about a way of doing this without a loop inside which I check the distance between the bullet and the enemy, but if I do it like that, I cannot control the main turtle during the loop.
import turtle as trtl
from random import randint
import time
class Game():
def __init__(self):
self.scr = trtl.Screen()
self.scr.update()
self.player = trtl.Turtle()
self.player.shape('turtle')
self.player.penup()
trtl.onkeypress(self.forward,'w')
trtl.onkeypress(self.backwards,'s')
trtl.onkeypress(self.left,'a')
trtl.onkeypress(self.right,'d')
trtl.onkeypress(self.shoot,'space')
trtl.listen()
self.enemy = trtl.Turtle()
self.enemy.shape('square')
self.enemy.penup()
self.enemy.speed(0)
self.move_enemy()
self.bullet = trtl.Turtle()
self.bullet.penup()
self.bullet.hideturtle()
self.bulletShot = False
def forward(self):
self.player.forward(5)
def backwards(self):
self.player.back(5)
def left(self):
self.player.left(6)
def right(self):
self.player.right(6)
def shoot(self):
if self.bulletShot == False:
self.bullet.speed(0)
self.bullet.goto(self.player.pos())
self.bullet.seth(self.player.heading())
self.bullet.showturtle()
self.bulletShot = True
def move_enemy(self):
x = randint(-300,300)
y = randint(-300,300)
self.enemy.hideturtle()
self.enemy.goto(x,y)
self.enemy.showturtle()
def play(self):
startTime = time.time()
print(time.time() - startTime)
while time.time() - startTime < 20:
if self.bulletShot:
self.bullet.forward(1)
collision = self.bullet.distance(self.enemy.pos()) < 10
isIn = (self.bullet.pos()[0] <= 300 and
self.bullet.pos()[0] >= -300 and
self.bullet.pos()[1] <= 300 and
self.bullet.pos()[1] >= -300)
if not(isIn):
self.bullet.hideturtle()
self.bulletShot = False
elif collision:
self.bullet.hideturtle()
self.bulletShot = False
self.move_enemy()
startTime = time.time()
self.player.write('You loose')
self.scr.exitonclick()
game = Game()
game.play()
I tried a simpler version in which one turtle moves automatically in a loop and another turtle is controlled with the keyboard, and it works well.
import turtle as trtl
def up():
jane.sety(jane.pos()[1] + 10)
def down():
jane.sety(jane.pos()[1] - 10)
scr = trtl.Screen()
scr.update()
bob = trtl.Turtle()
bob.penup()
bob.seth(180)
bob.setx(300)
bob.speed(1)
jane = trtl.Turtle()
jane.penup()
trtl.onkeypress(up,'w')
trtl.onkeypress(down,'s')
trtl.listen()
while True:
if bob.pos()[0] > -300:
bob.forward(10)
else:
break
scr.exitonclick()
Is there a way of fixing this with turtle?
Let's redesign the game to work with turtle's event system by using a timer event to control the action while still using your timer to limit play time:
from turtle import Screen, Turtle
from random import randint
import time
class Game():
def __init__(self):
self.startTime = -1
self.screen = Screen()
self.screen.tracer(False)
self.player = Turtle()
self.player.shape('turtle')
self.player.penup()
self.enemy = Turtle()
self.enemy.shape('square')
self.enemy.penup()
self.move_enemy()
self.bullet = Turtle()
self.bullet.hideturtle()
self.bullet.penup()
self.bulletShot = False
self.screen.onkeypress(self.forward, 'w')
self.screen.onkeypress(self.backwards, 's')
self.screen.onkeypress(self.left, 'a')
self.screen.onkeypress(self.right, 'd')
self.screen.onkeypress(self.shoot, 'space')
self.screen.listen()
def forward(self):
self.player.forward(5)
self.screen.update()
def backwards(self):
self.player.back(5)
self.screen.update()
def left(self):
self.player.left(6)
self.screen.update()
def right(self):
self.player.right(6)
self.screen.update()
def shoot(self):
if not self.bulletShot:
self.bullet.setposition(self.player.position())
self.bullet.setheading(self.player.heading())
self.bullet.showturtle()
self.bulletShot = True
self.screen.update()
def move_enemy(self):
x = randint(-300, 300)
y = randint(-300, 300)
self.enemy.goto(x, y)
self.screen.update()
def play(self):
if self.startTime == -1:
self.startTime = time.time()
if self.bulletShot:
self.bullet.forward(1)
x, y = self.bullet.position()
if not(-300 <= x <= 300 and -300 <= y <= 300):
self.bullet.hideturtle()
self.bulletShot = False
elif self.bullet.distance(self.enemy.pos()) < 10:
self.bullet.hideturtle()
self.bulletShot = False
self.move_enemy()
self.startTime = time.time()
self.screen.update()
if time.time() - self.startTime > 20:
self.player.write('You loose!')
self.screen.update()
else:
self.screen.ontimer(self.play, 10)
screen = Screen()
game = Game()
game.play()
screen.mainloop()
Still bit crude but should be playable. You had a self.scr.update() call in your original code but without an initial call to tracer(), it does nothing. Here we're using tracer() and update() to speed up and smooth out the motion by manually controlling all screen updates.
I managed to solve it without changing much the code and without making the function play() recursive. The problem was that if the if inside de while needs an else, otherwise, the key press is not recorded. So I gave it something to do and now it works as I want.
while time.time() - startTime < 20:
if self.bulletShot:
self.bullet.forward(3)
...
else:
self.scr.update()
So, I'm writing a snake program using the tkinter Library. The program is globally working but I have a little problem with the inputs' treatment indeed if i give two input too quickly only the last one will be interpret. And i don't really know how to solve this i try to force the update after every player's input but it's clearly not the good solution because it force the snake to move and make it able to teleport so I'm would be glad if someone has an idea to solve this issue. There is my code I'm sure that it could be improved but for now I would like to focus on the first issue.
import tkinter as tk
import numpy.random as rd
class snake:
def __init__(self,n,m):
self.n = n
self.m = m
self.body = [(n//2,m//2),(n//2,m//2-1)]
self.lenght = 2
self.food = (0,0)
self.relocate_food()
self.Game = -2
self.vector = (0,1) #(0,-1) = up, (0,1) = right, (0,1) = down, (-1,0) = left
self.speed = 120
def up(self):
self.vector = (-1,0)
def right(self):
self.vector = (0,1)
def down(self):
self.vector = (1,0)
def left(self):
self.vector = (0,-1)
def relocate_food(self):
x = rd.randint(0,self.n)
y = rd.randint(0,self.m)
i = 0
test = True
while i<self.lenght and test:
if (x,y) == self.body[i]:
test = False
self.relocate_food()
else:
i += 1
if i == self.lenght:
self.food = (x,y)
def collide(self):
head = self.body[0]
for i in range(1,self.lenght):
if head == self.body[i]:
self.Game = -1
break
x,y = head
if x>=self.n or y>=self.m or x<0 or y<0:
self.Game = -1
def eat(self):
head = self.body[0]
if head == self.food:
self.lenght +=1
x0, y0 = self.body[-1]
x1, y1 = self.body[-2]
x = x0 - x1
y = y0 - y1
self.body.append((x0+x,y0+y))
self.relocate_food()
if self.lenght%5 == 0:
self.speed = int(self.speed * 0.90)
def move(self):
dx, dy = self.vector
last_x, last_y = self.body[0]
new_x = last_x + dx
new_y = last_y + dy
self.body[0] = (new_x, new_y)
for k in range(1, self.lenght):
x, y = self.body[k]
self.body[k] = (last_x,last_y)
last_x, last_y = x, y
return
class screen(snake):
def __init__(self,root,n,m):
snake.__init__(self,n,m)
root.minsize(n*20,m*20)
root.maxsize(n*20,m*20)
root.configure(background='white')
self.root = root
self.n = n
self.m = m
self.speed = 130
self.canvas = tk.Canvas(root, width = n*20, height =m*20,bg='black')
self.canvas.bind_all("<Key-Up>",self.move_up)
self.canvas.bind_all("<Key-Down>",self.move_down)
self.canvas.bind_all("<Key-Left>",self.move_left)
self.canvas.bind_all("<Key-Right>",self.move_right)
self.canvas.grid(row=1,column=0)
self.draw_snake()
self.draw_food()
def draw_snake(self):
y,x = self.body[0]
self.canvas.create_rectangle(x*20,y*20,(x+1)*20,(y+1)*20,fill= 'red4')
for k in range(1,self.lenght):
y,x = self.body[k]
self.canvas.create_rectangle(x*20,y*20,(x+1)*20,(y+1)*20,fill= 'red')
def draw_food(self):
y,x =self.food
self.canvas.create_rectangle(x*20,y*20,(x+1)*20,(y+1)*20,fill= 'green')
def move_up(self,event):
if self.Game == -2:
self.Game =0
self.up()
self.update()
else:
self.up()
def move_down(self,event):
if self.Game == -2:
self.Game =0
self.down()
self.update()
else:
self.down()
def move_left(self,event):
if self.Game == -2:
self.Game =0
self.left()
self.update()
else:
self.left()
def move_right(self,event):
if self.Game == -2:
self.Game =0
self.right()
self.update()
else:
self.right()
def update(self):
if self.Game == -2:
return
self.move()
self.eat()
self.collide()
if self.Game == -1:
self.root.destroy()
return
self.canvas.delete("all")
self.draw_snake()
self.draw_food()
self.root.after(self.speed,self.update)
window = tk.Tk()
snake = screen(window,35,35)
snake.update()
window.mainloop()
This is not really a bug. Your animation uses an 'update' function that is executed every 120ms. So if you hit 2 arrow keys within 120ms (i.e. between two successive calls of 'update'), only the last hit is considered, because only one translation vector can be considered for each snake update. Nobody can blame you on that point, as time controlled animation is a discrete process with a given time window. It's the only solution to get fluid and regular animation (all video games are based on such a process), so that's clearly correct.
However, your code may still be improved on several aspects. For instance, at each animation frame, you delete all Canvas items and create a whole new set of items ('create_rectangle') for the snake elements and the food. This is not very efficient. It would be better to simply change the coordinates of the items (check the Canvas.coords function from the doc). Note that animating a snake simply requires to move the previous tail position to the new head position, to give the illusion of a moving beast. So moving only 1 item (2 items when eating food) is necessary at each frame, which is must faster to process.
Thank Furas for the basic idea it was what i needed. New code with my correction :
def __init__(self, root,n,m):
"""
"""
self.input = []
"""
"""
def move_up(self,event):
if self.Game == -2:
self.Game =0
self.up()
self.update()
else:
self.input.append(0)
"""
Same for all the move
"""
def update(self):
if self.Game == -2:
return
if len(self.input)>3: #Make sure that the player doesn't stack instruction
self.pop()
try:
input = self.input.pop(0)
except:
input = -1
if input == 0:
self.up()
elif input == 1:
self.right()
elif input == 2:
self.down()
elif input == 3:
self.left()
self.move()
self.eat()
self.collide()
if self.Game == -1:
self.root.destroy()
return
self.canvas.delete("all")
self.draw_snake()
self.draw_food()
self.root.after(self.speed,self.update)
I've been working on a Pong game, using the turtle module in Python. Below is my code:
from turtle import Turtle, _Screen, TurtleScreen
from random import choice, randrange, randint
from tkinter import *
from tkinter import messagebox
class Field(_Screen):
def __init__(self, width = 1024, height = 600):
# Get __init__ from _Screen
super().__init__()
# Get __init__ from TurtleScreen (parent class of _Screen)
TurtleScreen.__init__(self, self._canvas)
if Turtle._screen is None:
Turtle._screen = self
self.width = width
self.height = height
self.setup(self.width+100,self.height+50)
self.screensize(self.width,self.height)
self.title("Pong")
self.bgcolor("black")
# Define size of score bar, above the play field
self.score_height = self.height/10
# Offset 0 axis line, due to score bar
self.yzero = -(self.height/2 - (self.height-self.score_height)/2)
class Ball(Turtle):
def __init__(self, velocity = 5, size = 1, color = "white"):
super().__init__(shape="circle",visible=False)
self.color(color)
self.speed(0)
self.penup()
self.shapesize(size,size)
self.setposition(0,field.yzero)
self.st()
self.velocity = velocity
self.dirrection = 0
def player_collision(self,player):
bx,by = self.position()
px,py = player.position()
x_off = ball.shapesize()[1]*10 + player.shapesize()[0]*10
y_off = ball.shapesize()[0]*10 + player.shapesize()[1]*10
if px > 0:
if (bx > px-x_off and by <= py+y_off and by >= py-y_off):
return True
elif px < 0:
if (bx < px+x_off and by <= py+y_off and by >= py-y_off):
return True
return False
def court_collision(self,court):
if (ball.ycor() >= ((field.height/2)-
court.score_height-self.shapesize()[0]*10)
or ball.ycor() <= -court.height/2+10): return True
return False
def out_left(self,court):
if self.xcor() <= -court.width/2: return True
return False
def out_right(self,court):
if self.xcor() >= court.width/2: return True
return False
class Player(Turtle):
def __init__(self, x=0, y=0, color="white", up=None, down=None):
super().__init__(shape="square",visible=False)
self.color(color)
self.speed(0)
self.penup()
# setup player paddle
self.shapesize(1,10)
# Rotate turtle, to allow the use of forward method
self.setheading(90)
self.setposition(x,y)
self.st()
self.score = 0
self.height = self.shapesize()[1]*10
self.velocity = 50
self.ondrag(self.drag)
self.upkey = up
self.downkey = down
def drag(self,x,y):
self.ondrag(None) # Disable event handler to avoid recursion
if y >= (field.height/2-field.score_height) - self.height:
y = (field.height/2-field.score_height) - self.height
if y <= -field.height/2+self.height:
y = -field.height/2+self.height
self.goto(self.xcor(),y)
self.ondrag(self.drag) # Reactivate event handler
def up(self):
#field.onkeypress(None, self.upkey)
if (self.ycor()+self.height <=
(field.height-field.score_height)/2+field.yzero):
self.forward(self.velocity)
#field.onkeypress(self.up, self.upkey)
def down(self):
#field.onkeypress(None, self.downkey)
if self.ycor()-self.height >= -field.height/2:
self.forward(-self.velocity)
#field.onkeypress(self.down, self.downkey)
class Score(Turtle):
def __init__(self):
super().__init__(visible=False)
self.speed(0)
self.color("white")
self.pensize(3)
# Draw lower border
self.penup()
self.goto(-field.width,-field.height/2)
self.pendown()
self.goto(field.width,-field.height/2)
# Draw upper border
self.penup()
self.goto(-field.width,field.height/2-field.score_height)
self.pendown()
self.goto(field.width,field.height/2-field.score_height)
self.penup()
# Draw score
self.goto(-100,field.height/2-field.score_height)
self.write(player2.score,font=("Monospace",50,"bold"))
self.goto(100,field.height/2-field.score_height)
self.write(player1.score,font=("Monospace",50,"bold"))
def update(self):
# Clear the previous score
for i in range(3):
self.undo()
# And write the new one
self.write(player2.score,font=("Monospace",50,"bold"))
self.goto(100,field.height/2-field.score_height)
self.write(player1.score,font=("Monospace",50,"bold"))
class Game:
def __init__(self,court,difficulty=0):
# Difficulty = increase in ball speed
self.difficulty = difficulty
# Setup event handlers
court.onkeypress(self.qt, "Escape")
court.onkeypress(player1.up, player1.upkey)
court.onkeypress(player1.down, player1.downkey)
court.onkeypress(player2.up, player2.upkey)
court.onkeypress(player2.down, player2.downkey)
court.onkey(self.pause, "p")
court.listen()
# Try to implement game pause. Not working, for the moment
#self.pause = False
#self.pause_count = 0
def reset(self):
ball.setposition(0,field.yzero)
player1.setposition(player1.xcor(),field.yzero)
player2.setposition(player2.xcor(),field.yzero)
ball.dirrection = choice([0,180]) # Left or right
ball.setheading(ball.dirrection+randrange(-80,80))
def restart(self):
self.reset()
self.player1_score = 0
self.player2_score = 0
self.difficulty = 0
def qt(self):
prompt = Tk()
prompt.eval('tk::PlaceWindow %s center' % prompt.winfo_toplevel())
prompt.withdraw()
answer = messagebox.askyesno("Quit", "Are you sure you want to quit?")
if answer == True:
field.bye()
return
# Not currently working
def pause(self):
if self.pause_count % 2 == 0:
self.pause == True
else:
self.pause = False
class Play(Turtle):
def __init__(self):
super().__init__(visible=False)
self.shape("square")
self.color("white")
self.speed(0)
self.penup()
self.shapesize(2,4)
self.goto(-field.width/2,field.height/2-field.score_height/2)
self.write("Play",font=("Monospace",20,"bold"))
field.onscreenclick(self.click)
def click(self,x,y):
print(x,y)
if (x <= -field.width/2+field.width/2/10 and
x >= -field.width/2 and
y >= field.height/2-field.score_height/2 and y <= field.height/2):
self.color("green")
self.clear()
self.write("Play",font=("Monospace",20,"bold"))
self.color("white")
self.clear()
self.write("Play",font=("Monospace",20,"bold"))
game.reset()
main()
def main():
ball.forward(ball.velocity+game.difficulty)
# Check for paddle collision
if ball.player_collision(player1) or ball.player_collision(player2):
ball.setheading(180 - ball.heading())
# Bounce from upper or lower border
if ball.court_collision(field):
ball.setheading(-ball.heading())
# Check for ball out of field and update player score
elif ball.out_right(field):
game.reset()
player2.score += 1
score.update()
game.difficulty += 0.5
elif ball.out_left(field):
game.reset()
player1.score += 1
score.update()
game.difficulty += 0.5
field.ontimer(main)
if __name__ == "__main__":
field = Field(1280,720)
ball = Ball()
player1 = Player(field.width/2,field.yzero,up = "Up", down = "Down")
player2 = Player(-field.width/2,field.yzero, up = "w", down = "s")
game = Game(field)
score = Score()
play_button = Play()
#field.mainloop()
It kind of works, but if you use the keys to play, it will eventually return an error:
RecursionError: maximum recursion depth exceeded while calling a
Python object
It would first seem that the problem is with the main() function, but the actual problem is with the event-handler for the key presses. If I only play using the mouse, the game will give no error, it will just feel jerky.
I've read the following subjects so far:
maximum recursion depth exceeded
Avoid RecursionError in turtle paint code
Turtle.onkeypress not working (Python)
And tried to implement the solutions found there. The only one that works for me is disabling the event-handler for the ondrag() function. If I try to use the same solution on the player (uncomment the lines in up() and down() methods of Player) it will only work when main() is not running. If I start the main() function it will just run once and deactivate.
So what I need help with is:
Avoiding the maximum recursion error. (it only happens when main() is active);
Making the ondrag function work without jerking the main() function;
the qt() method from the game class is not properly working if main() is running.
So do you guys think I can improve these aspects?
Edit: Below is the full traceback
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 1702, in __call__
return self.func(*args)
File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 746, in callit
func(*args)
File "C:/Users/Bogey/Desktop/asd.py", line 209, in main
ball.forward(ball.velocity+game.difficulty)
File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\turtle.py", line 1637, in forward
self._go(distance)
File "C:\Users\Bogey\AppData\Local\Programs\Python\Python37-32\lib\turtle.py", line 1604, in _go
ende = self._position + self._orient * distance
RecursionError: maximum recursion depth exceeded
The primary issue I see is you're doing too much needless calculation during game play. For example, consider the court_collision() method which gets called on every ball movement:
def court_collision(self,court):
if (ball.ycor() >= ((field.height/2)-
court.score_height-self.shapesize()[0]*10)
or ball.ycor() <= -court.height/2+10): return True
return False
Of all these values, only ball.ycor() is changing, the rest should have been computed before game play began and stashed so that the method looks more like:
def court_collision(self):
return not self.wall_top_offset > ball.ycor() > self.wall_bottom_offset
Ditto for player_collision(), drag(), etc.
The main() function should be really be the move() method of Ball.
I've other nits but they don't have anything to do with game performance.