1

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:

  1. Avoiding the maximum recursion error. (it only happens when main() is active);
  2. Making the ondrag function work without jerking the main() function;
  3. 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
Bogdan Prădatu
  • 325
  • 5
  • 15
  • Please show the actual traceback. It will tell us what function is being called recursively. – Bryan Oakley Jan 23 '19 at 19:30
  • @Bryan Oakley I have edited my original post and added the full traceback. But, as I wrote in a comment to cdlane's answer, the issue was from not giving a time argument for ontimer method. The only unresolved issue remains the mouse drag function, wich slows down the ball. – Bogdan Prădatu Jan 29 '19 at 21:36

1 Answers1

0

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.

cdlane
  • 40,441
  • 5
  • 32
  • 81
  • You have some good points. Indeed I make some unnecessary calculations and the main could have been implemented as a Ball method. I took your remarks into consideration and changed my code, but found that the issue described in my question was actually from the ontimer() method, which I was using without an argument for time. I suppose the frequency was too high to handle, or something, I don't know. Even 1ms was enough to avoid crashing. – Bogdan Prădatu Jan 22 '19 at 20:28