Opis zadania — bitewne AI dla VCMI
Przebieg bitwy - jak to wygląda od strony AI
Lasciate ogni speranza, voi ch'entrate.
Contents
Informacje ogólne
Zadanie
Przedmiotem zadania jest napisanie programu, który będzie sterował graczem w czasie bitwy w otwartej reimplementacji Heroesa 3 — VCMI. Sprowadza się to do napisania w języku C++ dynamicznie ładowanej biblioteki (zależnie od platformy .dll bądź .so), zawierającej klasę implementującą interfejs dla AI.
Skrótowy opis bitwy
Uwaga: opis w tej sekcji jest mocno uproszczony. Ma za zadanie wprowadzić do zadania i wyrobić podstawowe intuicje, uszczegółowiony jest przez reguły zawarte dalej. W razie rozbieżności, to one są decydujące.
Podstawy
Bitwa toczona jest pomiędzy dwiema armiami, które zaczynają rozstawione po przeciwległych krańcach planszy. Armia może (choć nie musi) być dowodzona przez bohatera. Gracza „lewego” nazywamy „atakującym”, „prawego” zaś „broniącym się”. Armia składa się z oddziałów (jednostek) — każdy oddział jest charakteryzowany przez typ (np. pikinier albo czerwony smok) oraz liczebność. Dodatkowo każda jednostka posiada szereg zmiennych współczynników opisujących jej parametry bojowe, do najważniejszych zaliczają się:
- Atak
- Obrona
- Zakres zadawanych obrażeń
- Wytrzymałość (punkty życia — HP)
- Szybkość
Oddział ginie, gdy jego liczebność spadnie do zera. Gracz, który straci wszystkie oddziały, przegrywa bitwę.
Ruch
Bitwa podzielona jest na tury. Każda jednostka rusza się raz na turę. Oddziały wykonują ruchy po kolei, w porządku malejącej szybkości. Za każdym razem, gdy wypada kolej ruchu jednostki sterowanej przez AI, następuje wywołanie metody activeStack. Zadaniem AI jest zwrócenie struktury opisującej, co dana jednostka ma uczynić. Podstawowe akcje to:
- Atak — jednostka może zaatakować sąsiadującą jednostkę w zwarciu, bądź — jeśli umie strzelać — dowolną jednostkę na mapie.
- Ruch — jednostka może przesunąć się o tyle pól na mapie, ile wynosi jej szybkość. Ruch może zostać zakończony atakiem na osiągnięty oddział wroga.
- Czekanie — jednostka spróbuje się ruszyć później w tej turze (najwyżej raz na turę).
- Obrona — jednostka rezygnuje z akcji, aby czasowo poprawić swój współczynnik obrony.
Pole bitwy
Pole bitwy składa się z heksagonalnych pól ułożonych w 11 linii po 17 pól, ponumerowanych jak pokazano na rysunku.
Pola w dwóch skrajnych kolumnach nie są dostępne dla zwykłych jednostek. Ponadto niektóre z heksów (tj. pól) mogą być zablokowane ze względu na umieszczone na nich przeszkody. Heksy na których stoi już inne jednostka także traktowane są jak zablokowane. Na takim polu żadna z jednostek nie może zakończyć ruchu, przekraczać zaś to pole mogą wyłącznie jednostki latające.
Pozycją jednostki jest numer heksa, na którym stoi. Każda jednostka zajmuje jednego lub dwa sąsiadujące w poziomie heksy. W przypadku jednostki dwuheksowej jej pozycją jest pozycja PRZODU jednostki (wojska atakującego są zawsze zwrócone w prawo, broniącego się zaś w lewo).
Bohater
Jak wskazano wcześniej armia może być dowodzona przez bohatera. Wiąże się to paroma korzyściami:
- Bohater może posługiwać się magią (p. niżej)
- Bohater może posiadać specjalne machiny wojenne
- Jednostki otrzymują premie do atrybutów, zależne od parametrów bohatera
Czary
Bohater, jeżeli jest obecny na polu bitwy, może raz na turę, przed przesunięciem oddziału (w czasie, gdy ten jest aktywny) rzucić zaklęcie. Każdy bohater może posiadać księgę zaklęć, określającą, jakie czary są dostępne (bohater nie posiadający księgi nie może czarować). Rzucenie czaru wymaga poświęcenia pewnej liczby punktów many. Bohater, który wyczerpie swoją manę, traci możliwość rzucania czarów.
Jak AI komunikuje się z grą
Do komunikacji służą dwa interfejsy:
- CGlobalAI — główna klasa AI musi implementować ten interfejs. Silnik gry wywołuje jego metody, by informować AI o wydarzeniach w grze bądź by zapytać, jaką akcję chce podjąć.
- ICallback — interfejs zaimplementowany w silniku, udostępniany AI. AI może wywoływac jego metody, by pobierać informacje o stanie bitwy oraz by podejmować niektóre specjalne akcje.
Każde AI jest kompilowane do dynamicznie ładowanej biblioteki, która musi eksportować następujące funkcje:
-
void GetAiName(char* name);
— powinno wypisać nazwę AI -
CGlobalAI* GetNewAI();
— powinno stworzyć nowy obiekt głównej klasy AI (dziedziczącej po CGlobalAI), które pokieruje nadchodzącą bitwą. Silnik na otrzymanym obiekcie będzie wywoływał stosowne metody. Stanowić one będą podstawę komunikacji silnik -> AI. Pierwszym wywołaniem będzie metoda init, poprzez którą AI otrzyma wskaźnik na implementację interfejsu ICallback, poprzez który AI może „odpytywać silnik”.
Co AI *musi* robić?
Choć interfejs dla AI jest bogaty i zawiera wiele metod, tak naprawdę koniecznie wymagane jest zaimplementowanie tylko jednej. Jest to:
BattleAction activeStack(int stackID)
Metoda ta jest wołana, ilekroć AI musi podjąć akcję dla jakiejś jednostki. Należy zwrócić poprawnie wypełnioną strukturę BattleAction, opisującą przedsiębraną akcję akcję.
Implementacja wszystkich innych metod jest opcjonalna — służą one informowaniu AI o wydarzeniach w bitwie, niemniej AI może te informacje samodzielnie (acz żmudnie) pozyskiwać odpytując interfejs ICallback. Do tego jednak konieczne jest zapamiętanie jego adresu — drugą funkcją, którą należy więc przeciążyć jest:
void init(ICallback * CB)
Nie jest to „konieczne” w sensie ścisłym, niemniej bez tego AI nie będzie w stanie sensownie działać.
Ważne klasy i ich ważne atrybuty
Węzeł systemu bonusów — CBonusSystemNode
Wszystkie opisane niżej klasy (i wiele innych) dziedziczą po CBonusSystemNode. Oznacza to, że są zarządzane przez system bonusów. Najkrócej ujmując, system bonusów pozwala określić dla każdego z węzłów wartość szeregu atrybutów (np. liczba punktów ataku), obecność flag i efektów.
Oddział — CStack
Podstawowa klasa opisująca oddział na polu bitwy. Żaden z oddziałów nie zostanie skasowany w trakcie bitwy — obiekty tej klasy trwać będą nawet po śmierci oddziału. Natomiast w czasie bitwy mogą pojawić się nowe oddziały, wtedy AI otrzyma wywołanie battleNewStackAppeared.
Ważne pola
- TQuantity count — liczebność oddziału
- THex position — numer heksa na którym stoi oddział (lub jego przód, jeśli zajmuje dwa heksy)
- ui32 firstHPleft — ile punktów zdrowia potrzeba odebrać, by ubić pierwszego stwora w oddziale.
Ważne metody
- ui32 Speed(int turn = 0) — oblicza szybkość stwora (opcjonalnie — za ileś tur)
Obiekt z armią — CArmedInstace
Obie armie uczestniczące w bitwie nie biorą się znikąd. Silnik gry musi je skojarzyć z jakimś uzbrojonym obiektem. CArmedInstance stanowi bazową klasę dla bohatera, miasta (opisanych niżej) oraz szeregu mniej znaczących klas reprezentujących rozmaite obiekty z armią.
Bohater — CGHeroInstance
CGHeroInstance to podstawowa klasa reprezentująca bohatera - obiekt posiadający w sensie systemu bonusów wszystkie jednostki AI. W trakcie rozgrywki każde AI ma dokładnie jednego bohatera, żyjącego przez cały czas bitwy. Jest to węzeł pośredniczący w przekazywaniu pewnych bonusów jednostkom, jednak sam również generuje pewne premie. Jego obecność umożliwia AI rzucanie czarów zapisanych w księdze zaklęć bohatera.
Ważne pola
- si32 mana — liczba punktów many (za te punkty rzuca się czary).
- std::set<ui32> spells — identyfikatory znanych czarów.
Ważne metody
- int getPrimSkillLevel(int id) — pozwala sprawdzić wartość umiejętności drugorzędnej. Przykład: h->getPrimSkillLevel(PrimarySkill::SPELL_POWER)
- ui8 getSecSkillLevel(SecondarySkill skill) — pozwala sprawdzić wartość umiejętności drugorzędnej (wyniki: 0 — brak; ...; 3 — ekspert)
Miasto — CGTownInstance
Niektóre bitwy są oblężeniami miast. W takim przypadku broniące się AI (to po prawej stronie pola bitwy) dostaje dodatkowe premie z tego powodu. Przede wszystkim miasto może być wyposażone w fort/cytadelę/zamek, które powodują, że obrońca jest otoczony murem z opcjonalną fosą i wieżyczkami strażniczymi (zależne od poziomu ulepszenia). Ponadto niektóre miasta dodają inne premie broniącej się armii - patrz np. tutaj. AI atakujące miasto z murami otrzymuje katapultę mogącą niszczyć mury i wieżyczki strażnicze.
Informacje szczegółowe
Początek bitwy
Na początku bitwy AI otrzymuje od silnika wywołanie funkcji void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side); w której przekazywane są następujące informacje:
- jednostki należące do atakującego (znajduje się po lewej stronie pola bitwy)
- jednostki należące do broniącego się (znajduje się po prawej stronie pola bitwy)
- lokalizację pola bitwy na mapie przygody (mogą od tego parametru zależeć premie lub kary)
- informacje o bohaterze atakującym i broniącym się, takie jak czary, które może rzucać, bonusy dla jednostek itp.
- (nadmiarową) informację, po której stronie AI ma walczyć.
Opcjonalnie, zależnie od artefaktów posiadanych przez bohatera, zaraz po tym wywołaniu możliwe są wywołania takie jak przy rzucaniu czaru.
Przebieg tury
Każda tura zaczyna się od dwu wywołań:
void battleNewRoundFirst(int round) void battleNewRound(int round)
z których najpierw wykonywane jest pierwsze, a potem drugie (obydwa dostają jako parametr numer tury). Różnią się tym, że pierwsze jest robione przed, a drugie po naniesieniu na stan gry zmian wynikających z rozpoczęcia się nowej rudny (np. zakończenie działania pewnych efektów czarów).
Następnie po kolei dla każdej jednostki następuje jedna z poniższych sytuacji:
- Jednostka traci turę z powodu złego morale - szansa zależna od wartości morale danej jednostki. AI dostaje wywołania o początku i końcu akcji BAD_MORALE.
- Jednostka jest pod wpływem berserku i automatycznie atakuje najbliższą jednostkę pozostającą w zasięgu (AI otrzymuje informację o początku i końcu akcji ataku na najbliższą jednostkę - WALK_AND_ATTACK). W przypadku braku jednostek w zasięgu jednostka nic nie robi (AI dostaje informację o początku i końcu akcji DO_NOTHING).
- Jeżeli jednostka jest balistą, a bohater AI nie posiada umiejętności "artyleria", to AI otrzymuje informację o początku i końcu akcji strzału balisty.
- Jeśli jednostka jest namiotem medyka, a bohater AI nie posiada umiejętności "pierwsza pomoc", to AI otrzymuje informację o początku i końcu akcji STACK_HEAL (jeśli jakaś jednostka jest ranna) lub DO_NOTHING (jeśli żadna nie jest ranna).
- Jeśli nie zaszła żadna z poprzednich możliwości, AI będące posiadaczem jednostki (lub wrogie, jeśli jednostka jest zahipnotyzowana) jest proszone o podanie akcji, którą oddział winien podjąć.
- Po wykonaniu akcji, jeżeli jednostka jest żywa i ma dodatnie morale, istnieje szansa na uzyskanie przez nią powtórnego ruchu — AI wtedy ponownie jest pytane o akcję (jak wyżej).
Jeżeli po przejściu tej sekwencji bitwa ciągle nie jest skończona (obie strony posiadają żywe oddziały, następuje kolejna tura.
Akcje jednostek i ich wydawanie
Zapytanie o akcję realizowane jest za pomocą funkcji BattleAction activeStack(int stackID) gdzie jako parametr występuje unikalny identyfikator jednostki. Funkcja ma zwrócić obiekt opisujący ruch jednostki lub rzucany czar. Możliwe akcje są następujące:
- rzucenie czaru przez bohatera (nie powoduje utraty tury przez jednostkę, można rzucać czar tylko raz na turę - o ile bohater AI ma taką możliwość w ogóle)
- przejście jednostki na inne pole
- polecenie przejścia jednostki do obrony - jednostka traci turę, ale zwiększa się jej współczynnik obrony
- ucieczka AI z pola bitwy (może być niemożliwa, zależnie od posiadanych artefaktów
- poddanie się AI
- zaatakowanie pieszo (ang. melee) jednostki wroga znajdującej się w zasięgu, z wybranego pola sąsiadującego
- strzał do dowolnej jednostki (może być niemożliwy, nie każda jednostka strzela, stojąca koło jednostki wroga jednostka z reguły blokuje możliwość strzelania; każdy strzał zmniejsza liczbę pocisków jednostki, chyba, że na polu bitwy jest wóz z amunicją (Ammo Cart). Jednostka z liczbą pocisków równą zero nie może strzelać)
- czekanie (jednostka będzie się ruszała na końcu tury, po wszystkich jednostkach z niższą inicjatywą)
- rzucenie czaru przez jednostkę (nieliczne jednostki to potrafią, jest to jeszcze nieobsługiwane, ale powinno w końcu się pojawić)
- leczenie innej jednostki (dla namiotu medyka)
Informacja o początku / końcu akcji
Każde polecenie wysłane przez AI do silnika jest analizowane pod kątem możliwości jego wykonania. Jeśli uzna, że polecenie jest wykonalne (czyli np. nie jest poleceniem rzucenia nieposiadanego czaru lub próbą ataku własną jednostką na inną własną jednostkę), przystępuje do jego wykonania. Wykonanie akcji zawsze zaczyna się od poinformowania o jej początku przez wywołanie void actionStarted(const BattleAction *action){}
, następnie, jeśli jest taka potrzeba, następują wywołania o efektach akcji (np. jednostka ruszyła się na inne pole / zaatakowała jakąś inną / czar został rzucony), całość zaś kończy wywołanie void actionFinished(const BattleAction *action){}
oznaczające, że wszystkie efekty związane z daną akcją zostały obsłużone.
Wywołania AI -> silnik
Obecnie zaimplementowane są następujące wywołania w callbacku:
Umożliwiają one dostęp do każdej mechanicznie istotnej informacji o stanie gry, do jakiej dostęp ma AI. Część informacji zdobywana jest jednak w sposób pośredni, przez wywoływanie odpowiednich metod na obiektach dostarczanych przez wymienione metody. Pewne niezmienne w czasie elementy mechaniki sa także dostępne przez specjalny obiekt klasy VLC (dostępne dla AI po zainclude'owaniu pliku /lib/VCMI_Lib.h).
Czary bohaterów
Bohaterowie mogą rzucać w trakcie bitwy czarować. Aby rzucić zaklęcie konieczne jest jednak spełnienie następujących warunków:
- Bohater musi posiadać księgę zaklęć oraz nie rzucił jeszcze w tej turze czaru. Aby sprawdzić, czy nasz bohater w danym momencie bitwy jest zdolny do czarowania, można posłużyć się metody
bool CCallback::battleCanCastSpell()
. - Zaklęcie jest dostępne dla bohatera: ma je zapisane w księdze zaklęć bądź uzyskał w drodze bonusu (np. dzięki artefaktowi). Aby sprawdzić, czy konkretny czar jest dostępny dla bohatera, należy na nim wywołać metodę
bool CGHeroInstance::canCastThisSpell(const CSpell * spell) const
. Wektor wszystkich czarów w grze jest dostępny np. jakoVLC->spellh->spells
- Bohater ma nie mniej punktów many niż wynosi koszt zaklęcia. Aby sprawdzić koszt rzucenia czaru, należy użyć metody
int CCallback::getSpellCost(const CSpell * sp, const CGHeroInstance * caster) const
, podając jako argumenty wybrany czar i naszego bohatera-dowódcę. Liczba punktów many bohatera jest publicznie dostępnym polem klasyCGHeroInstance
o nazwiemana
- Czar można rzucić tylko, gdy nasza jednostka oczekuje na wykonanie akcji: musi się to odbyć po wywołaniu metody activeStack, a przed jej zwróceniem. Rzucenie czaru nie zwalnia od obowiązku zwrócenia akcji dla obecnego oddziału. [TODO: opisać synchronizację oraz przypadek utraty akcji, jeśli oddział zginie w efekcie rzuconego czaru.
Aby rzucić czar, należy wywołać metodę int CCallback::battleMakeAction(BattleAction* action)
, gdzie struktura BattleAction opisuje rzucony czar: typ akcji wynosi BattleAction::HERO_SPELL
, pole additionalInfo
zawiera ID czaru, pole destinationTile
docelowy heks na który ma być rzucony czar (jeśli dotyczy). Pamiętać również trzeba o polu side
— musi być ustawione zgodnie z naszą stroną (0 — atakujący, 1 — obrońca).
W grze obecnie zaimplementowane jest 48 z 59 czarów bitewnych dostępnych w oryginalnej grze. Listę zaimplementowanych czarów można znaleźć tutaj, a opisy działania czarów np. tutaj (wyświetla tylko czary magii powietrza; aby obejrzeć inne, trzeba wybrać inną opcję z menu na górze!) Wśród nich są czary zadające jednostkom wroga bezpośrednie obrażenia, zwiększające parametry jednostek AI, obniżające parametry wrogich jednostek lub specyficzne czary pozwalające np. przejąć kontrolę nad wrogą jednostką na pewien czas.
Koniec bitwy
Jeżeli jedna ze stron się podda, ucieknie, lub wszystkie jej jednostki poza maszynami bojowymi zostaną zabite, bitwa się kończy. Oba walczące AI dostają wywołanie void battleEnd(const BattleResult *br); Zawierające informacje o typie zwycięstwa, wygranej stronie, ofiarach, doświadczeniu zdobytym przez bohatera oraz przejętych artefaktach.
Przebieg rozgrywek i punktacja
TBD - za co będą przyznawane punkty, jak przebiegają poszczególne fazy rozgrywek, ujawnienie przynajmniej części testowych pól bitewnych, bohaterów i zestawów jednostek