diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000..1843c6f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "nimbuscasino" +description = "An example of a package developed with pipenv, built with build using setuptools, uploaded to PyPI using twine, and distributed via pip." +version = "0.1.6" +authors = [ + { name="Mojin Yuan", email="my2384@nyu.edu" }, +] +license = { file = "LICENSE" } +readme = "README.md" +keywords = ["python", "package", "build", "minigames"] +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "Intended Audience :: Education", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", +] + +[project.optional-dependencies] +dev = ["pytest"] + +[project.urls] +"Homepage" = "https://github.com/swe-students-fall2025/3-python-package-team_nimbus" +"Repository" = "https://github.com/swe-students-fall2025/3-python-package-team_nimbus.git" +"Bug Tracker" = "https://github.com/swe-students-fall2025/3-python-package-team_nimbus/issues" + +[project.scripts] +nimbuscasino = "nimbuscasino.__main__:main" \ No newline at end of file diff --git a/src/nimbuscasino/__init__.py b/src/nimbuscasino/__init__.py new file mode 100755 index 0000000..d9c6483 --- /dev/null +++ b/src/nimbuscasino/__init__.py @@ -0,0 +1,4 @@ +from .slots import spin_slots, SpinResult + +__all__ = ["spin_slots", "SpinResult"] + diff --git a/src/nimbuscasino/coinflip.py b/src/nimbuscasino/coinflip.py new file mode 100755 index 0000000..f1d0209 --- /dev/null +++ b/src/nimbuscasino/coinflip.py @@ -0,0 +1,44 @@ +from __future__ import annotations +import random +from typing import Optional, TypedDict, Literal + + +class CoinflipResult(TypedDict): + game: Literal["coinflip"] + guess: Literal["heads", "tails"] + flip: Literal["heads", "tails"] + win: bool + payout: int + prob_heads: float + + +def coinflip( + guess: str, + bet: int = 1, + rng: Optional[random.Random] = None, + bias: float = 0.5, +) -> CoinflipResult: + if not isinstance(guess, str): + raise ValueError("guess must be a string 'heads' or 'tails'.") + g = guess.strip().lower() + if g not in {"heads", "tails"}: + raise ValueError("guess must be 'heads' or 'tails'.") + if not isinstance(bet, int) or bet <= 0: + raise ValueError("bet must be a positive integer.") + if not (0.0 <= bias <= 1.0): + raise ValueError("bias must be between 0.0 and 1.0 inclusive.") + + r = rng if rng is not None else random.Random() + x = r.random() + flip = "heads" if x < bias else "tails" + win = (flip == g) + payout = bet if win else -bet + + return CoinflipResult( + game="coinflip", + guess=g, + flip=flip, + win=win, + payout=payout, + prob_heads=bias, + ) diff --git a/src/nimbuscasino/play_coinflip.py b/src/nimbuscasino/play_coinflip.py new file mode 100755 index 0000000..54bc80e --- /dev/null +++ b/src/nimbuscasino/play_coinflip.py @@ -0,0 +1,30 @@ +from nimbuscasino.coinflip import coinflip +import random + +print("šŸŽ° Welcome to NimbusCasino: Coin Flip Edition šŸŽ°") +rng = random.Random() + +credits = 100 +while True: + print(f"\nšŸ’° Current credits: {credits}") + guess = input("Guess 'heads' or 'tails' (or type 'quit' to exit): ").strip().lower() + if guess == "quit": + print(f"šŸ‘‹ Thanks for playing! Final credits: {credits}") + break + if guess not in ["heads", "tails"]: + print("āŒ Invalid input, please type 'heads' or 'tails'.") + continue + + try: + bet = int(input("Enter your bet amount: ")) + except ValueError: + print("āŒ Invalid bet! Please enter a number.") + continue + + res = coinflip(guess, bet=bet, rng=rng) + if res["win"]: + print(f"āœ… It was {res['flip']}! You WIN 🄳 +{res['payout']} credits") + else: + print(f"šŸ’€ It was {res['flip']}! You LOSE 😭 {res['payout']} credits") + + credits += res["payout"] diff --git a/src/nimbuscasino/rps.py b/src/nimbuscasino/rps.py new file mode 100755 index 0000000..dea6c34 --- /dev/null +++ b/src/nimbuscasino/rps.py @@ -0,0 +1,47 @@ +import random + +def rps(player, bet=1): + """ + Plays a Rock-Paper-Scissors round against the computer. + + Parameters: + ----------- + player : str + Player's choice: "rock", "paper", or "scissors". + bet : int or float, optional + Amount of money wagered for the round. Default = 1. + + Returns: + -------- + tuple (result, payout) + result: "win", "lose", or "tie" + payout: numeric value of the win/loss based on bet + """ + + # Normalize input + player = player.lower() + valid_choices = ["rock", "paper", "scissors"] + + if player not in valid_choices: + raise ValueError(f"Invalid choice '{player}'. Choose from {valid_choices}.") + + # Computer randomly chooses + computer = random.choice(valid_choices) + + # Determine outcome + if player == computer: + result = "tie" + payout = 0 + elif ( + (player == "rock" and computer == "scissors") or + (player == "scissors" and computer == "paper") or + (player == "paper" and computer == "rock") + ): + result = "win" + payout = bet + else: + result = "lose" + payout = -bet + + print(f"You chose {player}, computer chose {computer} → {result.upper()}") + return result, payout diff --git a/src/nimbuscasino/slots.py b/src/nimbuscasino/slots.py new file mode 100644 index 0000000..6336ef2 --- /dev/null +++ b/src/nimbuscasino/slots.py @@ -0,0 +1,84 @@ +# src/nimbuscasino/slots.py +from __future__ import annotations + +from dataclasses import dataclass +import random +from typing import Optional, Dict, List, TypedDict + +class SlotsLineInfo(TypedDict): + symbol: str + count: int + payout: int + +@dataclass(frozen=True) +class SpinResult: + grid: List[List[str]] + lines: Dict[str, Dict[str, int]] + total_payout: int + +DEFAULT_SYMBOLS = ["šŸ’", "šŸ‹", "šŸ””", "⭐", "7"] +DEFAULT_WEIGHTS = [30, 25, 20, 15, 10] +DEFAULT_PAYTABLE: Dict[str, Dict[int, int]] = { + "šŸ’": {3: 5}, + "šŸ‹": {3: 6}, + "šŸ””": {3: 12}, + "⭐": {3: 20}, + "7": {2: 5, 3: 50}, +} + +LINES = { + "top": [(0, 0), (0, 1), (0, 2)], + "middle": [(1, 0), (1, 1), (1, 2)], + "bottom": [(2, 0), (2, 1), (2, 2)], + "diag_down": [(0, 0), (1, 1), (2, 2)], + "diag_up": [(2, 0), (1, 1), (0, 2)], +} + +def spin_slots( + bet: int = 1, + rng: Optional[random.Random] = None, + symbols: Optional[List[str]] = None, + weights: Optional[List[int]] = None, + paytable: Optional[Dict[str, Dict[int, int]]] = None, + rows: int = 3, + cols: int = 3, +) -> SpinResult: + if not isinstance(bet, int) or bet <= 0: + raise ValueError("bet must be positive") + if rows <= 0 or cols <= 0: + raise ValueError("rows and cols must be positive") + + r = rng or random.Random() + syms = symbols or DEFAULT_SYMBOLS + wts = weights or DEFAULT_WEIGHTS + pt = paytable or DEFAULT_PAYTABLE + if len(syms) != len(wts): + raise ValueError("symbols and weights must be same length") + + grid: List[List[str]] = [[None] * cols for _ in range(rows)] # type: ignore + for c in range(cols): + for rr in range(rows): + grid[rr][c] = r.choices(syms, wts, k=1)[0] + + lines_out: Dict[str, Dict[str, int]] = {} + total = 0 + for name, coords in LINES.items(): + if any(rr >= rows or cc >= cols for rr, cc in coords): + continue + seq = [grid[rr][cc] for rr, cc in coords] + + if seq[0] == seq[1] == seq[2]: + sym = seq[0] + mult = pt.get(sym, {}).get(3, 0) + payout = bet * mult + if payout: + lines_out[name] = {"symbol": sym, "count": 3, "payout": payout} + total += payout + elif seq.count("7") == 2: + mult = pt.get("7", {}).get(2, 0) + payout = bet * mult + if payout: + lines_out[name] = {"symbol": "7", "count": 2, "payout": payout} + total += payout + + return SpinResult(grid=grid, lines=lines_out, total_payout=total) diff --git a/tests/rpstest.py b/tests/rpstest.py new file mode 100755 index 0000000..88ef8e8 --- /dev/null +++ b/tests/rpstest.py @@ -0,0 +1,44 @@ +import pytest +from nimbuscasino import rps + + +class Tests: + # + # Fixtures - these are functions that can do any optional setup or teardown before or after a test function is run. + # + + @pytest.fixture + def example_fixture(self): + """ + An example of a pytest fixture - a function that can be used for setup and teardown before and after test functions are run. + """ + + + + # place any setup you want to do before any test function that uses this fixture is run + + yield # at th=e yield point, the test function will run and do its business + + # place with any teardown you want to do after any test function that uses this fixture has completed + + # + # Test functions + # + + +def test_sanity_check(self, example_fixture): + """ + Test debugging... making sure that we can run a simple test that always passes. + Note the use of the example_fixture in the parameter list - any setup and teardown in that fixture will be run before and after this test function executes + From the main project directory, run the `python3 -m pytest` command to run all tests. + """ + expected = True # the value we expect to be present + actual = True # the value we see in reality + winorlose, bet = rps.rps('rock') + if winorlose == 'win': + assert bet == 1 + elif winorlose == 'lose': + assert bet == -1 + elif winorlose == 'tie': + assert bet == 0 + \ No newline at end of file diff --git a/tests/test_coinflip.py b/tests/test_coinflip.py new file mode 100755 index 0000000..9324506 --- /dev/null +++ b/tests/test_coinflip.py @@ -0,0 +1,40 @@ +import pytest +# tests/test_coinflip.py +from nimbuscasino.coinflip import coinflip + + + +class FakeRNG: + def __init__(self, values): + self.values = list(values) + self.i = 0 + + def random(self): + if self.i < len(self.values): + v = self.values[self.i] + self.i += 1 + return v + return self.values[-1] + + +def test_coinflip_win_heads_with_fair_bias(): + rng = FakeRNG([0.2]) + res = coinflip("heads", bet=3, rng=rng, bias=0.5) + assert res["flip"] == "heads" + assert res["win"] is True + assert res["payout"] == 3 + assert res["game"] == "coinflip" + +def test_coinflip_lose_heads_with_fair_bias(): + rng = FakeRNG([0.9]) + res = coinflip("heads", bet=2, rng=rng, bias=0.5) + assert res["flip"] == "tails" + assert res["win"] is False + assert res["payout"] == -2 + +def test_bias_edge_cases_always_heads_at_1(): + rng = FakeRNG([0.9999]) + res = coinflip("heads", bet=5, rng=rng, bias=1.0) + assert res["flip"] == "heads" + assert res["win"] is True + assert res["payout"] == 5 diff --git a/tests/test_slots.py b/tests/test_slots.py new file mode 100644 index 0000000..920418f --- /dev/null +++ b/tests/test_slots.py @@ -0,0 +1,17 @@ +import random +from nimbuscasino import spin_slots, SpinResult + +def test_spin_slots_shapes_and_types(): + rng = random.Random(123) + res = spin_slots(bet=2, rng=rng) + assert isinstance(res, SpinResult) + assert len(res.grid) == 3 + assert all(len(row) == 3 for row in res.grid) + +def test_spin_slots_deterministic_with_seed(): + rng1 = random.Random(42) + rng2 = random.Random(42) + a = spin_slots(bet=1, rng=rng1) + b = spin_slots(bet=1, rng=rng2) + assert a.grid == b.grid + assert a.total_payout == b.total_payout