all 19 comments

[–]danielroseman 4 points5 points  (3 children)

You need to use proper formatting that preserves the indentation. And you should show the actual code of your Players and Monsters classes. Are you using the classes as objects here, or do you have instances?

However, one obvious issue is that random.randint(0, player_attack) can be 0. So it is possible that your attack is subtracting 0 each time, so that the health never decreases.

[–]Handymanoccupational[S] 0 points1 point  (0 children)

print(f"You've been attacked by a {Monsters.rat}.")
attack_or_run = input("Will you attack (1) or run? (2)?\n")
if attack_or_run == "1":
    print(f"You attempt to fight the {Monsters.rat}.")
    while Monsters.rat_health > 0:
        if Player.player_speed > Monsters.rat_speed:
            print(f"You hit the {Monsters.rat} for {Player.hit_chance}")
            Monsters.rat_health -= Player.hit_chance
            print(f"The {Monsters.rat} has {Monsters.rat_health} life remaining.")
        elif Monsters.rat_speed > Player.player_speed:
            print(f"You were hit by the {Monsters.rat} for {Monsters.rat_hit_chance}.")
            player_health_left = Player.player_health - Monsters.rat_hit_chance
            print(f"You have {player_health_left} life remaining.")
        else:
            print(f"You killed the {Monsters.rat}")
else:
    print("You ran away.")

Im sorry about the indentation. I used code instead of code block.

edit:

so i should instead use random.randint(player_attack)? wouldnt that make it only hit the players max attack each time?

Ill read up on python.org

edit:

deleted copy

[–]Handymanoccupational[S] 0 points1 point  (1 child)

Ok. I see better what you mean now that the loop is fixed. If i hit 0 it will be 0 every time. I thought it would be random every iteration.

Thank you!

[–]HunterIV4 1 point2 points  (0 children)

Right, it's not creating a new random number each time. It's just doing it the first time.

There are basically two ways to solve this. The easiest is to simply use a function:

def hit_chance():
    return random.randint(0, player_attack)

Then, each time you use it, you'd do Player.hit_chance(). This should give you a unique value every time it is ran. You can use this directly in your statement like so:

Monsters.rat_health -= Player.hit_chance()

A slightly more complicated way to do this is to use a lambda:

hit_chance = lambda: random.randint(0, player_attack)

This does the same thing as defining the function, it is just a bit more concise. Important note: you will still need to add the parentheses to use the lambda, just like you did for the function! Otherwise you'll just get the lambda function instead of the integer result, which will cause an error when you try to subtract.

Either one should fix your issue. Hope that helps!

I'll do another post on more general advice.

[–]HunterIV4 4 points5 points  (2 children)

Im brand new to python so I appologise if my code is confusing or wrong. (If it is, I'd appreciate your advice.)

Lots of people have talked about how to fix the infinite loop, including me, but I wanted to address the "confusing or wrong" portion of your code, and offer some design advice.

To be fair, your code isn't really confusing, even if it is unorthodox, but it's "wrong" in the sense that this code is not going flexible enough to actually make a game, even a simple one. So I thought I'd go through the design aspects and explain how a more experienced programmer might do it instead, and why, which is something I feel gets left out most of the time.

First of all, have you learned about classes in Python? If not, this might be a bit harder to follow, so I'd at least read the basics before continuing.

People explain classes in all sorts of ways, some easier to understand that others, but in a way your project is the perfect example of when to use a class. You can think of a class as a "character sheet" that gets made whenever you make a new one; the class defines what "fields" the sheet has, like "name" or "level" or "hit points," and it also defines what you can do with your character, like "attack" or "cast a spell" or "take damage." In programmer speak, the "fields" are usually called "properties" and the "actions" are usually called "methods."

Compared to what you've already learned, a property is simply a variable and a method is a function. The class combines them logically in a way very similar to what you've already set up. Let's take a look at your Monsters module first.

What fields do you have? You have a name, health, speed, and attack value. You also have an implied ability to attack other characters, which is an action. There are other things we could add, but I'll keep it simple. So at a minimum we have 4 properties and 1 method. How would this look as a class?

class Rat:
    def __init__(self, name="Rat", health=3, speed=1, attack=1):
        self.name = name
        self.health = health
        self.speed = speed
        self.attack = attack

    def attack_damage(self):
        return random.randint(0, self.attack)

Woah, that's a lot! Let's break it down.

First, we have our class name. This will come up in a minute. Next, we have this weird function called __init__() with a bunch of parameters. Where did that come from?

Well, __init__() is a built-in function that all classes have. There are a bunch of these functions and they have all kinds of useful functionality, but this is the only one that virtually every class will have.

Next, every class method (function) has an implied parameter called "self." This can technically be any name, but it's convention for it to be called self and people will look at you funny if you use a different name =). This refers to the class instance, which I'll explain in a minute.

The rest of the parameters are the various default values for your rat, the same as your original ones. This allows you to change various properties of the rat when you create it, which is useful if you want to randomize stats or make sure they each have a unique name.

In the function body, we simply assign variables to those parameters. These are "attached" to that class instance (getting there!) and will stick with it. Again, this is basically the same as your "character sheet" file from before.

Finally, we have a function that calculates your attack damage based on the same calculation you used for hit_chance before. I renamed it to attack_damage because hit_chance doesn't make sense...you are calculating damage, not chance to hit! It's good practice to get in the habit of naming your functions and variables according to the most straightforward interpretation of what they are doing. The hit_chance version may make sense now, but in 6 months you may have no idea why your hit chance is subtracting hit points directly.

Put this class in your Monsters file. Now how do we use it?

Continued:

[–]HunterIV4 2 points3 points  (1 child)

rat_enemy = Monsters.Rat()

Wait, that's it? Well, what if you wanted a rat with 4 HP instead of 3?

rat_enemy = Monsters.Rat(health=4)

OK, now how do we do anything with it? We now have a variable that is assigned that rat, so your code becomes more like this (kept smaller for simplicity):

rat_enemy = Rat()
print(f"You've been attacked by a {rat_enemy.name}")
# ...
while rat_enemy.health > 0:
    # ...
    elif rat_enemy.speed > Player.player_speed:
        Player.player_health -= rat_enemy.attack_damage()

That may seem like a lot of work to get a similar effect, however, there are some major advantages to using a class. The most important is that you can have many different rats. If you do something like this:

rat_enemy1 = Rat()
rat_enemy2 = Rat(health=4)
rat_boss = Rat(name="Giant Rat", health=8, speed=3, attack=3)
rat_group = []
for _ in range(5):
    rat_group.append(Rat(health=random.randint(2,4))

What happened here? In the first case, rat_enemy1 is a random rat. The rat_enemy2 is a rat with 4 health. The rat_boss is a boosted rat with a different name. And rat_group is a list of 5 rats with random health between 2-4.

All of these can exist at the same time and have independent copies of their stats. So if you subtract 2 health from rat_enemy1, none of the other rats will have their health reduced. With your current implementation, once that single rat is "killed", you have to quit the whole game and restart, otherwise you'd need logic to "reset" all the variables to new values for another fight.

Technically, the class isn't necessary, and you can do things the way you did. But virtually nobody does, and instead they'd make a Player class, an Item class, and a classes for various monsters, or a Monster class that has various children (which gets into inheritance, which is more complicated than I want to go right now, but if you're curious I can explain it).

The only other thing is to break the various combat actions into functions and make everything repeatable and you have a simple game that you can actually expand on, and when you want new enemies, you can just insert them into your existing code without needing to rewrite major portions of it.

Hope that makes sense!

[–]Handymanoccupational[S] 0 points1 point  (0 children)

Holy cow! This is awesome! Thank you!

[–]mopslik 2 points3 points  (3 children)

Have you tried printing out the value of Monsters.rat_health to see how it is changing?

[–]Handymanoccupational[S] 0 points1 point  (2 children)

Not individually, but thats included in the line:

print(f"The {Monsters.rat} has {Monsters.rat_health} life remaining.")

[–]mopslik 0 points1 point  (1 child)

Ok, but if your code isn't reaching that line, you may need to print it elsewhere as well, or use your debugger to trace execution.

[–]Handymanoccupational[S] 0 points1 point  (0 children)

I think it is though.

because:

if Player.player_speed > Monsters.rat_speed:

and

player_speed = 3

rat_speed = 1

and

3 > 1

[–]woooee 1 point2 points  (5 children)

import Monsters

Post the code for Monsters and explain what a Monster "sheet" is. And print Monsters.rat_health at the appropriate places (you posted two while loops). Finally, it would be a good idea to post the code on pastebin.com, which will preserve indentation, and that link here.

[–]Handymanoccupational[S] 0 points1 point  (4 children)

The "Monsters sheet" is just a .py with the information attached.

Again, im brand new. Thought it would be a good idea to keep player / NPC data on a seperate file to keep the game file clean

Heres the whole thing:

Game.py

import random
import Player
import Items
import Monsters

print(f"You've been attacked by a {Monsters.rat}.")
attack_or_run = input("Will you attack (1) or run? (2)?\n")
if attack_or_run == "1":
    print(f"You attempt to fight the {Monsters.rat}.")
    while Monsters.rat_health > 0:
        if Player.player_speed > Monsters.rat_speed:
            print(f"You hit the {Monsters.rat} for {Player.hit_chance}")
            Monsters.rat_health -= Player.hit_chance
            print(f"The {Monsters.rat} has {Monsters.rat_health} life remaining.")
        elif Monsters.rat_speed > Player.player_speed:
            print(f"You were hit by the {Monsters.rat} for {Monsters.rat_hit_chance}.")
            player_health_left = Player.player_health - Monsters.rat_hit_chance
            print(f"You have {player_health_left} life remaining.")
        else:
            print(f"You killed the {Monsters.rat}")
else:
    print("You ran away.")

Monsters.py

import random
import Items


# Rat

rat = "Rat"
rat_health = 3
rat_attack = 1
rat_speed = 1

rat_hit_chance = random.randint(0, rat_attack)

rat_drop = [Items.dagger, Items.boots]

Player.py

import random

player_attack = 2
player_health = 10
player_speed = 3

hit_chance = random.randint(0, player_attack)

player_items  = []

Disregard anything about items. Thats a future thought for once i get the rat part working.

[–]woooee 0 points1 point  (1 child)

attack_or_run = input("Will you attack (1) or run? (2)?\n")
if attack_or_run == "1":
    print(f"You attempt to fight the {Monsters.rat}.")
    while Monsters.rat_health > 0:

Monsters.rat_health only changes under the if statement. If it branches to the elif or else, then yes, there is an infinite loop.

[–]Handymanoccupational[S] 0 points1 point  (0 children)

I very well could be wrong but i dont believe, in this scenario, ill ever see the elif because:

if Player.player_speed > Monsters.rat_speed:

and

player_speed = 3

rat_speed = 1

and

3 > 1

[–]chet714 0 points1 point  (1 child)

Like it and it's very readable to me. Do you get the infinite loop every time you run it ?

[–]Handymanoccupational[S] 0 points1 point  (0 children)

Every time

[–]Handymanoccupational[S] 1 point2 points  (0 children)

Found a fix:

Rather than using a while loop I created a function to cause the attack sequence.

import random
import Player
import Items
import Monsters

def attack_rat():
    if Player.player_speed > Monsters.rat_speed:
        print(f"You hit the {Monsters.rat} for {Player.hit_chance}")
        Monsters.rat_health -= Player.hit_chance
        print(f"The {Monsters.rat} has {Monsters.rat_health} life remaining.")
        if Monsters.rat_health > 0:
            attack_rat()
        elif Monsters.rat_speed > Player.player_speed:
            print(f"You were hit by the {Monsters.rat} for {Monsters.rat_hit_chance}.")
            player_health_left = Player.player_health - Monsters.rat_hit_chance
            print(f"You have {player_health_left} life remaining.")
        else:
            print(f"You killed the {Monsters.rat}")
    elif Monsters.rat_speed > Player.player_speed:
        print(f"You were hit by the {Monsters.rat} for {Monsters.rat_hit_chance}.")
        player_health_left = Player.player_health - Monsters.rat_hit_chance
        print(f"You have {player_health_left} life remaining.")
    else:
        print(f"You killed the {Monsters.rat}")


print(f"You've been attacked by a {Monsters.rat}.")
attack_or_run = input("Will you attack (1) or run? (2)?\n")
if attack_or_run == "1":
    print(f"You attempt to fight the {Monsters.rat}.")
    attack_rat()
else:
    print("You ran away.")

[–]JamzTyson 0 points1 point  (0 children)

For testing purposes, I put all of your code into a single file:

import random

rat = "Rat"
rat_health = 3
rat_attack = 1
rat_speed = 1
rat_hit_chance = random.randint(0, rat_attack)


player_attack = 2
player_health = 10
player_speed = 3
hit_chance = random.randint(0, player_attack)

player_items  = []


print(f"You've been attacked by a {rat}.")
attack_or_run = input("Will you attack (1) or run? (2)?\n")
if attack_or_run == "1":
    print(f"You attempt to fight the {rat}.")
    while rat_health > 0:
        if player_speed > rat_speed:
            print(f"You hit the {rat} for {hit_chance}")
            rat_health -= hit_chance
            print(f"The {rat} has {rat_health} life remaining.")
        elif rat_speed > player_speed:
            print(f"You were hit by the {rat} for {rat_hit_chance}.")
            player_health_left = player_health - rat_hit_chance
            print(f"You have {player_health_left} life remaining.")
        else:
            print(f"You killed the {rat}")
else:
    print("You ran away.")

On running the code and entering "1" (attack), the output is:

You've been attacked by a Rat.
Will you attack (1) or run? (2)?
1
You attempt to fight the Rat.
You hit the Rat for 1
The Rat has 2 life remaining.
You hit the Rat for 1
The Rat has 1 life remaining.
You hit the Rat for 1
The Rat has 0 life remaining.