Jak od podstaw stworzyć własnego bota do systemu Meridius [1]: aktualizacja bota, protokół komunikacji z grą, przykłady prostych botów.
Tworzenie i edycja bota [2]
Protokół komunikacji [3]
Tworzenie bota do gry Kółko i krzyżyk [4]
Tworzenie nowego bota (ekran tworzenia bota został przedstawiony poniżej) jest podobnym procesem do tworzenia profilu gracza, wymaga jednak podania większej liczby informacji. Oprócz gry, nazwy zawodnika i jego typu, potrzebna jest jeszcze wiedza o języku programowania w którym będzie on pisany oraz o trybach w które będzie mógł grać. Co ważne, obu tych danych nie będzie można później zmienić. Pole Język jest ustawione domyślnie na język programowania określony w profilu użytkownika jako ulubiony. Bot może (ale nie musi) mieć od razu przypisany swój kod źródłowy (patrz kolejna sekcja).
Ze świeżo utworzonym botem nie można rozegrać żadnego meczu jeśli nie posiada on żadnego kodu źródłowego. Aby uczynić go w pełni grywalnym należy w zakładce Statystyki wybrać plik z kodem źródłowym, opcjonalnie dodać opis i wybrać przycisk Zaktualizuj bota. W tabeli Moje aktywności w lewym panelu pojawi się informacja o kompilacji oraz jej efekcie.
Jeśli wszystko przebiegnie pomyślnie czyli znaczek informacyjny będzie zielony, w tabeli po prawej stronie (patrz rysunek poniżej) zostanie dodany nowy wiersz z aktualnym kodem źródłowym, który można pobrać na dysk (1) lub obejrzeć (2). Kod bota można wielokrotnie aktualizować i każdorazowo zwiększa to numer wersji bota.
Jeśli kompilacja z jakichś powodów się nie powiedzie, po najechaniu na przycisk informacyjny (który powinien mieć kolor czarny) pojawi się okno informujące o treści błędu. Numer wersji bota nie ulegnie zmianie i nadal obowiązujący będzie ostatnio wgrany poprawny kod.
Poniżej znajduje się tabela poleceń kompilujących przypisanych obsługiwanym językom programowania:
Język | Polecenie kompilujące |
Bash | bash -n /home/safeuser/program.sh |
C | gcc -Wall -W -std=gnu99 -O2 /home/safeuser/program.c -o /home/safeuser/program |
C++ | g++ -Wall -W -O2 /home/safeuser/program.cpp -o /home/safeuser/program |
C# | gmcs /home/safeuser/program.cs |
Haskell | ghc -Wall -Werror -O2 /home/safeuser/program.hs -o /home/safeuser/program |
Java | javac -encoding utf8 /home/safeuser/program.java |
Pascal | fpc -O2 -XS /home/safeuser/program.pas |
Perl | perl -c /home/safeuser/program.pl |
Python | python -mpy_compile /home/safeuser/program.py |
Ruby | ruby -c /home/safeuser/program.rb |
Text | echo -n |
Nazwy wysyłanych plików nie mają znaczenia. Jedyne obostrzenie dotyczy botów tworzonych w języku Java: metoda main
musi się znajdować w publicznej klasie program
.
Podczas oglądania profilu stworzonego przez siebie bota, oprócz możliwości aktualizacji kodu źródłowego, mamy dostęp do kilku dodatkowych opcji.
W zakładce Uprawnienia istnieje możliwość udostępnienia dowolnemu użytkownikowi wglądu w kod źródłowy bota. W tym miejscu znajduje sie również lista wszystkich użytkowników z nadanymi uprawnieniami do danego bota wraz z opcją ich usunięcia.
W zakładce Grupy bota możemy zobaczyć do jakich grup należy. Aby dodać bota do istniejącej grupy należy wyszukać ją w menu Zawodnicy podmenu Grupy Botów. Nie wszystkie grupy pozwalają na swobodny zapis: część jest dostępna jedynie dla botów z określonej gry lub określonego typu, a niektóre mogą nawet wymagać od użytkownika podania hasła.
W momencie oglądania szczegółów własnego bota, panel po prawej stronie rozszerza się o tabelę zatytułowaną Ostatnie błędy w której wylistowane są najświeższe rozgrywki w których bot spowodował błąd wraz ze szczegółową treścią tego błędu (niedostępną dla innych użytkowników). Treści zaczynające się od symbolu @
są błędami związanymi nie z wykonaniem programu, a z niezgodnością z zasadami wykrytą przez Meridiusa, i tak np. błędowi przekroczenia czasu (Timeout (bot)) towarzyszy informacja @STOP!TIME
.
Wciąż jednak nie wiemy w jaki sposób napisać bota, który potrafi komunikować się z grą i naprawdę w nią grać. Dlatego w tym rozdziale, na przykładzie gry Kółko i krzyżyk [5], przedstawimy protokół komunikacji, a w następnym stworzymy bardzo prostego bota poprawnie grającego w tę grę.
Komunikacja pomiędzy botem a logiką (programem kontrolującym zasady gry) przebiega przez standardowe wejście-wyjście. Każdy wysłany komunikat należy zakończyć znakiem nowej linii.
Wszystkie obsługiwane przez system gry są turowe. Każdy bot sam decyduje kiedy zakończyć swoją turę, ale ma na to określoną zasadami gry ilość czasu. Zakończenie tury polega na wypisaniu na ekran frazy #END\n
, gdzie \n
jest znakiem nowej linii. Jeśli komunikat systemowy (wszystkie komunikaty zaczynające się od #
posiadają specjalne właściwości i nazywane są komunikatami systemowymi) końca tury nie dotrze do gry w wymaganym czasie, gra kończy się błędem z powodu przekroczenia przez bota limitu czasu.
Z punktu widzenia programu bota komunikat końca tury dzieli wykonywany kod na to co się dzieje przed końcem tury od tego co się dzieje już w turze następnej. Innymi słowy wszystkie komunikaty wysłane po #END\n
dotrą do logiki dopiero w następnej turze. Program bota można więc sobie wyobrażać w pseudokodzie jako pętlę:
1 2 3 | while True: RozegrajTure() print "#END\n" |
Program bota nie musi się kończyć i dopuszczalne (a wręcz pożądane) jest aby działał w nieskończonej pętli. W momencie zakończenia gry system Meridius sam zadba o zamknięcie uruchomionych botów.. Warto jeszcze nadmienić, że zakończenie programu bota zanim zakończy się gra zostaje potraktowane jako ucieczka i kończy grę z błędem z winy bota.
Zapytania są to komunikaty których celem jest uzyskanie informacji na temat aktualnego stanu gry. Prawidłowe zapytania dla danej gry są określone w jej dokumentacji i mają postać funkcji która może (ale nie musi) pobierać pewne argumenty. Gra natomiast przesyła zakończoną znakiem nowej linii odpowiedź na to zapytanie na standardowe wejście.
Rozważmy kilka przykładów. W dokumentacji gry Kółko i krzyżyk jest napisane, że zapytanie LastMove
(konwencją jest, że w zapytania są oznaczane kolorem niebieskim) zwraca współrzędne ostatnio postawionego symbolu w formacie x
spacja y
lub -1 -1
jeśli jest to pierwsza tura gry i żaden z graczy nie postawił jeszcze symbolu. W związku z tym możliwy jest następujący scenariusz komunikacji (kolor czarny oznacza treści wysyłane przez logikę gry):
LastMove
2 2
Był to przykład zapytania które nie posiada argumentów. W grze można również poznać symbol znajdujący się na konkretnej pozycji korzystając z zapytania SymbolAt ?x ?y
. O zapytaniu takim można myśleć jak o funkcji nazywającej się SymbolAt
i pobierającej dwa argumenty (w przypadku tego zapytania są to dwie liczby oznaczające współrzędne pola o którym chcemy uzyskać informację). Zapytanie takie wywołujemy podstawiając w miejsce argumentów interesujące nas wartości. Pomiędzy kolejnymi argumentami (oraz pomiędzy nazwą funkcji a pierwszym argumentem) musi być co najmniej jedna spacja, a argumenty opcjonalnie można brać w cudzysłów. Przykładowy scenariusz komunikacji:
SymbolAt 1 2
X
SymbolAt "1" 2
X
SymbolAt 2 2
O
Przesłane do gry zapytania są od razu obliczane: odpowiedź jest generowana i niezwłocznie przekazywana botowi. Komendy są funkcjami wywoływanymi przez boty analogicznie jak zapytania, różnią się jednak tym, że wykonanie komend jest odłożone na czas po zakończeniu tury przez danego bota i gra w żaden sposób na nie nie odpowiada.
Przykładowo w grze Kółko i krzyżyk bot ma dostępną jedną komendę PlaceSymbol ?x ?y
(zgodnie z konwencją komendom przypisany jest kolor czerwony), która jest deklaracją postawienia symbolu na polu ?x ?y
. Ponieważ komenda jest wykonania dopiero po zakończeniu tury, nie modyfikuje ona wartości zwracanych przez zapytania, co ilustruje następujący przykład z gry:
SymbolAt 0 0
-
PlaceSymbol 0 0
SymbolAt 0 0
-
Natomiast w sytuacji gdy bot zakończy turę, to kolejne zapytanie będzie miało miejsce po ustawieniu symbolu na wskazanym polu i zwróci nową wartość:
#END
SymbolAt 0 0
O
Podsumowując, najczęstsze wykorzystanie komend i zapytań jest następujące:
Przesłane do gry komendy są składowane i wywoływane w takiej kolejności w jakiej przychodziły. Każda gra może jednak różnie definiować szczegóły, np. który ruch brany jest pod uwagę jeśli bot może w trakcie tury może wykonać tylko jedną akcję. W przypadku gry Kółko i krzyżyk liczy się decyzja która została podjęta jako ostatnia.
Przedstawimy teraz krok po kroku metodę konstrukcji prostego bota grającego w grę Kółko i krzyżyk [5]. Bot ten będzie stawiał swój symbol na losowym niezajętym dotychczas przez nikogo polu i zostanie przygotowany jedynie do standardowego trybu gry. Kod bota będzie tworzony równolegle w dwóch językach programowania: Python i C++.
Pola na planszy są to pary liczb. Ponieważ ograniczamy się do gry standardowej, możliwych jest 9 takich par: (0, 0), (0, 1), (0, 2), (1, 0), ..., (2, 2). Zauważmy, że korzystając z dzielenia całkowitoliczbowego oraz operacji modulo (reszty z dzielenia) możemy te pary zakodować jako dziewięć kolejnych liczb od 0 do 8. Wtedy np. liczbie 7 odpowiada para (2, 1) (bo 7/3 = 2 i 7%3 = 1), natomiast parze (1, 2) odpowiada liczba 5 (bo 5 = 1*3 + 2).
Zasada działania bota będzie polegała na pamiętaniu listy wolnych pól na planszy zakodowanych jako pojedyncze liczby i stopniowym usuwaniu ich jeśli któryś z zawodników postawi na nich swój symbol. Aby wykonać ruch, bot po prostu wylosuje jeden z pozostałych w liście elementów, odkoduje przypisane mu pole i usunie z listy.
Na początku wczytujemy odpowiednie biblioteki, inicjalizujemy generator liczb losowych, tworzymy i wypełniamy listę wolnych pól (początkowo są na niej wszystkie pola). Cała reszta kodu będzie się działa w nieskończonej pętli.
Python1 2 3 4 5 | import random free = range(9) while True: |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <iostream> #include <vector> #include <algorithm> #include <stdlib.h> #include <time.h> using namespace std; int main () { int x, y; srand ( time(NULL) ); vector<int> free (9, 0); for (int i = 0; i < 8; i++) free[i] = i; while (true) { |
Pierwszą czynnością, którą chcemy aby nasz bot wykonał w każdej turze jest uzyskanie wiedzy o ostatnim wykonanym ruchu. Korzystamy w tym celu z zapytania LastMove
zwracającego dwie liczby oddzielone spacją. Pozyskane współrzędne zapisujemy jako zmienne x
i y
.
1 | x, y = map(int, raw_input("LastMove\n").split()) |
1 2 | cout << "LastMove" << endl; cin >> x >> y; |
Następnie chcemy usunąć to pole z listy wolnych pól, należy je więc zakodować jako jedną liczbę wzorem x*3+y
. Musimy jednak pamiętać o przypadku gdy żaden ruch nie został wcześniej wykonany (zapytanie zwraca -1 -1
), wtedy nic z listy nie usuwamy.
1 2 | if x != -1: free.remove(x*3 + y) |
1 2 | if (x != -1) free.erase(remove(free.begin(), free.end(), x*3+y), free.end()); |
Kolejnym krokiem jest losowy wybór wolnego pola w postaci zakodowanej oraz usunięcie go z listy wolnych pól (za chwilę nie będzie ono wolne ponieważ zaraz postawimy tam symbol).
Python1 2 | move = random.choice(free) free.remove(move) |
1 2 | int move = free[rand()%free.size()]; free.erase(remove(free.begin(), free.end(), move), free.end()); |
Pozostało jedynie, korzystając z komendy PlaceSymbol 0 0
, wysłać do gry wybrany ruch i zakończyć turę.
1 2 | print "PlaceSymbol", move / 3, move % 3 print "#END" |
1 2 3 | cout << "PlaceSymbol " << move / 3 << " " << move % 3 << endl << "#END" << endl; } } |
Strategia przedstawionego bota nie ma oczywiście większego sensu, pozwala natomiast zapoznać się z obsługą systemu i w szybki sposób zdobyć podstawową wiedzę "jak pisać boty". W drugiej części tego poradnika omówimy dokładniej proces testowania bota, a więc systemy wspomagające wykrywanie błędów.
Poniżej znajdują się pełne kody źródłowe przykładowego bota (bot ten jest dostępny w systemie pod nazwą TutorialBot):
Python1 2 3 4 5 6 7 8 9 10 11 | import random free = range(9) while True: x, y = map(int, raw_input("LastMove\n").split()) if x != -1: free.remove(x*3 + y) move = random.choice(free) free.remove(move) print "PlaceSymbol", move / 3, move % 3 print "#END" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <iostream> #include <vector> #include <algorithm> #include <stdlib.h> #include <time.h> using namespace std; int main () { int x, y; srand ( time(NULL) ); vector<int> free (9, 0); for (int i = 0; i < 8; i++) free[i] = i; while (true) { cout << "LastMove" << endl; cin >> x >> y; if (x != -1) free.erase(remove(free.begin(), free.end(), x*3+y), free.end()); int move = free[rand()%free.size()]; free.erase(remove(free.begin(), free.end(), move), free.end()); cout << "PlaceSymbol " << move / 3 << " " << move % 3 << endl << "#END" << endl; } } |
Odnośniki:
[1] http://www.gryprogramistyczne.pl
[2] http://informatyka.wroc.pl/?page=0,0
[3] http://informatyka.wroc.pl/?page=0,1
[4] http://informatyka.wroc.pl/?page=0,2
[5] http://informatyka.wroc.pl/node/1409