Modelowanie gier w GDL

Celem laboratorium jest zrozumienie regułowego opisu gier turowych. Student po zakończeniu laboratorium powinien potrafić zamodelować proste gry oraz wskazać trudności związane z tym zadaniem.

1 Preliminaria

GDL (Game Description Language) jest językiem opisu gier turowych. Powstał na potrzeby General Game Playing - działu sztucznej inteligencji, który zajmuje się szukaniem strategii grania, które działałyby niezależnie od zasad gry. Innymi słowy, bot powinien być w stanie grać (i wygrać) w dowolną grę opisaną w języku GDL.

Laboratoria bazują w dużej mierze na tutorialu ze Stanfordu oraz slajdach z uniwersytetu z Dresden.

2 Modelowania świata gry

Istnieją dwa zasadnicze podejścia do modelowania świata/środowiska gry. Jedno z nich (nazwijmy je gruboziarnistym) traktuje świat jako maszynę stanową - gdzie każdy unikalny stan świata jest reprezentowany jako oddzielny stan maszyny. Unikalność stanu traktujemy tu na poważnie, co do jednego zaplutego, spróchniałego pnia, zatem można się domyślić, że maszyna stanowa nawet dla prostych gier będzie ogromna. Poza tym stan jest czymś pozbawionym struktury, co nie pomaga w inteligentnym rozwiązywaniu problemu.

Z drugiej strony mamy języki w stylu PDDL, które dynamikę świata modelują przy użyciu fluentów - zdań logicznych, które zmieniają wartość. Tutaj stan świata ma strukturę (aktualne wartościowanie fluentów), które można wykorzystać podczas planowania, wnioskowania, etc.

GDL należy do drugiego podejścia - stan gry modelowany jest poprzez fluenty. Poza nimi w skład gry wchodzą akcje graczy (również modelowane w postaci fluentów) oraz reguły gry, które są reprezentowane w postaci… reguł.

3 Przykład: Kółko i krzyżyk

Przedstawmy GDL na przykładzie gry kółko i krzyżyk.

Gracze

Zacznijmy od tego, że w grze występuje dwóch graczy o pięknych imionach „kolko” i „krzyzyk”. Służy do tego predefiniowany fakt role:

role(kolko)
role(krzyzyk)

Składnia jest taka sama jak w języku Prolog (lub Datalog) z tą różnicą, że na końcu nie ma kropki. Ponownie stałe pisane są małą, a zmienne wielką literą.

Definiowanie fluentów

Następnie należy zdefiniować fluenty, które będą opisywać stan gry. W przypadku kółka i krzyżyk stan gry opisany jest przy pomocy 29 fluentów:

  • po 1 fluencie na każdą komórkę, mówiącym, czy w tej komórce jest krzyżyk (razem 9 fluentów)
  • po 1 fluencie na każdą komórkę, mówiącym, czy w tej komórce jest kółko (razem 9 fluentów)
  • po 1 fluencie na każdą komórkę, mówiącym, czy ta komórka jest pusta (razem 9 fluentów)
  • po 1 fluencie na każdego gracza, mówiącym, czy to jest tura danego gracza (razem 2 fluenty).

W celu zdefiniowania fluentów należy się posłużyć predefiniowanym predykatem base:

base(cell(M,N,x)) :- index(M) & index(N)
base(cell(M,N,o)) :- index(M) & index(N)
base(cell(M,N,b)) :- index(M) & index(N)
 
base(control(R)) :- role(R)
 
index(1)
index(2)
index(3)

Symbol :- to podobnie jak w Prologu implikacja . & reprezentuje koniunkcję. W języku naturalnym można powiedzieć, że pierwsza linijka powyższego kodu oznacza: „jeżeli M i N to prawidłowe indeksy komórki, to istnieje fluent dla komórki o współrzędnych M i N, mówiący o tym, że jest w niej krzyżyk”. Bez użycia reguł musielibyśmy wszystkie fluenty definiować wprost:

base(cell(1,1,x))    base(cell(1,1,o))    base(cell(1,1,b))
base(cell(1,2,x))    base(cell(1,2,o))    base(cell(1,2,b))
base(cell(1,3,x))    base(cell(1,3,o))    base(cell(1,3,b))
base(cell(2,1,x))    base(cell(2,1,o))    base(cell(2,1,b))
base(cell(2,2,x))    base(cell(2,2,o))    base(cell(2,2,b))
base(cell(2,3,x))    base(cell(2,3,o))    base(cell(2,3,b))
base(cell(3,1,x))    base(cell(3,1,o))    base(cell(3,1,b))
base(cell(3,2,x))    base(cell(3,2,o))    base(cell(3,2,b))
base(cell(3,3,x))    base(cell(3,3,o))    base(cell(3,3,b))
base(control(kolko))
base(control(krzyzyk))

Reguły w GDL'u różnią się od reguł Prologowych pod jednym ważnym aspektem: musi być zapewniony fakt, że się nie zapętlą. Więcej szczegółów na ten temat w slajdach z uniwersytetu z Dresden.

Dostępne akcje

Teraz należy się zastanowić nad tym, jakie akcje są wykonywalne w grze. W przypadku kółka i krzyżyk jest ich 20:

  • 1 akcja dla każdej komórki, mówiąca, że gracz 'kółko' ją zaznacza (łącznie 9)
  • 1 akcja dla każdej komórki, mówiąca, że gracz 'krzyżyk' ją zaznacza (łącznie 9)
  • 1 akcja noop dla każdego grazca, mówiąca, że gracz nie wykonał ruchu (łącznie 2).

Akcja noop istnieje tylko po to, żeby zasymulować odpowiednio ruchy graczy na zmianę. Gracz w nie swojej turze może jedynie wykonać noop.

W celu zdefiniowania akcji posługujemy się wbudowanym predykatem input:

input(R,mark(M,N)) :- role(R) & index(M) & index(N)
input(R, noop) :- role(R)

Predykat input przyjmuje dwa argumenty, pierwszy to nazwa gracza, który może wykonać akcję, drugim jest sama akcja.

Dozwolone ruchy

Teraz, znając już możliwe akcje, wypadałoby powiedzieć, kiedy dany ruch jest dozwolony. Służy do tego predykat legal. Aby sprawdzić, czy dany fluent jest aktualnie prawdziwy posługujemy się predykatem true.

legal(W,mark(X,Y)) :-
   true(cell(X,Y,b)) &
   true(control(W))

legal(kolko,noop) :-
   true(control(krzyzyk))

legal(krzyzyk,noop) :-
   true(control(kolko))

Innymi słowy - gracz może zaznaczyć dane pole, o ile jest puste i jest jego tura. Gracz może też wykonać akcję noop, jeżeli tura należy do przeciwnika.

Stan początkowy

Podobnie jak w PDDL'u, musimy zdefiniować stan początkowy świata. Służy do tego tego predykat init. Możemy przy jego pomocy określić, które fluenty są prawdziwe u zarania dziejów. Dla kółka i krzyżyk, wszystkie komórki są puste, a tura należy do kółka:

init(cell(M,N,b)) :- index(M) & index(N)
init(control(kolko))

Zmiana stanu

Teraz przechodzimy do części najtrudniejszej — w jaki sposób zamodelować zmiany stanu świata. Zakładamy, że czas w grze jest dyskretny i w każdej kolejnej turze gry obliczany jest nowy stan świata. Zależy on jedynie od poprzedniego stanu świata oraz akcji wykonanych przez graczy. Pojawia się tutaj problem ramki (lub klatki). Nazwa pochodzi od analogii między następującymi po sobie stanami świata z klatkami na taśmie filmowej. Wszystkie klatki filmowe są od siebie niezależne, różnią się od swoich poprzedników jedynie szczegółami — reszta jest taka sama. Podobnie tutaj chcielibyśmy, żeby poza explicite zamodelowanymi zmianami, reszta świata pozostała bez zmian. W naszym przypadku oznacza to, że musimy zamodelować również brak zmiany :-(

Do definicji reguł zmiany stany w GDL posługujemy się predykatem next, mówiącym, czy fluent będzie prawdziwy w kolejnej turze. Predykat 'does' pozwala na sprawdzenie, jaką akcję dany gracz wykonał w aktualnej turze:

next(cell(M,N,x)) :-
  does(krzyzyk,mark(M,N)) &
  true(cell(M,N,b))
 
next(cell(M,N,o)) :-
  does(kolko,mark(M,N)) &
  true(cell(M,N,b))
 
next(cell(M,N,W)) :-
  true(cell(M,N,W)) &
  distinct(W,b)
 
next(cell(M,N,b)) :-
  does(W,mark(J,K)) &
  true(cell(M,N,b)) &
  distinct(M,J)
 
next(cell(M,N,b)) :-
  does(W,mark(J,K))
  true(cell(M,N,b)) &
  distinct(N,K)
 
next(control(kolko)) :-
  true(control(krzyzyk))
 
next(control(krzyzyk)) :-
  true(control(kolko))

Akcje te kolejno oznaczają:

  1. jeżeli komórka jest pusta, a gracz krzyżyk ją zaznaczył, to w kolejnej turze w tej komórce będzie krzyżyk
  2. jeżeli komórka jest pusta, a gracz kółko ją zaznaczył, to w kolejnej turze w tej komórce będzie kółko
  3. jeżeli dana komórka nie była pusta (pomocniczy predykat distinct sprawdza tożsamość argumentów), to w kolejnej turze będzie miała te samą zawartość
  4. jeżeli dana komórka była pusta, ale nie była komórką, która została zaznaczona, to w kolejnej turze też będzie pusta (aż dwie reguły!)
  5. jeżeli dana tura należała do kółka, to kolejna będzie należała do krzyżyka
  6. jeżeli dana tura należała do krzyżyka, to kolejna będzie należała do kółka

Warto zauważyć, że akcje 3 i 4 służą właśnie walce z problemem ramki.

Warunki końca gry

Każdy wie, że celem gry w kółko i krzyżyk jest ułożenie linii. Żeby sprawdzić, czy w danym stanie ułożona jest linia, należy zdefiniować odpowiednie reguły:

line(Z) :- row(M,Z)
line(Z) :- column(M,Z)
line(Z) :- diagonal(Z)
 
row(M,Z) :-
  true(cell(M,1,Z)) &
  true(cell(M,2,Z)) &
  true(cell(M,3,Z))
 
column(N,Z) :-
  true(cell(1,N,Z)) &
  true(cell(2,N,Z)) &
  true(cell(3,N,Z))
 
diagonal(Z) :-
  true(cell(1,1,Z)) &
  true(cell(2,2,Z)) &
  true(cell(3,3,Z)) 
 
diagonal(Z) :-
  true(cell(1,3,Z)) &
  true(cell(2,2,Z)) &
  true(cell(3,1,Z)) 

Zapytanie true(line(kolko)) powie nam teraz, czy na planszy jest linia kółek.

Teraz należy zdefiniować kryteria zakończenia gry, używając predykatu terminal:

terminal :- line(x)
terminal :- line(o)
terminal :- ~open
 
open :- true(cell(M,N,b))

Gra się kończy, gdy:

  • istnieje linia krzyżyków
  • istnieje linia kółek
  • wszystkie pola są zajęte

Operator '~' oznacza negację. Podobnie jak w prologu działa ona na zasadzie świata zamkniętego, tzn. zdanie uznajemy za fałszywe, jeżeli nie potrafimy udowodnić, że jest inaczej. W GDL'u istnieją specjalne obostrzenie ograniczające stosowanie negacji, ale na razie nie musimy się tym przejmować (więcej info na slajdach z uniwersytetu z Dresden).

Kryterium zwycięstwa

Koniec zabawy — boty nie mają uczuć, nie mogą więc czerpać z gry przyjemności. Natomiast nadal mogą dostawać nagrody za dobrą grę. I naszym obowiązkiem jest te nagrody rozdać. W tym celu posługujemy się predykatem goal:

goal(krzyzyk,100) :- line(x) & ~line(o)
goal(krzyzyk,50) :- ~line(x) & ~line(o)
goal(krzyzyk,0) :- ~line(x) & line(o)
 
goal(kolko,100) :- ~line(x) & line(o)
goal(kolko,50) :- ~line(x) & ~line(o)
goal(kolko,0) :- line(x) & ~line(o)

Powyższe reguły mówią, że w przypadku zwycięstwa gracz dostaje całe 100 (tyle wygrać!), w przypadku remisu 50, a w przypadki porażki 0. Nie wiemy, co ta liczba symbolizuje, ale uznajmy, że są to ciasteczka, albo coś, czego nigdy nie za wiele.

W GDL nie ma arytmetyki — liczby nie różnią się niczym od innych stałych. Stosowane są głównie do zliczania kroków oraz rozdawania nagród.

Koniec

To by było na tyle, właśnie udało się zamodelować prostą grę w kółko i krzyżyk.

3.1 Zad 1

Weźmy przykładowy model jeszcze prostszej gry.

role(white)
role(black)
 
base(p)
base(q)
base(r)
base(s)
 
input(R,a) :- role(R)
input(R,b) :- role(R)
input(R,c) :- role(R)
input(R,d) :- role(R)
 
init(s)
 
legal(white,a)
legal(white,b)
legal(white,c)
legal(black,d)
 
next(p) :- does(white,a) & ~true(p)
next(p) :- ~does(white,a) & true(p)
next(q) :- does(white,b) & true(p)
next(q) :- does(white,c) & true(r)
next(q) :- ~does(white,b) & ~does(white,c) & true(q)
next(r) :- does(white,c) & true(q)
next(r) :- ~does(white,c) & true(r)
 
goal(white,100) :- terminal
goal(white,0) :- ~terminal
goal(black,100) :- terminal
goal(black,0) :- ~terminal
 
terminal :- true(p) & true(q) & true(r)

Odpowiedz na poniższe pytania:

  1. jak wielu graczy występuje w grze?
  2. jak wiele fluentów opisuje stan gry?
  3. jak wiele jest dostępnych akcji?
  4. ile akcji może wykonać gracz 'white' w początkowym stanie gry?
  5. jak wiele fluentów jest prawdziwych w początkowym stanie gry?
  6. załóżmy, że pierwszej turze gracz 'white' wykonał akcję 'a', a gracz 'black' akcję 'd'. Ile fluentów jest prawdziwych w drugiej turze?
  7. jaka jest najmniejsza liczba kroków potrzebna do zakończenia gry?
  8. czy gra zawsze się kończy? (czy mogłaby się zapętlić?)

3.2 Zad 2

Proszę zamodelować grę w 'krzyżyk i krzyżyk'. Podobnie jak w przypadku 'kółka i krzyżyk' mamy dziewięć pól, tym razem jednak obaj gracze stawiają krzyżyki. Przegrywa ten gracz, który pierwszy doprowadzi do powstania linii.

3.3 Zad 3

Proszę zamodelować grę w „kamień, papier, nożyce, spocka i jaszczurkę”. W razie problemów ze zrozumieniem reguł, proszę obejrzeć instrukcję video. Obrazek obok przedstawia użyteczną referencję. . Poćwiczyć można na żywo w sali, lub, jak na prawdziwego człowieka XXI wieku przystało, online....

4 Knowledge Interchange Format

O ile wszystko, co zostało powyżej napisane, jest prawdą, o tyle większość systemów GGP nie wspiera reprezentacji w stylu Prolog. Na potrzeby wysyłania i zapisania wiedzy (reprezentowanej w sposób regułowy) powstał format KIF (Knowledge Interchange Format). Podobnie jak w przypadku PDDL, bazuje on na lispie i używa notacji prefixowej. Ponadto symbol :- zestępowany jest przez strzałkę , symbol koniunkcji & przez and, symbol negacji ~ przez not. Nazwy zmiennych poprzedzane są znakiem zapytania ?. Poniżej przedstawiony jest przykład translacji z wersji a'la Prolog:

p(a,Y)                  
~p(a,Y)                 
p(a,Y) & p(Y,c)         
q(Y) :- p(a,Y) & p(Y,c) 
q(Y) :- p(a,Y) & p(Y,c) 

do formatu KIF:

(p a ?y)
(not (p a ?y))
(and (p a ?y) (p ?y c))
(<= (q ?y) (and (p a ?y) (p ?y c)))
(<= (q ?y) (p a ?y) (p ?y c))

4.1 Zad 1

Dla każdej z poniższych par określ, czy wersja prefixowa, jest wiernym tłumaczeniem wersji Prologowej.

r(a,b) :- p(a) & q(b)
(<= (r a b) (and (p a) (q b)))
r(a,b) :- p(a) & q(b)
(<= (r a b) (p a) (q b))
r(x,y) :- p(x) & q(y)
(<= (r ?x ?y) (p ?x) (q ?y))
r(X,Y) :- p(X) & q(Y)
(<= (r ?x ?y) (p ?x) (q ?y))

4.2 Zad 2

Zapoznaj się z wersją 'kółka i krzyżyk' zapisaną w KIF. Jakie różnice widzisz między nią a naszym poprzednim modelem?

4.3 Zad 3

Zapoznaj się z modelem prostego świata klocków w GDL.

  • jakie są kryteria zakończenia tej gry?
  • w jaki sposób liczone są tury?

4.4 Zad 4

Przepisz modele 'kólka i krzyżyka', 'krzyżyka i krzyżyka' oraz 'papier, kamień, nożyce, spocka i jaszczurki' do postaci KIF.

5 Walidacja modeli

Poniższe instrukcje przygotowane są z myślą o laboratorium C2 316, gdzie każdy komputer ma Eclipse.

Proszę uruchomić Eclipse i następnie:

  • File → Import Project
  • Wybrać opcję importowania z repozytorium git
    • Jeżeli tej opcji nie ma (Eclipse jest za stare), to trzeba ręcznie sklonować repo i zaimportować projekt z katalogu. Reszta tutejszej instrukcji nie ma sensu FIXME
  • Wybrać opcję klonowania z zewnętrznego URI
  • Wybrać gałąź master
  • Wybrać jakiś rozsądny katalog dla projektu
  • Wybrać projekt ggp-base do zaimportowania

5.1 Dodanie gier do środowiska

W katalogu projektu proszę dostać się do ścieżki games/games i stworzyć tam trzy katalogi kolko-krzyzyk, krzyzyk-krzyzyk i spock. Każdy katalog powinien zawierać dwa pliki:

  • plik .kif z modelem gry
  • plik METADATA z zawartością:
{
  "gameName": "<nazwa gry>",  
  "rulesheet": "<nazwa pliku kif>"
}

5.2 Walidacja gier

Z poziomu Eclipse proszę uruchomić aplikację Validator (rozwijana lista przy guziki play).

Validator, jak sama nazwa wskazuje, ma na celu sprawdzenie, czy dany model jest prawidłowym modelem GDL.

Ćwiczenia

  1. W polu Repository wybrać Local Game Repository
  2. W polu Game wybrać Tic-Tac-Toe
  3. Wciśnąć przycisk Validate — program powinien pokazać same sukcesy
  4. Powtórzyć to samo dla własnych modeli
  5. Poprawić własne modele
  6. Zdobyć kolejne sukcesy.

6 Zabawa

Z poziomu Eclipse proszę uruchomić aplikację Kiosk (rozwijana lista przy guziki play).

Ćwiczenia

  1. W polu Opponent proszę wybrać SimpleMonteCarloPlayer
  2. Wybrać jakąś znajomą grę
  3. Wygrać!

pl/dydaktyka/ggp/gdl.txt · ostatnio zmienione: 2017/07/17 08:08 (edycja zewnętrzna)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0