#### [SOLVED] Battle simulator RPG

By Armv

I just learned about classes today and wanted to find a way to implement them that interested me. I whipped up this little battle simulator and would like any feedback and critiques as well as any ideas or similar projects I could pursue.

``````import random
import time
import decimal

rnd = 0

class Unit:
def __init__(self, n, hp, s, d, a, mp):
self.name = n
self.maxHP = hp
self.curHP = hp
self.strength = s
self.defence = d
self.agility = a
self.mp = mp

def describe(self):
print ("HP: "+ str(self.curHP) + " STRENGTH:"+ str(self.strength) + " DEFENCE:" + str(self.defence) + " AGILITY:" + str(self.agility) + " MP:" + str(self.mp))

def battle( a, b ):
turn = 1

if a.agility > b.agility:
attacker = a
defender = b
else:
attacker = b
defender = a

doBattle = True
while (doBattle):
time.sleep(.7)
print ("\n ---  Turn " , turn , ": " , attacker.name , " ---" )
print (attacker.name , ": " , attacker.curHP )
print (defender.name , ": " , defender.curHP , "\n")

if defender.agility > attacker.agility and random.randint(1,4) == 4:
print (attacker.name , " missed." )
else:
if random.randint(1,5) != random.randint(1,6):
multiplier_1 = random.randint(70,150)/100
multiplier_2 = random.randint(70,150)/100
attackDamage = round(decimal.Decimal(attacker.strength*multiplier_1),1) - round(decimal.Decimal(defender.defence*multiplier_2),1)
if attackDamage < 0:
attackDamage = 0
defender.curHP = defender.curHP - attackDamage
print (attacker.name , " did " , attackDamage , " damage to " , defender.name , "." )
else:
attackDamage = round(decimal.Decimal(attacker.strength*2),1) - round(decimal.Decimal(defender.defence/2),2)
if attackDamage < 0:
attackDamage = 0
defender.curHP = defender.curHP - attackDamage
print ("CRITICAL HIT!!!!!" + attacker.name , " did " , attackDamage , " damage to " , defender.name , "." )

if defender.curHP <= 0 and defender.mp == 0:
doBattle = False
print ("\n\n" , attacker.name , " won the battle!" )
elif defender.curHP <= 0 and defender.mp >= 0:
defender.mp -= 1
defender.curHP += random.randint(4,16)
if defender.curHP > 0:
print("You try to use your MP to heal!")
time.sleep(2)
print("It worked! HP: " + str(defender.curHP))
else:
print("You try to use your MP to heal!")
time.sleep(2)
print("No effect... you die.")

else:
temp = defender
defender = attacker
attacker = temp
turn = turn + 1

name = input("Your name: ")

character = Unit(name, random.randint(13,30), random.randint(2,5), random.randint(1,3), random.randint(1,5), random.randint(1,5) )
character.describe()

while input("Fight? [y/n]: ") == "y":
rnd += 1
print("Enemy Info:")
enemy = Unit("Monster", random.randint(10,25), random.randint(2,6), random.randint(1,3), random.randint(1,5), random.randint(0,1))
enemy.describe()
time.sleep(3)
character.maxHP = character.curHP
character.curHP += 3
powerup = random.randint(1,3)
if powerup == 1 and rnd >= 2:
character.strength += random.randint(1,2)
print("STR UP! Strength is now " + str(character.strength))
elif powerup == 2 and rnd >= 2:
character.defence += random.randint(1,2)
print("DEF UP! Defense is now " + str(character.defence))
elif powerup == 3 and rnd >= 2:
character.agility += random.randint(1,2)
print("AGILITY UP! Agility is now " + str(character.agility))
if rnd >= 2:
print("Character Info:")
character.describe()
time.sleep(3)
battle( character, enemy )
``````

# Modules

Since you only use the `sleep` function from the `time` module, you should emphasize it by declaring `from time import sleep` and change your calls to only `sleep`. I find it cleaner even though it's not a strong rule of thumb, taste varies and both forms are acceptable.

I’d recommend getting rid of the `decimal` module. You have no need of strict floating point computation and since you are rounding (thus converting to `float`s) anyway, you loose it's interest right after using it. Better round the final result of the computation and compute intermediate results using good-old `float`s.

# String representation

What do you do when you want to output the content of a dictionary? You `print` it. What about lists? You `print` them. Integers, sets, tuples? `print` them all.

Same goes for your `Unit`s. You should `print(character)` instead of `character.describe()`. How to do that without getting outputs like `<__main__.Unit object at 0x02EBFF10>`? By implementing the `__str__` special method.

# Object-Oriented Programming

Your functions does a lot of things and nearly all of them are directly related to your `Unit`s. You should both split them into smaller functions that (for now) do little but have meaning (and meaningful names) and make those function `Unit`'s methods. You will be able to more easily extend them latter if you decide to add more mechanisms to your battles.

Check if a `Unit` is alive, `heal` it, check whether of two `Unit`s is the swifter are all actions that can reasonably have their own function. For now the code will be simple but if you plan on adding a loot system with boost items, potions or even curses, you’ll have an easier time managing the whole logic because you will only have to include things in these tiny functions.

Similarly, since these functions are directly related to `Unit`s and perform actions on them, they should be implemented as methods in your class.

# Loops

Using a flag to end a `while` loop on certain condition feels not quite right. You should either:

• loop forever and `break` on such conditions;
• use the condition as the condition of the `while` itself.

If the condition is complex, a function call might even make things more readable. In `battle` the `while` condition should be “neither `a` nor `b` is dead”.

For your main loop, you should also try to sanitize your input. To me, `'y'`, `'Y'`, `' y'`, `' Yes '`, `''` are all valid values to continue. A common idiom is to use `input(...).strip()` and even `input(...).strip().lower()` if you want to check into a set of predefined values.

# General improvements

• Using functions/methods can also be a way to avoid repeating the same lines of code. You should also try to avoid it in `if...else`. For instance when computing the result of an attack:

``````attackDamage = round(decimal.Decimal(attacker.strength*multiplier_1),1) - round(decimal.Decimal(defender.defence*multiplier_2),1)
if attackDamage < 0:
attackDamage = 0
defender.curHP = defender.curHP - attackDamage
print (attacker.name , " did " , attackDamage , " damage to " , defender.name , "." )
``````

and:

``````attackDamage = round(decimal.Decimal(attacker.strength*2),1) - round(decimal.Decimal(defender.defence/2),2)
if attackDamage < 0:
attackDamage = 0
defender.curHP = defender.curHP - attackDamage
print ("CRITICAL HIT!!!!!" + attacker.name , " did " , attackDamage , " damage to " , defender.name , "." )
``````

are pretty similar. You can avoid it by setting only the multipliers in the test and performing the other operations “out” of the `if`.

• Python allow you to unpack iterables into several variables at once. See here and here for details. Combined with implicit tuples it allows to simply write:

``````attacker, defender = defender, attacker
``````

if you want to swap the two variables.

• Similarly, in some places, using the ternary operator `x if condition else y` will make your code shorter and cleaner.

• The builtin function `max` will also help you make the code more readable.

• Spacing is important for readability and should be consistent: no space before an opening parenthesis in a function call, one space after a coma… You should also try to limit your line length to 80 characters. Read PEP 8 for a full list of recommendations and some working examples.

Also `print` already uses a space as separator between arguments. You don't need to add them manually. Or, if you want to explicitly position them, you can override `print` default separator with the `sep` keyword:

``````print('Hello', 'world!', sep='-') # outputs 'Hello-world!'
``````

# Bugs

• `Unit`'s `maxHP` is set but never used. It sounds to me like you forgot to implement what you intended it for.

• A `battle` will continue if the `defender` died and failed to use it’s `mp` to heal itself.

# Proposed Improvements

I used `getattr` and `setattr` to dynamically update the attribute represented by the string chosen by `random.choice`. If you’re not used to it and don't quite get it, your approach is fine. Just make sure to not repeat yourself when computing the increment for the attribute.

``````import random
from time import sleep

class Unit:
def __init__(self, n, hp, s, d, a, mp):
self.name = n
self.maxHP = hp
self.curHP = hp
self.strength = s
self.defence = d
self.agility = a
self.mp = mp

def __str__(self):
descr = "[{}] HP: {}, STRENGTH: {}, DEFENSE: {}, AGILITY: {}, MP: {}"
return descr.format(self.name, self.curHP, self.strength,
self.defence, self.agility, self.mp)

return self.curHP <= 0

def perform_attack(self, ennemy):
if random.randint(1,5) != random.randint(1,6):
strength_multiplier = random.randint(70, 150) / 100
defence_multiplier = random.randint(70, 150) / 100
else:
strength_multiplier = 2
defence_multiplier = 0.5
print("CRITICAL HIT!!!!")
attackDamage = max(self.strength * strength_multiplier -
ennemy.defence * defence_multiplier, 0)
ennemy.curHP = round(ennemy.curHP - attackDamage, 1)
print(self.name, "did", attackDamage, "damage to", ennemy.name, ".")

def heal(self):
self.maxHP = self.curHP
self.curHP += 3

def heal_with_mp(self):
if self.mp > 0:
self.mp -= 1
print(self.name, "try to use an MP to heal!")
self.curHP += random.randint(4, 16)
sleep(2)
print("No effect…", self.name, "died")
else:
print("It worked! HP:", self.curHP)

def swifter(self, ennemy):
return self.agility > ennemy.agility

def battle(self, ennemy):
# Resolve first attacker based on agility
# we attack first if there is a draw
attacker, defender = ((ennemy, self)
if ennemy.swifter(self)
else (self, ennemy))

turn = 0
turn += 1
print()
print("---  Turn", turn, ":", attacker.name, "---" )
print(attacker.name, ":", attacker.curHP)
print(defender.name, ":", defender.curHP)
print()
sleep(.7)

# Allow less agile defenders to avoid the attack
# with a lower probability chance
miss_factor = 4 if defender.swifter(attacker) else 20
if random.randint(1,miss_factor) == miss_factor:
print(attacker.name, "missed.")
else:
attacker.perform_attack(defender)

defender.heal_with_mp()

attacker, defender = defender, attacker

winner = ennemy.name if self.is_dead() else self.name
print()
print()
print(winner, "won the battle!")

def encounter(self, ennemy):
self.battle(ennemy)
self.heal()
self.power_up()
print(self)

def power_up(self):
trait = random.choice(('strength', 'defence', 'agility'))
new_value = getattr(self, trait) + random.randint(1, 2)
setattr(self, trait, new_value)
print(trait.upper(), "UP!", trait.title(), "is now", new_value)

def journey():
name = input("Your name: ")

character = Unit(name,
random.randint(13, 30),
random.randint(2, 5),
random.randint(1, 3),
random.randint(1, 5),
random.randint(1, 5))
print(character)

enemy = Unit("Monster",
random.randint(10, 25),
random.randint(2, 6),
random.randint(1, 3),
random.randint(1, 5),
random.randint(0, 1))
print(enemy)
if input("Fight? [y/n]: ").strip().lower() not in {'y', 'yes', ''}:
break
character.encounter(enemy)

if __name__ == "__main__":
journey()
``````

# Going further

The first step forward to improve your script could be to use inheritance to provide several presets of monsters: create several class that inherit from `Unit` and use `super` to fix default values. You can still have a `GenericMonster` class whose call to `super().__init__` will still use random values. Try to make your monsters unique with a combination of two high traits and two low ones (or one high and 3 medium). You will then be able to build a random monster with:

``````EnemyType = random.choice((GenericMonster, Zombie, Blob, ReptilianWarrior,...))
enemy = EnemyType()
``````

You can also allow your monster to take the number of rounds as parameter and be stronger and stronger as the brawl goes on.

An other interesting change would be to allow the user to input a space separated sequence of integer to be used as an initializer of his character traits. Build a list of constraints (maximum 40 points, life between 15 and 30, strength between 2 and 10, etc) and check them either in the constructor or before.

With several types of monsters, you could rewrite your power-up system to gain traits based on the type of enemy you encountered (a strong enemy will build up your defense, an agile one your agility…). Or you could also let the user choose.

You can then build a text or ascii-art based labyrinth traversal so that refusing to fight a monster will still allow you to choose an other direction and find an other one (hopefully easier) to fight.

You can then improve it to add loots, inventory, curses, potions, shops…

Last step is to turn it into an MMORPG :)

#### @brian_o 2015-11-05 16:32:32

This is a great answer and great advice. Don't be daunted by the proposed changes. One of the nice things about prototyping in Python is the ability to mix object-oriented and procedural code; you don't have to implement all these changes at once! Using your code as the starting point, try implementing Mathias's `swifter` function, then use that to determine `attacker` and `defender`. You'll be slowly transitioning behavior to the `Unit` class instead of the `battle` function. Most important: Keep running and testing the program between changes.