How to manage event-handler recursion in python turtle? - python

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.

Related

Python - easily converting float to rounded number for set checking (turtle graphics)

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")

I am trying to make enemies spawn over time in my python arcade game, but i get a simple error that makes no sense

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.

How to Level Up

I'm currently working on building the game Pong. One aspect of the game is displaying the current level. What I want to happen is have 3 levels. When the score is equal to 5, I want to display "Level 2". When the score is equal to 10, I want to display "Level 3" and then at 15 I want to display "You Win!" What's happening right now is as soon as the score is equal to 5, the Level number counts up every time it updates the window (super fast). Where or how do I write this so that it functions the way I want?
I created a function in the Pong class called level_up for this.
import arcade
import random
# These are Global constants to use throughout the game
SCREEN_WIDTH = 400
SCREEN_HEIGHT = 300
BALL_RADIUS = 10
PADDLE_WIDTH = 10
PADDLE_HEIGHT = 50
MOVE_AMOUNT = 5
SCORE_HIT = 1
SCORE_MISS = 5
LEVEL_UP = 1
"""Point will identify the x and y coordinates of the ball"""
class Point:
def __init__(self):
"""The __init__ initializes the x and y coordinates"""
self.x=float(0)
self.y=float(0)
"""Velocity will identify the velocity"""
class Velocity:
def __init__(self):
"""The __init__ initializes the x and y velocity"""
self.dx=float(0)
self.dy=float(0)
"""Ball will identify the coordinates and the movement of the ball"""
class Ball:
def __init__(self):
"""The __init__ will initialize the Point and Velocity class values"""
self.center=Point()
#self.center will call the self.x and self.y float values that are found in the Point class.
self.velocity=Velocity()
#self.velocity will call the self.dx and self.dy values that are found in the Velocity class.
self.velocity.dx=2
self.velocity.dy=2
def draw(self):
"""This creates the ball"""
arcade.draw_circle_filled(self.center.x, self.center.y,
BALL_RADIUS, arcade.color.FLUORESCENT_YELLOW)
def advance(self):
self.center.x += self.velocity.dx
self.center.y += self.velocity.dy
def bounce_horizontal(self):
self.velocity.dx *=-1
def bounce_vertical(self):
self.velocity.dy *=-1
def restart(self):
self.center.x=0
self.center.y=random.uniform(0,SCREEN_HEIGHT)
self.velocity.dx=random.uniform(1,8)
self.velocity.dy=random.uniform(0,8)
"""Paddle will represent the paddle"""
class Paddle:
def __init__(self):
"""The __init__ will initialize the location of the paddle"""
self.center=Point()
#self.center calls the Point class
self.center.x=SCREEN_WIDTH
self.center.y=SCREEN_HEIGHT//2
def draw(self):
arcade.draw_rectangle_filled(self.center.x, self.center.y,
PADDLE_WIDTH, PADDLE_HEIGHT, arcade.color.FLUORESCENT_PINK)
def move_up(self):
self.center.y+=MOVE_AMOUNT
if self.center.y > SCREEN_HEIGHT:
self.center.y -= MOVE_AMOUNT
def move_down(self):
self.center.y-=MOVE_AMOUNT
if self.center.y < 0:
self.center.y += MOVE_AMOUNT
class Pong(arcade.Window):
"""
This class handles all the game callbacks and interaction
It assumes the following classes exist:
Point
Velocity
Ball
Paddle
This class will then call the appropriate functions of
each of the above classes.
You are welcome to modify anything in this class,
but should not have to if you don't want to.
"""
def __init__(self, width, height):
"""
Sets up the initial conditions of the game
:param width: Screen width
:param height: Screen height
"""
super().__init__(width, height)
self.ball = Ball()
self.paddle = Paddle()
self.score = 0
self.level = 1
# These are used to see if the user is
# holding down the arrow keys
self.holding_left = False
self.holding_right = False
arcade.set_background_color(arcade.color.BLACK)
def on_draw(self):
"""
Called automatically by the arcade framework.
Handles the responsiblity of drawing all elements.
"""
# clear the screen to begin drawing
arcade.start_render()
# draw each object
self.ball.draw()
self.paddle.draw()
self.draw_score()
self.draw_level()
def draw_score(self):
"""
Puts the current score on the screen
"""
score_text = "Score: {}".format(self.score)
start_x = 190
start_y = SCREEN_HEIGHT - 20
arcade.draw_text(score_text, start_x=start_x, start_y=start_y, font_size=12,
color=arcade.color.DEEP_SKY_BLUE)
def draw_level(self):
"""Displays the level"""
level_text = f"LEVEL {self.level}"
start_x= 175
start_y=SCREEN_HEIGHT - 40
arcade.draw_text(level_text, start_x=start_x, start_y=start_y, font_size=20,
color=arcade.color.ELECTRIC_GREEN)
def update(self, delta_time):
"""
Update each object in the game.
:param delta_time: tells us how much time has actually elapsed
"""
# Move the ball forward one element in time
self.ball.advance()
# Check to see if keys are being held, and then
# take appropriate action
self.check_keys()
# check for ball at important places
self.check_miss()
self.check_hit()
self.check_bounce()
def check_hit(self):
"""
Checks to see if the ball has hit the paddle
and if so, calls its bounce method.
:return:
"""
too_close_x = (PADDLE_WIDTH / 2) + BALL_RADIUS
too_close_y = (PADDLE_HEIGHT / 2) + BALL_RADIUS
if (abs(self.ball.center.x - self.paddle.center.x) < too_close_x and
abs(self.ball.center.y - self.paddle.center.y) < too_close_y and
self.ball.velocity.dx > 0):
# we are too close and moving right, this is a hit!
self.ball.bounce_horizontal()
self.score += SCORE_HIT
def level_up(self):
if self.score ==5:
self.level += LEVEL_UP
if self.score ==10:
self.level += LEVEL_UP
def check_miss(self):
"""
Checks to see if the ball went past the paddle
and if so, restarts it.
"""
if self.ball.center.x > SCREEN_WIDTH:
# We missed!
self.score -= SCORE_MISS
self.ball.restart()
def check_bounce(self):
"""
Checks to see if the ball has hit the borders
of the screen and if so, calls its bounce methods.
"""
if self.ball.center.x < 0 and self.ball.velocity.dx < 0:
self.ball.bounce_horizontal()
if self.ball.center.y < 0 and self.ball.velocity.dy < 0:
self.ball.bounce_vertical()
if self.ball.center.y > SCREEN_HEIGHT and self.ball.velocity.dy > 0:
self.ball.bounce_vertical()
def check_keys(self):
"""
Checks to see if the user is holding down an
arrow key, and if so, takes appropriate action.
"""
if self.holding_left:
self.paddle.move_down()
if self.holding_right:
self.paddle.move_up()
def on_key_press(self, key, key_modifiers):
"""
Called when a key is pressed. Sets the state of
holding an arrow key.
:param key: The key that was pressed
:param key_modifiers: Things like shift, ctrl, etc
"""
if key == arcade.key.LEFT or key == arcade.key.DOWN:
self.holding_left = True
if key == arcade.key.RIGHT or key == arcade.key.UP:
self.holding_right = True
def on_key_release(self, key, key_modifiers):
"""
Called when a key is released. Sets the state of
the arrow key as being not held anymore.
:param key: The key that was pressed
:param key_modifiers: Things like shift, ctrl, etc
"""
if key == arcade.key.LEFT or key == arcade.key.DOWN:
self.holding_left = False
if key == arcade.key.RIGHT or key == arcade.key.UP:
self.holding_right = False
# Creates the game and starts it going
window = Pong(SCREEN_WIDTH, SCREEN_HEIGHT)
arcade.run()
I just answered my own question, I changed the level_up function to say:
def level_up(self):
if self.score ==5 and self.level==1:
self.level += LEVEL_UP
if self.score ==10 and self.level==2:
self.level += LEVEL_UP

Time and keypress events with Python's Turtle

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()

Need help on snake program about user's input

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)

Categories