1 Nazwa implementacji: Tworzenie labiryntu z - e-Swoi
Transkrypt
1 Nazwa implementacji: Tworzenie labiryntu z - e-Swoi
Nazwa implementacji: Tworzenie labiryntu z wykorzystaniem algorytmu Backtracking Autor: Jarosław Żok Rafał Brzychcy grzegorzwilczek Opis implementacji: Zastosowanie języka Python oraz biblioteki Pygame do wizualizacji działania algorytmu Backtracking. Algorytm wykorzystuje jedną ze struktur danych używaną powszechnie w programowaniu - stos. Idea stosu sprowadza się do realizacji obszaru pamięci w taki sposób, że dane odkładane na stos, są zdejmowane w odwrotnej kolejności. Ostatni element odłożony na stos jest z niego zdejmowany pierwszy. Taki sposób dostępu jest powszechnie wykorzystywany w realizacji funkcji w programach, gdzie przed wywołaniem funkcji, wszystkie zmienne lokalne odkładane są na stosie a po wyjściu z niej, zdejmowane ze stosu co przywraca stan programu sprzed wywołania funkcji. Umożliwia to realizację algorytmów rekurencyjnych ale nie tylko, maszynowa interpretacja zapisu działań matematycznych za pomocą RPN (Reverse Polish Notation) jest algorytmem intensywnie korzystającym ze stosu. W tej implementacji zaś wykorzystamy stos do zapamiętywania stanu odwiedzonych komórek w algorytmie generowania labirytnu. Do wizualizacji postępów w działaniu algorytmu będziemy wykorzystywać język Python wraz z biblioteką PyGame. Stos w Pythonie nie wymaga pisania własnej jego implementacji, gdyż listy w tym języku mogą działać jak stos. Zacznijmy od opisu samego algorytmu. Labirynt jest tworzony na planszy o wymiarach N na M komnat. Każda komnata jest oddzielona od sąsiadów ścianą. Zadaniem algorytmu jest przechodzenie losowo między sąsiadującymi komnatami i burzenie ścian między nimi. Tworząc przejścia algorytm tworzy jednocześnie korytarze labiryntu. Na początku wszystkie komanty są oznaczone jako nieodwiedzone a początek labiryntu znajduje się w dowolnej wylosowanej komnacie. Algorytm Backtrack punkt po punkcie działa następująco: Wylosuj komnatę początkową, zaznacz ją jako odwiedzoną i aktualną Dopóki są nieodwiedzone komnaty • Jeżeli aktualna komnata ma jakichś nieodwiedzonych sąsiadów 1. Wybierz losowo jednego z nieodwiedzonych sąsiadów aktualnej komnaty. 2. Odłóż aktualną komnatę na stos. 3. Usuń ścianę między aktualną komnatą a wybranym losowo sąsiadem 4. Niech wybrany losowo sąsiad staje się aktualną komnatą, oznacz go także jako odwiedzonego. • Jeżeli nie ma sąsiadów, sprawdź czy stos nie jest pusty 1. Zdejmij ze stosu komnatę. 2. Zdjętą ze stosu komantę uczyń aktualną. • W przeciwnym wypadku 1. Spośród nieodwiedzonych jeszcze komnat wybierz losowo jedną, uczyń ją aktualną i zaznacz jako odwiedzoną. Do realizacji algorytmu w Pythonie użyjemy stworzonej przez nas klasy Maze, która przy tworzeniu wymaga dwóch parametrów numerycznych określających ilość komnat w wierszu oraz ilość wierszy labiryntu. Klasa Maze zajmuje się także inicjalizacją środowiska graficznego Pygame, otwarciem okna na którym będzie się rysował labirynt oraz inicjalizacją timera, który odliczając okreslony czas będzie wywoływał metodę relizującą kolejne kroki algorytmu. W wierszu 296 gotowego skryptu dołączonego do implementacji, tworzona jest instancja klasy Maze o wymiarach 16 na 16 komnat. Zmienna m przechowuje instancję klasy Maze. if __name__ == "__main__": m = Maze(16, 16) Dalsza część kodu poniżej realizuje pętlę oczekiwania na zdarzenie w Pygame, którym mogą być wciśnięcia klawiszy, naciśnięcie przycisku zamykania okna, zdarzenia związen z ruchem myszy, naciskaniem jej przycisków oraz zdarzenia wywoływane gdy ustawiony timer odliczy określoną ilość czasu. Wykorzystamy tylko dwa z nich, zamknięcie okna oraz odliczenie timera. W przypadku gdy timer odliczy określoną ilość czasu wywołamy metodę tick() na instancji klasy Maze, która wykona kolejny krok algorytmu. Dzięki czemu sterując czasem timera, jesteśmy w stanie wpływać na szybkość działania algorytmu i wizualizacji. 1 Projekt “Strategia Wolnych i Otwartych Implementacji jako innowacyjny model zainteresowania kierunkami informatyczno-technicznymi oraz wspierania uczniów i uczennic w kształtowaniu kompetencji kluczowych” współfinansowany ze środków Unii Europejskiej w ramach Europejskiego Funduszu Społecznego. while True: for event in pygame.event.get(): if event.type == QUIT: del m sys.exit(0) elif event.type == TIMER_TICK: m.tick() Zajmijmy się teraz samą klasą Maze. Jej deklaracja znajduje się w linii 82 przykładowego kodu. Metoda __init__ klasy jest odpowiednikiem konstruktora w innych językach programowania. Jest ona wywoływana automatycznie przez interpreter Pythona w momencie tworzenia instancji klasy. W naszym przypadku przyjmuje dwa parametry "width" i "height". Jak wspomniano wyżej, są to szerokość i wysokość labiryntu wyrażona w komnatach. Zobaczmy co dzieje się metodzie __init__ klasy Maze: random.seed() self.stack = [] self.unvisited = [] self.visited = [] Linia "random.seed()" inicjuje generator licz pseudolosowych. Bez wykonania tego kroku, za każdym uruchomieniem programu otrzymywalibyśmy taki sam rezultat. Wynika to z działania generatorów liczb losowych w językach programowania. Kolejne linie inicjują zmienne klasy, zapis zmienna = [] oznacza przypisanie do zmiennej pustej listy. Na takiej zmiennej możemy następnie wykonać wszystkie metody listy, także te które sprawiają, że lista może zachowywać się jak stos, czyli odkładanie elementu na stos: lista.append(element) i zdejmowanie elementu z listy: lista.pop(). W klasie będziemy przechowywać trzy listy: • stack - stos na który będziemy odkładać, komnaty do których będziemy wracać. • unvisited - lista nieodwiedzonych jeszcze komnat. Badając długość tej listy będziemy wiedzieć, że mamy jeszcze jakieś nieodwiedzone komnaty oraz z tej listy będziemy losować komnaty jeszcze nieodwiedzone. • visited - lista odwiedzonych komnat. W miarę postępów w działaniu algorytmu, lista będzie powiększać się o kolejne odwiedzone komnaty. Kolejne dwie linie sprawdzają czy podane na wejściu argumenty "width" i "height" mieszczą się w przewidzianym zakresie: self.width = width if width < MAX_WIDTH else MAX_WIDTH self.height = height if height < MAX_HEIGHT else MAX_HEIGHT Wyrażenie: zmienna = wartość_1 if <wyrażenie> else wartość_2 jest w Pythonie interpretowane następująco: Jeżeli wartość wyrażenia po słowie "if" jest True zwróć "wartość_1", w przeciwnym wypadku zwróć "wartość_2" i przypisz zwróconą wartość do "zmienna" Same komnaty są zdefiniowane jako pary parametrów X i Y określające położenie komnaty na planszy labiryntu. W Pythonie istnieje reprezentacja danych, która nazywana jest tuplą, jest to zbiór elementów do których możemy dostać się za pomocą indkesu, podobnie jak ma to miejsce w przypadku list, jednak w odróżnieniu od nich, tuple są niezmienne. Nie można usunąć elementu tupli ani dodać nowego do gotowej. tuple definiowane są za pomocą znaku "," (przecinka). Jednocześnie możliwe jest tworzenie nowych tupli za pomocą już istniejących. Daje to ogromne możliwości na przykład w przypadku funkcji zwracacjących więcej niż jedną wartość. W Pythonie można napisać funkcję: def get_xy(value): n = value / 100 m = value % 100 return n,m #Zwracamy tuplę z wartościami x i y x,y = get_xy(150) #Tworzymy nową tuplę x,y z wartości zwróconych przez funkcję get_xy() print x #Wypisze na konsoli wartość 1 (150/100 - dzielone całkowicie) print y #Wypisze na konsoli wartość 50 (150 % 100 - reszta z dzielenia 150 / 100) 2 Projekt “Strategia Wolnych i Otwartych Implementacji jako innowacyjny model zainteresowania kierunkami informatyczno-technicznymi oraz wspierania uczniów i uczennic w kształtowaniu kompetencji kluczowych” współfinansowany ze środków Unii Europejskiej w ramach Europejskiego Funduszu Społecznego. Wracając jednak do samego algorytmu i klasy Maze. Komnaty są opisane jako wartości X i Y i w postaci takich par zapisywane w listach visited, unvisited oraz odkładane na stosie. Wiemy więc dokładnie, które komórki odwiedziliśmy, które jeszcze możemy odwiedzić, jakie są odłożone na stosie i do których możemy wrócić, gdy nie mamy już możliwości pójścia dalej. Klasa Maze posiada także kilka dodatkowych metod, które pomagają w realizacji algorytmu: get_neighbours(self, cell) - Dla podanej w parametrze "cell" komnaty, funkcja losuje jej sąsiadów o ile są jacyś i zwraca jednego z nich albo nie zwraca żadnego. make_visited(self, cell) - Podaną w parametrze "cell" komnatę, metoda oznacza jako odwiedzoną, sprowadza się to do przeniesienia komnaty podanej w parametrze z listy "unvisited" do listy "visited" draw_cell(self, cell, color) - Rysuje na planszy komnatę podaną w parametrze "cell", kolorem określonym w prametrze "color" wreck_wall(self, neighbours, color) - Usuwa ściany między komnatami sąsiadami podanymi jako lista komnat w parametrze "neighbours". Lista sąsiadów komnaty jest tworzona z aktualnie wybranej komnaty oraz wylosowanego, nieodwiedzonego sąsiada. tick(self) - metoda realizująca właściwy algorytm. Algorytm jest realizowany krok po kroku aby umożliwić obserwację jego działania. Metoda ta jest synchronizowana ze zdarzeniami odliczenia czasu przez timer. Z naszego punktu widzenia metoda tick() jest najważniejszą metodą obiektu Maze. Zobaczmy jak jest ona zrealizowana w przykładzie: def tick(self): ''' Metoda realizuje właściwy algorytm. Każde jej wywołanie to jeden krok algorytmu. Jest ona wywoływana za każdym razem kiedy timer odliczy liczbę milisekund zawartą w zmiennej DELAY ''' if self.stack: #losujemy sąsiadów do których nie mamy jeszcze przejścia neighbours = self.get_neighbours(self.current) #Jeżeli są jacyś sąsiedzi if neighbours: cell = neighbours[0] self.stack.append(self.current) #Aktualną odkładamy na stos if (cell[0], cell[1]) == self.start: #Komórkę startową rysujemy na czerwono self.draw_cell(cell, RED) else: self.draw_cell(cell, GREEN) self.wreck_wall((self.current, cell), GRAY) #Burzymy ścianę między aktualną a wylosowanym sąsiadem self.current = cell #Sąsiad staje się aktualną komnatą self.make_visited(cell) #Zaznaczamy sąsiada jako odwiedzonego elif self.stack: #Jeżeli nie ma sąsiada ale stos nie jest pusty if self.current == self.start: self.draw_cell(self.current, RED) else: self.draw_cell(self.current, GRAY) 3 Projekt “Strategia Wolnych i Otwartych Implementacji jako innowacyjny model zainteresowania kierunkami informatyczno-technicznymi oraz wspierania uczniów i uczennic w kształtowaniu kompetencji kluczowych” współfinansowany ze środków Unii Europejskiej w ramach Europejskiego Funduszu Społecznego. cell = self.stack.pop() #Zdejmujemy komnatę ze stosu self.current = cell #Która staje się aktualną else: #Jeżeli stos jest pusty i nie ma żadnego sąsiada #do którego możemy przebić przejście #Losujemy komnatę z listy jeszcze nie odwiedzonych cell = self.unvisited[random.randint(0, len(self.unvisited) - 1)] self.current = cell #Staje się ona naszą aktualną self.make_visited(cell) #Oznaczmy ją jako odwiedzoną pygame.display.update() #Odrysowujemy zawartość okna else: print("Zrobione!") #Stos jest już pusty, koniec algorytmu pygame.time.set_timer(TIMER_TICK, 0) #Zatrzymujemy timer W programie, na jego początku zdefiniowano także kilka zmiennych ustalających parametry programu takie jak: • MAX_WIDTH - maksymalna ilość komnat w poziomie • MAX_HEIGHT - maksymalna ilość komnat w pionie • WINDOW_WIDTH - szerokość okna z prezentacją, w pikselach • WINDOW_HEIGHT - wysokość okna z prezentacją, w pikselach • LINE_WIDTH - grubość linii ścian oddzielających komnaty, w pikselach • TIMER_TICK - zdarzenie użytkownika zwracane, gdy timer odliczy określoną ilość czasu. • DELAY - opóźnienie w milisekundach, między kolejnymi "tyknięciami" timera. Z tym opóźnieniem będzie wołana metoda "tick()" • oraz zmienne NBR_UP, NBR_LEFT, NBR_RIGHT, NBR_DOWN, będące kolejnymi wartościami od 0 do 3 oznaczającymi, do którego sąsiada ma zostać stworzone przejście. Na podstawie tych wartości wreck_wall() wie, którą ścianę ma zburzyć. Nie jest to oczywiście jedyny algorytm generujący labirynty ani też najbardziej optymalny, zarówno jeżeli chodzi o czas wykonania, wymagania pamięciowe oraz rezultat w postaci gotowego labiryntu. Jego implementacja jest jednak w miarę prosta i pokazuje zastosowanie stosu. Ciekawostką jest fakt, że ten sam algorytm może służyć nie tylko do generowania labiryntów ale także do ich przechodzenia w sposób automatyczny. 4 Projekt “Strategia Wolnych i Otwartych Implementacji jako innowacyjny model zainteresowania kierunkami informatyczno-technicznymi oraz wspierania uczniów i uczennic w kształtowaniu kompetencji kluczowych” współfinansowany ze środków Unii Europejskiej w ramach Europejskiego Funduszu Społecznego. Algorytm w trakcie tworzenia przejść w labiryncie 5 Projekt “Strategia Wolnych i Otwartych Implementacji jako innowacyjny model zainteresowania kierunkami informatyczno-technicznymi oraz wspierania uczniów i uczennic w kształtowaniu kompetencji kluczowych” współfinansowany ze środków Unii Europejskiej w ramach Europejskiego Funduszu Społecznego. Rezultat działania algorytmu Filmy instruktażowe: • Labirynt przygotowanie narzędzia http://youtu.be/bcIVbUL-2_4 • Labirynt część 1 http://youtu.be/K6BR1n-dL5s • Labirynt część 2 http://youtu.be/V1eIwRyi7oo • Labirynt część 3 http://youtu.be/nXFm4-za_Ew 6 Projekt “Strategia Wolnych i Otwartych Implementacji jako innowacyjny model zainteresowania kierunkami informatyczno-technicznymi oraz wspierania uczniów i uczennic w kształtowaniu kompetencji kluczowych” współfinansowany ze środków Unii Europejskiej w ramach Europejskiego Funduszu Społecznego.