Python: Add tests and type annotations

This commit is contained in:
Martin Thoma
2022-03-22 11:55:13 +01:00
parent 97bf59b328
commit bf4ac6c3ca
11 changed files with 456 additions and 209 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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