Skip to content

Commit

Permalink
Refactor strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
Liesegang committed Feb 27, 2024
1 parent 6ab8367 commit 1833cb3
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 105 deletions.
60 changes: 4 additions & 56 deletions coc7e_combat_simulator/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,12 @@
from typing import List
import random

from .strategies.reaction import NothingReactionStrategy
from .strategies.skill_selection import RandomSkillSelectionStrategy
from .strategies.target_selection import RandomTargetSelectionStrategy
from .attribute import Attribute
from .dice_parser import DiceParser
from .skill import Skill

class TargetSelectionStrategy:
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
raise NotImplementedError()

class RandomTargetSelectionStrategy(TargetSelectionStrategy):
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
target_candidates = [target for target in characters if target.side != character.side and target.hp > 0]
return random.choice(target_candidates)

class MinimumHpTargetSelectionStrategy(TargetSelectionStrategy):
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
target_candidates = [target for target in characters if target.side != character.side and target.hp > 0]
return min(target_candidates, key=lambda target: target.hp)

class MaximumHpTargetSelectionStrategy(TargetSelectionStrategy):
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
target_candidates = [target for target in characters if target.side != character.side and target.hp > 0]
return max(target_candidates, key=lambda target: target.hp)

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

class RandomSkillSelectionStrategy(SkillSelectionStrategy):
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))

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

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

class NothingReplyStrategy(ReplyStrategy):
def reply(self, character: "Character", attacker: "Character") -> ReplyType:
return ReplyType.NOTHING

class DodgeReplyStrategy(ReplyStrategy):
def reply(self, character: "Character", attacker: "Character") -> ReplyType:
return ReplyType.DODGE

class FightBackReplyStrategy(ReplyStrategy):
def reply(self, character: "Character", attacker: "Character") -> ReplyType:
return ReplyType.FIGHT_BACK

class Character:
def __init__(self, name: str, attributes: Attribute, skills: List[Skill]):
if not isinstance(attributes, Attribute):
Expand All @@ -73,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 = NothingReplyStrategy()
self.reply_strategy = NothingReactionStrategy()

@classmethod
def of(cls, name: str, attribute_params: dict, skills: List[Skill] = []) -> "Character":
Expand Down
19 changes: 10 additions & 9 deletions coc7e_combat_simulator/combat_simulator.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from enum import IntEnum
import random
import logging
from typing import List, Tuple, Callable
from typing import List, Callable
import tqdm

from .character import Character, ReplyType
from .skill import Skill
from .strategies.reaction import ReactionType
from .character import Character
from .skill import Skill, FightingBrawl
from .dice_parser import DiceParser

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -57,13 +58,13 @@ def someone_left_alive(self, characters: List[Character], side: str) -> bool:

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 == ReplyType.NOTHING:
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 == ReplyType.DODGE:
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)
Expand All @@ -78,9 +79,9 @@ def check_damage(self, character: Character, skill: Skill, target: Character) ->
else:
logger.info(f"{character.name} in side {character.side} failed to attack {target.name}.")
return
elif reply == ReplyType.FIGHT_BACK:
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 Skill("Fighting (Brawl)", 25, "1D3", physical_attack=True)
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)
Expand Down Expand Up @@ -142,7 +143,7 @@ def print_character_status(round: int, characters: List[Character]) -> None:
for character in characters:
logger.info(f"{character.name}: {character.hp}HP")

def simulate_multiple_combats(self, number_of_simulations: int) -> Tuple[float, float, float]:
def simulate_multiple_combats(self, number_of_simulations: int) -> dict:
results = {"A": 0, "B": 0, "Draw": 0}
for _ in tqdm.tqdm(range(number_of_simulations)):
result = self.combat_simulation_single()
Expand All @@ -152,4 +153,4 @@ def simulate_multiple_combats(self, number_of_simulations: int) -> Tuple[float,
rate_b = results["B"] / number_of_simulations
rate_draw = results["Draw"] / number_of_simulations

return {"A": rate_a, "B": rate_b, "Draw": rate_draw}
return {"A": rate_a, "B": rate_b, "Draw": rate_draw}
22 changes: 22 additions & 0 deletions coc7e_combat_simulator/strategies/reaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from enum import Enum

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

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

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

class DodgeReactionStrategy(ReactionStrategy):
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
18 changes: 18 additions & 0 deletions coc7e_combat_simulator/strategies/skill_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import random

from ..skill import Skill
from ..dice_parser import DiceParser

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

class RandomSkillSelectionStrategy(SkillSelectionStrategy):
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))
21 changes: 21 additions & 0 deletions coc7e_combat_simulator/strategies/target_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import random
from typing import List

class TargetSelectionStrategy:
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
raise NotImplementedError()

class RandomTargetSelectionStrategy(TargetSelectionStrategy):
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
target_candidates = [target for target in characters if target.side != character.side and target.hp > 0]
return random.choice(target_candidates)

class MinimumHpTargetSelectionStrategy(TargetSelectionStrategy):
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
target_candidates = [target for target in characters if target.side != character.side and target.hp > 0]
return min(target_candidates, key=lambda target: target.hp)

class MaximumHpTargetSelectionStrategy(TargetSelectionStrategy):
def select_target(self, character: "Character", characters: List["Character"]) -> "Character":
target_candidates = [target for target in characters if target.side != character.side and target.hp > 0]
return max(target_candidates, key=lambda target: target.hp)
11 changes: 7 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from coc7e_combat_simulator.combat_simulator import CombatSimulator
from coc7e_combat_simulator.character import Character, FightBackReplyStrategy, MinimumHpTargetSelectionStrategy, RandomTargetSelectionStrategy, ExpectedDamageMaximizationSkillSelectionStrategy
from coc7e_combat_simulator.character import Character
from coc7e_combat_simulator.strategies.skill_selection import ExpectedDamageMaximizationSkillSelectionStrategy
from coc7e_combat_simulator.strategies.target_selection import MaximumHpTargetSelectionStrategy, RandomTargetSelectionStrategy
from coc7e_combat_simulator.strategies.reaction import FightBackReactionStrategy
from coc7e_combat_simulator.skill import FightingBrawl, FirearmHandgun

# group A has 4 members, group B has 3 members
Expand All @@ -8,15 +11,15 @@ def group_a_character_init():
characters = [Character.of_random(f"A_{i}", skills=[FightingBrawl, FirearmHandgun]) for i in range(4)]
for character in characters:
character.skill_selection_strategy = ExpectedDamageMaximizationSkillSelectionStrategy()
character.target_selection_strategy = MinimumHpTargetSelectionStrategy()
character.reply_strategy = FightBackReplyStrategy()
character.target_selection_strategy = MaximumHpTargetSelectionStrategy()
character.reply_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 = FightBackReplyStrategy()
character.reply_strategy = FightBackReactionStrategy()
return characters

simulator = CombatSimulator(group_a_character_init, group_b_character_init)
Expand Down
12 changes: 8 additions & 4 deletions readme.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@

```python
from coc7e_combat_simulator.combat_simulator import CombatSimulator
from coc7e_combat_simulator.character import Character, FightBackReplyStrategy, MinimumHpTargetSelectionStrategy, RandomTargetSelectionStrategy, ExpectedDamageMaximizationSkillSelectionStrategy
from coc7e_combat_simulator.character import Character
from coc7e_combat_simulator.strategies.skill_selection import ExpectedDamageMaximizationSkillSelectionStrategy
from coc7e_combat_simulator.strategies.target_selection import MaximumHpTargetSelectionStrategy, RandomTargetSelectionStrategy
from coc7e_combat_simulator.strategies.reaction import FightBackReactionStrategy
from coc7e_combat_simulator.skill import FightingBrawl, FirearmHandgun

# group A has 4 members, group B has 3 members
Expand All @@ -42,20 +45,21 @@ def group_a_character_init():
characters = [Character.of_random(f"A_{i}", skills=[FightingBrawl, FirearmHandgun]) for i in range(4)]
for character in characters:
character.skill_selection_strategy = ExpectedDamageMaximizationSkillSelectionStrategy()
character.target_selection_strategy = MinimumHpTargetSelectionStrategy()
character.reply_strategy = FightBackReplyStrategy()
character.target_selection_strategy = MaximumHpTargetSelectionStrategy()
character.reply_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 = FightBackReplyStrategy()
character.reply_strategy = FightBackReactionStrategy()
return characters

simulator = CombatSimulator(group_a_character_init, group_b_character_init)
results = simulator.simulate_multiple_combats(10000)
print(results)

```

## カスタマイズ
Expand Down
12 changes: 8 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ Example:

```python
from coc7e_combat_simulator.combat_simulator import CombatSimulator
from coc7e_combat_simulator.character import Character, FightBackReplyStrategy, MinimumHpTargetSelectionStrategy, RandomTargetSelectionStrategy, ExpectedDamageMaximizationSkillSelectionStrategy
from coc7e_combat_simulator.character import Character
from coc7e_combat_simulator.strategies.skill_selection import ExpectedDamageMaximizationSkillSelectionStrategy
from coc7e_combat_simulator.strategies.target_selection import MaximumHpTargetSelectionStrategy, RandomTargetSelectionStrategy
from coc7e_combat_simulator.strategies.reaction import FightBackReactionStrategy
from coc7e_combat_simulator.skill import FightingBrawl, FirearmHandgun

# group A has 4 members, group B has 3 members
Expand All @@ -41,20 +44,21 @@ def group_a_character_init():
characters = [Character.of_random(f"A_{i}", skills=[FightingBrawl, FirearmHandgun]) for i in range(4)]
for character in characters:
character.skill_selection_strategy = ExpectedDamageMaximizationSkillSelectionStrategy()
character.target_selection_strategy = MinimumHpTargetSelectionStrategy()
character.reply_strategy = FightBackReplyStrategy()
character.target_selection_strategy = MaximumHpTargetSelectionStrategy()
character.reply_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 = FightBackReplyStrategy()
character.reply_strategy = FightBackReactionStrategy()
return characters

simulator = CombatSimulator(group_a_character_init, group_b_character_init)
results = simulator.simulate_multiple_combats(10000)
print(results)

```

## Customization
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ply==3.11
tqdm==4.64.0
29 changes: 29 additions & 0 deletions tests/strategies/test_skill_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import unittest
from coc7e_combat_simulator.strategies.skill_selection import ExpectedDamageMaximizationSkillSelectionStrategy
from coc7e_combat_simulator.character import Character
from coc7e_combat_simulator.skill import Skill

class TestSkillSelection(unittest.TestCase):
def setUp(self):
self.strategy = ExpectedDamageMaximizationSkillSelectionStrategy()

def test_select_skill_damage(self):
skill1 = Skill("Skill1", 50, "2d6+3", False)
skill2 = Skill("Skill2", 50, "1d8+2", True)
skill3 = Skill("Skill3", 50, "3d4+1", False)
character = Character.of_random("Test", skills=[skill1, skill2, skill3])

selected_skill = self.strategy.select_skill(character)
self.assertEqual(selected_skill, skill1)

def test_select_skill_chance(self):
skill1 = Skill("Skill1", 10, "3D6", False)
skill2 = Skill("Skill2", 20, "3D6", True)
skill3 = Skill("Skill3", 30, "3D6", False)
character = Character.of_random("Test", skills=[skill1, skill2, skill3])

selected_skill = self.strategy.select_skill(character)
self.assertEqual(selected_skill, skill3)

if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit 1833cb3

Please sign in to comment.