diff --git a/15_Boxing/python/boxing.py b/15_Boxing/python/boxing.py index 2f38ddd0..15754326 100755 --- a/15_Boxing/python/boxing.py +++ b/15_Boxing/python/boxing.py @@ -1,7 +1,70 @@ #!/usr/bin/env python3 import random +from dataclasses import dataclass +from typing import Tuple, Dict, NamedTuple, Literal + +class HitStats(NamedTuple): + choices: int + threshold: int + hit_damage: int + block_damage: int + + pre_msg: str + hit_msg: str + blocked_msg: str + + knockout_possible: bool = False + + def is_hit(self) -> bool: + return random.randint(1, self.choices) <= self.threshold + + +@dataclass +class Player: + name: str + best: int # TODO: This is never used! + is_computer: bool + weakness: int + + # for each of the 4 punch types, we have a probability of hitting + hit_stats: Dict[Literal[1, 2, 3, 4], HitStats] + + damage: int = 0 + score: int = 0 + knockedout: bool = False + + def get_punch_choice(self) -> Literal[1, 2, 3, 4]: + if self.is_computer: + return random.randint(1, 4) # type: ignore + else: + punch = -1 + while punch not in [1, 2, 3, 4]: + print(f"{self.name}'S PUNCH", end="? ") + punch = int(input()) + return punch # type: ignore + + +KNOCKOUT_THRESHOLD = 35 + +# Texts QUESTION_PROMPT = "? " +KNOCKED_COLD = "{loser} IS KNOCKED COLD AND {winner} IS THE WINNER AND CHAMP" + + +def get_vulnerability() -> int: + print("WHAT IS HIS VULNERABILITY", end=QUESTION_PROMPT) + vulnerability = int(input()) + return vulnerability + + +def get_opponent_stats() -> Tuple[int, int]: + opponent_best = 0 + opponent_weakness = 0 + while opponent_best == opponent_weakness: + opponent_best = random.randint(1, 4) + opponent_weakness = random.randint(1, 4) + return opponent_best, opponent_weakness def play() -> None: @@ -10,15 +73,6 @@ def play() -> None: print("\n\n") print("BOXING OLYMPIC STYLE (3 ROUNDS -- 2 OUT OF 3 WINS)") - opponent_score = 0 - player_score = 0 - - opponent_damage = 0 - player_damage = 0 - - opponent_knockedout = False - player_knockedout = False - print("WHAT IS YOUR OPPONENT'S NAME", end=QUESTION_PROMPT) opponent_name = input() print("WHAT IS YOUR MAN'S NAME", end=QUESTION_PROMPT) @@ -27,147 +81,164 @@ def play() -> None: print("DIFFERENT PUNCHES ARE 1 FULL SWING 2 HOOK 3 UPPERCUT 4 JAB") print("WHAT IS YOUR MAN'S BEST", end=QUESTION_PROMPT) player_best = int(input()) # noqa: TODO - this likely is a bug! - - print("WHAT IS HIS VULNERABILITY", end=QUESTION_PROMPT) - player_weakness = int(input()) - - opponent_best = 0 - opponent_weakness = 0 - while opponent_best == opponent_weakness: - opponent_best = random.randint(1, 4) - opponent_weakness = random.randint(1, 4) - - print( - "{}'S ADVANTAGE is {} AND VULNERABILITY IS SECRET.".format( - opponent_name, opponent_weakness - ) + player_weakness = get_vulnerability() + player = Player( + name=player_name, + best=player_best, + weakness=player_weakness, + is_computer=False, + hit_stats={ + 1: HitStats( # FULL SWING + choices=30, + threshold=10, + hit_damage=15, + block_damage=0, + pre_msg="{active.name} SWINGS AND", + hit_msg="HE CONNECTS!", + blocked_msg="HE MISSES", + ), + 2: HitStats( # HOOK + choices=2, + threshold=1, + hit_damage=7, + block_damage=0, + pre_msg="{active.name} GIVES THE HOOK...", + hit_msg="", + blocked_msg="", + ), + 3: HitStats( # UPPERCUT + choices=100, + threshold=50, + hit_damage=4, + block_damage=0, + pre_msg="{player_name} TRIES AN UPPERCUT", + hit_msg="AND HE CONNECTS!", + blocked_msg="AND IT'S BLOCKED (LUCKY BLOCK!)", + ), + 4: HitStats( # JAB + choices=8, + threshold=3, + hit_damage=3, + block_damage=0, + pre_msg="{active.name} JABS AT {passive.name}'S HEAD", + hit_msg="AND HE CONNECTS!", + blocked_msg="AND IT'S BLOCKED (LUCKY BLOCK!)", + ), + }, ) - for round in (1, 2, 3): - print(f"ROUND {round} BEGINS...\n") - if opponent_score >= 2 or player_score >= 2: - break + opponent_best, opponent_weakness = get_opponent_stats() + opponent = Player( + name=opponent_name, + best=opponent_best, + weakness=opponent_weakness, + is_computer=True, + hit_stats={ + 1: HitStats( # FULL SWING + choices=60, + threshold=29, + hit_damage=15, + block_damage=0, + knockout_possible=True, + pre_msg="{active.name} TAKES A FULL SWING", + hit_msg="AND POW!!!! HE HITS HIM RIGHT IN THE FACE!", + blocked_msg="BUT IT'S BLOCKED!", + ), + 2: HitStats( # HOOK + choices=1, + threshold=1, + hit_damage=12, + block_damage=0, + knockout_possible=True, + pre_msg="{active.name} GETS {passive.name} IN THE JAW (OUCH!)....AND AGAIN", + hit_msg="CONNECTS...", + blocked_msg="BUT IT'S BLOCKED!!!!!!!!!!!!!", + ), + 3: HitStats( # UPPERCUT + choices=200, + threshold=125, + hit_damage=8, + block_damage=5, + pre_msg="{passive.name} IS ATTACKED BY AN UPPERCUT (OH,OH)", + hit_msg="AND {active.name} CONNECTS...", + blocked_msg="{passive.name} BLOCKS AND HITS {active.name} WITH A HOOK", + ), + 4: HitStats( # JAB + choices=7, + threshold=3, + hit_damage=3, + block_damage=0, + pre_msg="{active.name} JABS AND", + hit_msg="BLOOD SPILLS !!!", + blocked_msg="AND IT'S BLOCKED (LUCKY BLOCK!)", + ), + }, + ) - for _action in range(7): - if random.randint(1, 10) > 5: - # opponent swings - punch = random.randint(1, 4) + print( + f"{opponent.name}'S ADVANTAGE is {opponent.weakness} AND VULNERABILITY IS SECRET." + ) - if punch == player_weakness: - player_damage += 2 + for round_number in (1, 2, 3): + play_round(round_number, player, opponent) - if punch == 1: - print(f"{opponent_name} TAKES A FULL SWING AND", end=" ") - if player_weakness == 1 or random.randint(1, 60) < 30: - print("POW!!!! HE HITS HIM RIGHT IN THE FACE!") - if player_damage > 35: - player_knockedout = True - break - player_damage += 15 - else: - print("BUT IT'S BLOCKED!") - elif punch == 2: - print( - "{} GETS {} IN THE JAW (OUCH!)".format( - opponent_name, player_name - ), - end=" ", - ) - player_damage += 7 - print("....AND AGAIN") - if player_damage > 35: - player_knockedout = True - break - player_damage += 5 - elif punch == 3: - print(f"{player_name} IS ATTACKED BY AN UPPERCUT (OH,OH)") - if player_weakness == 3 or random.randint(1, 200) > 75: - print( - "{} BLOCKS AND HITS {} WITH A HOOK".format( - player_name, opponent_name - ) - ) - opponent_damage += 5 - else: - print(f"AND {opponent_name} CONNECTS...") - player_damage += 8 - else: - print(f"{opponent_name} JABS AND", end=" ") - if player_weakness == 4 or random.randint(1, 7) > 4: - print("BLOOD SPILLS !!!") - player_damage += 3 - else: - print("AND IT'S BLOCKED (LUCKY BLOCK!)") - else: - print(f"{player_name}'S PUNCH", end="? ") - punch = int(input()) - - if punch == opponent_weakness: - opponent_damage += 2 - - if punch == 1: - print(f"{player_name} SWINGS AND", end=" ") - if opponent_weakness == 1 or random.randint(1, 30) < 10: - print("HE CONNECTS!") - if opponent_damage > 35: - opponent_knockedout = True - break - opponent_damage += 15 - else: - print("HE MISSES") - elif punch == 2: - print(f"{player_name} GIVES THE HOOK...", end=" ") - if opponent_weakness == 2 or random.randint(1, 2) == 1: - print("CONNECTS...") - opponent_damage += 7 - else: - print("BUT IT'S BLOCKED!!!!!!!!!!!!!") - elif punch == 3: - print(f"{player_name} TRIES AN UPPERCUT", end=" ") - if opponent_weakness == 3 or random.randint(1, 100) < 51: - print("AND HE CONNECTS!") - opponent_damage += 4 - else: - print("AND IT'S BLOCKED (LUCKY BLOCK!)") - else: - print( - f"{player_name} JABS AT {opponent_name}'S HEAD", - end=" ", - ) - if opponent_weakness == 4 or random.randint(1, 8) < 4: - print("AND HE CONNECTS!") - opponent_damage += 3 - else: - print("AND IT'S BLOCKED (LUCKY BLOCK!)") - - if player_knockedout or opponent_knockedout: - break - elif player_damage > opponent_damage: - print(f"{opponent_name} WINS ROUND {round}") - opponent_score += 1 - else: - print(f"{player_name} WINS ROUND {round}") - player_score += 1 - - if player_knockedout: - print( - "{} IS KNOCKED COLD AND {} IS THE WINNER AND CHAMP".format( - player_name, opponent_name - ) - ) - elif opponent_knockedout: - print( - "{} IS KNOCKED COLD AND {} IS THE WINNER AND CHAMP".format( - opponent_name, player_name - ) - ) - elif opponent_score > player_score: - print(f"{opponent_name} WINS (NICE GOING), {player_name}") + if player.knockedout: + print(KNOCKED_COLD.format(loser=player.name, winner=opponent.name)) + elif opponent.knockedout: + print(KNOCKED_COLD.format(loser=opponent.name, winner=player.name)) + elif opponent.score > player.score: + print(f"{opponent.name} WINS (NICE GOING), {player.name}") else: - print(f"{player_name} AMAZINGLY WINS") + print(f"{player.name} AMAZINGLY WINS") print("\n\nAND NOW GOODBYE FROM THE OLYMPIC ARENA.") +def is_opponents_turn() -> bool: + return random.randint(1, 10) > 5 + + +def play_round(round_number: int, player: Player, opponent: Player) -> None: + print(f"ROUND {round_number} BEGINS...\n") + if opponent.score >= 2 or player.score >= 2: + return + + for _action in range(7): + if is_opponents_turn(): + punch = opponent.get_punch_choice() + active = opponent + passive = player + else: + punch = player.get_punch_choice() + active = player + passive = opponent + + # Load the hit characteristics of the current player's punch + hit_stats = active.hit_stats[punch] + + if punch == passive.weakness: + passive.damage += 2 + + print(hit_stats.pre_msg.format(active=active, passive=passive), end=" ") + if passive.weakness == punch or hit_stats.is_hit(): + print(hit_stats.hit_msg.format(active=active, passive=passive)) + if hit_stats.knockout_possible and passive.damage > KNOCKOUT_THRESHOLD: + passive.knockedout = True + break + passive.damage += hit_stats.hit_damage + else: + print(hit_stats.blocked_msg.format(active=active, passive=passive)) + active.damage += hit_stats.block_damage + + if player.knockedout or opponent.knockedout: + return + elif player.damage > opponent.damage: + print(f"{opponent.name} WINS ROUND {round_number}") + opponent.score += 1 + else: + print(f"{player.name} WINS ROUND {round_number}") + player.score += 1 + + if __name__ == "__main__": play() diff --git a/15_Boxing/python/test_boxing.py b/15_Boxing/python/test_boxing.py new file mode 100644 index 00000000..200b4d6b --- /dev/null +++ b/15_Boxing/python/test_boxing.py @@ -0,0 +1,62 @@ +import io + +from _pytest.capture import CaptureFixture +from _pytest.monkeypatch import MonkeyPatch + +from boxing import play + + +def test_boxing_bad_opponent( + monkeypatch: MonkeyPatch, capsys: CaptureFixture[str] +) -> None: + monkeypatch.setattr("boxing.Player.get_punch_choice", lambda self: 1) + monkeypatch.setattr("boxing.get_opponent_stats", lambda: (2, 1)) + monkeypatch.setattr("boxing.is_opponents_turn", lambda: False) + + opponent = "Anna" + my_man = "Bob" + strength = "1" + weakness = "2" + + monkeypatch.setattr( + "sys.stdin", + io.StringIO(f"{opponent}\n{my_man}\n{strength}\n{weakness}\n1\n1\n1"), + ) + play() + actual = capsys.readouterr().out + expected = """BOXING +CREATIVE COMPUTING MORRISTOWN, NEW JERSEY + + + +BOXING OLYMPIC STYLE (3 ROUNDS -- 2 OUT OF 3 WINS) +WHAT IS YOUR OPPONENT'S NAME? WHAT IS YOUR MAN'S NAME? DIFFERENT PUNCHES ARE 1 FULL SWING 2 HOOK 3 UPPERCUT 4 JAB +WHAT IS YOUR MAN'S BEST? WHAT IS HIS VULNERABILITY? Anna'S ADVANTAGE is 1 AND VULNERABILITY IS SECRET. +ROUND 1 BEGINS... + +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob WINS ROUND 1 +ROUND 2 BEGINS... + +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob SWINGS AND HE CONNECTS! +Bob WINS ROUND 2 +ROUND 3 BEGINS... + +Bob AMAZINGLY WINS + + +AND NOW GOODBYE FROM THE OLYMPIC ARENA. +""" + assert actual.split("\n") == expected.split("\n")