diff --git a/README.rst b/README.rst index 1d74ec6..f7fb0a3 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -gamble: a collection of gambling classes/tools +gamble ============================================== .. image:: https://travis-ci.org/jpetrucciani/gamble.svg?branch=master @@ -20,14 +20,14 @@ gamble: a collection of gambling classes/tools :alt: Python 3.6+ supported -**gamble** +**gamble** is a simple library that implements a collection of some common gambling-related classes Features -------- -- dice -- (coming soon) cards +- die, dice +- cards, decks Usage ----- @@ -40,8 +40,67 @@ Installation pip install gamble Basic Usage -^^^^^^^^^^^ +----------- + +Dice +^^^^ .. code-block:: python import gamble + + # create dice, defaults to 2 6-sided dice + dice = gamble.Dice() + + # roll + dice.roll() + >>> 6 + dice.rolls + >>> 1 + + # max, min + dice.max + >>> 12 + dice.min + >>> 2 + + # d-notation for dice constructor + dice = gamble.Dice('d20+8') + + # max, min + dice.max + >>> 28 + dice.min + >>> 9 + + # parts + dice.parts + >>> [, 8] + + +Cards +^^^^^ + +.. code-block:: python + + import gamble + + # create a deck, defaults to the standard 52 card deck, no jokers + # the deck will be shuffled by default, unless you pass shuffle=False + deck = gamble.Deck() + + deck.cards_left + >>> 52 + + deck.top + >>> + deck.bottom + >>> + deck.shuffle() # you can also pass times=(int) to shuffle more than once + + card = deck.draw() # you can also pass times=(int) to draw a list of cards + >>> + + # the unicode cards icons are implemented as well! + card.unicode + >>> "🂡" diff --git a/gamble/__init__.py b/gamble/__init__.py index 1d87cab..5922f07 100644 --- a/gamble/__init__.py +++ b/gamble/__init__.py @@ -1,4 +1,4 @@ """ gamble module """ -from gamble.models import Die, Dice # noqa +from gamble.models import * # noqa diff --git a/gamble/globals.py b/gamble/globals.py deleted file mode 100644 index 10ccf62..0000000 --- a/gamble/globals.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -globals for gamble -""" - -__library__ = "gamble" -__version__ = "0.0.4" diff --git a/gamble/models/__init__.py b/gamble/models/__init__.py index e610f17..ebff372 100644 --- a/gamble/models/__init__.py +++ b/gamble/models/__init__.py @@ -2,3 +2,4 @@ models submodule for gamble """ from gamble.models.dice import Die, Dice # noqa +from gamble.models.cards import Card, Deck, EuchreDeck # noqa diff --git a/gamble/models/cards.py b/gamble/models/cards.py new file mode 100644 index 0000000..d4bb841 --- /dev/null +++ b/gamble/models/cards.py @@ -0,0 +1,202 @@ +""" +standard deck of cards submodule +""" +import random +from types import SimpleNamespace +from typing import Any, List, Union + + +class Suit(SimpleNamespace): + """suit namespace class""" + + +class Value(SimpleNamespace): + """value namespace class""" + + +class Card: + """playing card model""" + + BLACK = 0 + RED = 1 + + class Suits: + """card suit enum""" + + SPADES = Suit(name="spades", char="♠", value=0, color=0, unicode=127136) + CLUBS = Suit(name="clubs", char="♣", value=1, color=0, unicode=127184) + DIAMONDS = Suit(name="diamonds", char="♦", value=2, color=1, unicode=127168) + HEARTS = Suit(name="hearts", char="♥", value=3, color=1, unicode=127152) + + @classmethod + def all(cls) -> List[Suit]: + """get all suits""" + return sorted( + [ + cls.__dict__[x] + for x in dir(cls) + if not x.startswith("_") and isinstance(cls.__dict__[x], Suit) + ], + key=lambda x: x.value, + ) + + class Values: + """card value enum""" + + ACE = Value(char="A", name="ace", value=1) + TWO = Value(char="2", name="two", value=2) + THREE = Value(char="3", name="three", value=3) + FOUR = Value(char="4", name="four", value=4) + FIVE = Value(char="5", name="five", value=5) + SIX = Value(char="6", name="six", value=6) + SEVEN = Value(char="7", name="seven", value=7) + EIGHT = Value(char="8", name="eight", value=8) + NINE = Value(char="9", name="nine", value=9) + TEN = Value(char="T", name="ten", value=10) + JACK = Value(char="J", name="jack", value=11) + QUEEN = Value(char="Q", name="queen", value=12) + KING = Value(char="K", name="king", value=13) + + @classmethod + def all(cls) -> List[Value]: + """get all suits""" + return sorted( + [ + cls.__dict__[x] + for x in dir(cls) + if not x.startswith("_") and isinstance(cls.__dict__[x], Value) + ], + key=lambda x: x.value, + ) + + def __init__(self, value: Value = Values.ACE, suit: Suit = Suits.SPADES) -> None: + """card constructor""" + self.value = value + self.suit = suit + + @property + def color(self) -> int: + """returns the color of the card""" + return self.suit.color + + @property + def full_name(self) -> str: + """returns the full name for this card""" + return "{} of {}".format(self.value.name, self.suit.name) + + @property + def is_black(self) -> bool: + """is_black property""" + return self.color == Card.BLACK + + @property + def unicode(self) -> str: + """get the fun little unicode card for this card""" + # we need to skip the 'knight' card if we're a queen or king + hack = int(self.value.value >= 12) + return chr(self.suit.unicode + self.value.value + hack) + + @property + def is_red(self) -> bool: + """is_red property""" + return self.color == Card.RED + + def __str__(self) -> str: + """string representation of this card""" + return "{}{}".format(self.value.char, self.suit.char) + + def __repr__(self) -> str: + """representation of this card""" + return "".format(str(self)) + + def __lt__(self, other: "Card") -> bool: + """less than dunder method""" + return self.value.value < other.value.value + + def __eq__(self, other: object) -> bool: + """equal to dunder method""" + if not isinstance(other, Card): + return False + return self.suit == other.suit and self.value == other.value + + +class Deck: + """playing card deck model""" + + def __init__(self, cards: List[Card] = None, shuffle: bool = True) -> None: + """deck constructor""" + if cards: + self.cards = cards + else: + # lets start with a default deck of 52 + self.cards = [] + for suit in Card.Suits.all(): + for value in Card.Values.all(): + self.cards.append(Card(value=value, suit=suit)) + self.cards.reverse() + self.shuffles = 0 + self.draws = 0 + if shuffle: + self.shuffle() + + def __contains__(self, item: object) -> bool: + """dunder contains method""" + if not isinstance(item, Card): + return False + return item in self.cards + + def __str__(self) -> str: + """string representation of a deck""" + return "".format(self.cards_left) + + def __repr__(self) -> str: + """term representation of a deck""" + return str(self) + + @property + def top(self) -> Card: + """the top card of the deck""" + return self.cards[-1] + + @property + def bottom(self) -> Card: + """the bottom card of the deck""" + return self.cards[0] + + @property + def cards_left(self) -> int: + """number of cards left in the deck""" + return len(self.cards) + + def draw(self, times: int = 1) -> Union[Card, List[Card]]: + """draws the given number of cards from the deck""" + if times == 1: + self.draws += 1 + return self.cards.pop() + cards = [] + for _ in range(times): + self.draws += 1 + cards.append(self.cards.pop()) + return cards + + def shuffle(self, times: int = 1) -> None: + """shuffle the deck""" + for _ in range(times): + self.shuffles += 1 + random.shuffle(self.cards) + + +class EuchreDeck(Deck): + """deck specifically for euchre""" + + def __init__(self, **kwargs: Any) -> None: + """euchre deck constructor""" + cards: List[Card] = [] + + # euchre uses 9, 10, J, Q, K, A of all suits + values = [x for x in Card.Values.all() if x.value >= 9 or x.value == 1] + for suit in Card.Suits.all(): + for value in values: + cards.append(Card(value=value, suit=suit)) + cards.reverse() + super().__init__(cards=cards) diff --git a/gamble/models/dice.py b/gamble/models/dice.py index c9502ca..d0d51b0 100644 --- a/gamble/models/dice.py +++ b/gamble/models/dice.py @@ -25,13 +25,13 @@ def __repr__(self) -> str: """repr""" return self.__str__() - def __lt__(self, other) -> bool: + def __lt__(self, other: "Die") -> bool: """less than dunder method""" return self.net_sides < other.net_sides @property def net_sides(self) -> int: - """""" + """the raw max sides * multiplier""" return self.sides * self.multiplier @property @@ -54,7 +54,7 @@ def roll(self) -> int: class Dice: """a group of die objects""" - def __init__(self, init_string: str) -> None: + def __init__(self, init_string: str = "2d6") -> None: """create a new d notation group of dice""" self.__d_string = init_string.strip().lower().replace("-", "+-") self.d_strings = [x.strip() for x in self.__d_string.split("+")] @@ -89,12 +89,12 @@ def parts(self) -> List[Union[Die, int]]: return [*self.dice, *self.bonuses] @property - def max(self): + def max(self) -> int: """returns the max value these dice + bonuses could return""" return sum([*[x.max for x in self.dice], *self.bonuses]) @property - def min(self): + def min(self) -> int: """returns the min value these dice + bonuses could return""" return sum([*[x.min for x in self.dice], *self.bonuses]) diff --git a/setup.cfg b/setup.cfg index c53e25e..e5437ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [mypy] python_version = 3.6 -disallow_untyped_defs = False +disallow_untyped_defs = True ignore_missing_imports = True [flake8] diff --git a/setup.py b/setup.py index 5aae99a..438c713 100755 --- a/setup.py +++ b/setup.py @@ -3,9 +3,11 @@ pip setup file """ from setuptools import setup, find_packages -from gamble.globals import __library__, __version__ +__library__ = "gamble" +__version__ = "0.0.5" + with open("README.rst") as readme: LONG_DESCRIPTION = readme.read() diff --git a/tests/models/test_cards.py b/tests/models/test_cards.py new file mode 100644 index 0000000..ed31788 --- /dev/null +++ b/tests/models/test_cards.py @@ -0,0 +1,64 @@ +""" +tests for the cards submodule of gamble +""" +from gamble import Card, Deck, EuchreDeck + + +def test_card_init() -> None: + """test that a card can be created""" + card = Card() + assert card.suit == Card.Suits.SPADES + assert card.full_name == "ace of spades" + assert str(card) == "A♠" + assert repr(card) == "" + assert card.unicode == "🂡" + assert card.color == Card.BLACK + assert card == card + assert card != "test" + assert not card < card + assert card.is_black + assert not card.is_red + + # check that the unicode lookup works + seven = Card(value=Card.Values.SEVEN, suit=Card.Suits.DIAMONDS) + assert seven + assert seven.unicode == "🃇" + + +def test_deck_init() -> None: + """test that we can create a deck of cards""" + deck = Deck(shuffle=False) + top = deck.top + bottom = deck.bottom + assert len(deck.cards) == 52 + assert deck.cards_left == 52 + assert top.value.name == "ace" + assert top.suit.name == "spades" + assert top.unicode == "🂡" + assert bottom.value.name == "king" + assert bottom.suit.name == "hearts" + assert bottom.unicode == "🂾" + assert top in deck + assert "test" not in deck + + draw = deck.draw() + assert draw not in deck + assert isinstance(draw, Card) + assert deck.top.unicode == "🂢" + assert draw.value.name == "ace" + assert draw.suit.name == "spades" + + draw_multiple = deck.draw(times=5) + assert isinstance(draw_multiple, list) + assert str(deck) == "" + assert repr(deck) == "" + + last_top = deck.top + deck.shuffle(times=10) + assert last_top != deck.top + + +def test_euchre_deck() -> None: + """tests a euchre specific deck""" + deck = EuchreDeck(shuffle=False) + assert deck.cards_left == 24 diff --git a/tests/models/test_dice.py b/tests/models/test_dice.py index 4fd7a03..042a8f9 100644 --- a/tests/models/test_dice.py +++ b/tests/models/test_dice.py @@ -1,14 +1,49 @@ """ tests for the dice submodule of gamble """ -import gamble +import pytest +from gamble import Die, Dice -def test_die(): +def test_die_init() -> None: """tests the use of a single die""" - die = gamble.Die() + die = Die() assert die.sides == 6 assert die.max == 6 assert die.min == 1 assert die.rolls == 0 assert die.net_sides == die.sides + assert str(die) == "" + assert repr(die) == "" + + +def test_dice_init() -> None: + """tests creating a set of dice""" + dice = Dice() + assert dice.rolls == 0 + assert not dice.bonuses + assert dice.max == 12 + assert dice.min == 2 + assert dice.parts + assert str(dice) == "{\n\n\n}" + assert repr(dice) == "{\n\n\n}" + + roll = dice.roll() + assert 2 <= roll <= 12 + assert dice.rolls == 1 + + +def test_dice_complex() -> None: + """tests complex dice string setup""" + dice = Dice("d20+8") + assert dice.rolls == 0 + assert dice.bonuses + assert dice.max == 28 + assert dice.min == 9 + assert dice.parts + + +def test_broken_die() -> None: + """tests broken issues with the die class""" + with pytest.raises(Exception): + Die(sides=1) diff --git a/tox.ini b/tox.ini index f9a64b3..394de6a 100644 --- a/tox.ini +++ b/tox.ini @@ -6,4 +6,4 @@ deps = pytest pytest-cov commands = - pytest -s --cov gamble --cov-report term + pytest -s --cov gamble --cov-report term --cov-report html