2

I'm trying to make a text-based RPG. I have some code like:

heroes.py:

class Hero():
   def __init__(self):
      pass
   def Attack(self, target):
      # ...
   def TakeDamage(self, amount):
      # ...

monsters.py:

class Monster():
   def __init__(self):
      pass
   def Attack(self, target):
      # ...
   def TakeDamage(self, amount):
      # ...

The whole file structure looks like this:

|__ backend
    __init__.py
    monsters.py
    heroes.py
MainGame.py

Let's say I want Monster and Hero to access each other's Attack and TakeDamage functions, for example:

class Monster():
   def __init__(self):
      pass
   def Attack(self, target):
      # ...
   def TakeDamage(self, amount, target:Hero):
      damage = # damage calculation here
      target.TakeDamage(damage)

How can I do this? So far I've tried:

  • Importing each other (e.g. from .monsters import Monster) in their respective files - this gives me an error reading ImportError: cannot import name 'Monster' from partially initialized module 'backend.monsters' (most likely due to a circular import).
nxover8
  • 33
  • 5
  • 3
    You don't need any imports in these files. You need a main which creates Heroes and Monsters and pits them against each other. – quamrana Jun 30 '22 at 08:25
  • Yeah, you need to change the logic of your program. Make Monsters and Heroes independent of each other, than make a some kind of environment that handles their creation and interaction. Also, if you did `Hero.TakeDamage(damage)`, it would be called on a class, not `target` object and most likely result in an error. – matszwecja Jun 30 '22 at 08:34
  • Welcome to Stack Overflow. Please read [ask] and [mre]. It is impossible to advise as to the "bunch of undefined variable errors" that you experienced when trying to "put the Monster and hero classes together in one file", because we can see neither the code that you used to attempt this, nor the errors that resulted. – Karl Knechtel Jun 30 '22 at 09:02
  • Also, note well that this is **not a discussion forum**. We expect a *clear, specific question*; "Any help is greatly appreciated" [does not qualify](https://meta.stackoverflow.com/questions/284236/), and "thank you" is [irrelevant](https://meta.stackoverflow.com/questions/343721). We are not interested in anything about you - we are interested in *the code, and your question about it*. – Karl Knechtel Jun 30 '22 at 09:03
  • I removed the `class-methods` tag from the question because that is a separate concept from what you appear to be asking about. – Karl Knechtel Jun 30 '22 at 09:16
  • Apologies for incorrect terminology use, everyone! I assumed it was a method because that's what VS Code tells me it is when you hover over the name of it. I'll update the question to reflect the correct terminology. – nxover8 Jun 30 '22 at 20:37

2 Answers2

1

Broadly speaking, classes don't have methods; instances do. The purpose of a class is to define a data type. You don't need to see that definition, in Python, in order to use instances.

Consider the code in the Monster class:

   def TakeDamage(self, amount, target:Hero):
      damage = # damage calculation here
      Hero.TakeDamage(damage)

(I will ignore for the moment that the logic here probably doesn't make that much sense.)

Writing :Hero is a hint - to the person reading the code, and possibly to third-party tools; Python itself does not care - that target will be an instance of the Hero class.

We want to call a method on that instance, not on the class. The class doesn't have a TakeDamage method; it only has a TakeDamage function, which is used to create the method when we look it up via an instance.

Therefore, the code should not say Hero.TakeDamage(damage). It should say target.TakeDamage(damage), because target is the name of the Hero instance whose method we will call.

To do this, we do not require the definition of the Hero class. monsters.py should not import anything to make this work.

When the code is running, at the moment that the method call is attempted, Python will check whether the thing that is named target has a TakeDamage attribute. When it doesn't find one directly attached to the instance, it will look in the class, and find the TakeDamage function in that class. It will automatically create a method from that. Then it will check that the TakeDamage that it got from this process is callable (spoiler: it is, because it's a method), and call it.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • Technically, classes do have methods - because they are, themselves, instances (of the builtin `type`). However, these are completely different from the methods that you define in your source code. – Karl Knechtel Jun 30 '22 at 09:06
  • You do need to `import` the `heroes` and/or `monsters` modules in order to *create* instances of `Hero` and `Monster`, respectively. That is because, in order to do so, you *will* need the class. In fact, this depends on one of those methods I mentioned in the first comment: it is named `__call__`, and it defines what happens when you call the class. Yes, when you create an instance, you are *calling the class*, in the same way that you would call a function. At least, that's how Python sees things. – Karl Knechtel Jun 30 '22 at 09:11
  • It is also possible to use special tools to make the functions in a class behave like methods of the class. That is not what you want, in this case. Explaining the topic properly is well beyond the scope of this answer or the comments; please see e.g. https://www.youtube.com/watch?v=HTLu2DFOdTg . – Karl Knechtel Jun 30 '22 at 09:19
  • Ah, thanks for the clarification. However, when I don't import `Heros` from the other file and leave `def TakeDamage(self, amount, target:Hero)` as is, change `Hero.TakeDamage` to `target.TakeDamage` and import `monsters` (`from backend import monsters`) in `MainGame.py`, I get an error: `NameError: name Hero is not defined`. Removing it seems to fix the issue, which has me confused because you mentioned that Python doesn't care if `:Hero` is there or not. Am I misunderstanding something? – nxover8 Jun 30 '22 at 21:26
  • 1
    This will depend on something else in your code. Please try to create a [mre] and ask a new question. Edit: oh, wait, I'm rather absent-minded. *The function annotation*, as written, requires `Hero` to be defined - at least, depending on your version of Python. Please see https://stackoverflow.com/questions/33533148 to understand the problem and see workarounds. – Karl Knechtel Jun 30 '22 at 21:52
0

Here is a way for me to design this game:

├── backend
│   ├── __init__.py
│   ├── character.py
│   ├── heros.py
│   └── monsters.py
└── main.py

character.py is the common class between hero and monster.

# character.py
class Character:
    def __init__(self):
        self.health = 100

    def attack(self, target):
        target.take_damage(1)

    def take_damage(self, amount):
        self.health -= amount

    def __repr__(self):
        return (
            f"{self.__class__.__name__}("
            f"health={self.health!r}"
            f")"
        )
# heros.py
from .character import Character

class Hero(Character):
    pass
# monsters.py
from .character import Character

class Monster(Character):
    def attack(self, target):
        target.take_damage(5)
# main.py
from backend.heros import Hero
from backend.monsters import Monster

h = Hero()
m = Monster()

h.attack(m)
m.attack(h)

print(h)
print(m)

Output:

Hero(health=95)
Monster(health=99)

The key here is happening inside the attack method: it calls target.take_damage() with the amount.

Note that heros.py does not import monsters.py and vice versa. Doing so could result in circular reference, which is messy.

Hai Vu
  • 37,849
  • 11
  • 66
  • 93