Compare commits

5 Commits

Author SHA1 Message Date
fad15e84c5 update .gitignore 2020-04-10 17:03:20 -04:00
4f9f790086 add Player class 2020-04-10 17:02:28 -04:00
2c421727e5 add Hand and NullCollections 2020-04-10 16:23:34 -04:00
c1120bcd93 beginning implementations of CardCollection and some of its subclasses 2020-03-17 22:43:03 -04:00
2e676e4708 implemented abstract Card class
implemented concrete PlayingCard class
2020-03-16 22:27:59 -04:00
12 changed files with 270 additions and 1 deletions

2
.gitignore vendored
View File

@@ -638,6 +638,7 @@ DerivedData/
# Icon must end with two \r
Icon
# Thumbnails
._*
@@ -683,3 +684,4 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
.vscode/settings.json

0
BearOnline/__init__.py Normal file
View File

View File

@@ -0,0 +1,2 @@
from ._card import Card
from ._playingcard import PlayingCard

25
BearOnline/cards/_card.py Normal file
View File

@@ -0,0 +1,25 @@
import abc
from abc import ABC
from functools import total_ordering
@total_ordering
class Card(ABC):
def __init__(self, rank, suit):
self._rank = rank
self._suit = suit
@abc.abstractmethod
def __lt__(self, other):
pass
@abc.abstractmethod
def __eq__(self, other):
pass
@property
def rank(self):
return self._rank
@property
def suit(self):
return self._suit

View File

@@ -0,0 +1,65 @@
from itertools import chain
from . import Card
class PlayingCard(Card):
# internal class attributes for a playing card including valid suits and ranks
suits = {"SPADES": "BLACK", "CLUBS": "BLACK",
"HEARTS": "RED", "DIAMONDS": "RED"}
ranks = {rank[0].upper() if not rank.isnumeric() else rank: rank.upper() for rank in ["ace"]+[str(i)
for i in range(2, 11)] + ["jack", "queen", "king"]}
# assign an absolute numeric value to each rank for easy compairison. Ace is 0 to make it high/low
_values = {rank: value for value, rank in enumerate(ranks.keys())}
def __init__(self, rank, suit):
# do some validation of rank and suit
if not isinstance(rank, str):
try:
if str(rank).isnumeric():
rank = str(rank)
else:
raise ValueError
except ValueError:
raise ValueError(f"Rank should be a string")
if not isinstance(suit, str):
raise TypeError("Suit should be a string.")
if not suit.lower().strip() in self.suits.keys():
raise ValueError(
f"'{suit.capitalize()}' is not a valid suit in a standard playing card deck")
if not rank.upper() in chain(self.ranks.keys(), self.ranks.values()):
raise IndexError(
f"'{rank.capitalize()}' is out of the range of valid playing card ranks")
# initialize the class and store its color
super().__init__(rank.upper(), suit.upper())
self._color = self.suits[suit]
# define __lt__ and __eq__ (< and =) as required by abstract Card class to take advantage of total ordering of Card
def __eq__(self, other):
if not isinstance(other, PlayingCard) or issubclass(other, PlayingCard):
raise NotImplementedError(f"Comparison of {type(self).__name__} to {type(other).__name__} is not supported")
return self._values[self.rank] == self._values[other.rank]
def __lt__(self, other):
if not isinstance(other, PlayingCard) or issubclass(other, PlayingCard):
raise NotImplementedError(f"Comparison of {type(self).__name__} to {type(other).__name__} is not supported")
# since Ace is 0, it will always pass one of these two conditions so it is always less than
return (self._values[self.rank] < self._values[other.rank]) or (-1 * self._values[self.rank] > -1 * self._values[other.rank])
# the total ordering of Card won't get __gt__ (>) right because it's not equivalent to not __lt__ because of Ace, so explicitly define it
def __gt__(self, other):
if not isinstance(other, PlayingCard) or issubclass(other, PlayingCard):
raise NotImplementedError(f"Comparison of {type(self).__name__} to {type(other).__name__} is not supported")
# since Ace is 0, it will always pass one of these two conditions so it is always greater than
return (self._values[self.rank] > self._values[other.rank]) or (-1 * self._values[self.rank] < -1 * self._values[other.rank])
@property
def color(self):
return self._color

View File

@@ -0,0 +1,5 @@
from ._cardcollection import CardCollection
from ._deck import Deck
from ._playingcarddeck import PlayingCardDeck
from ._nullcollection import NullDeck
from ._hand import Hand

View File

@@ -0,0 +1,10 @@
from collections import deque
from .. import Card
class CardCollection(deque):
def __init__(self, iterable, maxlen=None):
if not len(iterable) == 0 or all([isinstance(Card, item) for item in iterable]):
raise TypeError("CardCollections must only contain Cards")
else:
super().__init__(iterable, maxlen)

View File

@@ -0,0 +1,34 @@
from collections.abc import Iterable
from itertools import cycle
from more_itertools import random_permutation
from ._cardcollection import CardCollection
class Deck(CardCollection):
def draw(self):
return self.pop()
def shuffle(self):
shuffled = list(random_permutation(self))
self.clear()
self.extend(shuffled)
return self
def deal(self, card_collections, cards_per_rotation=1, max_rotations=None):
if not issubclass(card_collections, Iterable) or not all([issubclass(CardCollection, collection) for collection in card_collections]):
raise TypeError("You can only deal cards to an iterable containing CardCollections")
if max_rotations is None:
rotation = cycle(card_collections)
elif isinstance(max_rotations, int):
rotation = card_collections * max_rotations
else:
raise TypeError("Max rotations should be a positive integer")
for card_collection in rotation:
try:
for _ in range(cards_per_rotation):
card_collection.append(self.draw())
except IndexError:
break

View File

@@ -0,0 +1,6 @@
from ._cardcollection import CardCollection
class Hand(CardCollection):
def __init__(self, max_handsize=None):
super().__init__([], max_handsize)

View File

@@ -0,0 +1,26 @@
from ._deck import Deck
class NullDeck(Deck):
def __init__(self):
"""Initializes an empty deck that can never contain cards. Useful as a placeholder to prevent cards from being played to a space.
"""
super().__init__([], 0)
def __add__(self, *args, **kwargs):
raise NotImplementedError("Cards can not be added to a NullDeck")
def __iadd__(self, *args, **kwargs):
raise NotImplementedError("Cards can not be added to a NullDeck")
def append(self, *args, **kwargs):
raise NotImplementedError("Cards can not be added to a NullDeck")
def appendleft(self, *args, **kwargs):
raise NotImplementedError("Cards can not be added to a NullDeck")
def extend(self, *args, **kwargs):
raise NotImplementedError("Cards can not be added to a NullDeck")
def extendleft(self, *args, **kwargs):
raise NotImplementedError("Cards can not be added to a NullDeck")

View File

@@ -0,0 +1,9 @@
from itertools import product, starmap
from ._deck import Deck
from .. import PlayingCard
class PlayingCardDeck(Deck):
def __init__(self):
#initialize the deck with the product of the ranks and suits
super().__init__(list(starmap(PlayingCard, product(PlayingCard.ranks.keys(), PlayingCard.suits.keys()))))

View File

@@ -0,0 +1,85 @@
from collections.abc import Iterable
from functools import singledispatch
from ..cards.collections import Hand
from ..cards import Card
class Player(object):
valid_roles = {"dealer","host","guest"}
def __init__(self, name, roles={}):
self._name = name
self._hand = Hand()
self._score = 0
if not roles:
roles = {}
elif isinstance(roles, str):
if roles in self.valid_roles:
roles = {roles}
else:
raise ValueError(f"{roles} is not a valid role.")
elif isinstance(roles, Iterable):
if all([role in self.valid_roles for role in roles]):
self._roles = set(roles)
else:
raise ValueError(f"{', '.join(set(roles) - self.valid_roles)} are not valid roles")
else:
raise TypeError("Role should be a string or iterable")
@singledispatch
def __add__(self, other):
pass
@__add__.register
def _(self, other: int):
self.score += other
@__add__.register
def _(self, other: Card):
self._hand.append(other)
@property
def hand(self):
return self._hand
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@property
def score(self):
return self._score
@score.setter
def score(self, value):
self._score = value
@property
def roles(self):
return self._roles
@roles.setter
def roles(self, value):
if not value in self.valid_roles:
raise ValueError(f"Invalid Player role {value}")
if (value == "host" or "host" in value) and "host" in self._roles:
raise RuntimeError("Once a player is the host, they are the host forever")
if isinstance(value, str):
self._roles ^= set((value,))
elif isinstance(Iterable, value):
self._roles ^= set(value)
else:
raise TypeError("Invalid role type")
@property
def cards_in_hand(self):
return len(self._hand)