Skip to content

Commit

Permalink
Update damage calculation logic and write tests fpr combat simulation…
Browse files Browse the repository at this point in the history
… class
  • Loading branch information
Liesegang committed Feb 27, 2024
1 parent 8566b2e commit 73aafa1
Show file tree
Hide file tree
Showing 12 changed files with 533 additions and 122 deletions.
12 changes: 11 additions & 1 deletion coc7e_combat_simulator/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def __init__(self, name: str, attributes: Attribute, skills: List[Skill]):
self.side = None
self.target_selection_strategy = RandomTargetSelectionStrategy()
self.skill_selection_strategy = RandomSkillSelectionStrategy()
self.reply_strategy = NothingReactionStrategy()
self.reaction_strategy = NothingReactionStrategy()

@classmethod
def of(
Expand Down Expand Up @@ -70,6 +70,16 @@ def add_skill(self, skill: Skill) -> None:
raise ValueError("skill must be an instance of Skill")
self.skills.append(skill)

def get_skill(self, skill_name: str) -> Skill | None:
for skill in self.skills:
if skill.name == skill_name:
return skill
if skill_name == "Dodge":
return Skill("Dodge", self.attributes.dexterity // 2, "0")
if skill_name == "Fighting (Brawl)":
return Skill("Fighting (Brawl)", 25, "1D4")
return None

def __repr__(self) -> str:
skills_str = ", ".join([str(skill) for skill in self.skills])
return f"Attributes: {self.attributes}\nHP: {self.hp}, MP: {self.mp}\nSkills: {skills_str}"
212 changes: 104 additions & 108 deletions coc7e_combat_simulator/combat_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@


class LevelOfSuccess(IntEnum):
CRITICAL = 0
SPECIAL = 1
HARD = 2
SUCCESS = 3
FAILURE = 4
FUMBLE = 5
CRITICAL = 5
SPECIAL = 4
HARD = 3
SUCCESS = 2
FAILURE = 1
FUMBLE = 0


class CombatSimulator:
Expand All @@ -32,24 +32,41 @@ def __init__(
self.group_b_characters_init_strategy = group_b_characters_init_strategy

@staticmethod
def calculate_damage(character: Character, skill: Skill) -> int:
def calculate_damage(
character: Character, skill: Skill, success_level: LevelOfSuccess
) -> int:
logger.info(f"{character.name} in side {character.side} used {skill.name}.")

damage_result = dice_parser.parse(skill.damage)[0]

if skill.physical_attack:
damage_result += dice_parser.parse(character.db)[0]

logger.info(
f"{character.name} in side {character.side} dealt {damage_result} damage."
)
return damage_result
if success_level <= LevelOfSuccess.FAILURE:
logger.info(f"{character.name} in side {character.side} failed.")
return 0

if success_level <= LevelOfSuccess.HARD:
logger.info(f"{character.name} in side {character.side} succeeded with hard.")
damage_result = dice_parser.parse(skill.damage)[0]
if skill.physical_attack:
damage_result += dice_parser.parse(character.db)[0]
return damage_result

if success_level <= LevelOfSuccess.CRITICAL:
logger.info(f"{character.name} in side {character.side} succeeded with critical.")
if skill.impale:
damage_result = dice_parser.maximum(skill.damage) + dice_parser.parse(skill.damage)[0]
if skill.physical_attack:
damage_result += dice_parser.maximum(character.db) + dice_parser.parse(character.db)[0]
return damage_result
else:
damage_result = dice_parser.parse(skill.damage)[0] + dice_parser.parse(skill.damage)[0]
if skill.physical_attack:
damage_result += dice_parser.parse(character.db)[0] + dice_parser.parse(character.db)[0]
return damage_result

@staticmethod
def check_level_of_success(skill: Skill) -> LevelOfSuccess:
roll = random.randint(1, 100)
if roll == 1:
return LevelOfSuccess.CRITICAL
elif roll == 100:
return LevelOfSuccess.FUMBLE
elif roll <= skill.success_rate // 5:
return LevelOfSuccess.SPECIAL
elif roll <= skill.success_rate // 2:
Expand All @@ -61,102 +78,81 @@ def check_level_of_success(skill: Skill) -> LevelOfSuccess:
else:
return LevelOfSuccess.FUMBLE

def someone_left_alive(self, characters: List[Character], side: str) -> bool:
@staticmethod
def someone_left_alive(characters: List[Character], side: str) -> bool:
return any(
character.hp > 0 for character in characters if character.side == side
)

def check_damage(
self, character: Character, skill: Skill, target: Character
def process_attack(
self, attacker: Character, skill: Skill, defender: Character
) -> None:
reply = target.reply_strategy.reply(target, character)
if skill.name != "Fighting (Brawl)" or reply == ReactionType.NOTHING:
if self.check_level_of_success(skill) >= LevelOfSuccess.SUCCESS:
damage = self.calculate_damage(character, skill)
target.hp -= damage
logger.info(
f"{character.name} in side {character.side} attacked {target.name} and dealt {damage} damage."
)
return
elif reply == ReactionType.DODGE:
attacker_level_of_success = self.check_level_of_success(skill)
dodge_skill = (
list(filter(lambda skill: skill.name == "Dodge", target.skills))[0]
if list(filter(lambda skill: skill.name == "Dodge", target.skills))
else Skill(
"Dodge",
target.attributes.dexterity // 5 * 3,
"0",
physical_attack=False,
)
)
dodge_level_of_success = self.check_level_of_success(dodge_skill)
if (
attacker_level_of_success > dodge_level_of_success
and attacker_level_of_success >= LevelOfSuccess.SUCCESS
):
damage = self.calculate_damage(character, skill)
target.hp -= damage
logger.info(
f"{character.name} in side {character.side} attacked {target.name} and dealt {damage} damage."
)
return
elif (
attacker_level_of_success <= dodge_level_of_success
and dodge_level_of_success >= LevelOfSuccess.SUCCESS
):
logger.info(
f"{target.name} in side {target.side} dodged {character.name}'s attack."
)
return
else:
logger.info(
f"{character.name} in side {character.side} failed to attack {target.name}."
)
return
elif reply == ReactionType.FIGHT_BACK:
attacker_level_of_success = self.check_level_of_success(skill)
fight_back_skill = (
list(
filter(
lambda skill: skill.name == "Fighting (Brawl)", target.skills
)
)[0]
if list(
filter(
lambda skill: skill.name == "Fighting (Brawl)", target.skills
)
)
else FightingBrawl
)
fight_back_skill_level_of_success = self.check_level_of_success(
fight_back_skill
if not skill.can_take_reaction:
self.apply_attack_if_successful(attacker, skill, defender)
return

reaction = defender.reaction_strategy.reaction(defender, attacker)
if reaction == ReactionType.NOTHING:
self.apply_attack_if_successful(attacker, skill, defender)
elif reaction == ReactionType.DODGE:
self.handle_dodge_reaction(attacker, skill, defender)
elif reaction == ReactionType.FIGHT_BACK:
self.handle_fight_back_reaction(attacker, skill, defender)

def apply_attack_if_successful(
self, attacker: Character, skill: Skill, defender: Character
):
success_level = self.check_level_of_success(skill)
if success_level >= LevelOfSuccess.SUCCESS:
self.apply_damage(attacker, skill, defender, success_level)

def handle_dodge_reaction(
self, attacker: Character, skill: Skill, defender: Character
):
dodge_skill = defender.get_skill("Dodge")

attacker_success_level = self.check_level_of_success(skill)
dodge_success_level = self.check_level_of_success(dodge_skill)

if (
attacker_success_level > dodge_success_level
and attacker_success_level >= LevelOfSuccess.SUCCESS
):
self.apply_damage(attacker, skill, defender, attacker_success_level)

def handle_fight_back_reaction(
self, attacker: Character, skill: Skill, defender: Character
):
fight_back_skill = defender.get_skill("Fighting (Brawl)")

attacker_success_level = self.check_level_of_success(skill)
fight_back_success_level = self.check_level_of_success(fight_back_skill)

if (
attacker_success_level >= fight_back_success_level
and attacker_success_level >= LevelOfSuccess.SUCCESS
):
self.apply_damage(attacker, skill, defender, attacker_success_level)
elif (
attacker_success_level < fight_back_success_level
and fight_back_success_level >= LevelOfSuccess.SUCCESS
):
self.apply_damage(
defender, fight_back_skill, attacker, fight_back_success_level
)
if (
attacker_level_of_success >= fight_back_skill_level_of_success
and attacker_level_of_success >= LevelOfSuccess.SUCCESS
):
damage = self.calculate_damage(character, skill)
target.hp -= damage
logger.info(
f"{character.name} in side {character.side} attacked {target.name} and dealt {damage} damage."
)
return
elif (
attacker_level_of_success < fight_back_skill_level_of_success
and fight_back_skill_level_of_success >= LevelOfSuccess.SUCCESS
):
damage = self.calculate_damage(target, fight_back_skill)
character.hp -= damage
logger.info(
f"{target.name} in side {target.side} fought back {character.name} and dealt {damage} damage."
)
return
else:
logger.info(
f"{character.name} in side {character.side} failed to attack {target.name}."
)
return

def apply_damage(
self,
attacker: Character,
skill: Skill,
target: Character,
success_level: LevelOfSuccess,
):
damage = self.calculate_damage(attacker, skill, success_level)
target.hp -= damage
logger.info(
f"{attacker.name} in side {attacker.side} attacked {target.name} and dealt {damage} damage."
)

def combat_simulation_single(self) -> str:
class CombatEnd(Exception):
Expand Down Expand Up @@ -199,7 +195,7 @@ class CombatEnd(Exception):
character
)

self.check_damage(character, skill, target)
self.process_attack(character, skill, target)
round += 1
except CombatEnd:
pass
Expand Down
5 changes: 3 additions & 2 deletions coc7e_combat_simulator/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ class Skill:
damage: str
physical_attack: bool = False
impale: bool = False
can_take_reaction: bool = False

def __repr__(self) -> str:
return f"{self.name}: Success Rate: {self.success_rate}%, Damage: {self.damage}, Physical Attack: {self.physical_attack}, Impale: {self.impale}"


FightingBrawl = Skill("Fighting (Brawl)", 25, "1D3", True)
FirearmHandgun = Skill("Firearm (Handgun)", 20, "1D10", False)
FightingBrawl = Skill("Fighting (Brawl)", 25, "1D3", True, False, True)
FirearmHandgun = Skill("Firearm (Handgun)", 20, "1D10", False, True, False)
8 changes: 4 additions & 4 deletions coc7e_combat_simulator/strategies/reaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ class ReactionType(Enum):


class ReactionStrategy:
def reply(self, character: "Character", attacker: "Character") -> ReactionType:
def reaction(self, character: "Character", attacker: "Character") -> ReactionType:
raise NotImplementedError()


class NothingReactionStrategy(ReactionStrategy):
def reply(self, character: "Character", attacker: "Character") -> ReactionType:
def reaction(self, character: "Character", attacker: "Character") -> ReactionType:
return ReactionType.NOTHING


class DodgeReactionStrategy(ReactionStrategy):
def reply(self, character: "Character", attacker: "Character") -> ReactionType:
def reaction(self, character: "Character", attacker: "Character") -> ReactionType:
return ReactionType.DODGE


class FightBackReactionStrategy(ReactionStrategy):
def reply(self, character: "Character", attacker: "Character") -> ReactionType:
def reaction(self, character: "Character", attacker: "Character") -> ReactionType:
return ReactionType.FIGHT_BACK
4 changes: 2 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ def group_a_character_init():
for character in characters:
character.skill_selection_strategy = ExpectedDamageMaximizationSkillSelectionStrategy()
character.target_selection_strategy = MaximumHpTargetSelectionStrategy()
character.reply_strategy = FightBackReactionStrategy()
character.reaction_strategy = FightBackReactionStrategy()
return characters

def group_b_character_init():
characters = [Character.of_random(f"B_{i}", skills=[FightingBrawl]) for i in range(3)]
for character in characters:
character.target_selection_strategy = RandomTargetSelectionStrategy()
character.reply_strategy = FightBackReactionStrategy()
character.reaction_strategy = FightBackReactionStrategy()
return characters

simulator = CombatSimulator(group_a_character_init, group_b_character_init) # be careful to pass function not object
Expand Down
4 changes: 2 additions & 2 deletions readme.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ def group_a_character_init():
for character in characters:
character.skill_selection_strategy = ExpectedDamageMaximizationSkillSelectionStrategy()
character.target_selection_strategy = MaximumHpTargetSelectionStrategy()
character.reply_strategy = FightBackReactionStrategy()
character.reaction_strategy = FightBackReactionStrategy()
return characters

def group_b_character_init():
characters = [Character.of_random(f"B_{i}", skills=[FightingBrawl]) for i in range(3)]
for character in characters:
character.target_selection_strategy = RandomTargetSelectionStrategy()
character.reply_strategy = FightBackReactionStrategy()
character.reaction_strategy = FightBackReactionStrategy()
return characters

simulator = CombatSimulator(group_a_character_init, group_b_character_init) # be careful to pass function not object
Expand Down
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ def group_a_character_init():
for character in characters:
character.skill_selection_strategy = ExpectedDamageMaximizationSkillSelectionStrategy()
character.target_selection_strategy = MaximumHpTargetSelectionStrategy()
character.reply_strategy = FightBackReactionStrategy()
character.reaction_strategy = FightBackReactionStrategy()
return characters

def group_b_character_init():
characters = [Character.of_random(f"B_{i}", skills=[FightingBrawl]) for i in range(3)]
for character in characters:
character.target_selection_strategy = RandomTargetSelectionStrategy()
character.reply_strategy = FightBackReactionStrategy()
character.reaction_strategy = FightBackReactionStrategy()
return characters

simulator = CombatSimulator(group_a_character_init, group_b_character_init) # be careful to pass function not object
Expand Down
2 changes: 1 addition & 1 deletion tests/test_character.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_character_creation(self):
self.assertIsInstance(
self.character.skill_selection_strategy, RandomSkillSelectionStrategy
)
self.assertIsInstance(self.character.reply_strategy, NothingReactionStrategy)
self.assertIsInstance(self.character.reaction_strategy, NothingReactionStrategy)

def test_character_of_method(self):
attribute_params = {
Expand Down
Loading

0 comments on commit 73aafa1

Please sign in to comment.