indian-rummy-core
v0.1.1
Published
High-performance Indian Rummy game logic library implemented in Rust with TypeScript bindings for Node.js applications
Maintainers
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-coreRequirements
- 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 handPhase 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 reportDevelopment Roadmap
Phase 1: Core Card Evaluation ✅ (Complete)
- [x] Card and deck data structures
- [x] Hand validation and scoring algorithms
- [x] Set detection (sequences, triplets, life)
- [x] Joker handling (literal and designated)
- [x] TypeScript bindings and comprehensive test suite
Phase 2: Full Game Logic ✅ (Complete)
- [x] Player management and game state
- [x] Turn-based move processing
- [x] Game flow (deal, draw, discard, fold, win)
- [x] Syndicate games and tournament scoring
- [x] Game state persistence and serialization
- [x] 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.
