From a7a3c5bed2c1761c6fae25a5a0620b93806cbb3e Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Wed, 1 Dec 2021 14:27:40 -0800 Subject: [PATCH] Add some code --- LICENSE | 6 +- Makefile | 15 ++++ cardy/__init__.py | 0 cardy/__main__.py | 12 +++ cardy/deck.py | 178 +++++++++++++++++++++++++++++++++++++++++ cardy/phase_10.py | 137 +++++++++++++++++++++++++++++++ cardy/player.py | 20 +++++ cardy/playing_cards.py | 60 ++++++++++++++ requirements-dev.txt | 0 requirements.txt | 0 10 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 cardy/__init__.py create mode 100644 cardy/__main__.py create mode 100644 cardy/deck.py create mode 100644 cardy/phase_10.py create mode 100644 cardy/player.py create mode 100644 cardy/playing_cards.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt diff --git a/LICENSE b/LICENSE index d41c0bd..f167ce0 100644 --- a/LICENSE +++ b/LICENSE @@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. - - Copyright (C) + Cardy, a Python package for building card games + Copyright (C) 2021 Ian Fijolek This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. @@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Copyright (C) + Cardy Copyright (C) 2021 Ian Fijolek This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..78c274a --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: test clean all +VENV := venv +PYTHON := python3 + +.PHONY: default +default: run + +.PHONY: run +run: $(VENV) + $(VENV)/bin/python -m cardy + +$(VENV): + $(PYTHON) -m venv $(VENV) + $(VENV)/bin/pip install -r requirements-dev.txt -r requirements.txt + diff --git a/cardy/__init__.py b/cardy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cardy/__main__.py b/cardy/__main__.py new file mode 100644 index 0000000..e153723 --- /dev/null +++ b/cardy/__main__.py @@ -0,0 +1,12 @@ +from cardy.phase_10 import Game + + +if __name__ == "__main__": + g = Game(["Ian", "Jessica"]) + print(g) + + print(f"Deal it out {g.dealer.name}") + g.deal() + print(g) + + print(f"Next player is {g.next_player.name}") diff --git a/cardy/deck.py b/cardy/deck.py new file mode 100644 index 0000000..f3c4e1b --- /dev/null +++ b/cardy/deck.py @@ -0,0 +1,178 @@ +from random import shuffle +from typing import Any +from typing import Dict +from typing import List +from typing import Iterable +from typing import Optional + + +class EmptyPileError(IndexError): + pass + + +class Card(object): + _color: Optional[str] + _face: Optional[str] + _suit: Optional[str] + _value: int + + def __init__( + self, + value: int, + suit: Optional[str] = None, + color: Optional[str] = None, + face: Optional[str] = None, + ): + self._color = color + self._face = face + self._suit = suit + self._value = value + + @property + def color(self) -> Optional[str]: + return self._color + + @property + def face(self) -> Optional[str]: + return self._face + + @property + def suit(self) -> Optional[str]: + return self._suit + + @property + def value(self) -> int: + return self._value + + def __lt__(self, other) -> bool: + return self.value < other.value + + def __le__(self, other) -> bool: + return self.value <= other.value + + def __gt__(self, other) -> bool: + return self.value > other.value + + def __ge__(self, other) -> bool: + return self.value >= other.value + + def __eq__(self, other) -> bool: + return ( + self.value == other.value and + self.face == other.face and + self.suit == other.suit and + self.color == other.color + ) + + def __repr__(self) -> str: + return f"" + + def json(self) -> Dict[str, Any]: + return { + "value": self.value, + "suit": self.suit, + "color": self.color, + "face": self.face, + } + + +class FaceDownCard(Card): + def json(self) -> Dict[str, Any]: + return { + "value": "FACE_DOWN", + } + + +class WildCard(Card): + def __init__(self): + super().__init__(-1, face="Wild", suit="Wild", color="Wild") + + def __lt__(self, other) -> bool: + return True + + def __le__(self, other) -> bool: + return True + + def __gt__(self, other) -> bool: + return True + + def __ge__(self, other) -> bool: + return True + + def __eq__(self, other) -> bool: + return True + + +class Pile(object): + # Pile of cards where 0 is the top and -1 is the bottom + _pile: List[Card] + + def __init__(self, cards: Optional[List[Card]] = None): + if cards is not None: + self._pile = cards + else: + self._pile = [] + + def add_top(self, card: Card): + self._pile.insert(0, card) + + def add_bottom(self, card: Card): + self._pile.insert(-1, card) + + def peek_top(self) -> Optional[Card]: + if self._pile: + return self._pile[0] + + return None + + def draw_card(self) -> Card: + if not self._pile: + raise EmptyPileError("Cannot draw from pile because it's empty") + + return self._pile.pop(0) + + def draw_cards(self, num_cards: int = 1) -> Iterable[Card]: + return [self.draw_card() for _ in range(0, num_cards)] + + def shuffle(self): + shuffle(self._pile) + + def __getitem__(self, i): + return self._cards[i] + + def __len__(self) -> int: + return len(self._pile) + + def __str__(self) -> str: + if self: + return ( + f"[[{len(self)-1} cards]]" + ) + else: + return "[[empty pile]]" + + def json(self) -> List[Dict[str, Any]]: + return [c.json() for c in self._pile] + + +class FaceUpPile(Pile): + + def __str__(self) -> str: + top_card = self.peek_top() + if top_card: + return ( + f"[[{str(top_card)} +{len(self)-1} cards]]" + ) + else: + return "[[empty pile]]" + + +class OpenCardSet(Pile): + + def __str__(self) -> str: + cards = ", ".join(( + str(card) for card in self._pile + )) + return ( + f"[{cards}]" + ) diff --git a/cardy/phase_10.py b/cardy/phase_10.py new file mode 100644 index 0000000..3372363 --- /dev/null +++ b/cardy/phase_10.py @@ -0,0 +1,137 @@ +from dataclasses import dataclass +from itertools import product +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from cardy.deck import Card +from cardy.deck import FaceUpPile +from cardy.deck import OpenCardSet +from cardy.deck import Pile +from cardy.deck import WildCard +from cardy.player import Player + + +class Phase10Card(Card): + pass + + +class Skip(Phase10Card): + pass + + +class Wild(WildCard, Phase10Card): + pass + + +class Deck(Pile): + def __init__(self): + cards: List[Phase10Card] = [] + for c in product(range(1, 13), ("red", "blue", "green", "yellow")): + cards.append(Phase10Card(c[0], color=c[1])) + + super().__init__(cards) + + +class PhaseSet(OpenCardSet): + pass + + +class Phase10Player(Player): + _table: List[FaceUpPile] + + def __init__(self, name): + self._table = [] + super().__init__(name) + + def take_turn(self) -> Any: + pass + + def __str__(self): + table = " no sets played" + if self._table: + table = "\n".join((" "+str(s) for s in self._table)) + return "\n".join(( + f"{self.name}: {len(self._hand)} in hand", + "On table:", + table, + )) + + def json(self, is_owner: bool=False) -> Dict[str, Any]: + state = { + "table": [pile.json() for pile in self._table], + } + if is_owner: + state["hand"] = [card.json() for card in self._hand] + if is_owner: + state["hand"] = len(self._hand) + + +@dataclass +class Phase10Turn: + player: Player + draw_card: bool + discard_card: Optional[Card] + + play_sets: Optional[List[PhaseSet]] + play_on_sets: Optional[Dict[str, List[Card]]] + + +class Game(object): + _dealer_index: int + draw_pile: Deck + discard_pile: FaceUpPile + turn: int + game_round: int + players: List[Phase10Player] + sets: List[OpenCardSet] + + def __init__(self, player_names: List[str]): + self._dealer_index = 0 + self.turn = 1 + self.game_round = 1 + self.players = [Phase10Player(name) for name in player_names] + self.draw_pile = Deck() + self.draw_pile.shuffle() + self.discard_pile = FaceUpPile() + self.sets = [] + + def deal(self): + # each player draws 10 + for _ in range(0, 10): + for seat in range(0, len(self.players)): + draw_seat = seat + self._dealer_index + 1 + p = self.players[draw_seat % len(self.players)] + p.draw_cards(self.draw_pile) + + def take_turn(self): + end_round = self.next_player.take_turn() + self.turn += 1 + if end_round: + self._reset_round() + + @property + def next_player(self) -> Player: + return self.players[self.turn % len(self.players)] + + @property + def dealer(self) -> Player: + return self.players[self._dealer_index] + + def _reset_round(self): + self.draw_pile = Deck() + self.draw_pile.shuffle() + self.dealer += 1 + self.deal() + + def __str__(self) -> str: + return "\n".join(( + "Game", + f"Draw: {str(self.draw_pile)}", + f"Discard: {str(self.discard_pile)}", + "", + "\n\n".join( + str(p) for p in self.players + ), + )) diff --git a/cardy/player.py b/cardy/player.py new file mode 100644 index 0000000..4335221 --- /dev/null +++ b/cardy/player.py @@ -0,0 +1,20 @@ +from typing import Any +from typing import List + +from cardy.deck import Card +from cardy.deck import Pile + + +class Player(object): + def __init__(self, name: str): + self.name = name + self._hand: List[Card] = [] + + def draw_cards(self, pile: Pile, num_cards: int = 1): + self._hand += pile.draw_cards(num_cards) + + def take_turn(self) -> Any: + raise NotImplementedError() + + def __repr__(self) -> str: + return f"" diff --git a/cardy/playing_cards.py b/cardy/playing_cards.py new file mode 100644 index 0000000..9ae1a20 --- /dev/null +++ b/cardy/playing_cards.py @@ -0,0 +1,60 @@ +from itertools import product +from typing import List + +from cardy.deck import Card +from cardy.deck import Pile +from cardy.deck import WildCard + + +# SUITES is a collection of tuples with suites and their colors +SUITS = ( + ("diamonds", "red"), + ("hearts", "red"), + ("clubs", "black"), + ("spades", "black"), +) + +# FACES maps special values to their face names +FACES = { + 11: "Jack", + 12: "Queen", + 13: "King", + 14: "Ace", + -1: "Joker", +} + + +class PlayingCard(Card): + @property + def face(self) -> str: + return FACES.get(self.value, str(self.value)) + + def __str__(self) -> str: + return ( + f"[{self.face.capitalize()} of {self.suit.capitalize()} " + f"({self.color.capitalize()})]" + ) + + +class Joker(WildCard, PlayingCard): + def __str__(self) -> str: + return "[Joker]" + + +class Deck(Pile): + def __init__(self, num_jokers: int = 0, aces_high: bool = False): + cards: List[PlayingCard] = [] + card_values = range(2, 15) if aces_high else range(1, 14) + for c in product(card_values, SUITS): + cards.append( + PlayingCard( + value=c[0], + suit=c[1][0], + color=c[1][1], + ) + ) + + for _ in range(0, num_jokers): + cards.append(Joker()) + + super().__init__(cards2) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29