IT Blog

Интересные факты из аналитики, разработки, автоматизации и AI

← Все статьи

Комбинаторика покера: почему фулл-хаус реже флеша, хотя интуиция обещает обратное

Порядок покерных комбинаций — пара, две пары, сет, стрит, флеш, фулл-хаус, каре — не придуман для баланса игры. Он строго соответствует тому, насколько редко каждая комбинация встречается среди всех возможных 5-карточных рук.

Колода из 52 карт даёт ровно C(52, 5) = 52! / (5! · 47!) = 2 598 960 различных пятикарточных рук, и каждая из них попадает ровно в одну категорию — пара, флеш, каре и так далее. Поэтому вероятность любой комбинации — это просто отношение числа рук этой категории к 2 598 960.

Точный расчёт: перебираем все 2 598 960 рук

Вместо того чтобы искать формулу для каждой комбинации отдельно, переберём все сочетания 5 карт из 52 через itertools.combinations и для каждой определим тип руки. Единственная тонкость — стрит «колесо» (А-2-3-4-5), где туз считается младшей картой, и отдельная проверка на роял-флеш как частный случай стрит-флеша.

from itertools import combinations
from collections import Counter

RANKS = '23456789TJQKA'
SUITS = 'CDHS'
DECK = [r + s for r in RANKS for s in SUITS]

def classify(hand):
    rank_vals = sorted((RANKS.index(r) for r, s in hand), reverse=True)
    suits = {s for r, s in hand}
    counts = Counter(rank_vals)
    count_vals = sorted(counts.values(), reverse=True)

    is_flush = len(suits) == 1
    unique_vals = sorted(set(rank_vals))
    is_straight = (len(unique_vals) == 5 and unique_vals[-1] - unique_vals[0] == 4) \
        or unique_vals == [0, 1, 2, 3, 12]  # колесо А-2-3-4-5
    is_royal = is_straight and is_flush and unique_vals == [8, 9, 10, 11, 12]

    if is_royal:
        return 'Роял-флеш'
    if is_straight and is_flush:
        return 'Стрит-флеш'
    if count_vals == [4, 1]:
        return 'Каре'
    if count_vals == [3, 2]:
        return 'Фулл-хаус'
    if is_flush:
        return 'Флеш'
    if is_straight:
        return 'Стрит'
    if count_vals == [3, 1, 1]:
        return 'Сет'
    if count_vals == [2, 2, 1]:
        return 'Две пары'
    if count_vals == [2, 1, 1, 1]:
        return 'Пара'
    return 'Старшая карта'

result = Counter()
for hand in combinations(DECK, 5):
    result[classify(hand)] += 1

total = sum(result.values())
order = ['Роял-флеш', 'Стрит-флеш', 'Каре', 'Фулл-хаус', 'Флеш', 'Стрит',
         'Сет', 'Две пары', 'Пара', 'Старшая карта']

for name in order:
    count = result[name]
    odds = f'1 к {round(total / count):,}'
    print(f'{name:14s} {count:>10,}   {count/total:>8.4%}   {odds}')

print(f'\nВсего рук: {total:,}')
Роял-флеш               4    0.0002%   1 к 649,740
Стрит-флеш             36    0.0014%   1 к 72,193
Каре                  624    0.0240%   1 к 4,165
Фулл-хаус           3,744    0.1441%   1 к 694
Флеш                5,108    0.1965%   1 к 509
Стрит              10,200    0.3925%   1 к 255
Сет                54,912    2.1128%   1 к 47
Две пары          123,552    4.7539%   1 к 21
Пара            1,098,240   42.2569%   1 к 2
Старшая карта   1,302,540   50.1177%   1 к 2

Всего рук: 2,598,960

Перебор проходит все 2 598 960 рук без потерь и задвоений (сумма по категориям равна общему числу рук), а порядок, который получился по частоте, совпадает с официальным рангом комбинаций в покере. Это не совпадение: правила игры отражают именно эту таблицу.

Комбинация Число рук Вероятность Один шанс из
Роял-флеш 4 0,0002 % 649 740
Стрит-флеш 36 0,0014 % 72 193
Каре 624 0,0240 % 4 165
Фулл-хаус 3 744 0,1441 % 694
Флеш 5 108 0,1965 % 509
Стрит 10 200 0,3925 % 255
Сет 54 912 2,1128 % 47
Две пары 123 552 4,7539 % 21
Пара 1 098 240 42,2569 % 2
Старшая карта 1 302 540 50,1177 % 2

Где интуиция обманывает

Флеш (пять карт одной масти) визуально выглядит «строже» стрита (пять карт подряд разных мастей) — кажется, что подобрать одну масть сложнее, чем подобрать последовательность. Перебор подтверждает именно это: стрит выпадает 10 200 раз, флеш — только 5 108, то есть флеш почти вдвое реже. Здесь интуиция и правила игры совпадают.

А вот что интуиция обычно упускает — это масштаб. 92,4 % всех возможных покерных рук — это старшая карта или одна пара (50,12 % + 42,26 %). Всё остальное — две пары, сет, стрит, флеш, фулл-хаус, каре, стрит-флеш — вместе занимает меньше 8 % пространства исходов. Именно поэтому одна пара на руках в реальной игре часто оказывается сильнее большинства других раздач: у подавляющего большинства случайных раздач нет даже пары.

Monte Carlo: сходится ли оценка к точному значению

Точный перебор возможен, потому что 2,6 миллиона комбинаций — это ещё подъёмный объём для компьютера. Для более сложных вопросов (например, вероятностей в Техасском холдеме с учётом чужих карт) перебор быстро становится неподъёмным, и вместо него используют случайную выборку — Monte Carlo. Проверим на уже известном ответе: раздадим случайные пятикарточные руки много раз подряд и посмотрим, как оценка вероятности фулл-хауса ведёт себя с ростом числа попыток.

import random

random.seed(42)

def random_hand():
    return random.sample(DECK, 5)

exact = result['Фулл-хаус'] / total
print(f'Точная вероятность фулл-хауса: {exact:.4%}\n')

for n in [1_000, 10_000, 100_000, 1_000_000]:
    hits = sum(1 for _ in range(n) if classify(random_hand()) == 'Фулл-хаус')
    est = hits / n
    print(f'{n:>9,} раздач:   оценка {est:.4%}   отклонение {abs(est - exact):.4%}')
Точная вероятность фулл-хауса: 0.1441%

    1,000 раздач:   оценка 0.1000%   отклонение 0.0441%
   10,000 раздач:   оценка 0.1100%   отклонение 0.0341%
  100,000 раздач:   оценка 0.1240%   отклонение 0.0201%
1,000,000 раздач:   оценка 0.1495%   отклонение 0.0054%

Отклонение оценки от точного значения в целом убывает с ростом числа раздач — это закон больших чисел в действии, хотя убывание не гарантированно монотонно на каждом шаге: это случайный процесс, а не детерминированная сходимость. Monte Carlo даёт право на ошибку в обмен на то, что вообще не нужно перечислять все исходы — размен, который становится обязательным, как только точный перебор перестаёт помещаться в разумное время.

Та же комбинаторика — на столе, а не в коде

Термин «ауты» (outs) в живой игре — это то же самое подсчётное рассуждение, просто без полного перебора. Если после флопа у игрока четыре карты одной масти, до флеша не хватает одной карты, и она может быть любой из 9 оставшихся карт этой масти среди 47 карт, которые ещё не видны (52 минус 5 своих и на столе). Вероятность добрать флеш на следующей карте — 9 / 47 ≈ 19,1 %, тот же принцип «нужный исход делить на все возможные», что и в переборе всей колоды, только для одной конкретной ситуации, а не для всех рук сразу.

Чек-лист

  1. Можно ли явно перечислить все исходы? Тогда вероятность — это просто отношение «сколько подходит» к «сколько всего».
  2. Проверена ли полнота перебора — сумма по категориям равна общему числу исходов, ни один вариант не пропущен и не задвоен?
  3. Если перебор всех исходов физически неподъёмен — заменяется ли он случайной выборкой (Monte Carlo) с явной оценкой того, насколько оценка ещё может отклоняться от истинного значения?
  4. Совпадает ли результат подсчёта с интуицией — а если нет, то интуиция ошибается в структуре события или просто недооценивает масштаб?

Вывод

Ранжирование покерных комбинаций не результат договорённости игроков — это прямое следствие комбинаторики колоды, которое можно вывести с нуля перебором 2,6 миллиона рук. Там, где перебор всех исходов подъёмен, он даёт точный ответ без всяких предположений. Там, где неподъёмен — Monte Carlo даёт оценку, которая с ростом числа попыток приближается к тому же ответу, но остаётся приближением, а не точным значением.

Интуиция хорошо угадывает порядок редкости, но почти всегда врёт в масштабе — вот почему стоит хотя бы иногда пересчитывать то, что «и так понятно».
← Все статьи