Skip to content

Commit

Permalink
Format codes
Browse files Browse the repository at this point in the history
  • Loading branch information
Liesegang committed Feb 27, 2024
1 parent f30cb7f commit fa86b58
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 62 deletions.
6 changes: 4 additions & 2 deletions coc7e_combat_simulator/character.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from enum import Enum
from typing import List
import random

Expand All @@ -8,6 +7,7 @@
from .attribute import Attribute
from .skill import Skill


class Character:
def __init__(self, name: str, attributes: Attribute, skills: List[Skill]):
if not isinstance(attributes, Attribute):
Expand All @@ -24,7 +24,9 @@ def __init__(self, name: str, attributes: Attribute, skills: List[Skill]):
self.reply_strategy = NothingReactionStrategy()

@classmethod
def of(cls, name: str, attribute_params: dict, skills: List[Skill] = []) -> "Character":
def of(
cls, name: str, attribute_params: dict, skills: List[Skill] = []
) -> "Character":
attributes = Attribute(**attribute_params)
return cls(name, attributes, skills)

Expand Down
120 changes: 97 additions & 23 deletions coc7e_combat_simulator/combat_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
logger = logging.getLogger(__name__)
dice_parser = DiceParser()


class LevelOfSuccess(IntEnum):
CRITICAL = 0
SPECIAL = 1
Expand All @@ -20,8 +21,13 @@ class LevelOfSuccess(IntEnum):
FAILURE = 4
FUMBLE = 5


class CombatSimulator:
def __init__(self, group_a_characters_init_strategy: Callable[[], List[Character]], group_b_characters_init_strategy: Callable[[], List[Character]]):
def __init__(
self,
group_a_characters_init_strategy: Callable[[], List[Character]],
group_b_characters_init_strategy: Callable[[], List[Character]],
):
self.group_a_characters_init_strategy = group_a_characters_init_strategy
self.group_b_characters_init_strategy = group_b_characters_init_strategy

Expand All @@ -34,7 +40,9 @@ def calculate_damage(character: Character, skill: Skill) -> int:
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.")
logger.info(
f"{character.name} in side {character.side} dealt {damage_result} damage."
)
return damage_result

@staticmethod
Expand All @@ -54,47 +62,100 @@ def check_level_of_success(skill: Skill) -> LevelOfSuccess:
return LevelOfSuccess.FUMBLE

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

def check_damage(self, character: Character, skill: Skill, target: Character) -> None:
def check_damage(
self, character: Character, skill: Skill, target: 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.")
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_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:
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.")
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.")
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}.")
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 attacker_level_of_success >= fight_back_skill_level_of_success and attacker_level_of_success >= LevelOfSuccess.SUCCESS:
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 (
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.")
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:
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.")
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}.")
logger.info(
f"{character.name} in side {character.side} failed to attack {target.name}."
)
return

def combat_simulation_single(self) -> str:
Expand All @@ -110,20 +171,33 @@ class CombatEnd(Exception):

characters = characters_a + characters_b

characters.sort(key=lambda character: (character.attributes.dexterity, random.random()), reverse=True)
characters.sort(
key=lambda character: (character.attributes.dexterity, random.random()),
reverse=True,
)

round = 1
try:
while self.someone_left_alive(characters, "A") and self.someone_left_alive(characters, "B"):
while self.someone_left_alive(characters, "A") and self.someone_left_alive(
characters, "B"
):
self.print_character_status(round, characters)
for character in characters:
if character.hp > 0:
target_candidates = [target for target in characters if target.side != character.side and target.hp > 0]
target_candidates = [
target
for target in characters
if target.side != character.side and target.hp > 0
]
if not target_candidates:
raise CombatEnd()

target = character.target_selection_strategy.select_target(character, characters)
skill = character.skill_selection_strategy.select_skill(character)
target = character.target_selection_strategy.select_target(
character, characters
)
skill = character.skill_selection_strategy.select_skill(
character
)

self.check_damage(character, skill, target)
round += 1
Expand Down
22 changes: 17 additions & 5 deletions coc7e_combat_simulator/dice_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
import random

import logging

logger = logging.getLogger(__name__)


class RollDetails:
def __init__(self, roll: int, details: list[int]):
self.roll = roll
self.details = details

def __repr__(self) -> str:
return f"Roll: {self.roll}, Details: {self.details}"


class DiceParser:
class Mode(Enum):
ROLL = 1
Expand Down Expand Up @@ -91,7 +95,7 @@ def p_expression_binop(self, p):
p[0] = p[1] % p[3]
elif p[2].lower() == "d":
dice_faces = p[3]
dice_count =p[1]
dice_count = p[1]
if self.mode == self.Mode.ROLL:
rolls = [random.randint(1, dice_faces) for _ in range(dice_count)]
p[0] = sum(rolls)
Expand Down Expand Up @@ -140,7 +144,9 @@ def p_error(self, p):
else:
print("Syntax error at EOF")

def parse(self, expression: str, parameters=None) -> Tuple[float, list[RollDetails]]:
def parse(
self, expression: str, parameters=None
) -> Tuple[float, list[RollDetails]]:
logger.info(f"Parsing {expression} with parameters {parameters}")
self.current_parameters = parameters if parameters is not None else {}
self.dice_rolls = []
Expand All @@ -149,23 +155,29 @@ def parse(self, expression: str, parameters=None) -> Tuple[float, list[RollDetai
return result, self.dice_rolls

def expected(self, expression: str, parameters=None) -> float:
logger.info(f"Calculating expected value of {expression} with parameters {parameters}")
logger.info(
f"Calculating expected value of {expression} with parameters {parameters}"
)
self.current_parameters = parameters if parameters is not None else {}
self.dice_rolls = []
self.mode = self.Mode.EXPECTED
result = self.parser.parse(expression)
return result

def maximum(self, expression: str, parameters=None) -> float:
logger.info(f"Calculating maximum value of {expression} with parameters {parameters}")
logger.info(
f"Calculating maximum value of {expression} with parameters {parameters}"
)
self.current_parameters = parameters if parameters is not None else {}
self.dice_rolls = []
self.mode = self.Mode.MAXIMUM
result = self.parser.parse(expression)
return result

def minimum(self, expression: str, parameters=None) -> float:
logger.info(f"Calculating minimum value of {expression} with parameters {parameters}")
logger.info(
f"Calculating minimum value of {expression} with parameters {parameters}"
)
self.current_parameters = parameters if parameters is not None else {}
self.dice_rolls = []
self.mode = self.Mode.MINIMUM
Expand Down
5 changes: 4 additions & 1 deletion coc7e_combat_simulator/skill.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class Skill:
def __init__(self, name: str, success_rate: int, damage: str, physical_attack: bool):
def __init__(
self, name: str, success_rate: int, damage: str, physical_attack: bool
):
self.name = name
self.success_rate = success_rate
self.damage = damage
Expand All @@ -8,5 +10,6 @@ def __init__(self, name: str, success_rate: int, damage: str, physical_attack: b
def __repr__(self) -> str:
return f"{self.name}: Success Rate: {self.success_rate}%, Damage: {self.damage}"


FightingBrawl = Skill("Fighting (Brawl)", 25, "1D3", True)
FirearmHandgun = Skill("Firearm (Handgun)", 20, "1D10", False)
16 changes: 11 additions & 5 deletions coc7e_combat_simulator/strategies/reaction.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
from enum import Enum

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ..character import Character


class ReactionType(Enum):
NOTHING = 0
DODGE = 1
FIGHT_BACK = 2


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


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


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


class FightBackReactionStrategy(ReactionStrategy):
def reply(self, character: Character, attacker: Character) -> ReactionType:
return ReactionType.FIGHT_BACK
def reply(self, character: "Character", attacker: "Character") -> ReactionType:
return ReactionType.FIGHT_BACK
19 changes: 15 additions & 4 deletions coc7e_combat_simulator/strategies/skill_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,30 @@
from ..dice_parser import DiceParser

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ..character import Character


class SkillSelectionStrategy:
def select_skill(self, character: Character) -> Skill:
def select_skill(self, character: "Character") -> Skill:
raise NotImplementedError()


class RandomSkillSelectionStrategy(SkillSelectionStrategy):
def select_skill(self, character: Character) -> Skill:
def select_skill(self, character: "Character") -> Skill:
return random.choice(character.skills)


class ExpectedDamageMaximizationSkillSelectionStrategy(SkillSelectionStrategy):
dice_parser = DiceParser()

def select_skill(self, character: Character) -> Skill:
return max(character.skills, key=lambda skill: skill.success_rate / 100 * ExpectedDamageMaximizationSkillSelectionStrategy.dice_parser.expected(skill.damage))
def select_skill(self, character: "Character") -> Skill:
return max(
character.skills,
key=lambda skill: skill.success_rate
/ 100
* ExpectedDamageMaximizationSkillSelectionStrategy.dice_parser.expected(
skill.damage
),
)
Loading

0 comments on commit fa86b58

Please sign in to comment.