Thursday, February 4, 2021

Python - Project - Snake Game - Part 2 (Inheritance and List Slicing) (Day 21)

This is a 100 Days challenge to learn a new language (Python). 100 Days of Code - The Complete Python Pro Bootcamp 

I will post some notes to motivate myself to finish this challenge.


Project - Snake Game



We will build a snake game on day 20 and 21.

Break down the problem:

  1. Create a snake body (day 20)
  2. Move the snake (day 20)
  3. Control the snake (day 20)
  4  Detect collision with food (day 21)
  5. Create a scoreboard (day 21)
  6. Detect collision with wall (day 21)
  7. Detect collision with tail (day 21)

In order to complete our snake game, we need to introduce inheritance first.


Inheritance



Inheritance allows us to define a class (child) that inherits all the attributes and methods from another class (parent).

Ex:
class Animal:
    """Animal"""

    def __init__(self):
        self.num_eyes = 2

        print("I am an animal")

    def show_eye_number(self):
        """get_eye_number"""

        print(f"I have {self.num_eyes} eyes")

    def breath(self):
        """breath"""

        print("I am breathing")


# Define a Fish class which inherit from Animal Class
class Fish(Animal):
    """Fish"""

    def __init__(self):
        # Trigger parent's constructor
        super().__init__()

        print("I am a fish")

    # Override parent function
    def breath(self):
        """breath"""

        # Trigger parrent's breath function
        super().breath()

        print("I am breathing under the water")

    # New child function
    def swin(self):
        """swim"""

        print("I am swimming")


# Initiate an animal
animal = Animal()
animal.show_eye_number()
animal.breath()

# Initiate a fish
fish = Fish()
# Call an inherited function
fish.show_eye_number()
# Call an overridden function
fish.breath()
# Call Fish's own function
fish.swin()

If child class define the same methods like that of parent class, it will override the methods which are inherited from the parent class

Python provide super() to help to call parent's methods


Snake Game (Part 2)



food.py
import random
from turtle import Turtle

class Food(Turtle):
    """Food"""

    # Constructor
    def __init__(self):
        # Call Turtle's constructor
        super().__init__()

        self.create_food()

    # create_food
    def create_food(self):
        """create_food"""

        # Setup food
        self.shape("circle")
        self.penup()
        self.color("red")
        self.shapesize(stretch_wid=0.5, stretch_len=0.5)
        self.speed("fastest")

        # refresh food with random position
        self.refresh()

    # Update food position
    def refresh(self):
        """refresh"""

        x_position = random.randint(-280, 280)
        y_position = random.randint(-280, 280)
        self.setpos(x_position, y_position)


scoreboard.py
from turtle import Turtle


class Scoreboard(Turtle):
    """Scoreboard"""

    # Constructor
    def __init__(self):
        super().__init__()

        # Define an attribute to track score
        self.score = 0

        self.create_scoreboard()

    # create_scoreboard
    def create_scoreboard(self):
        """create_scoreboard"""

        self.hideturtle()
        self.color("white")
        self.penup()
        self.setpos(0, 280)

        self.refresh_score()

    # increase_score
    def increase_score(self):
        """increase_score"""
        self.score += 1

        self.refresh_score()

    # refresh_score
    def refresh_score(self):
        """refresh_score"""

        self.clear()
        self.write("Score: " + str(self.score), False, align="center")

    # game_over
    def game_over(self):
        """game_over"""

        self.setpos(0, 0)

        self.write("Game Over", False, align="center")


snake.py
from turtle import Turtle

# Constants
STARTING_POSITIONS = [(0, 0), (-20, 0), (-40, 0)]
MOVING_DISTANCE = 20
UP = 90
DOWN = 270
LEFT = 180
RIGHT = 0


class Snake:
    """Snake Class"""

    # Constructor
    def __init__(self):
        # Define an attribute to track snake body
        self.segments = []

        # Call func to init snake
        self.create_snake()

        # Create a attribute instead of using magic number 0
        self.head = self.segments[0]

    # Methods - create snake
    def create_snake(self):
        """Adding segment(turtle object) to segments list"""

        for position in STARTING_POSITIONS:
            # add segment with passing position one by one
            self.add_segment(position)

    # add_segment
    def add_segment(self, position):
        """add_segment"""

        segment = Turtle()
        segment.shape("square")
        segment.color("white")
        segment.penup()
        segment.setpos(position)

        self.segments.append(segment)

    # extend
    def extend(self):
        """extend"""
        self.add_segment(self.segments[-1].pos())

    # Methods - move
    def move(self):
        """move"""

        # Starting from tail,
# make each segment move to the position of the previous segment
        for index in range(len(self.segments) - 1, 0, -1):
            new_x = self.segments[index - 1].xcor()
            new_y = self.segments[index - 1].ycor()
            self.segments[index].setpos(new_x, new_y)

        # Magic number!!
        # self.segments[0].forward(MOVING_DISTANCE)

        # Using attribute instead of magic number
        self.head.forward(MOVING_DISTANCE)

    def move_up(self):
        """move_up"""

        if self.head.heading() != DOWN:
            self.head.setheading(UP)

    def move_right(self):
        """move_right"""

        if self.head.heading() != LEFT:
            self.head.setheading(RIGHT)

    def move_left(self):
        """move_left"""

        if self.head.heading() != RIGHT:
            self.head.setheading(LEFT)

    def move_down(self):
        """move_down"""

        if self.head.heading() != UP:
            self.head.setheading(DOWN)

main.py
import time
from turtle import Screen
from snake import Snake
from food import Food
from scoreboard import Scoreboard

# Screen Setup
screen = Screen()
screen.setup(width=600, height=600)
screen.title("My Snake Game")
screen.bgcolor("black")
# Turn turtle animation off# Disable screen
screen.tracer(0)

# init a snake
snake = Snake()
# init a food
food = Food()
# init a scoreboard
scoreboard = Scoreboard()

# Listen events
# Bind fun to key-release event of key
screen.onkey(snake.move_up, "Up")
screen.onkey(snake.move_down, "Down")
screen.onkey(snake.move_right, "Right")
screen.onkey(snake.move_left, "Left")
# Set focus on TurtleScreen (in order to collect key-events)
screen.listen()

game_is_on = True

while game_is_on:
    # Perform a TurtleScreen update# Update Screen
    screen.update()

    # Give some delay for this while loop
    time.sleep(0.1)

    # Keep moving
    snake.move()

    # Check if there is a collision for food
    if snake.head.distance(food) < 15:
        # Increase score
        scoreboard.increase_score()

        # Make snake longer
        snake.extend()

        # Update food position
        food.refresh()

    # Check if there is a collision for wall
    if (
        snake.head.xcor() >= 285
        or snake.head.xcor() <= -285
        or snake.head.ycor() >= 285
        or snake.head.ycor() <= -285
    ):
        # Terminate the while loop
        game_is_on = False

        # Show game over message
        scoreboard.game_over()

    # Check if there is a collision for tails
    for segment in snake.segments:
# Ignore head
        if segment == snake.head:
            continue

        if snake.head.distance(segment) <= 15:
            # Terminate the while loop
            game_is_on = False

            # Show game over message
            scoreboard.game_over()


# Bind bye() method to mouse clicks on the Screen.
screen.exitonclick()


Highlight



1. In Python, negative index of list mean it counts from the right (instead of from the left) Reference

snake.py

    # extend
    def extend(self):
        """extend"""
        self.add_segment(self.segments[-1].pos())


2. The following code can be improved by using list slicing.

main.py

    # Check if there is a collision for tails
    for segment in snake.segments:
# Ignore head
        if segment == snake.head:
            continue

        if snake.head.distance(segment) <= 15:
            # Terminate the while loop
            game_is_on = False

            # Show game over message
            scoreboard.game_over()

main.py (Using List Slicing)

    # Check if there is a collision for tails
    for segment in snake.segments[1:]:
        if snake.head.distance(segment) <= 15:
            # Terminate the while loop
            game_is_on = False

            # Show game over message
            scoreboard.game_over()


List Slicing



If we want to take some item from a list, we can iterate it and pick up what we want based on some conditions. In python, it provide a handy way to achieve it. It is slicing.

Format:

list[start:end:jump]

Ex:
list = ["a", "b", "c", "d", "e"]
print(list[::])
# ['a', 'b', 'c', 'd', 'e']

list = ["a", "b", "c", "d", "e"]
print(list[2::])
# ['c', 'd', 'e']

list = ["a", "b", "c", "d", "e"]
print(list[:2:])
# ['a', 'b']

list = ["a", "b", "c", "d", "e"]
print(list[::2])
# ['a', 'c', 'e']

list = ["a", "b", "c", "d", "e"]
print(list[::-1])
# ['e', 'd', 'c', 'b', 'a']

No comments:

Post a Comment