Контринтуитивные события окружают нас повсюду. Взять, например, парадокс дней рождений: каждый раз, когда оказываешься в небольшой группе и узнаёшь, что у двух участников группы праздник в один и тот же день, то думаешь: какое удивительное совпадение, ведь кажется, что событие редкое. Но вот уже при детальном рассмотрении оказывается, что вероятность такого события достаточно высокая (например, если группа 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 игру, и его статистика запишется. Параметр «количество игр одного игрока» будет описываться различными распределениями, о которых поговорим далее.
Нормальное распределение
Нормальное распределение — это непрерывное распределение вероятностей с пиком в центре и симметричными боковыми сторонами. Формула выглядит следующим образом:
Это распределение берут, когда есть какое‑то среднее, и мы считаем что 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 симуляций, чтобы видеть более общую картинку. Итоги эксперимента графически:

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

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

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

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

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

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

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

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

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

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

Тут примерно 12к игроков не выиграли ни одной игры и 12к игроков выиграли все игры. Как раз из‑за того, что игр у большинства игроков было мало.
Отрицательное биномиальное распределение
Отрицательное биномиальное распределение я решил взять, так как по моему мнению, оно наиболее правдоподобно распределяет параметр игр.
Распределение лимита боёв будет выглядеть следующим образом:

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

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

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

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

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

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

По сути мы определили, что, как минимум, определённый сервер, в данный момент игрового времени, может быть «несчастливым». Поймав луз стрик игроки либо переходят на другой сервер, с целью выравнивания статистики, либо вообще выходят из игры.
При добавлении параметра «выживаемости» игрока, мы видим, что средний процент побед игроков сместился влево. Эксперимент можно назвать радикальным, потому что в распределении выживаемости много игроков, которые уйдут после 2-3-х поражений подряд. Но при этом, суть остаётся — при добавлении параметров, которые могут быть разными: психологическими, ситуативными, физическими, то даже в банальной игре с двумя возможными событиями — победа/поражение, средняя статистика игроков будет смещаться влево.
Чтобы не запутаться с дальнейшими данными реальных данных, повторю — изначальный спор был о том, может ли статистика игроков целого сервера в данных момент игрового времени быть меньше теоретических 50%? Ответ — да, может. Первое — за счёт того, что игроки играют разное количество матчей. Второе — за счёт психологического параметра выживаемости. И третье — малое количество игр. Так как мы рассматривали 30 игр, а это 4–5 часов реального времени.
Реальные данные
Меня всегда интересовало, как вообще распределён средний процент игроков в игре. Я нашёл сайт сессионной аналитики для игр «Мир танков» и «World of Tanks» и запросил у разработчика сайта данные. Он любезно их предоставил, чтобы можно было посмотреть средний процент игроков. Данные по RU региону, режим игры — «Случайный бой».

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

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

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

fermentum
11.09.2026 10:42Дополню из общего опыта в онлайн-играх что еще может влиять на баланс:
Не стоит недооценивать влияние клановых игроков, играющих взводами. Часто договариваются всем кланом когда и на каком сервере будут играть, чтобы взвода пересекались в противоположных командах. В некоторых кланах есть просто излюбленные сервера, куда ходят всем онлайном. Понятно, что баланс они ломают очень сильно.
Ну и время онлайна по этой же причине имеет значение. Утром проще, чем вечером.
0xdead926e
во-первых в игре существуют ничьи, из-за которых средних 50% побед не может быть чисто физически.
во-вторых там на каждом сервере в каждый конкретный день свой набор карт/арен. и обычному стандартному игроку вполне может попасться сервер с картами, на которых он не умеет играть. и он очевидно будет играть на них хуже, чем он играет в среднем и вполне может посчитать его "несчастливым".
ну и банально до разных серверов разный пинг, что тоже несколько влияет на результаты и ощущения от игры.