Poker Simulator in Python
Designing a poker simulator in python is a great way to get some practice and experience in various aspects of the python language and have fun doing it.
Through this exercise you will gain an understanding of classes, dictionaries, list and dictionary comprehension, writing to csv files, and Poker of course.
Let’s start with the objective: Our objective is to write a program that will simulate a game of Texas hold’em poker between a specified number of players. Through this simulation we will try to learn what starting hands in Poker give us the best chance of winning a round. Of course, if you just want to learn poker strategy you can go somewhere else and get these answers. The goal here is to write our own program to figure it out.
In this article I will explain how I approached this problem, and the various stages and steps in development. When I thought about this problem, the logical starting point for me was with the Card.
Card
The card is the fundamental unit of poker. Without the concept of a Card you don’t have anything. So, the first step was to create a class named Card.
class Card:
static_suites = ["Heart", "Club", "Diamond", "Spade"]
static_cardvalues = [str(n) for n in range(2, 10)] + ["T", "J", "Q", "K", "A"]
def __init__(self, suite, value):
if suite not in Card.static_suites:
raise ValueError("Invalid suite " + suite)
if value not in Card.static_cardvalues:
raise ValueError("Invalid card value " + value)
self.suite = suite
self.value = value
def __sub__(self, other):
""" subtract rank / positional value of cards, positive result means
left operand higher card than right """
if self.value == "A" and other.value == "2":
return -1
if self.value == "2" and other.value == "A":
return 1
return Card.static_cardvalues.index(self.value) - Card.static_cardvalues.index(other.value)
def __gt__(self, other):
""" is this (self) a higher card than the other card """
return Card.static_cardvalues.index(self.value) > Card.static_cardvalues.index(other.value)
def __ge__(self, other):
""" is this (self) card higher or equal to other card """
return Card.static_cardvalues.index(self.value) >= Card.static_cardvalues.index(other.value)
def __lt__(self, other):
""" is this (self) a lower card than the other card """
return Card.static_cardvalues.index(self.value) < Card.static_cardvalues.index(other.value)
def __le__(self, other):
""" is this (self) a lower or equal to the other card """
return Card.static_cardvalues.index(self.value) <= Card.static_cardvalues.index(other.value)
def __eq__(self, other):
""" is this (self) card equal to the other card """
return Card.static_cardvalues.index(self.value) == Card.static_cardvalues.index(other.value)
def __str__(self):
match self.suite:
case "Heart":
return '\u2665' + self.value
case "Club":
return '\u2663' + self.value
case "Spade":
return '\u2660' + self.value
case "Diamond":
return '\u2666' + self.value
case _:
raise ValueError("Invalid card suite")
def __repr__(self):
return str(self)
def compare(self, other):
"""
return 1 if greater, 2 if smaller, 0 if same
:param other: another Card object
:return: 1, 2 or 0
"""
if self > other:
return 1
elif self < other:
return 2
else:
return 0
In this class I created two static class variables to store the range of valid card values and suites. The class itself is very simple and only stores the value and suite of a card. e.g. J♠. The rest of the functions are overrides for built-in methods to enable Cards to be compared to each other (__gt__, __lt__, __eq__), printed on screen (__str__, __repr__).
Comparing cards is straight forward. In poker suites don’t matter (i.e., a Heart is no better than a Spade), so they are ignored. The values of the cards are compared. But how do you compare 9 to Ace (9 == “A”) in code? This is where the static variables I defined come in handy. The variable static_cardvalues is storing the values of the cards in ascending order. What this means then is that rather than comparing values directly we compare their positions in the sorted array. A card at index 9 is better than a card at index 2.
static_cardvalues = [str(n) for n in range(2, 10)] + ["T", "J", "Q", "K", "A"]
Once a Card is defined we can then define a Deck, poker hands, players, and rules of poker.
Deck
The Deck class is very simple, it is simply a list of Cards. The Deck constructor takes no arguments, the deck will contain one Card of every suite and every value.
from card import Card
import random
class Deck:
def __init__(self):
self._deck = []
for s in Card.static_suites:
for c in Card.static_cardvalues:
self._deck.append(Card(s, c))
def shuffle(self):
for _ in range(random.randint(1, 5)):
random.shuffle(self._deck)
def get_cards(self):
"""return list of cards in deck"""
return self._deck
def dealcard(self):
return self._deck.pop()
def __len__(self):
return len(self._deck)
def __str__(self):
return " ".join([str(crd) for crd in self._deck])
Methods are defined to shuffle the deck, get or print cards in the deck, and deal cards from the deal.
The shuffle() method is written to not only randomly shuffle the deck, but also randomize the number of times it is shuffled. Just as in real life 😊
The dealcard() method will permanently reduce the size of the deck, just as in real life. Once a card is dealt, the deck gets smaller.
Player
A player is a participant in the game of poker. My player class stores a name, the player’s hole cards (the two cards you are dealt at the start of a round), and the player best possible poker hand constructed from the two hole cards, and five community cards on the table. The class is importing a bunch of methods from the module poker_rules.py which is defining how poker hands are compared and ranked, more on this later.
from poker_rules import *
from itertools import combinations
class Player:
def __init__(self, name):
self.name = str(name)
self._holeCards = []
self._bestHand = None
def __str__(self):
if not self._bestHand:
return self.name + ":" + str(self._holeCards)
else:
return self.name + ":" + str(self._bestHand) + "," + categorize_hand(self._bestHand)
def __repr__(self):
return str(self)
def add_card(self, c):
if len(self._holeCards) < 2:
self._holeCards.append(c)
else:
raise ValueError("Player can only have two hole cards")
def update_best_hand(self, table):
"""
return the best 5 card hand possible for player
using a combination of hole cards and community cards
:param table: list of Cards on the table
:return: best possible hand for player
:rtype: list[Card]
"""
if len(table) < 3:
raise ValueError("table has insufficient community cards")
if len(table) >= 3:
lst_hands = [list(combo) for combo in combinations(self._holeCards + table, 5)]
self._bestHand = best_hand(lst_hands)
self._bestHand.sort(reverse=True)
return self._bestHand
def get_holecards(self):
return self._holeCards
def get_best_hand(self):
if not self._bestHand:
raise ValueError("Best hand undetermined. Call update_best_hand")
return self._bestHand
def get_holecards_pokernotation(self):
"""
return a string representation of the holecards in conventional poker notation
e.g. 'AA', 'AKs', 'KQo'
:return: two or three character string:
each capitalized character represents the value of a card
non-pairs are classified as 's' or 'o' where s means suited, and o means offsuite
:rtype: string
"""
self._holeCards.sort(reverse=True)
poker_notation = self._holeCards[0].value + self._holeCards[1].value
if poker_notation[0] == poker_notation[1]:
return poker_notation
else:
if self._holeCards[0].suite == self._holeCards[1].suite:
poker_notation = poker_notation + "s"
else:
poker_notation = poker_notation + "o"
return poker_notation
The method get_holecard_pokernotation() returns the players hole cards as a compact string specifying if the cards are pocket pairs (.e.g. AA, 99) suited (e.g. AKs) or off-suite (QTo). This poker shorthand is common in online poker analytics, and I use it in the program to collect winrate statistics.
The method update_best_hand(self, table) is one of the key functions that enable the simulation. In my simulation I am assuming that the round has progressed until the river card has been dealt so that there are five cards on the table.
Now this is where programming becomes very interesting. There are 5 cards on the table, and 2 in your hand, for a total of 7 cards. The goal of poker is to build the strongest poker hand of 5 cards from these 7 cards. There are 7C5 = 21 possible 5 card combinations. When I play poker, I’m not consciously evaluating each of the 21 possible combos to decide on the best hand. Instead I (and I assume most other humans) use the process of elimination. First we scan and check if a straight or flush is possible. If not, check for repeated cards to build four of a kind, full house, or pairs and two pairs. When all of these yield nothing, we know that we’ve lost the round 😂
While I am using the process of elimination later on in my program when I compare poker hands from two different players, to decide on the best hand for a given player, I simply use brute force and check each of the 21 possible 5 card combinations to find the best among them.
Why? Why don’t I try to implement some fancy algorithm that models human thinking in my program? Am I not being inefficient using a brute force approach?
Because that would be risky, complex and error prone. There is no guarantee that your fancy algorithm is actually working properly, since validating it would be challenging. There are 52C5 = 2,598,960 possible five card poker hands from a deck of 52 cards. How can you prove that your clever algorithm that does not use an exhaustive search (brute force) will always find the best hand from any given subset of 21 hands.
And there is nothing wrong with using a brute force method. Computers are GOOD at brute force, it’s the reason they exist. Also, we’re talking about 21 combinations, not 21 billion. Some perspective is necessary.
Rules of Poker
The poker_hands and poker_rules modules are the heart of the simulation.
poker_hands.py
from itertools import pairwise
hand_categories = ("RoyalFlush",
"StraightFlush",
"FourofaKind",
"FullHouse",
"Flush",
"Straight",
"ThreeofaKind",
"TwoPair",
"Pair",
"HighCard")
def is_royal_flush(hand):
"""
check if hand contains royal flush
:return: status, True for royal flush and sorted hand
:rtype: tuple(True, hand) or tuple(False, None)
"""
hand.sort(reverse=True)
b_royal = is_straight_flush(hand)[0] \
and hand[0].value == "A" \
and hand[-1].value == "T"
if b_royal:
return True, hand
else:
return False, None
def is_straight_flush(hand):
"""
check if hand contains straight flush
:return: status, True for straight flush and sorted hand
:rtype: tuple(True, hand) or tuple(False, None)
"""
hand.sort(reverse=True)
b_st_flush = is_flush(hand)[0] and is_straight(hand)[0]
if b_st_flush:
return True, hand
else:
return False, None
def is_fourkind(hand):
"""
check if hand contains four of a kind
:return: status, True for four of a kind and sorted hand
:rtype: tuple(True, hand) or tuple(False, None)
"""
hand.sort(reverse=True)
b_foundKind = hand.count(hand[0]) == 4 or hand.count(hand[-1]) == 4
if b_foundKind:
return True, hand
else:
return False, None
def is_fullhouse(hand):
"""
check if hand contains full house
:return: status, True for full house and sorted hand
:rtype: tuple(True, hand) or tuple(False, None)
"""
b_isFullHouse, tres = is_threekind(hand)
if b_isFullHouse:
other_two = [c for c in hand if c not in tres]
duo = is_pair(other_two)
b_isFullHouse = b_isFullHouse and duo[0]
if b_isFullHouse:
return True, hand
else:
return False, None
def is_flush(hand):
"""
check if hand contains flush
:return: status, True for flush and sorted hand
:rtype: tuple(True, hand) or tuple(False, None)
"""
hand.sort(reverse=True)
suites_in_hand = [c.suite for c in hand]
b_isFlush = suites_in_hand.count(suites_in_hand[0]) == len(suites_in_hand)
if b_isFlush:
return True, hand
else:
return False, None
def is_straight(hand):
"""
check if hand contains straight
:return: status, True for straight and sorted hand
:rtype: tuple(True, hand) or tuple(False, None)
"""
hand.sort(reverse=True)
if hand[0].value == "A" and hand[1].value.isdigit():
# if Ace in hand but no other face cards, Ace treated as 1
hand = hand[1:] + hand[0:1]
card_pairs = list(pairwise(hand))
deltas = [c1 - c2 for c1, c2 in card_pairs]
if deltas.count(1) == 4:
return True, hand
else:
return False, None
def is_threekind(hand):
"""
check if hand contains three of a kind
:return: status and three of a kind cards
:rtype: tuple(True, (Card,Card,Card)) or tuple(False, None)
"""
hand.sort(reverse=True)
for i in range(3):
tres = hand[i:i + 3]
if tres[0].value == tres[1].value == tres[2].value:
return True, tres
return False, None
def is_twopair(hand):
"""
check if hand contains two pairs
:return: status and pairs of cards if they exist
:rtype: tuple(True, (Card,Card, Card, Card)) or tuple(False, None)
"""
hand.sort(reverse=True)
card_pairs = list(pairwise(hand))
two_pairs = []
skipNext = False
for c1, c2 in card_pairs:
if skipNext:
skipNext = False
elif c1.value == c2.value:
two_pairs.extend([c1, c2])
skipNext = True
# to avoid detecting a triple as two pair, skip the next overlapping pair
if len(two_pairs) == 4:
return True, two_pairs
else:
return False, None
def is_pair(hand):
"""
check if hand contains pair
:return: status and pair of cards if they exist
:rtype: tuple(True, (Card,Card)) or tuple(False, None)
"""
hand.sort(reverse=True)
card_pairs = list(pairwise(hand))
for c1, c2 in card_pairs:
if c1.value == c2.value:
return True, (c1, c2)
return False, None
def is_highcard(hand):
"""
check if hand is of type HighCard and return the high card
:param hand: a list of five Cards
:return: tuple(True, Card) Card is the highest card in hand
:rtype: tuple(bool, Card)
"""
hand.sort(reverse=True)
return True, hand[0]
poker_rules.py
from poker_hands import *
category_funcs = (is_royal_flush,
is_straight_flush,
is_fourkind,
is_fullhouse,
is_flush,
is_straight,
is_threekind,
is_twopair,
is_pair,
is_highcard)
category_func_dict = dict(zip(hand_categories, category_funcs))
def best_hand(lst_hands):
"""
given a set of poker hands, return the hand with the highest value
:param lst_hands: list of hands, a hand is a list of five Cards
:return: best hand in list
:rtype: list[Card]
"""
if len(lst_hands) < 2:
return lst_hands[0]
elif len(lst_hands) == 2:
match compare_hands(lst_hands[0], lst_hands[1]):
case 0:
# both hands are tied
return lst_hands[0]
case 1:
return lst_hands[0]
case 2:
return lst_hands[1]
else:
# divide list into two sublists and recursively call function on each half list
left = best_hand(lst_hands[:len(lst_hands) // 2])
right = best_hand(lst_hands[len(lst_hands) // 2:])
return best_hand([left, right])
def compare_hands(hand1, hand2):
"""
a poker hand is a collection of 5 Cards
compare two hands to decide which one is better
:param hand1: list of five Cards, 1st hand
:param hand2: list of five Cards, 2nd hand
:return: which hand is better (first=1, second=2, tie=0)
:rtype: int
"""
global hand_categories
h1_category = categorize_hand(hand1)
h2_category = categorize_hand(hand2)
if hand_categories.index(h1_category) < hand_categories.index(h2_category):
return 1
elif hand_categories.index(h1_category) > hand_categories.index(h2_category):
return 2
else:
# both hands same category
if h1_category == "RoyalFlush":
return 0 # royal flush is the highest hand
elif h1_category == "StraightFlush" or h1_category == "Straight" or h1_category == "Flush":
# hands sorted largest to smallest card, therefore only need to compare
# the first card in list to determine which hand is better for five card hands
return hand1[0].compare(hand2[0])
elif h1_category == "FourofaKind": # not a five card hand, must compare the quad then kicker
# first card could be kicker or part of the quad,
# so compare the second card which is always part of the quad
h1_cmp_h2 = hand1[1].compare(hand2[1])
if h1_cmp_h2 == 0:
if hand1[0] == hand1[1]:
return hand1[-1].compare(hand2[-1]) # the last card was the kicker
else:
return hand1[0].compare(hand2[0]) # the first card was the kicker
else:
return h1_cmp_h2
elif h1_category == "FullHouse": # not a five card hand, must compare the triple then pair
# third card is always part of the triple regardless of whether pair is larger or smaller
h1_cmp_h2 = hand1[2].compare(hand2[2])
if h1_cmp_h2 == 0:
if hand1[1] == hand1[2]:
return hand1[-1].compare(hand2[-1]) # the last two are the pair
else:
return hand1[0].compare(hand2[0]) # the first two are the pair
else:
return h1_cmp_h2
elif h1_category == "ThreeofaKind":
# third card is always part of the triple regardless of other two
h1_cmp_h2 = hand1[2].compare(hand2[2])
if h1_cmp_h2 == 0:
if hand1[1] == hand1[2]:
# triple is last 3 cards
match hand1[0].compare(hand2[0]):
case 1: return 1
case 2: return 2
case 0: return hand1[1].compare(hand2[1])
else:
# triple is first 3 cards
match hand1[-2].compare(hand2[-2]):
case 1:
return 1
case 2:
return 2
case 0:
return hand1[-1].compare(hand2[-1])
else:
return h1_cmp_h2
elif h1_category == "TwoPair":
b, h1_twop = is_twopair(hand1)
b, h2_twop = is_twopair(hand2)
h1_cmp_h2 = h1_twop[0].compare(h2_twop[0]) # first pair
if h1_cmp_h2 == 0:
cmp = h1_twop[2].compare(h2_twop[2]) # second pair
if cmp != 0:
# compare kicker
k1, k2 = None, None
for c in hand1:
if c not in h1_twop:
k1 = c
break
for c in hand2:
if c not in h2_twop:
k2 = c
break
return k1.compare(k2)
else:
return cmp
else:
return h1_cmp_h2
elif h1_category == "Pair":
b, h1_pair = is_pair(hand1)
b, h2_pair = is_pair(hand2)
h1_cmp_h2 = h1_pair[0].compare(h2_pair[0]) # compare pairs
if h1_cmp_h2 == 0:
for l, r in zip(hand1, hand2):
cmp = l.compare(r)
if cmp != 0:
return cmp
return 0
else:
return h1_cmp_h2
else:
for l, r in zip(hand1, hand2):
cmp = l.compare(r)
if cmp != 0:
return cmp
return 0
def categorize_hand(hand):
"""
assign a category to a poker hand
:param hand: a list of five Cards
:return: category of poker hand, one of
["RoyalFlush", "StraightFlush", "FourofaKind", "FullHouse",
"Flush", "Straight", "ThreeofaKind", "TwoPair", "Pair", "HighCard"]
:rtype: str
"""
global category_func_dict
for category, func in category_func_dict.items():
match, h = func(hand)
if match:
return category
The function categorize_hand(hand) is used to determine the type of the specified poker hand. This is done by repeatedly applying the various test functions from poker_hands.py (is_royal_flush, is_straight_flush…) to the specified hand until one of them returns True. The tests are applied in descending order of rarity/value by checking if the poker hand is a Royal Flush, and if not whether it is a Straight Flush and so on, down the list.
The objective of the categorize_hand(hand) function is to find the highest value category that a hand can be assigned to. For example: A straight flush meets the definition of a straight, a flush and a straight flush, but it is important to properly categorize the hand as a straight flush and not either of the other two categories since a straight flush beats a straight and a flush.
The function best_hand(lst_hands) uses a technique called recursive binary search. The function is provided with a list of poker hands. The list contains the best poker hand from each player, with which they hope to win the round. The function divides lst_hands into two halves and recursively call itself on each half, until the input list length is ≤ 2. If there are two hands in the list it will call compare_hands(hand1, hand2) to determine which of the two is better, and if there is only one hand in the list, it simply returns that hand since it cannot be compared against anything. And finally, the best hand from each half is compared to determine the ultimate best hand.
Let’s do a mock run of best_hands function for an input lst_hands of size 4 [h1, h2, h3, h4]. Suppose h3 was the best hand.
best_hands([h1, h2, h3, h4]) -> spawns two calls:
left half: best_hands([h1, h2]) -> h1 is better
right half: best_hands([h3, h4]) -> h3 is better
finally compare_hands(h1, h3) -> h3 is better
Why did I use recursive binary search? I cannot claim that it is the absolute best method, but it IS intuitive, and the code is easy to express, and understand.
The best analogy for a binary recursive search is to think of a single elimination sports tournament bracket. The best_hands function essentially creates a tournament for poker hands and the best hand rises to the top through successive competition and elimination.
Simulation
A few more modules are required to complete the simulation.
poker_stats.py
from card import Card
from itertools import combinations
def create_stats_dict(starting_hands_stats, stats_dict):
"""
updated input dictionary with 169 keys representing the unique starting hands in poker
each key maps to a nested statistics dictionary
:param starting_hands_stats: empty dict
:return: None
"""
pocket_pairs = [c*2 for c in Card.static_cardvalues]
combos = list(combinations(Card.static_cardvalues, 2)) # 13C2 = 78 two card combinations
suited_hands = [b+a+'s' for a,b in combos] # 78 suited two card combinations
offsuite_hands = [b+a+'o' for a,b in combos] # any two offsuite cards
starting_hands_stats.update({k: stats_dict.copy() for k in pocket_pairs + suited_hands + offsuite_hands})
table.py
from player import Player
from poker_rules import best_hand
def winning_player(players: Player):
"""
given a list of players, return the winners who have the best poker hand
:param players: list of [Player] objects
:return: list of players that won
:rtype: list[Player]
"""
player_best_hands = [p.get_best_hand() for p in players]
winning_hand = best_hand(player_best_hands)
winners = [p for p in players if p.get_best_hand() == winning_hand]
return winners
main.py
from deck import Deck
from table import *
from poker_stats import *
import csv
starting_hands_stats = {}
# dict to track win/loss stats
hand_stats = {'won': 0, 'played': 0}
create_stats_dict(starting_hands_stats, hand_stats)
num_simulations = 100000
num_players = 8
for n in range(2, num_players+1):
for i in range(num_simulations):
print(f"game{i}")
# define a deck of cards
play_deck = Deck()
play_deck.shuffle()
assert len(play_deck) == 52
# set players
players = [Player(f"Player{p}") for p in range(n)]
# deal cards
for _ in range(2):
for p in players:
p.add_card(play_deck.dealcard())
for p in players:
p_hand = p.get_holecards_pokernotation()
starting_hands_stats[p_hand]['played'] += 1
play_deck.dealcard() # burn before flop
flop_cards = [play_deck.dealcard() for _ in range(3)]
# print("Flop:", flop_cards)
# print(players)
play_deck.dealcard() # burn before flop
turn_card = [play_deck.dealcard()]
# print("Turn:", turn_card)
play_deck.dealcard() # burn before flop
river_card = [play_deck.dealcard()]
# print("River:", river_card)
table = flop_cards + turn_card + river_card
# print("Table: ", table)
for p in players:
bh = p.update_best_hand(table)
# print(poker_rules.categorize_hand(bh), bh)
for p in winning_player(players):
p_hand = p.get_holecards_pokernotation()
starting_hands_stats[p_hand]['won'] += 1
with open(f"simulation_{n}.csv", 'w', newline='') as f:
cols = ['hand','won','played']
w = csv.DictWriter(f, cols)
w.writeheader()
for hand, stat in starting_hands_stats.items():
row = {"hand": hand}
row.update(stat)
w.writerow(row)
main.py is the entry point for the program. There are 169 unique starting hands in poker:
13 Pocket pairs: [AA, KK … 22]
78 Suited hands: [AKs , AQs, .. 32s]. A suited hand means that both player cards are of the same suite. E.g. Ace and King of Clubs.
78 Offsuite hands: [AKo, AQo, .. 32o]
The goal of the simulation is to determine the win rate of a given starting hand. The dictionary starting_hand_stats in main.py tracks win rate statistics of various starting hands during the simulation.
The simulation is run 100,000 times for 2 to 8 players. Each simulation run uses a newly created, randomly shuffled deck. At the end of the simulation the collected statistics are written to a csv file. The csv file contains three columns: hand, won, played.
I could have used python to further sort, process and visualize this data, and I still might do so in the future. But I was too excited to see the results, so I chose to quickly analyze them in excel instead.
I divide the won by played to calculate win rate. If you search on the web you will find that pocket Aces have a win rate of 85% when facing ONE opponent. But it is harder to find good data on the odds when facing multiple opponents, and the odds for other hands.
I will list the top 20 poker hands for a table of 2, 4, 6 and 8 players based on my simulation.
One thing to note, the odds below are for Win or Tie.
2 player: The results for the 2 player simulation match the poker odds matrix you will see on many websites, so this evidence that my simulator is working properly.
+------+------+--------+----------+
| hand | won | played | win rate |
+------+------+--------+----------+
| AA | 746 | 886 | 84.20% |
| KK | 774 | 945 | 81.90% |
| QQ | 742 | 957 | 77.53% |
| JJ | 629 | 841 | 74.79% |
| TT | 646 | 893 | 72.34% |
| 99 | 634 | 912 | 69.52% |
| 88 | 618 | 918 | 67.32% |
| AKs | 399 | 594 | 67.17% |
| AJs | 392 | 591 | 66.33% |
| A7s | 409 | 627 | 65.23% |
| AKo | 1196 | 1842 | 64.93% |
| AQo | 1169 | 1807 | 64.69% |
| 66 | 576 | 897 | 64.21% |
| AQs | 386 | 607 | 63.59% |
| KQs | 383 | 605 | 63.31% |
| QJs | 380 | 602 | 63.12% |
| KTs | 384 | 610 | 62.95% |
| 77 | 560 | 893 | 62.71% |
| ATs | 394 | 632 | 62.34% |
| A9o | 1057 | 1697 | 62.29% |
+------+------+--------+----------+
4 player
+------+------+--------+----------+
| hand | won | played | win rate |
+------+------+--------+----------+
| AA | 2878 | 3993 | 72.08% |
| KK | 2735 | 4153 | 65.86% |
| QQ | 2474 | 4010 | 61.70% |
| JJ | 2246 | 3978 | 56.46% |
| TT | 2127 | 4052 | 52.49% |
| AKs | 1318 | 2666 | 49.44% |
| 99 | 2004 | 4105 | 48.82% |
| AQs | 1367 | 2822 | 48.44% |
| ATs | 1352 | 2820 | 47.94% |
| 88 | 1980 | 4134 | 47.90% |
| KQs | 1322 | 2769 | 47.74% |
| AKo | 3853 | 8183 | 47.09% |
| AJs | 1272 | 2718 | 46.80% |
| AQo | 3775 | 8093 | 46.65% |
| KJs | 1232 | 2671 | 46.13% |
| QJs | 1262 | 2750 | 45.89% |
| KTs | 1219 | 2717 | 44.87% |
| 77 | 1793 | 4020 | 44.60% |
| A9s | 1174 | 2633 | 44.59% |
| A7s | 1226 | 2766 | 44.32% |
+------+------+--------+----------+
6 player
+------+------+--------+----------+
| hand | won | played | win rate |
+------+------+--------+----------+
| AA | 5507 | 8970 | 61.39% |
| KK | 4930 | 9091 | 54.23% |
| QQ | 4423 | 8917 | 49.60% |
| JJ | 3997 | 9083 | 44.01% |
| AKs | 2414 | 5962 | 40.49% |
| TT | 3680 | 9142 | 40.25% |
| AQs | 2397 | 6209 | 38.61% |
| ATs | 2337 | 6094 | 38.35% |
| KQs | 2293 | 6053 | 37.88% |
| KJs | 2259 | 6014 | 37.56% |
| 99 | 3353 | 8990 | 37.30% |
| AKo | 6753 | 18214 | 37.08% |
| AJs | 2218 | 6024 | 36.82% |
| QJs | 2203 | 6051 | 36.41% |
| AQo | 6501 | 17894 | 36.33% |
| KTs | 2117 | 5950 | 35.58% |
| 88 | 3203 | 9031 | 35.47% |
| A7s | 2102 | 5965 | 35.24% |
| A9s | 2097 | 5990 | 35.01% |
| QTs | 2065 | 6005 | 34.39% |
+------+------+--------+----------+
8 player:
+------+------+--------+----------+
| hand | won | played | win rate |
+------+------+--------+----------+
| AA | 8296 | 15863 | 52.30% |
| KK | 7231 | 15672 | 46.14% |
| QQ | 6372 | 15715 | 40.55% |
| JJ | 5670 | 15815 | 35.85% |
| AKs | 3614 | 10566 | 34.20% |
| TT | 5220 | 16000 | 32.63% |
| AQs | 3498 | 10800 | 32.39% |
| KQs | 3359 | 10510 | 31.96% |
| ATs | 3353 | 10620 | 31.57% |
| AJs | 3301 | 10477 | 31.51% |
| KJs | 3285 | 10552 | 31.13% |
| AKo | 9773 | 31798 | 30.73% |
| QJs | 3190 | 10551 | 30.23% |
| AQo | 9439 | 31471 | 29.99% |
| KTs | 3106 | 10489 | 29.61% |
| 99 | 4641 | 15772 | 29.43% |
| QTs | 3085 | 10506 | 29.36% |
| A7s | 3004 | 10428 | 28.81% |
| A9s | 3017 | 10516 | 28.69% |
| JTs | 2985 | 10414 | 28.66% |
+------+------+--------+----------+
All of the source code has been provided in this article.