diff --git a/01_Acey_Ducey/python/test_acey_ducey.py b/01_Acey_Ducey/python/test_acey_ducey.py index 40457cbf..c385c52c 100644 --- a/01_Acey_Ducey/python/test_acey_ducey.py +++ b/01_Acey_Ducey/python/test_acey_ducey.py @@ -1,12 +1,14 @@ import io from unittest import mock from typing import TypeVar +from _pytest.capture import CaptureFixture +from _pytest.monkeypatch import MonkeyPatch from acey_ducey import play_game @mock.patch("random.shuffle") -def test_play_game_lose(mock_random_shuffle, monkeypatch, capsys) -> None: +def test_play_game_lose(mock_random_shuffle, monkeypatch: MonkeyPatch, capsys: CaptureFixture) -> None: monkeypatch.setattr("sys.stdin", io.StringIO("100\n100")) T = TypeVar("T") diff --git a/03_Animal/python/animal.py b/03_Animal/python/animal.py index a3cb509a..10e9041f 100644 --- a/03_Animal/python/animal.py +++ b/03_Animal/python/animal.py @@ -1,46 +1,44 @@ -######################################################## -# -# Animal -# -# From: Basic computer Games(1978) -# -# Unlike other computer games in which the computer -# picks a number or letter and you must guess what it is, -# in this game you think of an animal and the computer asks -# you questions and tries to guess the name of your animal. -# If the computer guesses incorrectly, it will ask you for a -# question that differentiates the animal it guessed -# from the one you were thinking of. In this way the -# computer "learns" new animals. Questions to differentiate -# new animals should be input without a question mark. -# This version of the game does not have a SAVE feature. -# If your sistem allows, you may modify the program to -# save array A$, then reload the array when you want -# to play the game again. This way you can save what the -# computer learns over a series of games. -# At any time if you reply 'LIST' to the question "ARE YOU -# THINKING OF AN ANIMAL", the computer will tell you all the -# animals it knows so far. -# The program starts originally by knowing only FISH and BIRD. -# As you build up a file of animals you should use broad, -# general questions first and then narrow down to more specific -# ones with later animals. For example, If an elephant was to be -# your first animal, the computer would ask for a question to distinguish -# an elephant from a bird. Naturally there are hundreds of possibilities, -# however, if you plan to build a large file of animals a good question -# would be "IS IT A MAMAL". -# This program can be easily modified to deal with categories of -# things other than animals by simply modifying the initial data -# in Line 530 and the dialogue references to animal in Lines 10, -# 40, 50, 130, 230, 240 and 600. In an educational environment, this -# would be a valuable program to teach the distinguishing chacteristics -# of many classes of objects -- rock formations, geography, marine life, -# cell structures, etc. -# Originally developed by Arthur Luehrmann at Dartmouth College, -# Animal was subsequently shortened and modified by Nathan Teichholtz at -# DEC and Steve North at Creative Computing -# -######################################################## +""" +Animal + +From: Basic computer Games(1978) + + Unlike other computer games in which the computer + picks a number or letter and you must guess what it is, + in this game you think of an animal and the computer asks + you questions and tries to guess the name of your animal. + If the computer guesses incorrectly, it will ask you for a + question that differentiates the animal it guessed + from the one you were thinking of. In this way the + computer "learns" new animals. Questions to differentiate + new animals should be input without a question mark. + This version of the game does not have a SAVE feature. + If your sistem allows, you may modify the program to + save array A$, then reload the array when you want + to play the game again. This way you can save what the + computer learns over a series of games. + At any time if you reply 'LIST' to the question "ARE YOU + THINKING OF AN ANIMAL", the computer will tell you all the + animals it knows so far. + The program starts originally by knowing only FISH and BIRD. + As you build up a file of animals you should use broad, + general questions first and then narrow down to more specific + ones with later animals. For example, If an elephant was to be + your first animal, the computer would ask for a question to distinguish + an elephant from a bird. Naturally there are hundreds of possibilities, + however, if you plan to build a large file of animals a good question + would be "IS IT A MAMAL". + This program can be easily modified to deal with categories of + things other than animals by simply modifying the initial data + in Line 530 and the dialogue references to animal in Lines 10, + 40, 50, 130, 230, 240 and 600. In an educational environment, this + would be a valuable program to teach the distinguishing chacteristics + of many classes of objects -- rock formations, geography, marine life, + cell structures, etc. + Originally developed by Arthur Luehrmann at Dartmouth College, + Animal was subsequently shortened and modified by Nathan Teichholtz at + DEC and Steve North at Creative Computing +""" from typing import Optional @@ -80,7 +78,7 @@ class Node: def list_known_animals(root_node: Optional[Node]) -> None: - # Traversing the tree by recursion until we reach the leafs + """Traversing the tree by recursion until we reach the leafs.""" if root_node is None: return diff --git a/04_Awari/python/test_awari.py b/04_Awari/python/test_awari.py index 87864da8..7ff61b95 100644 --- a/04_Awari/python/test_awari.py +++ b/04_Awari/python/test_awari.py @@ -1,5 +1,15 @@ -from awari import print_with_tab +import io +from _pytest.monkeypatch import MonkeyPatch +import pytest + +from awari import print_with_tab, main -def test_print_with_tab(): +def test_print_with_tab() -> None: print_with_tab(3, "Hello") + + +def test_main(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr("sys.stdin", io.StringIO("1\n2\n3\n4\n5\n6")) + with pytest.raises(EOFError): + main() diff --git a/05_Bagels/python/bagels.py b/05_Bagels/python/bagels.py index 13014389..77e8b473 100644 --- a/05_Bagels/python/bagels.py +++ b/05_Bagels/python/bagels.py @@ -1,35 +1,33 @@ -###################################################################### -# -# Bagels -# -# From: BASIC Computer Games (1978) -# Edited by David H. Ahl -# -# "In this game, the computer picks a 3-digit secret number using -# the digits 0 to 9 and you attempt to guess what it is. You are -# allowed up to twenty guesses. No digit is repeated. After -# each guess the computer will give you clues about your guess -# as follows: -# -# PICO One digit is correct, but in the wrong place -# FERMI One digit is in the correct place -# BAGELS No digit is correct -# -# "You will learn to draw inferences from the clues and, with -# practice, you'll learn to improve your score. There are several -# good strategies for playing Bagels. After you have found a good -# strategy, see if you can improve it. Or try a different strategy -# altogether and see if it is any better. While the program allows -# up to twenty guesses, if you use a good strategy it should not -# take more than eight guesses to get any number. -# -# "The original authors of this program are D. Resek and P. Rowe of -# the Lawrence Hall of Science, Berkeley, California." -# -# -# Python port by Jeff Jetton, 2019 -# -###################################################################### +""" +Bagels + +From: BASIC Computer Games (1978) + Edited by David H. Ahl + +"In this game, the computer picks a 3-digit secret number using + the digits 0 to 9 and you attempt to guess what it is. You are + allowed up to twenty guesses. No digit is repeated. After + each guess the computer will give you clues about your guess + as follows: + + PICO One digit is correct, but in the wrong place + FERMI One digit is in the correct place + BAGELS No digit is correct + +"You will learn to draw inferences from the clues and, with + practice, you'll learn to improve your score. There are several + good strategies for playing Bagels. After you have found a good + strategy, see if you can improve it. Or try a different strategy + altogether and see if it is any better. While the program allows + up to twenty guesses, if you use a good strategy it should not + take more than eight guesses to get any number. + +"The original authors of this program are D. Resek and P. Rowe of + the Lawrence Hall of Science, Berkeley, California." + + +Python port by Jeff Jetton, 2019 +""" import random diff --git a/05_Bagels/python/test_bagels.py b/05_Bagels/python/test_bagels.py index fc68236b..06071545 100644 --- a/05_Bagels/python/test_bagels.py +++ b/05_Bagels/python/test_bagels.py @@ -1,5 +1,35 @@ -from bagels import build_result_string +import io +from _pytest.capture import CaptureFixture +from _pytest.monkeypatch import MonkeyPatch + +from bagels import build_result_string, main, pick_number def test_build_result_string() -> None: build_result_string(["a", "b", "c"], "abc") + + +def test_pick_number() -> None: + picked = pick_number() + assert len(picked) == 3 + for el in picked: + assert el in "0123456789" + + +def test_main(monkeypatch: MonkeyPatch, capsys: CaptureFixture) -> None: + # Succeed + round_1 = "Y\n4444\nabc\n444\n456\n145\n321\n123" + + # Fail after 20 guesses + round_2 = ( + "666\n132\n321\n312\n132\n213\n678\n678\n678\n678\n678\n" + "678\n678\n678\n678\n678\n678\n678\n678\n678\n678\nNo" + ) + monkeypatch.setattr("sys.stdin", io.StringIO(f"{round_1}\nYES\n{round_2}")) + monkeypatch.setattr("bagels.pick_number", lambda: ["1", "2", "3"]) + main() + captured = capsys.readouterr() + assert "Would you like the rules" in captured.out + assert "I have a number in mind" in captured.out + assert "My number was" in captured.out + assert "Hope you had fun." in captured.out diff --git a/06_Banner/python/banner.py b/06_Banner/python/banner.py index 868d98f0..6e563e46 100644 --- a/06_Banner/python/banner.py +++ b/06_Banner/python/banner.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 -# BANNER -# -# Converted from BASIC to Python by Trevor Hobson +""" +BANNER + +Converted from BASIC to Python by Trevor Hobson +""" letters = { " ": [0, 0, 0, 0, 0, 0, 0], @@ -56,8 +58,8 @@ def print_banner() -> None: while True: try: - x = int(input("Horizontal ")) - if x < 1: + horizontal = int(input("Horizontal ")) + if horizontal < 1: raise ValueError("Horizontal must be greater than zero") break @@ -65,8 +67,8 @@ def print_banner() -> None: print("Please enter a number greater than zero") while True: try: - y = int(input("Vertical ")) - if y < 1: + vertical = int(input("Vertical ")) + if vertical < 1: raise ValueError("Vertical must be greater than zero") break @@ -75,18 +77,20 @@ def print_banner() -> None: g1 = 0 if input("Centered ").lower().startswith("y"): g1 = 1 - mStr = input("Character (type 'ALL' if you want character being printed) ").upper() - aStr = input("Statement ") + character = input( + "Character (type 'ALL' if you want character being printed) " + ).upper() + statement = input("Statement ") input("Set page ") # This means to prepare printer, just press Enter - for lStr in aStr: - s = letters[lStr].copy() - xStr = mStr - if mStr == "ALL": - xStr = lStr + for statement_char in statement: + s = letters[statement_char].copy() + xStr = character + if character == "ALL": + xStr = statement_char if xStr == " ": - print("\n" * (7 * x)) + print("\n" * (7 * horizontal)) else: for u in range(0, 7): for k in range(8, -1, -1): @@ -98,16 +102,16 @@ def print_banner() -> None: if s[u] == 1: f[u] = 8 - k break - for _t1 in range(1, x + 1): - line_str = " " * int((63 - 4.5 * y) * g1 / len(xStr) + 1) + for _t1 in range(1, horizontal + 1): + line_str = " " * int((63 - 4.5 * vertical) * g1 / len(xStr) + 1) for b in range(0, f[u] + 1): if j[b] == 0: - for _ in range(1, y + 1): + for _ in range(1, vertical + 1): line_str = line_str + " " * len(xStr) else: - line_str = line_str + xStr * y + line_str = line_str + xStr * vertical print(line_str) - print("\n" * (2 * x - 1)) + print("\n" * (2 * horizontal - 1)) # print("\n" * 75) # Feed some more paper from the printer diff --git a/06_Banner/python/test_banner.py b/06_Banner/python/test_banner.py index 0f297395..cd5d45ff 100644 --- a/06_Banner/python/test_banner.py +++ b/06_Banner/python/test_banner.py @@ -1,9 +1,10 @@ import io - +from _pytest.monkeypatch import MonkeyPatch +from _pytest.capture import CaptureFixture from banner import print_banner -def test_print_banner(monkeypatch) -> None: +def test_print_banner(monkeypatch: MonkeyPatch) -> None: horizontal = "1" vertical = "1" centered = "1" @@ -17,3 +18,92 @@ def test_print_banner(monkeypatch) -> None: ), ) print_banner() + + +def test_print_banner_horizontal_0( + monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + horizontal = "1" + vertical = "1" + centered = "1" + char = "*" + statement = "O" # only capital letters + set_page = "2" + monkeypatch.setattr( + "sys.stdin", + io.StringIO( + f"0\n{horizontal}\n{vertical}\n{centered}\n{char}\n{statement}\n{set_page}" + ), + ) + print_banner() + captured = capsys.readouterr() + assert "Please enter a number greater than zero" in captured.out + + +def test_print_banner_vertical_0( + monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + horizontal = "1" + vertical = "1" + centered = "1" + char = "*" + statement = "O" # only capital letters + set_page = "2" + monkeypatch.setattr( + "sys.stdin", + io.StringIO( + f"{horizontal}\n0\n{vertical}\n{centered}\n{char}\n{statement}\n{set_page}" + ), + ) + print_banner() + captured = capsys.readouterr() + assert "Please enter a number greater than zero" in captured.out + + +def test_print_banner_centered( + monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + horizontal = "1" + vertical = "1" + centered = "Y" + char = "*" + statement = "O" # only capital letters + set_page = "2" + monkeypatch.setattr( + "sys.stdin", + io.StringIO( + f"{horizontal}\n{vertical}\n{centered}\n{char}\n{statement}\n{set_page}" + ), + ) + print_banner() + captured = capsys.readouterr() + expected = ( + "Horizontal Vertical Centered Character " + "(type 'ALL' if you want character being printed) Statement Set page " + " *****\n" + " * *\n" + " * *\n" + " * *\n" + " * *\n" + " * *\n" + " *****\n\n\n" + ) + assert captured.out.split("\n") == expected.split("\n") + + +def test_print_banner_all_statement( + monkeypatch: MonkeyPatch, capsys: CaptureFixture +) -> None: + horizontal = "1" + vertical = "1" + centered = "1" + char = "UNIT TESTING" + statement = "ALL" # only capital letters + set_page = "2" + monkeypatch.setattr( + "sys.stdin", + io.StringIO( + f"{horizontal}\n{vertical}\n{centered}\n{char}\n{statement}\n{set_page}" + ), + ) + print_banner() diff --git a/07_Basketball/python/basketball.py b/07_Basketball/python/basketball.py index a29f4f09..762b0d1b 100644 --- a/07_Basketball/python/basketball.py +++ b/07_Basketball/python/basketball.py @@ -1,48 +1,53 @@ -import random -from typing import Optional +""" +The basketball class is a computer game that allows you to play as +Dartmouth College's captain and playmaker +The game uses set probabilites to simulate outcomes of each posession +You are able to choose your shot types as well as defensive formations +""" -# The basketball class is a computer game that allows you to play as -# Dartmouth College's captain and playmaker -# The game uses set probabilites to simulate outcomes of each posession -# You are able to choose your shot types as well as defensive formations +import random +from typing import Optional, List, Literal + + +def explain_keyboard_inputs(): + print("\t\t\t Basketball") + print("\t Creative Computing Morristown, New Jersey\n\n\n") + print("This is Dartmouth College basketball. ") + print("Υou will be Dartmouth captain and playmaker.") + print("Call shots as follows:") + print( + "1. Long (30ft.) Jump Shot; " + "2. Short (15 ft.) Jump Shot; " + "3. Lay up; 4. Set Shot" + ) + print("Both teams will use the same defense. Call Defense as follows:") + print("6. Press; 6.5 Man-to-Man; 7. Zone; 7.5 None.") + print("To change defense, just type 0 as your next shot.") + print("Your starting defense will be? ", end="") class Basketball: def __init__(self) -> None: self.time = 0 self.score = [0, 0] # first value is opponents score, second is home - self.defense_choices = [6, 6.5, 7, 7.5] + self.defense_choices: List[float] = [6, 6.5, 7, 7.5] self.shot: Optional[int] = None - self.shot_choices = [0, 1, 2, 3, 4] + self.shot_choices: List[Literal[0, 1, 2, 3, 4]] = [0, 1, 2, 3, 4] self.z1: Optional[float] = None - # Explains the keyboard inputs - print("\t\t\t Basketball") - print("\t Creative Computing Morristown, New Jersey\n\n\n") - print("This is Dartmouth College basketball. ") - print("Υou will be Dartmouth captain and playmaker.") - print("Call shots as follows:") - print( - "1. Long (30ft.) Jump Shot; " - "2. Short (15 ft.) Jump Shot; " - "3. Lay up; 4. Set Shot" - ) - print("Both teams will use the same defense. Call Defense as follows:") - print("6. Press; 6.5 Man-to-Man; 7. Zone; 7.5 None.") - print("To change defense, just type 0 as your next shot.") - print("Your starting defense will be? ", end="") + explain_keyboard_inputs() - self.defense = get_defense(self.defense_choices) + self.defense = get_defense_choice(self.defense_choices) - # takes input for opponent's name - print("\nChoose your opponent? ", end="") - - self.opponent = input() + self.opponent = get_opponents_name() self.start_of_period() - # adds points to the score - # team can take 0 or 1, for opponent or Dartmouth, respectively - def add_points(self, team, points) -> None: + def add_points(self, team: Literal[0, 1], points: Literal[0, 1, 2]) -> None: + """ + Add points to the score. + + Team can take 0 or 1, for opponent or Dartmouth, respectively + """ self.score[team] += points self.print_score() @@ -50,8 +55,8 @@ class Basketball: print("Ball passed back to you. ", end="") self.dartmouth_ball() - # change defense, called when the user enters 0 for their shot def change_defense(self) -> None: + """change defense, called when the user enters 0 for their shot""" defense = None while defense not in self.defense_choices: @@ -64,8 +69,8 @@ class Basketball: self.defense = defense self.dartmouth_ball() - # simulates two foul shots for a player and adds the points - def foul_shots(self, team) -> None: + def foul_shots(self, team: Literal[0, 1]) -> None: + """Simulate two foul shots for a player and adds the points.""" print("Shooter fouled. Two shots.") if random.random() > 0.49: if random.random() > 0.75: @@ -79,18 +84,18 @@ class Basketball: self.print_score() - # called when t = 50, starts a new period def halftime(self) -> None: + """called when t = 50, starts a new period""" print("\n ***** End of first half *****\n") self.print_score() self.start_of_period() - # prints the current score def print_score(self) -> None: - print("Score: " + str(self.score[1]) + " to " + str(self.score[0]) + "\n") + """Print the current score""" + print(f"Score: {self.score[1]} to {self.score[0]}\n") - # simulates a center jump for posession at the beginning of a period def start_of_period(self) -> None: + """Simulate a center jump for posession at the beginning of a period""" print("Center jump") if random.random() > 0.6: print("Dartmouth controls the tap.\n") @@ -99,12 +104,12 @@ class Basketball: print(self.opponent + " controls the tap.\n") self.opponent_ball() - # called when t = 92 def two_minute_warning(self) -> None: + """called when t = 92""" print(" *** Two minutes left in the game ***") - # called when the user enters 1 or 2 for their shot def dartmouth_jump_shot(self) -> None: + """called when the user enters 1 or 2 for their shot""" self.time += 1 if self.time == 50: self.halftime() @@ -156,9 +161,12 @@ class Basketball: self.add_points(1, 2) self.opponent_ball() - # called when the user enters 0, 3, or 4 - # lay up, set shot, or defense change def dartmouth_non_jump_shot(self) -> None: + """ + Lay up, set shot, or defense change + + called when the user enters 0, 3, or 4 + """ self.time += 1 if self.time == 50: self.halftime() @@ -202,22 +210,9 @@ class Basketball: self.add_points(1, 2) self.opponent_ball() - # plays out a Dartmouth posession, starting with your choice of shot def dartmouth_ball(self) -> None: - print("Your shot? ", end="") - shot = None - try: - shot = int(input()) - except ValueError: - shot = None - - while shot not in self.shot_choices: - print("Incorrect answer. Retype it. Your shot? ", end="") - try: - shot = int(input()) - except Exception: - continue - assert isinstance(shot, int) + """plays out a Dartmouth posession, starting with your choice of shot""" + shot = get_dartmouth_ball_choice(self.shot_choices) self.shot = shot if self.time < 100 or random.random() < 0.5: @@ -251,8 +246,8 @@ class Basketball: self.time = 93 self.start_of_period() - # simulates the opponents jumpshot def opponent_jumpshot(self) -> None: + """Simulate the opponents jumpshot""" print("Jump Shot.") if 8 / self.defense * random.random() > 0.35: if 8 / self.defense * random.random() > 0.75: @@ -292,8 +287,8 @@ class Basketball: self.add_points(0, 2) self.dartmouth_ball() - # simulates opponents lay up or set shot def opponent_non_jumpshot(self) -> None: + """Simulate opponents lay up or set shot.""" if self.z1 > 3: # type: ignore print("Set shot.") else: @@ -329,9 +324,12 @@ class Basketball: self.add_points(0, 2) self.dartmouth_ball() - # simulates an opponents possesion - # #randomly picks jump shot or lay up / set shot. def opponent_ball(self) -> None: + """ + Simulate an opponents possesion + + Randomly picks jump shot or lay up / set shot. + """ self.time += 1 if self.time == 50: self.halftime() @@ -342,8 +340,8 @@ class Basketball: self.opponent_jumpshot() -def get_defense(defense_choices) -> float: - # takes input for a defense +def get_defense_choice(defense_choices: List[float]) -> float: + """Takes input for a defense""" try: defense = float(input()) except ValueError: @@ -360,5 +358,29 @@ def get_defense(defense_choices) -> float: return defense +def get_dartmouth_ball_choice(shot_choices: List[Literal[0, 1, 2, 3, 4]]) -> int: + print("Your shot? ", end="") + shot = None + try: + shot = int(input()) + except ValueError: + shot = None + + while shot not in shot_choices: + print("Incorrect answer. Retype it. Your shot? ", end="") + try: + shot = int(input()) + except Exception: + continue + assert isinstance(shot, int) + return shot + + +def get_opponents_name() -> str: + """Take input for opponent's name""" + print("\nChoose your opponent? ", end="") + return input() + + if __name__ == "__main__": Basketball() diff --git a/07_Basketball/python/test_basketball.py b/07_Basketball/python/test_basketball.py new file mode 100644 index 00000000..8644dc86 --- /dev/null +++ b/07_Basketball/python/test_basketball.py @@ -0,0 +1,16 @@ +import io + +import pytest +from _pytest.monkeypatch import MonkeyPatch +from _pytest.capture import CaptureFixture + +from basketball import Basketball + + +def test_basketball(monkeypatch: MonkeyPatch, capsys: CaptureFixture[str]) -> None: + monkeypatch.setattr( + "sys.stdin", + io.StringIO("\n1\n6\n1\n2\n1\n2\n1\n2\n1\n2\n3\n4\n5\n4"), + ) + with pytest.raises(EOFError): + Basketball() diff --git a/08_Batnum/python/batnum.py b/08_Batnum/python/batnum.py index 91480d23..60c7aad9 100644 --- a/08_Batnum/python/batnum.py +++ b/08_Batnum/python/batnum.py @@ -1,18 +1,44 @@ -from enum import Enum -from typing import Tuple, Union +from enum import IntEnum +from typing import Tuple, Any -class WinOptions(Enum): +class WinOptions(IntEnum): Undefined = 0 TakeLast = 1 AvoidLast = 2 + @classmethod + def _missing_(cls, value: Any) -> "WinOptions": + try: + int_value = int(value) + except Exception: + return WinOptions.Undefined + if int_value == 1: + return WinOptions.TakeLast + elif int_value == 2: + return WinOptions.AvoidLast + else: + return WinOptions.Undefined -class StartOptions(Enum): + +class StartOptions(IntEnum): Undefined = 0 ComputerFirst = 1 PlayerFirst = 2 + @classmethod + def _missing_(cls, value: Any) -> "StartOptions": + try: + int_value = int(value) + except Exception: + return StartOptions.Undefined + if int_value == 1: + return StartOptions.ComputerFirst + elif int_value == 2: + return StartOptions.PlayerFirst + else: + return StartOptions.Undefined + def print_intro() -> None: """Prints out the introduction and rules for the game.""" @@ -34,7 +60,7 @@ def print_intro() -> None: return -def get_params() -> Tuple[int, int, int, int, int]: +def get_params() -> Tuple[int, int, int, StartOptions, WinOptions]: """This requests the necessary parameters to play the game. Returns a set with the five game parameters: @@ -46,46 +72,69 @@ def get_params() -> Tuple[int, int, int, int, int]: winOption - 1 if the goal is to take the last object or 2 if the goal is to not take the last object """ + pile_size = get_pile_size() + if pile_size < 0: + return (-1, 0, 0, StartOptions.Undefined, WinOptions.Undefined) + win_option = get_win_option() + min_select, max_select = get_min_max() + start_option = get_start_option() + return (pile_size, min_select, max_select, start_option, win_option) + + +def get_pile_size() -> int: + # A negative number will stop the game. pile_size = 0 - win_option: Union[WinOptions, int] = WinOptions.Undefined + while pile_size == 0: + try: + pile_size = int(input("ENTER PILE SIZE ")) + except ValueError: + pile_size = 0 + return pile_size + + +def get_win_option() -> WinOptions: + win_option: WinOptions = WinOptions.Undefined + while win_option == WinOptions.Undefined: + win_option = WinOptions(input("ENTER WIN OPTION - 1 TO TAKE LAST, 2 TO AVOID LAST: ")) # type: ignore + return win_option + + +def get_min_max() -> Tuple[int, int]: min_select = 0 max_select = 0 - start_option: Union[StartOptions, int] = StartOptions.Undefined - - while pile_size < 1: - pile_size = int(input("ENTER PILE SIZE ")) - while win_option == WinOptions.Undefined: - win_option = int(input("ENTER WIN OPTION - 1 TO TAKE LAST, 2 TO AVOID LAST: ")) - assert isinstance(win_option, int) while min_select < 1 or max_select < 1 or min_select > max_select: (min_select, max_select) = ( int(x) for x in input("ENTER MIN AND MAX ").split(" ") ) + return min_select, max_select + + +def get_start_option() -> StartOptions: + start_option: StartOptions = StartOptions.Undefined while start_option == StartOptions.Undefined: - start_option = int(input("ENTER START OPTION - 1 COMPUTER FIRST, 2 YOU FIRST ")) - assert isinstance(start_option, int) - return (pile_size, min_select, max_select, start_option, win_option) + start_option = StartOptions(input("ENTER START OPTION - 1 COMPUTER FIRST, 2 YOU FIRST ")) # type: ignore + return start_option def player_move( - pile_size, min_select, max_select, start_option, win_option + pile_size: int, min_select: int, max_select: int, win_option: WinOptions ) -> Tuple[bool, int]: """This handles the player's turn - asking the player how many objects to take and doing some basic validation around that input. Then it checks for any win conditions. Returns a boolean indicating whether the game is over and the new pileSize.""" - playerDone = False - while not playerDone: - playerMove = int(input("YOUR MOVE ")) - if playerMove == 0: + player_done = False + while not player_done: + player_move = int(input("YOUR MOVE ")) + if player_move == 0: print("I TOLD YOU NOT TO USE ZERO! COMPUTER WINS BY FORFEIT.") return (True, pile_size) - if playerMove > max_select or playerMove < min_select: + if player_move > max_select or player_move < min_select: print("ILLEGAL MOVE, REENTER IT") continue - pile_size = pile_size - playerMove - playerDone = True + pile_size = pile_size - player_move + player_done = True if pile_size <= 0: if win_option == WinOptions.AvoidLast: print("TOUGH LUCK, YOU LOSE.") @@ -95,7 +144,9 @@ def player_move( return (False, pile_size) -def computer_pick(pile_size, min_select, max_select, start_option, win_option) -> int: +def computer_pick( + pile_size: int, min_select: int, max_select: int, win_option: WinOptions +) -> int: """This handles the logic to determine how many objects the computer will select on its turn. """ @@ -110,7 +161,7 @@ def computer_pick(pile_size, min_select, max_select, start_option, win_option) - def computer_move( - pile_size, min_select, max_select, start_option, win_option + pile_size: int, min_select: int, max_select: int, win_option: WinOptions ) -> Tuple[bool, int]: """This handles the computer's turn - first checking for the various win/lose conditions and then calculating how many objects @@ -132,13 +183,19 @@ def computer_move( return (True, pile_size) # Otherwise, we determine how many the computer selects - currSel = computer_pick(pile_size, min_select, max_select, start_option, win_option) - pile_size = pile_size - currSel - print(f"COMPUTER TAKES {currSel} AND LEAVES {pile_size}") + curr_sel = computer_pick(pile_size, min_select, max_select, win_option) + pile_size = pile_size - curr_sel + print(f"COMPUTER TAKES {curr_sel} AND LEAVES {pile_size}") return (False, pile_size) -def play_game(pile_size, min_select, max_select, start_option, win_option) -> None: +def play_game( + pile_size: int, + min_select: int, + max_select: int, + start_option: StartOptions, + win_option: WinOptions, +) -> None: """This is the main game loop - repeating each turn until one of the win/lose conditions is met. """ @@ -150,30 +207,29 @@ def play_game(pile_size, min_select, max_select, start_option, win_option) -> No while not game_over: if players_turn: (game_over, pile_size) = player_move( - pile_size, min_select, max_select, start_option, win_option + pile_size, min_select, max_select, win_option ) players_turn = False if game_over: return if not players_turn: (game_over, pile_size) = computer_move( - pile_size, min_select, max_select, start_option, win_option + pile_size, min_select, max_select, win_option ) players_turn = True -if __name__ == "__main__": - - pileSize = 0 - minSelect = 0 - maxSelect = 0 - # 1 = to take last, 2 = to avoid last - winOption = 0 - # 1 = computer first, 2 = user first - startOption = 0 - +def main() -> None: while True: print_intro() - (pileSize, minSelect, maxSelect, startOption, winOption) = get_params() + (pile_size, min_select, max_select, start_option, win_option) = get_params() + + if pile_size < 0: + return + # Just keep playing the game until the user kills it with ctrl-C - play_game(pileSize, minSelect, maxSelect, startOption, winOption) + play_game(pile_size, min_select, max_select, start_option, win_option) + + +if __name__ == "__main__": + main() diff --git a/08_Batnum/python/test_batnum.py b/08_Batnum/python/test_batnum.py new file mode 100644 index 00000000..50929388 --- /dev/null +++ b/08_Batnum/python/test_batnum.py @@ -0,0 +1,21 @@ +import io +from _pytest.capture import CaptureFixture +from _pytest.monkeypatch import MonkeyPatch + +from batnum import main + + +def test_main_win(monkeypatch: MonkeyPatch, capsys: CaptureFixture[str]) -> None: + pile_size = 1 + monkeypatch.setattr("sys.stdin", io.StringIO(f"{pile_size}\n1\n1 2\n2\n1\n-1\n")) + main() + captured = capsys.readouterr() + assert "CONGRATULATIONS, YOU WIN" in captured.out + + +def test_main_lose(monkeypatch: MonkeyPatch, capsys: CaptureFixture[str]) -> None: + pile_size = 3 + monkeypatch.setattr("sys.stdin", io.StringIO(f"{pile_size}\n2\n1 2\n2\n1\n1\n-1\n")) + main() + captured = capsys.readouterr() + assert "TOUGH LUCK, YOU LOSE" in captured.out