Когда речь заходит о роли случая в игре, мнения аудитории обычно расходятся. Одни утверждают, что всё «хорошо весьма» и генератор случайных чисел (ГСЧ) нисколько не портит игру, другие с горечью рассказывают, как очередное выпадение морали защитникам замка предрешило исход часовой битвы.
А между тем, в чём-то правы и те, и другие. Первые правы в том, что ряд игровых моментов хорошо сбалансирован, то есть случай в них загнан в предсказуемые рамки. Но правы и другие, винящие козни ГСЧ в разбитой об стенку со злости клавиатуре. Например известно, что наши китайские коллеги любят очень сложные карты. В боях на таких картах без выпадения целой комбинации бонусов победить просто невозможно. И наоборот. Победа зависит от чётко выстроенного и исполняемого по шагам плана, который может нарушить любой двойной ход противника. Стоит ли говорить, что закон подлости в нужный момент проявляет себя на все 100%. Что мы имеем в итоге? Злость, досаду, потерю интереса.
Давайте разберёмся подробнее, где истина и лежит ли она посередине. Чтобы понять природу проблемы, нужно обратить внимание на то, что мы именуем «ГСЧ». На самом деле правильный термин для алгоритма генерации случайных чисел — генератор псевдослучайных чисел, так как каждое новое число полностью предопределяется состоянием генератора.
У программных ГСЧ есть немало проблем. Во-первых, все они рано или поздно начинают повторяться. То есть существует параметр длины последовательности, после которой генерация начинается с начала, повторяя один-в-один то, что уже было. Впрочем, скажете вы, для игр со множеством случайных факторов предсказание чисел и повтор последовательности являются вещами маловероятными. Соглашусь с вами.
Другой проблемой является качество генератора, а вернее качество последовательности чисел, которую он выдаёт. Предполагается, что последовательность будет иметь равномерное распределение. То есть на 1000 сгенерированных значений от 1..10 должно прийтись в среднем по 100 каждого. Положим, приблизительно так и будет. Значит генератор подходит для боевого применения? Не спешите.
Большинство игровых событий требует небольшой длины последовательности. Например, если дерево знаний даёт в 20% случаев повышение, то не стоит всерьёз надеяться, что игроку дадут возможность посетить 1000 деревьев. На карте будет расположено от силы 10 объектов данного типа. А значит к генератору предъявляются повышенные требования. И здесь мы сталкиваемся с проблемами. Оказывается, на небольших последовательностях значения могут скакать абсолютно произвольным образом. Например 4 дерева из 5-и окажутся удачливыми. Или наоборот, все 5 бесполезными. Представьте себе разочарование игрока, пробившегося с потерями в зону с тремя такими объектами и ушедшего ни с чем после значительной потери времени.
Но даже если генератор имеет схожее с равномерным распределение на небольших по размерам последовательностях, это отнюдь не гарантирует того математического ожидания, которое будет искать игрок. Дело в том, что один и тот же генератор с одним и тем же состоянием используется одновременно для всех игровых событий. Это значит, что если мы генерируем числа от 1..5 и нам жизненно важно получить 5 хотя бы за десять генераций, мы можем не получить требуемое значение вообще. Всё потому, что после каждой нашей генерации работают другие события, например расчёт урона для отрядов в бою, где указанная пятёрка, возможно, не раз выпадала, но когда ход вновь возвращается нашему событию, генератор снова отплёвывается повтором. Поэтому драколич с шансом блока в 20% в битвах нередко блокирует 4-5 ударов подряд! Автор не раз лично был свидетелем этого явления и смеялся в голос. Даже шутка такая родилась, об абсолютной защите драколичей.
Иными словами, для исправления указанного выше недостатка достаточно иметь собственный ГСЧ для каждого «пользователя» в игре. Однако гарантии математического ожидания на малых и средних последовательностях всё равно не будет.
Давайте теперь разберём такое понятие, как хаотичность событий, или меру хаоса, вносимого событием в планы игроков или ИИ. Для начала введём несколько обозначений:
N - (number) - длина последовательности.
D - (distance) - разбежка или расстояние между максимальным и минимальным значением.
A - (average distribution) - математическое ожидание. Значение, к которому стремится среднее, рассчитанное по последовательности. Для равномерного распределения вычисляется как (Макс - Мин) / 2. Если измеряется в %, то это % от разбежки.
Quote:Мин...Мин + A * D...Макс
NA - произведение начальных A и N. Показывает ресурс, который идеальный генератор должен распределить по последовательности. Например, если имеется A = 20% и N = 10, то NA = 200%. Каждое новое генерируемое значение — это минимальное + некоторый бонус, который берётся из общего хранилища. NA * D показывает предполагаемую сумму всех бонусов для каждого значения.
M - количество оставшихся членов последовательности (<= N).
Под хаотичностью события мы будем подразумевать его минимальные и максимальные отклонения от математического ожидания. Так, для диапазона 50..80 математическое ожидание A = (80 + 50) / 2 = 65. Возможные отклонения: ±15. 15 — это 23% от среднего. Значит хаотичность: -23%..+23%.
Посмотрим, какую хаотичность предлагают разработчики игр для самого важного параметра в бою — урона. В Эадоре урон монстров задаётся одним числом (A), а конкретное значение при ударе рассчитывается как 0.75 * А .. 1,25 * А. Иначе говоря, хаотичность: ±25%. Запомним это значение. Оно вносит достаточно разнообразие в бой, позволяя случаться событием, вроде «успел добить жирного монстра» или «выжил с 1 жизнью», в то же время предоставляя игроку возможность оценки рисков и планирования.
Проверим наиболее популярные разбросы урона в Героях 3.
2-3, 4-6, 10-15 и т.д. Хаотичность: ±20%. Предсказуемость хорошая. Благословение не критично, проклятие не смертельно.
1-2, 2-4, 10-20, 20-40 и т.д.. Хаотичность: ±33%. Имеют значительную зависимость от ГСЧ. Благословения и проклятья играют существенную роль. Критические успехи и провалы приводят к изменению оперативных планов.
1-3, 3-8, 20-45. Хаотичность ±40%..50%. Практически выводит ситуацию из под контроля игрока. Урон скачет непредсказуемо. Невезенье или везенье всего в нескольких ходах может сыграть решающую роль в бою. Благословения и проклятья (артефакты для них) критичны.
1..100. Хаотичность: ±100%. Классический пример полного разгула Случая. Палатка лечит на 5, 17, 13? Ценный монстр погибает? Утешьтесь, в среднем могла вылечить 150 единиц здоровья! Чтобы добиться этого среднего понадобится весь бой. Но жизнь палатки скоротечна, а нужда в ней возникает локальная. Так что вариантов у вас не много. Наилучший — не надеяться на неё вообще.
Стоит отдельно рассмотреть довольно популярные двоичные генерации или генерации по принципу «всё сработало» и «всё не сработало». Они порой вносят гораздо больше хаоса в партию, чем расчёт урона. Например, 5% шанс блокировать магию противника. Хаотичность: -100%..+1900(!)%. Иначе говоря, ожидается срабатывание блока только в одном из двадцати случаев. Большей частью времени польза от эффекта нулевая (-100% от среднего). То есть бой идёт как будто способности нет. Но время от времени (в неподходящее) срабатывает блок, которого никто не ждал в виду некритичного шанса.
20% двоичное событие имеет хаотичность: -100%..+400%. Стоит ли говорить, что если требуется использовать накопленную ману в нужный момент для важного заклинания (ослепление последнего отряда, взрыв и т.д), предугадать наступление этого самого правильного момента нельзя. И наивен тот, кто думает, что в случае двойного запаса маны и стабильности плана с расчётом на одну неудачу, из двух подряд попыток колдовать хоть одна, да пройдёт. Может не пройти.
А ведь на самом деле игрока-тактика вполне устроило бы гарантированное или предсказуемое уменьшение эффекта заклинания, при котором само заклинание всё-таки действует. И защитнику не пришлось бы трястись, ожидая чудодейственного выпадения двадцатки, когда можно было бы весь бой игнорировать часть наносимого магического урона, справедливо осознавая, какие у соперника шансы убить ваш ценный отряд заклинанием.
Не менее печально известен и воговский блок в 50%, берущий своё начало из блока минотавров в Героях 4. Несмотря на значение 35%, минотавры умудрялись блокировать порой целые серии ударов, так что бой с ними был нервным и долгим, с нередкими перезагрузками. Чтобы уж наверняка не оставить тактике шанса, воговские командиры могут учить пятидесятипроцентный блок. Если задача командира выжить, то это самое сильное умение в игре. И 50% там только на словах. Визуально блок срабатывает гораздо чаще (причины указаны выше), так что в критические моменты Ваш командир может пережить и по три летальных удара. С другой стороны, если отряды противника тоже обладают подобной способностью, приготовьтесь к абсолютному цирку, учитывая, что блок может полностью нивелировать действия ваших заклинаний.
С учётом вышесказанного рекомендации следующие. Практически везде заменять двоичные генераторы на диапазонные (1), для каждого события использовать свой генератор, чтобы гарантировать чистоту последовательности (2) и использовать алгоритмы-надстройки над базовым ГСЧ для обеспечения выбранных характеристик последовательности (N, A, характер распределения) (3).
Библиотека BalancedGenerator.era выполняет все вышеуказанные функции. Она позволяет создавать объекты-генераторы с двумя вариантами распределения и предоставляет три функции для различных видов генераций. Все функции позволяют задавать диапазон длин последовательностей, чтобы уменьшить возможности игрока по предсказанию значений. Рассмотрим интерфейс библиотеки подробнее:
Code:
function CreateBalance50Obj (MinN, MaxN: integer): {O} TErmArr; stdcall;
Функция создаёт генератор с
гарантированным равномерным (A=50%) распределением. Аргументы: минимальная и максимальная длина последовательности, в рамках которой математическое ожидание будет равно 50%. Рекомендуется использовать значения в интервалах 7-14 для событий, имеющих среднюю и большую важность для игрока и 17-24 для длительных явлений с малой или средней значимостью.
С точки зрения игрока если фактическое среднее значение становится меньше 50%, то повышается минимальная планка генерируемых значений. И наоборот, если слишком везёт и фактическое мат. ожидание > 50%, то снижается верхняя планка генерируемых значений. Это позволяет даже интуитивно предполагать, что серия неудач завершится выравнивающими успехами. Иными словами, реальные значения зависят от того, что выпадало в предыдущих генерациях.
Пример. Пусть диапазон урона: 50..100. N = 10, A = 50%, NA = 500%. M = 10.
Выпадает 100. Теперь NA = 400%, M = 9, A = 400%/9 = 44.44%. Это значит что в оставшейся последовательности из девяти чисел среднее значение будет уже не Мин + 0.5 * Разбежка, а Мин + 0,44 * Разбежка. конкретно во второй генерации диапазон урона (реальный) будет 50..94.
Code:
function CreateCrit10Obj (MinN, MaxN: integer; A: single): TErmArr; stdcall;
Создаёт генератор с неравномерным распределением. Гарантируется переход состояния между последовательностями (если остался накопленный эффект несправедливой удачей/неудачей) и то, что максимальное отклонение от А
не превысит Max (верхняя граница генерируемых значений). Практически для каждой генерации в
90% случаев генерируется значение в нижней (для A <= 50%) или верхней (для A > 50%), части диапазона, что описывается как
«обычное» значение. Обычное значение варьируется около заданного A. В
10% случаев происходит
критическая удача(провал) и генерируется значение из альтернативного диапазона.
Аргумент A - дробное число (e) 0.0..1.0. Для равномерного распределения 0.5 (что бессмысленно, так как для равномерного есть генератор выше).
Code:
procedure DestroyObj (hObj: TErmArr); stdcall;
Удаляет генератор. Генераторы сохраняются в файлах сохранения, так что временные генераторы нужно удалять, а о постоянных не заботиться. Аргумент - объект-генератор.
Code:
function GenerateBalance50Val (hObj: TErmArr; aMin, aMax: integer): integer; stdcall;
Аргументы: генератор Баланс-50, минимальное и максимальное значения.
Генерирует случайное число в указанном диапазоне. Гарантирует равномерное распределение.
Code:
function GenerateBinary50Val (hObj: TErmArr; Chance: single): integer; stdcall;
Аргументы: генератор Баланс-50, шанс успеха (вещественное число, e). Отрицательный шанс означает невозможность события вовсе. Шанс > 100% не гарантирует успех события, если генератор находится в пессимистичном состоянии (ранее везло).
Генерирует двоичное значение (1 или 0), где 1 - событие произошло, 0 - не произошло.
Гарантирует равномерное распределение для NA кратным 100% и
переход состояния между последовательностями в противном случае.
Code:
function GenerateCrit10Val (hObj: TErmArr; aMin, aMax: integer): integer; stdcall;
Аргументы: генератор Крит-10, минимальное и максимальное значения.
ВАЖНО! Между генерациями диапазоны значений могут быть произвольными. Генераторы гарантируют математические ожидания (A) в %-х значениях, так что если 20..40 дало 20, а потом 40..80 дало 80, то для генератора всё отлично. Сперва выпало минимальное значение (0%), затем максимальное (100%). Баланс соблюдён (50%).