npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@irithell-js/uno

v1.0.0

Published

Complete UNO card game engine with AI players

Readme

UNO Game Engine

A fully-featured, production-ready UNO card game engine built with TypeScript. Zero dependencies for core gameplay, with modular architecture supporting human players, AI opponents, and persistent game storage.

Features

  • Complete UNO Rules - All official rules including +2, +4, Skip, Reverse, and Wild cards
  • AI Opponents - Intelligent AI players with strategic decision-making
  • Human Player Support - Interactive gameplay with move validation
  • Game Persistence - Save and restore games using SQLite3
  • Zero Core Dependencies - Minimal external libraries for game logic
  • Extensible Design - Easy to add new player types or storage backends
  • ESM Support - Modern ES module exports
  • CJS Support - Full support for CJS uses

Installation

npm install @irithell-js/uno

Quick Start

Basic Usage (ESM)

import { GameEngine } from "@irithell-js/uno";

// Create a new game with human and AI players
const game = new GameEngine({
  players: [
    { name: "Irithell", type: "HUMAN" },
    { name: "AI-SkyNet", type: "AI" },
    { name: "AI-R2-D2", type: "AI" },
  ],
  language: "pt-BR", // Portuguese or "en-US" for English
});

// Get game state
const state = game.getState();
console.log(`Current player: ${state.currentPlayer}`);
console.log(`Top card: ${state.topCard.display}`);

// Process player turn
const result = game.processHumanTurn("Player 1", {
  action: "play_card",
  cardIndex: 0,
  chosenColor: "red", // Required for Wild cards
});

if (result.success) {
  console.log("Card played successfully!");
}

// Process AI turns
while (
  !game.gameOver &&
  game.players[game.currentPlayerIdx].name !== "Player 1"
) {
  game.processAITurn();
}

Using GameStorage

import { GameEngine, GameStorage } from "@irithell-js/uno";

const storage = new GameStorage();
const game = new GameEngine({
  players: [
    { name: "Irithell", type: "HUMAN" },
    { name: "AI-SkyNet", type: "AI" },
    { name: "AI-R2-D2", type: "AI" },
  ],
});

// Save game
storage.saveGame(game.id, game);

// List saved games
const gameIds = storage.listGames();
console.log("Saved games:", gameIds);

// Load game
const loaded = storage.loadGame(game.id);
if (loaded) {
  console.log("Game restored!");
}

// Get game info
const info = storage.getDetailedGameInfo(game.id);
console.log(`Players: ${info.playerNames.join(", ")}`);
console.log(`Turn: ${info.turnNumber}`);

Architecture

GameEngine

Main game logic orchestrator.

const game = new GameEngine(options?, savedState?);

// Properties
game.id; // Unique game identifier
game.players; // Array of Player instances
game.deck; // Current deck
game.discardPile; // Cards played
game.currentPlayerIdx; // Current player index
game.direction; // 1 (normal) or -1 (reversed)
game.currentColor; // Active color (for Wild cards)
game.gameOver; // Game status
game.winner; // Winning player or null

// Methods
game.getState(turnLog?); // Get current game state
game.processHumanTurn(playerName, data); // Process human player action
game.processAITurn(); // Process AI player turn
game.toSavedState(); // Serialize for storage

Player Types

HumanPlayer - Receives explicit move commands.

const player = new HumanPlayer("Alice");
player.drawCard(deck, 1);
const valid = player.hand[0].isValid(topCard, currentColor);

AIPlayer - Makes strategic decisions autonomously.

const aiPlayer = new AIPlayer("Computer");
const move = aiPlayer.chooseMove(
  topCard,
  currentColor,
  allPlayers,
  idx,
  direction,
);
// Returns either "draw" or a Card object to play

Card System

const card = new Card("red", "5");
card.display; // "5 Vermelho"
card.toString(); // Serialization format
card.toJSON(); // Object representation

// Special cards
new Card("Especial", "Wild"); // Wild card
new Card("Especial", "Wild +4"); // Wild +4
new Card("Red", "+2"); // Draw 2
new Card("Blue", "Pular"); // Skip
new Card("Blue", "reverse"); // Reverse

Cards assets

import { CardImageResolver, Card } from "../dist/index.mjs";
import { existsSync } from "fs";
const card = new Card("Vermelho", "3");
const paths = card.toJSON();
console.log(
  "Absolute:",
  paths.imageAbsolutePath,
  existsSync(paths.imageAbsolutePath),
);
console.log(
  "CWD relative:",
  CardImageResolver.getCwdRelativePath("Vermelho", "3"),
  existsSync("../dist/assets/cards/vermelho_3.png"),
);
console.log(
  "Relative dist:",
  CardImageResolver.getRelativePath("Vermelho", "3"),
);

Deck Management

const deck = new Deck();
deck.shuffle();
const card = deck.drawCard();
deck.cardsRemaining();
deck.refill(discardPile); // Reshuffle used cards

GameStorage

Persistent storage using SQLite3.

const storage = new GameStorage(dbPath?);

// Save game
storage.saveGame(gameId, gameEngine);

// Load game
const loaded = storage.loadGame(gameId);

// List all games
const ids = storage.listGames();

// Get game details
const info = storage.getDetailedGameInfo(gameId);

// Clean up
storage.close();

Game State

The getState() method returns comprehensive game information:

interface GameState {
  gameId: string;
  turnNumber: number;
  gameOver: boolean;
  winner: string | null;
  currentPlayer: string;
  nextPlayer: string;
  playerOrder: string[];
  currentColor: CardColor;
  topCard: CardJSON;
  deckCardsRemaining: number;
  players: PlayerInfo[];
  turnLog: TurnLog[];
}

interface PlayerInfo {
  name: string;
  cardsInHand: number;
  isUno: boolean;
  hand?: CardJSON[]; // Only for current human player
  validMoves?: number[]; // Indices of playable cards
}

Configuration

GameOptions

interface GameOptions {
  players: Array<{
    name: string;
    type: "HUMAN" | "AI";
  }>;
  language?: "pt-BR" | "en-US"; // Default: "pt-BR"
}

// Create game
const game = new GameEngine({
  players: [
    { name: "Irithell", type: "HUMAN" },
    { name: "AI-SkyNet", type: "AI" },
    { name: "AI-R2-D2", type: "AI" },
  ],
  language: "en-US",
});

Storage Configuration

// Use default database (./uno_games.db)
const storage = new GameStorage();

// Specify custom database path
const storage = new GameStorage("./my-games.db");

Turn Processing

Human Turn

const result = game.processHumanTurn("Player Name", {
  action: "play_card",
  cardIndex: 0,
  chosenColor: "red", // Only for Wild cards
});

// Or draw card
const result = game.processHumanTurn("Player Name", {
  action: "draw_card",
});

// Result structure
interface TurnResult {
  success: boolean;
  log?: TurnLog[];
  error?: string;
}

AI Turn

const log = game.processAITurn();

if (log) {
  console.log(`${log.player} played: ${log.card || "drew card"}`);
}

// Result
interface TurnLog {
  player: string;
  action: "PLAY_CARD" | "DRAW_CARD";
  card?: string;
  quantity?: number;
}

Game Rules Implementation

Card Effects

  • +2 (Draw 2) - Next player draws 2 cards and skips turn
  • Curinga +4 (Wild +4) - Player chooses color, next player draws 4 cards and skips turn
  • Pular (Skip) - Skip next player's turn
  • Inverter (Reverse) - Reverse play direction
  • Curinga (Wild) - Player chooses any valid color

Valid Moves

A card is valid if:

  • It matches the current card's color, OR
  • It matches the current card's value, OR
  • It is a Wild card

Win Condition

Player wins when they play their last card and no cards remain in hand.

Deck Management

  • Initial deck: 108 cards (4 colors × 25 cards + 4 Wilds + 4 Wild+4)
  • Discard pile reshuffled into deck when empty
  • Game continues until winner found

API Reference

GameEngine

constructor(options?: GameOptions, savedState?: SavedGameState)

Create new game or restore from saved state.

getState(turnLog?: TurnLog[]): GameState

Get current game state snapshot.

processHumanTurn(playerName: string, data: PlayData): TurnResult

Process human player action. Returns success/error result.

processAITurn(): TurnLog | null

Process AI player's automatic turn. Returns action log or null if game over.

toSavedState(): SavedGameState

Serialize game for storage.

GameStorage

constructor(dbPath?: string)

Initialize storage with optional custom database path.

saveGame(gameId: string, game: GameEngine): void

Save game state to database.

loadGame(gameId: string): GameEngine | null

Load game from database or null if not found.

listGames(): string[]

Get array of all saved game IDs.

getDetailedGameInfo(gameId: string): GameInfo

Get metadata about saved game (players, turn, winner, dates).

close(): void

Close database connection.

Supported Languages

  • pt-BR - Portuguese (Brazil) - Default
  • en-US - English (US)

All card names, error messages, and prompts are translated.

Database Schema

SQLite3 database with games table:

CREATE TABLE games (
  game_id TEXT PRIMARY KEY,
  game_state TEXT NOT NULL,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
)

Query saved games:

sqlite3 uno_games.db "SELECT * FROM games;"

Performance

Typical operation times:

| Operation | Time | | -------------------------- | ------- | | Create new game | <1ms | | Get game state | <1ms | | Save game | 2-5ms | | Load game | 3-8ms | | AI turn decision | 5-20ms | | Batch AI turns (3 players) | 15-60ms |

Error Handling

try {
  const result = game.processHumanTurn("Player", {
    action: "play_card",
    cardIndex: 0,
  });

  if (!result.success) {
    console.error(result.error);
  }
} catch (error) {
  console.error("Fatal error:", error.message);
}

Common errors:

  • "errors.playerRequired" - No players provided
  • "errors.notYourTurn" - Wrong player attempted move
  • "errors.invalidCardIndex" - Card index out of range
  • "errors.invalidMove" - Card cannot be played
  • "errors.colorRequired" - Wild card color not specified
  • "errors.notEnoughCardsToRefill" - Deck empty and cannot refill

Troubleshooting

Database File Not Found

# Check database location
ls -la uno_games.db

### Games Not Persisting

Ensure database directory is writable:

```bash
chmod 755 .

AI Takes Too Long

License

MIT

Changelog

1.0.0 (Latest)

  • Initial release
  • Complete UNO game implementation
  • AI player system with strategic decision-making
  • SQLite3 persistent storage
  • Full TypeScript support
  • Full CJS and ESM support
  • Portuguese and English translations
  • Comprehensive debug logging