ink-playing-cards

Published on January 11, 2025

ink-playing-cards featured image

ink-playing-cards is a React-based framework that enables developers to create engaging card games that run in the terminal.

By combining the power of React‘s component model with Ink‘s terminal rendering capabilities, this library provides a flexible and extensible foundation for building everything from simple card games to complex trading card game systems.

The Challenge

Creating card games for the terminal presents unique challenges. While graphical card games can rely on rich visual feedback and complex UI interactions, terminal-based games need to work within the constraints of character-based display and limited input methods. Additionally, card games often require complex state management, event handling, and rule systems that need to be both flexible and maintainable.

The Solution

To address these challenges, I developed ink-playing-cards with several key design principles in mind:

  1. Component-Based Architecture: Leveraging React’s component model for reusable and composable game elements
  2. Flexible State Management: Using a zone-based system for tracking card locations and game state
  3. Event-Driven Design: Implementing a robust event system for handling game actions and effects
  4. Terminal-Optimized UI: Provide comprehensive out-of-box kit for Ink terminal apps

Core Features

Component Library

The library provides a set of carefully designed components optimized for terminal display:

import { Card, MiniCard, CardStack } from 'ink-playing-cards'

const GameBoard = () => (
  <Box flexDirection="column">
    <CardStack cards={deckCards} name="Deck" />
    <Box>
      {handCards.map((card) => (
        <MiniCard
          key={card.id}
          {...card}
          // variant="mini" // or "micro" for tighter spaces
          faceUp
        />
      ))}
    </Box>
  </Box>
)
TypeScript

Zone System

The zone system provides a structured approach to managing game state:

const { deck, hand, discardPile, playArea } = useDeck()

// Move a card from hand to play area
const playCard = (card) => {
  hand.removeCard(card)
  playArea.addCard(card)
}
TypeScript

Event System

The event system enables complex game interactions and rule implementation:

useEffect(() => {
  eventManager.addEventListener('CARD_DRAWN', handleCardDrawn)
  eventManager.addEventListener('CARD_PLAYED', handleCardPlayed)

  return () => {
    eventManager.removeEventListener('CARD_DRAWN', handleCardDrawn)
    eventManager.removeEventListener('CARD_PLAYED', handleCardPlayed)
  }
}, [])
TypeScript

Effect System

The effect system allows for the implementation of complex card abilities and game mechanics:

const healEffect = {
  apply: (gameState, amount) => {
    gameState.playerHealth += amount
  },
}

// Apply effect when card is played
effectManager.applyCardEffects(card, gameState, { amount: card.amount })
TypeScript

Technical Implementation

Architecture Overview

The library is built on several key technologies:

  • React for component management and state handling
  • Ink for terminal rendering
  • TypeScript for type safety and developer experience
  • Event Emitter for the event system

Key Design Decisions

  1. Terminal-First Design

    • Optimized component rendering for character-based display
    • Multiple card display options (simple, ASCII, minimal, mini, micro) for space efficiency
    • Careful consideration of color and Unicode character support
  2. Flexible State Management

    • Zone-based system for intuitive card management
    • Event-driven architecture for decoupled components
    • Effect system for extensible game mechanics
  3. Developer Experience

    • Comprehensive TypeScript definitions
    • Intuitive APIs for common card game operations
    • Extensive documentation and examples

Example Games

The library includes several example implementations showcasing its capabilities:

Getting Started

Bash
npm install ink-playing-cards
Bash
yarn add ink-playing-cards
Bash
pnpm install ink-playing-cards

Basic usage example:

import React from 'react'
import { Box, Text } from 'ink'
import { DeckProvider, useDeck, Card } from 'ink-playing-cards'

const Game = () => {
  const { deck, hand, draw, shuffle } = useDeck()

  React.useEffect(() => {
    shuffle()
    draw(5, 'player1')
  }, [])

  return (
    <Box flexDirection="column">
      <CardStack cards={deck.cards} name="Deck" />
      <Text>Your hand:</Text>
      <CardStack cards={hand.cards} name="Hand" faceUp maxDisplay={5} />
    </Box>
  )
}

const App = () => (
  <DeckProvider>
    <Game />
  </DeckProvider>
)
TypeScript

Technical Challenges and Solutions

Challenge 1: Terminal Space Constraints

The limited space in terminal environments required careful consideration of component sizing and layout. The solution was to create multiple card components, each with a variant enum prop

  • Card Component: Full-size card display with simple, ascii, and minimal variants
  • MiniCard Component: Supporting both mini and micro variants

Challenge 2: State Management

Managing complex game states across multiple zones required a robust yet flexible system. The zone-based approach provides:

  • Clear ownership of cards
  • Intuitive movement operations
  • Event-driven updates
  • Type-safe interactions

Challenge 3: Effect Implementation

Implementing card effects in a terminal environment required creative solutions for visual feedback and state management:

  • Event-based effect triggering
  • Async effect resolution
  • Visual state indicators
  • Effect queueing system

Future Development

The library continues to evolve with planned features including:

  1. Multiplayer Support

    • Network play capabilities
    • Synchronized game state
    • Player interaction systems
  2. Enhanced Visual Effects

    • More animation options
    • Additional card styles
    • Custom effect visualizations
  3. Advanced Game Features

    • AI player support
    • Save/load game state
    • Replay system

Conclusion

ink-playing-cards demonstrates how modern web technologies can be adapted to create engaging terminal-based applications. Hopefully, it enables developers to focus on game logic and creativity rather than low-level implementation details if/when they are curious about building games or card-based interfaces for Ink terminal applications.

The project showcases the power of component-based architecture and event-driven design in creating flexible, maintainable systems, even within the constraints of terminal-based interfaces.

Resources and Documentation

Return to Projects Page

griffen.codes

made with 💖 and

2025 © all rights are reserved | updated 10 seconds ago

Footer Background Image