indian-rummy-core
Version:
High-performance Indian Rummy game logic library implemented in Rust with TypeScript bindings for Node.js applications
645 lines (492 loc) • 16.7 kB
Markdown
# 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
```bash
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
```typescript
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
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
interface JsPlayer {
id: string;
name: string;
hand: JsCard[];
}
```
#### `JsMoveType`
Types of moves a player can make.
```typescript
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.
```typescript
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.
```typescript
interface JsMoveResult {
isValid: boolean;
isWin: boolean;
winner?: string;
scores: Record<string, number>;
errorMessage?: string;
}
```
#### `JsGameState`
Current state of the game.
```typescript
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.
```typescript
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.
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// Serialize entire tournament
const syndicateJson = syndicate.serialize();
// Restore tournament
const restoredSyndicate = JsSyndicateGame.deserialize(syndicateJson);
console.log(`Restored syndicate with ${restoredSyndicate.getGameCount()} games`);
```
### Error Handling
```typescript
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:
```bash
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)
- [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.