Контринтуитивные события окружают нас повсюду. Взять, например, парадокс дней рождений: каждый раз, когда оказываешься в небольшой группе и узнаёшь, что у двух участников группы праздник в один и тот же день, то думаешь: какое удивительное совпадение, ведь кажется, что событие редкое. Но вот уже при детальном рассмотрении оказывается, что вероятность такого события достаточно высокая (например, если группа 23 человека, то вероятность составит 50%, для группы в 57 человек — 99%). Примеров контринтуитивных событий много и они удивительны тем, что их разоблачение позволяет нам менять своё мировосприятие. 

Вот и мы с друзьями затеяли спор об одном таком событии, связанным с игрой Мир танков. Суть спора: двое из нас считают один сервер «несчастливым», потому что при нашей средней статистике в 50%, именно на этом сервере происходят частые поражения. Третий друг считает, что не может быть такого, чтоб сервер был «несчастливым» и тем более, невозможно, чтобы средний процент побед всех игроков на нём был меньше 50%. 

Его аргументы: исход боя либо победа, либо поражение (ничью, которая является поражением для обеих команд — в расчёт не берём). Вот мы берём 30к игроков, они играют между собой (команды набираются по 15 игроков), допустим, 30 боёв, статистику каждого записываем и получаем средний процент побед игроков в 50% (кто‑то выиграл 10 из 30, кто‑то 20 из 30, тем самым, получаем среднее). И вроде всё так. Но в такой модели мы говорим: каждый обязан провести ровно 30 игр. В жизни так не бывает, существует очень много параметров, всех их не рассмотреть. Но я уверен, что даже один параметр уже будет смещать среднее вправо или влево от теоретических 50%. Самый главный параметр — количество игр. Все играют по‑разному. У кого‑то времени больше, у кого‑то меньше. И сначала я захотел провести эксперименты именно с этим параметром.

Логика эксперимента

Для начала опишу логику: что и как мы будем смотреть, проводить, измерять. Определимся с количеством игроков. Могу по опыту сказать, что в вечернее время на сервере играет около 30к игроков. То есть количество игроков онлайн 30 000. Это константа, одни игроки уходят, другие заходят, но онлайн в эксперименте всегда ровно 30 000. Сколько будет длиться эксперимент — количество игр/итераций равно 30. Исходя из опыта, 30 игр это примерно 4–5 часов реального времени, онлайн в 30к примерно столько и держится. Ожидание и подбор соперников из эксперимента убраны.

Как происходит эксперимент: играют все 30к одновременно, то есть сначала набираются матчи (в матче 30 игроков), получается 1000 матчей. В каждом матче игроки случайно формируют команды (15 в одной команде, оставшиеся 15 в другой). Проводится матч, игрокам, которые победили, записывается 1 победа — так ведётся статистика каждого игрока, сколько матчей он сыграл и сколько матчей он выиграл. После одного матча все игроки перемешиваются по матчам, потом по командам и опять проводится игра. Также, каждому игроку случайным образом присваивается параметр — сколько матчей он сыграет. Например, игрок с id 1 должен сыграть 10 матчей. После 10-го матча он «уходит» из игры (но его статистика сохраняется), на его место приходит игрок, который должен сыграть 5 матчей и так далее. Если осталась последняя игра (всего 30 игр), а пришёл игрок у которого параметр 10 игр — он сыграет всего 1 игру, и его статистика запишется. Параметр «количество игр одного игрока» будет описываться различными распределениями, о которых поговорим далее.

Нормальное распределение

Нормальное распределение — это непрерывное распределение вероятностей с пиком в центре и симметричными боковыми сторонами. Формула выглядит следующим образом:

f(x) = \frac{1}{\sigma \sqrt{2\pi}} \exp\left(-\frac{(x-\mu)^2}{2\sigma^2}\right)

Это распределение берут, когда есть какое‑то среднее, и мы считаем что 99,99% всех значений лежит в 3-х среднеквадратичных отклонениях вправо и влево. Вот и я в этом эксперименте решил использовать это распределение как параметр количества игр у игрока. 

Предполагаем, что в среднем игрок проводит 10 игр, значит, что дисперсия будет 3,5427, а среднее квадратичное отклонение 1,88. Ниже я оставлю код, чтобы вы могли воспроизвести этот эксперимент у себя. Следующие эксперименты будут отличаться только распределениями и их параметрами.

import random
import pandas as pd
import math
from scipy.stats import norm

# ========== КОНСТАНТЫ ==========
PRE_TOTAL_PLAYERS = 30000      # начальное количество активных игроков
TEAM_SIZE = 15
MATCHES_PER_ROUND = PRE_TOTAL_PLAYERS // (TEAM_SIZE * 2)  # 1000
TOTAL_ROUNDS = 30              # максимальное количество раундов (т.к. максимум 30)

# Параметры нормального распределения для max_games
MEAN_MAX_GAMES = 10.0
STD_MAX_GAMES = 1.88
MIN_MAX_GAMES = 3
MAX_MAX_GAMES = 30

# ========== ФУНКЦИЯ ГЕНЕРАЦИИ ЛИМИТА ==========
def generate_max_games():
    """Генерирует целое число боёв для нового игрока из усечённого нормального распределения."""
    # Генерируем значение из нормального распределения
    raw = norm.rvs(loc=MEAN_MAX_GAMES, scale=STD_MAX_GAMES)
    # Обрезаем до допустимого диапазона
    clamped = max(MIN_MAX_GAMES, min(MAX_MAX_GAMES, raw))
    # Округляем вверх до целого
    return math.ceil(clamped)

# ========== ИНИЦИАЛИЗАЦИЯ ДАННЫХ ==========
# Создаём начальных игроков с нормально распределёнными лимитами
active_players = []
max_games_dict = {}
stats = {}

print("=== Инициализация ===")
print(f"Генерация {PRE_TOTAL_PLAYERS} начальных игроков...")
for i in range(PRE_TOTAL_PLAYERS):
    player_id = i
    max_g = generate_max_games()
    active_players.append(player_id)
    max_games_dict[player_id] = max_g
    stats[player_id] = {'wins': 0, 'games': 0}

next_id = PRE_TOTAL_PLAYERS
print(f"Сгенерировано {len(active_players)} игроков. Следующий ID: {next_id}")

# Посчитаем распределение лимитов для справки
from collections import Counter
init_counts = Counter(max_games_dict.values())
print("Распределение max_games среди начальных игроков (первые 10 значений):")
for g in sorted(init_counts.keys())[:10]:
    print(f"  {g}: {init_counts[g]} игроков")
print("...")

# ========== ФУНКЦИЯ ПРОВЕДЕНИЯ ОДНОГО РАУНДА ==========
def run_round(active_players, stats):
    """Проводит один раунд (MATCHES_PER_ROUND матчей) среди активных игроков."""
    shuffled = active_players.copy()
    random.shuffle(shuffled)

    for match_idx in range(MATCHES_PER_ROUND):
        start = match_idx * (TEAM_SIZE * 2)
        match_players = shuffled[start:start + (TEAM_SIZE * 2)]

        team1 = match_players[:TEAM_SIZE]
        team2 = match_players[TEAM_SIZE:]

        winner = random.choice([1, 2])
        winning_team = team1 if winner == 1 else team2
        all_players = team1 + team2

        for pid in winning_team:
            stats[pid]['wins'] += 1
        for pid in all_players:
            stats[pid]['games'] += 1

    return stats

# ========== ОСНОВНОЙ ЦИКЛ СИМУЛЯЦИИ ==========
if __name__ == '__main__':
    print("\nНачинаем симуляцию на {} раундов...".format(TOTAL_ROUNDS))
    for round_num in range(1, TOTAL_ROUNDS + 1):
        stats = run_round(active_players, stats)
        print(f"Раунд {round_num} завершён.")

        # Если это не последний раунд, выполняем замену завершивших игроков
        if round_num < TOTAL_ROUNDS:
            # Находим игроков, достигших своего max_games
            players_to_remove = [
                pid for pid in active_players
                if stats[pid]['games'] == max_games_dict[pid]
            ]

            # Удаляем их из активного списка
            for pid in players_to_remove:
                active_players.remove(pid)

            # Создаём новых игроков с новыми лимитами (по одному на каждого удалённого)
            for _ in players_to_remove:
                new_id = next_id
                next_id += 1
                new_max = generate_max_games()
                max_games_dict[new_id] = new_max
                stats[new_id] = {'wins': 0, 'games': 0}
                active_players.append(new_id)

            print(f"  Заменено {len(players_to_remove)} игроков. Активных: {len(active_players)}")

    print("\n=== Симуляция завершена ===")
    print(f"Всего уникальных игроков создано: {next_id}")

    # ========== ПРОВЕРКИ ==========
    total_games = sum(record['games'] for record in stats.values())
    total_wins = sum(record['wins'] for record in stats.values())

    print(f"Общее количество сыгранных матчей (игр) на всех игроков: {total_games}")
    print(f"Общее количество побед: {total_wins}")

    # Проверка, что все игроки не превысили свой лимит
    violations = 0
    for pid, record in stats.items():
        if record['games'] > max_games_dict[pid]:
            violations += 1
    print(f"Игроков, превысивших лимит: {violations}")

    # Количество игроков, которые завершили свою карьеру (games == max_games)
    finished = sum(1 for pid in stats if stats[pid]['games'] == max_games_dict[pid] and stats[pid]['games'] > 0)
    print(f"Игроков, завершивших карьеру (сыграли ровно свой лимит): {finished}")

    # ========== СОХРАНЕНИЕ В EXCEL ==========
    try:
        data = []
        for pid, record in stats.items():
            games = record['games']
            wins = record['wins']
            win_rate = (wins / games * 100) if games > 0 else 0.0
            data.append({
                'player_id': pid,
                'max_games': max_games_dict[pid],
                'games': games,
                'wins': wins,
                'win_rate': win_rate
            })

        df = pd.DataFrame(data)
        df.sort_values('player_id', inplace=True)

        filename = 'statistics_normal_distribution.xlsx'
        df.to_excel(filename, index=False)
        print(f"\nСтатистика сохранена в файл {filename}")
    except Exception as e:
        print(f"Ошибка при сохранении: {e}")

    # ========== ДОПОЛНИТЕЛЬНАЯ СВОДКА ПО ВСЕМ ИГРОКАМ ==========
    avg_win_rate_all = sum(stats[pid]['wins'] / max(1, stats[pid]['games']) * 100 for pid in stats) / len(stats)
    print(f"\nСредний процент побед по всем игрокам (включая активных): {avg_win_rate_all:.2f}%")

    # Сводка только по завершившим (для сравнения)
    if finished > 0:
        finished_players = [pid for pid in stats if stats[pid]['games'] == max_games_dict[pid] and stats[pid]['games'] > 0]
        avg_win_rate_finished = sum(stats[pid]['wins'] / stats[pid]['games'] * 100 for pid in finished_players) / len(finished_players)
        print(f"Средний процент побед только среди завершивших карьеру: {avg_win_rate_finished:.2f}%")

    # Пример для первого игрока
    if 0 in stats:
        print(f"\nПример: игрок 0 (max_games={max_games_dict[0]}) "
              f"сыграл {stats[0]['games']} игр, побед {stats[0]['wins']} "
              f"(процент {stats[0]['wins']/max(1, stats[0]['games'])*100:.2f}%)")

Сразу хочется сказать — очевидно, что один результат хоть и покажет то, что мы ожидаем, но для чистоты эксперимента я провёл 100 симуляций, чтобы видеть более общую картинку. Итоги эксперимента графически:

Результаты среднего процента побед игроков в 100 симуляциях
Результаты среднего процента побед игроков в 100 симуляциях

Здесь мы видим, что хоть и средний процент побед игроков в каждой симуляции стремился к 50%, но почти ни в одной не был 50%. Разброс хоть и небольшой, в пределах 0,07 в каждую сторону, но даже так мы получаем ответ — если учитывать такой параметр, что каждый игрок играет определённое количество игр, то средний процент игроков будет смещаться либо вправо, либо влево.

Посмотрим каким образом выбирался параметр игроков:

Частотность по лимитам боёв
Частотность по лимитам боёв

Теперь посмотрим распределение игроков по проценту побед:

В каждой симуляции генерируется по итогу 100-110к игроков
В каждой симуляции генерируется по итогу 100–110к игроков

Мы видим, что график первой симуляции несимметричен. Асимметрию создают игроки, которые проводят случайное количество матчей. Например, очевидно, что игроки у которых 0% и 100% побед играли совсем мало матчей, может, выиграли 5 из 5 или проиграли 3 из 3. 

Ради интереса я решил проверить — является ли распределение процента побед игроков нормальным. Для этого я использовал график qq‑plot.

Видно, что в середине графика идёт совпадение с нормальными значениями (красная линия — нормальное распределение, синий график — распределение процента побед по игрокам), но хвосты всё портят
Видно, что в середине графика идёт совпадение с нормальными значениями (красная линия — нормальное распределение, синий график — распределение процента побед по игрокам), но хвосты всё портят

Ну и на всякий случай, решил использовать тест Колмогорова‑Смирнова для определения нормальности в каждой симуляции.

Чтобы не растягивать статью скажу: ни в одном эксперименте, распределение процента побед игроков не было нормальным
Чтобы не растягивать статью скажу: ни в одном эксперименте, распределение процента побед игроков не было нормальным

Распределение Пуассона

Распределение Пуассона применяется для подсчёта событий, которые происходят случайно, независимо и с постоянной средней интенсивностью, используется для определения потока посетителей в магазине, звонков в колл‑центры в час и так далее

P(X = k) = \frac{\lambda^k e^{-\lambda}}{k!}, \quad k = 0,1,2,\dots, \quad \lambda > 0

Оно будет несимметричным, поэтому я ради эксперимента его тоже взял. Вот так будет выглядеть распределение параметров, при лямбда 10 игр:

Распределение лимитов стало более широким и асимметричным
Распределение лимитов стало более широким и асимметричным

Эксперимент при 100 симуляциях дал следующие результаты:

Видим всё такое же отклонение среднего процента побед всех игроков
Видим всё такое же отклонение среднего процента побед всех игроков

Распределение игроков по проценту побед:

Тут мы видим более жирные хвосты тех у кого 0% и 100% побед, как раз за счёт  асимметрии распределения Пуассона, то есть распределение стало давать больше вероятности, что зайдёт игрок, который играет мало матчей
Тут мы видим более жирные хвосты тех у кого 0% и 100% побед, как раз за счёт асимметрии распределения Пуассона, то есть распределение стало давать больше вероятности, что зайдёт игрок, который играет мало матчей

Про асимметрию в распределении и вклад в общую статистику игроков, которые играют мало матчей, я говорю неслучайно. Эта закономерность подтолкнула меня на мысль — взять одно из самых асимметричных распределений и проверить его — распределение Парето.

Распределение Парето

Распределение Парето — это степенное математическое распределение непрерывной случайной величины, которое описывает дисбаланс и неравномерность в экономике, природе и обществе.

F(x) = P(X \le x) =  \begin{cases} 1 - \left(\dfrac{x_m}{x}\right)^\alpha, & x \ge x_m, \\[6pt] 0, & x < x_m, \end{cases} \quad \text{где } x_m > 0,\; \alpha > 0.

Параметр распределения по играм получился в таком виде:

То есть большинство игроков сыграло только 4 игры, уже гораздо меньше 5 и так далее
То есть большинство игроков сыграло только 4 игры, уже гораздо меньше 5 и так далее

С одной стороны такое распределение даст нам хорошую симметрию, за счёт того, что сыграть 4 победы из 4 боёв или проиграть все из 4-х, шанс не такой уж и большой, что мы и увидим на следующих графиках:

Распределение 100 симуляций отличается от предыдущих минимальным разбросом
Распределение 100 симуляций отличается от предыдущих минимальным разбросом

Распределение игроков выглядит гораздо интереснее, чем раньше:

Распределение с самыми жирными хвостами
Распределение с самыми жирными хвостами

Тут примерно 12к игроков не выиграли ни одной игры и 12к игроков выиграли все игры. Как раз из‑за того, что игр у большинства игроков было мало.

Отрицательное биномиальное распределение

Отрицательное биномиальное распределение я решил взять, так как по моему мнению, оно наиболее правдоподобно распределяет параметр игр.

P(X = k) = \binom{k+r-1}{k} p^r (1-p)^k, \quad k = 0,1,2,\dots

Распределение лимита боёв будет выглядеть следующим образом:

Большинство проводит 3 игры, а остальное распределение равномерно угасает, исключение составляют лишь «старички», которые просто хотят поиграть подольше
Большинство проводит 3 игры, а остальное распределение равномерно угасает, исключение составляют лишь «старички», которые просто хотят поиграть подольше

В таком распределении результат 100 симуляций выглядит так:

Разброс примерно 0,1 в каждую сторону
Разброс примерно 0,1 в каждую сторону

Распределение по проценту побед у игроков выглядит так:

Тут хвост получился гораздо меньше, чем у Парето, всего по 6к игроков либо все игры выиграли, либо все проиграли. Но всё ещё больше, чем у распределений Пуассона (2,5к)  и нормального (2к)
Тут хвост получился гораздо меньше, чем у Парето, всего по 6к игроков либо все игры выиграли, либо все проиграли. Но всё ещё больше, чем у распределений Пуассона (2,5к) и нормального (2к)

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

Второй и главный вывод этого эксперимента: даже если в игре всего 2 исхода, это не означает, что средний процент всех игроков будет 50%, он будет смещаться влево или вправо, в зависимости от случайности. 

Последний эксперимент

Мы получили эксперимент, который не может похвастаться большим разбросом от теоретического значения. Но мы добавили лишь один математический параметр — количество игр каждого игрока. А что если добавить ещё один параметр? Например, психологический. Далеко не каждый игрок может мириться с луз стриками. Ладно 2–3 поражения подряд. А что если мы опишем функцию «выживания» игрока. Возьмём за параметр, что в среднем игрок уйдёт после 5 поражения подряд. Опишем это также: отрицательным биномиальным распределением. Тогда будет следующая логика: игрок зашёл сыграть 10 матчей, но если произойдёт 5 поражений подряд, то он выйдет из игры (или перейдёт на другой сервер).

Тогда получается, что с таким распределением лимита боёв:

Распределение как и в предыдущем эксперименте
Распределение как и в предыдущем эксперименте

И с новым параметром выживаемости:

Распределение начинается с 2-х поражений подряд
Распределение начинается с 2-х поражений подряд

Мы получаем такое распределение среднего процента побед игроков по 100 симуляциям:

Видим сильное смещение влево за счёт добавления нового параметра
Видим сильное смещение влево за счёт добавления нового параметра

И следующее распределение процента побед игроков в первой симуляции:

Почему график сместился так сильно влево? Потому что много игроков не захотело мириться с тем, что у них несколько поражений подряд. Соответственно они ничего не выиграли и ушли, оставив за собой плохую статистику
Почему график сместился так сильно влево? Потому что много игроков не захотело мириться с тем, что у них несколько поражений подряд. Соответственно они ничего не выиграли и ушли, оставив за собой плохую статистику

По сути мы определили, что, как минимум, определённый сервер, в данный момент игрового времени, может быть «несчастливым». Поймав луз стрик игроки либо переходят на другой сервер, с целью выравнивания статистики, либо вообще выходят из игры.

При добавлении параметра «выживаемости» игрока, мы видим, что средний процент побед игроков сместился влево. Эксперимент можно назвать радикальным, потому что в распределении выживаемости много игроков, которые уйдут после 2-3-х поражений подряд. Но при этом, суть остаётся — при добавлении параметров, которые могут быть разными: психологическими, ситуативными, физическими, то даже в банальной игре с двумя возможными событиями — победа/поражение, средняя статистика игроков будет смещаться влево.

Чтобы не запутаться с дальнейшими данными реальных данных, повторю — изначальный спор был о том, может ли статистика игроков целого сервера в данных момент игрового времени быть меньше теоретических 50%? Ответ — да, может. Первое — за счёт того, что игроки играют разное количество матчей. Второе — за счёт психологического параметра выживаемости. И третье — малое количество игр. Так как мы рассматривали 30 игр, а это 4–5 часов реального времени.

Реальные данные

Меня всегда интересовало, как вообще распределён средний процент игроков в игре. Я нашёл сайт сессионной аналитики для игр «Мир танков» и «World of Tanks» и запросил у разработчика сайта данные. Он любезно их предоставил, чтобы можно было посмотреть средний процент игроков. Данные по RU региону, режим игры — «Случайный бой».

Тут мы видим жирные хвосты за счёт новичков, которые больше в игру не играли
Тут мы видим жирные хвосты за счёт новичков, которые больше в игру не играли

Теперь посмотрим статистику тех, кто отыграл больше 1000 игр:

На графике хорошо видна асимметрия, что меньше 40% игроков почти нет, но больше 60% есть и не мало
На графике хорошо видна асимметрия, что меньше 40% игроков почти нет, но больше 60% есть и не мало

Ну и посмотрим ветеранов игры, которые провели более 10 000 игр

Статистика говорит о том, что среднее ветеранов составляет аж 51,46%
Статистика говорит о том, что среднее ветеранов составляет аж 51,46%

Эксперимент, который я провёл, рассматривает текущую ситуацию в игре. Это можно использовать (или, скорее всего, используют) для ребаланса игры. И я имею в виду не только один показатель как средний процент побед, но ещё и жирность хвостов. Как мы помним — жирнохвостость показывает определённую имбалансность параметров (как было с параметром количества лимитов игр с распределением Парето), хоть и средний процент при этом был почти равен теоретическому.

Комментарии (3)


  1. 0xdead926e
    11.09.2026 10:42

    не может быть такого, чтоб сервер был «несчастливым» и тем более, невозможно, чтобы средний процент побед всех игроков на нём был меньше 50%

    во-первых в игре существуют ничьи, из-за которых средних 50% побед не может быть чисто физически.

    во-вторых там на каждом сервере в каждый конкретный день свой набор карт/арен. и обычному стандартному игроку вполне может попасться сервер с картами, на которых он не умеет играть. и он очевидно будет играть на них хуже, чем он играет в среднем и вполне может посчитать его "несчастливым".

    ну и банально до разных серверов разный пинг, что тоже несколько влияет на результаты и ощущения от игры.


  1. titulusdesiderio
    11.09.2026 10:42

    Вот так надо повышать интерес к матану у студентов!


  1. fermentum
    11.09.2026 10:42

    Дополню из общего опыта в онлайн-играх что еще может влиять на баланс:

    1. Не стоит недооценивать влияние клановых игроков, играющих взводами. Часто договариваются всем кланом когда и на каком сервере будут играть, чтобы взвода пересекались в противоположных командах. В некоторых кланах есть просто излюбленные сервера, куда ходят всем онлайном. Понятно, что баланс они ломают очень сильно.

    2. Ну и время онлайна по этой же причине имеет значение. Утром проще, чем вечером.