Skip to main content

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 types
  • SimpleCard — freeform rank/suit card for rapid prototyping
  • PlayingCard — immutable standard 52-card card (with jokers)
    • Subclass to make custom cards with mutable attributes
  • Rank / Suit enums with Unicode symbols
  • StandardPlayingCards — 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

🎲 Dice

  • Die — standard N-sided die
  • Dice — collection of dice
  • WeightedDie — rolls with custom side probabilities
  • ExplodingDie — 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.deque for 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cardlib-1.1.0.tar.gz (18.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cardlib-1.1.0-py3-none-any.whl (16.4 kB view details)

Uploaded Python 3

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

Hashes for cardlib-1.1.0.tar.gz
Algorithm Hash digest
SHA256 1467a2e224fb7bd78a18d40beb008c3d6c86f9a205313999228832a70451863a
MD5 1b3c81c7feed0afdd8689f76975ff2de
BLAKE2b-256 ea5a00299b087f51365713681c9f842d2f6409b58d7f76698a2207cb7267baf8

See more details on using hashes here.

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

Hashes for cardlib-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d04bf3367d6248901adf8d3a539021fa4a665a799ecff1ed1a01d0c7af4cdd0c
MD5 58d3d63b7faa896b0af6e64d3640a785
BLAKE2b-256 316b8f11936dcd60c7843dfc90c52770e35471770a14327c2688e8305ce56ad8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page