Добавлен , опубликован

Энкаунтеры

Содержание:
В первом уроке я указал на наличие проблем в работе системы. А именно - неправильно выбирается нация, неправильно отрабатывают бонусы за остров, отключена система исключений. В качестве практического занятия мы попробуем эти проблемы решить.
На всякий случай уточню, что я работаю с ремастером Hokkins'a. Прежде, чем скачивать отсюда файлы, убедитесь что у вас под рукой такая же игра. А лучше - попытайтесь самостоятельно реализовать описанное в уроке. Для этого он и был написан. Практика!

Выбор нации

Для начала, восстановим в памяти, как эта функция выглядит в данный момент:
// выбор нации энкаунтера
int GetRandomNationForMapEncounter(string sIslandID, bool bMerchant)
{
	int iNation = -1;

    // если передан остров - смотрим его колонию
	if(sIslandID != "") 
	{
		int iIsland = FindIsland(sIslandID);    // не используется
		for(int i = 0; i < MAX_COLONIES; i++)
		{
			if(colonies[i].island == sIslandID)
			{
				if (colonies[i].nation != "none")
				{
					iNation = sti(colonies[i].nation);
					break;
				}
			}
		}
	}

    // дефолтный вес каждой нации
	float fEngland  = 1.0;
	float fFrance   = 1.0;
	float fSpain    = 1.0;
	float fHolland  = 1.0;
	float fPirate   = 1.0;

    // бонус за ближайшую колонию
	if(iNation != -1) 
	{
	    switch (iNation )
		{
			case ENGLAND:
				fEngland += 0.2;
			break;
			case FRANCE:
    			fFrance += 0.2;
			break;
			case SPAIN:
    			fSpain += 0.2;
			break;
			case HOLLAND:
    			fHolland += 0.2;
			break;
			case PIRATE:
    			fPirate += 0.2;
			break;
		}
	}

    // собираем всё в один ряд
	float fProbablyNation;
	if(bMerchant) 
	{
		fProbablyNation = fEngland + fFrance + fSpain + fHolland;
	}
	else
	{
		fProbablyNation = fEngland + fFrance + fSpain + fHolland + fPirate;
	}

	fProbablyNation = frand(fProbablyNation);

    // определяем диапазоны в нашем ряду
    // для каждой нации
	fFrance   = fFrance  + fEngland;
	fSpain    = fFrance  + fSpain;
	fHolland  = fSpain   + fHolland;
	fPirate   = fHolland + fPirate;

    // вычисления итоговой нации
    // но вместо этого начинаются баги
	if(bMerchant == 0)
	{
        // франция уехала за свой диапазон, своровав его у испании
		if(fProbablyNation >= fFrance && fProbablyNation < fSpain) 
		{
			return FRANCE;
		}

        // и дальше пошло то же самое
		if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
		{
			return SPAIN;
		}

		if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
		{
			return HOLLAND;
		}

		if(fEngland <= fProbablyNation) 
		{
			return ENGLAND;
		}
	}
	else
	{
		if (rand(2) == 1) return HOLLAND; // можно подумать, что это попытка в реализм
                                          // ведь Голландских торговцев действительно
                                          // было больше всего
                                          // но на самом деле это костыль
		
		if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
		{
			return FRANCE;
		}

		if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
		{
			return SPAIN;
		}

		if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
		{
			return HOLLAND;
		}

		if(fEngland <= fProbablyNation)
		{
			return ENGLAND;
		}
	}

    // а сами англичане стали пиратами
    // или это еще одна, очень тонкая, попытка в реализм?
	return PIRATE; 
}
Нас интересует фрагмент с вычислением конкретной нации. Чтобы не повторяться, приведу комментарий из первого урока:
Первой в диапазоне стоит Англия. Соответственно, значения от 0.0 до fEngland включительно это Англия. Далее, от fEngland до fFrance - это Франция и так далее.
По факту же, Франция засчитывается на диапазоне Испании, Испания на диапазоне Голландии, Голландия на пиратах, а пираты на диапазоне Англии. Да-да, всё что ниже значения fEngland - засчитывается как пираты, потому что этот диапазон не покрыт if-ами. А Англия засчитывается если выпало ровно значение fPirate или диапазон Франции.
Так как логику сломали, но явно не понимали почему - появилась дюжина костылей. Первый из них - это переменная bMerchant, которая обозначает, что мы генерируем торговца. Как я уже сказал - Голландия вычисляется на диапазоне пиратов. Но диапазон пиратов не суммируется, если мы вычисляем торговцев. И это приводит нас к тому, что на карте нет Голландских торговцев. Так родилась вот эта строка:
if (rand(2) == 1) return HOLLAND;
Тот факт, что нации определяются на диапазонах других наций, ломает еще и логику бонусов за остров. Например, бонус за колонию Испании фактически повышает вероятность спавна Французов и т.д.
То есть нужно поправить диапазоны. Ничего сложного:
if(fProbablyNation <= fEngland) return ENGLAND;
if(fProbablyNation > fEngland && fProbablyNation <= fFrance) return FRANCE;
if(fProbablyNation > fFrance && fProbablyNation <= fSpain) return SPAIN;
if(fProbablyNation > fSpain && fProbablyNation <= fHolland) return HOLLAND;

return PIRATE;
Проверка bMerchant в результирующих вычислениях нам вообще не нужна, так как этот флаг проверяется ещё на этапе создания ряда и пираты туда вообще не включаются, если тип энкаунтера - торговец.
Для починки бонуса за остров никаких дополнительных манипуляций не нужно - ведь он и так работал. Просто этот бонус засчитывался не той нации, что должен.

Перечень доступных наций

Размялись? Перейдём к задаче, где действительно нужно подумать. Ещё одна цитата:
Где-то тут должен быть учёт допустимых для энкаунтера наций, которые описываются в таблице инициализации. Но его нет. Вероятно потому, что в энкаунтере с пиратами из-за этой функции не генерировались пираты. Ведь они вычисляются на диапазоне Аглии, которая исключена из пиратских энкаунтеров.
Речь вот об этом куске кода из таблицы инициализации:
makeref(rEnc, EncountersTypes[ENCOUNTER_TYPE_PIRATE_SMALL]);
rEnc.Type = ENCOUNTER_SCENARIO_WAR;
rEnc.MinRank = 1;
rEnc.MaxRank = 1000;
rEnc.worldMapShip = "sloop";
Enc_AddShips(rEnc, "War", 1, 2);
Enc_ExcludeNation(rEnc, ENGLAND);   // Вот эти строки
Enc_ExcludeNation(rEnc, FRANCE);    // нас интересуют
Enc_ExcludeNation(rEnc, SPAIN);
Enc_ExcludeNation(rEnc, HOLLAND);

Enc_AddClasses(rEnc, 1, 0, 0, 5, 7);
Enc_AddClasses(rEnc, 1000,0, 0, 5, 7);
Сама функция Enc_ExcludeNation() лежит здесь же, в файле Encounters_init.c:
void Enc_ExcludeNation(ref rEnc, int iNation)
{
	string sNation = Nations[iNation].Name;
	rEnc.Nation.Exclude.(sNation) = true;
}
Она сохраняет в энкаунтер параметр "Nation.Exclude.(sNation)". Он обозначает нацию, которую не_могут иметь корабли в составе энкаунтера. Простая логика подсказывает, что где-то в функции подбора нации для энкаунтера этот параметр следовало бы проверять.
Сразу хочу отметить, что я не видел, как эта функция устроена в оригинале и в алгоритмах я тоже плохо разбираюсь. Поэтому моё решение далеко не самое лаконичное. Если у вас получилось реализовать это более красиво - обязательно выкладывайте результат в комментариях.
Сначала я опишу своё виденье данной фичи, а затем мы посмотрим код:
Необходимо создать массив из наций, которые могут учавствовать в энкаунтере. При этом нужно сохранить функционал бонуса за остров, поэтому массивов будет два: сначала мы определим веса, а затем перенесём их в новый массив, где будут только нужные нам нации.
В остальном всё остаётся как было - веса собираются в ряд, назначаются диапазоны и определяется победитель. Только теперь это всё будет работать с нашим результирующим массивом допустимых наций, а не со всеми нациями сразу.
// выбор нации энкаунтера
// выбрасываем bMerchant за ненадобностью
// и добавляем в аргументы тип энкаунтера
//int GetRandomNationForMapEncounter(string sIslandID, bool bMerchant)
int GetRandomNationForMapEncounter(string sIslandID, int iEncounterType)
{
    // создаём необходимые для работы переменные
    float fNation[MAX_NATIONS];             // здесь будут веса
    int iAllowedNations[MAX_NATIONS + 1];   // допустимые для энкаунтера нации
    float fAllowedNation[MAX_NATIONS + 1];  // диапазоны
    int iNationsCount, i;                   // счётчики
    bool bExcluded[MAX_NATIONS];            // флаг что нация исключена
    string sNationName;                     // имя нации
	int iNation = -1;
	float fProbablyNation;

    // если передан остров - смотрим его колонию
	if(sIslandID != "") {
		//int iIsland = FindIsland(sIslandID);    // не используется
		for(i = 0; i < MAX_COLONIES; i++) {
			if(colonies[i].island == sIslandID) {
				if (colonies[i].nation != "none") {
					iNation = sti(colonies[i].nation);
					break;
				}
			}
		}
	}

    // дефолтный вес каждой нации
    /*
	float fEngland  = 1.0;
	float fFrance   = 1.0;
	float fSpain    = 1.0;
	float fHolland  = 1.0;
	float fPirate   = 1.0;
    */
    for (i = 0; i < MAX_NATIONS; i++) {
        // дефолтный вес каждой нации
        fNation[i] = 1.0;

        // здесь же проверяем не является ли данная нация исключённой
        sNationName = "Nation.Exclude." + Nations[i].Name;
        if (CheckAttribute(EncountersTypes[iEncounterType], sNationName)) {
            bExcluded[i] = sti(EncountersTypes[iEncounterType].(sNationName));
        } else {
            bExcluded[i] = false;
        }
    }

    // бонус за ближайшую колонию
    /*
	if(iNation != -1) 
	{
	    switch (iNation )
		{
			case ENGLAND:
				fEngland += 0.2;
			break;
			case FRANCE:
    			fFrance += 0.2;
			break;
			case SPAIN:
    			fSpain += 0.2;
			break;
			case HOLLAND:
    			fHolland += 0.2;
			break;
			case PIRATE:
    			fPirate += 0.2;
			break;
		}
	}
    */
    if(iNation != -1) fNation[iNation] += 0.2;

    // собираем всё в один ряд
    /*
	if(bMerchant) 
	{
		fProbablyNation = fEngland + fFrance + fSpain + fHolland;
	}
	else
	{
		fProbablyNation = fEngland + fFrance + fSpain + fHolland + fPirate;
	}
    */

    // определяем диапазоны в нашем ряду
    // для каждой нации
    /*
	fFrance   = fFrance  + fEngland;
	fSpain    = fFrance  + fSpain;
	fHolland  = fSpain   + fHolland;
	fPirate   = fHolland + fPirate;
    */
    fProbablyNation = 0.0;
    fAllowedNation[0] = 0.0;
    iNationsCount = 0;
    for (i = 0; i < MAX_NATIONS; i++) {
        if (bExcluded[i] == false) {
            iNationsCount++;
            // создаём массив допустимых наций
            iAllowedNations[iNationsCount] = i; 
            // и тут же определяем диапазоны в ряду
            fAllowedNation[iNationsCount] = fAllowedNation[iNationsCount - 1] + fNation[i];
            // собираем ряд
            fProbablyNation += fNation[i];
        }
    }

    // вычисляем победителя
    /*
	if(bMerchant == 0)
	{
		if(fProbablyNation >= fFrance && fProbablyNation < fSpain) 
		{
			return FRANCE;
		}

		if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
		{
			return SPAIN;
		}

		if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
		{
			return HOLLAND;
		}

		if(fEngland <= fProbablyNation) 
		{
			return ENGLAND;
		}
	}
	else
	{
		if (rand(2) == 1) return HOLLAND;
		
		if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
		{
			return FRANCE;
		}

		if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
		{
			return SPAIN;
		}

		if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
		{
			return HOLLAND;
		}

		if(fEngland <= fProbablyNation)
		{
			return ENGLAND;
		}
	}
    */
	fProbablyNation = frand(fProbablyNation);
    if (fProbablyNation == 0.0) return iAllowedNations[1];
    for (i = 1; i <= iNationsCount; i++) {
        if (fProbablyNation <= fAllowedNation[i] && fProbablyNation > fAllowedNation[i - 1]) return iAllowedNations[i];
    }

	return PIRATE; 
}
Я оставил блоки изначального кода закомментированными, чтоб было наглядно понятно какой функционал чем был заменён. Давайте разбираться.
Прежде всего - я выбросил аргумент функции bMerchant, т.к. это костыль и он больше не нужен, ведь мы восстанавливаем функционал, который этот костыль заменял.
Вместо него я добавил другой аргумент - iEncounterType, чтоб мы могли получить доступ к списку исключённых наций энкаунтера.
ВАЖНО Не забудьте поменять передаваемые аргументы там, где эта функция вызывается.
Далее я создал необходимые для работы переменные - все они прокомментированы.
Теперь, непосредственно, изменения в функционале.
Циклом перебираем все нации. Задаём им значения веса по умолчанию и отмечаем какие из них исключённые.
Добавляем банус за остров, если таковой имеется.
for (i = 0; i < MAX_NATIONS; i++) {
    // дефолтный вес каждой нации
    fNation[i] = 1.0;

    // здесь же проверяем не является ли данная нация исключённой
    sNationName = "Nation.Exclude." + Nations[i].Name;
    if (CheckAttribute(EncountersTypes[iEncounterType], sNationName)) {
        bExcluded[i] = sti(EncountersTypes[iEncounterType].(sNationName));
    } else {
        bExcluded[i] = false;
    }
}
// бонус за ближайшую колонию
if(iNation != -1) fNation[iNation] += 0.2;
Теперь другим циклом переносим допустимые к использованию нации в результирующий массив. В этом же цикле собираем ряд и назначаем диапазоны:
fProbablyNation = 0.0;
fAllowedNation[0] = 0.0;
iNationsCount = 0;
for (i = 0; i < MAX_NATIONS; i++) {
    if (bExcluded[i] == false) {
        iNationsCount++;
        // создаём массив допустимых наций
        iAllowedNations[iNationsCount] = i; 
        // и тут же определяем диапазоны в ряду
        fAllowedNation[iNationsCount] = fAllowedNation[iNationsCount - 1] + fNation[i];
        // собираем ряд
        fProbablyNation += fNation[i];
    }
}
И последний цикл определяет победителя:
fProbablyNation = frand(fProbablyNation);
if (fProbablyNation == 0.0) return iAllowedNations[1];
for (i = 1; i <= iNationsCount; i++) {
    if (fProbablyNation <= fAllowedNation[i] && fProbablyNation > fAllowedNation[i - 1]) return iAllowedNations[i];
}
Поскольку победитель выбирается циклом, мы не можем охватить if-ами нулевое значение диапазона. Решением этой проблемы служит строка if (fProbablyNation == 0.0) return iAllowedNations[1];.
В целом, логика процессов осталась такой же, как и была. Только теперь это всё работает в циклах, потому что мы не знаем заранее, сколько элементов нужно обрабатывать.
Если у вас что-то не получается - к статье будет приклеплён архив с приведённым кодом. Сможете посмотреть, что вы сделали неправильно.
Примечание:
Должен отметить, что исправление бага с выбором наций приводит к снижению количества пиратов на карте. Точнее пиратов осталось столькоже. Но другие энкаунтеры больше не отменяются костылями из-за неверно подобранной нации.
Если игра с таким количеством пиратов кажется вам некомфортной/непривычной - попробуйте немножко увеличить значение rEnc.Chance в таблице инициализации для пиратских энкаунтеров.

Минутка оптимизации

Как я уже говорил - я не силён в алгоритмах. И приведённое выше решение несёт в себе достаточно большое количество вычислений, которые будут вызываться при каждом появлении кораблика на глобалке. Предлагаю, в качестве дополнительных практических упражнений, рассмотреть варианты оптимизации этого кода.
В своём моде Encounters Rebalance я немного разгрузил эту функцию, положив в энкаунтеры список ДОПУСТИМЫХ наций, а не исключённых. Таким образом существенно снизилось количество операций внутри циклов.
Для этого в таблице инициализации появилась функция, которая сохраняет в энкаунтер список всех наций (она вызывается в цикле присвоения энкаунтерам значений по умолчанию), а функция Enc_ExcludeNation() теперь удаляет лишние:
void Enc_ExcludeNation(ref rEnc, int iNation)
{
	string sAttribute = "Nation." + Nations[iNation].Name;
    DeleteAttribute(&rEnc, sAttribute);
}

void Enc_AddNations(ref rEnc)
{
    int i;
    string sNation;

    for (i=0; i<MAX_NATIONS; i++) {
        sNation = Nations[i].Name;
        rEnc.Nation.(sNation) = i;
    }
}
Сама функция выбора нации теперь выглядит следующим образом:
int Encounter_GetRandomNation(int iEncounterType, string sIslandID)
{
    int iNation = -1;
    float fNation[MAX_NATIONS];
    float fProbablyNation = 0.0;
    int i, iCount;
    int iAllowedNations[MAX_NATIONS + 1];
    float fAllowedNations[MAX_NATIONS + 1];
    aref arAttribute, arNation;

    // дефолтные значения веса
    for (i = 0; i < MAX_NATIONS; i++) {
        fNation[i] = 1.0;
    }

    // если передан остров - смотрим его колонию
	if(sIslandID != "") {
		for(i = 0; i < MAX_COLONIES; i++) {
			if(colonies[i].island == sIslandID) {
				if (colonies[i].nation != "none") {
					iNation = sti(colonies[i].nation);
					break;
				}
			}
		}
	}

    // бонус за ближайшую колонию
    if(iNation != -1) fNation[iNation] += 0.2;

    makearef(arNation, EncountersTypes[iEncounterType].Nation);
    iCount = GetAttributesNum(arNation);
    // если доступна всего одна нация - сразу возвращаем
    if (iCount == 1) {
        arAttribute = GetAttributeN(arNation, 0);
        return sti(GetAttributeValue(arAttribute));
    }

    // формируем массив из допустимых для энкаунтера наций
    fAllowedNations[0] = 0.0;
    for (i = 1; i <= iCount; i++) {
        arAttribute = GetAttributeN(arNation, (i - 1));
        iNation = sti(GetAttributeValue(arAttribute));
        // собираем ряд
        fProbablyNation += fNation[iNation];
        // создаём массив допустимых наций
        iAllowedNations[i] = iNation;
        // и тут же определяем диапазоны в ряду
        fAllowedNations[i] = fAllowedNations[i - 1] + fNation[iNation];
    }

    // определяем победителя
	fProbablyNation = frand(fProbablyNation);
    if (fProbablyNation == 0.0) return iAllowedNations[1];
    for (i = 1; i <= iCount; i++) {
        if (fProbablyNation <= fAllowedNations[i] && fProbablyNation > fAllowedNations[i - 1]) return iAllowedNations[i];
    }
    
    return PIRATE;
}
Циклов осталось столько же, но операций внутри них стало меньше. Массивов тоже стало меньше. Из таких маленьких кубиков оптимизация и собирается.

Надеюсь, натолкнул вас на новые интересные решения.
Жду ваших вариантов реализации в комментариях (:

Содержание
`
ОЖИДАНИЕ РЕКЛАМЫ...