Building a DouDizhu Card Game Engine from Scratch

Pattern Enumeration and Weight Assignment

DouDizhu supports 37 distinct card patterns, including singles, pairs, triplets, straights (5- to 10-card sequences), bombs (four-of-a-kind or joker pair), planes (multiple consecutive triplets), and combinations like "three-with-two" or "four-with-two-pairs." Each pattern variant is exhaustively enumerated and assigned a unique numerical weight. For example:

  • Single cards: 3 < 4 < ... < A < 2 < Little Joker < Big Joker
  • Five-card straights: 3-4-5-6-7 < 4-5-6-7-8 < ... < 10-J-Q-K-A

This precomputed mapping enables O(1) pattern validation and strength comparison. When a player plays a hand, the engine strips suits, sorts the remaining ranks, and looks up the resulting string in a dictionary to retrieve all matching patterns and their weights.

Pattern Ambiguity and Multiple Matches

A single hand may correspond to multiple valid patterns. For instance, the hand "K-K-K-K-Q-Q-Q-Q" can be interpreted as:

  • "Four-of-a-kind with two pairs" (weight: 300)
  • "Four-of-a-kind with two pairs" (reordered, weight: 200)
  • "Plane with two singles" (weight: 300)

The engine returns all valid interpretations. Only patterns of the same type are comparable — for example, a "four-with-two-pairs" cannot be compared directly to a "plane." However, bombs (four-of-a-kind or joker pair) override all other patterns regardless of type.

Card Representation

Since suits are irrelevant for comparison, the system normalizes all input by removing suit information. The card set consists of:

public static List CardRanks = new List { "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A", "2", "LJ", "BJ" };

public static List FullDeck { get { var deck = new List(); for (int i = 0; i < 13; i++) { foreach (string suit in CardSuits) { deck.Add(CardRanks[i] + "*" + suit); } } deck.Add(CardRanks[13]); // Little Joker deck.Add(CardRanks[14]); // Big Joker return deck; } }


</div>### Shuffling and Dealing

The deck is shuffled using a Fisher-Yates-inspired algorithm. Each card is inserted at a random position in a new list. Special handling ensures the first card is not always placed at the start:

<div>```
public List<string> Shuffle()
{
    var deck = new List<string>(FullDeck);
    var shuffled = new List<string>();
    var rng = new Random(Guid.NewGuid().GetHashCode());

    foreach (string card in deck)
    {
        int pos = rng.Next(0, shuffled.Count + 1);
        shuffled.Insert(pos, card);
    }

    // Reinsert first card at random position to avoid bias
    string firstCard = deck[0];
    shuffled.Remove(firstCard);
    shuffled.Insert(rng.Next(0, shuffled.Count + 1), firstCard);

    return shuffled;
}
for (int i = 0; i < 17; i++)
{
    p1.Add(shuffled[i * 3]);
    p2.Add(shuffled[i * 3 + 1]);
    p3.Add(shuffled[i * 3 + 2]);
}

bonus.Add(shuffled[51]);
bonus.Add(shuffled[52]);
bonus.Add(shuffled[53]);

return true;

}


</div>### Pattern Recognition Engine

The core validation logic is a dictinoary lookup. Input cards are normalized by removing suits, sorting by rank (descending), and joining with hyphens:

<div>```
public List<Pattern> ValidateHand(List<string> hand)
{
    string normalized = NormalizeHand(hand);
    List<Pattern> matches = new List<Pattern>();

    if (PatternDatabase.ContainsKey(normalized))
        matches.AddRange(PatternDatabase[normalized]);

    return matches;
}

private string NormalizeHand(List<string> hand)
{
    var ranks = hand.Select(c => c.Split('*')[0])
                   .Select(r => CardRanks.IndexOf(r))
                   .OrderByDescending(x => x)
                   .Select(x => CardRanks[x])
                   .ToList();
    return string.Join("-", ranks);
}

Tags: DouDizhu card-game-engine pattern-recognition game-ai card-dealing

Posted on Sun, 20 Sep 2026 16:48:31 +0000 by soniared2002