Search code examples
pythonpython-3.xturtle-graphicspython-turtle

Detecting a collision between turtles/out of bounds/a line of the turtle and a turtle


So I'm coding a tron game in turtle and the last part I need to do is to code the collisions between the turtles themselves, out of bound collisions, and collisions between the line made by the a turtle and the other turtle. I coded the out of bound collisions and they didn't work, and I also tried coding the player against player collisions which didn't work either. I've got no clue how to do the other one (where it outputs something when the turtle collides with the line generated by the other turtle). I thought of using an array in some way but I'm not sure how those work. Can anyone help me out?

Code for the out of bound collisions:

#Width and height of the screen
width = turtle.window_width()
height = turtle.window_height()

def collisions():
    while True:
      if(math.isclose(blue_player.xcor(), red_player.xcor(), abs_tol=1e-10) and
       math.isclose(blue_player.ycor(), red_player.ycor(), abs_tol=1e-10)):
        TurtleCrash = turtle.Turtle(visible=False)
        TurtleCrash.color("white")
        style = ('Arial', 25, 'italic')
        TurtleCrash.write("Red and Blue Crashed!\nGame over!", font=style, align='center')
        break
         
      if blue_player.xcor() > width or blue_player.xcor() < -1*width:
        BlueTurtleOut = turtle.Turtle(visible=False)
        BlueTurtleOut.color("white")
        style = ('Arial', 25, 'italic')
        BlueTurtleOut.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break
        
      if blue_player.ycor() > height or blue_player.ycor() < -1*height:
        BlueTurtleOut2 = turtle.Turtle(visible=False)
        BlueTurtleOut2.color("white")
        style = ('Arial', 25, 'italic')
        BlueTurtleOut2.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break
  
      if red_player.xcor() > width or red_player.xcor() < -1*width:
        RedTurtleOut = turtle.Turtle(visible=False)
        RedTurtleOut.color("white")
        style = ('Arial', 25, 'italic')
        RedTurtleOut.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break
  
      if red_player.ycor() > height or red_player.ycor() < -1*height:
        RedTurtleOut2 = turtle.Turtle(visible=False)
        RedTurtleOut2.color("white")
        style = ('Arial', 25, 'italic')
        RedTurtleOut2.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break
    gameScreen()

gameScreen is a function called earlier in my code by the way, it's not the issue, although it could be in the wrong place in the function. I'm not sure.

Code for (attempted) collision between the turtles where nobody wins.

collision = turtle.Turtle()
collision.hideturtle()

if blue_player.pos() == red_player.pos():
    collision.goto(0,0)
    collision.showturtle()
    collision.write("You collided with each other!\nIt's a tie!")

Here's the full code:

#Width and height of the screen
width = turtle.window_width()
height = turtle.window_height()

#Variables for tron game coded later on
red_direction = None
blue_direction = None

def TronGame():
    #Border
    #box = Turtle()
    #box.ht()
    #box.color('purple')
    #box.speed('fastest')
    #box.pensize(10)

   # box.pu()
   # box.goto(-1*height, -1*width)
   # box.pd()

   # for i in range(4):
   #   box.forward(height)
   #   box.left(90)
   #   box.forward(width)
   #   box.left(90)
      
  #Blue Player movements
  def blue_up():  
      global blue_direction
      blue_direction = 'up'
    
  def blue_down():
      global blue_direction
      blue_direction = 'down'
    
  def blue_left():
      global blue_direction
      blue_direction = 'left'
    
  def blue_right():
      global blue_direction
      blue_direction = 'right'

  #Red player Movemnts 
  def red_up():   
      global red_direction
      red_direction = 'up'
    
  def red_down():
      global red_direction
      red_direction = 'down'
    
  def red_left():
      global red_direction
      red_direction = 'left'
    
  def red_right():
      global red_direction
      red_direction = 'right'

  #Player movements   
  def move_player(player, direction):
      if direction == 'up':
          player.setheading(90)
          player.forward(5)
      elif direction == 'down':
          player.setheading(270)
          player.forward(5)
      elif direction == 'left':
          player.setheading(180)
          player.forward(5)
      elif direction == 'right':
          player.setheading(0)
          player.forward(5)

  def gameloop():
      move_player(red_player, red_direction)
      move_player(blue_player, blue_direction)
    
      #Repeat after 10ms (0.01s) (1000ms/10ms = 100 FPS)
      screen.ontimer(gameloop, 10)

  def collisions():
    while True:
      if(math.isclose(blue_player.xcor(), red_player.xcor(), abs_tol=1e-10) and
       math.isclose(blue_player.ycor(), red_player.ycor(), abs_tol=1e-10)):
        TurtleCrash = turtle.Turtle(visible=False)
        TurtleCrash.color("white")
        style = ('Arial', 25, 'italic')
        TurtleCrash.write("Red and Blue Crashed!\nGame over!", font=style, align='center')
        break
         
      if blue_player.xcor() > width or blue_player.xcor() < -1*width:
        BlueTurtleOut = turtle.Turtle(visible=False)
        BlueTurtleOut.color("white")
        style = ('Arial', 25, 'italic')
        BlueTurtleOut.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break
        
      if blue_player.ycor() > height or blue_player.ycor() < -1*height:
        BlueTurtleOut2 = turtle.Turtle(visible=False)
        BlueTurtleOut2.color("white")
        style = ('Arial', 25, 'italic')
        BlueTurtleOut2.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break
  
      if red_player.xcor() > width or red_player.xcor() < -1*width:
        RedTurtleOut = turtle.Turtle(visible=False)
        RedTurtleOut.color("white")
        style = ('Arial', 25, 'italic')
        RedTurtleOut.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break
  
      if red_player.ycor() > height or red_player.ycor() < -1*height:
        RedTurtleOut2 = turtle.Turtle(visible=False)
        RedTurtleOut2.color("white")
        style = ('Arial', 25, 'italic')
        RedTurtleOut2.write("Red went out of bounds./nBlue wins!", font=style, align='center')
        break


  def MainTron():
    global screen
    global blue_player
    global red_player
  
    screen = turtle.Screen()
    screen.setup(width, height)
    screen.bgpic('TronBg.png')
    screen.bgcolor('black')
    screen.addshape('BlueBike.gif')
    screen.addshape('RedBike.gif')

    blue_player = turtle.Turtle()
    blue_player.shape('BlueBike.gif')
    blue_player.pencolor("blue")
    blue_player.pensize(3)
    blue_player.pu()
    blue_player.goto(-1*(width)/3, height/8)
    blue_player.pd()

    red_player = turtle.Turtle()
    red_player.shape('RedBike.gif')
    red_player.pencolor("red")
    red_player.pensize(3)
    red_player.pu()
    red_player.goto(width/3, height/8)
    red_player.pd()

    for x in range(10):
      my_turtle = turtle.Turtle(visible=False)
      my_turtle.color("white")
      style = ('Arial', 25, 'italic')
      my_turtle.write(10-x, font=style, align='center') 
      time.sleep(1)
      my_turtle.undo()
  
    screen.listen()

    #collisions()
        
    screen.onkey(red_up, "w")
    screen.onkey(red_down, "s")
    screen.onkey(red_left, "a")
    screen.onkey(red_right, "d")
  
    screen.onkey(blue_up, "Up")
    screen.onkey(blue_down, "Down")
    screen.onkey(blue_left, "Left")
    screen.onkey(blue_right, "Right")

    screen.ontimer(gameloop, 250)


    #collision = turtle.Turtle()
    #collision.hideturtle()

    #if blue_player.pos() == red_player.pos():
    #  collision.goto(0,0)
    #  collision.showturtle()
    #  collision.write("You collided with each other!\nIt's a tie!")
    
    screen.mainloop()

  MainTron()

Solution

  • As Johnny Mopp already mentioned, there are lot of places to improve the code. Below a for sure incomplete list describing only part of the many changes done to the original code along with a short explanation what do the changes improve along with description of Python features making these changes possible:

    • If a value returned by a function is used multiple times it's better to assign its value to a variable first
    • An empty string s="" is considered False in Python (bool("") is False ). This allows to use the string as a flag indicating found collision
    • On key-press the Turtle can directly get its direction assigned. No need for a forth and back between "up" and 90.
    • As in simple cases nesting of functions or using classes makes the code worse and harder to understand instead of better I have fully 'flattened' the code.
    • As good as possible eliminated exactly the same code repeating again and again
    • Have put things which belong together near to each other in code
    • Added the possibility to pause/restart the game by pressing the "space" button.
    • Set up the threshold for detecting collisions, so crashes of players are now detected
    • Set the width and height values to a half, so that collisions at screen edges are now detected
    • Considering that in Python you can write good looking and intuitive min < x < max instead of min < x and x < max I decided to use it in detecting collisions, but not as a check if the player is outside of the screen area (because the expression max < x < min doesn't work) but if he is within. If the player is no more within, there is a collision.
    • Another shortcut in collision detection is to use the .distance() function for detecting collision between the player turtles instead of evaluation of their coordinates.
    • Added collision detection for the trace the player turtles leave on the screen so that now running into the path of the other player is detected.
    • Added the possibility to pause and restart the game by pressing "space'. Not restarted game will exit and close the window.

    What could be further done to this code in order to improve it even more? The most urgent improvement would be to speed up the code in the collision detection part by replacing cloning of the turtles by another way of storing the turtle path and another way of detecting collision with the path.

    Below the background image the after the image following code will try to load from the same directory from which the code is running:

    Game background image

    import time, turtle
    S_WIDTH, S_HEIGHT   = 1280, 850 # size of created Screen (window) 
    PLAYER_MIN_DISTANCE = 15.0      # for collision between players
    X_MIN, X_MAX = -S_WIDTH/2, S_WIDTH/2 # for collision of players with ... 
    Y_MIN, Y_MAX = -S_HEIGHT/2, S_HEIGHT/2 # ... sides of the screen
    
    # For displaying of messages to the user about game status (win/loss): 
    message_turtle = turtle.Turtle(visible=False)
    message_turtle.color("white")
    
    red_clones  = []
    blue_clones = []
    CLONE_DIST  = PLAYER_MIN_DISTANCE/1.1
    def check_collisions():
        global red_clones, blue_clones, game_running
        msg = "" # msg as not empty string will indicate collision
    
        if not any( blue_player.distance(blue_clone) < CLONE_DIST for blue_clone in  blue_clones):
            bpc = blue_player.clone(); bpc.hideturtle(); blue_clones.append( bpc )
        if not any( red_player.distance( red_clone ) < CLONE_DIST for red_clone  in  red_clones):
            rpc = red_player.clone();  rpc.hideturtle();  red_clones.append( rpc )
    
        for red_clone in red_clones: 
            if blue_player.distance(red_clone) < PLAYER_MIN_DISTANCE:
                msg = "Blue crashed in Red's path.\nRed wins!"
        for blue_clone in blue_clones:
            if red_player.distance(blue_clone) < PLAYER_MIN_DISTANCE:
                msg = "Red crashed in Blue's path.\nBlue wins!"
                
        if( blue_player.distance(red_player) < PLAYER_MIN_DISTANCE ): 
            msg = "Red and Blue Crashed!\nGame over!"
    
        if not ( X_MIN < blue_player.xcor() < X_MAX and Y_MIN < blue_player.ycor() < Y_MAX ):
            msg = "Blue went out of bounds.\nRed wins!"
        if not ( X_MIN < red_player.xcor() < X_MAX and Y_MIN < red_player.ycor() < Y_MAX ):
            msg = "Red went out of bounds.\nBlue wins!"
    
        if msg: 
            message_turtle.write( msg, font=('Arial', 25, 'italic'), align='center')
            pause_game()
            game_running = False
            del red_clones[:] ; del blue_clones[:]
            time.sleep(5)
            message_turtle.undo()
            blue_player.clear(); red_player.clear() 
            blue_player.is_moving=False; red_player.is_moving=False
            blue_player.pu();blue_player.goto(-1*S_WIDTH/3, S_HEIGHT/8);blue_player.pd()
            red_player.pu() ; red_player.goto(   S_WIDTH/3, S_HEIGHT/8); red_player.pd()
            blue_player.setheading(  0); red_player.setheading(180)
            message_turtle.write( "Press SPACE for a NEW GAME", font=('Arial', 25, 'italic'), align='center')
            countdown()
            if not game_paused:
                message_turtle.undo()
                game_running = True
            else: 
                screen.bye()
            
    # Setting up the display area and making shapes available for later use 
    screen = turtle.Screen()
    screen.setup(S_WIDTH, S_HEIGHT)
    # 
    screen.bgpic(    'TronBg.png'  )
    screen.bgcolor(  'green'       )
    # screen.addshape( 'BlueBike.gif')
    # screen.addshape( 'RedBike.gif' )
    
    # Create the Blue Player Turtle and its controls 
    blue_player = turtle.Turtle("turtle")
    blue_player.shapesize(stretch_wid=3, stretch_len=3)
    blue_player.setheading(  0)
    # blue_player.shape('BlueBike.gif')
    blue_player.pencolor("blue")
    blue_player.pensize(15)
    blue_player.pu()
    blue_player.goto(-1*S_WIDTH/3, S_HEIGHT/8)
    blue_player.pd()
    blue_player.is_moving = False
    def blue_up()   : blue_player.setheading( 90);blue_player.is_moving=True
    def blue_down() : blue_player.setheading(270);blue_player.is_moving=True
    def blue_left() : blue_player.setheading(180);blue_player.is_moving=True
    def blue_right(): blue_player.setheading(  0);blue_player.is_moving=True
    screen.onkey(blue_up,       "Up")
    screen.onkey(blue_down,   "Down")
    screen.onkey(blue_left,   "Left")
    screen.onkey(blue_right, "Right")
    
    # Create the Red Player Turtle and its controls 
    red_player = turtle.Turtle("turtle")
    red_player.shapesize(stretch_wid=3, stretch_len=3)
    red_player.setheading(180)
    # red_player.shape('RedBike.gif')
    red_player.pencolor("red")
    red_player.pensize(15)
    red_player.pu()
    red_player.goto(S_WIDTH/3, S_HEIGHT/8)
    red_player.pd()
    red_player.is_moving = False
    def red_up()    : red_player.setheading( 90); red_player.is_moving=True
    def red_down()  : red_player.setheading(270); red_player.is_moving=True
    def red_left()  : red_player.setheading(180); red_player.is_moving=True
    def red_right() : red_player.setheading(  0); red_player.is_moving=True
    screen.onkey(red_up,         "w")
    screen.onkey(red_down,       "s")
    screen.onkey(red_left,       "a")
    screen.onkey(red_right,      "d")
    
    # Allow pausing the game by pressing space
    game_paused = False
    def pause_game():
        global game_paused
        if game_paused: game_paused = False
        else:           game_paused = True
    screen.onkey(pause_game, "space")
    
    # Countdown: 
    countdown_turtle = turtle.Turtle(visible=False)
    countdown_turtle.color("white")
    countdown_turtle.goto(0, -50)
    def countdown(): 
        for i in range(3,-1,-1):
            countdown_turtle.write(i, font=('Arial', 25, 'italic'), align='center') 
            time.sleep(1)
            countdown_turtle.undo()
    countdown()
    
    # Establishing a screen.ontimer() loop driving the turtles movement
    game_running = True
    def gameloop():
        if not game_paused and game_running:
            if blue_player.is_moving: blue_player.forward(3)
            if  red_player.is_moving:  red_player.forward(3)
            check_collisions()
        screen.ontimer(gameloop, 30) # 10ms (0.01s) (1000ms/10ms = 100 FPS)
    gameloop()
    
    # Start processing of key-presses
    screen.listen()
    screen.mainloop()