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