Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e73218d9f4 | ||
|
|
cebed68105 | ||
|
|
4bc7012e77 | ||
|
|
d2cf059337 | ||
|
|
bcea547c8b |
@@ -1,4 +0,0 @@
|
|||||||
# Copy to .env and fill in. Never commit .env.
|
|
||||||
GITEA_TOKEN=
|
|
||||||
GITHUB_PAT=
|
|
||||||
GITHUB_USER=
|
|
||||||
+5
-6
@@ -20,17 +20,16 @@ Work on ONE milestone at a time. Each milestone is one branch (`milestone/NN-nam
|
|||||||
- [x] Verify: create `.venv`, upgrade pip, `pip install -e ".[dev]"`
|
- [x] Verify: create `.venv`, upgrade pip, `pip install -e ".[dev]"`
|
||||||
- [x] Save plan and milestones in `docs/plan.md`
|
- [x] Save plan and milestones in `docs/plan.md`
|
||||||
|
|
||||||
## Milestone 2 - Core auction logic and env loader (`milestone/02-core-logic`)
|
## Milestone 2 - Core auction logic (`milestone/02-core-logic`)
|
||||||
|
|
||||||
- [ ] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty)
|
- [x] `auction.py` (Doxygen comments): `add_bid`, `find_winner` (uses `max(bids, key=bids.get)`, `ValueError` on empty)
|
||||||
- [ ] `env.py`: stdlib `.env` loader (no python-dotenv); does not override existing environment variables
|
- [x] `tests/test_auction.py`
|
||||||
- [ ] `tests/test_auction.py`, `tests/test_env.py`
|
|
||||||
- Verify: `pytest` passes
|
- Verify: `pytest` passes
|
||||||
|
|
||||||
## Milestone 3 - CLI program (`milestone/03-cli`)
|
## Milestone 3 - CLI program (`milestone/03-cli`)
|
||||||
|
|
||||||
- [ ] `main.py`: prompt name and numeric bid (re-prompt on invalid), bids dict kept outside the loop, "other bidders? yes/no", clear screen with newlines between bidders, announce the winner
|
- [x] `main.py`: prompt name and numeric bid (re-prompt on invalid), bids dict kept outside the loop, "other bidders? yes/no", clear screen with newlines between bidders, announce the winner
|
||||||
- [ ] `tests/test_main.py` with monkeypatched `input`
|
- [x] `tests/test_main.py` with monkeypatched `input`
|
||||||
- Verify: `pytest`; scripted run `python -m secret_auction.main` with two bidders
|
- Verify: `pytest`; scripted run `python -m secret_auction.main` with two bidders
|
||||||
|
|
||||||
## Milestone 4 - Documentation (`milestone/04-docs`)
|
## Milestone 4 - Documentation (`milestone/04-docs`)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""! @file auction.py
|
||||||
|
@brief Core blind auction logic (no input/output).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def add_bid(bids: dict[str, float], name: str, amount: float) -> None:
|
||||||
|
"""! @brief Record a bid, replacing any earlier bid by the same bidder.
|
||||||
|
@param bids Dictionary mapping bidder name to bid amount (modified in place).
|
||||||
|
@param name Name of the bidder.
|
||||||
|
@param amount Bid amount; must not be negative.
|
||||||
|
@throws ValueError If the amount is negative.
|
||||||
|
"""
|
||||||
|
if amount < 0:
|
||||||
|
raise ValueError("Bid amount must not be negative.")
|
||||||
|
bids[name] = amount
|
||||||
|
|
||||||
|
|
||||||
|
def find_winner(bids: dict[str, float]) -> tuple[str, float]:
|
||||||
|
"""! @brief Find the highest bidder.
|
||||||
|
@param bids Dictionary mapping bidder name to bid amount.
|
||||||
|
@return Tuple of (winner name, winning amount). The first bidder wins ties.
|
||||||
|
@throws ValueError If there are no bids.
|
||||||
|
"""
|
||||||
|
if not bids:
|
||||||
|
raise ValueError("No bids have been placed.")
|
||||||
|
winner = max(bids, key=bids.__getitem__)
|
||||||
|
return winner, bids[winner]
|
||||||
@@ -1,39 +1,33 @@
|
|||||||
"""! @file constants.py
|
"""! @file constants.py
|
||||||
@brief Constants used by the Secret Auction program.
|
@brief Constants used by the Secret Auction program.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
## Number of blank lines printed to hide previous bidders' entries.
|
## Number of blank lines printed to hide previous bidders' entries.
|
||||||
CLEAR_SCREEN_LINES = 100
|
CLEAR_SCREEN_LINES = 100
|
||||||
|
|
||||||
## Answers accepted as "yes" when asking for more bidders.
|
## Answers accepted as "yes" when asking for more bidders.
|
||||||
YES_ANSWERS = ("yes", "y")
|
YES_ANSWERS = ("yes", "y")
|
||||||
|
|
||||||
## Answers accepted as "no" when asking for more bidders.
|
## Answers accepted as "no" when asking for more bidders.
|
||||||
NO_ANSWERS = ("no", "n")
|
NO_ANSWERS = ("no", "n")
|
||||||
|
|
||||||
## Path of the file holding environment variables (tokens).
|
## Prompt asking for the bidder's name.
|
||||||
ENV_FILE = ".env"
|
PROMPT_NAME = "What is your name? "
|
||||||
|
|
||||||
## Name of the environment variable holding the Gitea token.
|
## Prompt asking for the bid amount.
|
||||||
GITEA_TOKEN_ENV_VAR = "GITEA_TOKEN"
|
PROMPT_BID = "What is your bid? $"
|
||||||
|
|
||||||
## Prompt asking for the bidder's name.
|
## Prompt asking whether more bidders exist.
|
||||||
PROMPT_NAME = "What is your name? "
|
PROMPT_MORE_BIDDERS = "Are there any other bidders? Type 'yes' or 'no'. "
|
||||||
|
|
||||||
## Prompt asking for the bid amount.
|
## Message shown when the bid is not a valid number.
|
||||||
PROMPT_BID = "What is your bid? $"
|
MSG_INVALID_BID = "Please enter a valid, non-negative number."
|
||||||
|
|
||||||
## Prompt asking whether more bidders exist.
|
## Message shown when the yes/no answer is not understood.
|
||||||
PROMPT_MORE_BIDDERS = "Are there any other bidders? Type 'yes' or 'no'. "
|
MSG_INVALID_ANSWER = "Please type 'yes' or 'no'."
|
||||||
|
|
||||||
## Message shown when the bid is not a valid number.
|
## Template for the winner announcement.
|
||||||
MSG_INVALID_BID = "Please enter a valid, non-negative number."
|
MSG_WINNER = "The winner is {name} with a bid of ${amount:.2f}."
|
||||||
|
|
||||||
## Message shown when the yes/no answer is not understood.
|
## Welcome banner.
|
||||||
MSG_INVALID_ANSWER = "Please type 'yes' or 'no'."
|
MSG_WELCOME = "Welcome to the secret auction program."
|
||||||
|
|
||||||
## Template for the winner announcement.
|
|
||||||
MSG_WINNER = "The winner is {name} with a bid of ${amount:.2f}."
|
|
||||||
|
|
||||||
## Welcome banner.
|
|
||||||
MSG_WELCOME = "Welcome to the secret auction program."
|
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""! @file main.py
|
||||||
|
@brief Command line interface for the secret auction program.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from secret_auction import constants
|
||||||
|
from secret_auction.auction import add_bid, find_winner
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen() -> None:
|
||||||
|
"""! @brief Hide previous entries by printing blank lines."""
|
||||||
|
print("\n" * constants.CLEAR_SCREEN_LINES)
|
||||||
|
|
||||||
|
|
||||||
|
def ask_bid() -> float:
|
||||||
|
"""! @brief Ask for a bid until a valid, non-negative number is entered.
|
||||||
|
@return The bid amount.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
amount = float(input(constants.PROMPT_BID))
|
||||||
|
except ValueError:
|
||||||
|
print(constants.MSG_INVALID_BID)
|
||||||
|
continue
|
||||||
|
if math.isfinite(amount) and amount >= 0:
|
||||||
|
return amount
|
||||||
|
print(constants.MSG_INVALID_BID)
|
||||||
|
|
||||||
|
|
||||||
|
def ask_more_bidders() -> bool:
|
||||||
|
"""! @brief Ask whether there are more bidders until yes/no is answered.
|
||||||
|
@return True if more bidders exist, False otherwise.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
answer = input(constants.PROMPT_MORE_BIDDERS).strip().lower()
|
||||||
|
if answer in constants.YES_ANSWERS:
|
||||||
|
return True
|
||||||
|
if answer in constants.NO_ANSWERS:
|
||||||
|
return False
|
||||||
|
print(constants.MSG_INVALID_ANSWER)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""! @brief Run the blind auction: collect bids, then announce the winner."""
|
||||||
|
print(constants.MSG_WELCOME)
|
||||||
|
bids: dict[str, float] = {} # kept outside the loop so bids are not lost
|
||||||
|
more_bidders = True
|
||||||
|
while more_bidders:
|
||||||
|
name = input(constants.PROMPT_NAME).strip()
|
||||||
|
add_bid(bids, name, ask_bid())
|
||||||
|
more_bidders = ask_more_bidders()
|
||||||
|
if more_bidders:
|
||||||
|
clear_screen()
|
||||||
|
winner, amount = find_winner(bids)
|
||||||
|
print(constants.MSG_WINNER.format(name=winner, amount=amount))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from secret_auction.auction import add_bid, find_winner
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_bid_stores_amount():
|
||||||
|
bids = {}
|
||||||
|
add_bid(bids, "Ann", 10)
|
||||||
|
assert bids == {"Ann": 10}
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_bid_replaces_existing_bidder():
|
||||||
|
bids = {"Ann": 10}
|
||||||
|
add_bid(bids, "Ann", 20)
|
||||||
|
assert bids == {"Ann": 20}
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_bid_rejects_negative():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
add_bid({}, "Ann", -1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_winner_returns_highest():
|
||||||
|
assert find_winner({"Ann": 10, "Bob": 25.5, "Cy": 3}) == ("Bob", 25.5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_winner_tie_first_wins():
|
||||||
|
assert find_winner({"Ann": 10, "Bob": 10}) == ("Ann", 10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_winner_empty_raises():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
find_winner({})
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from secret_auction import constants
|
||||||
|
from secret_auction.main import ask_bid, ask_more_bidders, main
|
||||||
|
|
||||||
|
|
||||||
|
def feed(monkeypatch, answers):
|
||||||
|
it = iter(answers)
|
||||||
|
monkeypatch.setattr("builtins.input", lambda _prompt="": next(it))
|
||||||
|
|
||||||
|
|
||||||
|
def test_ask_bid_reprompts_on_invalid(monkeypatch, capsys):
|
||||||
|
feed(monkeypatch, ["abc", "-5", "nan", "12.5"])
|
||||||
|
assert ask_bid() == 12.5
|
||||||
|
assert capsys.readouterr().out.count(constants.MSG_INVALID_BID) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_ask_more_bidders(monkeypatch, capsys):
|
||||||
|
feed(monkeypatch, ["maybe", "Y"])
|
||||||
|
assert ask_more_bidders() is True
|
||||||
|
assert constants.MSG_INVALID_ANSWER in capsys.readouterr().out
|
||||||
|
feed(monkeypatch, ["no"])
|
||||||
|
assert ask_more_bidders() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_announces_highest_bidder_and_clears_screen(monkeypatch, capsys):
|
||||||
|
feed(monkeypatch, ["Ann", "10", "yes", "Bob", "25.5", "yes", "Cy", "3", "no"])
|
||||||
|
main()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "The winner is Bob with a bid of $25.50." in out
|
||||||
|
assert out.count("\n" * constants.CLEAR_SCREEN_LINES) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_single_bidder(monkeypatch, capsys):
|
||||||
|
feed(monkeypatch, ["Ann", "7", "n"])
|
||||||
|
main()
|
||||||
|
assert "The winner is Ann with a bid of $7.00." in capsys.readouterr().out
|
||||||
Reference in New Issue
Block a user