Skip to content
Back to skills

Ink Playing Cards

ASecurity

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.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 3, 2026
ai-agentsgobashreactnodeawsapi

Works with

  • terminal
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned September 3, 2026

npx -y skills add gfargo/skills --skill ink-playing-cards --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ink Playing Cards?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ink Playing Cards
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gfargo-ink-playing-cards/badge)](https://www.skillsdirectory.com/skills/gfargo-ink-playing-cards)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ink-playing-cards
description: "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."
---

# 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.

```bash
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.

```text
DeckProvider
├── zones.deck: TCard[]
├── zones.hands: Record<string, TCard[]>
├── zones.discardPile: TCard[]
├── zones.playArea: TCard[]
├── zones.custom: Record<string, TCard[]>   // arbitrary named zones (tableau, foundation, ...)
├── players: string[]
├── eventManager / effectManager
└── dispatch(DeckAction)
```

Custom zones are addressed by name via `moveCard`/`setZone`/`clearZone`/`getZone` — useful for games (solitaire, etc.) that need more structure than deck/hand/discard/play. See `MOVE_CARD`/`SET_ZONE`/`CLEAR_ZONE` below.

## Quick Start

```tsx
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

```ts
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)
}

// 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>`

```ts
const {
  // State
  deck,          // TCard[]
  hands,         // Record<string, TCard[]>
  discardPile,   // TCard[]
  playArea,      // TCard[]
  customZones,   // Record<string, TCard[]> — named zones, keyed by name
  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
  moveCard,         // (cardId, from, to, position?) => void — move between any two zones
  setZone,          // (name, cards) => void — seed/overwrite a custom zone
  clearZone,        // (name) => void — remove a custom zone
  getZone,          // (name) => TCard[] — read any zone (built-in or custom) by name
} = useDeck()
```

`draw()` and `deal()` dispatch actions — they do NOT return cards. Access drawn cards via `hands[playerId]` after re-render.

### useHand(playerId) — convenience wrapper

```ts
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).

```tsx
<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`.

```tsx
<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.

```tsx
<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`.

```tsx
<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`.

```tsx
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.

```tsx
<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)

```tsx
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.

```tsx
<Deck variant="simple" showTopCard placeholderCard={{ suit: 'hearts', value: 'A' }} />
```

## Contexts

### DeckProvider

```tsx
<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)

```tsx
<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()`.

```ts
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), `CARD_MOVED` (card, from, to), `DECK_RESET`, `DECK_CUT`.

## Effect System

Attach abilities to cards. All implement `CardEffect.apply(gameState, eventData)`.

```ts
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

```ts
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)

```ts
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: 'MOVE_CARD'; payload: { cardId: string; from: ZoneName; to: ZoneName; position?: 'top' | 'bottom' | number } }
  | { type: 'SET_ZONE'; payload: { name: string; cards: TCard[] } }
  | { type: 'CLEAR_ZONE'; payload: { name: string } }

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.

Files in this skill

  • .source-version7 B
  • SKILL.md14.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.