likewinter / card-deck
Card deck games library
Requires
- php: ^8.4
Requires (Dev)
- carthage-software/mago: ^1.45
- pestphp/pest: ^4.2
- phpstan/phpstan: ^2.0
README
A PHP 8.4+ engine for building card games — the primitives, not the UI.
likewinter/card-deck provides game-agnostic building blocks for playing
card games: cards, decks, stacks, a table, rank/suit ordering,
trick-taking primitives, deck builders, wildcards, and face-down state.
You bring the rules; the framework brings the table.
Five reference games prove the primitives fit real, non-trivial games: Poker (hand ranking and tiebreakers), Blackjack (additive scoring with soft/hard aces), Spades (trick-taking with trump), Solitaire (face-down tableau management), and JokerPoker (wildcard substitution with jokers).
Requirements
- PHP 8.4 or newer
Install
composer require likewinter/card-deck
Quick start
use Likewinter\CardDeck\{DeckBuilder, Stack, Table}; // Build a standard 52-card deck and shuffle it $deck = DeckBuilder::standard52()->build(); $table = new Table(deck: $deck, shuffle: true); // Deal 5 cards to each of 3 players $table->addHand('alice', new Stack(capacity: 5)); $table->addHand('bob', new Stack(capacity: 5)); $table->addHand('carol', new Stack(capacity: 5)); $table->drawAll(5); echo "Alice: {$table->hand('alice')}\n"; // Alice: A♣,K♦,Q♥,J♠,10♣
What's included
| Primitive | Purpose |
|---|---|
Card, Rank, Suit |
Identity of a playing card |
PlayableCard |
Interface for anything a Stack can hold (Card, CardInPlay, Wildcard) |
Stack |
Ordered collection of playable cards with capacity |
DeckBuilder |
Fluent factory for standard and custom decks |
Table |
Orchestrates dealing, discarding, and resetting across named hands |
DrawMode |
How Table draws from the deck (Sequential, OneByOne, Random) |
RankOrder |
Game-specific rank values and comparison |
SuitOrder |
Trick-taking: trump and lead-suit rules |
Trick |
One round of play with turn order and winner determination |
PlayerRing |
Rotating turn order (standalone, also used internally by Trick) |
CardInPlay, Face |
Face-up / face-down state |
Wildcard |
Wildcard substitution without mutating cards |
Documentation
- Getting started — install, first deal, the mental model
- Cards, ranks, and suits —
Card,Rank,Suit, string formats - Stacks and decks —
StackAPI, capacity,DeckBuilderoutput - Building decks —
DeckBuilder: standard, short, multi, custom - The table — dealing modes, discarding, resetting
- Rank ordering — why ranks have no intrinsic value,
RankOrderpresets - Trick-taking —
SuitOrder,Trick,PlayerRing - Face-down cards —
CardInPlay,Face, when to use them - Wildcards —
Wildcard, jokers, wild 8s, Canasta - Implementing a game — walk-through using Poker as the reference
Game fit
The framework supports most popular card games. ✅ = directly possible with the current primitives.
| Game | Fit | Notes |
|---|---|---|
| 5-Card Stud Poker | ✅ | Reference implementation in src/Games/Poker/ |
| Texas Hold'em / Omaha | ✅ | Table::drawAll(2) for holes + Stack::takeTop() for community cards; best-5-from-7 evaluator is game logic |
| Blackjack | ✅ | RankOrder::blackjack() + multi-deck via DeckBuilder::times(6) |
| Bridge / Spades / Hearts | ✅ | SuitOrder + Trick + PlayerRing |
| Rummy / Gin Rummy | ✅ | Stack::takeTop() to draw discards, Table::discard() to discard, Stack for melds |
| War | ✅ | CardInPlay + Face::Down for the face-down war cards |
| Crazy Eights | ✅ | Wildcard for wild 8s |
| Euchre | ✅ | DeckBuilder::euchre() + trump primitives |
| Canasta | ✅ | Wildcard + DeckBuilder::standard52WithJokers(4)->times(2) |
| Solitaire (Klondike) | ✅ | CardInPlay + Face::Down for the tableau |
| Pinochle | ✅ | DeckBuilder::pinochle() + custom RankOrder |
| Skat | ✅ | Custom RankOrder (Jacks above Ace) + SuitOrder |
| Belote | ✅ | Custom RankOrder (J=20, 9=14, A=11, …) + SuitOrder |
Design principles
- Game-agnostic core.
Card,Stack,Tableknow nothing about poker, bridge, or blackjack. Game-specific ordering lives inRankOrderandSuitOrder, supplied by the game. - Immutable where it matters.
Card,Rank,Suit,RankOrder,SuitOrder,CardInPlay, andWildcardare immutable. Mutable state (card collections) lives inStackand its subclasses. - Composable, not prescriptive. The framework gives you primitives;
you assemble them into a game. There is no
Gamebase class to extend. - No UI, no I/O, no persistence. Pure domain logic. Rendering, networking, and storage are the consumer's responsibility.
- Honest about limits. Capacity is enforced. Failed moves roll back. Cards can't be lost to a thrown exception.
Reference games
Five reference implementations prove the primitives fit real games:
- Poker —
PokerHand(immutable 5-card value object with classification, flush/straight detection, and full tiebreaker comparison),HandRank(10 ranks, pure enum),Poker(game orchestration: deal, hands, winners, multi-round play) - Blackjack — hand-value game with additive scoring, soft/hard ace logic, multi-deck shoe, dealer AI
- Spades — trick-taking game using
SuitOrder,Trickwith enforced turn order, trick-counting scoring - Solitaire — Klondike solitaire with
face-down tableau columns via
CardInPlay/Face - JokerPoker — 5-card poker with jokers
as wildcards via
Wildcard(assign/unassign,underlyingCard()resolving to the assigned card for classification)
Run the demos:
php demo/poker.php # 3 players, 3 rounds php demo/poker.php 5 1 # 5 players, 1 round php demo/blackjack.php # player vs dealer php demo/spades.php # 4 players, 13 tricks php demo/solitaire.php # Klondike solitaire php demo/joker-poker.php # poker with wild jokers
Testing
Tests cover every primitive and all five reference games:
composer test # Pest test suite composer phpstan # PHPStan level 8 static analysis composer lint # Mago linter composer analyze # Mago static analyzer composer format:check # Mago formatting check composer ci # All of the above + security audit
Changelog
See CHANGELOG.md for notable changes between releases.
License
MIT — see LICENSE.