|
|
@@ -0,0 +1,2345 @@
|
|
|
+# Tutorial: Implementing Gin Rummy
|
|
|
+
|
|
|
+## Overview
|
|
|
+
|
|
|
+In this tutorial, you'll build a complete Gin Rummy card game using the CardsStarterKit framework. This is a more complex implementation than the magic trick, teaching you:
|
|
|
+
|
|
|
+- Managing larger hands (10 cards vs 2)
|
|
|
+- Implementing meld detection (sets and runs)
|
|
|
+- Creating intermediate AI opponents
|
|
|
+- Turn-based gameplay with draw/discard mechanics
|
|
|
+- Calculating deadwood and scoring
|
|
|
+- Multi-player support
|
|
|
+- Advanced UI for card organization
|
|
|
+
|
|
|
+**Target Audience:** MonoGame developers new to card games
|
|
|
+
|
|
|
+**Estimated Time:** 6-8 hours
|
|
|
+
|
|
|
+**Difficulty:** Intermediate
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## What is Gin Rummy?
|
|
|
+
|
|
|
+### Game Rules Summary
|
|
|
+
|
|
|
+**Objective:** Form melds (sets/runs) and minimize deadwood (unmatched cards).
|
|
|
+
|
|
|
+**Setup:**
|
|
|
+- 2-4 players (we'll support up to 4)
|
|
|
+- Each player receives 10 cards
|
|
|
+- Remaining cards form the stock pile
|
|
|
+- Top card of stock is flipped to start discard pile
|
|
|
+
|
|
|
+**Gameplay Flow:**
|
|
|
+1. **Draw Phase:** Player draws from stock or discard pile
|
|
|
+2. **Discard Phase:** Player discards one card to discard pile
|
|
|
+3. **Optional Knock:** If deadwood ≤ 10 points, player can knock
|
|
|
+4. **Gin:** If deadwood = 0, player declares "Gin!"
|
|
|
+
|
|
|
+**Melds:**
|
|
|
+- **Set:** 3-4 cards of same rank (e.g., 7♥ 7♠ 7♣)
|
|
|
+- **Run:** 3+ consecutive cards of same suit (e.g., 4♠ 5♠ 6♠)
|
|
|
+
|
|
|
+**Deadwood Points:**
|
|
|
+- Ace = 1 point
|
|
|
+- 2-10 = face value
|
|
|
+- J, Q, K = 10 points
|
|
|
+
|
|
|
+**Scoring (Single Round - Tutorial Focus):**
|
|
|
+- **Gin:** Knocker gets opponent's deadwood + 25 bonus
|
|
|
+- **Knock:** Knocker gets difference in deadwood
|
|
|
+- **Undercut:** If opponent has less/equal deadwood, opponent gets difference + 25 bonus
|
|
|
+
|
|
|
+### What We'll Build
|
|
|
+
|
|
|
+This tutorial focuses on **single-round gameplay** to keep it manageable. At the end, we'll discuss extending it to full match scoring (first to 100 points).
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 1: Project Structure
|
|
|
+
|
|
|
+### Step 1.1: Create Directory Structure
|
|
|
+
|
|
|
+```bash
|
|
|
+mkdir -p Core/Game/GinRummy/Game
|
|
|
+mkdir -p Core/Game/GinRummy/Players
|
|
|
+mkdir -p Core/Game/GinRummy/Rules
|
|
|
+mkdir -p Core/Game/GinRummy/UI
|
|
|
+mkdir -p Core/Game/GinRummy/AI
|
|
|
+```
|
|
|
+
|
|
|
+Your structure will look like:
|
|
|
+
|
|
|
+```
|
|
|
+Core/Game/GinRummy/
|
|
|
+├── Game/
|
|
|
+│ ├── GinRummyCardGame.cs
|
|
|
+│ ├── GinRummyGameState.cs
|
|
|
+│ ├── Meld.cs
|
|
|
+│ └── MeldDetector.cs
|
|
|
+├── Players/
|
|
|
+│ ├── GinRummyPlayer.cs
|
|
|
+│ └── GinRummyAIPlayer.cs
|
|
|
+├── Rules/
|
|
|
+│ ├── KnockRule.cs
|
|
|
+│ ├── GinRule.cs
|
|
|
+│ └── TurnCompleteRule.cs
|
|
|
+├── UI/
|
|
|
+│ └── HandOrganizer.cs
|
|
|
+└── AI/
|
|
|
+ └── GinRummyAI.cs
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 2: Core Data Structures
|
|
|
+
|
|
|
+### Step 2.1: Define Game State
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Game/GinRummyGameState.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// States of a Gin Rummy game
|
|
|
+ /// </summary>
|
|
|
+ public enum GinRummyGameState
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Dealing initial hands
|
|
|
+ /// </summary>
|
|
|
+ Dealing,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Player's turn - drawing phase
|
|
|
+ /// </summary>
|
|
|
+ Drawing,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Player's turn - discarding phase
|
|
|
+ /// </summary>
|
|
|
+ Discarding,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Player knocked - showing hands
|
|
|
+ /// </summary>
|
|
|
+ Knocked,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Player got Gin - showing hands
|
|
|
+ /// </summary>
|
|
|
+ Gin,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Calculating scores
|
|
|
+ /// </summary>
|
|
|
+ Scoring,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Round complete - showing results
|
|
|
+ /// </summary>
|
|
|
+ RoundEnd,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Waiting between turns
|
|
|
+ /// </summary>
|
|
|
+ Waiting
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 2.2: Define Meld Structure
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Game/Meld.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using CardsFramework.Cards;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Types of melds in Gin Rummy
|
|
|
+ /// </summary>
|
|
|
+ public enum MeldType
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// 3+ cards of same rank (e.g., 7♥ 7♠ 7♣)
|
|
|
+ /// </summary>
|
|
|
+ Set,
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 3+ consecutive cards of same suit (e.g., 4♠ 5♠ 6♠)
|
|
|
+ /// </summary>
|
|
|
+ Run
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Represents a meld (set or run) of cards
|
|
|
+ /// </summary>
|
|
|
+ public class Meld
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Type of this meld
|
|
|
+ /// </summary>
|
|
|
+ public MeldType Type { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Cards in this meld
|
|
|
+ /// </summary>
|
|
|
+ public List<TraditionalCard> Cards { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Total point value of cards in this meld
|
|
|
+ /// </summary>
|
|
|
+ public int PointValue
|
|
|
+ {
|
|
|
+ get
|
|
|
+ {
|
|
|
+ return Cards.Sum(card => GetCardPoints(card));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public Meld()
|
|
|
+ {
|
|
|
+ Cards = new List<TraditionalCard>();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Creates a meld from a list of cards
|
|
|
+ /// </summary>
|
|
|
+ public Meld(MeldType type, List<TraditionalCard> cards)
|
|
|
+ {
|
|
|
+ Type = type;
|
|
|
+ Cards = new List<TraditionalCard>(cards);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets the point value of a card for deadwood calculation
|
|
|
+ /// </summary>
|
|
|
+ public static int GetCardPoints(TraditionalCard card)
|
|
|
+ {
|
|
|
+ switch (card.Value)
|
|
|
+ {
|
|
|
+ case CardValue.Ace:
|
|
|
+ return 1;
|
|
|
+ case CardValue.Two:
|
|
|
+ return 2;
|
|
|
+ case CardValue.Three:
|
|
|
+ return 3;
|
|
|
+ case CardValue.Four:
|
|
|
+ return 4;
|
|
|
+ case CardValue.Five:
|
|
|
+ return 5;
|
|
|
+ case CardValue.Six:
|
|
|
+ return 6;
|
|
|
+ case CardValue.Seven:
|
|
|
+ return 7;
|
|
|
+ case CardValue.Eight:
|
|
|
+ return 8;
|
|
|
+ case CardValue.Nine:
|
|
|
+ return 9;
|
|
|
+ case CardValue.Ten:
|
|
|
+ case CardValue.Jack:
|
|
|
+ case CardValue.Queen:
|
|
|
+ case CardValue.King:
|
|
|
+ return 10;
|
|
|
+ default:
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Checks if this meld is valid
|
|
|
+ /// </summary>
|
|
|
+ public bool IsValid()
|
|
|
+ {
|
|
|
+ if (Cards.Count < 3)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ if (Type == MeldType.Set)
|
|
|
+ return IsValidSet();
|
|
|
+ else
|
|
|
+ return IsValidRun();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Validates a set (same rank)
|
|
|
+ /// </summary>
|
|
|
+ private bool IsValidSet()
|
|
|
+ {
|
|
|
+ if (Cards.Count < 3 || Cards.Count > 4)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ CardValue firstValue = Cards[0].Value;
|
|
|
+
|
|
|
+ // All cards must have the same value
|
|
|
+ return Cards.All(card => card.Value == firstValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Validates a run (consecutive same suit)
|
|
|
+ /// </summary>
|
|
|
+ private bool IsValidRun()
|
|
|
+ {
|
|
|
+ if (Cards.Count < 3)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ CardSuit suit = Cards[0].Type;
|
|
|
+
|
|
|
+ // All cards must be same suit
|
|
|
+ if (!Cards.All(card => card.Type == suit))
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // Sort cards by value
|
|
|
+ var sortedCards = Cards.OrderBy(card => GetCardNumericValue(card)).ToList();
|
|
|
+
|
|
|
+ // Check consecutive values
|
|
|
+ for (int i = 1; i < sortedCards.Count; i++)
|
|
|
+ {
|
|
|
+ int prevValue = GetCardNumericValue(sortedCards[i - 1]);
|
|
|
+ int currValue = GetCardNumericValue(sortedCards[i]);
|
|
|
+
|
|
|
+ if (currValue != prevValue + 1)
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets numeric value for sorting (Ace=1, King=13)
|
|
|
+ /// </summary>
|
|
|
+ private static int GetCardNumericValue(TraditionalCard card)
|
|
|
+ {
|
|
|
+ switch (card.Value)
|
|
|
+ {
|
|
|
+ case CardValue.Ace: return 1;
|
|
|
+ case CardValue.Two: return 2;
|
|
|
+ case CardValue.Three: return 3;
|
|
|
+ case CardValue.Four: return 4;
|
|
|
+ case CardValue.Five: return 5;
|
|
|
+ case CardValue.Six: return 6;
|
|
|
+ case CardValue.Seven: return 7;
|
|
|
+ case CardValue.Eight: return 8;
|
|
|
+ case CardValue.Nine: return 9;
|
|
|
+ case CardValue.Ten: return 10;
|
|
|
+ case CardValue.Jack: return 11;
|
|
|
+ case CardValue.Queen: return 12;
|
|
|
+ case CardValue.King: return 13;
|
|
|
+ default: return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public override string ToString()
|
|
|
+ {
|
|
|
+ string cardList = string.Join(", ", Cards.Select(c => $"{c.Value} of {c.Type}"));
|
|
|
+ return $"{Type}: [{cardList}]";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Key Features:**
|
|
|
+- `IsValid()`: Validates sets (same rank) and runs (consecutive, same suit)
|
|
|
+- `PointValue`: Sums card values for scoring
|
|
|
+- `GetCardPoints()`: Standard Gin Rummy point values
|
|
|
+
|
|
|
+### Step 2.3: Create Meld Detector
|
|
|
+
|
|
|
+This is the brain of Gin Rummy - finding optimal melds to minimize deadwood.
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Game/MeldDetector.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using CardsFramework.Cards;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Detects valid melds in a hand and calculates deadwood
|
|
|
+ /// </summary>
|
|
|
+ public class MeldDetector
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Finds the optimal set of melds that minimizes deadwood
|
|
|
+ /// </summary>
|
|
|
+ public static List<Meld> FindOptimalMelds(List<TraditionalCard> cards)
|
|
|
+ {
|
|
|
+ // Find all possible melds
|
|
|
+ List<Meld> allPossibleMelds = FindAllPossibleMelds(cards);
|
|
|
+
|
|
|
+ // Find combination with minimum deadwood
|
|
|
+ return FindBestMeldCombination(cards, allPossibleMelds);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Finds all possible valid melds in the hand
|
|
|
+ /// </summary>
|
|
|
+ private static List<Meld> FindAllPossibleMelds(List<TraditionalCard> cards)
|
|
|
+ {
|
|
|
+ List<Meld> melds = new List<Meld>();
|
|
|
+
|
|
|
+ // Find all sets (3-4 of same rank)
|
|
|
+ melds.AddRange(FindSets(cards));
|
|
|
+
|
|
|
+ // Find all runs (3+ consecutive same suit)
|
|
|
+ melds.AddRange(FindRuns(cards));
|
|
|
+
|
|
|
+ return melds;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Finds all possible sets in the hand
|
|
|
+ /// </summary>
|
|
|
+ private static List<Meld> FindSets(List<TraditionalCard> cards)
|
|
|
+ {
|
|
|
+ List<Meld> sets = new List<Meld>();
|
|
|
+
|
|
|
+ // Group by card value
|
|
|
+ var grouped = cards.GroupBy(card => card.Value)
|
|
|
+ .Where(g => g.Count() >= 3);
|
|
|
+
|
|
|
+ foreach (var group in grouped)
|
|
|
+ {
|
|
|
+ var cardList = group.ToList();
|
|
|
+
|
|
|
+ // Sets of 3
|
|
|
+ if (cardList.Count >= 3)
|
|
|
+ {
|
|
|
+ sets.Add(new Meld(MeldType.Set, cardList.Take(3).ToList()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Sets of 4
|
|
|
+ if (cardList.Count == 4)
|
|
|
+ {
|
|
|
+ sets.Add(new Meld(MeldType.Set, cardList));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return sets;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Finds all possible runs in the hand
|
|
|
+ /// </summary>
|
|
|
+ private static List<Meld> FindRuns(List<TraditionalCard> cards)
|
|
|
+ {
|
|
|
+ List<Meld> runs = new List<Meld>();
|
|
|
+
|
|
|
+ // Group by suit
|
|
|
+ var bySuit = cards.GroupBy(card => card.Type);
|
|
|
+
|
|
|
+ foreach (var suitGroup in bySuit)
|
|
|
+ {
|
|
|
+ var sortedCards = suitGroup.OrderBy(card => GetCardNumericValue(card)).ToList();
|
|
|
+
|
|
|
+ // Find consecutive sequences
|
|
|
+ for (int i = 0; i < sortedCards.Count; i++)
|
|
|
+ {
|
|
|
+ List<TraditionalCard> sequence = new List<TraditionalCard> { sortedCards[i] };
|
|
|
+
|
|
|
+ // Build consecutive sequence
|
|
|
+ for (int j = i + 1; j < sortedCards.Count; j++)
|
|
|
+ {
|
|
|
+ int prevValue = GetCardNumericValue(sortedCards[j - 1]);
|
|
|
+ int currValue = GetCardNumericValue(sortedCards[j]);
|
|
|
+
|
|
|
+ if (currValue == prevValue + 1)
|
|
|
+ {
|
|
|
+ sequence.Add(sortedCards[j]);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add all possible runs of length 3+
|
|
|
+ if (sequence.Count >= 3)
|
|
|
+ {
|
|
|
+ // Add runs of different lengths
|
|
|
+ for (int length = 3; length <= sequence.Count; length++)
|
|
|
+ {
|
|
|
+ runs.Add(new Meld(MeldType.Run, sequence.Take(length).ToList()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return runs;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Finds the best combination of non-overlapping melds
|
|
|
+ /// </summary>
|
|
|
+ private static List<Meld> FindBestMeldCombination(
|
|
|
+ List<TraditionalCard> allCards,
|
|
|
+ List<Meld> possibleMelds)
|
|
|
+ {
|
|
|
+ List<Meld> bestCombination = new List<Meld>();
|
|
|
+ int minDeadwood = CalculateDeadwood(allCards, new List<Meld>());
|
|
|
+
|
|
|
+ // Try all combinations of melds
|
|
|
+ FindBestCombinationRecursive(
|
|
|
+ allCards,
|
|
|
+ possibleMelds,
|
|
|
+ new List<Meld>(),
|
|
|
+ ref bestCombination,
|
|
|
+ ref minDeadwood);
|
|
|
+
|
|
|
+ return bestCombination;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Recursive helper to find best non-overlapping meld combination
|
|
|
+ /// </summary>
|
|
|
+ private static void FindBestCombinationRecursive(
|
|
|
+ List<TraditionalCard> allCards,
|
|
|
+ List<Meld> possibleMelds,
|
|
|
+ List<Meld> currentCombination,
|
|
|
+ ref List<Meld> bestCombination,
|
|
|
+ ref int minDeadwood)
|
|
|
+ {
|
|
|
+ // Calculate deadwood for current combination
|
|
|
+ int deadwood = CalculateDeadwood(allCards, currentCombination);
|
|
|
+
|
|
|
+ // Update best if this is better
|
|
|
+ if (deadwood < minDeadwood)
|
|
|
+ {
|
|
|
+ minDeadwood = deadwood;
|
|
|
+ bestCombination = new List<Meld>(currentCombination);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try adding each remaining meld
|
|
|
+ for (int i = 0; i < possibleMelds.Count; i++)
|
|
|
+ {
|
|
|
+ Meld meld = possibleMelds[i];
|
|
|
+
|
|
|
+ // Check if this meld overlaps with current combination
|
|
|
+ if (!OverlapsWithCombination(meld, currentCombination))
|
|
|
+ {
|
|
|
+ // Add this meld and recurse
|
|
|
+ currentCombination.Add(meld);
|
|
|
+
|
|
|
+ FindBestCombinationRecursive(
|
|
|
+ allCards,
|
|
|
+ possibleMelds.Skip(i + 1).ToList(),
|
|
|
+ currentCombination,
|
|
|
+ ref bestCombination,
|
|
|
+ ref minDeadwood);
|
|
|
+
|
|
|
+ // Backtrack
|
|
|
+ currentCombination.RemoveAt(currentCombination.Count - 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Checks if a meld uses any cards already in the combination
|
|
|
+ /// </summary>
|
|
|
+ private static bool OverlapsWithCombination(Meld meld, List<Meld> combination)
|
|
|
+ {
|
|
|
+ var usedCards = new HashSet<TraditionalCard>();
|
|
|
+
|
|
|
+ foreach (var existingMeld in combination)
|
|
|
+ {
|
|
|
+ foreach (var card in existingMeld.Cards)
|
|
|
+ {
|
|
|
+ usedCards.Add(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return meld.Cards.Any(card => usedCards.Contains(card));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Calculates deadwood (unmatched cards) point value
|
|
|
+ /// </summary>
|
|
|
+ public static int CalculateDeadwood(List<TraditionalCard> allCards, List<Meld> melds)
|
|
|
+ {
|
|
|
+ // Get all cards in melds
|
|
|
+ var meldedCards = new HashSet<TraditionalCard>();
|
|
|
+ foreach (var meld in melds)
|
|
|
+ {
|
|
|
+ foreach (var card in meld.Cards)
|
|
|
+ {
|
|
|
+ meldedCards.Add(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Calculate deadwood points
|
|
|
+ int deadwood = 0;
|
|
|
+ foreach (var card in allCards)
|
|
|
+ {
|
|
|
+ if (!meldedCards.Contains(card))
|
|
|
+ {
|
|
|
+ deadwood += Meld.GetCardPoints(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return deadwood;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets numeric value for card ordering
|
|
|
+ /// </summary>
|
|
|
+ private static int GetCardNumericValue(TraditionalCard card)
|
|
|
+ {
|
|
|
+ return Meld.GetCardNumericValue(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Algorithm Explanation:**
|
|
|
+1. **Find All Possible Melds:** Identify every valid set and run
|
|
|
+2. **Try Combinations:** Recursively try non-overlapping meld combinations
|
|
|
+3. **Minimize Deadwood:** Keep the combination with lowest deadwood points
|
|
|
+4. **Backtracking:** Classic combinatorial optimization problem
|
|
|
+
|
|
|
+This is computationally intensive for 10 cards but acceptable for gameplay.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 3: Player Classes
|
|
|
+
|
|
|
+### Step 3.1: GinRummyPlayer
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Players/GinRummyPlayer.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System.Collections.Generic;
|
|
|
+using CardsFramework.Cards;
|
|
|
+using CardsFramework.Players;
|
|
|
+using CardsFramework.Game;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Represents a player in Gin Rummy
|
|
|
+ /// </summary>
|
|
|
+ public class GinRummyPlayer : Player
|
|
|
+ {
|
|
|
+ #region Properties
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Current melds in player's hand
|
|
|
+ /// </summary>
|
|
|
+ public List<Meld> Melds { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Deadwood point value
|
|
|
+ /// </summary>
|
|
|
+ public int Deadwood { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Whether player has knocked
|
|
|
+ /// </summary>
|
|
|
+ public bool HasKnocked { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Whether player has gin
|
|
|
+ /// </summary>
|
|
|
+ public bool HasGin { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Score for this round
|
|
|
+ /// </summary>
|
|
|
+ public int RoundScore { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Total score across all rounds (for future multi-round support)
|
|
|
+ /// </summary>
|
|
|
+ public int TotalScore { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Whether it's currently this player's turn
|
|
|
+ /// </summary>
|
|
|
+ public bool IsMyTurn { get; set; }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Whether player has drawn this turn
|
|
|
+ /// </summary>
|
|
|
+ public bool HasDrawn { get; set; }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Initialization
|
|
|
+
|
|
|
+ public GinRummyPlayer(string name, CardsGame game)
|
|
|
+ : base(name, game)
|
|
|
+ {
|
|
|
+ Melds = new List<Meld>();
|
|
|
+ Deadwood = 0;
|
|
|
+ HasKnocked = false;
|
|
|
+ HasGin = false;
|
|
|
+ RoundScore = 0;
|
|
|
+ TotalScore = 0;
|
|
|
+ IsMyTurn = false;
|
|
|
+ HasDrawn = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Methods
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Analyzes hand to find optimal melds and calculate deadwood
|
|
|
+ /// </summary>
|
|
|
+ public void AnalyzeHand()
|
|
|
+ {
|
|
|
+ List<TraditionalCard> handCards = new List<TraditionalCard>();
|
|
|
+
|
|
|
+ for (int i = 0; i < Hand.Count; i++)
|
|
|
+ {
|
|
|
+ handCards.Add(Hand[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Find optimal melds
|
|
|
+ Melds = MeldDetector.FindOptimalMelds(handCards);
|
|
|
+
|
|
|
+ // Calculate deadwood
|
|
|
+ Deadwood = MeldDetector.CalculateDeadwood(handCards, Melds);
|
|
|
+
|
|
|
+ // Check for Gin (no deadwood)
|
|
|
+ HasGin = (Deadwood == 0 && handCards.Count == 10);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Checks if player can knock (deadwood <= 10)
|
|
|
+ /// </summary>
|
|
|
+ public bool CanKnock()
|
|
|
+ {
|
|
|
+ return Deadwood <= 10 && Hand.Count == 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Resets player state for new round
|
|
|
+ /// </summary>
|
|
|
+ public void ResetForNewRound()
|
|
|
+ {
|
|
|
+ Melds.Clear();
|
|
|
+ Deadwood = 0;
|
|
|
+ HasKnocked = false;
|
|
|
+ HasGin = false;
|
|
|
+ RoundScore = 0;
|
|
|
+ IsMyTurn = false;
|
|
|
+ HasDrawn = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**Key Methods:**
|
|
|
+- `AnalyzeHand()`: Uses `MeldDetector` to find melds and deadwood
|
|
|
+- `CanKnock()`: Checks if knock is legal
|
|
|
+- `ResetForNewRound()`: Prepares for next round
|
|
|
+
|
|
|
+### Step 3.2: GinRummyAIPlayer
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Players/GinRummyAIPlayer.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using CardsFramework.Game;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// AI-controlled Gin Rummy player
|
|
|
+ /// </summary>
|
|
|
+ public class GinRummyAIPlayer : GinRummyPlayer
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// AI decision-making component
|
|
|
+ /// </summary>
|
|
|
+ public GinRummyAI AI { get; private set; }
|
|
|
+
|
|
|
+ public GinRummyAIPlayer(string name, CardsGame game)
|
|
|
+ : base(name, game)
|
|
|
+ {
|
|
|
+ AI = new GinRummyAI(this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Simple wrapper - the AI logic will be in a separate class.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 4: AI Implementation
|
|
|
+
|
|
|
+### Step 4.1: Intermediate AI
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/AI/GinRummyAI.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using CardsFramework.Cards;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Intermediate AI for Gin Rummy
|
|
|
+ /// </summary>
|
|
|
+ public class GinRummyAI
|
|
|
+ {
|
|
|
+ private GinRummyPlayer player;
|
|
|
+ private Random random;
|
|
|
+
|
|
|
+ public GinRummyAI(GinRummyPlayer player)
|
|
|
+ {
|
|
|
+ this.player = player;
|
|
|
+ this.random = new Random();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Decides whether to draw from stock or discard pile
|
|
|
+ /// </summary>
|
|
|
+ public bool ShouldDrawFromDiscard(TraditionalCard topDiscard)
|
|
|
+ {
|
|
|
+ if (topDiscard == null)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ // Simulate adding this card to hand
|
|
|
+ List<TraditionalCard> testHand = GetCurrentHandCards();
|
|
|
+ testHand.Add(topDiscard);
|
|
|
+
|
|
|
+ // Find melds with this card
|
|
|
+ var meldsWithDiscard = MeldDetector.FindOptimalMelds(testHand);
|
|
|
+ int deadwoodWithDiscard = MeldDetector.CalculateDeadwood(testHand, meldsWithDiscard);
|
|
|
+
|
|
|
+ // Compare to current deadwood
|
|
|
+ return deadwoodWithDiscard < player.Deadwood;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Decides which card to discard
|
|
|
+ /// </summary>
|
|
|
+ public TraditionalCard SelectCardToDiscard()
|
|
|
+ {
|
|
|
+ List<TraditionalCard> handCards = GetCurrentHandCards();
|
|
|
+
|
|
|
+ // Analyze hand
|
|
|
+ var melds = MeldDetector.FindOptimalMelds(handCards);
|
|
|
+ var meldedCards = GetMeldedCards(melds);
|
|
|
+
|
|
|
+ // Get deadwood cards
|
|
|
+ var deadwoodCards = handCards.Where(card => !meldedCards.Contains(card)).ToList();
|
|
|
+
|
|
|
+ if (deadwoodCards.Count > 0)
|
|
|
+ {
|
|
|
+ // Discard highest value deadwood card
|
|
|
+ return deadwoodCards.OrderByDescending(card => Meld.GetCardPoints(card)).First();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // No deadwood - discard card that breaks the smallest meld
|
|
|
+ return SelectCardFromMelds(melds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Decides whether to knock
|
|
|
+ /// </summary>
|
|
|
+ public bool ShouldKnock()
|
|
|
+ {
|
|
|
+ // Knock if deadwood is very low
|
|
|
+ if (player.HasGin)
|
|
|
+ return true;
|
|
|
+
|
|
|
+ if (player.Deadwood <= 5)
|
|
|
+ return true;
|
|
|
+
|
|
|
+ // Knock with higher deadwood with some probability
|
|
|
+ if (player.Deadwood <= 10)
|
|
|
+ {
|
|
|
+ // 50% chance if deadwood is 6-10
|
|
|
+ return random.NextDouble() < 0.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Evaluates how useful a card is for melds
|
|
|
+ /// </summary>
|
|
|
+ private int EvaluateCardUsefulness(TraditionalCard card, List<TraditionalCard> hand)
|
|
|
+ {
|
|
|
+ int usefulness = 0;
|
|
|
+
|
|
|
+ // Check for potential sets (same rank)
|
|
|
+ int sameRankCount = hand.Count(c => c.Value == card.Value);
|
|
|
+ usefulness += sameRankCount * 10;
|
|
|
+
|
|
|
+ // Check for potential runs (consecutive same suit)
|
|
|
+ var sameSuit = hand.Where(c => c.Type == card.Type).ToList();
|
|
|
+
|
|
|
+ foreach (var otherCard in sameSuit)
|
|
|
+ {
|
|
|
+ int valueDiff = Math.Abs(
|
|
|
+ GetCardNumericValue(card) - GetCardNumericValue(otherCard));
|
|
|
+
|
|
|
+ if (valueDiff == 1)
|
|
|
+ usefulness += 15; // Adjacent card
|
|
|
+ else if (valueDiff == 2)
|
|
|
+ usefulness += 5; // One card away
|
|
|
+ }
|
|
|
+
|
|
|
+ return usefulness;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Selects a card to discard from melds (when no deadwood)
|
|
|
+ /// </summary>
|
|
|
+ private TraditionalCard SelectCardFromMelds(List<Meld> melds)
|
|
|
+ {
|
|
|
+ // Find the smallest meld
|
|
|
+ var smallestMeld = melds.OrderBy(m => m.Cards.Count).First();
|
|
|
+
|
|
|
+ // Discard highest value card from smallest meld
|
|
|
+ return smallestMeld.Cards.OrderByDescending(card => Meld.GetCardPoints(card)).First();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets all cards currently in melds
|
|
|
+ /// </summary>
|
|
|
+ private HashSet<TraditionalCard> GetMeldedCards(List<Meld> melds)
|
|
|
+ {
|
|
|
+ var meldedCards = new HashSet<TraditionalCard>();
|
|
|
+
|
|
|
+ foreach (var meld in melds)
|
|
|
+ {
|
|
|
+ foreach (var card in meld.Cards)
|
|
|
+ {
|
|
|
+ meldedCards.Add(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return meldedCards;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets current hand as list
|
|
|
+ /// </summary>
|
|
|
+ private List<TraditionalCard> GetCurrentHandCards()
|
|
|
+ {
|
|
|
+ List<TraditionalCard> cards = new List<TraditionalCard>();
|
|
|
+
|
|
|
+ for (int i = 0; i < player.Hand.Count; i++)
|
|
|
+ {
|
|
|
+ cards.Add(player.Hand[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return cards;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Gets numeric value for card
|
|
|
+ /// </summary>
|
|
|
+ private int GetCardNumericValue(TraditionalCard card)
|
|
|
+ {
|
|
|
+ switch (card.Value)
|
|
|
+ {
|
|
|
+ case CardValue.Ace: return 1;
|
|
|
+ case CardValue.Two: return 2;
|
|
|
+ case CardValue.Three: return 3;
|
|
|
+ case CardValue.Four: return 4;
|
|
|
+ case CardValue.Five: return 5;
|
|
|
+ case CardValue.Six: return 6;
|
|
|
+ case CardValue.Seven: return 7;
|
|
|
+ case CardValue.Eight: return 8;
|
|
|
+ case CardValue.Nine: return 9;
|
|
|
+ case CardValue.Ten: return 10;
|
|
|
+ case CardValue.Jack: return 11;
|
|
|
+ case CardValue.Queen: return 12;
|
|
|
+ case CardValue.King: return 13;
|
|
|
+ default: return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**AI Strategy:**
|
|
|
+1. **Drawing:** Takes discard if it reduces deadwood
|
|
|
+2. **Discarding:** Discards highest-value deadwood card
|
|
|
+3. **Knocking:** Knocks with Gin or very low deadwood, probabilistic for medium deadwood
|
|
|
+
|
|
|
+This creates a competent but not unbeatable opponent.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 5: Game Rules
|
|
|
+
|
|
|
+### Step 5.1: Knock Rule
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Rules/KnockRule.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using CardsFramework.Rules;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ public class KnockEventArgs : EventArgs
|
|
|
+ {
|
|
|
+ public GinRummyPlayer Player { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Rule that fires when a player knocks
|
|
|
+ /// </summary>
|
|
|
+ public class KnockRule : GameRule
|
|
|
+ {
|
|
|
+ private readonly GinRummyCardGame game;
|
|
|
+
|
|
|
+ public KnockRule(GinRummyCardGame game)
|
|
|
+ {
|
|
|
+ this.game = game;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Check()
|
|
|
+ {
|
|
|
+ foreach (var player in game.Players)
|
|
|
+ {
|
|
|
+ if (player is GinRummyPlayer ginPlayer)
|
|
|
+ {
|
|
|
+ if (ginPlayer.HasKnocked && !ginPlayer.HasGin)
|
|
|
+ {
|
|
|
+ FireRuleMatch(new KnockEventArgs { Player = ginPlayer });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 5.2: Gin Rule
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Rules/GinRule.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using CardsFramework.Rules;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ public class GinEventArgs : EventArgs
|
|
|
+ {
|
|
|
+ public GinRummyPlayer Player { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Rule that fires when a player gets Gin
|
|
|
+ /// </summary>
|
|
|
+ public class GinRule : GameRule
|
|
|
+ {
|
|
|
+ private readonly GinRummyCardGame game;
|
|
|
+
|
|
|
+ public GinRule(GinRummyCardGame game)
|
|
|
+ {
|
|
|
+ this.game = game;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Check()
|
|
|
+ {
|
|
|
+ foreach (var player in game.Players)
|
|
|
+ {
|
|
|
+ if (player is GinRummyPlayer ginPlayer)
|
|
|
+ {
|
|
|
+ if (ginPlayer.HasGin)
|
|
|
+ {
|
|
|
+ FireRuleMatch(new GinEventArgs { Player = ginPlayer });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 5.3: Turn Complete Rule
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Rules/TurnCompleteRule.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using CardsFramework.Rules;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ public class TurnCompleteEventArgs : EventArgs
|
|
|
+ {
|
|
|
+ public GinRummyPlayer Player { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Rule that fires when a player completes their turn
|
|
|
+ /// </summary>
|
|
|
+ public class TurnCompleteRule : GameRule
|
|
|
+ {
|
|
|
+ private readonly GinRummyCardGame game;
|
|
|
+ private int previousHandCount = -1;
|
|
|
+
|
|
|
+ public TurnCompleteRule(GinRummyCardGame game)
|
|
|
+ {
|
|
|
+ this.game = game;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Check()
|
|
|
+ {
|
|
|
+ var currentPlayer = game.GetCurrentGinRummyPlayer();
|
|
|
+
|
|
|
+ if (currentPlayer != null && currentPlayer.IsMyTurn)
|
|
|
+ {
|
|
|
+ // Turn is complete when player has drawn and discarded
|
|
|
+ // (hand back to 10 cards after temporarily having 11)
|
|
|
+ if (currentPlayer.HasDrawn && currentPlayer.Hand.Count == 10)
|
|
|
+ {
|
|
|
+ if (previousHandCount == 11)
|
|
|
+ {
|
|
|
+ FireRuleMatch(new TurnCompleteEventArgs { Player = currentPlayer });
|
|
|
+ previousHandCount = 10;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (currentPlayer.Hand.Count == 11)
|
|
|
+ {
|
|
|
+ previousHandCount = 11;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**How It Works:**
|
|
|
+- Detects when hand goes from 11 cards (after draw) to 10 cards (after discard)
|
|
|
+- Signals turn completion to advance to next player
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 6: Main Game Class (Part 1/3)
|
|
|
+
|
|
|
+### Step 6.1: Fields and Initialization
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/Game/GinRummyCardGame.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using Microsoft.Xna.Framework;
|
|
|
+using Microsoft.Xna.Framework.Graphics;
|
|
|
+using CardsFramework.Cards;
|
|
|
+using CardsFramework.Game;
|
|
|
+using CardsFramework.Players;
|
|
|
+using CardsFramework.UI;
|
|
|
+using CardsFramework.Rules;
|
|
|
+using GameStateManagement;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ public class GinRummyCardGame : CardsGame
|
|
|
+ {
|
|
|
+ #region Fields
|
|
|
+
|
|
|
+ // Game state
|
|
|
+ private GinRummyGameState currentState;
|
|
|
+ public GinRummyGameState State
|
|
|
+ {
|
|
|
+ get { return currentState; }
|
|
|
+ set { currentState = value; }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stock and discard piles
|
|
|
+ private List<TraditionalCard> discardPile;
|
|
|
+ public TraditionalCard TopDiscard
|
|
|
+ {
|
|
|
+ get { return discardPile.Count > 0 ? discardPile[discardPile.Count - 1] : null; }
|
|
|
+ }
|
|
|
+
|
|
|
+ // UI Components
|
|
|
+ private List<AnimatedHandGameComponent> animatedHands;
|
|
|
+ private AnimatedCardsGameComponent discardPileComponent;
|
|
|
+ private DeckDisplayComponent stockPileComponent;
|
|
|
+
|
|
|
+ private Button buttonDrawStock;
|
|
|
+ private Button buttonDrawDiscard;
|
|
|
+ private Button buttonKnock;
|
|
|
+ private Button buttonGin;
|
|
|
+ private Button buttonNewRound;
|
|
|
+
|
|
|
+ // Current turn management
|
|
|
+ private int currentPlayerIndex;
|
|
|
+
|
|
|
+ // Game rules
|
|
|
+ private KnockRule knockRule;
|
|
|
+ private GinRule ginRule;
|
|
|
+ private TurnCompleteRule turnCompleteRule;
|
|
|
+
|
|
|
+ // Display
|
|
|
+ private SpriteFont gameFont;
|
|
|
+ private string statusText;
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Initialization
|
|
|
+
|
|
|
+ public GinRummyCardGame(GameTable gameTable)
|
|
|
+ : base(
|
|
|
+ decks: 1,
|
|
|
+ jokersInDeck: 0,
|
|
|
+ suits: CardSuit.AllSuits,
|
|
|
+ cardValues: CardValue.NonJokers,
|
|
|
+ minimumPlayers: 2,
|
|
|
+ maximumPlayers: 4,
|
|
|
+ gameTable: gameTable,
|
|
|
+ theme: "Default")
|
|
|
+ {
|
|
|
+ discardPile = new List<TraditionalCard>();
|
|
|
+ animatedHands = new List<AnimatedHandGameComponent>();
|
|
|
+ currentPlayerIndex = 0;
|
|
|
+ statusText = "";
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Initialize()
|
|
|
+ {
|
|
|
+ base.Initialize();
|
|
|
+ currentState = GinRummyGameState.Dealing;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void LoadContent()
|
|
|
+ {
|
|
|
+ base.LoadContent();
|
|
|
+
|
|
|
+ gameFont = Game.Content.Load<SpriteFont>(@"Fonts\Regular");
|
|
|
+
|
|
|
+ // Load button textures
|
|
|
+ Texture2D buttonTexture = Game.Content.Load<Texture2D>(@"Images\UI\ButtonRegular");
|
|
|
+ Texture2D buttonPressedTexture = Game.Content.Load<Texture2D>(@"Images\UI\ButtonPressed");
|
|
|
+
|
|
|
+ int screenWidth = GraphicsDevice.Viewport.Width;
|
|
|
+ int screenHeight = GraphicsDevice.Viewport.Height;
|
|
|
+ int buttonWidth = 200;
|
|
|
+ int buttonHeight = 60;
|
|
|
+
|
|
|
+ // Draw Stock button
|
|
|
+ buttonDrawStock = new Button(
|
|
|
+ new Rectangle(screenWidth / 2 - buttonWidth - 120, screenHeight - 150, buttonWidth, buttonHeight),
|
|
|
+ buttonTexture,
|
|
|
+ buttonPressedTexture,
|
|
|
+ gameFont,
|
|
|
+ "Draw from Stock",
|
|
|
+ Color.White
|
|
|
+ );
|
|
|
+ buttonDrawStock.Click += ButtonDrawStock_Click;
|
|
|
+ buttonDrawStock.Visible = false;
|
|
|
+ Game.Components.Add(buttonDrawStock);
|
|
|
+
|
|
|
+ // Draw Discard button
|
|
|
+ buttonDrawDiscard = new Button(
|
|
|
+ new Rectangle(screenWidth / 2 + 120 - buttonWidth, screenHeight - 150, buttonWidth, buttonHeight),
|
|
|
+ buttonTexture,
|
|
|
+ buttonPressedTexture,
|
|
|
+ gameFont,
|
|
|
+ "Draw from Discard",
|
|
|
+ Color.White
|
|
|
+ );
|
|
|
+ buttonDrawDiscard.Click += ButtonDrawDiscard_Click;
|
|
|
+ buttonDrawDiscard.Visible = false;
|
|
|
+ Game.Components.Add(buttonDrawDiscard);
|
|
|
+
|
|
|
+ // Knock button
|
|
|
+ buttonKnock = new Button(
|
|
|
+ new Rectangle(screenWidth / 2 - buttonWidth / 2, screenHeight - 230, buttonWidth, buttonHeight),
|
|
|
+ buttonTexture,
|
|
|
+ buttonPressedTexture,
|
|
|
+ gameFont,
|
|
|
+ "Knock",
|
|
|
+ Color.Yellow
|
|
|
+ );
|
|
|
+ buttonKnock.Click += ButtonKnock_Click;
|
|
|
+ buttonKnock.Visible = false;
|
|
|
+ Game.Components.Add(buttonKnock);
|
|
|
+
|
|
|
+ // Gin button
|
|
|
+ buttonGin = new Button(
|
|
|
+ new Rectangle(screenWidth / 2 - buttonWidth / 2, screenHeight - 310, buttonWidth, buttonHeight),
|
|
|
+ buttonTexture,
|
|
|
+ buttonPressedTexture,
|
|
|
+ gameFont,
|
|
|
+ "Gin!",
|
|
|
+ Color.Green
|
|
|
+ );
|
|
|
+ buttonGin.Click += ButtonGin_Click;
|
|
|
+ buttonGin.Visible = false;
|
|
|
+ Game.Components.Add(buttonGin);
|
|
|
+
|
|
|
+ // New Round button
|
|
|
+ buttonNewRound = new Button(
|
|
|
+ new Rectangle(screenWidth / 2 - buttonWidth / 2, screenHeight - 150, buttonWidth, buttonHeight),
|
|
|
+ buttonTexture,
|
|
|
+ buttonPressedTexture,
|
|
|
+ gameFont,
|
|
|
+ "New Round",
|
|
|
+ Color.LightBlue
|
|
|
+ );
|
|
|
+ buttonNewRound.Click += ButtonNewRound_Click;
|
|
|
+ buttonNewRound.Visible = false;
|
|
|
+ Game.Components.Add(buttonNewRound);
|
|
|
+
|
|
|
+ // Create stock pile display
|
|
|
+ stockPileComponent = new DeckDisplayComponent(dealer, gameTable, Game);
|
|
|
+ stockPileComponent.LoadContent();
|
|
|
+ Game.Components.Add(stockPileComponent);
|
|
|
+
|
|
|
+ // Initialize rules
|
|
|
+ knockRule = new KnockRule(this);
|
|
|
+ knockRule.RuleMatch += KnockRule_RuleMatch;
|
|
|
+ Rules.Add(knockRule);
|
|
|
+
|
|
|
+ ginRule = new GinRule(this);
|
|
|
+ ginRule.RuleMatch += GinRule_RuleMatch;
|
|
|
+ Rules.Add(ginRule);
|
|
|
+
|
|
|
+ turnCompleteRule = new TurnCompleteRule(this);
|
|
|
+ turnCompleteRule.RuleMatch += TurnCompleteRule_RuleMatch;
|
|
|
+ Rules.Add(turnCompleteRule);
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+```
|
|
|
+
|
|
|
+### Step 6.2: Player Management
|
|
|
+
|
|
|
+```csharp
|
|
|
+ #region Player Management
|
|
|
+
|
|
|
+ public override void AddPlayer(Player newPlayer)
|
|
|
+ {
|
|
|
+ if (!(newPlayer is GinRummyPlayer))
|
|
|
+ {
|
|
|
+ throw new ArgumentException("Player must be of type GinRummyPlayer");
|
|
|
+ }
|
|
|
+
|
|
|
+ base.AddPlayer(newPlayer);
|
|
|
+
|
|
|
+ // Create animated hand for this player
|
|
|
+ AnimatedHandGameComponent animatedHand = new AnimatedHandGameComponent(
|
|
|
+ newPlayer.Hand,
|
|
|
+ gameTable[Players.Count - 1],
|
|
|
+ Game
|
|
|
+ );
|
|
|
+ animatedHand.LoadContent();
|
|
|
+ animatedHands.Add(animatedHand);
|
|
|
+ Game.Components.Add(animatedHand);
|
|
|
+ }
|
|
|
+
|
|
|
+ public override Player GetCurrentPlayer()
|
|
|
+ {
|
|
|
+ if (Players.Count == 0)
|
|
|
+ return null;
|
|
|
+
|
|
|
+ return Players[currentPlayerIndex];
|
|
|
+ }
|
|
|
+
|
|
|
+ public GinRummyPlayer GetCurrentGinRummyPlayer()
|
|
|
+ {
|
|
|
+ return GetCurrentPlayer() as GinRummyPlayer;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void NextPlayer()
|
|
|
+ {
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (currentPlayer != null)
|
|
|
+ {
|
|
|
+ currentPlayer.IsMyTurn = false;
|
|
|
+ currentPlayer.HasDrawn = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ currentPlayerIndex = (currentPlayerIndex + 1) % Players.Count;
|
|
|
+
|
|
|
+ var nextPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (nextPlayer != null)
|
|
|
+ {
|
|
|
+ nextPlayer.IsMyTurn = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ statusText = $"{nextPlayer.Name}'s turn";
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+```
|
|
|
+
|
|
|
+### Step 6.3: Dealing
|
|
|
+
|
|
|
+```csharp
|
|
|
+ #region Card Management
|
|
|
+
|
|
|
+ public override void Deal()
|
|
|
+ {
|
|
|
+ // Shuffle deck
|
|
|
+ dealer.Shuffle();
|
|
|
+
|
|
|
+ // Deal 10 cards to each player
|
|
|
+ for (int i = 0; i < 10; i++)
|
|
|
+ {
|
|
|
+ foreach (var player in Players)
|
|
|
+ {
|
|
|
+ dealer[0].MoveToHand(player.Hand);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Flip top card to start discard pile
|
|
|
+ TraditionalCard topCard = dealer[0];
|
|
|
+ discardPile.Add(topCard);
|
|
|
+
|
|
|
+ // Create discard pile component
|
|
|
+ Vector2 discardPosition = new Vector2(
|
|
|
+ GraphicsDevice.Viewport.Width / 2 + 100,
|
|
|
+ GraphicsDevice.Viewport.Height / 2
|
|
|
+ );
|
|
|
+
|
|
|
+ discardPileComponent = new AnimatedCardsGameComponent(topCard, Game);
|
|
|
+ discardPileComponent.CurrentPosition = discardPosition;
|
|
|
+ discardPileComponent.IsFaceDown = false;
|
|
|
+ discardPileComponent.LoadContent();
|
|
|
+ Game.Components.Add(discardPileComponent);
|
|
|
+
|
|
|
+ // Analyze all hands
|
|
|
+ foreach (var player in Players)
|
|
|
+ {
|
|
|
+ if (player is GinRummyPlayer ginPlayer)
|
|
|
+ {
|
|
|
+ ginPlayer.AnalyzeHand();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Start first player's turn
|
|
|
+ var firstPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (firstPlayer != null)
|
|
|
+ {
|
|
|
+ firstPlayer.IsMyTurn = true;
|
|
|
+ statusText = $"{firstPlayer.Name}'s turn";
|
|
|
+ }
|
|
|
+
|
|
|
+ currentState = GinRummyGameState.Drawing;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void DrawFromStock()
|
|
|
+ {
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (currentPlayer == null || !currentPlayer.IsMyTurn)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (dealer.Count > 0)
|
|
|
+ {
|
|
|
+ TraditionalCard card = dealer[0];
|
|
|
+ card.MoveToHand(currentPlayer.Hand);
|
|
|
+ currentPlayer.HasDrawn = true;
|
|
|
+ currentPlayer.AnalyzeHand();
|
|
|
+
|
|
|
+ currentState = GinRummyGameState.Discarding;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void DrawFromDiscard()
|
|
|
+ {
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (currentPlayer == null || !currentPlayer.IsMyTurn)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (discardPile.Count > 0)
|
|
|
+ {
|
|
|
+ TraditionalCard card = discardPile[discardPile.Count - 1];
|
|
|
+ discardPile.RemoveAt(discardPile.Count - 1);
|
|
|
+ card.MoveToHand(currentPlayer.Hand);
|
|
|
+ currentPlayer.HasDrawn = true;
|
|
|
+ currentPlayer.AnalyzeHand();
|
|
|
+
|
|
|
+ // Update discard pile visual
|
|
|
+ UpdateDiscardPileVisual();
|
|
|
+
|
|
|
+ currentState = GinRummyGameState.Discarding;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void DiscardCard(TraditionalCard card)
|
|
|
+ {
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (currentPlayer == null || !currentPlayer.IsMyTurn || !currentPlayer.HasDrawn)
|
|
|
+ return;
|
|
|
+
|
|
|
+ // Remove from hand
|
|
|
+ currentPlayer.Hand.LostCard -= null; // Detach events if any
|
|
|
+ discardPile.Add(card);
|
|
|
+
|
|
|
+ // Update visual
|
|
|
+ UpdateDiscardPileVisual();
|
|
|
+
|
|
|
+ // Analyze hand after discard
|
|
|
+ currentPlayer.AnalyzeHand();
|
|
|
+
|
|
|
+ // Check if player can knock or has gin
|
|
|
+ if (currentPlayer.HasGin)
|
|
|
+ {
|
|
|
+ currentState = GinRummyGameState.Gin;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ currentState = GinRummyGameState.Waiting;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateDiscardPileVisual()
|
|
|
+ {
|
|
|
+ if (discardPileComponent != null)
|
|
|
+ {
|
|
|
+ Game.Components.Remove(discardPileComponent);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (TopDiscard != null)
|
|
|
+ {
|
|
|
+ Vector2 discardPosition = new Vector2(
|
|
|
+ GraphicsDevice.Viewport.Width / 2 + 100,
|
|
|
+ GraphicsDevice.Viewport.Height / 2
|
|
|
+ );
|
|
|
+
|
|
|
+ discardPileComponent = new AnimatedCardsGameComponent(TopDiscard, Game);
|
|
|
+ discardPileComponent.CurrentPosition = discardPosition;
|
|
|
+ discardPileComponent.IsFaceDown = false;
|
|
|
+ discardPileComponent.LoadContent();
|
|
|
+ Game.Components.Add(discardPileComponent);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 7: Main Game Class (Part 2/3) - Game Flow
|
|
|
+
|
|
|
+```csharp
|
|
|
+ #region Game Flow
|
|
|
+
|
|
|
+ public override void StartPlaying()
|
|
|
+ {
|
|
|
+ currentState = GinRummyGameState.Dealing;
|
|
|
+ Deal();
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Update(GameTime gameTime)
|
|
|
+ {
|
|
|
+ base.Update(gameTime);
|
|
|
+
|
|
|
+ CheckRules();
|
|
|
+ UpdateUIForState();
|
|
|
+ ProcessAITurns(gameTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateUIForState()
|
|
|
+ {
|
|
|
+ // Hide all buttons initially
|
|
|
+ buttonDrawStock.Visible = false;
|
|
|
+ buttonDrawDiscard.Visible = false;
|
|
|
+ buttonKnock.Visible = false;
|
|
|
+ buttonGin.Visible = false;
|
|
|
+ buttonNewRound.Visible = false;
|
|
|
+
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+
|
|
|
+ switch (currentState)
|
|
|
+ {
|
|
|
+ case GinRummyGameState.Drawing:
|
|
|
+ // Only show draw buttons for human player
|
|
|
+ if (currentPlayer != null && !(currentPlayer is GinRummyAIPlayer))
|
|
|
+ {
|
|
|
+ buttonDrawStock.Visible = true;
|
|
|
+ buttonDrawDiscard.Visible = (TopDiscard != null);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case GinRummyGameState.Discarding:
|
|
|
+ // Show knock/gin buttons if eligible
|
|
|
+ if (currentPlayer != null && !(currentPlayer is GinRummyAIPlayer))
|
|
|
+ {
|
|
|
+ if (currentPlayer.HasGin)
|
|
|
+ {
|
|
|
+ buttonGin.Visible = true;
|
|
|
+ }
|
|
|
+ else if (currentPlayer.CanKnock())
|
|
|
+ {
|
|
|
+ buttonKnock.Visible = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ statusText = $"{currentPlayer.Name}: Select a card to discard";
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case GinRummyGameState.RoundEnd:
|
|
|
+ buttonNewRound.Visible = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ProcessAITurns(GameTime gameTime)
|
|
|
+ {
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+
|
|
|
+ if (currentPlayer == null || !(currentPlayer is GinRummyAIPlayer))
|
|
|
+ return;
|
|
|
+
|
|
|
+ GinRummyAIPlayer aiPlayer = (GinRummyAIPlayer)currentPlayer;
|
|
|
+
|
|
|
+ // AI drawing phase
|
|
|
+ if (currentState == GinRummyGameState.Drawing)
|
|
|
+ {
|
|
|
+ // Small delay for realism
|
|
|
+ System.Threading.Tasks.Task.Delay(1000).ContinueWith(t =>
|
|
|
+ {
|
|
|
+ if (aiPlayer.AI.ShouldDrawFromDiscard(TopDiscard))
|
|
|
+ {
|
|
|
+ DrawFromDiscard();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ DrawFromStock();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // AI discarding phase
|
|
|
+ else if (currentState == GinRummyGameState.Discarding)
|
|
|
+ {
|
|
|
+ // Check if AI should knock
|
|
|
+ if (aiPlayer.AI.ShouldKnock())
|
|
|
+ {
|
|
|
+ if (aiPlayer.HasGin)
|
|
|
+ {
|
|
|
+ aiPlayer.HasGin = true;
|
|
|
+ currentState = GinRummyGameState.Gin;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ aiPlayer.HasKnocked = true;
|
|
|
+ currentState = GinRummyGameState.Knocked;
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Select card to discard
|
|
|
+ System.Threading.Tasks.Task.Delay(1000).ContinueWith(t =>
|
|
|
+ {
|
|
|
+ TraditionalCard cardToDiscard = aiPlayer.AI.SelectCardToDiscard();
|
|
|
+ DiscardCard(cardToDiscard);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 8: Main Game Class (Part 3/3) - Event Handlers and Scoring
|
|
|
+
|
|
|
+```csharp
|
|
|
+ #region Event Handlers
|
|
|
+
|
|
|
+ private void ButtonDrawStock_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ DrawFromStock();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ButtonDrawDiscard_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ DrawFromDiscard();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ButtonKnock_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (currentPlayer != null && currentPlayer.CanKnock())
|
|
|
+ {
|
|
|
+ currentPlayer.HasKnocked = true;
|
|
|
+ currentState = GinRummyGameState.Knocked;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ButtonGin_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ var currentPlayer = GetCurrentGinRummyPlayer();
|
|
|
+ if (currentPlayer != null && currentPlayer.HasGin)
|
|
|
+ {
|
|
|
+ currentState = GinRummyGameState.Gin;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ButtonNewRound_Click(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ ResetForNewRound();
|
|
|
+ StartPlaying();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void KnockRule_RuleMatch(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ KnockEventArgs args = (KnockEventArgs)e;
|
|
|
+ statusText = $"{args.Player.Name} knocked!";
|
|
|
+
|
|
|
+ currentState = GinRummyGameState.Scoring;
|
|
|
+ CalculateScores();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void GinRule_RuleMatch(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ GinEventArgs args = (GinEventArgs)e;
|
|
|
+ statusText = $"{args.Player.Name} got Gin!";
|
|
|
+
|
|
|
+ currentState = GinRummyGameState.Scoring;
|
|
|
+ CalculateScores();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void TurnCompleteRule_RuleMatch(object sender, EventArgs e)
|
|
|
+ {
|
|
|
+ NextPlayer();
|
|
|
+ currentState = GinRummyGameState.Drawing;
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Scoring
|
|
|
+
|
|
|
+ private void CalculateScores()
|
|
|
+ {
|
|
|
+ var knocker = Players.OfType<GinRummyPlayer>().FirstOrDefault(p => p.HasKnocked || p.HasGin);
|
|
|
+
|
|
|
+ if (knocker == null)
|
|
|
+ return;
|
|
|
+
|
|
|
+ // Get opponents
|
|
|
+ var opponents = Players.OfType<GinRummyPlayer>().Where(p => p != knocker).ToList();
|
|
|
+
|
|
|
+ if (knocker.HasGin)
|
|
|
+ {
|
|
|
+ // Gin: Knocker gets all opponents' deadwood + 25 bonus
|
|
|
+ int totalOpponentDeadwood = opponents.Sum(opp => opp.Deadwood);
|
|
|
+ knocker.RoundScore = totalOpponentDeadwood + 25;
|
|
|
+
|
|
|
+ statusText = $"{knocker.Name} wins with Gin!\n" +
|
|
|
+ $"Score: {knocker.RoundScore} points";
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Regular knock: Check for undercut
|
|
|
+ bool undercut = false;
|
|
|
+
|
|
|
+ foreach (var opponent in opponents)
|
|
|
+ {
|
|
|
+ if (opponent.Deadwood <= knocker.Deadwood)
|
|
|
+ {
|
|
|
+ // Undercut! Opponent wins
|
|
|
+ undercut = true;
|
|
|
+ int difference = knocker.Deadwood - opponent.Deadwood;
|
|
|
+ opponent.RoundScore = difference + 25;
|
|
|
+
|
|
|
+ statusText = $"{opponent.Name} undercuts {knocker.Name}!\n" +
|
|
|
+ $"{opponent.Name} scores {opponent.RoundScore} points";
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!undercut)
|
|
|
+ {
|
|
|
+ // Knocker wins
|
|
|
+ int totalDifference = opponents.Sum(opp => opp.Deadwood) - knocker.Deadwood;
|
|
|
+ knocker.RoundScore = totalDifference;
|
|
|
+
|
|
|
+ statusText = $"{knocker.Name} wins!\n" +
|
|
|
+ $"Score: {knocker.RoundScore} points";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ currentState = GinRummyGameState.RoundEnd;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ResetForNewRound()
|
|
|
+ {
|
|
|
+ // Clear discard pile
|
|
|
+ discardPile.Clear();
|
|
|
+
|
|
|
+ // Remove visual components
|
|
|
+ if (discardPileComponent != null)
|
|
|
+ {
|
|
|
+ Game.Components.Remove(discardPileComponent);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Reset all players
|
|
|
+ foreach (var player in Players)
|
|
|
+ {
|
|
|
+ if (player is GinRummyPlayer ginPlayer)
|
|
|
+ {
|
|
|
+ // Clear hand
|
|
|
+ while (ginPlayer.Hand.Count > 0)
|
|
|
+ {
|
|
|
+ ginPlayer.Hand[0].MoveToHand(dealer);
|
|
|
+ }
|
|
|
+
|
|
|
+ ginPlayer.ResetForNewRound();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Reshuffle dealer
|
|
|
+ dealer.Shuffle();
|
|
|
+
|
|
|
+ currentPlayerIndex = 0;
|
|
|
+ statusText = "";
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ #region Utilities
|
|
|
+
|
|
|
+ public override int CardValue(TraditionalCard card)
|
|
|
+ {
|
|
|
+ return Meld.GetCardPoints(card);
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void Draw(GameTime gameTime)
|
|
|
+ {
|
|
|
+ base.Draw(gameTime);
|
|
|
+
|
|
|
+ // Draw status text
|
|
|
+ if (!string.IsNullOrEmpty(statusText) && gameFont != null)
|
|
|
+ {
|
|
|
+ SpriteBatch spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
|
|
|
+
|
|
|
+ if (spriteBatch != null)
|
|
|
+ {
|
|
|
+ Vector2 position = new Vector2(20, 20);
|
|
|
+
|
|
|
+ // Draw with shadow
|
|
|
+ spriteBatch.DrawString(gameFont, statusText, position + new Vector2(2, 2), Color.Black);
|
|
|
+ spriteBatch.DrawString(gameFont, statusText, position, Color.White);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 9: UI Enhancement - Hand Organizer
|
|
|
+
|
|
|
+**Create:** `Core/Game/GinRummy/UI/HandOrganizer.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Linq;
|
|
|
+using Microsoft.Xna.Framework;
|
|
|
+using CardsFramework.Cards;
|
|
|
+
|
|
|
+namespace CardsFramework.GinRummy
|
|
|
+{
|
|
|
+ /// <summary>
|
|
|
+ /// Organizes cards in hand for better visualization
|
|
|
+ /// </summary>
|
|
|
+ public class HandOrganizer
|
|
|
+ {
|
|
|
+ /// <summary>
|
|
|
+ /// Sorts and groups cards for display
|
|
|
+ /// </summary>
|
|
|
+ public static List<TraditionalCard> OrganizeHand(List<TraditionalCard> cards, List<Meld> melds)
|
|
|
+ {
|
|
|
+ List<TraditionalCard> organized = new List<TraditionalCard>();
|
|
|
+
|
|
|
+ // Get cards in melds
|
|
|
+ var meldedCards = new HashSet<TraditionalCard>();
|
|
|
+ foreach (var meld in melds)
|
|
|
+ {
|
|
|
+ foreach (var card in meld.Cards)
|
|
|
+ {
|
|
|
+ meldedCards.Add(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add melded cards first (grouped by meld)
|
|
|
+ foreach (var meld in melds)
|
|
|
+ {
|
|
|
+ organized.AddRange(meld.Cards);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add remaining cards (deadwood) sorted
|
|
|
+ var deadwood = cards.Where(c => !meldedCards.Contains(c))
|
|
|
+ .OrderBy(c => c.Type)
|
|
|
+ .ThenBy(c => GetCardNumericValue(c))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ organized.AddRange(deadwood);
|
|
|
+
|
|
|
+ return organized;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int GetCardNumericValue(TraditionalCard card)
|
|
|
+ {
|
|
|
+ switch (card.Value)
|
|
|
+ {
|
|
|
+ case CardValue.Ace: return 1;
|
|
|
+ case CardValue.Two: return 2;
|
|
|
+ case CardValue.Three: return 3;
|
|
|
+ case CardValue.Four: return 4;
|
|
|
+ case CardValue.Five: return 5;
|
|
|
+ case CardValue.Six: return 6;
|
|
|
+ case CardValue.Seven: return 7;
|
|
|
+ case CardValue.Eight: return 8;
|
|
|
+ case CardValue.Nine: return 9;
|
|
|
+ case CardValue.Ten: return 10;
|
|
|
+ case CardValue.Jack: return 11;
|
|
|
+ case CardValue.Queen: return 12;
|
|
|
+ case CardValue.King: return 13;
|
|
|
+ default: return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Calculates card positions for visual display
|
|
|
+ /// </summary>
|
|
|
+ public static List<Vector2> CalculateCardPositions(
|
|
|
+ int cardCount,
|
|
|
+ Vector2 startPosition,
|
|
|
+ float cardSpacing,
|
|
|
+ List<int> meldBreaks = null)
|
|
|
+ {
|
|
|
+ List<Vector2> positions = new List<Vector2>();
|
|
|
+
|
|
|
+ float currentX = startPosition.X;
|
|
|
+
|
|
|
+ for (int i = 0; i < cardCount; i++)
|
|
|
+ {
|
|
|
+ positions.Add(new Vector2(currentX, startPosition.Y));
|
|
|
+
|
|
|
+ // Add extra space between melds
|
|
|
+ if (meldBreaks != null && meldBreaks.Contains(i))
|
|
|
+ {
|
|
|
+ currentX += cardSpacing + 20; // Extra gap
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ currentX += cardSpacing;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return positions;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+This helper organizes cards visually, grouping melds together and separating deadwood.
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 10: Screen Integration
|
|
|
+
|
|
|
+### Step 10.1: Create Gameplay Screen
|
|
|
+
|
|
|
+**Create:** `Core/Game/Screens/GinRummyGameplayScreen.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+using System;
|
|
|
+using Microsoft.Xna.Framework;
|
|
|
+using CardsFramework.GinRummy;
|
|
|
+using CardsFramework.UI;
|
|
|
+using GameStateManagement;
|
|
|
+
|
|
|
+namespace CardsStarterKit
|
|
|
+{
|
|
|
+ public class GinRummyGameplayScreen : GameScreen
|
|
|
+ {
|
|
|
+ private GinRummyCardGame ginRummyGame;
|
|
|
+
|
|
|
+ public GinRummyGameplayScreen()
|
|
|
+ {
|
|
|
+ EnabledGestures = Microsoft.Xna.Framework.Input.Touch.GestureType.Tap;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void LoadContent()
|
|
|
+ {
|
|
|
+ base.LoadContent();
|
|
|
+
|
|
|
+ // Create game table (4 player positions)
|
|
|
+ GameTable gameTable = new GameTable(ScreenManager.Game, 4);
|
|
|
+
|
|
|
+ // Create game
|
|
|
+ ginRummyGame = new GinRummyCardGame(gameTable);
|
|
|
+ ScreenManager.Game.Components.Add(ginRummyGame);
|
|
|
+ ginRummyGame.Initialize();
|
|
|
+ ginRummyGame.LoadContent();
|
|
|
+
|
|
|
+ // Add players
|
|
|
+ GinRummyPlayer humanPlayer = new GinRummyPlayer("You", ginRummyGame);
|
|
|
+ ginRummyGame.AddPlayer(humanPlayer);
|
|
|
+
|
|
|
+ GinRummyAIPlayer ai1 = new GinRummyAIPlayer("AI 1", ginRummyGame);
|
|
|
+ ginRummyGame.AddPlayer(ai1);
|
|
|
+
|
|
|
+ GinRummyAIPlayer ai2 = new GinRummyAIPlayer("AI 2", ginRummyGame);
|
|
|
+ ginRummyGame.AddPlayer(ai2);
|
|
|
+
|
|
|
+ // Start game
|
|
|
+ ginRummyGame.StartPlaying();
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void HandleInput(InputState input)
|
|
|
+ {
|
|
|
+ base.HandleInput(input);
|
|
|
+
|
|
|
+ if (input.IsPauseGame(null))
|
|
|
+ {
|
|
|
+ ScreenManager.AddScreen(new PauseScreen(), null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle card selection for discarding
|
|
|
+ HandleCardSelection(input);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void HandleCardSelection(InputState input)
|
|
|
+ {
|
|
|
+ if (ginRummyGame.State != GinRummyGameState.Discarding)
|
|
|
+ return;
|
|
|
+
|
|
|
+ var currentPlayer = ginRummyGame.GetCurrentGinRummyPlayer();
|
|
|
+
|
|
|
+ if (currentPlayer == null || currentPlayer is GinRummyAIPlayer)
|
|
|
+ return;
|
|
|
+
|
|
|
+ // Check for tap on cards in hand
|
|
|
+ // (Implementation depends on your touch/mouse handling)
|
|
|
+ // You would iterate through animated hand components and check bounds
|
|
|
+ }
|
|
|
+
|
|
|
+ public override void UnloadContent()
|
|
|
+ {
|
|
|
+ if (ginRummyGame != null)
|
|
|
+ {
|
|
|
+ ScreenManager.Game.Components.Remove(ginRummyGame);
|
|
|
+ }
|
|
|
+
|
|
|
+ base.UnloadContent();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Step 10.2: Add Menu Entry
|
|
|
+
|
|
|
+**Modify:** `Core/Game/Screens/MainMenuScreen.cs`
|
|
|
+
|
|
|
+```csharp
|
|
|
+// Add in constructor
|
|
|
+MenuEntry ginRummyMenuEntry = new MenuEntry("Gin Rummy");
|
|
|
+ginRummyMenuEntry.Selected += GinRummyMenuEntrySelected;
|
|
|
+menuEntries.Add(ginRummyMenuEntry);
|
|
|
+
|
|
|
+// Add event handler
|
|
|
+private void GinRummyMenuEntrySelected(object sender, EventArgs e)
|
|
|
+{
|
|
|
+ ScreenManager.AddScreen(new GinRummyGameplayScreen(), null);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 11: Testing
|
|
|
+
|
|
|
+### Step 11.1: Build and Run
|
|
|
+
|
|
|
+```bash
|
|
|
+dotnet build
|
|
|
+dotnet run --project Platforms/Desktop/CardsStarterKit.Desktop.csproj
|
|
|
+```
|
|
|
+
|
|
|
+### Step 11.2: Test Scenarios
|
|
|
+
|
|
|
+1. **Basic Gameplay:**
|
|
|
+ - Draw from stock/discard
|
|
|
+ - Discard cards
|
|
|
+ - Verify turn rotation
|
|
|
+
|
|
|
+2. **Meld Detection:**
|
|
|
+ - Check that sets are recognized (3 7s, etc.)
|
|
|
+ - Check that runs are recognized (4♠ 5♠ 6♠)
|
|
|
+ - Verify deadwood calculation
|
|
|
+
|
|
|
+3. **Knocking:**
|
|
|
+ - Get deadwood ≤ 10 and test knock
|
|
|
+ - Verify scoring
|
|
|
+
|
|
|
+4. **Gin:**
|
|
|
+ - Form all melds (0 deadwood)
|
|
|
+ - Verify Gin declaration and bonus
|
|
|
+
|
|
|
+5. **AI Behavior:**
|
|
|
+ - Watch AI draw and discard decisions
|
|
|
+ - Verify AI knocks appropriately
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Part 12: Enhancements and Next Steps
|
|
|
+
|
|
|
+### Enhancement 1: Card Clicking for Human Player
|
|
|
+
|
|
|
+Add touch/mouse handling in the screen to let human players click cards to discard:
|
|
|
+
|
|
|
+```csharp
|
|
|
+private void HandleCardSelection(InputState input)
|
|
|
+{
|
|
|
+ // Get tap position
|
|
|
+ foreach (var gesture in input.Gestures)
|
|
|
+ {
|
|
|
+ if (gesture.GestureType == GestureType.Tap)
|
|
|
+ {
|
|
|
+ // Check which card was tapped
|
|
|
+ // (Iterate through animated hand components and check bounds)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Enhancement 2: Visual Meld Highlighting
|
|
|
+
|
|
|
+Highlight cards that are part of melds:
|
|
|
+
|
|
|
+```csharp
|
|
|
+// In AnimatedHandGameComponent or custom renderer:
|
|
|
+foreach (var card in meldedCards)
|
|
|
+{
|
|
|
+ // Draw green border or glow effect
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Enhancement 3: Multi-Round Scoring
|
|
|
+
|
|
|
+To extend to full matches (first to 100 points):
|
|
|
+
|
|
|
+```csharp
|
|
|
+// Add to GinRummyPlayer:
|
|
|
+public int MatchScore { get; set; }
|
|
|
+
|
|
|
+// Add game state:
|
|
|
+public enum GinRummyGameState
|
|
|
+{
|
|
|
+ // ... existing states
|
|
|
+ MatchEnd // When someone reaches 100
|
|
|
+}
|
|
|
+
|
|
|
+// After each round:
|
|
|
+knocker.MatchScore += knocker.RoundScore;
|
|
|
+
|
|
|
+if (knocker.MatchScore >= 100)
|
|
|
+{
|
|
|
+ currentState = GinRummyGameState.MatchEnd;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Enhancement 4: Laying Off Cards
|
|
|
+
|
|
|
+After a knock, allow opponents to add cards to knocker's melds:
|
|
|
+
|
|
|
+```csharp
|
|
|
+public class LayOffPhase
|
|
|
+{
|
|
|
+ public static List<TraditionalCard> FindLayOffCards(
|
|
|
+ List<TraditionalCard> hand,
|
|
|
+ List<Meld> opponentMelds)
|
|
|
+ {
|
|
|
+ // Check each hand card to see if it extends opponent's melds
|
|
|
+ // ...
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Enhancement 5: Better AI
|
|
|
+
|
|
|
+Advanced AI improvements:
|
|
|
+- Track discarded cards (card counting)
|
|
|
+- Infer opponent hands from discards
|
|
|
+- Calculate probability of completing melds
|
|
|
+- Defensive discarding (don't give opponent useful cards)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Key Takeaways
|
|
|
+
|
|
|
+### What You Learned
|
|
|
+
|
|
|
+1. **Complex State Management:**
|
|
|
+ - Multi-phase turns (draw → discard)
|
|
|
+ - Turn rotation among players
|
|
|
+ - Handling knocking and gin conditions
|
|
|
+
|
|
|
+2. **Meld Detection Algorithm:**
|
|
|
+ - Finding all possible melds
|
|
|
+ - Combinatorial optimization to minimize deadwood
|
|
|
+ - Backtracking algorithm
|
|
|
+
|
|
|
+3. **AI Implementation:**
|
|
|
+ - Evaluating card usefulness
|
|
|
+ - Making draw/discard decisions
|
|
|
+ - Probabilistic knocking strategy
|
|
|
+
|
|
|
+4. **Scoring Logic:**
|
|
|
+ - Standard knock scoring
|
|
|
+ - Gin bonuses
|
|
|
+ - Undercut detection
|
|
|
+
|
|
|
+5. **Multi-Player Support:**
|
|
|
+ - Turn management
|
|
|
+ - Player indexing
|
|
|
+ - Mixed human/AI players
|
|
|
+
|
|
|
+### Design Patterns Used
|
|
|
+
|
|
|
+1. **State Machine:** GinRummyGameState controls game flow
|
|
|
+2. **Strategy Pattern:** AI decision-making in separate class
|
|
|
+3. **Rule Pattern:** Knock, Gin, TurnComplete rules
|
|
|
+4. **Component Pattern:** Animated hands, cards, deck display
|
|
|
+5. **Observer Pattern:** Event-driven rule matching
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Troubleshooting
|
|
|
+
|
|
|
+### Melds Not Detected
|
|
|
+- Debug `MeldDetector.FindOptimalMelds()`
|
|
|
+- Print all possible melds before optimization
|
|
|
+- Check card sorting logic
|
|
|
+
|
|
|
+### AI Makes Bad Moves
|
|
|
+- Add logging to AI decision methods
|
|
|
+- Print deadwood calculations
|
|
|
+- Verify card evaluation logic
|
|
|
+
|
|
|
+### Turn Doesn't Advance
|
|
|
+- Check `TurnCompleteRule` hand count tracking
|
|
|
+- Verify `NextPlayer()` is called
|
|
|
+- Debug state transitions
|
|
|
+
|
|
|
+### Scoring Incorrect
|
|
|
+- Print each player's deadwood
|
|
|
+- Verify meld point calculations
|
|
|
+- Check undercut logic
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Extending to Full Matches
|
|
|
+
|
|
|
+To implement full Gin Rummy matches to 100 points:
|
|
|
+
|
|
|
+**1. Add Match Tracking:**
|
|
|
+```csharp
|
|
|
+public int MatchScore { get; set; }
|
|
|
+public int RoundsWon { get; set; }
|
|
|
+```
|
|
|
+
|
|
|
+**2. Add Match State:**
|
|
|
+```csharp
|
|
|
+public enum MatchPhase
|
|
|
+{
|
|
|
+ InProgress,
|
|
|
+ Complete
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**3. Check After Each Round:**
|
|
|
+```csharp
|
|
|
+if (winner.MatchScore >= 100)
|
|
|
+{
|
|
|
+ currentState = GinRummyGameState.MatchEnd;
|
|
|
+ ShowMatchResults();
|
|
|
+}
|
|
|
+else
|
|
|
+{
|
|
|
+ PrepareNextRound();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**4. Add Bonuses:**
|
|
|
+- Game bonus: +100 for winning match
|
|
|
+- Shutout bonus: Additional points if opponent scored 0
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Related Gin Rummy Variants
|
|
|
+
|
|
|
+Want to explore other variants? Here are some links:
|
|
|
+
|
|
|
+**Oklahoma Gin:**
|
|
|
+- First upcard determines knock limit (not always 10)
|
|
|
+- https://en.wikipedia.org/wiki/Oklahoma_gin
|
|
|
+
|
|
|
+**Hollywood Gin:**
|
|
|
+- Multiple simultaneous games
|
|
|
+- Wins count toward different game tracks
|
|
|
+- https://www.pagat.com/rummy/ginrummy.html#hollywood
|
|
|
+
|
|
|
+**Straight Gin:**
|
|
|
+- Can only knock with Gin (0 deadwood)
|
|
|
+- Higher scoring, faster games
|
|
|
+- https://www.pagat.com/rummy/ginrummy.html#straight
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Complete File Checklist
|
|
|
+
|
|
|
+- [ ] `Core/Game/GinRummy/Game/GinRummyGameState.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Game/Meld.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Game/MeldDetector.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Game/GinRummyCardGame.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Players/GinRummyPlayer.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Players/GinRummyAIPlayer.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/AI/GinRummyAI.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Rules/KnockRule.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Rules/GinRule.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/Rules/TurnCompleteRule.cs`
|
|
|
+- [ ] `Core/Game/GinRummy/UI/HandOrganizer.cs`
|
|
|
+- [ ] `Core/Game/Screens/GinRummyGameplayScreen.cs`
|
|
|
+- [ ] Modified `Core/Game/Screens/MainMenuScreen.cs`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Conclusion
|
|
|
+
|
|
|
+Congratulations! You've built a complete Gin Rummy implementation with:
|
|
|
+
|
|
|
+- Full game rules (melds, knocking, gin)
|
|
|
+- Intelligent AI opponents
|
|
|
+- Multi-player support
|
|
|
+- Proper scoring
|
|
|
+- Turn-based gameplay
|
|
|
+
|
|
|
+This tutorial demonstrated advanced card game concepts including meld detection algorithms, AI decision-making, and complex game state management. You're now equipped to build any card game using the CardsStarterKit framework!
|
|
|
+
|
|
|
+**Next challenges to try:**
|
|
|
+- Implement other Rummy variants (Rummy 500, Canasta)
|
|
|
+- Build trick-taking games (Hearts, Spades, Bridge)
|
|
|
+- Create shedding games (Crazy Eights, Uno-style)
|
|
|
+
|
|
|
+Happy coding!
|