Python Forum
OOP vs functional - - elaborate turn based RPG game (Derek Banas Udemy course again)
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
OOP vs functional - - elaborate turn based RPG game (Derek Banas Udemy course again)
#1
I’m learning OOP. Derek Banas is the instructor in the online (self directed, non-credit) Udemy course titled, “Python Programming Bootcamp” that I am taking. He demonstrates OOP well with a CLI game where two warriors with 100 health take turns attacking each other, reducing health by 20-50 points at each turn. The first warrior that reaches 0 health, loses. I am able to follow along and understand the basic principles of OOP demonstrated by the instructor. But for the fun of it and as an exercise, I decided to port the script from class methods/attributes to functions/variables as a personal challenge.

My script works but only sometimes. Here is some sample output:

Quote:$ python script.py
Ironman's health is: 30
Loki's health is: -2
The winner is: Ironman with 30 health remaining

Above, that output is expected. However below, the output is wrong:

Quote:$ python script.py
Ironman's health is: -28
Loki's health is: -12
The winner is: Ironman with -28 health remaining

This is wrong because Ironman’s health is less than Loki’s yet the winner is declared to be Ironman. How might you people alter my script to better account for this discrepancy?

Here is my script:

import random
import math

health = 100
attack_max = 50
first_warrior = "Ironman"
second_warrior = "Loki"

def attack(attack_max):
    attack_amount = attack_max * (random.random() +.5)
    return attack_amount

def fight(first_warrior_health, second_warrior_health):
    while True:
        warrior_A_attack_amount = attack(attack_max)
        warrior_B_attack_amount = attack(attack_max)
        damage_to_warrior_B = math.ceil(warrior_A_attack_amount)
        damage_to_warrior_A = math.ceil(warrior_B_attack_amount)
        first_warrior_health = health - damage_to_warrior_A
        second_warrior_health = health - damage_to_warrior_B
        first_warrior_health = first_warrior_health - damage_to_warrior_A
        second_warrior_health = second_warrior_health - damage_to_warrior_B
        print(f"{first_warrior}'s health is: {first_warrior_health}")
        print(f"{second_warrior}'s health is: {second_warrior_health}")
        if second_warrior_health <= 0:
            print(f"The winner is: {first_warrior} with {first_warrior_health} health remaining")
            break
        elif first_warrior_health <= 0:
            print(f"The winner is: {second_warrior} with  {second_warrior_health} health remaining")
            break
    
def main():
    fight(100, 100)
    
main()
I see that the instructor’s OOP demo (below) is more dynamic and feature rich. It’s easier to read and follow along too. OOP is clearly the better way to go in this situation. But how might you people improve my attempt at implementing this game via functions/variables (above)?

Here is the instructor’s original:

'''

This original script was provided by Derek Banas in his Udemy course in Section 19 - Classes and Objects. Derek provided two scripts, demonstrating how to model classes of a video game.

I have ventured to re-write the functionality  of this OOP script but using functions/variables instead of class methods/objects

'''


# We will create classes for both a Warrior and a Battle class
# The Warrior class will simulate both the attributes and capabilities of a Warrior
# The Battle class will however simulate the actions that occur in a battle such as starting the fight and getting the results

import random
import math

# Warriors will have names, health, and attack and block maximums
# They will have the capabilities to attack and block random amounts
class Warrior:
    def __init__(self, name="warrior", health=0, attk_max=0, block_max=0):
        self.name = name
        self.health = health
        self.attk_max = attk_max
        self.block_max = block_max

    def attack(self):
        # Randomly calculate the attack amount
        # random() returns a value from 0.0 to 1.0
        attk_amt = self.attk_max * (random.random() + .5)
        return attk_amt

    def block(self):
        # Randomly calculate how much of the attack was blocked
        block_amt = self.block_max * (random.random() + .5)
        return block_amt

# The Battle class will have the capability to loop until 1 Warrior dies
# The Warriors will each get a turn to attack each turn
class Battle:
    def start_fight(self, warrior1, warrior2):
        # Continue looping until a Warrior dies switching back and
        # forth as the Warriors attack each other
        while True:
            if self.get_attack_result(warrior1, warrior2) == "Game Over":
                print("Game Over")
                break

            if self.get_attack_result(warrior2, warrior1) == "Game Over":
                print("Game Over")
                break

    # A function will receive each Warrior that will attack the other
    # Have the attack and block amounts be integers to make the results clean
    # Output the results of the fight as it goes
    # If a Warrior dies return that result to end the looping in the
    # above function

    # Make this method static because we don't need to use self
    @staticmethod
    def get_attack_result(warriorA, warriorB):
        warrior_a_attk_amt = warriorA.attack()
        warrior_b_block_amt = warriorB.block()
        damage_2_warrior_b = math.ceil(warrior_a_attk_amt - warrior_b_block_amt)
        warriorB.health = warriorB.health - damage_2_warrior_b

        print("{} attacks {} and deals {} damage".format(warriorA.name, warriorB.name, damage_2_warrior_b))
        print("{} is down to {} health".format(warriorB.name,warriorB.health))

        if warriorB.health <= 0:
            print("{} has Died and {} is Victorious".format(warriorB.name, warriorA.name))
            return "Game Over"
        else:
            return "Fight Again"

def main():
    # Create 2 Warriors
    Batman = Warrior("Batman", 100, 40, 10)
    Joker = Warrior("Joker", 100, 40, 10)
    # Create Battle object
    battle = Battle()
    # Initiate Battle
    battle.start_fight(Batman, Joker)

main()
Here is some output from running the instructor's scritp:
Quote:› python re-written-v1.py
Ironman's health is: -28
Loki's health is: -12
The winner is: Ironman with -28 health remaining
gnull at gnosis in [~/dev/projects/python/2018-and-2020/learning-classes-Derek-Banas-Udemy/video-game] on git:master ✗ d8f0a68 "Initialize Derek Banas script"
18:36:42 › python original.py
Batman attacks Joker and deals 31 damage
Joker is down to 69 health
Joker attacks Batman and deals 27 damage
Batman is down to 73 health
Batman attacks Joker and deals 14 damage
Joker is down to 55 health
Joker attacks Batman and deals 48 damage
Batman is down to 25 health
Batman attacks Joker and deals 34 damage
Joker is down to 21 health
Joker attacks Batman and deals 40 damage
Batman is down to -15 health
Batman has Died and Joker is Victorious
Game Over
Reply
#2
You assume a dead warrior still has an attack. There may be other problems, but that is a big one.

I would randomly choose which warrior goes first, otherwise one is always at a distinct disadvantage
Reply
#3
Note that functional programming isn't just about programming with functions. There are a whole bunch of other ideas - immutability, pure functions, referential transparency being some of them. Kelvin Henney has some talks on these things (they tend to be in other languages, but it's the ideas that are important):

https://youtu.be/lNKXTlCOGEc
https://youtu.be/NSzsYWckGd4

In case you're interested in how one does error handling in a functional style, I wrote a post about it here: https://python-forum.io/Thread-returning...#pid138294.
Reply
#4
One way to go about it would be to make each warrior a dictionary.

import random
import math

def get_attack_result (warriorA, warriorB) :
	warrior_a_attk_amt = warriorA ['attk_max'] * (random.random () + .5)
	warrior_b_block_amt = warriorB ['block_max'] * (random.random () + .5)
	damage_2_warrior_b = math.ceil(warrior_a_attk_amt - warrior_b_block_amt)
	warriorB ['health'] = warriorB ['health'] - damage_2_warrior_b

	print("{} attacks {} and deals {} damage"
		.format (warriorA ['name'], warriorB ['name'], damage_2_warrior_b))
	print("{} is down to {} health"
		.format (warriorB ['name'], warriorB ['health']))

	if warriorB ['health'] <= 0:
		print("{} has Died and {} is Victorious"
			.format (warriorB ['name'], warriorA ['name']))
		return "Game Over"
	else:
		return "Fight Again"

player_one = {
	'name': 'Ironman',
	'health': 100,
	'attk_max': 40,
	'block_max': 10
}
player_two = {
	'name': 'Loki',
	'health': 100,
	'attk_max': 40,
	'block_max': 10
}

while True:
	if get_attack_result(player_one, player_two) == "Game Over":
		print("Game Over")
		break

	if get_attack_result(player_two, player_one) == "Game Over":
		print("Game Over")
		break
Reply
#5
Holy Smokes! Thank you, @BashBedlam, for taking the time to re-write the script. Your approach with dictionaries to model player properties is terrific. I'm going to have alotta fun playing around with your script in my Python debugger tonight. Thanks for showing a wonderful alternative!
Smile
Reply
#6
I've worked on code that uses dictionaries where it should be using classes. I cannot stress strongly enough how bad this kind of code is to work with. Not only is it difficult to read, but it is hard to trace when dictionary values are set. A good linter will find where you made a typo in an attribute name, but I don't think any tool will catch the mistake when you set player2['Health']=80.
Reply
#7
collections.namedtuple is good for immutable value objects.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  functional LEDs in an array or list? // RPi user Doczu 5 1,522 Aug-23-2022, 05:37 PM
Last Post: Yoriz
  Stuck in functional proggaming haze hammer 2 1,374 Oct-27-2021, 02:07 PM
Last Post: hammer
  A text-based game [SOLVED] Gameri1 6 3,846 Apr-20-2021, 02:26 PM
Last Post: buran
  Calculating surface area - - OOP or functional? Derek Banas Udemy course Drone4four 5 3,529 Mar-13-2021, 06:22 AM
Last Post: buran
  Winning/Losing Message Error in Text based Game kdr87 2 2,927 Dec-14-2020, 12:25 AM
Last Post: bowlofred
  Turn coordinates in string into a tuple Kolterdyx 3 2,697 Jun-10-2020, 05:04 AM
Last Post: buran
  How do i turn my program into a .exe julio2000 1 1,850 Feb-14-2020, 08:18 PM
Last Post: snippsat
  How do I turn a directory of HTML files into one CSV? glittergirl 2 1,748 Sep-21-2019, 05:33 PM
Last Post: glittergirl
  Turn py into exe tester21 4 2,982 Jul-22-2019, 04:31 PM
Last Post: nilamo
  Not sure how to turn this into a loop iamgonge 1 2,058 Dec-05-2018, 11:03 PM
Last Post: anandoracledba

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020