├── tests ├── __init__.py ├── conftest.py ├── test_card.py └── test_deck.py ├── docs ├── _static │ └── .gitinclude ├── examples │ ├── examples.rst │ ├── hearthstone.rst │ ├── poker.rst │ ├── blackjack.rst │ └── kittens.rst ├── index.rst ├── api.rst ├── Makefile ├── make.bat └── conf.py ├── setup.cfg ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── .travis.yml ├── .codeclimate.yml ├── pyCardDeck ├── __init__.py ├── errors.py ├── cards.py ├── standard_deck.yml └── deck.py ├── tox.ini ├── LICENSE ├── examples ├── hearthstone_arena.py ├── poker.py ├── blackjack.py └── exploding_kittens.py ├── README.rst └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/.gitinclude: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE requirements.txt pyCardDeck/standard_deck.yml -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | codeclimate-test-reporter>=0.2.0 2 | jsonpickle>=0.9.3 3 | py>=1.4.31 4 | pytest>=3.0.3 5 | pytest-cov>=2.3.1,<2.7.0 6 | PyYAML>=3.12 7 | typing>=3.5.2.2 8 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import shutil 4 | 5 | 6 | @pytest.fixture(scope="session", autouse=True) 7 | def temp_dir(request): 8 | os.mkdir(os.getcwd() + "/tests/temp") 9 | 10 | def after_tests(): 11 | shutil.rmtree(os.getcwd() + "/tests/temp") 12 | request.addfinalizer(after_tests) 13 | -------------------------------------------------------------------------------- /docs/examples/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | If you don't want to read through the whole documentation, you can just have a look 5 | at the examples we wrote to help you understand how to use pyCardDeck, enjoy! 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | blackjack 11 | hearthstone 12 | poker 13 | kittens -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs and environemnts of contributors (feel free to add yours) 2 | .idea 3 | virtualenv 4 | venv 5 | .env 6 | 7 | # Sphinx 8 | docs/_build 9 | 10 | # Packaging 11 | build 12 | dist 13 | pyCardDeck.egg-info 14 | 15 | # Created while running/debugging 16 | .coverage 17 | .cache 18 | .tox 19 | *.pyc 20 | test*.yaml 21 | test*.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: false 4 | language: python 5 | python: 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | install: pip install tox-travis 10 | script: tox 11 | # magic to enable 3.7 without globally enabling sudo and dist: xenial for other build jobs 12 | matrix: 13 | include: 14 | - python: 3.7 15 | dist: xenial 16 | sudo: true 17 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | python: 7 | python_version: 3 8 | mass_threshold: 40 9 | fixme: 10 | enabled: true 11 | radon: 12 | enabled: true 13 | config: 14 | threshold: "C" 15 | 16 | ratings: 17 | paths: 18 | - "pyCardDeck/**.py" 19 | - "examples/**.py" 20 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pyCardDeck's documentation! 2 | ====================================== 3 | 4 | We hope you'll find everything you'll ever need in here. If youn don't, why not submit a pull request :) 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | api 10 | examples/examples 11 | 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` -------------------------------------------------------------------------------- /tests/test_card.py: -------------------------------------------------------------------------------- 1 | from pyCardDeck import * 2 | 3 | 4 | # noinspection PyPep8Naming 5 | def test_BaseCard(): 6 | card = BaseCard("BaseCard") 7 | assert str(card) == "BaseCard" 8 | assert repr(card) == "BaseCard({'name': 'BaseCard'})" 9 | 10 | 11 | # noinspection PyPep8Naming 12 | def test_PokerCard(): 13 | card = PokerCard("Hearts", "J", "Jack") 14 | assert str(card) == "Jack of Hearts" 15 | assert card.rank == "J" 16 | assert repr(card).startswith("PokerCard({'") 17 | -------------------------------------------------------------------------------- /pyCardDeck/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | pyCardDeck 5 | ========== 6 | 7 | Deck of cards with all the logic, so you don't have to! 8 | 9 | :copyright: (c) 2016 David Jetelina 10 | :license: MIT 11 | """ 12 | 13 | __title__ = 'pyCardDeck' 14 | __author__ = 'David Jetelina' 15 | __license__ = 'MIT' 16 | __copyright__ = 'Copyright 2016 David Jetelina' 17 | __version__ = '1.4.0' 18 | 19 | from .deck import * 20 | from .errors import * 21 | from .cards import * 22 | import logging 23 | 24 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py34, py35, py36, py37, docs, flake8 3 | 4 | [tox:travis] 5 | 3.5 = py35, docs, flake8 6 | 7 | [testenv] 8 | deps = -rrequirements.txt 9 | platform = 10 | win: windows 11 | linux: linux 12 | commands = 13 | py.test --cov=pyCardDeck --durations=10 tests 14 | 15 | [testenv:py35] 16 | commands = 17 | python setup.py testcov 18 | passenv = 19 | CODECLIMATE_REPO_TOKEN 20 | TRAVIS_BRANCH 21 | TRAVIS_JOB_ID 22 | TRAVIS_PULL_REQUEST 23 | CI_NAME 24 | 25 | [testenv:docs] 26 | basepython = python 27 | changedir = docs 28 | deps = 29 | sphinx 30 | sphinx_rtd_theme 31 | commands = 32 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 33 | 34 | [testenv:flake8] 35 | basepython = python 36 | deps = flake8 37 | commands= 38 | flake8 pyCardDeck --ignore E501,E402,F401,F403 39 | -------------------------------------------------------------------------------- /pyCardDeck/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class DeckException(Exception): 5 | """ 6 | Base exception class for pyCardDeck 7 | """ 8 | pass 9 | 10 | 11 | class NoCards(DeckException): 12 | """ 13 | Exception that's thrown when there are no cards to be manipulated. 14 | """ 15 | pass 16 | 17 | 18 | class OutOfCards(DeckException): 19 | """ 20 | Exception that's thrown when the deck runs out of cards. 21 | Unlike NoCardsException, this will happen naturally when reshuffling is disabled 22 | """ 23 | pass 24 | 25 | 26 | class NotACard(DeckException): 27 | """ 28 | Exception that's thrown when the manipulated object is False/None 29 | """ 30 | pass 31 | 32 | 33 | class CardNotFound(DeckException): 34 | """ 35 | Exception that's thrown when a card is not found 36 | """ 37 | pass 38 | 39 | 40 | class UnknownFormat(Exception): 41 | """ 42 | Exception thrown when trying to export to a unknown format. 43 | Supported formats: YaML, JSON 44 | """ 45 | pass 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Jetelina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /pyCardDeck/cards.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # noinspection PyCompatibility 5 | # because we are installing it through pip 6 | from typing import Union 7 | 8 | 9 | class BaseCard: 10 | """ 11 | This is an example Card, showing that each Card should have a name. 12 | 13 | This is good, because when we can show player their cards just by converting 14 | them to strings. 15 | """ 16 | 17 | def __init__(self, name: str) -> None: 18 | self.name = name 19 | 20 | def __str__(self): 21 | return self.name 22 | 23 | def __repr__(self): 24 | return '{0}({1.__dict__})'.format(type(self).__name__, self) 25 | 26 | 27 | class PokerCard(BaseCard): 28 | """ 29 | Example Poker Card, since Poker is a a deck of Unique cards, 30 | we can say that if their name equals, they equal too. 31 | """ 32 | 33 | def __init__(self, suit: str, rank: str, name: str) -> None: 34 | # Define self.name through BaseCard Class 35 | super().__init__("{} of {}".format(name, suit)) 36 | self.suit = suit 37 | self.rank = rank 38 | 39 | def __eq__(self, other): 40 | return self.name == other 41 | 42 | 43 | CardType = Union[BaseCard, PokerCard, object, str, int] 44 | -------------------------------------------------------------------------------- /examples/hearthstone_arena.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This is an example of pyCardDeck, it's not meant to be complete poker script, 5 | but rather a showcase of pyCardDeck's usage. 6 | """ 7 | 8 | import pyCardDeck 9 | import random 10 | import requests 11 | 12 | arena_deck = pyCardDeck.Deck(reshuffle=False, name="Awesome arena deck!") 13 | rarity = {"Common": 100, "Rare": 50, "Epic": 15, "Legendary": 1} 14 | 15 | 16 | def card_choice() -> list: 17 | """ 18 | Picks a rarity, then lets you make a choice 19 | 20 | :return: List with the card information 21 | """ 22 | pick_rarity = random.choice([k for k in rarity for _ in range(rarity[k])]) 23 | # This api doesn't provide an easy way to get class and rarity filter at the same time 24 | # and I'm too lazy to look for another, reminder: this is an example 25 | cards = requests.get("https://omgvamp-hearthstone-v1.p.mashape.com/cards/qualities/{}".format(pick_rarity), 26 | headers={"X-Mashape-Key": "GkQg9DFiZWmshWn6oYqlfXXlXeK9p1QuB6QjsngIi1sHnJiJqv"}).json() 27 | first, second, third = [random.choice(cards)] * 3 28 | while second == first: 29 | second = random.choice(cards) 30 | while third == first or third == second: 31 | third = random.choice(cards) 32 | choice = input("Which one would you like?\n 1: {0}, 2: {1}, 3: {2}\n".format( 33 | first['name'], second['name'], third['name'])) 34 | while choice not in ["1", "2", "3"]: 35 | if choice == "1": 36 | return first 37 | elif choice == "2": 38 | return second 39 | elif choice == "3": 40 | return third 41 | 42 | 43 | def draft(): 44 | """ 45 | Simple draft logic 46 | """ 47 | for _ in range(30): 48 | arena_deck.add_single(card_choice()) 49 | print(arena_deck) 50 | 51 | 52 | if __name__ == '__main__': 53 | draft() 54 | -------------------------------------------------------------------------------- /docs/examples/hearthstone.rst: -------------------------------------------------------------------------------- 1 | Hearthstone Arena 2 | ----------------- 3 | 4 | This shows how simple something like drafting can be with pyCardDeck. Although not much more complicated 5 | with just a list :D 6 | 7 | .. code-block:: python 8 | 9 | #!/usr/bin/env python3 10 | # -*- coding: utf-8 -*- 11 | """ 12 | This is an example of pyCardDeck, it's not meant to be complete poker script, 13 | but rather a showcase of pyCardDeck's usage. 14 | """ 15 | 16 | import pyCardDeck 17 | import random 18 | import requests 19 | 20 | arena_deck = pyCardDeck.Deck(reshuffle=False, name="Awesome arena deck!") 21 | rarity = {"Common": 100, "Rare": 50, "Epic": 15, "Legendary": 1} 22 | 23 | 24 | def card_choice() -> list: 25 | """ 26 | Picks a rarity, then lets you make a choice 27 | 28 | :return: List with the card information 29 | """ 30 | pick_rarity = random.choice([k for k in rarity for _ in range(rarity[k])]) 31 | # This api doesn't provide an easy way to get class and rarity filter at the same time 32 | # and I'm too lazy to look for another, reminder: this is an example 33 | cards = requests.get("https://omgvamp-hearthstone-v1.p.mashape.com/cards/qualities/{}".format(pick_rarity), 34 | headers={"X-Mashape-Key": "GkQg9DFiZWmshWn6oYqlfXXlXeK9p1QuB6QjsngIi1sHnJiJqv"}).json() 35 | first, second, third = [random.choice(cards)] * 3 36 | while second == first: 37 | second = random.choice(cards) 38 | while third == first or third == second: 39 | third = random.choice(cards) 40 | choice = input("Which one would you like?\n 1: {0}, 2: {1}, 3: {2}\n".format( 41 | first['name'], second['name'], third['name'])) 42 | while choice not in ["1", "2", "3"]: 43 | if choice == "1": 44 | return first 45 | elif choice == "2": 46 | return second 47 | elif choice == "3": 48 | return third 49 | 50 | 51 | def draft(): 52 | """ 53 | Simple draft logic 54 | """ 55 | for _ in range(30): 56 | arena_deck.add_single(card_choice()) 57 | print(arena_deck) 58 | 59 | 60 | if __name__ == '__main__': 61 | draft() 62 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyCardDeck 2 | ========== 3 | 4 | Status 5 | ------ 6 | 7 | .. list-table:: 8 | :widths: 30 30 9 | 10 | * - License 11 | - .. image:: https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000 12 | :target: https://opensource.org/licenses/MIT 13 | :alt: MIT License 14 | * - Versioning 15 | - .. image:: https://badge.fury.io/py/pyCardDeck.svg 16 | :target: https://badge.fury.io/py/pyCardDeck 17 | :alt: pypi 18 | .. image:: https://requires.io/github/iScrE4m/pyCardDeck/requirements.svg?branch=master 19 | :target: https://requires.io/github/iScrE4m/pyCardDeck/requirements/?branch=master 20 | :alt: Requirements Status 21 | * - Documentation 22 | - .. image:: https://readthedocs.org/projects/pycarddeck/badge/?version=latest 23 | :target: http://pycarddeck.readthedocs.io/en/latest/?badge=latest 24 | :alt: Documentation Status 25 | * - Tests 26 | - .. image:: https://travis-ci.org/iScrE4m/pyCardDeck.svg?branch=master 27 | :target: https://travis-ci.org/iScrE4m/pyCardDeck 28 | :alt: Travis CI 29 | .. image:: https://codeclimate.com/github/iScrE4m/pyCardDeck/badges/coverage.svg 30 | :target: https://codeclimate.com/github/iScrE4m/pyCardDeck/coverage 31 | :alt: Test Coverage 32 | * - Code Quality 33 | - .. image:: https://codeclimate.com/github/iScrE4m/pyCardDeck/badges/gpa.svg 34 | :target: https://codeclimate.com/github/iScrE4m/pyCardDeck 35 | :alt: Code Climate 36 | .. image:: https://codeclimate.com/github/iScrE4m/pyCardDeck/badges/issue_count.svg 37 | :target: https://codeclimate.com/github/iScrE4m/pyCardDeck 38 | :alt: Issue Count 39 | 40 | Library aimed at anyone who wants to do any kind of deck manipulation in python. 41 | So probably game developers. The goal is to have the ultimate library for all of this, 42 | supporting all kinds of game types with clean and beautiful API - kind of like requests :) 43 | 44 | If you happeen to use this library - please do let me know, I'd like to explore how it's being used. 45 | Also, you can leave a `thank you note `_ :) 46 | 47 | How to use 48 | ---------- 49 | 50 | First, install with pip:: 51 | 52 | pip install pyCardDeck 53 | 54 | Then use in your code: 55 | 56 | .. code-block:: python 57 | 58 | import pyCardDeck 59 | 60 | my_deck = pyCardDeck.Deck(cards=[1, 2, 3], name='My Awesome Deck') 61 | 62 | my_deck.shuffle() 63 | 64 | card = my_deck.draw() 65 | 66 | For more elaborate examples check out `GitHub `_ 67 | 68 | For developers 69 | -------------- 70 | 71 | The library will support Python 3.3+, for requirements look at requirements.txt in the repository. 72 | 73 | This library should be very easy to contribute to for first timers. Nothing is sacred, File issues, contribute 74 | where you feel it's useful and fun for you! If you need hlep, just ask. 75 | 76 | Always aim to write clean and readable code, make sure the tests are passing, document in docstrings (rst format) 77 | and when writing new modules, classes or functions, add them to docs (we are using Shpinx autodocs) 78 | 79 | Running tests 80 | ~~~~~~~~~~~~~ 81 | 82 | To run tests enter the pyCardDeck directory and run:: 83 | 84 | py.test tests 85 | 86 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. automodule:: pyCardDeck 5 | 6 | Types 7 | ----- 8 | 9 | pyCardDeck isn't strict about types. It's however nice to use Python 3's type annotations. 10 | That's why we have custom types set up when needed 11 | 12 | .. _CardType: 13 | 14 | CardType 15 | ~~~~~~~~ 16 | 17 | Can be either instance of an object, string or an integer. Basically, it's important 18 | that they aren't bool or NoneType. It's however recommended to inherit from 19 | one of the classes in :ref:`Cards` 20 | 21 | Classes and Functions 22 | --------------------- 23 | 24 | .. _Deck: 25 | 26 | Deck 27 | ~~~~ 28 | 29 | .. autoclass:: pyCardDeck.deck.Deck 30 | 31 | Attributes 32 | ^^^^^^^^^^ 33 | 34 | .. py:attribute:: Deck.name 35 | 36 | :return: The name of the deck 37 | :rtype: str 38 | 39 | .. py:attribute:: Deck.reshuffle 40 | 41 | :return: Whether the deck will be reshuffled when drawn out 42 | :rtype: bool 43 | 44 | .. py:attribute:: Deck._cards 45 | 46 | :return: Cards in the deck 47 | :rtype: list 48 | 49 | .. py:attribute:: Deck._discard_pile 50 | 51 | .. note:: 52 | 53 | Cards are not put in the discard pile automatically after drawing, 54 | the code assumes they went into a hand of sorts and must be discarded 55 | with :py:func:`discard` from there. This means that :py:attr:`reshuffle` doesn't 56 | work on one card deck as you can't reshuffle an empty deck 57 | (:py:exc:`errors.NoCards` would be raised). 58 | 59 | :return: Cards in the discard pile 60 | :rtype: list 61 | 62 | .. autoattribute:: pyCardDeck.deck.Deck.empty 63 | 64 | .. autoattribute:: pyCardDeck.deck.Deck.cards_left 65 | 66 | .. autoattribute:: pyCardDeck.deck.Deck.discarded 67 | 68 | .. autoattribute:: pyCardDeck.deck.Deck.json 69 | 70 | .. autoattribute:: pyCardDeck.deck.Deck.yaml 71 | 72 | Card drawing 73 | ^^^^^^^^^^^^ 74 | 75 | .. automethod:: pyCardDeck.deck.Deck.draw 76 | 77 | .. automethod:: pyCardDeck.deck.Deck.draw_bottom 78 | 79 | .. automethod:: pyCardDeck.deck.Deck.draw_random 80 | 81 | .. automethod:: pyCardDeck.deck.Deck.draw_specific 82 | 83 | Card information 84 | ^^^^^^^^^^^^^^^^ 85 | 86 | .. automethod:: pyCardDeck.deck.Deck.card_exists 87 | 88 | Deck Manipulation 89 | ^^^^^^^^^^^^^^^^^ 90 | 91 | .. automethod:: pyCardDeck.deck.Deck.shuffle 92 | 93 | .. automethod:: pyCardDeck.deck.Deck.shuffle_back 94 | 95 | .. automethod:: pyCardDeck.deck.Deck.discard 96 | 97 | .. automethod:: pyCardDeck.deck.Deck.add_single 98 | 99 | .. automethod:: pyCardDeck.deck.Deck.add_many 100 | 101 | .. automethod:: pyCardDeck.deck.Deck.show_top 102 | 103 | Import/Export 104 | ^^^^^^^^^^^^^ 105 | 106 | .. automethod:: pyCardDeck.deck.Deck.export 107 | 108 | .. automethod:: pyCardDeck.deck.Deck.load 109 | 110 | .. automethod:: pyCardDeck.deck.Deck.load_standard_deck 111 | 112 | Magic Methods 113 | ^^^^^^^^^^^^^ 114 | 115 | .. automethod:: pyCardDeck.deck.Deck.__repr__ 116 | 117 | .. automethod:: pyCardDeck.deck.Deck.__str__ 118 | 119 | .. automethod:: pyCardDeck.deck.Deck.__len__ 120 | 121 | Other Functions 122 | ^^^^^^^^^^^^^^^ 123 | 124 | .. autofunction:: pyCardDeck.deck._card_compare 125 | 126 | .. autofunction:: pyCardDeck.deck._get_exported_string 127 | 128 | .. _Cards: 129 | 130 | Cards 131 | ~~~~~ 132 | 133 | These classes are only recommended to inherit from, feel free to use your own! 134 | 135 | .. autoclass:: pyCardDeck.cards.BaseCard 136 | 137 | .. autoclass:: pyCardDeck.cards.PokerCard 138 | 139 | Exceptions 140 | ~~~~~~~~~~ 141 | 142 | .. automodule:: pyCardDeck.errors 143 | 144 | .. autoexception:: DeckException 145 | 146 | .. autoexception:: NoCards 147 | 148 | .. autoexception:: OutOfCards 149 | 150 | .. autoexception:: NotACard 151 | 152 | .. autoexception:: CardNotFound 153 | 154 | .. autoexception:: UnknownFormat 155 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | # noinspection PyPep8Naming 3 | from setuptools.command.test import test as TestCommand 4 | from setuptools import Command 5 | from subprocess import call 6 | from codecs import open 7 | import os 8 | import sys 9 | 10 | if sys.version_info.major < 3: 11 | sys.exit('Python 2 is not supported') 12 | 13 | 14 | # noinspection PyCallByClass,PyAttributeOutsideInit 15 | class PyTest(TestCommand): 16 | user_options = [('pytest-args', 'a', 'Arguments to pass into py.test')] 17 | 18 | def initialize_options(self): 19 | TestCommand.initialize_options(self) 20 | self.pytest_args = [] 21 | 22 | def finalize_options(self): 23 | TestCommand.finalize_options(self) 24 | self.test_args = [] 25 | self.test_suite = True 26 | 27 | def run_tests(self): 28 | import pytest 29 | 30 | errno = pytest.main(self.pytest_args) 31 | sys.exit(errno) 32 | 33 | 34 | class PyTestCov(Command): 35 | description = "run tests and report them to codeclimate" 36 | user_options = [] 37 | 38 | def initialize_options(self): 39 | pass 40 | 41 | def finalize_options(self): 42 | pass 43 | 44 | def run(self): 45 | errno = call(["py.test --cov=pyCardDeck --durations=10 tests"], shell=True) 46 | if os.getenv("TRAVIS_PULL_REQUEST") == "false": 47 | call(["python -m codeclimate_test_reporter --file .coverage"], shell=True) 48 | raise SystemExit(errno) 49 | 50 | 51 | class Publish(Command): 52 | description = 'Automate all the boring stuff when releasing the package' 53 | user_options = [] 54 | 55 | def initialize_options(self): 56 | pass 57 | 58 | def finalize_options(self): 59 | pass 60 | 61 | def run(self): 62 | os.system('python setup.py register') 63 | os.system('python setup.py sdist upload') 64 | os.system('python setup.py bdist_wheel upload') 65 | os.system('python setup.py bdist upload') 66 | print('All done!') 67 | sys.exit() 68 | 69 | 70 | test_requirements = ['pytest>=3.0.3', 'pytest-cov>=2.3.1', 'codeclimate-test-reporter>=0.1.2'] 71 | 72 | requirements = ['PyYAML>=3.11', 'jsonpickle>=0.9.3'] 73 | if sys.version_info.minor < 5: 74 | requirements.append('typing>=3.5.2.2') 75 | 76 | with open('README.rst', encoding='utf-8') as f: 77 | long_description = f.read() 78 | 79 | setup(name='pyCardDeck', 80 | version='1.4.0', 81 | description='Logic for decks with cards', 82 | long_description=long_description, 83 | url='https://www.djetelina.cz/project/pycarddeck/', 84 | author='David Jetelina', 85 | author_email='david@djetelina.cz', 86 | license='MIT', 87 | classifiers=[ 88 | 'Development Status :: 5 - Production/Stable', 89 | 'Intended Audience :: Developers', 90 | 'Topic :: Software Development :: Libraries', 91 | 'Topic :: Software Development :: Libraries :: Python Modules', 92 | 'Topic :: Games/Entertainment', 93 | 'Topic :: Utilities', 94 | 'License :: OSI Approved :: MIT License', 95 | 'Natural Language :: English', 96 | 'Operating System :: OS Independent', 97 | 'Programming Language :: Python', 98 | 'Programming Language :: Python :: 3 :: Only', 99 | 'Programming Language :: Python :: 3.3', 100 | 'Programming Language :: Python :: 3.4', 101 | 'Programming Language :: Python :: 3.5', 102 | 'Programming Language :: Python :: 3.6', 103 | 'Programming Language :: Python :: Implementation :: PyPy' 104 | ], 105 | keywords='cards deck card game shuffle draw discard', 106 | packages=find_packages(exclude=['tests', 'docs', 'examples']), 107 | install_requires=requirements, 108 | cmdclass={'test': PyTest, 'testcov': PyTestCov, 'publish': Publish}, 109 | tests_require=test_requirements, 110 | include_package_data=True 111 | ) 112 | -------------------------------------------------------------------------------- /examples/poker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This is an example of pyCardDeck, it's not meant to be complete poker script, 5 | but rather a showcase of pyCardDeck's usage. 6 | """ 7 | 8 | import pyCardDeck 9 | # noinspection PyCompatibility 10 | from typing import List 11 | from pyCardDeck.cards import PokerCard 12 | 13 | class Player: 14 | 15 | def __init__(self, name: str): 16 | self.hand = [] 17 | self.name = name 18 | 19 | def __str__(self): 20 | return self.name 21 | 22 | 23 | class PokerTable: 24 | 25 | def __init__(self, players: List[Player]): 26 | self.deck = pyCardDeck.Deck( 27 | cards=generate_deck(), 28 | name='Poker deck', 29 | reshuffle=False) 30 | self.players = players 31 | self.table_cards = [] 32 | print("Created a table with {} players".format(len(self.players))) 33 | 34 | def texas_holdem(self): 35 | """ 36 | Basic Texas Hold'em game structure 37 | """ 38 | print("Starting a round of Texas Hold'em") 39 | self.deck.shuffle() 40 | self.deal_cards(2) 41 | # Imagine pre-flop logic for betting here 42 | self.flop() 43 | # Imagine post-flop, pre-turn logic for betting here 44 | self.river_or_flop() 45 | # Imagine post-turn, pre-river logic for betting here 46 | self.river_or_flop() 47 | # Imagine some more betting and winner decision here 48 | self.cleanup() 49 | 50 | def deal_cards(self, number: int): 51 | """ 52 | Dealer will go through all available players and deal them x number of cards. 53 | 54 | :param number: How many cards to deal 55 | :type number: int 56 | """ 57 | for _ in range(0, number): 58 | for player in self.players: 59 | card = self.deck.draw() 60 | player.hand.append(card) 61 | print("Dealt {} to player {}".format(card, player)) 62 | 63 | def flop(self): 64 | """ 65 | Burns a card and then shows 3 new cards on the table 66 | """ 67 | # Burn a card 68 | burned = self.deck.draw() 69 | self.deck.discard(burned) 70 | print("Burned a card: {}".format(burned)) 71 | for _ in range(0, 3): 72 | card = self.deck.draw() 73 | self.table_cards.append(card) 74 | print("New card on the table: {}".format(card)) 75 | 76 | def river_or_flop(self): 77 | """ 78 | Burns a card and then shows 1 new card on the table 79 | """ 80 | burned = self.deck.draw() 81 | self.deck.discard(burned) 82 | print("Burned a card: {}".format(burned)) 83 | card = self.deck.draw() 84 | self.table_cards.append(card) 85 | print("New card on the table: {}".format(card)) 86 | 87 | def cleanup(self): 88 | """ 89 | Cleans up the table to gather all the cards back 90 | """ 91 | for player in self.players: 92 | for card in player.hand: 93 | self.deck.discard(card) 94 | for card in self.table_cards: 95 | self.deck.discard(card) 96 | self.deck.shuffle_back() 97 | print("Cleanup done") 98 | 99 | 100 | def generate_deck() -> List[PokerCard]: 101 | """ 102 | Function that generates the deck, instead of writing down 50 cards, we use iteration 103 | to generate the cards for use 104 | 105 | :return: List with all 50 poker playing cards 106 | :rtype: List[PokerCard] 107 | """ 108 | suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades'] 109 | ranks = {'A': 'Ace', 110 | '2': 'Two', 111 | '3': 'Three', 112 | '4': 'Four', 113 | '5': 'Five', 114 | '6': 'Six', 115 | '7': 'Seven', 116 | '8': 'Eight', 117 | '9': 'Nine', 118 | '10': 'Ten', 119 | 'J': 'Jack', 120 | 'Q': 'Queen', 121 | 'K': 'King'} 122 | cards = [] 123 | for suit in suits: 124 | for rank, name in ranks.items(): 125 | cards.append(PokerCard(suit, rank, name)) 126 | print('Generated deck of cards for the table') 127 | return cards 128 | 129 | if __name__ == '__main__': 130 | table = PokerTable([Player("Jack"), Player("John"), Player("Peter")]) 131 | table.texas_holdem() -------------------------------------------------------------------------------- /pyCardDeck/standard_deck.yml: -------------------------------------------------------------------------------- 1 | !!python/object:pyCardDeck.deck.Deck 2 | _cards: 3 | - !!python/object:pyCardDeck.cards.PokerCard {name: Seven of Spades, rank: '7', suit: Spades} 4 | - !!python/object:pyCardDeck.cards.PokerCard {name: Four of Hearts, rank: '4', suit: Hearts} 5 | - !!python/object:pyCardDeck.cards.PokerCard {name: Five of Clubs, rank: '5', suit: Clubs} 6 | - !!python/object:pyCardDeck.cards.PokerCard {name: Six of Diamonds, rank: '6', suit: Diamonds} 7 | - !!python/object:pyCardDeck.cards.PokerCard {name: Five of Spades, rank: '5', suit: Spades} 8 | - !!python/object:pyCardDeck.cards.PokerCard {name: King of Hearts, rank: K, suit: Hearts} 9 | - !!python/object:pyCardDeck.cards.PokerCard {name: Jack of Diamonds, rank: J, suit: Diamonds} 10 | - !!python/object:pyCardDeck.cards.PokerCard {name: Queen of Clubs, rank: Q, suit: Clubs} 11 | - !!python/object:pyCardDeck.cards.PokerCard {name: Eight of Diamonds, rank: '8', 12 | suit: Diamonds} 13 | - !!python/object:pyCardDeck.cards.PokerCard {name: Seven of Clubs, rank: '7', suit: Clubs} 14 | - !!python/object:pyCardDeck.cards.PokerCard {name: Two of Diamonds, rank: '2', suit: Diamonds} 15 | - !!python/object:pyCardDeck.cards.PokerCard {name: King of Clubs, rank: K, suit: Clubs} 16 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ace of Hearts, rank: A, suit: Hearts} 17 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ace of Clubs, rank: A, suit: Clubs} 18 | - !!python/object:pyCardDeck.cards.PokerCard {name: Nine of Spades, rank: '9', suit: Spades} 19 | - !!python/object:pyCardDeck.cards.PokerCard {name: Nine of Hearts, rank: '9', suit: Hearts} 20 | - !!python/object:pyCardDeck.cards.PokerCard {name: Seven of Hearts, rank: '7', suit: Hearts} 21 | - !!python/object:pyCardDeck.cards.PokerCard {name: Six of Spades, rank: '6', suit: Spades} 22 | - !!python/object:pyCardDeck.cards.PokerCard {name: Two of Spades, rank: '2', suit: Spades} 23 | - !!python/object:pyCardDeck.cards.PokerCard {name: Seven of Diamonds, rank: '7', 24 | suit: Diamonds} 25 | - !!python/object:pyCardDeck.cards.PokerCard {name: Eight of Spades, rank: '8', suit: Spades} 26 | - !!python/object:pyCardDeck.cards.PokerCard {name: Three of Diamonds, rank: '3', 27 | suit: Diamonds} 28 | - !!python/object:pyCardDeck.cards.PokerCard {name: Three of Clubs, rank: '3', suit: Clubs} 29 | - !!python/object:pyCardDeck.cards.PokerCard {name: Jack of Hearts, rank: J, suit: Hearts} 30 | - !!python/object:pyCardDeck.cards.PokerCard {name: Jack of Spades, rank: J, suit: Spades} 31 | - !!python/object:pyCardDeck.cards.PokerCard {name: Jack of Clubs, rank: J, suit: Clubs} 32 | - !!python/object:pyCardDeck.cards.PokerCard {name: Eight of Hearts, rank: '8', suit: Hearts} 33 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ten of Hearts, rank: '10', suit: Hearts} 34 | - !!python/object:pyCardDeck.cards.PokerCard {name: King of Spades, rank: K, suit: Spades} 35 | - !!python/object:pyCardDeck.cards.PokerCard {name: Four of Spades, rank: '4', suit: Spades} 36 | - !!python/object:pyCardDeck.cards.PokerCard {name: Two of Clubs, rank: '2', suit: Clubs} 37 | - !!python/object:pyCardDeck.cards.PokerCard {name: Nine of Clubs, rank: '9', suit: Clubs} 38 | - !!python/object:pyCardDeck.cards.PokerCard {name: Three of Spades, rank: '3', suit: Spades} 39 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ace of Diamonds, rank: A, suit: Diamonds} 40 | - !!python/object:pyCardDeck.cards.PokerCard {name: Six of Clubs, rank: '6', suit: Clubs} 41 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ace of Spades, rank: A, suit: Spades} 42 | - !!python/object:pyCardDeck.cards.PokerCard {name: Queen of Spades, rank: Q, suit: Spades} 43 | - !!python/object:pyCardDeck.cards.PokerCard {name: Three of Hearts, rank: '3', suit: Hearts} 44 | - !!python/object:pyCardDeck.cards.PokerCard {name: Six of Hearts, rank: '6', suit: Hearts} 45 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ten of Spades, rank: '10', suit: Spades} 46 | - !!python/object:pyCardDeck.cards.PokerCard {name: Nine of Diamonds, rank: '9', suit: Diamonds} 47 | - !!python/object:pyCardDeck.cards.PokerCard {name: Four of Diamonds, rank: '4', suit: Diamonds} 48 | - !!python/object:pyCardDeck.cards.PokerCard {name: Queen of Diamonds, rank: Q, suit: Diamonds} 49 | - !!python/object:pyCardDeck.cards.PokerCard {name: Five of Diamonds, rank: '5', suit: Diamonds} 50 | - !!python/object:pyCardDeck.cards.PokerCard {name: Eight of Clubs, rank: '8', suit: Clubs} 51 | - !!python/object:pyCardDeck.cards.PokerCard {name: Four of Clubs, rank: '4', suit: Clubs} 52 | - !!python/object:pyCardDeck.cards.PokerCard {name: King of Diamonds, rank: K, suit: Diamonds} 53 | - !!python/object:pyCardDeck.cards.PokerCard {name: Two of Hearts, rank: '2', suit: Hearts} 54 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ten of Diamonds, rank: '10', suit: Diamonds} 55 | - !!python/object:pyCardDeck.cards.PokerCard {name: Five of Hearts, rank: '5', suit: Hearts} 56 | - !!python/object:pyCardDeck.cards.PokerCard {name: Queen of Hearts, rank: Q, suit: Hearts} 57 | - !!python/object:pyCardDeck.cards.PokerCard {name: Ten of Clubs, rank: '10', suit: Clubs} 58 | _discard_pile: [] 59 | _reshuffle: false 60 | _save_location: None 61 | name: "Standard Deck" 62 | -------------------------------------------------------------------------------- /docs/examples/poker.rst: -------------------------------------------------------------------------------- 1 | Poker example 2 | ============= 3 | 4 | 5 | This is a poker example of pyCardDeck, it's not meant to be complete poker script, 6 | but rather a showcase of pyCardDeck's usage. 7 | 8 | .. code-block:: python 9 | 10 | import pyCardDeck 11 | from typing import List 12 | from pyCardDeck.cards import PokerCard 13 | 14 | For python 3.3 and 3.4 compatibility and type hints, we import typing.List - this is not needed, however 15 | the package itself and PokerCard are recommended here 16 | 17 | 18 | .. code-block:: python 19 | 20 | class Player: 21 | 22 | def __init__(self, name: str): 23 | self.hand = [] 24 | self.name = name 25 | 26 | def __str__(self): 27 | return self.name 28 | 29 | 30 | class PokerTable: 31 | 32 | def __init__(self, players: List[Player]): 33 | self.deck = pyCardDeck.Deck( 34 | cards=generate_deck(), 35 | name='Poker deck', 36 | reshuffle=False) 37 | self.players = players 38 | self.table_cards = [] 39 | print("Created a table with {} players".format(len(self.players))) 40 | 41 | We define our Player class, to have a hand and a name, and our PokerTable which will hold all the information 42 | and will have following methods: 43 | 44 | .. code-block:: python 45 | 46 | def texas_holdem(self): 47 | """ 48 | Basic Texas Hold'em game structure 49 | """ 50 | print("Starting a round of Texas Hold'em") 51 | self.deck.shuffle() 52 | self.deal_cards(2) 53 | # Imagine pre-flop logic for betting here 54 | self.flop() 55 | # Imagine post-flop, pre-turn logic for betting here 56 | self.river_or_flop() 57 | # Imagine post-turn, pre-river logic for betting here 58 | self.river_or_flop() 59 | # Imagine some more betting and winner decision here 60 | self.cleanup() 61 | 62 | This is the core "loop" of Texas Hold'em 63 | 64 | 65 | .. code-block:: python 66 | 67 | def deal_cards(self, number: int): 68 | for _ in range(0, number): 69 | for player in self.players: 70 | card = self.deck.draw() 71 | player.hand.append(card) 72 | print("Dealt {} to player {}".format(card, player)) 73 | 74 | Dealer will go through all available players and deal them x number of cards. 75 | 76 | 77 | .. code-block:: python 78 | 79 | def flop(self): 80 | # Burn a card 81 | burned = self.deck.draw() 82 | self.deck.discard(burned) 83 | print("Burned a card: {}".format(burned)) 84 | for _ in range(0, 3): 85 | card = self.deck.draw() 86 | self.table_cards.append(card) 87 | print("New card on the table: {}".format(card)) 88 | 89 | 90 | Burns a card and then shows 3 new cards on the table 91 | 92 | .. code-block:: python 93 | 94 | def river_or_flop(self): 95 | burned = self.deck.draw() 96 | self.deck.discard(burned) 97 | print("Burned a card: {}".format(burned)) 98 | card = self.deck.draw() 99 | self.table_cards.append(card) 100 | print("New card on the table: {}".format(card)) 101 | 102 | 103 | Burns a card and then shows 1 new card on the table 104 | 105 | .. code-block:: python 106 | 107 | def cleanup(self): 108 | for player in self.players: 109 | for card in player.hand: 110 | self.deck.discard(card) 111 | for card in self.table_cards: 112 | self.deck.discard(card) 113 | self.deck.shuffle_back() 114 | print("Cleanup done") 115 | 116 | 117 | Cleans up the table to gather all the cards back 118 | 119 | .. code-block:: python 120 | 121 | def generate_deck() -> List[PokerCard]: 122 | suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades'] 123 | ranks = {'A': 'Ace', 124 | '2': 'Two', 125 | '3': 'Three', 126 | '4': 'Four', 127 | '5': 'Five', 128 | '6': 'Six', 129 | '7': 'Seven', 130 | '8': 'Eight', 131 | '9': 'Nine', 132 | '10': 'Ten', 133 | 'J': 'Jack', 134 | 'Q': 'Queen', 135 | 'K': 'King'} 136 | cards = [] 137 | for suit in suits: 138 | for rank, name in ranks.items(): 139 | cards.append(PokerCard(suit, rank, name)) 140 | print('Generated deck of cards for the table') 141 | return cards\ 142 | 143 | 144 | Function that generates the deck, instead of writing down 50 cards, we use iteration to generate the cards for use 145 | 146 | .. code-block:: python 147 | 148 | if __name__ == '__main__': 149 | table = PokerTable([Player("Jack"), Player("John"), Player("Peter")]) 150 | table.texas_holdem() 151 | 152 | And finally this is how we start the "game" -------------------------------------------------------------------------------- /examples/blackjack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This is an example of pyCardDeck, it's not meant to be complete poker script, 5 | but rather a showcase of pyCardDeck's usage. 6 | """ 7 | 8 | import sys 9 | import pyCardDeck 10 | from typing import List 11 | from pyCardDeck.cards import PokerCard 12 | 13 | class Player: 14 | 15 | def __init__(self, name: str): 16 | self.hand = [] 17 | self.name = name 18 | 19 | def __str__(self): 20 | return self.name 21 | 22 | class BlackjackGame: 23 | 24 | def __init__(self, players: List[Player]): 25 | self.deck = pyCardDeck.Deck() 26 | self.deck.load_standard_deck() 27 | self.players = players 28 | self.scores = {} 29 | print("Created a game with {} players.".format(len(self.players))) 30 | 31 | def blackjack(self): 32 | """ 33 | The main blackjack game sequence. 34 | 35 | Each player takes an entire turn before moving on. 36 | 37 | If each player gets a turn and no one has won, the player or players 38 | with the highest score below 21 are declared the winner. 39 | """ 40 | print("Setting up...") 41 | print("Shuffling...") 42 | self.deck.shuffle() 43 | print("All shuffled!") 44 | print("Dealing...") 45 | self.deal() 46 | print("\nLet's play!") 47 | for player in self.players: 48 | print("{}'s turn...".format(player.name)) 49 | self.play(player) 50 | else: 51 | print("That's the last turn. Determining the winner...") 52 | self.find_winner() 53 | 54 | def deal(self): 55 | """ 56 | Deals two cards to each player. 57 | """ 58 | for _ in range(2): 59 | for p in self.players: 60 | newcard = self.deck.draw() 61 | p.hand.append(newcard) 62 | print("Dealt {} the {}.".format(p.name, str(newcard))) 63 | 64 | def find_winner(self): 65 | """ 66 | Finds the highest score, then finds which player(s) have that score, 67 | and reports them as the winner. 68 | """ 69 | winners = [] 70 | try: 71 | win_score = max(self.scores.values()) 72 | for key in self.scores.keys(): 73 | if self.scores[key] == win_score: 74 | winners.append(key) 75 | else: 76 | pass 77 | winstring = " & ".join(winners) 78 | print("And the winner is...{}!".format(winstring)) 79 | except ValueError: 80 | print("Whoops! Everybody lost!") 81 | 82 | def hit(self, player): 83 | """ 84 | Adds a card to the player's hand and states which card was drawn. 85 | """ 86 | newcard = self.deck.draw() 87 | player.hand.append(newcard) 88 | print(" Drew the {}.".format(str(newcard))) 89 | 90 | def play(self, player): 91 | """ 92 | An individual player's turn. 93 | 94 | If the player's cards are an ace and a ten or court card, 95 | the player has a blackjack and wins. 96 | 97 | If a player's cards total more than 21, the player loses. 98 | 99 | Otherwise, it takes the sum of their cards and determines whether 100 | to hit or stand based on their current score. 101 | """ 102 | while True: 103 | points = sum_hand(player.hand) 104 | if points < 17: 105 | print(" Hit.") 106 | self.hit(player) 107 | elif points == 21: 108 | print(" {} wins!".format(player.name)) 109 | sys.exit(0) # End if someone wins 110 | elif points > 21: 111 | print(" Bust!") 112 | break 113 | else: # Stand if between 17 and 20 (inclusive) 114 | print(" Standing at {} points.".format(str(points))) 115 | self.scores[player.name] = points 116 | break 117 | 118 | def sum_hand(hand: list): 119 | """ 120 | Converts ranks of cards into point values for scoring purposes. 121 | 'K', 'Q', and 'J' are converted to 10. 122 | 'A' is converted to 1 (for simplicity), but if the first hand is an ace 123 | and a 10-valued card, the player wins with a blackjack. 124 | """ 125 | vals = [card.rank for card in hand] 126 | intvals = [] 127 | while len(vals) > 0: 128 | value = vals.pop() 129 | try: 130 | intvals.append(int(value)) 131 | except ValueError: 132 | if value in ['K', 'Q', 'J']: 133 | intvals.append(10) 134 | elif value == 'A': 135 | intvals.append(1) # Keep it simple for the sake of example 136 | if intvals == [1, 10] or intvals == [10, 1]: 137 | print(" Blackjack!") 138 | return(21) 139 | else: 140 | points = sum(intvals) 141 | print(" Current score: {}".format(str(points))) 142 | return(points) 143 | 144 | 145 | if __name__ == "__main__": 146 | game = BlackjackGame([Player("Kit"), Player("Anya"), Player("Iris"), 147 | Player("Simon")]) 148 | game.blackjack() 149 | -------------------------------------------------------------------------------- /docs/examples/blackjack.rst: -------------------------------------------------------------------------------- 1 | Blackjack 2 | --------- 3 | 4 | Blackjack game made using pyCardDeck. This is an example of pyCardDeck; it's not 5 | meant to be complete blackjack game, but rather a showcase of pyCardDeck's usage. 6 | 7 | .. code-block:: python 8 | 9 | #!/usr/bin/env python3 10 | # -*- coding: utf-8 -*- 11 | 12 | import sys 13 | import pyCardDeck 14 | from typing import List 15 | from pyCardDeck.cards import PokerCard 16 | 17 | class Player: 18 | 19 | def __init__(self, name: str): 20 | self.hand = [] 21 | self.name = name 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | class BlackjackGame: 27 | 28 | def __init__(self, players: List[Player]): 29 | self.deck = pyCardDeck.Deck() 30 | self.deck.load_standard_deck() 31 | self.players = players 32 | self.scores = {} 33 | print("Created a game with {} players.".format(len(self.players))) 34 | 35 | The main blackjack game sequence 36 | '''''''''''''''''''''''''''''''' 37 | 38 | Each player takes an entire turn before moving on. If each player gets a turn 39 | and no one has won, the player or players with the highest score below 21 are 40 | declared the winner. 41 | 42 | .. code-block:: python 43 | 44 | def blackjack(self): 45 | 46 | print("Setting up...") 47 | print("Shuffling...") 48 | self.deck.shuffle() 49 | print("All shuffled!") 50 | print("Dealing...") 51 | self.deal() 52 | print("\nLet's play!") 53 | for player in self.players: 54 | print("{}'s turn...".format(player.name)) 55 | self.play(player) 56 | else: 57 | print("That's the last turn. Determining the winner...") 58 | self.find_winner() 59 | 60 | Dealing. 61 | ```````` 62 | 63 | Deals two cards to each player. 64 | 65 | .. code-block:: python 66 | 67 | def deal(self): 68 | for _ in range(2): 69 | for p in self.players: 70 | newcard = self.deck.draw() 71 | p.hand.append(newcard) 72 | print("Dealt {} the {}.".format(p.name, str(newcard))) 73 | 74 | 75 | 76 | Determining the winner. 77 | ``````````````````````` 78 | 79 | Finds the highest score, then finds which player(s) have that score, 80 | and reports them as the winner. 81 | 82 | .. code-block:: python 83 | 84 | def find_winner(self): 85 | 86 | winners = [] 87 | try: 88 | win_score = max(self.scores.values()) 89 | for key in self.scores.keys(): 90 | if self.scores[key] == win_score: 91 | winners.append(key) 92 | else: 93 | pass 94 | winstring = " & ".join(winners) 95 | print("And the winner is...{}!".format(winstring)) 96 | except ValueError: 97 | print("Whoops! Everybody lost!") 98 | 99 | Hit. 100 | ```` 101 | 102 | Adds a card to the player's hand and states which card was drawn. 103 | 104 | .. code-block:: python 105 | 106 | def hit(self, player): 107 | 108 | newcard = self.deck.draw() 109 | player.hand.append(newcard) 110 | print(" Drew the {}.".format(str(newcard))) 111 | 112 | An individual player's turn. 113 | ```````````````````````````` 114 | 115 | If the player's cards are an ace and a ten or court card, 116 | the player has a blackjack and wins. 117 | 118 | If a player's cards total more than 21, the player loses. 119 | 120 | Otherwise, it takes the sum of their cards and determines whether 121 | to hit or stand based on their current score. 122 | 123 | .. code-block:: python 124 | 125 | def play(self, player): 126 | 127 | while True: 128 | points = sum_hand(player.hand) 129 | if points < 17: 130 | print(" Hit.") 131 | self.hit(player) 132 | elif points == 21: 133 | print(" {} wins!".format(player.name)) 134 | sys.exit(0) # End if someone wins 135 | elif points > 21: 136 | print(" Bust!") 137 | break 138 | else: # Stand if between 17 and 20 (inclusive) 139 | print(" Standing at {} points.".format(str(points))) 140 | self.scores[player.name] = points 141 | break 142 | 143 | Sum of cards in hand. 144 | ''''''''''''''''''''' 145 | 146 | Converts ranks of cards into point values for scoring purposes. 147 | 'K', 'Q', and 'J' are converted to 10. 'A' is converted to 1 (for simplicity), 148 | but if the first hand is an ace and a 10-valued card, the player wins with a blackjack. 149 | 150 | .. code-block:: python 151 | 152 | def sum_hand(hand: list): 153 | 154 | vals = [card.rank for card in hand] 155 | intvals = [] 156 | while len(vals) > 0: 157 | value = vals.pop() 158 | try: 159 | intvals.append(int(value)) 160 | except ValueError: 161 | if value in ['K', 'Q', 'J']: 162 | intvals.append(10) 163 | elif value == 'A': 164 | intvals.append(1) # Keep it simple for the sake of example 165 | if intvals == [1, 10] or intvals == [10, 1]: 166 | print(" Blackjack!") 167 | return(21) 168 | else: 169 | points = sum(intvals) 170 | print(" Current score: {}".format(str(points))) 171 | return(points) 172 | 173 | .. code-block:: python 174 | 175 | if __name__ == "__main__": 176 | game = BlackjackGame([Player("Kit"), Player("Anya"), Player("Iris"), 177 | Player("Simon")]) 178 | game.blackjack() 179 | -------------------------------------------------------------------------------- /examples/exploding_kittens.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This is an example of pyCardDeck, it's not meant to be complete poker script, 5 | but rather a showcase of pyCardDeck's usage. 6 | """ 7 | 8 | import pyCardDeck 9 | from pyCardDeck.cards import BaseCard 10 | from random import randrange 11 | 12 | 13 | class Player: 14 | 15 | def __init__(self): 16 | self.hand = [] 17 | 18 | def turn(self): 19 | pass 20 | 21 | def skip(self): 22 | pass 23 | 24 | def take_turn_twice(self): 25 | self.turn() 26 | self.turn() 27 | 28 | def nope_prompt(self) -> bool: 29 | for card in self.hand: 30 | if card.name == "Nope": 31 | if input("Do you want to use your Nope card?").lower().startswith("y"): 32 | return True 33 | else: 34 | return False 35 | return False 36 | 37 | def insert_explode(self) -> int: 38 | position = int(input("At which position from top do you want to insert Exploding Kitten back into the deck?")) 39 | return position 40 | 41 | 42 | class KittenCard(BaseCard): 43 | 44 | def __init__(self, name: str, targetable: bool = False, selfcast: bool = False): 45 | super().__init__(name) 46 | self.selfcast = selfcast 47 | self.targetable = targetable 48 | 49 | def effect(self, player: Player, target: Player): 50 | pass 51 | 52 | 53 | class ExplodeCard(KittenCard): 54 | 55 | def __init__(self, name: str = "Exploding Kitten"): 56 | super().__init__(name) 57 | 58 | 59 | class DefuseCard(KittenCard): 60 | 61 | def __init__(self, deck: pyCardDeck.deck, name: str = "Defuse"): 62 | super().__init__(name, selfcast=True) 63 | self.deck = deck 64 | 65 | def effect(self, player: Player, target: Player): 66 | position = player.insert_explode() 67 | self.deck.add_single(ExplodeCard(), position=position) 68 | 69 | 70 | class TacocatCard(KittenCard): 71 | 72 | def __init__(self, name: str = "Tacocat"): 73 | super().__init__(name) 74 | 75 | 76 | class OverweightCard(KittenCard): 77 | 78 | def __init__(self, name: str = "Overweight Bikini Cat"): 79 | super().__init__(name) 80 | 81 | 82 | class ShuffleCard(KittenCard): 83 | 84 | def __init__(self, deck: pyCardDeck.Deck, name: str = "Shuffle"): 85 | super().__init__(name) 86 | self.deck = deck 87 | 88 | def effect(self, player: Player, target: Player): 89 | self.deck.shuffle() 90 | 91 | 92 | class AttackCard(KittenCard): 93 | 94 | def __init__(self, name: str = "Attack"): 95 | super().__init__(name, selfcast=True, targetable=True) 96 | 97 | def effect(self, player: Player, target: Player): 98 | player.skip() 99 | target.take_turn_twice() 100 | 101 | 102 | class SeeTheFuture(KittenCard): 103 | 104 | def __init__(self, deck: pyCardDeck.Deck, name: str = "See The Future"): 105 | super().__init__(name) 106 | self.deck = deck 107 | 108 | def effect(self, player: Player, target: Player): 109 | self.deck.show_top(3) 110 | 111 | 112 | class NopeCard(KittenCard): 113 | 114 | def __init__(self, name: str = "Nope"): 115 | super().__init__(name) 116 | 117 | 118 | class SkipCard(KittenCard): 119 | 120 | def __init__(self, name: str = "Skip"): 121 | super().__init__(name, selfcast=True) 122 | 123 | def effect(self, player: Player, target: Player): 124 | player.skip() 125 | 126 | 127 | class FavorCard(KittenCard): 128 | 129 | def __init__(self, name: str = "Favor"): 130 | super().__init__(name, targetable=True, selfcast=True) 131 | 132 | def effect(self, player: Player, target: Player): 133 | random_target_card = target.hand.pop(randrange(target.hand)) 134 | player.hand.append(random_target_card) 135 | 136 | 137 | class Game: 138 | 139 | def __init__(self, players: list): 140 | self.deck = pyCardDeck.Deck() 141 | self.players = players 142 | self.prepare_cards() 143 | self.deal_to_players() 144 | self.add_defuses() 145 | self.add_explodes() 146 | while len(self.players) > 1: 147 | self.play() 148 | 149 | def play(self): 150 | pass 151 | 152 | def turn(self): 153 | pass 154 | 155 | def prepare_cards(self): 156 | print("Preparing deck from which to deal to players") 157 | self.deck.add_many(construct_deck(self)) 158 | 159 | def deal_to_players(self): 160 | print("Dealing cards to players") 161 | for _ in range(4): 162 | for player in self.players: 163 | player.hand.append(self.deck.draw()) 164 | 165 | def ask_for_nope(self): 166 | noped = False 167 | for player in self.players: 168 | noped = player.nope_prompt() 169 | return noped 170 | 171 | def add_explodes(self): 172 | print("Adding explodes to the deck") 173 | self.deck.add_many([ExplodeCard() for _ in range(len(self.players) - 1)]) 174 | 175 | def add_defuses(self): 176 | print("Adding defuses to the deck") 177 | self.deck.add_many([DefuseCard(self.deck) for _ in range(6 - len(self.players))]) 178 | 179 | def play_card(self, card: KittenCard, player: Player = None, target: Player = None): 180 | if card.selfcast and player is None: 181 | raise Exception("You must pass a player who owns the card!") 182 | elif card.targetable and target is None: 183 | raise Exception("You must pass a target!") 184 | elif not self.ask_for_nope(): 185 | card.effect(player, target) 186 | else: 187 | print("Card was noped :(") 188 | 189 | 190 | def construct_deck(game: Game): 191 | card_list = [ 192 | TacocatCard(), 193 | TacocatCard(), 194 | TacocatCard(), 195 | TacocatCard(), 196 | OverweightCard(), 197 | OverweightCard(), 198 | OverweightCard(), 199 | OverweightCard(), 200 | ShuffleCard(game.deck), 201 | ShuffleCard(game.deck), 202 | ShuffleCard(game.deck), 203 | ShuffleCard(game.deck), 204 | AttackCard(), 205 | AttackCard(), 206 | AttackCard(), 207 | AttackCard(), 208 | SeeTheFuture(game.deck), 209 | SeeTheFuture(game.deck), 210 | SeeTheFuture(game.deck), 211 | SeeTheFuture(game.deck), 212 | SeeTheFuture(game.deck), 213 | NopeCard(), 214 | NopeCard(), 215 | NopeCard(), 216 | NopeCard(), 217 | NopeCard(), 218 | SkipCard(), 219 | SkipCard(), 220 | SkipCard(), 221 | SkipCard(), 222 | FavorCard(), 223 | FavorCard(), 224 | FavorCard(), 225 | FavorCard(), 226 | ] 227 | return card_list 228 | -------------------------------------------------------------------------------- /docs/examples/kittens.rst: -------------------------------------------------------------------------------- 1 | Exploding Kittens 2 | ----------------- 3 | 4 | Here's a bit more advanced game using pyCardDeck. This code itself is not the full game, but should showcase how 5 | the library is meant to be used. If you find anything in here impractical or not clean, easy and nice, please 6 | file an issue! 7 | 8 | .. code-block:: python 9 | 10 | import pyCardDeck 11 | from pyCardDeck.cards import BaseCard 12 | from random import randrange 13 | 14 | 15 | class Player: 16 | 17 | def __init__(self): 18 | self.hand = [] 19 | 20 | def turn(self): 21 | pass 22 | 23 | def skip(self): 24 | pass 25 | 26 | def take_turn_twice(self): 27 | self.turn() 28 | self.turn() 29 | 30 | def nope_prompt(self) -> bool: 31 | for card in self.hand: 32 | if card.name == "Nope": 33 | if input("Do you want to use your Nope card?").lower().startswith("y"): 34 | return True 35 | else: 36 | return False 37 | return False 38 | 39 | def insert_explode(self) -> int: 40 | position = int(input("At which position from top do you want to insert Exploding Kitten back into the deck?")) 41 | return position 42 | 43 | 44 | class KittenCard(BaseCard): 45 | 46 | def __init__(self, name: str, targetable: bool = False, selfcast: bool = False): 47 | super().__init__(name) 48 | self.selfcast = selfcast 49 | self.targetable = targetable 50 | 51 | def effect(self, player: Player, target: Player): 52 | pass 53 | 54 | 55 | class ExplodeCard(KittenCard): 56 | 57 | def __init__(self, name: str = "Exploding Kitten"): 58 | super().__init__(name) 59 | 60 | 61 | class DefuseCard(KittenCard): 62 | 63 | def __init__(self, deck: pyCardDeck.deck, name: str = "Defuse"): 64 | super().__init__(name, selfcast=True) 65 | self.deck = deck 66 | 67 | def effect(self, player: Player, target: Player): 68 | position = player.insert_explode() 69 | self.deck.add_single(ExplodeCard(), position=position) 70 | 71 | 72 | class TacocatCard(KittenCard): 73 | 74 | def __init__(self, name: str = "Tacocat"): 75 | super().__init__(name) 76 | 77 | 78 | class OverweightCard(KittenCard): 79 | 80 | def __init__(self, name: str = "Overweight Bikini Cat"): 81 | super().__init__(name) 82 | 83 | 84 | class ShuffleCard(KittenCard): 85 | 86 | def __init__(self, deck: pyCardDeck.Deck, name: str = "Shuffle"): 87 | super().__init__(name) 88 | self.deck = deck 89 | 90 | def effect(self, player: Player, target: Player): 91 | self.deck.shuffle() 92 | 93 | 94 | class AttackCard(KittenCard): 95 | 96 | def __init__(self, name: str = "Attack"): 97 | super().__init__(name, selfcast=True, targetable=True) 98 | 99 | def effect(self, player: Player, target: Player): 100 | player.skip() 101 | target.take_turn_twice() 102 | 103 | 104 | class SeeTheFuture(KittenCard): 105 | 106 | def __init__(self, deck: pyCardDeck.Deck, name: str = "See The Future"): 107 | super().__init__(name) 108 | self.deck = deck 109 | 110 | def effect(self, player: Player, target: Player): 111 | self.deck.show_top(3) 112 | 113 | 114 | class NopeCard(KittenCard): 115 | 116 | def __init__(self, name: str = "Nope"): 117 | super().__init__(name) 118 | 119 | 120 | class SkipCard(KittenCard): 121 | 122 | def __init__(self, name: str = "Skip"): 123 | super().__init__(name, selfcast=True) 124 | 125 | def effect(self, player: Player, target: Player): 126 | player.skip() 127 | 128 | 129 | class FavorCard(KittenCard): 130 | 131 | def __init__(self, name: str = "Favor"): 132 | super().__init__(name, targetable=True, selfcast=True) 133 | 134 | def effect(self, player: Player, target: Player): 135 | random_target_card = target.hand.pop(randrange(target.hand)) 136 | player.hand.append(random_target_card) 137 | 138 | 139 | class Game: 140 | 141 | def __init__(self, players: list): 142 | self.deck = pyCardDeck.Deck() 143 | self.players = players 144 | self.prepare_cards() 145 | self.deal_to_players() 146 | self.add_defuses() 147 | self.add_explodes() 148 | while len(self.players) > 1: 149 | self.play() 150 | 151 | def play(self): 152 | pass 153 | 154 | def turn(self): 155 | pass 156 | 157 | def prepare_cards(self): 158 | print("Preparing deck from which to deal to players") 159 | self.deck.add_many(construct_deck(self)) 160 | 161 | def deal_to_players(self): 162 | print("Dealing cards to players") 163 | for _ in range(4): 164 | for player in self.players: 165 | player.hand.append(self.deck.draw()) 166 | 167 | def ask_for_nope(self): 168 | noped = False 169 | for player in self.players: 170 | noped = player.nope_prompt() 171 | return noped 172 | 173 | def add_explodes(self): 174 | print("Adding explodes to the deck") 175 | self.deck.add_many([ExplodeCard() for _ in range(len(self.players) - 1)]) 176 | 177 | def add_defuses(self): 178 | print("Adding defuses to the deck") 179 | self.deck.add_many([DefuseCard(self.deck) for _ in range(6 - len(self.players))]) 180 | 181 | def play_card(self, card: KittenCard, player: Player = None, target: Player = None): 182 | if card.selfcast and player is None: 183 | raise Exception("You must pass a player who owns the card!") 184 | elif card.targetable and target is None: 185 | raise Exception("You must pass a target!") 186 | elif not self.ask_for_nope(): 187 | card.effect(player, target) 188 | else: 189 | print("Card was noped :(") 190 | 191 | 192 | 193 | def construct_deck(game: Game): 194 | card_list = [ 195 | TacocatCard(), 196 | TacocatCard(), 197 | TacocatCard(), 198 | TacocatCard(), 199 | OverweightCard(), 200 | OverweightCard(), 201 | OverweightCard(), 202 | OverweightCard(), 203 | ShuffleCard(game.deck), 204 | ShuffleCard(game.deck), 205 | ShuffleCard(game.deck), 206 | ShuffleCard(game.deck), 207 | AttackCard(), 208 | AttackCard(), 209 | AttackCard(), 210 | AttackCard(), 211 | SeeTheFuture(game.deck), 212 | SeeTheFuture(game.deck), 213 | SeeTheFuture(game.deck), 214 | SeeTheFuture(game.deck), 215 | SeeTheFuture(game.deck), 216 | NopeCard(), 217 | NopeCard(), 218 | NopeCard(), 219 | NopeCard(), 220 | NopeCard(), 221 | SkipCard(), 222 | SkipCard(), 223 | SkipCard(), 224 | SkipCard(), 225 | FavorCard(), 226 | FavorCard(), 227 | FavorCard(), 228 | FavorCard(), 229 | ] 230 | return card_list 231 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyCardDeck.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyCardDeck.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyCardDeck" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyCardDeck" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyCardDeck.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyCardDeck.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyCardDeck documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Aug 17 20:12:10 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.coverage', 36 | 'sphinx.ext.viewcode', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | # 50 | # source_encoding = 'utf-8-sig' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = 'pyCardDeck' 57 | copyright = '2016, David Jetelina' 58 | author = 'David Jetelina' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = '1.4' 66 | # The full version, including alpha/beta/rc tags. 67 | release = '1.4.0' 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | # 79 | # today = '' 80 | # 81 | # Else, today_fmt is used as the format for a strftime call. 82 | # 83 | # today_fmt = '%B %d, %Y' 84 | 85 | # List of patterns, relative to source directory, that match files and 86 | # directories to ignore when looking for source files. 87 | # This patterns also effect to html_static_path and html_extra_path 88 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 89 | 90 | # The reST default role (used for this markup: `text`) to use for all 91 | # documents. 92 | # 93 | # default_role = None 94 | 95 | # If true, '()' will be appended to :func: etc. cross-reference text. 96 | # 97 | # add_function_parentheses = True 98 | 99 | # If true, the current module name will be prepended to all description 100 | # unit titles (such as .. function::). 101 | # 102 | # add_module_names = True 103 | 104 | # If true, sectionauthor and moduleauthor directives will be shown in the 105 | # output. They are ignored by default. 106 | # 107 | # show_authors = False 108 | 109 | # The name of the Pygments (syntax highlighting) style to use. 110 | pygments_style = 'sphinx' 111 | 112 | # A list of ignored prefixes for module index sorting. 113 | # modindex_common_prefix = [] 114 | 115 | # If true, keep warnings as "system message" paragraphs in the built documents. 116 | # keep_warnings = False 117 | 118 | todo_include_todos = False 119 | 120 | 121 | # -- Options for HTML output ---------------------------------------------- 122 | 123 | # The theme to use for HTML and HTML Help pages. See the documentation for 124 | # a list of builtin themes. 125 | # 126 | html_theme = 'sphinx_rtd_theme' 127 | 128 | # Theme options are theme-specific and customize the look and feel of a theme 129 | # further. For a list of options available for each theme, see the 130 | # documentation. 131 | # 132 | # html_theme_options = {} 133 | 134 | # Add any paths that contain custom themes here, relative to this directory. 135 | # html_theme_path = [] 136 | 137 | # The name for this set of Sphinx documents. 138 | # " v documentation" by default. 139 | # 140 | # html_title = 'pyCardDeck v1.0.0.dev1' 141 | 142 | # A shorter title for the navigation bar. Default is the same as html_title. 143 | # 144 | # html_short_title = None 145 | 146 | # The name of an image file (relative to this directory) to place at the top 147 | # of the sidebar. 148 | # 149 | # html_logo = None 150 | 151 | # The name of an image file (relative to this directory) to use as a favicon of 152 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 153 | # pixels large. 154 | # 155 | # html_favicon = None 156 | 157 | # Add any paths that contain custom static files (such as style sheets) here, 158 | # relative to this directory. They are copied after the builtin static files, 159 | # so a file named "default.css" will overwrite the builtin "default.css". 160 | html_static_path = ['_static'] 161 | 162 | # Add any extra paths that contain custom files (such as robots.txt or 163 | # .htaccess) here, relative to this directory. These files are copied 164 | # directly to the root of the documentation. 165 | # 166 | # html_extra_path = [] 167 | 168 | # If not None, a 'Last updated on:' timestamp is inserted at every page 169 | # bottom, using the given strftime format. 170 | # The empty string is equivalent to '%b %d, %Y'. 171 | # 172 | # html_last_updated_fmt = None 173 | 174 | # If true, SmartyPants will be used to convert quotes and dashes to 175 | # typographically correct entities. 176 | # 177 | # html_use_smartypants = True 178 | 179 | # Custom sidebar templates, maps document names to template names. 180 | # 181 | # html_sidebars = {} 182 | 183 | # Additional templates that should be rendered to pages, maps page names to 184 | # template names. 185 | # 186 | # html_additional_pages = {} 187 | 188 | # If false, no module index is generated. 189 | # 190 | # html_domain_indices = True 191 | 192 | # If false, no index is generated. 193 | # 194 | # html_use_index = True 195 | 196 | # If true, the index is split into individual pages for each letter. 197 | # 198 | # html_split_index = False 199 | 200 | # If true, links to the reST sources are added to the pages. 201 | # 202 | # html_show_sourcelink = True 203 | 204 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 205 | # 206 | # html_show_sphinx = True 207 | 208 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 209 | # 210 | # html_show_copyright = True 211 | 212 | # If true, an OpenSearch description file will be output, and all pages will 213 | # contain a tag referring to it. The value of this option must be the 214 | # base URL from which the finished HTML is served. 215 | # 216 | # html_use_opensearch = '' 217 | 218 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 219 | # html_file_suffix = None 220 | 221 | # Language to be used for generating the HTML full-text search index. 222 | # Sphinx supports the following languages: 223 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 224 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 225 | # 226 | # html_search_language = 'en' 227 | 228 | # A dictionary with options for the search language support, empty by default. 229 | # 'ja' uses this config value. 230 | # 'zh' user can custom change `jieba` dictionary path. 231 | # 232 | # html_search_options = {'type': 'default'} 233 | 234 | # The name of a javascript file (relative to the configuration directory) that 235 | # implements a search results scorer. If empty, the default will be used. 236 | # 237 | # html_search_scorer = 'scorer.js' 238 | 239 | # Output file base name for HTML help builder. 240 | htmlhelp_basename = 'pyCardDeckdoc' 241 | 242 | # -- Options for LaTeX output --------------------------------------------- 243 | 244 | latex_elements = { 245 | # The paper size ('letterpaper' or 'a4paper'). 246 | # 247 | # 'papersize': 'letterpaper', 248 | 249 | # The font size ('10pt', '11pt' or '12pt'). 250 | # 251 | # 'pointsize': '10pt', 252 | 253 | # Additional stuff for the LaTeX preamble. 254 | # 255 | # 'preamble': '', 256 | 257 | # Latex figure (float) alignment 258 | # 259 | # 'figure_align': 'htbp', 260 | } 261 | 262 | # Grouping the document tree into LaTeX files. List of tuples 263 | # (source start file, target name, title, 264 | # author, documentclass [howto, manual, or own class]). 265 | latex_documents = [ 266 | (master_doc, 'pyCardDeck.tex', 'pyCardDeck Documentation', 267 | 'David Jetelina', 'manual'), 268 | ] 269 | 270 | # The name of an image file (relative to this directory) to place at the top of 271 | # the title page. 272 | # 273 | # latex_logo = None 274 | 275 | # For "manual" documents, if this is true, then toplevel headings are parts, 276 | # not chapters. 277 | # 278 | # latex_use_parts = False 279 | 280 | # If true, show page references after internal links. 281 | # 282 | # latex_show_pagerefs = False 283 | 284 | # If true, show URL addresses after external links. 285 | # 286 | # latex_show_urls = False 287 | 288 | # Documents to append as an appendix to all manuals. 289 | # 290 | # latex_appendices = [] 291 | 292 | # It false, will not define \strong, \code, itleref, \crossref ... but only 293 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 294 | # packages. 295 | # 296 | # latex_keep_old_macro_names = True 297 | 298 | # If false, no module index is generated. 299 | # 300 | # latex_domain_indices = True 301 | 302 | 303 | # -- Options for manual page output --------------------------------------- 304 | 305 | # One entry per manual page. List of tuples 306 | # (source start file, name, description, authors, manual section). 307 | man_pages = [ 308 | (master_doc, 'pycarddeck', 'pyCardDeck Documentation', 309 | [author], 1) 310 | ] 311 | 312 | # If true, show URL addresses after external links. 313 | # 314 | # man_show_urls = False 315 | 316 | 317 | # -- Options for Texinfo output ------------------------------------------- 318 | 319 | # Grouping the document tree into Texinfo files. List of tuples 320 | # (source start file, target name, title, author, 321 | # dir menu entry, description, category) 322 | texinfo_documents = [ 323 | (master_doc, 'pyCardDeck', 'pyCardDeck Documentation', 324 | author, 'pyCardDeck', 'One line description of project.', 325 | 'Miscellaneous'), 326 | ] 327 | 328 | # Documents to append as an appendix to all manuals. 329 | # 330 | # texinfo_appendices = [] 331 | 332 | # If false, no module index is generated. 333 | # 334 | # texinfo_domain_indices = True 335 | 336 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 337 | # 338 | # texinfo_show_urls = 'footnote' 339 | 340 | # If true, do not generate a @detailmenu in the "Top" node's menu. 341 | # 342 | # texinfo_no_detailmenu = False 343 | -------------------------------------------------------------------------------- /tests/test_deck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import pytest 5 | 6 | from pyCardDeck import * 7 | 8 | 9 | class Card: 10 | """ 11 | Example Card object, will be replaced by template later down the line 12 | """ 13 | 14 | def __init__(self, name: str, specific_string="abc"): 15 | self.name = name 16 | self.specific_string = specific_string 17 | 18 | def __repr__(self): # pragma: no cover 19 | return "Card instance {0.name}, {0.specific_string}" \ 20 | .format(self) 21 | 22 | 23 | class DifferentCard(Card): 24 | """ 25 | Card object that's a different class than card 26 | """ 27 | 28 | 29 | class ExportCard(Card): 30 | """ 31 | Card object used for importing and exporting 32 | """ 33 | 34 | def __eq__(self, other): 35 | return self.name == other 36 | 37 | 38 | def test_draw(): 39 | d = Deck(cards=[ 40 | Card('One'), Card('Two'), Card('Three'), Card('Four') 41 | ], reshuffle=False) 42 | assert d.draw().name == Card('One').name 43 | d.draw() 44 | d.draw() 45 | assert d.draw().name == Card('Four').name 46 | with pytest.raises(OutOfCards): 47 | d.draw() 48 | assert d.empty 49 | 50 | 51 | def test_draw_bottom(): 52 | d = Deck(cards=[ 53 | Card('One'), Card('Two'), Card('Three'), Card('Four') 54 | ], reshuffle=False) 55 | d.draw_bottom() 56 | assert d.draw_bottom().name == Card('Three').name 57 | d.draw_bottom() 58 | assert d.draw_bottom().name == Card('One').name 59 | with pytest.raises(OutOfCards): 60 | d.draw_bottom() 61 | assert d.empty 62 | 63 | 64 | def test_draw_random(): 65 | d = Deck(cards=[ 66 | Card('One'), Card('Two'), Card('Three'), Card('Four') 67 | ], reshuffle=False) 68 | d.draw_random() 69 | d.draw_random() 70 | d.draw_random() 71 | d.draw_random() 72 | assert d.empty 73 | with pytest.raises(OutOfCards): 74 | d.draw_random() 75 | assert d.empty 76 | 77 | 78 | def test_draw_specific_instance(): 79 | one = Card('One', specific_string="bbc") 80 | one_small = Card('one', specific_string="bbc") 81 | one_diff_string = Card('One', specific_string="car") 82 | one_bare = Card('One') 83 | d = Deck(cards=[ 84 | one, 85 | one_small, 86 | one_diff_string, 87 | one_bare 88 | ], reshuffle=False) 89 | assert d.draw_specific(one).__dict__ == one.__dict__ 90 | with pytest.raises(CardNotFound): 91 | d.draw_specific(Card('Seven')) 92 | d.draw_specific(one_small) 93 | d.draw_specific(one_diff_string) 94 | d.draw_specific(one_bare) 95 | assert d.empty 96 | with pytest.raises(NoCards): 97 | d.draw_specific(one_bare) 98 | 99 | 100 | def test_draw_specific_string(): 101 | d = Deck(cards=['a', 'b', 'c', 'd']) 102 | assert d.draw_specific('a') == 'a' 103 | 104 | 105 | def test_draws_else(): 106 | d = Deck() 107 | with pytest.raises(NoCards): 108 | d.draw() 109 | with pytest.raises(NoCards): 110 | d.draw_bottom() 111 | with pytest.raises(NoCards): 112 | d.draw_random() 113 | assert d.empty 114 | 115 | 116 | def test_draws_reshuffle(): 117 | d = Deck(cards=[Card('One')]) 118 | d.discard(Card('One')) 119 | d.draw_specific(Card('One')) 120 | assert not d.empty 121 | d.discard(Card('One')) 122 | d.draw() 123 | assert not d.empty 124 | d.discard(Card('One')) 125 | d.draw_bottom() 126 | assert not d.empty 127 | d.discard(Card('One')) 128 | d.draw_random() 129 | assert not d.empty 130 | 131 | 132 | def test_card_exists_instance(): 133 | one = Card('One', specific_string='bbc') 134 | one_alt = DifferentCard('One', specific_string='bbc') 135 | d = Deck(cards=[ 136 | one, 137 | Card('one', specific_string='bbc'), 138 | Card('One', specific_string='car'), 139 | Card('One') 140 | ], reshuffle=False) 141 | assert d.card_exists(one) 142 | 143 | assert not d.card_exists(one_alt) 144 | assert not d.card_exists(Card('Five')) 145 | 146 | 147 | def test_card_exist_string(): 148 | d = Deck(cards=['a', 'b', 'c', 'd']) 149 | assert d.card_exists('a') 150 | assert not d.card_exists('z') 151 | 152 | 153 | def test_shuffle(): 154 | d = Deck() 155 | with pytest.raises(NoCards): 156 | d.shuffle() 157 | 158 | 159 | def test_discard(): 160 | d = Deck(cards=[ 161 | Card('One'), Card('Two'), Card('Three'), Card('Four') 162 | ], reshuffle=False) 163 | d.discard(Card('One')) 164 | d.discard(Card('Two')) 165 | assert d.discarded == 2 166 | with pytest.raises(NotACard): 167 | d.discard(False) 168 | assert d.discarded == 2 169 | d.discard(0) 170 | assert d.discarded == 3 171 | 172 | 173 | def test_deck_discard(): 174 | cardlist = [ 175 | Card('One'), Card('Two'), Card('Three'), Card('Four') 176 | ] 177 | discardpile = Deck(reshuffle=False) 178 | d = Deck(cards=cardlist[:], reshuffle=False, discard=discardpile) 179 | d.discard(d.draw()) 180 | d.discard(d.draw()) 181 | assert d.discarded == 2 182 | assert len(discardpile) == 2 183 | assert discardpile[1] == cardlist[0] 184 | d.shuffle_back() 185 | assert d.discarded == 0 186 | assert len(d) == 4 187 | assert len(discardpile) == 0 188 | 189 | 190 | def test_shuffle_back(): 191 | d = Deck(cards=[ 192 | Card('One'), Card('Two'), Card('Three'), Card('Four') 193 | ], reshuffle=False) 194 | d.draw() 195 | d.draw() 196 | assert len(d) == 2 197 | d.discard(Card('One')) 198 | d.discard(Card('Two')) 199 | d.shuffle_back() 200 | assert len(d) == 4 201 | 202 | 203 | # noinspection PyProtectedMember 204 | def test_add_single(): 205 | d = Deck(cards=[ 206 | Card('One'), Card('Two'), Card('Three'), Card('Four') 207 | ], reshuffle=False) 208 | d.add_single(Card('Five')) 209 | assert len(d) == 5 210 | six = Card('Six') 211 | d.add_single(six, position=0) 212 | assert len(d) == 6 213 | assert d._cards[0] == six 214 | 215 | 216 | def test_add_many(): 217 | d = Deck(cards=[ 218 | Card('One'), Card('Two'), Card('Three'), Card('Four') 219 | ], reshuffle=False) 220 | d.add_many([Card('Five'), Card('Six')]) 221 | assert len(d) == 6 222 | 223 | 224 | def test_show_top_one(): 225 | d = Deck(cards=[ 226 | Card('One'), Card('Two'), Card('Three'), Card('Four') 227 | ], reshuffle=False) 228 | assert d.show_top(1)[0].__dict__ == Card('One').__dict__ 229 | 230 | 231 | def test_show_top_three(): 232 | d = Deck(cards=[ 233 | Card('One'), Card('Two'), Card('Three'), Card('Four') 234 | ], reshuffle=False) 235 | assert d.show_top(3)[0].__dict__, Card('One').__dict__ 236 | assert d.show_top(3)[1].__dict__, Card('Two').__dict__ 237 | assert d.show_top(3)[2].__dict__, Card('Three').__dict__ 238 | 239 | 240 | def test_cards_left(): 241 | d = Deck(cards=[ 242 | Card('One'), Card('Two'), Card('Three'), Card('Four') 243 | ], reshuffle=False) 244 | assert d.cards_left == 4 245 | 246 | 247 | def test_cards_left_empty(): 248 | d = Deck() 249 | assert d.cards_left == 0 250 | 251 | 252 | def test_discarded(): 253 | d = Deck(cards=[ 254 | Card('One'), Card('Two'), Card('Three'), Card('Four') 255 | ], reshuffle=False) 256 | d.discard(d.draw()) 257 | d.discard(d.draw_bottom()) 258 | assert d.discarded, 2 259 | 260 | 261 | def test__repr__(): 262 | d = Deck() 263 | assert 'Deck(cards=0, discarded=0, reshuffle=True, name=None)', repr(d) 264 | d = Deck(cards=[Card('One'), Card('Two'), Card('Three')], reshuffle=False, name='Deck') 265 | d.discard(Card('Four')) 266 | assert 'Deck(cards=3, discarded=1, reshuffle=False, name=Deck)', repr(d) 267 | 268 | 269 | def test__str__named(): 270 | d = Deck(name='SuperDeck') 271 | assert 'SuperDeck' == str(d) 272 | 273 | 274 | def test__str__(): 275 | d = Deck() 276 | assert 'Deck of cards' == str(d) 277 | 278 | 279 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 280 | def test_yaml_import_export_ints(): 281 | d = Deck(cards=[1, 2, 3, 4], name="Test yaml int") 282 | d.export(" yaMl ", to_file=True, location="tests/temp/test_int.yaml") 283 | e = Deck() 284 | e.load("tests/temp/test_int.yaml", is_file=True) 285 | assert e._cards == d._cards 286 | assert e._reshuffle == d._reshuffle 287 | assert e.name == d.name 288 | 289 | 290 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 291 | def test_yaml_import_export_strings(): 292 | d = Deck(cards=["one", "two", "three", 'four', '5', '6'], name="Test yaml str") 293 | d.export("YAML", to_file=True, location="tests/temp/test_str.yaml") 294 | e = Deck() 295 | e.load("tests/temp/test_str.yaml", is_file=True) 296 | assert e._cards == d._cards 297 | assert d._save_location == e.file_location 298 | assert e._reshuffle == d._reshuffle 299 | assert e.name == d.name 300 | 301 | 302 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 303 | def test_yaml_import_export_base_card(): 304 | d = Deck(cards=[BaseCard("One"), BaseCard("Two"), BaseCard("Three"), BaseCard("Four")], 305 | name="Test yaml basecard") 306 | d.export("yaml ", to_file=True, location="tests/temp/test_basecard.yaml") 307 | e = Deck() 308 | e.load("tests/temp/test_basecard.yaml", is_file=True) 309 | assert e._cards[0].__dict__ == d._cards[0].__dict__ 310 | assert e._reshuffle == d._reshuffle 311 | assert e.name == d.name 312 | 313 | 314 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 315 | def test_yaml_import_export_custom_card(): 316 | d = Deck(cards=[ExportCard("One"), ExportCard("Two"), ExportCard("Three")], name="Test yaml custom") 317 | d.export(" YaML", to_file=True, location="tests/temp/test_custom.yaml") 318 | e = Deck(name="This will get overridden") 319 | e.load("tests/temp/test_custom.yaml", is_file=True) 320 | assert e._cards == d._cards 321 | assert e._reshuffle == d._reshuffle 322 | assert e.name == d.name 323 | 324 | 325 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 326 | def test_json_import_export_ints(): 327 | d = Deck(cards=[1, 2, 3, 4], name="Test json int") 328 | d.export(" Json ", to_file=True, location="tests/temp/test_int.json") 329 | e = Deck() 330 | e.load("tests/temp/test_int.json", is_file=True) 331 | assert e._cards == d._cards 332 | assert e._reshuffle == d._reshuffle 333 | assert e.name == d.name 334 | 335 | 336 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 337 | def test_json_import_export_strings(): 338 | d = Deck(cards=["one", "two", "three", 'four', '5', '6'], name="Test json str") 339 | d.export("jsON", to_file=True, location="tests/temp/test_str.json") 340 | e = Deck() 341 | e.load("tests/temp/test_str.json", is_file=True) 342 | assert e._cards == d._cards 343 | assert d._save_location == e.file_location 344 | assert e._reshuffle == d._reshuffle 345 | assert e.name == d.name 346 | 347 | 348 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 349 | def test_json_import_export_base_card(): 350 | d = Deck(cards=[BaseCard("One"), BaseCard("Two"), BaseCard("Three"), BaseCard("Four")], 351 | name="Test json basecard") 352 | d.export("jSon ", to_file=True, location="tests/temp/test_basecard.json") 353 | e = Deck() 354 | e.load("tests/temp/test_basecard.json", is_file=True) 355 | assert e._cards[0].__dict__ == d._cards[0].__dict__ 356 | assert e._reshuffle == d._reshuffle 357 | assert e.name == d.name 358 | 359 | 360 | # noinspection PyProtectedMember,PyProtectedMember,PyProtectedMember,PyProtectedMember 361 | def test_json_import_export_custom_card(): 362 | d = Deck(cards=[ExportCard("One"), ExportCard("Two"), ExportCard("Three")], name="Test json custom") 363 | d.export(" json", to_file=True, location="tests/temp/test_custom.json") 364 | e = Deck(name="This will get overridden") 365 | e.load("tests/temp/test_custom.json", is_file=True) 366 | assert e._cards == d._cards 367 | assert e._reshuffle == d._reshuffle 368 | assert e.name == d.name 369 | 370 | 371 | # noinspection PyProtectedMember,PyProtectedMember 372 | def test_import_standard(): 373 | d = Deck() 374 | d.load("pyCardDeck/standard_deck.yml", is_file=True) 375 | e = Deck() 376 | e.load_standard_deck() 377 | assert e._cards == d._cards 378 | 379 | 380 | def test_load_exception(): 381 | d = Deck() 382 | with pytest.raises(UnknownFormat): 383 | d.load("") 384 | 385 | 386 | def test_export_exception(): 387 | d = Deck() 388 | with pytest.raises(UnknownFormat): 389 | d.export("") 390 | 391 | 392 | def test_json(): 393 | d = Deck() 394 | d.load_standard_deck() 395 | prop = d.json 396 | export = d.export("json") 397 | assert prop == export 398 | 399 | 400 | def test_yaml(): 401 | d = Deck() 402 | d.load_standard_deck() 403 | prop = d.yaml 404 | export = d.export("yaml") 405 | assert prop == export 406 | -------------------------------------------------------------------------------- /pyCardDeck/deck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import os 6 | import yaml 7 | import jsonpickle 8 | # noinspection PyCompatibility 9 | # because we are installing it through pip 10 | from typing import List, Union, Optional, Iterator 11 | from random import shuffle, randint, randrange 12 | from .errors import OutOfCards, NotACard, NoCards, CardNotFound, UnknownFormat 13 | from .cards import CardType 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | class Deck: 19 | """ 20 | Deck you will be using. Make sure to create the instance somewhere reachable :) 21 | 22 | :param cards: | Use this parameter if you don't plan to register your cards another way 23 | | Cards can be either an instance of a object, string or an integer, 24 | | the documentation will be calling this :ref:`CardType` (because of Python's rank hinting) 25 | :type cards: List[:ref:`CardType`] 26 | :param reshuffle: Set reshuffle to false if you want your deck not to reshuffle after it's depleted 27 | :type reshuffle: bool 28 | :param name: Name of the deck, used when converting the Deck instance into string 29 | :type name: string 30 | :param discard: optional Deck object to use as discard pile 31 | :type discard: Union[Deck, None] 32 | """ 33 | 34 | def __init__(self, cards: Optional[List[CardType]] = None, reshuffle: object = True, name: str = None, 35 | discard: Optional['Deck'] = None): 36 | """ 37 | Create the deck 38 | """ 39 | 40 | self.name = name 41 | if cards is None: 42 | self._cards = [] # type: List[CardType] 43 | else: 44 | self._cards = cards 45 | if discard is None: 46 | self._discard_pile = [] # type: Union[Deck, List[CardType]] 47 | else: 48 | self._discard_pile = discard 49 | self._reshuffle = reshuffle 50 | self.set_file_location("exported_deck") 51 | 52 | def _get_card(self, position: str = "top") -> CardType: 53 | """ 54 | Helper function for drawing from the deck. Shouldn't be used 55 | 56 | :param position: Where to draw from 57 | :type position: str 58 | :return: Drawn card 59 | :rtype: :ref:`CardType` 60 | :raises OutOfCards: when there are no cards in the deck 61 | :raises NoCards: when the deck runs out of cards (no reshuffle) 62 | """ 63 | if len(self._cards): 64 | positions = { 65 | "top": 0, 66 | "bottom": len(self._cards) - 1, 67 | "random": randrange(len(self._cards)) 68 | } 69 | card = self._cards.pop(positions[position]) 70 | self.reshuffle_if_empty() 71 | log.debug('Card drawn from %s: %s', (position, card)) 72 | return card 73 | 74 | elif not self._reshuffle: 75 | log.debug('You tried to draw. No more cards to be drawn. Position: %s', position) 76 | raise OutOfCards('You tried to draw. No more cards to be drawn. Position: %s', position) 77 | 78 | else: 79 | log.debug('You tried to draw from an empty deck. Position: %s', position) 80 | raise NoCards('You tried to draw from an empty deck. Position: %s', position) 81 | 82 | def draw(self) -> CardType: 83 | """ 84 | Draw the topmost card from the deck 85 | 86 | :return: Card from the list 87 | :rtype: :ref:`CardType` 88 | :raises OutOfCards: when there are no cards in the deck 89 | :raises NoCards: when the deck runs out of cards (no reshuffle) 90 | """ 91 | return self._get_card("top") 92 | 93 | def draw_bottom(self) -> CardType: 94 | """ 95 | Draw the bottommost card from the deck 96 | 97 | :return: Card from the list 98 | :rtype: :ref:`CardType` 99 | :raises OutOfCards: when there are no cards in the deck 100 | :raises NoCards: when the deck runs out of cards (no reshuffle) 101 | """ 102 | return self._get_card("bottom") 103 | 104 | def draw_random(self) -> CardType: 105 | """ 106 | Draw a random card from the deck 107 | 108 | :return: Card from the list 109 | :rtype: :ref:`CardType` 110 | :raises OutOfCards: when there are no cards in the deck 111 | :raises NoCards: when the deck runs out of cards (no reshuffle) 112 | """ 113 | return self._get_card("random") 114 | 115 | # noinspection PyUnboundLocalVariable 116 | def draw_specific(self, specific_card: CardType) -> CardType: 117 | """ 118 | Draw a specific card from the deck 119 | 120 | .. note:: 121 | 122 | For card instances to match, they should have `__eq__` method 123 | set to compare their equality. If you don't want to set those up, 124 | make sure their `__dict__` are the same and their name is the same. 125 | 126 | If you are using a string or an integer, don't worry about this! 127 | 128 | :param specific_card: Card identical to the one you are looking for 129 | :type specific_card: :ref:`CardType` 130 | :return: Card from the list 131 | :rtype: :ref:`CardType` 132 | :raises OutOfCards: when there are no cards in the deck 133 | :raises NoCards: when the deck runs out of cards (no reshuffle) 134 | :raises CardNotFound: when the card is not found in the deck 135 | """ 136 | log.debug('Attempting to find card: %s', specific_card) 137 | if len(self._cards): 138 | for available_card in self._cards: 139 | if _card_compare(specific_card, available_card): 140 | card = available_card 141 | break 142 | else: 143 | log.debug('Specific card not found in the deck') 144 | raise CardNotFound('Specific card not found in the deck') 145 | self._cards.remove(card) 146 | self.reshuffle_if_empty() 147 | log.debug('Specific card drawn: %s', card) 148 | return card 149 | 150 | else: 151 | log.debug('You tried to draw a specific card from an empty deck') 152 | raise NoCards('You tried to draw a specific card from an empty deck') 153 | 154 | def card_exists(self, card: CardType) -> bool: 155 | """ 156 | Checks if a card exists in the deck 157 | 158 | .. note:: 159 | 160 | For card instances to match, they should have `__eq__` method 161 | set to compare their equality. If you don't want to set those up, 162 | make sure their `__dict__` are the same and their name is the same. 163 | 164 | If you are using a string or an integer, don't worry about this! 165 | 166 | :param card: Card identical to the one you are looking for 167 | :type card: :ref:`CardType` 168 | :return: | True if exists 169 | | False if doesn't exist 170 | :rtype: bool 171 | """ 172 | found = False 173 | for available_card in self._cards: 174 | if _card_compare(card, available_card): 175 | found = True 176 | break 177 | log.debug('Card %s exists in the deck: %s', card, found) 178 | return found 179 | 180 | def shuffle(self) -> None: 181 | """ 182 | Randomizes the order of cards in the deck 183 | 184 | :raises NoCards: when there are no cards to be shuffled 185 | """ 186 | if len(self._cards): 187 | shuffle(self._cards) 188 | log.debug('Deck shuffled') 189 | else: 190 | log.warning('You tried to shuffle an empty deck') 191 | raise NoCards('You tried to shuffle an empty deck') 192 | 193 | def reshuffle_if_empty(self) -> None: 194 | """ 195 | Function that checks if the deck is out of cards and if reshuffle is true, it 196 | shuffles the discard pile back into the card pile 197 | """ 198 | if not len(self._cards) and self._reshuffle: 199 | self.shuffle_back() 200 | 201 | def shuffle_back(self) -> None: 202 | """ 203 | Shuffles the discard pile back into the main pile 204 | """ 205 | for card in self._discard_pile: 206 | self._cards.append(card) 207 | self.shuffle() 208 | if isinstance(self._discard_pile, Deck): 209 | self._discard_pile.clear() 210 | else: 211 | self._discard_pile = [] 212 | log.debug('Cards have been shuffled back from the discard pile') 213 | 214 | def discard(self, card: CardType) -> None: 215 | """ 216 | Puts a card into the discard pile 217 | 218 | :param card: Card to be discarded 219 | :type card: :ref:`CardType` 220 | :raises NotACard: When you try to insert False/None into a discard pile 221 | """ 222 | log.debug("Card being discarded: %s", card) 223 | if card or type(card) == int: 224 | if isinstance(self._discard_pile, Deck): 225 | self._discard_pile.add_single(card, 0) 226 | else: 227 | self._discard_pile.append(card) 228 | log.debug('Card %s discarded', card) 229 | # This had a reason, I remember testing something and ending 230 | # up with False/None in discard_pile - if anyone knows what 231 | # that was, let me know! 232 | else: 233 | log.warning('You tried to insert %s (rank(%s) into a discard pile', 234 | card, type(card).__name__) 235 | raise NotACard('You tried to insert {} (rank({}) into a discard pile' 236 | .format(card, type(card).__name__)) 237 | 238 | def clear(self) -> None: 239 | """ 240 | Empties the deck, destroying contents 241 | """ 242 | self._cards = [] 243 | 244 | def add_single(self, card: CardType, position: int = False) -> None: 245 | """ 246 | Shuffles (or inserts) a single card into the active deck 247 | 248 | :param card: Card you want to insert 249 | :type card: :ref:`CardType` 250 | :param position: | If you want to let player insert card to a specific location, use position 251 | | where 0 = top of the deck, 1 = second card from top etc. 252 | | By default the position is random 253 | :type position: int 254 | """ 255 | if position is not False: 256 | self._cards.insert(position, card) 257 | log.debug("Card %s inserted to position %i", card, position) 258 | log.debug(self._cards) 259 | else: 260 | self._cards.insert(randint(0, len(self._cards)), card) 261 | log.debug('Card %s shuffled into the deck', card) 262 | 263 | def add_many(self, cards: List[CardType]) -> None: 264 | """ 265 | Shuffles a list of cards into the deck 266 | 267 | :param cards: Cards you want to shuffle in 268 | :type cards: List[:ref:`CardType`] 269 | """ 270 | for card in cards: 271 | self.add_single(card) 272 | log.debug('New cards shuffled into the deck') 273 | 274 | def show_top(self, number: int) -> List[CardType]: 275 | """ 276 | Selects the top X cards from the deck without drawing them 277 | 278 | Useful for mechanics like scry in Magic The Gathering 279 | 280 | If there are less cards left than you want to show, it will show 281 | only the remaining cards 282 | 283 | :param number: How many cards you want to show 284 | :type number: int 285 | :return: Cards you want to show 286 | :rtype: List[:ref:`CardType`] 287 | """ 288 | return self._cards[0:number] 289 | 290 | def set_file_location(self, location) -> None: 291 | """ 292 | Used to update the location 293 | It will expand relative file path and ~ if it exists 294 | The file path should be relative to the main script 295 | A filename must be passed in as part of location 296 | 297 | Example: ~/mydeck.yml 298 | 299 | :param location: the location of the file 300 | """ 301 | 302 | # I expanded the user and relative file paths so it's an absolute file path 303 | self._save_location = os.path.abspath(os.path.expanduser(location)) 304 | 305 | def export(self, fmt: str, to_file: bool = False, location: str = None) -> str: 306 | """ 307 | Export the deck. By default it returns string with either JSON or YaML, 308 | but if you set `to_file=True`, you can instead save the deck as a file. 309 | If no location (with filename) is provided, it'll save to the folder the script 310 | is opened from as `exported_deck` without an extension. 311 | 312 | :param fmt: Desired format, either YaML or JSON 313 | :type fmt: str 314 | :param to_file: Whether you want to get a string back or save to a file 315 | :type to_file: bool 316 | :param location: Where you want to save your file - include file name! 317 | :type location: str 318 | :raises UnknownFormat: When entered format is not supported 319 | :return: Your exported deck as a string in your desired format 320 | :rtype: str 321 | """ 322 | 323 | # Eliminate some human error 324 | format_stripped = fmt.lower().strip() 325 | 326 | # Find out where we want to export, if we do 327 | if location: 328 | self.set_file_location(location) 329 | # if we don't know where, let's make a default export file 330 | else: 331 | self.set_file_location("exported_deck") 332 | 333 | # Hide location for security from exported deck 334 | temp_location = self._save_location 335 | self._save_location = None 336 | 337 | # Get the actual exported data 338 | exported = _get_exported_string(format_stripped, self) 339 | 340 | # Restore saved value 341 | self._save_location = temp_location 342 | 343 | if to_file: 344 | with open(self._save_location, 'w') as target_file: 345 | target_file.writelines(exported) 346 | log.debug("File exported to: %s", self._save_location) 347 | 348 | # We return the string either way, because why not? 349 | return exported 350 | 351 | def load(self, to_load: str, is_file: bool = False) -> None: 352 | """ 353 | Way to override a deck instance with a saved deck from either yaml, JSON 354 | or a file with either of those. 355 | 356 | The library will first try to check if you have a save location saved, then verifies 357 | if the file exists as a path to a file. If it doesn't, it'l assume it's a string with one of 358 | the supported formats and will load from those. 359 | 360 | :param to_load: | This should be either a path to a file or a string containing 361 | | json/yaml generated by Deck.export(). It's not safe to trust your users 362 | | with this, as they can provide harmful pickled JSON (see jsonpickle docs for more) 363 | :type to_load: str 364 | :param is_file: whether to_load is a file path or actual data. Default is False 365 | :type is_file: bool 366 | :raises UnknownFormat: When the entered yaml or json is not valid 367 | """ 368 | # I tried auto identifying but it didn't work, everything could be a file so it would always accept it 369 | if is_file: 370 | self.set_file_location(to_load) 371 | with open(self._save_location, 'r') as file: 372 | loadable = file.read() 373 | else: 374 | loadable = to_load 375 | # This is not really an elegant solution, but it works 376 | # noinspection PyBroadException 377 | try: 378 | result = jsonpickle.decode(loadable) 379 | log.debug("loading JSON") 380 | # When we try to catch a specific exception (JSONDecodeError), it doesn't exist in 3.3 and 3.4 381 | except Exception: 382 | result = yaml.load(loadable) 383 | log.debug("loading YAML") 384 | try: 385 | del result.__dict__["_save_location"] 386 | self.__dict__.update(result.__dict__) 387 | except AttributeError: 388 | raise UnknownFormat 389 | 390 | def load_standard_deck(self) -> None: 391 | """ 392 | Loads a standard deck of 52 cards into the deck 393 | """ 394 | location = os.path.join(os.path.dirname(__file__), "standard_deck.yml") 395 | 396 | # removing save location so it is not overwritten 397 | data = yaml.load(open(location)).__dict__ 398 | del data["_save_location"] 399 | self.__dict__.update(data) 400 | 401 | @property 402 | def cards_left(self) -> int: 403 | """ 404 | Cards left in the deck 405 | 406 | :return: Number of cards in the deck 407 | :rtype: int 408 | """ 409 | if len(self._cards): 410 | return len(self._cards) 411 | else: 412 | return 0 413 | 414 | @property 415 | def discarded(self) -> int: 416 | """ 417 | Cards in the discard pile 418 | 419 | :return: Number of cards in the discard pile 420 | :rtype: int 421 | """ 422 | return len(self._discard_pile) 423 | 424 | @property 425 | def json(self) -> str: 426 | """ 427 | Alternative to Deck.export("json") 428 | 429 | :return: jsonpickled Deck 430 | :rtype: str 431 | """ 432 | return self.export("json", to_file=False) 433 | 434 | @property 435 | def yaml(self) -> str: 436 | """ 437 | Alternative to Deck.export("yaml") 438 | 439 | :return: yaml dump of the Deck 440 | :rtype: str 441 | """ 442 | return self.export("yaml") 443 | 444 | @property 445 | def empty(self) -> bool: 446 | """ 447 | :return: Whether the deck is empty 448 | :rtype: bool 449 | """ 450 | if len(self._cards): 451 | return False 452 | else: 453 | return True 454 | 455 | @property 456 | def file_location(self) -> str: 457 | """ 458 | Returns the file location of which current is loaded from/ will be saved to 459 | 460 | :return: file location 461 | :rtype: str 462 | """ 463 | return self._save_location 464 | 465 | def __repr__(self) -> str: # pragma: no cover 466 | """ 467 | Used for representation of the object 468 | 469 | called with repr(Deck) 470 | 471 | :return: 'Deck of cards' 472 | :rtype: string 473 | """ 474 | return 'Deck(cards={0}, discarded={3}, reshuffle={1}, name={2})' \ 475 | .format(self.cards_left, self._reshuffle, self.name, self.discarded) 476 | 477 | def __str__(self) -> str: 478 | """ 479 | Used for representation of the object for humans 480 | 481 | called with str(Deck) 482 | 483 | This method is also called when you are providing arguments to str.format(), you can just provide 484 | your Deck instance and it will magically know the name, yay! 485 | 486 | :return: Name of the deck if it has a name or 'Deck of cards' if it has none 487 | :rtype: string 488 | """ 489 | 490 | if self.name: 491 | return self.name 492 | else: 493 | return 'Deck of cards' 494 | 495 | def __len__(self) -> int: 496 | """ 497 | Instead of doing len(Deck.cards) you can just check len(Deck) 498 | 499 | It's however recommended to use the :py:attr:`cards_left` attribute 500 | 501 | :return: Number of cards left in the deck 502 | :rtype: int 503 | """ 504 | return len(self._cards) 505 | 506 | def __getitem__(self, position: int) -> CardType: 507 | """ 508 | For more pythonic usage 509 | 510 | Allows for slicing and other things like random.choice(Deck) 511 | """ 512 | return self._cards[position] 513 | 514 | def __setitem__(self, position: int, card: CardType) -> None: 515 | """ 516 | For more pythonic usage 517 | 518 | Allows for things like random.shuffle(Deck) 519 | """ 520 | self._cards[position] = card 521 | 522 | def __iter__(self) -> Iterator[CardType]: 523 | """ 524 | For faster pythonic iteration protected against internal changes 525 | """ 526 | return iter(self._cards) 527 | 528 | 529 | def _card_compare(card: CardType, second_card: CardType) -> bool: 530 | """ 531 | Function for comparing two cards. First it checks their `__eq__`, 532 | if that returns False, it checks `__dict__` and name of the Class 533 | that spawned them . 534 | 535 | :param card: First card to match 536 | :type card: :ref:`CardType` 537 | :param second_card: Second card to match 538 | :type second_card: :ref:`CardType` 539 | :return: Whether they are the same 540 | :rtype: bool 541 | """ 542 | identity = False 543 | if second_card == card: 544 | identity = True 545 | else: 546 | # For comparing instances of objects that have different IDs and don't 547 | # have their __eq__ method set up to work when comparing. 548 | try: 549 | if second_card.__dict__ == card.__dict__ \ 550 | and type(second_card).__name__ == type(card).__name__: 551 | identity = True 552 | except AttributeError: 553 | pass 554 | return identity 555 | 556 | 557 | def _get_exported_string(format_stripped: str, deck: Deck) -> str: 558 | """ 559 | Helper function to Deck.export() 560 | 561 | :param format_stripped: Desired format stripped of any spaces and lowercase 562 | :type format_stripped: str 563 | :param deck: instance of a Deck 564 | :type deck: :ref:`Deck` 565 | :return: YAML/JSON string of the deck 566 | :rtype: str 567 | :raises UnknownFormat: when it doesn't recognize format_stripped 568 | """ 569 | if format_stripped == "yaml" or format_stripped == "yml": 570 | exported = yaml.dump(deck) 571 | log.debug("Exported deck %r to a yaml string", deck) 572 | elif format_stripped == "json": 573 | exported = jsonpickle.encode(deck) 574 | log.debug("Exported deck %r to a json string", deck) 575 | else: 576 | log.debug("Unknown format: %s", format) 577 | raise UnknownFormat("Unknown format: {}".format(format)) 578 | return exported 579 | --------------------------------------------------------------------------------