Reusable card and dice library for text-based games
Project description
♥️♠️ cardlib ♦️♣️
A lightweight, extensible Python library for cards and dice,
built for text-based games, simulations, and custom card systems.
Features 🎯
🃏 Cards
Card(abstract) — base interface for all card typesSimpleCard— freeform rank/suit card for rapid prototypingPlayingCard— immutable standard 52-card card (with jokers)- Subclass to make custom cards with mutable attributes
Rank/Suitenums with Unicode symbolsStandardPlayingCards— predefined constants (e.g.,ACE_SPADES)
📇 Decks
-
Deck(base class) — generic, expandable deck container- Rich API with clear docstrings
- Enforces card type via
CARD_TYPE - Full Python container protocol (
len(),in, indexing, slicing, iteration) - Provides shuffling, dealing, copying, splitting, rotating, adding/removing
- Fast filtering and functional-style methods (
filter,remove_if,count) - Arithmetic operations (
+,-,*,/,//,<<,>>) - Internal helpers streamline subclassing
-
PlayingCardDeck— standard 52-card deck implementation- Only accepts
PlayingCard - Optional jokers
- Customizable suit/rank order
- Built-in card comparison and sorting based on deck rules
- Only accepts
🎲 Dice
Die— standard N-sided dieDice— collection of diceWeightedDie— rolls with custom side probabilitiesExplodingDie— rerolls on max side, sums total
📦 Installation
pip install cardlib
- Installs the library into
site-packages - Ready to import from anywhere
🃏 Deck Example
from cardlib import PlayingCardDeck, Suit, Rank
# Create a standard shuffled deck (include jokers if desired)
deck = PlayingCardDeck(include_jokers=True)
# Draw cards
hand = deck.draw(5)
print('Dealt hand:', hand)
# Add cards back
deck.add(hand, position='bottom')
# Split loosely into 3 piles
p1, p2, p3 = deck / 3
# Rotate (cut)
deck << 10 # move top 10 cards to bottom
# Filter only hearts
deck.filter(lambda c: c.suit == Suit.HEARTS)
# Sort using this deck's rank/suit ordering
deck.sort(descending=True)
🎲 Dice Example
from cardlib import Die, Dice, ExplodingDie
# Roll a single d6
d6 = Die(6)
print('d6:', d6.roll())
# Roll a dice pool
dice_pool = Dice(6, 6, 8)
print('Pool total:', dice_pool.roll())
# Exploding die (re-roll on max)
xd6 = ExplodingDie(6)
print('Exploding roll:', xd6.roll(), 'explosions:', xd6.explosions)
📐 Advanced Deck Operations
| Operation | Description |
|---|---|
deck.copy(deep=True) |
Clone deck (optionally deep copy cards) |
deck.reset(shuffle=True) |
Restore to new-deck order |
deck.deal(num_hands, cards_each) |
Deal cards into hands |
deck.add(*cards, position='top/bottom/random') |
Add cards dynamically |
deck.remove_if(lambda c: c.rank == Rank.ACE) |
Remove all Aces |
deck.compare(a, b) |
Compare two cards using this deck's rules |
deck / 3 |
Split loosely into 3 piles |
deck // 2 |
Split strictly into 2 even halves |
deck << n / deck >> n |
Rotate (in-place) |
deck1 + deck2 |
Merge decks |
deck - Suit.HEARTS |
Remove all hearts |
- Use
help(Deck)for the full docs.
📎 Deck Notes
- Internally uses
collections.dequefor O(1) top/bottom operations. - Slicing and arbitrary indexing are less efficient O(n).
deck[0]is the "top" for card insertion.- Fully subclassable — build any card system you want.
🛠️ Extending cardlib: Build Your Own Cards & Decks!
from collections import deque
from cardlib import Card, Deck
# Let's create our own monster cards
class MonsterCard(Card):
def __init__(self, name, level, element):
self.name = name
self.level = level
self.element = element
def __str__(self):
return f'Lvl.{self.level} {self.element} {self.name}'
Deck Subclass Quick Guide
- There are 7 key deck methods to override.
| Method or Attribute | Description | Frequency |
|---|---|---|
CARD_TYPE |
A type (or tuple of types) that the deck will accept. Dictates type checking for methods that add cards. |
Common |
_build_deck() |
Change how new decks are generated. Return a collections.deque filled with cards. |
Common |
__init__(self, *, **kwargs) |
Add instance attributes like custom rules or counters. | Uncommon |
_empty_clone() |
Return a copy of this deck with no ._cards set. Used by methods that create a deck from self (e.g., copy, split, +, -, *) All attributes of self are copied by default. Override to control which instance attributes are NOT copied, and how the other ones are. For instance, deep copy lists or reset counters. |
Rare |
value(card) |
Return the numeric value of a card. Not used internally. | Uncommon |
_cmp_key(card) |
Return a comparable object to represent this card's sort value. Used internally by sort() and compare(). |
Common |
compare(a, b) |
Return -1, 0, or 1 if card a is less than, equal to, or greater than card b. Uses _cmp_key if not overridden. Useful when you want different behavior for sorting and comparing cards. |
Uncommon |
# Next, we will create a custom deck
class MonsterDeck(Deck):
CARD_TYPE = MonsterCard # We will enforce type checking when adding cards
# Let's define all monsters with their base strength
# This applies for all instances
MONSTERS = {
'Wolf': 0,
'Skeleton': 1,
'Pirate': 2,
'Giant': 3,
'Vampire': 5,
'Dragon': 7,
}
# We also define some elemental types
ELEMENTS = ('Ancient', 'Fire', 'Nightmare', 'Holy')
def _build_deck(self):
cards = deque()
for monster in self.MONSTERS.keys(): # 6 monsters
for element in self.ELEMENTS: # 4 elements
for level in range(1, 7): # 6 levels
cards.append(MonsterCard(monster, level, element))
return cards # 6*4*6 = 144-card deck
# Let's say we wanted a deck instance attribute
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.fights = 0 # Here, we track how many times `compare()` is called
def _empty_clone(self):
clone = super()._empty_clone() # Copy all attributes from self
# Now, we can choose how instance attributes are handled when deck is copied
clone.fights = 0 # Reset for clones
return clone
def value(self, card):
# Base strength + level
return self.MONSTERS[card.name] + card.level
def _cmp_key(self, card):
# Sort cards first by level, then element, then name
return (card.level, card.element, card.name)
def compare(self, c1, c2):
self.fights += 1
strength_1 = self.value(c1)
strength_2 = self.value(c2)
'''
Let's implement a type-advantage cycle:
Ancient -> Fire -> Nightmare -> Holy -> Ancient
for a +3 strength bonus.
'''
element_i1 = self.ELEMENTS.index(c1.element)
element_i2 = self.ELEMENTS.index(c2.element)
# If the index of the enemy is 1 greater than mine (wrapped), gain 3 strength
if (element_i1 + 1) % 4 == element_i2:
strength_1 += 3
elif (element_i2 + 1) % 4 == element_i1:
strength_2 += 3
# The card with the greater total strength is the greater card
# Return positive if c1 > c2, 0 if c1 = c2, or negative if c1 < c2
return strength_1 - strength_2
Now we can do interesting things like:
deck = MonsterDeck(shuffle=True)
# Deal 2 hands of 5 cards
hand_1, hand_2 = deck.deal(2, 5)
# Sort the hands based on deck ordering: (level, element, name)
deck.sort_cards(hand_1)
deck.sort_cards(hand_2)
# Let the highest level monster from each hand FIGHT!
x = hand_1[-1]
y = hand_2[-1]
outcome = deck.compare(x, y) # deck.fights == 1
if outcome > 0:
print('x is the winner!')
elif outcome < 0:
print('y has defeated x!')
else:
print('The monsters have tied!')
# Split the remaining deck into 2 new decks
p1, p2 = deck / 2 # p1.fights == 0, since we overrode _empty_clone()
# Print the first 10 cards of pile 1
p1.show(10, sep='\n')
# Send those cards to the bottom of the pile
p1 << 10
🏆 You can continue to:
- Add more card types
- Expand your deck rules and behaviors
- Construct game logic to make a unique card game
📄 License
MIT License © Jeffrey Ray
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cardlib-1.1.0.tar.gz.
File metadata
- Download URL: cardlib-1.1.0.tar.gz
- Upload date:
- Size: 18.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1467a2e224fb7bd78a18d40beb008c3d6c86f9a205313999228832a70451863a
|
|
| MD5 |
1b3c81c7feed0afdd8689f76975ff2de
|
|
| BLAKE2b-256 |
ea5a00299b087f51365713681c9f842d2f6409b58d7f76698a2207cb7267baf8
|
File details
Details for the file cardlib-1.1.0-py3-none-any.whl.
File metadata
- Download URL: cardlib-1.1.0-py3-none-any.whl
- Upload date:
- Size: 16.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d04bf3367d6248901adf8d3a539021fa4a665a799ecff1ed1a01d0c7af4cdd0c
|
|
| MD5 |
58d3d63b7faa896b0af6e64d3640a785
|
|
| BLAKE2b-256 |
316b8f11936dcd60c7843dfc90c52770e35471770a14327c2688e8305ce56ad8
|