Ink playing cards
Personal skills marketplace — add it once, install any plugin for terminal, devops... and whatever else comes up
npx -y skills add gfargo/skills --skill ink-playing-cardsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when building terminal card games with ink-playing-cards, working with card components, deck management, zone systems, event systems, effect systems, or any card game logic using Ink and React for CLI rendering.
SKILL.md
13.2 KB, as published. Nobody here has run it
ink-playing-cards v1.0.0
React component library for terminal-based card games with Ink 6 (React 19 for CLIs). Provides UI components, hooks, contexts, and game systems.
npm install ink-playing-cards
# deps: ink ^6.0.0, react ^19.0.0 — requires Node >= 20
Architecture
All state flows through DeckProvider (React context + useReducer). Zones are immutable TCard[] arrays. The useDeck hook wraps dispatch.
DeckProvider
├── zones.deck: TCard[]
├── zones.hands: Record<string, TCard[]>
├── zones.discardPile: TCard[]
├── zones.playArea: TCard[]
├── players: string[]
├── eventManager / effectManager
└── dispatch(DeckAction)
Quick Start
import React from 'react'
import { render, Box, Text, useInput } from 'ink'
import { DeckProvider, useDeck, useHand, CardStack } from 'ink-playing-cards'
const Game = () => {
const { deck, shuffle, draw } = useDeck()
const { hand, playCard, discard } = useHand('player1')
React.useEffect(() => { shuffle(); draw(5, 'player1') }, [])
useInput((input) => { if (input === 'd') draw(1, 'player1') })
return (
<Box flexDirection="column">
<Text>Deck: {deck.length}</Text>
<CardStack cards={hand} name="Hand" isFaceUp maxDisplay={5} />
</Box>
)
}
render(<DeckProvider><Game /></DeckProvider>)
Types
type TCard = CardProps | CustomCardProps // every card has a unique `id`
type BaseCardProps = {
id: string; effects?: CardEffect[]; faceUp?: boolean; selected?: boolean; rounded?: boolean
}
type CardProps = BaseCardProps & {
suit: 'hearts' | 'diamonds' | 'clubs' | 'spades'
value: '2' | '3' | ... | 'K' | 'A' | 'JOKER'
theme?: AsciiTheme // 'original' | 'geometric' | 'animal' | 'robot' | 'pixel' | 'medieval'
}
type CustomCardProps = BaseCardProps & {
size?: 'micro' | 'mini' | 'small' | 'medium' | 'large' // or explicit width/height
title?: string; cost?: string // header row
asciiArt?: string; artColor?: string // art region
typeLine?: string // between art and body
description?: string // body (auto-wraps)
footerLeft?: string; footerRight?: string // footer row
symbols?: CustomCardSymbol[] // corner symbols
content?: ReactNode // freeform mode — overrides all regions
back?: CustomCardBack // { art?, symbol?, color?, label? }
borderColor?: string; textColor?: string
value?: TCardValue | string; type?: string // game logic metadata (not rendered)
onClick?: () => void
}
// Type guards
function isStandardCard(card: TCard): card is CardProps
function isCustomCard(card: TCard): card is CustomCardProps
function generateCardId(suit: TSuit, value: TCardValue): string // "hearts-A-<rand6>"
Hooks
useDeck — must be inside <DeckProvider>
const {
// State
deck, // TCard[]
hands, // Record<string, TCard[]>
discardPile, // TCard[]
playArea, // TCard[]
players, // string[]
backArtwork, // { ascii, simple, minimal }
eventManager, // EventManager
effectManager, // EffectManager
// Actions
shuffle, // () => void
draw, // (count, playerId) => void — auto-registers player
deal, // (count, playerIds[]) => void — auto-registers players
reset, // (cards?) => void
cutDeck, // (index) => void
addPlayer, // (id) => void
removePlayer, // (id) => void
getPlayerHand, // (id) => TCard[]
addCustomCard, // (card: CustomCardProps) => void
removeCustomCard, // (cardId) => void
setBackArtwork, // (Partial<BackArtwork>) => void
} = useDeck()
draw() and deal() dispatch actions — they do NOT return cards. Access drawn cards via hands[playerId] after re-render.
useHand(playerId) — convenience wrapper
const { hand, drawCard, playCard, discard } = useHand('player1')
// hand: TCard[]
// drawCard(count = 1) — draws from deck
// playCard(cardId: string) — hand → playArea
// discard(cardId: string) — hand → discardPile
Components
Card — standard playing card
Variants: simple (11×9, default), ascii (15×13, themed face cards), minimal (6×5).
<Card id="h-A-1" suit="hearts" value="A" variant="simple" faceUp selected rounded theme="original" />
MiniCard — compact card
Variants: mini (5×4, default), micro (4×4). Same props as CardProps plus variant.
<MiniCard id="s-K-1" suit="spades" value="K" variant="mini" faceUp />
UnicodeCard — single character
Unicode playing card characters. Auto-colors red/white by suit.
<UnicodeCard suit="hearts" value="A" faceUp bordered rounded size={1} dimmed color="red" />
CustomCard — non-standard cards
Two modes: structured (title/cost/art/typeLine/description/footer regions) or freeform (content ReactNode).
Size presets: micro (5×3), mini (8×5), small (12×7), medium (18×11, default), large (24×15). Override with width/height.
<CustomCard id="c1" size="large" title="Dragon" cost="{4}{R}"
asciiArt={art} typeLine="Creature" description="Flying"
footerLeft="5/5" borderColor="red" />
<CustomCard id="c2" content={<Text>Freeform</Text>} />
<CustomCard id="c3" faceUp={false} back={{ art: '♠♠♠', color: 'cyan' }} />
TarotCard — tarot deck cards
Wraps CustomCard with tarot-specific semantics. Uses a discriminated union on arcana for Major vs Minor Arcana. Renders a 20×13 card with themed defaults.
Standard 78-card tarot deck: 22 Major Arcana + 56 Minor Arcana (14 per suit × 4 suits).
Tarot suits: wands (🜂), cups (☽), swords (⚔), pentacles (⛤).
Minor values: Ace–10 + Page, Knight, Queen, King.
import { TarotCard, createTarotDeck } from 'ink-playing-cards'
// Major Arcana (index 0–21: The Fool through The World)
<TarotCard id="fool" arcana="major" majorIndex={0} />
<TarotCard id="tower" arcana="major" majorIndex={16} reversed />
// Minor Arcana
<TarotCard id="ace-cups" arcana="minor" suit="cups" value="Ace" />
<TarotCard id="qw" arcana="minor" suit="wands" value="Queen" reversed />
// Face down with custom tarot back
<TarotCard id="hidden" arcana="major" majorIndex={13} faceUp={false} />
// Custom styling
<TarotCard id="devil" arcana="major" majorIndex={15}
borderColor="red" textColor="red" artColor="red" />
Props (Major): arcana="major", majorIndex (0–21), reversed?, asciiArt?, borderColor?, textColor?, artColor?, back?, plus all BaseCardProps.
Props (Minor): arcana="minor", suit (TarotSuit), value (TarotMinorValue), reversed?, asciiArt?, borderColor?, textColor?, artColor?, back?, plus all BaseCardProps.
Types: TarotCardProps, TarotMajorProps, TarotMinorProps, TarotSuit, TarotMinorValue, MajorArcanaIndex.
Defaults: Major Arcana uses yellow border / magenta art. Minor Arcana uses cyan border / cyan art. All 22 Major Arcana have built-in ASCII art. Minor pip cards auto-generate suit symbol art. reversed shows "⟳ Reversed" on the type line.
CardStack — overlapping card list
Renders TCard[] with overlap. Handles both standard and custom cards.
<CardStack cards={hand} name="Hand" isFaceUp maxDisplay={5}
variant="simple" stackDirection="horizontal"
spacing={{ overlap: -2, margin: 1 }} alignment="start" />
Defaults: isFaceUp: false, maxDisplay: 3, stackDirection: 'vertical', variant: 'simple'.
CardGrid — grid layout (standard cards only)
import { CardGrid, type GridCard } from 'ink-playing-cards'
// GridCard = { id: string, suit: TSuit, value: TCardValue }
<CardGrid rows={4} cols={4} cards={gridCards} variant="simple"
isFaceUp fillEmpty spacing={{ row: 1, col: 1, margin: 1 }} />
Deck — visual deck display
Must be inside DeckProvider. Shows top card + placeholder.
<Deck variant="simple" showTopCard placeholderCard={{ suit: 'hearts', value: 'A' }} />
Contexts
DeckProvider
<DeckProvider initialCards={myCards} customReducer={myReducer}>
<Game />
</DeckProvider>
Default deck: 52 cards via createStandardDeck(). Pass customReducer to extend — note the built-in deckReducer is NOT exported; replicate needed cases or use Zones utilities.
GameProvider — turn management (separate from DeckProvider)
<GameProvider initialPlayers={['alice', 'bob']} customReducer={myGameReducer}>
<DeckProvider><Game /></DeckProvider>
</GameProvider>
// Access: React.useContext(GameContext)
const { currentPlayerId, players, turn, phase, dispatch } = React.useContext(GameContext)
dispatch({ type: 'NEXT_TURN' }) // next player, turn++
dispatch({ type: 'SET_PHASE', payload: 'playing' })
dispatch({ type: 'SET_CURRENT_PLAYER', payload: 'bob' })
Event System
Events dispatch automatically from the reducer. Subscribe via eventManager from useDeck().
const { eventManager } = useDeck()
const listener = {
handleEvent(event: GameEventData) { /* event.type, .playerId, .card, .cards, .count */ }
}
eventManager.addEventListener('CARDS_DRAWN', listener)
eventManager.removeEventListener('CARDS_DRAWN', listener)
eventManager.dispatchEvent({ type: 'CUSTOM_EVENT', playerId: 'p1' }) // custom events OK
eventManager.removeAllListeners()
Built-in events: DECK_SHUFFLED, CARDS_DRAWN (playerId, cards), CARDS_DEALT (playerId, cards, count — per player), CARD_PLAYED (playerId, card), CARD_DISCARDED (playerId, card), DECK_RESET, DECK_CUT.
Effect System
Attach abilities to cards. All implement CardEffect.apply(gameState, eventData).
import { Effects } from 'ink-playing-cards'
new Effects.DrawCardEffect(2) // deck → hand
new Effects.DamageEffect(3) // target.life or .health
new Effects.ConditionalEffect(condFn, effect) // fires when condition true
new Effects.TriggeredEffect('CARD_PLAYED', effect) // fires on event match
new Effects.DelayedEffect(3, effect) // fires after N turns
new Effects.TargetedEffect(selectorFn, effect) // selects target from gameState
new Effects.ContinuousEffect(condFn, applyFn, removeFn) // toggle on condition
Effects.attachEffectToCard(card, effect) // pushes to card.effects[]
effectManager.applyCardEffects(card, gameState, eventData) // runs all effects on card
Effects mutate gameState/eventData directly — designed for effect pipelines, not reducer use.
Zone Utilities
import { Zones, createStandardDeck, createPairedDeck, createTarotDeck } from 'ink-playing-cards'
// Pure functions (immutable, safe for reducers)
Zones.shuffleCards(cards) // Fisher-Yates, new array
Zones.drawCards(cards, count) // [drawn, remaining] — draws from end
Zones.addCard(cards, card) // append
Zones.addCards(cards, newCards) // append multiple
Zones.removeCard(cards, cardId) // filter by id
Zones.findCard(cards, cardId) // find by id
Zones.cutDeck(cards, index) // split and reorder
// Deck creation
createStandardDeck() // 52 cards with unique IDs
createPairedDeck(shuffle?) // paired deck for Memory games
createTarotDeck() // 78-card tarot deck (22 Major + 56 Minor Arcana)
// Legacy class API (mutable): Zones.Deck, Zones.Hand, Zones.DiscardPile, Zones.PlayArea
// Each has addCard(), removeCard(), shuffle(). Deck also has drawCard(), drawCards(n).
DeckAction / GameAction (for custom reducers)
type DeckAction =
| { type: 'SHUFFLE' }
| { type: 'DRAW'; payload: { count: number; playerId: string } }
| { type: 'DEAL'; payload: { count: number; playerIds: string[] } }
| { type: 'RESET'; payload?: { cards?: TCard[] } }
| { type: 'CUT_DECK'; payload: number }
| { type: 'PLAY_CARD'; payload: { playerId: string; cardId: string } }
| { type: 'DISCARD'; payload: { playerId: string; cardId: string } }
| { type: 'ADD_PLAYER'; payload: string }
| { type: 'REMOVE_PLAYER'; payload: string }
| { type: 'ADD_CUSTOM_CARD'; payload: CustomCardProps }
| { type: 'REMOVE_CUSTOM_CARD'; payload: { cardId: string } }
| { type: 'SET_BACK_ARTWORK'; payload: Partial<BackArtwork> }
type GameAction =
| { type: 'SET_CURRENT_PLAYER'; payload: string }
| { type: 'NEXT_TURN' }
| { type: 'SET_PHASE'; payload: string }
Gotchas
draw()/deal()are async via dispatch — cards appear inhands[playerId]on next render, not as return values.- Every card needs a unique
id. UsegenerateCardId()or your own scheme. useHand.playCard(cardId)anddiscard(cardId)take a string ID, not a card object.isStandardCard(card)type guard is required before accessing.suit/.valueon aTCard.- The built-in
deckReduceris not exported. Custom reducers receive(DeckContextType, DeckAction)and must handle all cases or delegate toZonesutilities. DeckProviderandGameProviderare separate contexts — use both for full game state.