Uniwersytet Warszawski Wydział Matematyki, Informatyki i Mechaniki

Michał Raczkowski Nr albumu: 214564

Implementacja gry szachy tradycyjne Praca magisterska na kierunku INFORMATYKA

Praca wykonana pod kierunkiem dra hab. Damiana Niwińskiego Instytut Informatyki

Czerwiec 2009

Oświadczenie kierującego pracą Potwierdzam, że niniejsza praca została przygotowana pod moim kierunkiem i kwalifikuje się do przedstawienia jej w postępowaniu o nadanie tytułu zawodowego.

Data

Podpis kierującego pracą

Oświadczenie autora (autorów) pracy Świadom odpowiedzialności prawnej oświadczam, że niniejsza praca dyplomowa została napisana przeze mnie samodzielnie i nie zawiera treści uzyskanych w sposób niezgodny z obowiązującymi przepisami. Oświadczam również, że przedstawiona praca nie była wcześniej przedmiotem procedur związanych z uzyskaniem tytułu zawodowego w wyższej uczelni. Oświadczam ponadto, że niniejsza wersja pracy jest identyczna z załączoną wersją elektroniczną.

Data

Podpis autora (autorów) pracy

Streszczenie W pracy przedstawiono grę w szachy z omówieniem jej złożoności, algorytmicznych aspektów, opisano implementację z omówieniem użytych algorytmów i ich porównaniem.

Słowa kluczowe szachy, sztuczna inteligencja, algorytmy szachowe, programowanie niskopoziomowe

Dziedzina pracy (kody wg programu Socrates-Erasmus) 11.3 Informatyka

Klasyfikacja tematyczna E.2 DATA STORAGE REPRESENTATIONS, E.1 DATA STRUCTURES, D.0 GENERAL, J.6 COMPUTER-AIDED ENGINEERING

Tytuł pracy w języku angielskim An implementation of game traditional chess

Spis treści Wprowadzenie . . . . . . . . . . . . . . . . . . 0.1. Główne motywacje . . . . . . . . . . . . 0.2. Dlaczego szachy są bardziej odpowiednie 0.3. Wcześniejsze implementacje . . . . . . . 0.4. Ogólnie o implementacji . . . . . . . . .

. . . . . . . . . . . . od innych . . . . . . . . . . . .

. . . . . . gier . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

5 5 5 6 6

1. Deterministyczne gry logiczne . . . . . . . . . 1.1. Ogólna anatomia gry . . . . . . . . . . . . . . 1.2. Podstawowe narzędzia teorii gier . . . . . . . 1.3. Determinizm . . . . . . . . . . . . . . . . . . 1.4. Strategia w grze . . . . . . . . . . . . . . . . 1.5. Strategia optymalna . . . . . . . . . . . . . . 1.6. Strategia wygrywająca? Twierdzenie Zermelo 1.7. Ogólna klasyfikacja . . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

7 7 7 9 10 10 10 11

2. Szachy w informatyce . . . . . . . . 2.1. Początki . . . . . . . . . . . . . . 2.2. Minimax . . . . . . . . . . . . . . 2.3. Schematy minimaxowe Shannona 2.4. Pierwszy program szachowy . . . 2.5. Zakład Levy’ego . . . . . . . . . 2.6. Dalszy rozwój . . . . . . . . . . . 2.7. Brute force . . . . . . . . . . . . 2.8. Pierwsze pokonanie arcymistrza . 2.9. Upadek Levy’ego i zastój . . . . 2.10. Początek Deep Blue . . . . . . . 2.11. Pokonanie mistrza świata . . . . 2.12. Nowa Era . . . . . . . . . . . . . 2.13. Rybka . . . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

13 13 13 14 14 15 15 15 15 16 16 16 16 16

3. Algorytmy przeszukiwania i funkcje oceny pozycji 3.1. Alpha-beta cięcia . . . . . . . . . . . . . . . . . . . . 3.2. Funkcja oceniająca . . . . . . . . . . . . . . . . . . . 3.3. Funkcja oceny pozycji . . . . . . . . . . . . . . . . . 3.4. Efekt horyzontu . . . . . . . . . . . . . . . . . . . . . 3.5. Usprawnienia . . . . . . . . . . . . . . . . . . . . . . 3.6. Alfabeta z aspiration search . . . . . . . . . . . . . . 3.7. Tablice transpozycji . . . . . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

17 17 18 19 20 20 20 20

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

3

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

. . . . . . . . . . . . . .

3.8. Selektywne pogłębianie . . . . . . . 3.9. Negascout, MTD-f, SSS . . . . . . 3.10. Negascout ze schematem Shannona 3.11. Algorytm probabilistyczny . . . . . 3.12. Wariant losowy z filtrem . . . . . .

. . . . B . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

21 21 22 22 22

4. Opis implementacji . . . . . . . . . . . . . 4.1. Cel i założenia . . . . . . . . . . . . . . 4.2. Podstawowe statyczne struktury danych 4.3. Tablicowanie . . . . . . . . . . . . . . . 4.4. Maski bitowe . . . . . . . . . . . . . . . 4.5. Struktura ruchu . . . . . . . . . . . . . . 4.6. Generowanie ruchów oraz sortowanie . . 4.7. Tablica . . . . . . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

23 23 23 24 27 27 27 27

5. Podsumowanie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

29

A. Kompilacja i obsługa źródeł projektu . . . . . . . . . . . . . . . . . . . . . .

31

B. Definicje i wyjaśnienie pojęć . . . . . . . . . . . . . . . . . . . . . . . . . . . .

33

Bibliografia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

39

4

Wprowadzenie 0.1. Główne motywacje Szachami [1] zainteresowałem się wiele lat przed rozpoczęciem studiów i pierwszą stycznością z programowaniem. Korzystałem z bardzo wielu różnych programów szachowych na różnych maszynach. Pierwszym programem, którego używałem było chess [?]. na archaicznym komputerze Pentium 7 MHz. Fascynowało mnie to, że maszyna licząca jest w stanie bez trudu pokonać mnie oraz była bardzo dobrym nauczycielem samej gry - wypracowywała dokładność. Zawsze chciałem być autorem swojej własnej implementacji, rozumiejąc tylko intuicyjnie wtedy na czym to rzeczywiście może polegać.

0.2. Dlaczego szachy są bardziej odpowiednie od innych gier Uważam, że program szachowy to bardzo ciekawy problem teoretyczno-implementacyjny ze względu na swój odpowiedni stopień złożoności. Mam tu na myśli względną równowagę pomiędzy możliwościami ludzkiego umysłu a siłą gry amatorskich programów na współczesnych maszynach. Szachy nie są problemem czysto obliczeniowym, wtedy nawet proste programy szachowe deklasowałyby ludzi ze względu na miażdżącą przewagę w prędkości dokonywanych obliczeń. Coś podobnego miało miejsce z grą Othello [2] (również Reversi), gdzie niezbyt złożony program Logistello Michaela Buro bez trudu pokonał w 1997 r. aktualnego mistrza świata Takeshi Murakami 6 - 0. Dzisiejsze programy są daleko poza zasięgiem ludzi, ponieważ niewielka złożoność tej gry pozwala na głębokie i bezbłędne obliczenia. Stopień rozgałęzienia drzewa w przypadku tej gry waha się od około 5 do 12. Z drugiej strony szachy nie są grą bazującą na intuicji na tyle, żeby algorytm obliczeniowy nie miał szans dorównać ludzkiej sieci neuronowej. Tak jest w przypadku starochińskiej gry Go [3], gdzie złożoność jest zbyt duża, aby program był w stanie wystarczająco zagłębić się w drzewo gry. Jest 19 linii poziomych oraz pionowych, czyli możliwości postawienia kamienia jest: 19 ∗ 19 = 361 czyli (pomijając nawet możliwości bicia) możliwych partii jest: 361! ≈ 1.43792325e + 768 ≈ 10750 w przypadku szachów mamy średnio 35 możliwych posunięć do wyboru. Zakładając, że partia ma około 80 ruchów ze średnią 35 na ruch mamy: (oczywiście czysto teoretycznie partie mogą być dłuższe, ale wtedy jest dużo mniejsze rozgałęzienie, wiec szacowanie dobrze obrazuje ilość): 3580 ≈ 3.353077e + 123 5

log(3580 ) ≈ 120 3580 ≈ 10120 czyli możliwych partii szachów jest około 10120 , zatem partii Go jest 10750 /10120 = 10630 razy więcej. Najlepsze programy Go grają na poziomie średniego amatora. Muszą one polegać na zupełnie innych algorytmach niż szachowe, zdecydowanie bardziej heurystycznych, bazujących w dużo większej mierze na ocenie ”jakości kształtów” układanych pionów, niż na analizie drzewa gry. Szachy są zatem bardzo interesującą grą do implementacji i weryfikacji siły gry.

0.3. Wcześniejsze implementacje Oczywiście nie jestem w stanie sam napisać programu grającego na poziomie współczesnych najsilniejszych programów. Są one pisane przez sztab ludzi z pomocą ekspertów i jest to poza moim zasięgiem. Taki program chciałem napisać od początku studiów. Do tej pory wykonałem jedną implementację w języku funkcyjnym Haskell [4], jednak przy zachowaniu czystego paradygmatu programowania funkcyjnego (czyli pisząc komponenty idealnie funkcyjnie) samo generowanie ruchów było zbyt wolne, aby były szanse na istotną siłę programu. Generator ruchów zwracał listę pozycji, a sama pozycja nie była reprezentowana statycznie, co wpływało na wielokrotne spowolnienie kodu, czyniąc go przy tym jednak bardziej funkcyjnym, czytelnym i krótszym.

0.4. Ogólnie o implementacji Teraz mam zamiar wykorzystać szybkość i możliwość optymalizacji kodu w języku C++, używając dedykowanego kompilatora pod system Windows XP. Planuję zastosować kilka pomysłów na modyfikację funkcji oceniającej oraz wypróbować algorytm heurystyczny, polegający z grubsza na zbieraniu statystyk z losowo generowanych partii za pomocą szybkiej heurystycznej funkcji oceniającej. Mam zamiar porównać skuteczność tej metody, skonfrontować ją z powszechnie znanymi debiutami oraz podstawowymi metodami programowania szachów. Dopisać.

6

Rozdział 1

Deterministyczne gry logiczne 1.1. Ogólna anatomia gry Podstawowe cechy deterministycznych gier logicznych są bardzo intuicyjne. Rzutując gry tej kategorii na podstawową teorię gier możemy rozróżnić podstawowe elementy i schemat postępowania: 1. rozpoczęcie gry

2. kolejne posunięcia będące uporządkowanym ciągiem ruchów

3. osiągniecie stanu końcowego

4. wypłaty graczy

5. zakończenie gry

W ogólniejszej wersji jest miejsce na posunięcia losowe lub czynniki losowe wpływające na podejmowanie decyzji o konkretnych ruchach przez graczy, np. tasowanie kart, rzut kostką - wszystko to traktowane jest jako ruchy losowe. Jednak tutaj skupię się na grach z pełną informacją.

1.2. Podstawowe narzędzia teorii gier Definicja 1.2.1 Drzewem topologicznym lub dendrytem gry nazwiemy zbiór węzłów (wierzchołków), połączonych liniami (łukami) tak, że otrzymana figura jest spójna i nie zawiera łamanych zamkniętych. Czyli w drzewie topologicznym, jeżeli da się dojść od wierzchołka X do Y, to da się to zrobić w jeden sposób (bez wracania), połączone są dokładnie jednym konkretnym ciągiem łuków. 7

Definicja 1.2.2 Niech (T, X) będzie ukorzenionym drzewem topologicznym, X jego korzeniem. Mówimy, że wierzchołek Z następuje po wierzchołku Y wzgelędem X, jeżeli ciąg łuków z X do Z przechodzi przez Y. Mówimy, że Z następuje bezpośrednio po Y, gdy Z następuje po Y i Y jest połączone łukiem z Z. Wierzchołek F dendrytu T jest końcowy, gdy zbiór wierzchołków następujących po F jest pusty. Definicja 1.2.3 Gra n-osobowa postaci ekstensywnej jest to: 1. dendryt T z dokładnie jednym wyróżnionym wierzchołkiem A - jest to punkt początkowy 2. funkcja wypłaty, przyporządkowująca każdemu wierzchołkowi końcowemu wektor n wymiarowy zawierający informację o wypłatach graczy 3. rozbicie zbioru niekońcowych wierzchołków dendrytu na n + 1 zbiorów S0 , S1 , . . . Sn zbiory pozycji graczy 4. rodzina rozkładów prawdopodobieństwa, dla każdego wierzchołka z S0 , na zbiorze wierzchołków następujących bezpośrednio po nim. 5. Podrozbicie każdego ze zbiorów S0 , . . . , Sn , na zbiory informacyjne Sij i każde dwa wierzchołki z tego samego zbioru informacyjnego mają samą ilość wierzchołków następujących bezpośrednio po nich, żaden wierzchołek z tego zbioru informacyjnego nie następuje po żadnym innym ze zbioru. 6. Określony dla każdego zbioru informacyjnego Sij zbiór wskaźników Iij (posunięć) wraz z przyporządkowanym każdemu wierzchołkowi W ∈ Sij wzajemnie jednoznacznym odwzorowaniem zbioru Iij na zbiór wierzchołków następujących bezpośrednio po W.

1. Wierzchołek A z pierwszego punktu definicji jest punktem początkowym gry (w przypadku szachów jest to ustawienie pierwotne przed pierwszym ruchem gracza dysponującego białymi). 2. funkcja wypłaty określa dla każdej sytuacji końcowej wypłaty dla graczy, w przypadku szachów jest to umownie • 1:0 w przypadku wygrania gracza białego • 0.5:0.5 gdy nastąpił remis • 0:1 w przypadku gdy wygra gracz czarny ale dla ułatwienia i nie zmieniając sytuacji można umownie odjąć stronami 0.5 i mamy: 8

• 0.5:-0.5 w przypadku wygrania gracza białego • 0:0 gdy nastąpił remis • -0.5:0.5 w przypadku gdy wygra gracz czarny W szachach n = 2, czyli wektory będą 2 wymiarowe. Zatem zbiór wartości funkcji wypłaty na zbiorze wierzchołków końcowych dla szachów wygląda tak: {[0.5, −0.5], [0, 0], [−0.5, 0.5]} symetria oznacza dokładnie identyczny zysk gracza jednego i stratę gracza drugiego. Potem nawiążę do tej właściwości. 3. niekońcowe wierzchołki są podzielone na zbiory S0 , S1 , . . . Sn , przy czym w przypadku szachów n=2 oraz zbiór posunięć losowych, niezależnych od graczy S0 = ∅ Podział jest zatem prosty i przejrzysty: S0 = ∅ S1 ∩ S2 = ∅ S1 ∪ S2 = W (T ) gdzie W (T ) oznacza zbiór wszystkich niekońcowych wierzchołków dendrytu T , gdzie S1 - zbiór posunięć gracza białego, S2 czarnego. 4. w przypadku omawianej gry S0 jest puste, więc nie określa się

Podobnie zbiory informacyjne i wskaźników trywializują się, używa się ich w przypadku gier z niepełną informacją.

1.3. Determinizm Definicja 1.3.1 Dany gracz w określonej grze posiada pełną informację, jeśli wszystkie jego zbiory informacyjne Sij są jednoelementowe. Jeśli jeden ze zbiorów informacyjnych dowolnego gracza nie jest singletonem, mówimy że gra posiada niepełną informację (jest grą z niepełną informacją). Grę określa się mianem gry z pełną informacją gdy każdy gracz posiada pełną informację. 9

Nawet czysto intuicyjnie podchodząc oczywistym wydaje się być, iż szachy są grą z pełną informacją. Nie ma absolutnie żadnych czynników losowych, nawet czas przeciwnika jest do wglądu. Oczywiście z praktycznego punktu widzenia szachisty, można mówić o podejmowaniu ryzyka grając agresywnie, na przykład poświęcając figurę - czynnik losowy polega tu na tym, że człowiek bądź maszyna grająca nie jest w stanie do końca przewidzieć konsekwencji tego ruchu, który mocno zaburza równowagę pozycyjną oraz materialną. Taka sytuacja na ogół powoduje zaostrzenie gry i wyklucza spokojny remis, jednak z punktu widzenia teorii jest to ciągle zupełnie jasna sytuacja, wejście w inna gałąź drzewa, które jest w całości znane, jednak zbyt duże, żeby dostatecznie przeanalizować i przewidzieć skutki danego ”ryzykownego” ruchu.

1.4. Strategia w grze Intuicyjnie poprzez strategię rozumie się ogólny pomysł, plan gracza na rozgrywkę. Definicja 1.4.1 Strategia to opis zachowania gracza w każdej możliwej sytuacji, dokładnie ruch w każdej możliwej sytuacji w rozgrywce. Bardziej formalnie niech G oznacza graf skierowany gry 2 graczy (X oraz Y) z pełną informacją, gdzie wierzchołki to pozycje, a krawędzie reprezentują ruchy. Wtedy strategia dla gracza X to dowolne drzewo ścieżek w G, w którym każda ścieżka zakończona w nie-terminalnej pozycji gracza X ma przedłużenie o 1, a ścieżka zakończona w pozycji gracza Y ma przedłużenia o wszystkie możliwe ruchy. Czyli gracz X ma plan biorąc pod uwagę wszystkie możliwe odpowiedzi przeciwnika. Oczywiście w omawianej klasie gier skończonych zbiór strategii obu graczy jest również skończony, co wynika z tego, że jest skończona ilość możliwych pozycji, a w każdej skończona ilość ruchów do wyboru. Nawet dla najprostszych gier mamy bardzo duża ilość możliwych strategii. Na przykład dla kółka i krzyżyka 3 ? 3 mamy: 9 ∗ 78 ∗ 56 ∗ 34 ∗ 12 = 65664686390625 ≈ 6 ∗ 1013 strategii.

1.5. Strategia optymalna Twierdzenie 1.5.1 Skończona jest ilość strategii dla graczy X i Y, czyli skończony jest też iloczyn kartezjański zbiorów ich strategii, a to implikuje istnienie strategii optymalnej ∗ gracza X (symetrycznie Y), którą oznaczamy SX Stosując strategię optymalną gracz minimalizuje swoją przegraną w stosunku do gry przeciwnika lub maksymalizuje swój zysk.

1.6. Strategia wygrywająca? Twierdzenie Zermelo Nie wiadomo, czy w szachach istnieje strategia wygrywająca dla białego lub czarnego. Moim zdaniem najbardziej prawdopodobną odpowiedzią na to pytanie jest remis. Nie jestem w stanie jednak tego udowodnić, za to przytoczę twierdzenie niemieckiego matematyka Ernsta Zermelo (1871 - 1953), który w roku 1912 udowodnił poniższe twierdzenie. Twierdzenie 1.6.1 Twierdzenie szachowe Zermelo W grze w szachy są trzy możliwości: 10

1. istnieje strategia wygrywająca dla białych 2. obaj gracze mają strategie na remis 3. istnieje strategia wygrywająca dla czarnych Dowód korzysta z logicznego prawa wyłączonego środka: Twierdzenie 1.6.2 Dla dowolnego zdania logicznego p w logice dwuwartościowej prawdziwe jest zdanie: p∨∼p Niech zdanie w oznacza, iż białe przynajmniej remisują: w ≡ ∃w1 ∀b1 ∃w2 ∀b2 . . . ∃wn ∀bn (n oznacza ilość wszystkich ruchów w partii), że białe nie przegrywają (biorąc pod uwagę ruchy generujące legalne rozgrywki), gdzie w1 . . . wn oznaczają półruchy białego prowadzące do legalnej pozycji, natomiast b1 . . . bn półruchy czarnego, n ilość pełnych ruchów w partii. Zdanie w mówi, że biały od początku może wybierać takie ruchy, które dają mu co najmniej remis. Z prawa wyłączonego środka to zdanie jest prawdziwe lub fałszywe (w ∨ ∼ w). Zaprzeczeniem będzie : ∼ w ≡ ∀w1 ∃b1 ∀w2 ∃b2 . . . ∀wn ∃bn , że wygrywają czarne, co jest wyrażoną obrazowo strategią czarnych prowadzących do ich zwycięstwa.

1.7. Ogólna klasyfikacja Szachy, podobnie jak inne przytoczone wcześniej gry, należą do gatunku gier dwuosobowych, deterministycznych, skończonych, z pełną informacją, o sumie zerowej, nazwijmy je dla uproszczenia GD. Co to oznacza? Gra: • dwuosobowa - jest dokładnie 2 przeciwników • deterministyczna - nie ma elementów losowości • skończona - zgodnie z zasadami szachów [1] w przypadku wystąpienia 3 razy takiej samej pozycji następuje remis. Są 32 figury, zatem dowolnych ustawień ich na 64 polowej szachownicy jest: 2 32 32 V64 +...+V64 < 31∗V64 = 31∗325384564096568811635262532334610443843665920000000

= 10086921486993633160693138502372923759153643520000000 = R czyli po 2 ∗ R + 1 ruchów któraś pozycja musi się powtórzyć 3 raz.

11

• z pełną informacją - obie strony wiedzą wszystko o stanie rozgrywki • o sumie zerowej - zwycięstwo jednego gracza oznacza porażkę drugiego, nie ma stanów pośrednich. Inne gry należące do tej rodziny to np. Warcaby, Reversi, Gomoku, Go, ConnectFour i wiele innych. Za pomocą analizy komputerowej udało się bardzo mocno poszerzyć wiedzę z zakresu gier z GD. Grę można rozwiązać w sposób: • słaby (rozwiązanie słabe) - strategia wygrywająca podawana jest od stanu początkowego • silny (rozwiązanie silne) - w każdej pozycji znane jest najlepsze posunięcie i możliwości gry od tego momentu - czy jest przegrana, strategia remisowa lub wygrywająca wygrana oraz najlepsza kontynuacja Banalnym przykładem gry, którą każdy może rozwiązać silnie na poczekaniu jest proste kółko i krzyżyk 3 x 3. W każdej pozycji można wskazać najlepszą kontynuację oraz orzec o wygranej jednej ze stron bądź remisie. Właściwie o tej grze wiadomo absolutnie wszystko. Inną grą rozwiązana silnie przez Holendrów jest Awari, gdzie liczba stanów to około 1012 . Grami rozwiązanymi słabo są np. Gomoku - nowatorskie pomysły Victora Allisa wykazały, iż gracz rozpoczynający ma strategię wygrywającą. Bardzo ciekawym osiągnięciem zespołu pracującego nad programem Chinook [5], pogromcy warcabowego mistrza wszechczasów Mariona Tinsley [6], jest udowodnienie, że gracz rozpoczynający ma strategie remisującą. W praktyce oznacza to, iż współczesna implementacja Chinook-a zawsze wygrywa bądź remisuje. ConnectFour [7] zostało słabo rozwiązane w 1988 r. równolegle przez Jamesa D. Allen oraz wcześniej wspomnianego Victora Allis. Przy idealnej grze gracz zaczynający stawiając pierwszy kamień w środkowej kolumnie ma strategię wygrywającą, zaczynając w jednej z 2 przyśrodkowych kolumn daje graczowi drugiemu strategie remisującą, natomiast rozpoczynając poza nimi strategię wygrywającą. Szachy nie są i prawdopodobnie nie będą w najbliższej przyszłości rozwiązane w żaden ze sposobów (oczywiście rozwiązanie silne zawiera w sobie słabe). Sztuczna inteligencja w grach z GD generalnie opiera się o: • przeszukiwanie i analizy drzewa gry, algorytmy z rodziny MINIMAX. • baza końcówek (generowana poprzez analizę) • baza debiutów (generowana poprzez analizę lub korzystające z wiedzy ekspertów) • algorytmy heurystyczne i symulacje np. w przypadku Go

12

Rozdział 2

Szachy w informatyce 2.1. Początki Jak już wcześniej pisałem, szachy są bardzo wdzięcznym i ciekawym problemem programistycznym. Pierwszym człowiekiem, który w jakikolwiek sposób połączył starą, królewską grę z informatyką, był Leonardo Torres y Quevedo[8]. W 1890 roku stworzy mechanizm elektroniczny pokaźnych gabarytów, który rozgrywał końcówkę matującą król + wieża kontra król. Pierwszą poważną teoretyczną przymiarką do programowania szachów była praca Clauda Elwooda Shannona [9] pod tytułem ”Programming the computer for playing chess”. Postawowe metody w niej przedstawione są aktualne po dziś dzień.

2.2. Minimax Ogólny schemat dobrze znanego algorytmu Minimaks w najprostszej wersji w pseudokodzie wygląda następująco:

int minimax(p : pozycja, l : lista int) if jestKońcowa(p) return wynikGry(p) else l = pustaLista dla każdego nastepnika p pn dodajDoListy(l, minimax(pn, l)) if posuniecieGracza1(p) return maxymalnyElementListy(l) else return minimalnyElementListy(l)

jest on metodą minimalizowania maksymalnych możliwych strat lub maksymalizację minimalnego zysku. W szachach pełne drzewo gry ma około 1044 , więc przejrzenie całego drzewa gry jest fizycznie niemożliwe, należało więc przejrzeć tylko część drzewa. 13

2.3. Schematy minimaxowe Shannona int funkcjaShannonaA (p : pozycja, l : lista int, g : glebokosc) if jestKońcowa(p) lub g == 0 return ocenaPozycji(p) else l = pustaLista dla każdego nastepnika p pn dodajDoListy(l, minimax(pn, l, g - 1)) if posuniecieGracza1(p) return maxymalnyElementListy(l) else return minimalnyElementListy(l) Funkcja ShannonaA wykonuje pełnego minimaxa na okrojonym drzewie do danej głębokości g i zamiast wyniku gry zwraca heurystyczną ocenę pozycji na danej głębokości, gdy gra się jeszcze nie skończyła. Był to bardzo dobry pomysł, który przetrwał, oczywiście w tej postaci niewystarczająco wydajny. Przeglądanie całego drzewa do danej głębokości pozwalało na osiągnięcie poziomu początkującego, jednak sensownie grającego zawodnika. int funkcjaShannonaB (p : pozycja, l : lista int, g : glebokosc) if jestKońcowa(p) lub g == 0 return ocenaPozycji(p) else l = pustaLista dla każdego nastepnika p pn, ktory jest "wystarczający" dodajDoListy(l, minimax(pn, l, g - 1)) if posuniecieGracza1(p) return maxymalnyElementListy(l) else return minimalnyElementListy(l) ”Wystarczający” oznacza tutaj, że bierzemy pod uwagę tylko następniki, które są uznana za sensowne. To imituje sposób myślenia zawodnika ludzkiego, jednak trudno jest go zasymulować sprzętowo. Sposób pozwala na szybszą i głębszą analizę rokujących posunięć, dopuszcza jednak ryzyko popełnienia błędu. Schemat B ma oczywiście zastosowania we współczesnych algorytmach.

2.4. Pierwszy program szachowy Alex Bernstein był autorem pierwszego kompletnego programu szachowego, ukończonego w 1957 roku. Wcześniej spotykane były programy rozwiązujące końcówki i inne podproblemy szachowe. Pierwszy oficjalny turniej programów szachowych odbył się w 1966 między programami z uniwersytetów MIT oraz Moskiewskiego zakończony zwycięstwem Rosjan 3:1, zwycięski 14

program nazywał się Kaissa. Był on inspiracją ludzi z MIT do kontynuacji i w 1967 ukazał się ”Mat Hack Six”, który był pierwszym ”sensownym” programem, będącym w stanie wygrywać z szachistami amatorami.

2.5. Zakład Levy’ego David Levy - amerykański szachowy mistrz międzynarodowy założył się, że w ciągu najbliższej dekady nie przegra meczu z programem szachowym. Wygrał zakład 3.5 - 1.5 pokonując program Chess 4.7 w 1978 roku, jednak zakład bardzo mobilizująco przyczynił się do postępu w dziedzinie programowania królewskiej gry.

2.6. Dalszy rozwój Pierwsze oficjalne mistrzostwa świata programów szachowych miały miejsce w Sztokholmie w roku 1974 zakończone zwycięstwem rozwijanej wciąż Kaissy, a najmocniejszym przeciwnikiem był amerykański Chess. Kaissę wycenianio wtedy na około 1700 punktów ELO. W 1977 Chess osiągnął 2000 punktów ELO i zdetronizował Kaissę. Miał innowacyjne rozwiązania, które potem wywarły wpływ na późniejsze dokonania, np. tablica transpozycyjna ruchów oraz podział drzewa gry na regiony.

2.7. Brute force W 1980 najlepszym światowym programem został Belle programistów Kena Thompsona i Joe Condona. Siła gry tego programu polegała na specjalnych szachowych sprzętowych układach elektronicznych, pozwalającej zejść na 5 pełnych ruchów przy analizie 15 000 pozycji na sekundę, co wprowadziło pojęcie brute force (brutalna siła obliczeniowa). Przy takiej szybkości nawet prosty algorytm i funkcja oceniająca pozwalały zdeklasować konkurencję. Następcą Chess i mistrzem świata w 1983 oraz 1986 został Cray Blitz na najszybszej maszynie Cray X-MP. W dalszym ciągu program był za słaby na pokonanie mistrza Levy-ego, przegrał z nim 4 - 0 w 1984 roku. W dalszych latach zaczęto projektować szybkie mikrokomputery szachowe stawiające na technikę brute force. Ich siła w 1987 sięgała około 2200 punktów ELO. Stosowano głównie Schemat A Shannona na szybkich układach elektronicznych. Schemat B Shannona był rozwijany między innymi przez Szachowego Mistrza Świata M. Botvinnika, jednak bez zadowalających rezultatów. Pod koniec lat osiemdziesiątych programy znacznie przerosły ludzi w rozwiązywaniu końcówek szachowych - co łatwo wytłumaczyć dużo mniejszym stopniem rozgałęzienie drzewa.

2.8. Pierwsze pokonanie arcymistrza W 1988 Deep Thought pokonał arcymistrza Bent-a Larsen-a, a w 1989 został mistrzem świata programów z siłą około 2500 punktów ELO, a wśród mikrokomputerów Mephisto. W tym samym roku program Bebe został pierwszym samouczącym się programem szachowym. 15

2.9. Upadek Levy’ego i zastój Rok 1989 okazał się przełomowy również w dziedzinie szachów. Deep Thought pokonał mistrza Levy’ego 4 : 0, co było symbolicznym przełomem. Deep Tought był w pierwszej 150 tce światowej na poziomie arcymistrza, co wbrew pozorom było daleką drogą do pokonania ludzkiego szachowego mistrza świata, wielkiego Garry-ego Kasparova. Od 1989 do 1993 nie udało się poprawić siły gdy Deep Thought, nawet w 1993 przegrał on kontrmecz z arcymistrzem Bentem Larsenem.

2.10. Początek Deep Blue Deep Blue był komputerem szachowym rozwijanym przez firmę IBM. W 1996 podjął pojedynek z mistrzem świata i mimo przegrania 4:2, stal się pierwszym programem, który wygrał partię z mistrzem świata. To nasunęło sugestię, że przewyższenie mistrza jest możliwe. Deep Blue bazował na połączeniu dotychczasowych osiągnięć informatyki w dziedzinie szachów, z brute force na czele. Miał 32-węzłowy klaster IBM RS/6000 SP, po 8 wyspecjalizowanych procesorów szachowych na każdym węźle, co daje 256 procesorów. Program był napisany oczywiście w C i był w stanie wyprowadzać 200 000 000 pozycji na sekundę. Funkcja oceniająca używała zoptymalizowanych parametrów na podstawie statystyk z partii mistrzowskich oraz maszyna dysponowała duża biblioteką otwarć.

2.11. Pokonanie mistrza świata W 1997 Deep Blue pokonał Kasparova 3.5 : 2.5, było to wydarzenie przełomowe, choć nie pozbawione kontrowersji. Program był modyfikowany po każdej partii istnieją też podejrzenia o ingerencji ludzi w grę maszyny. Kasparov zażądał rewanżu, jednak IMB team odmówił i porzucił rozwój Deep Blue.

2.12. Nowa Era W dzisiejszej dekadzie nawet programy na zwykłe PC osiągają poziom mistrza świata. Mistrzowie świata rozgrywali pojedynki z PC-tami kilkakrotnie z różnym powodzeniem, w 2006 mistrz świata Vladimir Karmnik przegrał z programem Deep Fritz. Pojedynki ludzi z maszynami przestały powoli budzić takie emocje, z wiadomych przyczyn.

2.13. Rybka W tej chwili niepodważalnie najmocniejszym programem szachowym jest Rybka autorstwa mistrza międzynarodowego Vasika Rajlicha. Od 2006 wygrywa wszystkie mistrzostwa świata programów szachowych. Pod koniec 2008 roku Rybka 3 osiągnęła 3238 punkty ELO i z ludźmi grała tylko na ”fory” (dając przewagę piona lub figury) z całkiem niezłym powodzeniem.

16

Rozdział 3

Algorytmy przeszukiwania i funkcje oceny pozycji 3.1. Alpha-beta cięcia Zarówno współczesne algorytmy szachowe jak i te dawniejsze opierają się na algorytmach minimaxopodobnych. Jednak przeglądanie całego drzewa gry do pewnej zadanej głębokości jest bardzo nieefektywne, gdyż skoro mamy średnio około 35 możliwości wykonania ruchu, to aby zanalizować grę na 2 półruchy do przody potrzeba 352 = 1225 ocen pozycji, natomiast odpowiednio dla 4, 5, 6, 7, 8: 354 = 1500625 355 = 52521875 356 = 1838265625 357 = 64339296875 358 = 2251875390625 Czyli dla zejścia na 4 ruchy do przodu (co daje niedużą siłę gry) potrzeba by było 2251875390625 > 2 ∗ 1012 ocen pozycji. Przy idealnym procesorze o taktowaniu 3 GHz/sec, który ocenia jedną pozycję w 30 taktach zegara wychodzi: 30 ∗ 2 ∗ 1012 = 2 ∗ 104 s ≈ 5, 5h 1 9 3 ∗ 10 s Jest to oczywiście wielokrotnie więcej niż całkowity czas partii. Najbardziej podstawowym sposobem polepszenia tego wyniku jest zastosowanie cięć alphabeta do minimaxa. Polega on na pomijaniu gałęzi w drzewie gry, co do których jest pewność, że zostaną odrzucone bez wyliczania dla nich wartości. Oto prosty schemat: int mimimax (p : pozycja, g : glebokosc) return alphabeta(węzeł, głębokość, $-\infty$, $+\infty$) int alphabeta (p : pozycja, g : glebokosc, a, b) if jestKońcowa(p) lub g == 0 17

return ocenaPozycji(p) if gra przeciwnik dla każdego nastepnika p pn\\ b := min(b, alfabeta(pn, g-1, a, b)) if a przekracza b return a return b else dla każdego nastepnika p pn a := max(a, alfabeta(pn, g-1, a, b)) if a przekracza b return b return a Przy algorytmie minimax bez apha-beta cięć mamy złożoność na głębokości g: O(35 ∗ 35 ∗ 35 ∗ . . . ∗ 35) = O(35g ) tak samo jest dla alpha-beta przy pesymistycznym uporządkowaniu ruchów od najsłabszego do najmocniejszego (alpha-beta nie ”gubi” ruchów, więc musi przejrzeć wtedy wszystkie). Natomiast przy optymalnym porządkowaniu ruchów mamy: g

O(35 ∗ 1 ∗ 35 ∗ 1 ∗ . . .) = O(35 2 ) Zatem bardzo istotne jest porządkowanie ruchów. Można to uzyskać tak zwanym iteracyjnym pogłębianiem, czyli wykonywaniem analizy iteracyjnie dla coraz większych głębokości. Po każdej analizie można sortować ruchy dla zoptymalizowania cięć zgodnie z ich aktualnymi wartościami heurystycznymi. Zauważmy też, że dobrym wyznacznikiem efektywności cięć jest ilość przejrzanych liści analizowanego obciętego drzewa, gdyż jest ich więcej niż pozostałych węzłów. Np. dla głębokości 4 półruchów mamy średnio: 35 + 35 ∗ 35 + 353 = 44135 węzłów niekońcowych, natomiast liści (na których wywoływana ocena pozycji) jest: 354 = 1500465

3.2. Funkcja oceniająca Funkcja oceniająca (funkcja oceny pozycji) jest to funkcja uruchamiana w liściach analizowanego obciętego drzewa gry i zwracająca ocenę pozycji. Nie jest to rozwiązanie specyficzne dla szachów. Najprostsza funkcja oceniająca bierze pod uwagę materiał, czyli sumaryczną wartość punktową figur/bierek na planszy. W przypadku szachów akurat jest to rzeczywiście najważniejszy element, ale w niektórych grach np. Reversi mało ważny. W szachach zwyczajowe wartościowanie figur wygląda następująco: • Hetman 900 pkt • Wieża 500 pkt - niektóre źródła podają wartość 450

18

• Goniec 300 pkt - niektóre źródła podają wartość 350

• Skoczek 300 pkt

• Pion 100 pkt

• Król bez punktów - ponieważ nie może zostać zbity, a nieodwracalna groźba jego zbicia oznacza przegranie gry

Oczywiście są to tylko ogólne szacowania, w konkretnej sytuacji w grze pion może być bardziej wartościowy od hetmana, gdyż na przykład umożliwia zwycięstwo w następnym ruchu. Podobnie wartość skoczka zależy od ilości figur na planszy, ponieważ jego ruchy są niezależne od pokonywanej drogi, więc im bardziej zastawiona plansza, tym więcej warty jest skoczek. Z gońcem jest sytuacja odwrotna.

3.3. Funkcja oceny pozycji Oprócz wartości materialnej figur trzeba brać pod uwagę pozycję. Jest to temat niewyczerpany, gdyż bardzo trudno jest obliczeniowo stwierdzić ”jakość” danej pozycji. Trzeba brać pod uwagę takie elementy jak: • bezpieczeństwo króla (jego pozycję i ochronę względem figur nieprzyjaciela)

• spójność łańcuchów pionów (czy piony mają silne broniące się łańcuchy)

• zajęcie kluczowych części szachownicy, czyli centrum, odległość figur atakujących od króla przeciwnika

• mobilność figur (czy figury nie blokują się wzajemnie)

• odpowiednia kolejność wyprowadzania - w początkowej fazie gry opłaca się wyprowadzać figury lekkie (skoczek, goniec) oraz piony, żeby nie narażać ciężkich (wieże, hetman) na konfrontację z mniej wartościowymi figurami przeciwnika

• agresywność króla - w początkowej fazie króla należy chronić, natomiast pod koniec gry, gdy prawie wszystkie figury się wymienią, staje się on bardzo silną figurą, która powinna być aktywna.

• odległość piona od pola przemiany

19

Są to elementy istotne, jednak w szczególnych sytuacjach zbyt ogólne, wyrafinowane funkcje oceniające biorą pod uwagę dużo więcej czynników. Dodatkowymi aspektami branymi pod uwagę przez funkcję oceniająca mogą być dane z analizy wielu partii arcymistrzów - np. w końcówkach położenie odpowiednich figur, newralgicznie linie i konfiguracja ustawień pionów względem innych figur.

3.4. Efekt horyzontu Jest to sytuacja w której nie można polegać na aktualnej ”lokalnej” ocenie pozycji, gdyż przedstawia etap przejściowy w wymianie posunięć. Np. jeśli analizujemy do n-tego ruchu i w n-tym ruchu bijemy hetmanem hetmana przeciwnika, co w następnym ruchu skutkuje nieuniknionym odbiciu analiza zwróci bardzo wysoką ocenę tej pozycji, która po chwili się ustabilizuje. Metodą na to jest pogłębienie przeszukiwania, jeśli w danym momencie nagle pojawia się nieoczekiwana przewaga materialna jednej ze stron.

3.5. Usprawnienia Znakomita większość algorytmów przeszukujących jest oparta na analizie obciętego drzewa gry z cięciami mało rokujących gałęzi. W dzisiejszej technologii programów szachowych używa się kilku standardowych usprawnień i zmodyfikowanych algorytmów przeszukiwania poprawiających szybkość i głębokość przeszukiwania. W dzisiejszych czasach oprócz szybkich równoległych procesorów można też korzystać z wykładniczo większych rozmiarów pamięci niż u początków programowania szachów.

3.6. Alfabeta z aspiration search Jest to usprawnienie alfabety polegające na zawężeniu okna przeszukiwania. Podczas gdy w standardowej wersji mamy: a = −∞ b = +∞ w wersji aspiration search mamy: a = expectedV alue − window b = expectedV alue + window czyli okno zawężone jest do przedziału +- window od oczekiwanej wartości. Powoduje to bardziej skuteczniejsze cięcia i przeszukiwanie mniejszej ilości pozycji dla takiej samej głębokości. Jeśli wynik wyjdzie poza przedział przeszukiwanie należy powtórzyć. Istotne jest odpowiednie dobranie rozmiaru przedziału czyli parametru window.

3.7. Tablice transpozycji Jest to tablica hashująca do wykrywania pozycji w różnych gałęziach drzewa, które są identyczne. Jeśli znajdziemy taką pozycję, która była juz przeszukana nie trzeba ponawiać przeszukiwania. Poza tym można używać wpisów do lepszego sortowania analizowanych ruchów. 20

Problemem jest to, że ruchy identyczne względem tablicy transpozycji nie są identyczne pod względem gry - mają inna historię gry i np. 2 identyczne ruchy mogą skutkować innym wynikiem partii, gdy jeden powoduje remis przez 3-krotne powtórzenie pozycji.

3.8. Selektywne pogłębianie Polega na forsowaniu głębszej analizy (zejście na lokalnie głębszy poziom w dzrzewie gry) w miejscach, gdzie daje to rokowanie na korzystną kontynuację, np. w sytuacji gdy: • odległość piona od pola przemiany jest niewielka i może bardzo korzystnie zmienić pozycję. • analogicznie odległość piona przeciwnika stanowi duże zagrożenie, czego funkcja oceniająca nie jest w stanie stwierdzić. • w ostatnim ruchu następuje szachowanie króla którejś ze stron • jest do wykonania potencjalnie korzystne bicie, np. pionem figury Pogłębienie w pewien sposób obrazuje myślenie człowieka, który każdy wariant analizuje z inną głębokością w zależności od stopnia zagrożenia, potencjalnego zysku lub skomplikowania. Użycie tej metody może być bardzo korzystne, jednak jest dosyć niebezpieczne z powodu wykładniczej eksplozji rozmiaru drzewa przy nadmiernym pogłębianiu.

3.9. Negascout, MTD-f, SSS Jest algorytmem przeszukiwania drzewa, ma pewną przewagę nad alfabetą. Jego przewagą jest fakt, iż nigdy nie przeszukuje węzłów, które mogą być obcięte przez alfabetę. Algorytm polega na dobrym sortowaniu ruchów i zakłada, że pierwszy przeszukiwany węzeł jest najlepszy. Po sprawdzeniu pierwszego hipotetycznie najlepszego węzła algorytm kontynuuje przeszukiwanie pozostałych z bardzo wąskim oknem dla potwierdzenia hipotezy. Jeśli hipoteza okazuje się błędna, przeszukanie odbywa się za pomocą zwykłej alfabety. Przy złym porządkowaniu ruchów Negascout zajmie więcej czasu niż alfabeta, ale nie ze względu na przeszukiwanie większej ilości węzłów, lecz ze względu na ponawianie przeszukiwania. W silnikach szachowych stwierdza się około 10% lepszą wydajność od zwykłej alfabety. Pseudokod: int negascout(p : pozycja, g : glebokosc, a, b) if p jest końcowa OR g == 0 return ocenaPozycji(p) b1 := a dla każdego nastepnika p pn a1 := -negascout (pn, g - 1, -b1, -a) if a1 > a a := a1 if a >= b 21

return a if a >= b1 a := -negascout(pn, g - 1, -b, -a) if a >= b return a b1 := a + 1 return a