Building a reliable poker hand evaluator in C# is a common task for game developers, server engineers, and hobbyists who want accurate, high-performance hand ranking for variants like Texas Hold'em or Omaha. In this guide I share hands-on experience, architectural choices, and readable C# examples that take you from a straightforward implementation to optimized solutions suitable for production. If you're starting out, try this basic walkthrough first; if you need speed, I cover practical optimizations and trade-offs.
For a quick reference or downloadable tools I sometimes link to community resources—start by exploring the poker hand evaluator c# resource if you want examples and inspiration from real-game implementations.
Why a custom poker hand evaluator matters
Not all evaluators are equal. The trivial brute-force approach (generate every possible 5-card combination and compare) is clear and often correct, but it can be slow when you evaluate millions of hands per second or need deterministic tie-breaking across distributed servers. Writing your own evaluator lets you:
- Control ranking semantics and tie-breaker rules across suits and ranks
- Optimize for throughput when evaluating many hands concurrently (server-side AI, simulations, or equity calculators)
- Integrate cleanly into your game engine without large external dependencies
Core concepts every evaluator must implement
At its core, a poker hand evaluator must reliably answer: given N cards (commonly 5, 6, or 7), what is the best 5-card hand and how strong is it compared to every other possible hand?
- Card representation — choose compact, fast-to-compare structures
- Hand classification — detect straights, flushes, pairs, trips, quads
- Tie breaking — determine unique numeric ranking or stable comparison
- Performance — balance memory and CPU for your expected load
Design choices and representations
There are several ways to represent cards and hands. Each has trade-offs:
- Object-based: Card as class with Suit and Rank enums — readable, slightly slower due to object overhead
- Byte/struct-based: byte or struct with bit fields — fewer allocations, faster comparisons
- Bitmask/bitboard: 64-bit integer representing ranks and/or suits — fastest for bitwise operations and pattern matching
For a practical, balanced design in C#, I recommend using a small struct for Card and integer-based encodings for evaluation paths. Below I provide code that favors clarity but can be optimized further with precomputed tables.
Readable C# evaluator: sample implementation
The following example shows a straightforward approach for evaluating 5-card hands and an extension for 7-card best-hand selection. It avoids complex lookup tables and is easy to test and maintain.
// SimpleCard.cs
public enum Suit { Clubs, Diamonds, Hearts, Spades }
public enum Rank {
Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten,
Jack = 11, Queen, King, Ace = 14
}
public readonly struct Card {
public readonly Rank Rank;
public readonly Suit Suit;
public Card(Rank rank, Suit suit) { Rank = rank; Suit = suit; }
public override string ToString() => $"{Rank} of {Suit}";
}
// HandEvaluator.cs
using System;
using System.Linq;
using System.Collections.Generic;
public static class HandEvaluator {
// Evaluate a 5-card hand: returns tuple (primaryCategory, tiebreakerValue)
// Categories: 8 = Straight Flush, 7 = Four of a Kind, 6 = Full House,
// 5 = Flush, 4 = Straight, 3 = Three of a Kind, 2 = Two Pair, 1 = One Pair, 0 = High Card
public static (int category, int[] key) Evaluate5(Card[] cards) {
if (cards == null || cards.Length != 5)
throw new ArgumentException("Evaluate5 needs exactly 5 cards.");
// Count ranks and suits
var rankCounts = new Dictionary();
var suitCounts = new Dictionary();
foreach (var c in cards) {
var r = (int)c.Rank;
rankCounts[r] = rankCounts.GetValueOrDefault(r) + 1;
suitCounts[c.Suit] = suitCounts.GetValueOrDefault(c.Suit) + 1;
}
bool isFlush = suitCounts.Values.Any(v => v == 5);
// Build sorted distinct ranks high-to-low, treat Ace-low straight
var ranks = rankCounts.Keys.OrderByDescending(x => x).ToArray();
var highStraight = IsStraight(ranks);
// handle wheel straight (A-2-3-4-5)
if (!highStraight && ranks.Contains(14)) {
var lowRanks = ranks.Select(r => r==14?1:r).OrderByDescending(x => x).ToArray();
highStraight = IsStraight(lowRanks);
if (highStraight) ranks = lowRanks;
}
if (isFlush && highStraight) {
// straight flush
return (8, new[]{ranks.Max()});
}
// counts sorted by frequency then by rank
var freqList = rankCounts.OrderByDescending(kv => kv.Value)
.ThenByDescending(kv => kv.Key)
.Select(kv => new { Rank = kv.Key, Count = kv.Value })
.ToArray();
if (freqList[0].Count == 4) {
// Four of a kind
var kicker = freqList[1].Rank;
return (7, new[]{freqList[0].Rank, kicker});
}
if (freqList[0].Count == 3 && freqList.Length > 1 && freqList[1].Count == 2) {
// Full house
return (6, new[]{freqList[0].Rank, freqList[1].Rank});
}
if (isFlush) {
var ordered = cards.Select(c => (int)c.Rank).OrderByDescending(x => x).ToArray();
return (5, ordered);
}
if (highStraight) {
return (4, new[]{ranks.Max()});
}
if (freqList[0].Count == 3) {
// Three of a kind; kickers follow
var kickers = freqList.Skip(1).Select(x => x.Rank).OrderByDescending(x=>x).ToArray();
return (3, new[]{freqList[0].Rank}.Concat(kickers).ToArray());
}
if (freqList[0].Count == 2 && freqList[1].Count == 2) {
// Two pair
var highPair = freqList[0].Rank;
var lowPair = freqList[1].Rank;
var kicker = freqList.Skip(2).Select(x=>x.Rank).First();
return (2, new[]{highPair, lowPair, kicker});
}
if (freqList[0].Count == 2) {
// One pair
var pairRank = freqList[0].Rank;
var kickers = freqList.Skip(1).Select(x => x.Rank).OrderByDescending(x=>x).ToArray();
return (1, new[]{pairRank}.Concat(kickers).ToArray());
}
// High card
var highCards = cards.Select(c => (int)c.Rank).OrderByDescending(x => x).ToArray();
return (0, highCards);
}
private static bool IsStraight(IEnumerable orderedDescendingRanks) {
var ranks = orderedDescendingRanks.Distinct().OrderByDescending(x => x).ToArray();
if (ranks.Length != 5) return false;
for (int i = 0; i < 4; i++) {
if (ranks[i] - ranks[i+1] != 1) return false;
}
return true;
}
// Evaluate best 5-card hand from up to 7 cards
public static (int category, int[] key) EvaluateBest(IEnumerable cards) {
var arr = cards.ToArray();
if (arr.Length < 5 || arr.Length > 7) throw new ArgumentException("Use 5 to 7 cards.");
int bestCat = -1; int[] bestKey = null;
// brute-force combinations C(n,5) small (max 21 combos for 7)
var comb = Combinatorics.Combinations(arr, 5);
foreach (var c in comb) {
var res = Evaluate5(c);
if (res.category > bestCat || (res.category == bestCat && CompareKeys(res.key, bestKey) > 0)) {
bestCat = res.category; bestKey = res.key;
}
}
return (bestCat, bestKey);
}
private static int CompareKeys(int[] a, int[] b) {
if (a == null) return -1;
if (b == null) return 1;
for (int i = 0; i < Math.Min(a.Length,b.Length); i++) {
if (a[i] != b[i]) return a[i].CompareTo(b[i]);
}
return a.Length.CompareTo(b.Length);
}
}
// Small combinatorics helper
public static class Combinatorics {
public static IEnumerable Combinations(T[] arr, int k) {
int n = arr.Length;
int[] idx = Enumerable.Range(0,k).ToArray();
while (true) {
yield return idx.Select(i => arr[i]).ToArray();
int i;
for (i = k - 1; i >= 0; i--) {
if (idx[i] != i + n - k) break;
}
if (i < 0) yield break;
idx[i]++;
for (int j = i + 1; j < k; j++) idx[j] = idx[j - 1] + 1;
}
}
}
Notes about the example
- The Evaluate5 method is explicit and easy to reason about. It returns a category integer and an ordered key array for tie-breaking.
- EvaluateBest demonstrates the combinatorial approach for 7-card hands. It's simple and correct but can be made much faster.
- This code is ideal for clarity, unit testing, and correctness checks. Performance optimizations are the next step when necessary.
Optimizations for production
If you need thousands to millions of evaluations per second, consider these optimizations:
- Precompute lookup tables for 5-card hands (Cactus Kev style). This can reduce evaluation to table lookups.
- Use bitboard encodings: one 16-bit mask per suit, or a 64-bit key encoding ranks and suits to evaluate patterns with bit operations.
- Avoid allocations and arrays in hot paths: use spans, stackalloc, or pooled arrays.
- Parallelize evaluation with care: ensure thread-safety of any shared lookup tables and avoid false sharing.
A commonly used high-performance technique multiplies rank-specific primes and uses product lookups; while clever, it requires robust testing and careful table generation. If you prefer not to reinvent these techniques, there are open-source evaluators to study and adapt. For example, community implementations and posts often tagged with poker hand evaluator c# provide optimizations and prebuilt tables suitable for high-throughput servers.
Testing and validation
Correctness is non-negotiable. Test your evaluator with:
- All unique 5-card combinations (2,598,960 possibilities) — can be used as a gold standard for 5-card evaluation
- Randomized 7-card full-deck tests and head-to-head comparisons with a trusted evaluator
- Edge cases like Ace-low straights, duplicate cards (should be invalid), and suit ties
Unit tests should assert both category and tiebreaker order. Integration tests should simulate real-game scenarios and verify deterministic behavior across servers.
Designing for maintainability and reliability
An evaluator is often a foundational component for a gambling system or multiplayer game engine. Consider these best practices:
- Keep the API simple: EvaluateBest returns a stable, serializable ranking object.
- Document ranking rules explicitly — especially if you deviate from standard poker conventions for game variants.
- Expose deterministic tiebreaker details so audit logs and payouts can be traced.
- Protect against malformed inputs (duplicate cards, invalid ranks or suits) and log suspicious behavior in production.
Performance profiling and diagnostics
Before optimizing, profile. Use BenchmarkDotNet or Visual Studio's profiler to identify hotspots. Often the biggest wins are:
- Reducing allocations in hot loops
- Replacing LINQ in per-hand evaluation with manual loops
- Using smaller value types and readonly structs to reduce GC pressure
Example: switching from arrays allocated per call to a pooled scratch buffer cut GC time dramatically in one of my projects. For server-side evaluators, careful memory management yields clearer throughput improvements than micro-optimizing arithmetic.
Deploying in distributed systems
If your evaluator runs across game servers, ensure deterministic results and versioning. A change in tie-break rules or table generation must be deployed synchronously or guarded by version checks to avoid inconsistent payouts. Add telemetry around evaluation counts, average latency, and error rates.
Real-world anecdote
When I implemented an evaluator for a small online tournament engine, the naive evaluator passed all unit tests but created a latency spike under load. I initially suspected network issues, but profiling showed hot allocation churn in per-hand LINQ operations. Converting core paths to span-based loops and precomputed rank tables cut per-hand evaluation time by ~70% and resolved the issue. Investing a few days to profile and refactor yielded far better user experience than chasing micro-optimizations early on.
Further reading and resources
There are many community-written evaluators and reference articles (Cactus Kev's evaluator, bitboard strategies, prime-product lookups). Study those implementations to learn different trade-offs, and always test thoroughly before you deploy.
Conclusion
Creating a robust poker hand evaluator in C# is both an engineering and design exercise. Start with a clear, correct implementation like the one above, then profile and optimize only where necessary. Maintainability, exhaustive testing, and deterministic behavior across deployments are as important as raw throughput. If you want practical examples or community-driven libraries to compare, check the linked resources and sample implementations to accelerate your development.
About the author
I've spent years building card-game logic and server components for multiplayer applications. This guide summarizes practical lessons learned in production systems: prioritize correctness, measure performance with real workloads, and keep your evaluator's interface simple and auditable.
Happy coding — may your evaluators be fast, correct, and well-tested.