agentsclimarketplace

Ink playing cards

Skill gfargo/skills/plugins/games/skills/ink-playing-cards

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.From its SKILL.md

Install
npx -y skills add gfargo/skills --skill ink-playing-cards

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things 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.
  • runs commandsInstructs the agent to run 1 command, including `npm install ink-playing-cards`.

SKILL.md

13.2 KB, ~3.7k tokens by cl100k_base, 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: Ace10 + 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 in hands[playerId] on next render, not as return values.
  • Every card needs a unique id. Use generateCardId() or your own scheme.
  • useHand.playCard(cardId) and discard(cardId) take a string ID, not a card object.
  • isStandardCard(card) type guard is required before accessing .suit/.value on a TCard.
  • The built-in deckReducer is not exported. Custom reducers receive (DeckContextType, DeckAction) and must handle all cases or delegate to Zones utilities.
  • DeckProvider and GameProvider are separate contexts — use both for full game state.

What ships with it: 1 file

6 B alongside SKILL.md

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.