UNPKG

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
# 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.