0

I just started learning python so I'm not familiar with the various tricks or tools, or the proper way to word my question. Because of that, I was unable to find previous questions that do what I am looking for.

I have a working code outlined here:

import random

class UserGroup:
    def __init__(self, users):
        self.user_list = users

    def random_users(self):
        self.random_1 = random.choice(self.user_list)
        self.random_2 = self.random_1
        while self.random_2 == self.random_1:
            self.random_2 = random.choice(self.user_list)
        return self.random_1, self.random_2

class User:
    def __init__(self, nickname, stats):
        self.nickname = nickname
        self.strength = stats['strength']
        self.constitution = stats['constitution']
        self.dexterity = stats['dexterity']
        self.intelligence = stats['intelligence']
        self.wisdom = stats['wisdom']
        self.charisma = stats['charisma']

    def __repr__(self):
        return self.nickname

class Jared(User):
    def fight_stat(self):
        self.attack = self.strength + self.intelligence
        self.defense = self.constitution * 2
        self.speed = self.dexterity / 2

class Poptart(User):
    def fight_stat(self):
        self.attack = self.strength + self.dexterity
        self.defense = self.dexterity
        self.speed = self.dexterity + self.charisma

class Kaos(User):
    def fight_stat(self):
        self.attack = self.dexterity + self.wisdom
        self.defense = self.wisdom * 2
        self.speed = self.dexterity

class Magda(User):
    def fight_stat(self):
        self.attack = self.intelligence + self.charisma
        self.defense = self.dexterity + self.charisma
        self.speed = self.dexterity + self.constitution / 2

class Battle:
    def __init__(self, user1, user2):
        self.user1 = user1
        self.user2 = user2
        print(user1, "and", user2, "have entered the fight!")

    def fight(self):
        self.user1.fight_stat()
        self.user2.fight_stat()
        if self.user1.speed > self.user2.speed:
            self.attacker = self.user1
            self.defender = self.user2
        elif self.user2.speed > self.user1.speed:
            self.attacker = self.user2
            self.defender = self.user1
        elif self.user1.dexterity > self.user2.dexterity:
            self.attacker = self.user1
            self.defender = self.user2
        else: 
            self.attacker = self.user2
            self.defender = self.user1
        if self.attacker.attack > self.defender.defense:
            return self.attacker
        elif self.defender.attack > self.attacker.defense:
            return self.defender
        else:
            return "Draw"

# HERE STARTS BATTLE CODE
jared = Jared('Jarebear', {'strength': 7, 'constitution': 6, 'dexterity': 6, 'intelligence': 8, 'wisdom': 5, 'charisma': 5})
poptart = Poptart('Yung SLizzy', {'strength': 4, 'constitution': 5, 'dexterity': 10, 'intelligence': 7, 'wisdom': 5, 'charisma': 7})
kaos = Kaos('Kung Cows', {'strength': 8, 'constitution': 7, 'dexterity': 6, 'intelligence': 4, 'wisdom': 7, 'charisma': 4})
magda = Magda('Meghan M', {'strength': 7, 'constitution': 5, 'dexterity': 7, 'intelligence': 8, 'wisdom': 5, 'charisma': 5})

users = UserGroup([jared, poptart, kaos, magda])

for i in range(1,4):
    print("Battle number", i)
    battle = Battle(*users.random_users())
    print("The winner is: ", battle.fight())

The example output is shown below:

Battle number 1
Jarebear and Kung Cows have entered the fight!
The winner is:  Kung Cows
Battle number 2
Jarebear and Kung Cows have entered the fight!
The winner is:  Kung Cows
Battle number 3

As I have written it, the code performs as expected. However, I am concerned about the way that I've implemented the fight() method inside the Battle class. I don't think the large sequence of if statements is the proper way to say "user with higher speed attacks first". Logically, I just need a statement that is like self.attacker = max(self.user1.speed, self.user2.speed) but the attacker is set to the user, not the user's speed. However, I don't know how to accomplish this in one or two lines of code in python.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
Jared C
  • 362
  • 7
  • 19
  • Try `self.attacker = max(self.user1, self.user2, key=lambda u: u.speed)`. The only issue is that it will favor user1 when the speed of user1 and user2 are the same. – John Szakmeister Jan 02 '19 at 01:27
  • in my if/else statements I have it look at the dexterity if speed happens to be the same. Any way to include that? – Jared C Jan 02 '19 at 01:31
  • why not create a `def who_goes_first(user1,user2)` method - put your logic into it and if they draw, choose one randomly? cleans up your fight code, creates a funciton with low reason to change and makes it more obvious whats happening? – Patrick Artner Jan 02 '19 at 01:34
  • I felt if I move the if/else statements into yet another function, that I would just be over-engineering what should be a simple solution – Jared C Jan 02 '19 at 01:35
  • @JaredC yes, just return a tuple of what you want out of the lambda function. In this case, it would be: `self.attacker = max(self.user1, self.user2, key=lambda u: (u.speed, u.dexterity))` I do recommend the suggestion about factoring the logic out into it's own method. As a seasoned developer (20+ years of experience), I would not call it over-engineering, I'd call it making it readable and providing logical separation of responsibilities. :-) – John Szakmeister Jan 02 '19 at 09:31

7 Answers7

3

The min and max functions accept a key function that tells them how to compare the inputs. The key accepts each input and returns the actual value to compare:

max(self.user1, self.user2, key=lambda item: item.speed)

For a large number of comparisons, this can be rewritten as

from operator import attrgetter

speed_key = attrgetter('speed')
max(self.user1, self.user2, key=speed_key)

If you have equal speeds, you can decide to compare using a different attribute. This is done by understanding that Python sequences are compared in lexicographixal order. This is easiest to understand with strings, since it's basically just dictionary order: e.g. 'abc' > 'abb' because each element is compared in order. The same applies to lists and tuples: [1, 2, 3] > [1, 2, 2].

So to use the dexterity attribute as a fallback for equal speeds, do

max(self.user1, self.user2, key=lambda item: (item.speed, item.dexterity))

OR

speedex_key = attrgetter('speed', 'dexterity')
max(self.user1, self.user2, key=speedex_key)
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
1

Creating small methods that concentrate logic is no "overhead". They are easy to understand, and do not need to change for lots of reasons - hence they are done, testet and unchanged most of the time.

class Battle:

    # snipped some code    

    @staticmethod
    def get_attacker_defender(u1, u2):
        """Returs tuple (attacker,defender) based on given user."""
        if u1.speed > u2.speed:
            return u1,u2
        elif u2.speed > u1.speed:
            return u2,u1
        # probably cleaner to stay with elif: ... else ... but this is shorter
        return (u1,u2) if u1.dexterity > u2.dexterity else (u2,u1)


    def fight(self):
        self.user1.fight_stat()
        self.user2.fight_stat()
        self.attacker, self.defender = Battle.get_attacker_defender(self.user1,self.user2)

        if self.attacker.attack > self.defender.defense:
            return self.attacker
        elif self.defender.attack > self.attacker.defense:
            return self.defender
        else:
            return "Draw"

If you like a brain teaser you could as well do:

@staticmethod
def get_attacker_defender(u1,u2):
    return sorted( [u1,u2], key = lambda u:(u.speed,u.dexterity), reverse=True)

which leverages tuple-sorting that sorts based on the 1st element, and when the 1st ones are equal on the 2nd element of the tuple. If both are equal the order stays as is (stable sorting with Timsort).

Doku:

Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • If I can learn to understand and use the "brain teaser" then I think I will become a better programmer – Jared C Jan 02 '19 at 01:51
  • @JaredC you might - but it will make the code only shorter - not cleaner – Patrick Artner Jan 02 '19 at 01:52
  • There has been a wealth of knowledge offered in response to my question, and I think I can take things from every answer and improve my understanding of python. This is the answer which best fits my specific needs, although many of the answers presented could be used to achieve the desired results. – Jared C Jan 02 '19 at 12:54
1

If you want to use max with your objects, you can implement __gt__ com compare them (and __eq__ for consistency). So your User class could look like this:

class User:
    def __init__(self, nickname, stats):
        self.nickname = nickname
        self.strength = stats['strength']
        self.constitution = stats['constitution']
        self.dexterity = stats['dexterity']
        self.intelligence = stats['intelligence']
        self.wisdom = stats['wisdom']
        self.charisma = stats['charisma']

    def __repr__(self):
        return self.nickname

    def __eq__(self, other):
        return self.speed == other.speed and self.dexterity == other.dexterity

    def __gt__(self, other):
        return self.speed > other.speed or self.dexterity > other.dexterity

This implementation would allow you to use max and min to define the attacker and defender in the Battle class.

class Battle:
    def __init__(self, user1, user2):
        self.user1 = user1
        self.user2 = user2
        print(user1, "and", user2, "have entered the fight!")

    def fight(self):
        self.user1.fight_stat()
        self.user2.fight_stat()

        self.attacker = max(self.user1, self.user2)
        self.defender = min(self.user1, self.user2)

        if self.attacker.attack > self.defender.defense:
            return self.attacker
        elif self.defender.attack > self.attacker.defense:
            return self.defender
        else:
            return "Draw"
Renan Ivo
  • 1,368
  • 9
  • 17
0

You can indeed use max() to get the user with most speed.

The way of doing it is with the keyargument which you use to pass a custom scoring function that takes each object and returns a orderable value like a float or int.

def score(user):
    return user.speed

fastest_user = max(list_of_users, key=score)

The most common is to pass an anonymous lambda function which you define with the syntax:

lambda var1, var2: expression

So the code would look like:

fastest_user = max(list_of_users, key=lambda user: user.speed)
  • Is there a way to have it check the dexterity score (or just select one at random) if the speed of the users is the same? – Jared C Jan 02 '19 at 01:36
  • Max will just return the first element if all the users have the same speed, so shuffling the list before should do the trick. for that just do `from random import shuffle` and use that function to mess the list before. –  Jan 02 '19 at 01:43
0

I just need a statement that is like self.attacker = max(self.user1.speed, self.user2.speed) but the attacker is set to the user, not the user's speed.

You can use

from operator import attrgetter
...
self.attacker = max(self.user1, self.user2, key=attrgetter('speed'))

This will return the user (not speed) has max speed

If you have more than 2 users, you can also pass list

More information about attrgetter

user3790180
  • 433
  • 5
  • 12
  • Is there a way to have it check the dexterity score (or just select one at random) if the speed of the users is the same? – Jared C Jan 02 '19 at 01:36
0

Here is an answer that separates the logic as was mentioned above, and uses your original max desire. The benefit to this solution is that logic to determine first attack could be easily refactored. Simply change the order (or add attributes to) attribute_priority_order

class Battle:
    def __init__(self, user1, user2):
        self.user1 = user1
        self.user2 = user2
        print(user1, "and", user2, "have entered the fight!")


    @staticmethod
    def _compare_attributes(user1: User, user2: User, attribute: str):
        if getattr(user1, attribute) != getattr(user2, attribute):
            return max(
                [user1, user2],
                key=lambda user: getattr(user, attribute)
            )

    def get_attacker(self):
        """
        Returns attacker based on attribute comparison
        :return: User
        """
        default_attacker = self.user2
        attribute_priority_order = [
            'speed',
            'dexterity'
        ]

        for attribute in attribute_priority_order:
            attacker = self._compare_attributes(
                user1=self.user1,
                user2=self.user2,
                attribute=attribute
            )
            if attacker:
                return attacker

        return default_attacker

    def get_defender(self):
        """
        User in the battle that isn't the attacker.
        :return: User
        """
        for user in [self.user1, self.user2]:
            if str(user) != str(self.attacker):
                return user


    def fight(self):
        self.user1.fight_stat()
        self.user2.fight_stat()

        self.attacker = self.get_attacker()
        self.defender = self.get_defender()

        if self.attacker.attack > self.defender.defense:
            return self.attacker
        elif self.defender.attack > self.attacker.defense:
            return self.defender
        else:
            return "Draw"

With a little extra abstraction, _compare_attributes method could be recycled to determine the winner as well maybe with an optional second attribute. The return from this method is optionally a user if there is no tie.

Andrew Riess
  • 324
  • 4
  • 6
0

How about using the fact that python lists compare so your ability measure is:

def ability(user):
    return (user.speed, user.dexterity)

Your winning criteria is:

def winner(attacker, defender):
    return attacker.attack > defender.defense

And the fight becomes:

def fight(a, b):
    return winner(a, b) if ability(a) >= ability(b) else winner(b, a)
Mike Robins
  • 1,733
  • 10
  • 14