Merge pull request #675 from coding-horror/py-boxing-refactoring

Boxing (Python): Split configuration from logic
This commit is contained in:
Jeff Atwood
2022-03-24 15:33:31 -05:00
committed by GitHub
4 changed files with 289 additions and 141 deletions

View File

@@ -1,7 +1,80 @@
#!/usr/bin/env python3
import json
import random
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Literal, NamedTuple, Tuple
class PunchProfile(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 # this hit guarantees 2 damage on opponent
weakness: int # you're always hit when your opponent uses this punch
is_computer: bool
# for each of the 4 punch types, we have a probability of hitting
punch_profiles: Dict[Literal[1, 2, 3, 4], PunchProfile]
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
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 read_punch_profiles(filepath: Path) -> Dict[Literal[1, 2, 3, 4], PunchProfile]:
with open(filepath) as f:
punch_profile_dict = json.load(f)
result = {}
for key, value in punch_profile_dict.items():
result[int(key)] = PunchProfile(**value)
return result # type: ignore
def play() -> None:
@@ -10,15 +83,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 +91,92 @@ 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,
punch_profiles=read_punch_profiles(
Path(__file__).parent / "player-profile.json"
),
)
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,
punch_profiles=read_punch_profiles(
Path(__file__).parent / "opponent-profile.json"
),
)
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
punch_profile = active.punch_profiles[punch]
if punch == active.best:
passive.damage += 2
print(punch_profile.pre_msg.format(active=active, passive=passive), end=" ")
if passive.weakness == punch or punch_profile.is_hit():
print(punch_profile.hit_msg.format(active=active, passive=passive))
if punch_profile.knockout_possible and passive.damage > KNOCKOUT_THRESHOLD:
passive.knockedout = True
break
passive.damage += punch_profile.hit_damage
else:
print(punch_profile.blocked_msg.format(active=active, passive=passive))
active.damage += punch_profile.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()

View File

@@ -0,0 +1,40 @@
{
"1": {
"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": {
"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": {
"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": {
"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!)"
}
}

View File

@@ -0,0 +1,38 @@
{
"1": {
"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": {
"choices": 2,
"threshold": 1,
"hit_damage": 7,
"block_damage": 0,
"pre_msg": "{active.name} GIVES THE HOOK...",
"hit_msg": "CONNECTS...",
"blocked_msg": "BUT IT'S BLOCKED!!!!!!!!!!!!!"
},
"3": {
"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": {
"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!)"
}
}

View File

@@ -0,0 +1,61 @@
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")