Enable Javascript in your browser and then refresh this page, for a much enhanced experience.
Final optimized OOP Solution (iteratively improved among all challenges of the saga) solution in Clear category for The Warlords by bsquare
from collections import deque
class Weapon:
def __init__(self, health, attack, defense=0, vampirism=0, heal_power=0):
self.health = self.max_health = health
self.attack = attack
self.defense = defense
self.vampirism = vampirism
self.heal_power = heal_power
class Sword(Weapon):
def __init__(self):
super().__init__(health=5, attack=2)
class Shield(Weapon):
def __init__(self):
super().__init__(health=20, attack=-1, defense=2)
class GreatAxe(Weapon):
def __init__(self):
super().__init__(health=-15, attack=5, defense=-2, vampirism=10)
class Katana(Weapon):
def __init__(self):
super().__init__(health=-20, attack=6, defense=-5, vampirism=50)
class MagicWand(Weapon):
def __init__(self):
super().__init__(health=30, attack=3, heal_power=3)
class Warrior:
_max_health = 50
def __init__(self, attack=5):
self._army = None
self._weapons = []
self._health = self._max_health
self._attack = attack
def join_army(self, army):
self._army = army
return self
@property
def health(self):
return self._health
@health.setter
def health(self, new_health):
self._health = min(new_health, self.max_health)
if not self.is_alive and self._army is not None:
self._army.notify_dead_warrior(self)
@property
def max_health(self):
return self._max_health + sum([weapon.max_health for weapon in self._weapons])
@property
def attack(self):
return max(0, self._attack + sum([weapon.attack for weapon in self._weapons]))
@attack.setter
def attack(self, new_attack):
self._attack = new_attack
@property
def is_alive(self):
return self.health > 0
def equip_weapon(self, weapon):
self._weapons.append(weapon)
self.health = max(0, self.health + weapon.health)
def manage_heal(self, heal_strength):
self.health += heal_strength
def manage_hit(self, hit_power):
# Ensures hit power is positive, for instance with weapons it can not be the case.
if hit_power < 0:
return 0
self.health -= hit_power
return hit_power
def hit(self, targets, specific_damage=None):
# In this implementation targets is the list of all living opponents,
# by default, we still keep figthing only the first one.
target = targets[0]
# Optional specific_damage can be specified (when the lancer is hitting the 'next unit'.
return target.manage_hit(specific_damage if specific_damage is not None else self.attack)
def fight(self, target):
attacking_unit, target_unit = self, target
while attacking_unit.is_alive and target_unit.is_alive:
attacking_unit.hit([target_unit])
attacking_unit, target_unit = target_unit, attacking_unit
return self.is_alive
def __str__(self):
return f"Type={self.__class__.__name__}; Health={self.health}; Attack={self.attack}"
class Knight(Warrior):
def __init__(self):
super().__init__(attack=7)
class Defender(Warrior):
_max_health = 60
def __init__(self, attack=3, defense=2):
super().__init__(attack=attack)
self._defense = defense
@property
def defense(self):
return max(0, self._defense + sum([weapon.defense for weapon in self._weapons]))
def manage_hit(self, hit_power):
return super().manage_hit(hit_power - self.defense)
def __str__(self):
return super().__str__() + f"; Defense={self.defense}"
class Vampire(Warrior):
_max_health = 40
def __init__(self):
super().__init__(attack=4)
self._vampirism = 50
@property
def vampirism(self):
return max(0, self._vampirism + sum([weapon.vampirism for weapon in self._weapons]))
def hit(self, targets, specific_damage=None):
removed_health = super().hit(targets)
self.health += self.vampirism * removed_health // 100
def __str__(self):
return super().__str__() + f"; Vampirism={self.vampirism}"
class Lancer(Warrior):
def __init__(self):
super().__init__(attack=6)
def hit(self, targets, specific_damage=None):
# The hit is the same than usual, with the first opponent.
removed_health = super().hit(targets)
# If there is another opponent "behind", deals half of the same damage.
if len(targets) > 1:
additional_damage = removed_health // 2
# Important: the first argument MUST be a list, even if there is only one warrior.
super().hit([targets[1]], specific_damage=additional_damage)
class Healer(Warrior):
_max_health = 60
def __init__(self):
super().__init__(attack=0)
self._heal_power = 2
@property
def heal_power(self):
return max(0, self._heal_power + sum([weapon.heal_power for weapon in self._weapons]))
def heal(self, target):
target.manage_heal(self.heal_power)
def __str__(self):
return super().__str__() + f"; Heal_power={self.heal_power}"
class Warlord(Defender):
_max_health = 100
def __init__(self):
super().__init__(attack=4, defense=2)
class Army(deque):
army_number = 0
def __init__(self):
super().__init__()
Army.army_number += 1
self._army_number = Army.army_number
@property
def units(self):
return self
def add_units(self, cls, unit_count):
# An army can only have one Warlord at most.
if cls == Warlord:
if any(filter(lambda unit: isinstance(unit, Warlord), self)):
return
unit_count = 1
self.extend(cls().join_army(self) for _ in range(unit_count))
def head_unit_healer(self):
'''
Returns the second unit if exists, and it is a Healer.
'''
return self[1] if len(self) > 1 and isinstance(self[1], Healer) else None
@property
def is_alive(self):
return bool(len(self))
def notify_dead_warrior(self, warrior):
self.remove(warrior)
self.move_units()
def move_units(self):
warlord = next(filter(lambda unit: isinstance(unit, Warlord), self), None)
if not warlord:
return
lancers, healers, others = [], [], []
for unit in self:
if isinstance(unit, Lancer):
lancers.append(unit)
elif isinstance(unit, Healer):
healers.append(unit)
elif not isinstance(unit, Warlord):
others.append(unit)
new_ordered_units = lancers or others
new_ordered_units[1:1] = healers
if lancers:
new_ordered_units.extend(others)
new_ordered_units.append(warlord)
self.clear()
self.extend(new_ordered_units)
def fight(self, target):
# N.B.: compared to previous challenge's solution, we can't use anymore the Warrior.fight() method
# because we need to manage several opponents.
attacking_army, target_army = self, target
while attacking_army.is_alive and target_army.is_alive:
attacking_army, target_army = self, target
while True:
attacking_unit, target_unit = attacking_army[0], target_army[0]
attacking_unit.hit(target_army)
attacking_unit_healer = attacking_army.head_unit_healer()
if attacking_unit_healer:
attacking_unit_healer.heal(attacking_unit)
if not target_unit.is_alive:
break
attacking_army, target_army = target_army, attacking_army
return self.is_alive
def straight_fight(self, target):
while self.is_alive and target.is_alive:
# N.B.: using list to duplicate army data structure to avoid mutation while iterating on them.
for unit_1, unit_2 in zip(*map(list, (self, target))):
fight(unit_1, unit_2)
return self.is_alive
def __str__(self):
return f"Army #{self._army_number}: " + "; ".join([f"({unit})" for unit in self])
# Defines cosmetic functions/methods to fit challenge requirements.
class Battle:
fight, straight_fight = map(staticmethod, (Army.fight, Army.straight_fight))
fight = Warrior.fight
Sept. 14, 2019
Comments: