1

I am relatively new to OOP and Pygame and have been trying to code a tower defense game and have hit a roadblock. I keep getting this error:

AttributeError: type object 'Game' has no attribute 'path'

I have tried reading posts from people with the same problem, however none of the fixes seem to work for me.

I am attempting to have several different levels in my game that have different backgrounds and therefore different paths. I'm trying to do this by creating a parent Game class and then having a subclass for each level. (Each class in my program has a different .py file.) Ideally, each level subclass would have its own path attribute which would override the path attribute in the game class. Then the path is passed into my Enemy class where there is code that makes enemies follow the path.

I can fix my error by putting self.path (in my game class) above the constructor and just defining it as path. However in doing this, I am unable or don't know how to override the attribute in the subclass.

Additionally, in my enemy class, I have tried to circumvent the problem of having a circular import with the game class by placing it lower down and I'm thinking that this may have something to do with it, however, I'm unsure.

If this is the case, is there a better way of allowing my enemy class to access the path?

This is the relevant code for my level select file:

# If button is pressed then execute its corresponding function
if event.type == pygame.MOUSEBUTTONDOWN:
    # If level 1 button is pressed then instantiate Level 1
    if level1.buttonPress(pos):
        level1_class = Level1(self.screen)
        # Runs the main game loop for the instantiated level
        level1_class.run()

This is the relevant code for my Enemy class:

import pygame

lightGreen = (0, 255, 0)
red = (200, 0, 0)


# Creates Enemy class
class Enemy(pygame.sprite.Sprite):
    imgs = []

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.width = 150
        self.height = 150
        self.max_health = 100
        self.health = 100
        self.path = [(0, 0)]
        self.x = self.path[0][0]
        self.y = self.path[0][1]
        self.img = None
        self.animation = 0
        self.speed = 1
        self.i = 1
        self.pos_check = 0

    # Draws the sprite to the screen
    def draw(self, screen):
        # Only works for the first time it is called
        if self.pos_check == 0:
            self.pos_check += 1
            # Sets starting x and y as the first co-ordinates of the path
            from Game import Game
            self.x = Game.path[0][0]
            self.y = Game.path[0][1]
        # Chooses an image from a list of images based on the number of self.animation
        self.img = self.imgs[self.animation]
        # Draws image
        screen.blit(self.img, (self.x - self.width / 2, self.y - self.height / 2))
        # Draws health bar
        self.draw_health_bar(screen)

    def update(self):
        # Increments self.animation each call
        self.animation += 1
        # Resets the animation count to 0 if the animation count exceeds the length of the list of images
        if self.animation >= len(self.imgs):
            self.animation = 0
        # Calls the moving method a number of times depending on speed
        for i in range(self.speed):
            self.follow_path()

    def draw_health_bar(self, screen):
        # Draws the red portion of the health bar depending on max health
        pygame.draw.rect(screen, red, (self.x - self.width / 2, self.y - self.height / 2, self.max_health, 10))
        # Draws the green portion of the health bar depending on the enemies current health
        pygame.draw.rect(screen, lightGreen, (self.x - self.width / 2, self.y - self.height / 2, self.health, 10))

    def follow_path(self):
        # Imports game class
        from Game import Game
        # self.path = path passed from Game class
        self.path = Game.path
        # If the x co-ord and y co-ord == the x and y co-ord of the next path position then add 1 to counter
        if (self.x, self.y) == (self.path[self.i][0], self.path[self.i][1]):
            self.i += 1
            # If x < than next x co-ord in path then increase x by 1 pixel
        if self.x < self.path[self.i][0]:
            self.x += 1
            # If x > than next x co-ord in path then decrease x by 1 pixel
        elif self.x > self.path[self.i][0]:
            self.x -= 1
            # If y < than next x co-ord in path then increase y by 1 pixel
        if self.y < self.path[self.i][1]:
            self.y += 1
            # If y > than next x co-ord in path then decrease y by 1 pixel
        elif self.y > self.path[self.i][1]:
            self.y -= 1

Code for my Game class:

import pygame
import os
from Wizard import Wizard
from Button import Button
import sys


# Creates Game class
class Game:

    def __init__(self, screen):
        self.path = [(-30, 783), (0, 783), (271, 767), (369, 471), (566, 414), (625, 352), (699, 138), (856, 93),
                     (1206, 93),
                     (1400, 46), (1500, 97), (1759, 97), (1784, 311), (1622, 434), (1487, 734), (1670, 789),
                     (1756, 842), (1782, 1016), (1782, 1200)]
        self.enemies = None
        self.towers = None
        self.game_button_list = None
        self.pause_button_list = None
        self.lives = 10
        self.money = 100
        self.width = 1920
        self.height = 1080
        self.background = pygame.image.load(os.path.join("Images", "game_background_3.png"))
        self.background = pygame.transform.scale(self.background, (self.width, self.height))
        self.screen = screen
        self.pause_button = Button(1800, 20, "button_pause.png")
        self.table = pygame.image.load(os.path.join("Images", "s_table.png"))
        self.table_size = self.table.get_size()
        self.play_button = Button(720, 465, "button_play_scaledDown.png")
        self.restart_button = Button(890, 465, "button_restart.png")
        self.close_button = Button(1055, 465, "button_close.png")
        self.fast_forward_button = Button(1670, 20, "button_quick.png")
        self.running = True
        self.paused = False
        self.paused_check = True
        self.remove_coordinate = 1100
        self.fastForward = False
        self.fastForward_counter = 1
        self.clock = pygame.time.Clock()
        self.fps = 60
        self.dt = self.clock.tick(self.fps)
        self.spawn_timer = 0
        self.spawn_frequency = 1000
        self.original_speed = None
        self.fastForward_check = 1

    def new(self):
        # Creates sprite groups for enemies, towers, game buttons and pause buttons
        self.enemies = pygame.sprite.Group()
        self.towers = pygame.sprite.Group()
        self.game_button_list = pygame.sprite.Group()
        self.pause_button_list = pygame.sprite.Group()
        # Adds instantiated objects to the sprite groups
        self.game_button_list.add(self.pause_button)
        self.game_button_list.add(self.fast_forward_button)
        self.pause_button_list.add(self.play_button)
        self.pause_button_list.add(self.restart_button)
        self.pause_button_list.add(self.close_button)

    # Main Game loop
    def run(self):
        self.new()
        while self.running:
            # Sets fps to 60
            self.clock.tick(self.fps)
            # Calls events
            self.events()
            # If not paused then update
            if not self.paused:
                self.update()
            # Draws everything to screen
            self.draw()

    # Events method
    def events(self):
        for event in pygame.event.get():
            # Gets mouse position (x, y)
            pos = pygame.mouse.get_pos()
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()

            # If the mouse is clicked then
            if event.type == pygame.MOUSEBUTTONDOWN:
                # If pause button is pressed pause the game
                if self.pause_button.buttonPress(pos):
                    self.paused = True
                # Loaded when pause button is pressed. Resumes the game
                if self.play_button.buttonPress(pos):
                    self.paused = False
                # Loaded when pause button is pressed. Restarts the game
                if self.restart_button.buttonPress(pos):
                    self.paused = False
                    self.run()
                # Loaded when pause button is pressed. Closes the game and returns to level select
                if self.close_button.buttonPress(pos):
                    self.running = False
                # Sets everything to 2x speed
                if self.fast_forward_button.buttonPress(pos):
                    self.fastForward_counter += 1
                    if self.fastForward_counter % 2 == 0:
                        self.fastForward_check = 2
                        for en in self.enemies:
                            self.original_speed = en.speed
                            en.speed = en.speed * 2
                        self.spawn_frequency = self.spawn_frequency // 2
                    else:
                        self.fastForward_check = 1
                        for en in self.enemies:
                            en.speed = self.original_speed
                        self.spawn_frequency = self.spawn_frequency * 2

    # Update method
    def update(self):
        # Counts the time since the main loop was loaded
        self.spawn_timer += self.dt
        # If 1 second has passed then
        if self.spawn_timer >= 60:
            # Removes one second from the current time
            self.spawn_timer -= self.spawn_frequency
            # Instantiates a wizard. CURRENTLY UNFINISHED. NEED TO MAKE A WAY TO ONLY SPAWN A SET AMOUNT
            wizard1 = Wizard(5 * self.fastForward_check)
            # Adds the new object to the sprite group
            self.enemies.add(wizard1)
        # Calls the method in Enemy class for updating the sprites in the sprite group
        self.enemies.update()

    def draw(self):
        # If the game is not paused
        if not self.paused:
            self.paused_check = True
            # Draws the background to the screen
            self.screen.blit(self.background, (0, 0))
            # Draws the enemies in the sprite group
            for en in self.enemies:
                en.draw(self.screen)
                # If an enemy reaches the end of the path then remove the enemy.
                if en.y > self.remove_coordinate:
                    self.enemies.remove(en)
            # Draws the UI buttons for the game
            for buttons in self.game_button_list:
                buttons.draw(self.screen)
            pygame.display.update()
        # If paused
        elif self.paused and self.paused_check:
            self.paused_check = False
            # Darkens the background
            rectangle = pygame.Surface((1920, 1080))
            rectangle.set_alpha(200)  # alpha level
            rectangle.fill((0, 0, 0))  # this fills the entire surface
            self.screen.blit(rectangle, (0, 0))
            # Draws a table to the middle of the screen
            self.screen.blit(self.table, (960 - self.table_size[0] / 2, 540 - self.table_size[1] / 2))
            # Draws the buttons on the table
            for buttons in self.pause_button_list:
                buttons.draw(self.screen)
            pygame.display.update()

Code for my level_1 subclass:

from Game import Game

# Subclass Level 1 inherits Game's methods and attributes
class Level1(Game):

    def __init__(self, screen):
        super().__init__(screen)
        self.path = [(-30, 783), (0, 783), (271, 767), (369, 471), (566, 414), (625, 352), (699, 138), (856, 93),
                     (1206, 93),
                     (1400, 46), (1500, 97), (1759, 97), (1784, 311), (1622, 434), (1487, 734), (1670, 789),
                     (1756, 842), (1782, 1016), (1782, 1200)]
martineau
  • 119,623
  • 25
  • 170
  • 301
Marcus543
  • 13
  • 2
  • 2
    Welcome to StackOverflow! So much code is rarely all relevant to the problem you're facing. You should condense it down into a [mre]. – Pranav Hosangadi Aug 24 '20 at 21:46
  • 2
    In this case, your error has nothing to do with the "relevant code" you shared in your question. The error is presumably here: `self.x = Game.path[0][0]; self.y = Game.path[0][1]`. The `Game` class has no member attribute called `path`, _Instances of the `Game` class_, however, do. [Here's](https://stackoverflow.com/questions/2714573/instance-variables-vs-class-variables-in-python) some more info about class variables and instance variables in Python. – Pranav Hosangadi Aug 24 '20 at 21:48

1 Answers1

1

The problem is that the Game object is never instantiated - that is, there is only the definition of Game, not a variable-version "copy" where the Game.__init__() function has been called. Obviously until that Game initialiser is called, the member variable game.path does not exist (because it's defined in __init__() ).

There's two ways around this. The first is to make member of the Game object purely static:

class Game:
    path = [(-30, 783), (0, 783), (271, 767), (369, 471), (566, 414), (625, 352), (699, 138), (856, 93), (1206, 93), (1400, 46), (1500, 97), (1759, 97), (1784, 311), (1622, 434), (1487, 734), (1670, 789), (1756, 842), (1782, 1016), (1782, 1200)]

    def __init__(self, screen):
        self.path = 
        self.enemies = None
        self.towers = None

This allows Game.path to be freely accessed independently of any initialisation. However looking at the rest of your class, this does not seem to be how it's designed to work.

So, the better approach would be to simply instantiate a Game object:

import Game

...

game = Game()   # Create an instantiated Game object.

...

    # Sets starting x and y as the first co-ordinates of the path
    self.x = game.path[0][0]
    self.y = game.path[0][1]

There seems to be a reasonable description of Python Object Instantiation here. It could be worth your time reading it, if you are unfamiliar with object oriented concepts.

Kingsley
  • 14,398
  • 5
  • 31
  • 53