indian-rummy-core

Crates.ioindian-rummy-core
lib.rsindian-rummy-core
version0.1.0
created_at2025-08-09 14:54:46.941668+00
updated_at2025-08-09 14:54:46.941668+00
descriptionA high-performance, thread-safe library for Indian Rummy game logic.
homepagehttps://github.com/your-org/indian-rummy-core
repositoryhttps://github.com/your-org/indian-rummy-core
max_upload_size
id1788009
size398,731
(mudont)

documentation

README

Indian Rummy Core

A high-performance Indian Rummy game logic library implemented in Rust with TypeScript bindings for Node.js applications.

Features

  • High Performance: Native Rust implementation for optimal speed
  • TypeScript Support: Full type definitions included
  • Cross-Platform: Supports Windows, macOS, and Linux
  • Complete Game Logic: Full implementation including game state, player management, and tournaments
  • Joker Support: Handles both designated jokers and literal jokers
  • Node.js 22+: Built for modern Node.js environments

Implementation Complete: Both Phase 1 (core card evaluation) and Phase 2 (full game logic) are now implemented as specified in rummy.md.

Installation

npm install indian-rummy-core

Requirements

  • Node.js >= 22.0.0
  • Supported platforms: Windows (x64, arm64), macOS (x64, arm64), Linux (x64, arm64)

Quick Start

Phase 1: Card Evaluation

import { score, isCompletedHand, JsCard } from "indian-rummy-core";

// Define a hand
const hand: JsCard[] = [
  { rank: "A", suit: "S" },
  { rank: "2", suit: "S" },
  { rank: "3", suit: "S" }, // Life sequence
  { rank: "4", suit: "H" },
  { rank: "5", suit: "H" },
  { rank: "6", suit: "H" }, // Another sequence
  { rank: "7", suit: "S" },
  { rank: "7", suit: "H" },
  { rank: "7", suit: "D" }, // Triplet
  { rank: "K", suit: "S" },
  { rank: "K", suit: "H" },
  { rank: "K", suit: "D" },
  { rank: "K", suit: "C" }, // Triplet
];

// Check if hand is completed
const completed = isCompletedHand(hand);
console.log("Hand completed:", completed); // true

// Calculate penalty score
const penaltyScore = score(hand);
console.log("Penalty score:", penaltyScore); // 0 for completed hand

Phase 2: Complete Game

import { JsIndianRummyGame, JsMoveType } from "indian-rummy-core";

// Create a new game
const game = new JsIndianRummyGame(
  ['player1', 'player2'], 
  ['Alice', 'Bob'], 
  1 // number of decks
);

// Get game state
const state = game.getState();
console.log(`${state.nextTurnPlayer}'s turn`);

// Make a move
const move = {
  playerId: state.nextTurnPlayer,
  moveType: JsMoveType.OpenCard,
  cardReceived: game.getTopOpenCard()!,
  cardDiscarded: state.players[0].hand[0],
  didClaimWin: false
};

const result = game.processMove(move);
console.log("Move valid:", result.isValid);

API Reference

Core Types

JsCard

Represents a playing card with rank and suit.

interface JsCard {
  rank: string; // 'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'J' | 'Q' | 'K'
  suit: string; // 'S' (Spades) | 'C' (Clubs) | 'D' (Diamonds) | 'H' (Hearts) | 'J' (Joker)
}

JsCompletedHandResult

Result returned when finding a completed hand from a larger collection.

interface JsCompletedHandResult {
  completedHand: JsCard[];
  remainingCards: JsCard[];
}

Phase 1: Card Evaluation Functions

score(hand: JsCard[], designatedJoker?: JsCard | null): number

Calculate the penalty score for a hand according to Indian Rummy rules.

  • Returns the minimum possible penalty score
  • Jokers don't contribute to penalty scores
  • Completed hands return 0

isCompleteDeck(deck: JsCard[]): boolean

Check if a deck contains all 48 standard playing cards (12 ranks × 4 suits).

isCompletedHand(hand: JsCard[], designatedJoker?: JsCard | null): boolean

Check if a hand is completed according to Indian Rummy rules:

  • Must contain exactly 13 cards
  • All cards grouped into valid sets (sequences or triplets)
  • At least 2 sequences required
  • At least 1 "life" sequence (no jokers) required

completedHandExists(cards: JsCard[], designatedJoker?: JsCard | null): JsCompletedHandResult | null

Find a completed hand from a collection of 13 or more cards.

Returns the optimal 13-card arrangement if possible, along with remaining cards.

Phase 2: Game Management

JsPlayer

Represents a player in the game.

interface JsPlayer {
  id: string;
  name: string;
  hand: JsCard[];
}

JsMoveType

Types of moves a player can make.

enum JsMoveType {
  OpenCard = 'OpenCard',   // Take card from open pile
  CloseCard = 'CloseCard', // Take card from closed pile
  Fold = 'Fold'            // Fold the game
}

JsMove

Represents a move made by a player.

interface JsMove {
  playerId: string;
  moveType: JsMoveType;
  cardReceived?: JsCard;    // Required for OpenCard/CloseCard
  cardDiscarded?: JsCard;   // Required for OpenCard/CloseCard
  didClaimWin: boolean;
}

JsMoveResult

Result of processing a move.

interface JsMoveResult {
  isValid: boolean;
  isWin: boolean;
  winner?: string;
  scores: Record<string, number>;
  errorMessage?: string;
}

JsGameState

Current state of the game.

interface JsGameState {
  players: JsPlayer[];
  designatedJoker: JsCard;
  openPileTop?: JsCard;
  nextTurnPlayer: string;
  isComplete: boolean;
  winner?: string;
  finalScores: Record<string, number>;
}

Game Classes

JsIndianRummyGame

Main game class for managing a complete Indian Rummy game.

class JsIndianRummyGame {
  constructor(playerIds: string[], playerNames: string[], nDecks: number);
  
  // Game state
  getState(): JsGameState;
  isGameComplete(): boolean;
  
  // Move processing
  processMove(gameMove: JsMove): JsMoveResult;
  isValidMove(gameMove: JsMove): boolean;
  
  // Card access
  getTopOpenCard(): JsCard | null;
  getTopClosedCard(): JsCard | null;
  
  // Player access
  getPlayer(playerId: string): JsPlayer | null;
  
  // Serialization
  serialize(): string;
  static deserialize(json: string): JsIndianRummyGame;
}

JsSyndicateGame

Tournament management for multiple games.

class JsSyndicateGame {
  constructor(playerIds: string[]);
  
  // Game management
  addRummyGame(game: JsIndianRummyGame): void;
  getGameCount(): number;
  
  // Scoring
  getPlayerPoints(): Record<string, number>;
  getLeaderboard(): string[][]; // [playerName, points][]
  
  // Serialization
  serialize(): string;
  static deserialize(json: string): JsSyndicateGame;
}

Game Rules

Indian Rummy Basics

  • Objective: Form valid sets and sequences with 13 cards
  • Sets: Groups of 3-4 cards of the same rank with different suits
  • Sequences: Groups of 3+ consecutive cards of the same suit
  • Life: A sequence without any jokers (at least one required)

Jokers

  • Literal Jokers: Cards with suit 'J'
  • Designated Jokers: Any card can be designated as a wild card
  • Usage: Can substitute any card except in life sequences
  • Scoring: Jokers have 0 penalty value

Scoring

  • Numbered cards: Face value (1-9)
  • Face cards: 10 points each (J, Q, K)
  • Aces: 1 point
  • Jokers: 0 points

Examples

Phase 1: Card Evaluation

Basic Hand Validation

import { isCompletedHand, score, JsCard } from "indian-rummy-core";

const validHand: JsCard[] = [
  // Life sequence: A-2-3 of Spades
  { rank: "A", suit: "S" },
  { rank: "2", suit: "S" },
  { rank: "3", suit: "S" },
  // Sequence with joker: 4-5-Joker of Hearts
  { rank: "4", suit: "H" },
  { rank: "5", suit: "H" },
  { rank: "J", suit: "J" },
  // Triplet: 7s
  { rank: "7", suit: "S" },
  { rank: "7", suit: "H" },
  { rank: "7", suit: "D" },
  // Triplet: Kings
  { rank: "K", suit: "S" },
  { rank: "K", suit: "H" },
  { rank: "K", suit: "D" },
  { rank: "K", suit: "C" },
];

console.log(isCompletedHand(validHand)); // true
console.log(score(validHand)); // 0 (completed hand)

Finding Completed Hands

import { completedHandExists } from "indian-rummy-core";

const cards = [
  // 15 cards that include a possible completed hand
  { rank: "A", suit: "S" },
  { rank: "2", suit: "S" },
  { rank: "3", suit: "S" },
  { rank: "4", suit: "H" },
  { rank: "5", suit: "H" },
  { rank: "6", suit: "H" },
  { rank: "7", suit: "S" },
  { rank: "7", suit: "H" },
  { rank: "7", suit: "D" },
  { rank: "K", suit: "S" },
  { rank: "K", suit: "H" },
  { rank: "K", suit: "D" },
  { rank: "K", suit: "C" },
  { rank: "9", suit: "S" },
  { rank: "J", suit: "C" }, // Extra cards
];

const result = completedHandExists(cards);
if (result) {
  console.log("Found completed hand:", result.completedHand);
  console.log("Remaining cards:", result.remainingCards);
}

Working with Designated Jokers

import { score, isCompletedHand } from "indian-rummy-core";

const hand = [
  { rank: "A", suit: "S" },
  { rank: "2", suit: "S" }, // This will be our designated joker
  { rank: "3", suit: "S" },
  // ... rest of hand
];

const designatedJoker = { rank: "2", suit: "S" };

// Score with designated joker
const scoreWithJoker = score(hand, designatedJoker);
const isComplete = isCompletedHand(hand, designatedJoker);

Phase 2: Complete Game Management

Creating and Managing a Game

import { JsIndianRummyGame, JsMoveType } from "indian-rummy-core";

// Create a new game with 3 players
const playerIds = ['player1', 'player2', 'player3'];
const playerNames = ['Alice', 'Bob', 'Charlie'];
const game = new JsIndianRummyGame(playerIds, playerNames, 1);

// Get initial game state
const state = game.getState();
console.log(`Designated joker: ${state.designatedJoker.rank}${state.designatedJoker.suit}`);
console.log(`Next turn: ${state.nextTurnPlayer}`);
console.log(`Open pile top: ${game.getTopOpenCard()?.rank}${game.getTopOpenCard()?.suit}`);

// Check each player's hand
state.players.forEach(player => {
  console.log(`${player.name} has ${player.hand.length} cards`);
});

Processing Player Moves

// Get current player
const currentPlayer = state.players.find(p => p.id === state.nextTurnPlayer)!;

// Create a move to take from open pile
const move = {
  playerId: state.nextTurnPlayer,
  moveType: JsMoveType.OpenCard,
  cardReceived: game.getTopOpenCard()!,
  cardDiscarded: currentPlayer.hand[0], // Discard first card
  didClaimWin: false
};

// Validate and process the move
if (game.isValidMove(move)) {
  const result = game.processMove(move);
  
  if (result.isValid) {
    console.log('Move processed successfully');
    
    if (result.isWin) {
      console.log(`🎉 Winner: ${result.winner}`);
      console.log('Final scores:', result.scores);
    } else {
      console.log('Game continues...');
    }
  } else {
    console.log('Move failed:', result.errorMessage);
  }
}

Player Folding

// Player decides to fold
const foldMove = {
  playerId: 'player2',
  moveType: JsMoveType.Fold,
  didClaimWin: false
};

const foldResult = game.processMove(foldMove);
if (foldResult.isValid) {
  console.log('Player folded, game continues with remaining players');
  
  // Check if game ended due to folding
  if (game.isGameComplete()) {
    const finalState = game.getState();
    console.log('Game ended. Winner:', finalState.winner);
    console.log('Final scores:', finalState.finalScores);
  }
}

Win Declaration

// Player claims a win
const winMove = {
  playerId: state.nextTurnPlayer,
  moveType: JsMoveType.CloseCard,
  cardReceived: game.getTopClosedCard()!,
  cardDiscarded: currentPlayer.hand[1],
  didClaimWin: true // Claiming win!
};

const winResult = game.processMove(winMove);
if (winResult.isWin) {
  console.log(`🎉 Valid win by ${winResult.winner}!`);
  console.log('Final scores:', winResult.scores);
} else if (!winResult.isValid) {
  console.log('Invalid win claim:', winResult.errorMessage);
  // Player gets middle drop penalty for false win claim
}

Tournament Management

Creating and Managing Syndicates

import { JsSyndicateGame } from "indian-rummy-core";

// Create a syndicate for tournament play
const playerIds = ['player1', 'player2', 'player3'];
const syndicate = new JsSyndicateGame(playerIds);

// Add multiple games to the syndicate
for (let i = 0; i < 5; i++) {
  const game = new JsIndianRummyGame(
    playerIds, 
    ['Alice', 'Bob', 'Charlie'], 
    1
  );
  
  // Simulate game completion (in real usage, games would be played)
  syndicate.addRummyGame(game);
}

console.log(`Tournament has ${syndicate.getGameCount()} games`);

Tournament Scoring and Leaderboards

// Get cumulative points across all games
const totalPoints = syndicate.getPlayerPoints();
console.log('Total points:', totalPoints);

// Get leaderboard (sorted by points, ascending - lower is better)
const leaderboard = syndicate.getLeaderboard();
console.log('Tournament standings:');
leaderboard.forEach(([playerName, points], index) => {
  console.log(`${index + 1}. ${playerName}: ${points} points`);
});

Game Persistence

Saving and Loading Games

// Serialize game state
const gameJson = game.serialize();
console.log('Game saved to JSON');

// Save to file or database
// fs.writeFileSync('game.json', gameJson);

// Later, restore the game
const restoredGame = JsIndianRummyGame.deserialize(gameJson);
console.log('Game restored from JSON');

// Verify state is preserved
const originalState = game.getState();
const restoredState = restoredGame.getState();
console.log('States match:', 
  originalState.nextTurnPlayer === restoredState.nextTurnPlayer
);

Syndicate Persistence

// Serialize entire tournament
const syndicateJson = syndicate.serialize();

// Restore tournament
const restoredSyndicate = JsSyndicateGame.deserialize(syndicateJson);
console.log(`Restored syndicate with ${restoredSyndicate.getGameCount()} games`);

Error Handling

try {
  // Invalid game creation
  const invalidGame = new JsIndianRummyGame([], [], 0);
} catch (error) {
  console.log('Game creation failed:', error.message);
}

try {
  // Invalid move
  const invalidMove = {
    playerId: 'nonexistent',
    moveType: JsMoveType.OpenCard,
    cardReceived: { rank: 'A', suit: 'S' },
    cardDiscarded: { rank: '2', suit: 'C' },
    didClaimWin: false
  };
  
  const result = game.processMove(invalidMove);
  if (!result.isValid) {
    console.log('Move rejected:', result.errorMessage);
  }
} catch (error) {
  console.log('Move processing error:', error.message);
}

Performance

This library is implemented in Rust for optimal performance:

  • Fast scoring: Efficient algorithms for finding minimum penalty scores (~19ms average)
  • Quick validation: Deck validation in ~0.05ms, hand completion in ~9.4ms
  • Memory efficient: Minimal allocations and optimal data structures
  • Game processing: Move validation and processing in microseconds
  • Serialization: Fast JSON serialization for game persistence
  • Cross-platform: Native binaries for all major platforms

Benchmarks

  • Score calculation: ~19ms average for complex hands
  • Deck validation: ~0.05ms average for standard deck
  • Hand completion check: ~9.4ms average for complex hands
  • Completed hand search: ~0.03ms average for large collections
  • 1000 score calculations: <30 seconds total
  • Memory usage: No significant memory leaks during repeated operations

License

MIT

Testing

The library includes comprehensive test coverage with 113+ tests:

  • Phase 1 Tests: Core card evaluation functions (98 tests)
  • Phase 2 Tests: Complete game logic (15 tests)
  • Performance Tests: Benchmarks and stress testing
  • Error Handling: Edge cases and invalid input handling
  • Type Safety: TypeScript integration validation

Run tests with:

npm test                    # All tests
npm run test:game-logic     # Phase 2 game logic tests
npm run test:performance    # Performance benchmarks
npm run test:coverage       # Coverage report

Development Roadmap

Phase 1: Core Card Evaluation ✅ (Complete)

  • Card and deck data structures
  • Hand validation and scoring algorithms
  • Set detection (sequences, triplets, life)
  • Joker handling (literal and designated)
  • TypeScript bindings and comprehensive test suite

Phase 2: Full Game Logic ✅ (Complete)

  • Player management and game state
  • Turn-based move processing
  • Game flow (deal, draw, discard, fold, win)
  • Syndicate games and tournament scoring
  • Game state persistence and serialization
  • Complete TypeScript API with full type safety

See rummy.md for complete game specifications and tests/future-game-logic.test.ts for comprehensive test coverage.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Commit count: 0

cargo fmt