Mrówka Langtona – Pokażmy Jej Nowy Kierunek
- Jarosław Piszczała
- 07 May, 2020
- 07 May, 2020
W poprzednim wpisie zajęliśmy się budową prostego algorytmu do prezentacji tego, jak porusza się Mrówka Langtona. Iteracyjnie rozszerzaliśmy możliwości naszego kodu od obsługi małej planszy i kilku kroków do wyświetlania dużo większej planszy z pomocą tkinter
. Dziś zajmiemy się rozszerzeniem tego kodu o więcej możliwości sterowania naszą mrówką, i podziwiania jej na żywo!
Baza
Bazą do dzisiejszej pracy, będzie kod z poprzedniego wpisu. W razie potrzeb wyjaśnień warto udać się do tamtego artykułu.
Algorytm w formie stringu
Na ten moment nasz algorytm opisany był poprzez if else
ze względu na to, że każde pole posiadało tylko dwa stany, i od nich zależał obrót naszej mrówki. Aby móc łatwiej zmieniać algorytm, najlepiej będzie go opisać poprzez string
. O dziwo, zmiana na takie podejście uprości nasz kod! Teraz, aby otrzymać kierunek poruszania, wystarczy odczytać wartość spod pozycji w naszej zmiennej pattern
. Następnie zmiana wartości to dodanie jeden i użycie modulo, aby nasza wartość nie przekroczyła zakresu.
Po uruchomieniu mrówka zachowała się tak jak poprzednio - czyli świetnie i bez zmian!
Odcienie
Dla algorytmu z dwoma stanami kwestia kolorów jest prosta - mamy biały albo czarny. Jeżeli chcemy rozszerzyć naszą aplikację o dłuższe algorytmy, musimy zrobić coś, co odróżni te stany. Moglibyśmy użyć kolorów, ale uważam, że fajniej będzie to zrobić poprzez odcienie szarości. Będziemy je definiować na podstawie liczby możliwych stanów. W pierwszej kolejności wprowadźmy sobie pojęcie koloru w formie kodu hex. Dla koloru białego jest to #FFFFFF
, dla czarnego #000000
.
Następnie dobrze byłoby utworzyć logikę do generowania odcieni. Kolory mamy tutaj w zakresach 0-255, więc musimy podzielić odpowiednio 255, aby otrzymać wartość różnicy między kolejnymi barwami. Następnie poprzez list comprehension
generujemy ich listę.
RGB hex
To, co otrzymaliśmy na górze, to lista wartości z zakresu 0-255. Aby poprawnie użyć ich w kodzie, musimy przekształcić je na format hex. Dla przykładu RGB(255, 255, 255)
jest równoznaczne z zapisem #FFFFFF
. Co musimy osiągnąć, to przekształcić ją na liczbę heksadecymalną, a następnie zduplikować 3 razy, aby otrzymać ostateczny wynik. Wykorzystałem do tego formatowanie w stringach. Formatowanie zmiennej n
poprzez zapis n:02x
przekształci naszą liczbę na heksadecymalną (dzięki znakowi x
) oraz doda 0
z przodu, jeżeli liczba w formacie heksadecymalnym będzie jednoznakowa. Pozostało powtórzyć ten manewr trzy razy i poprzedzić go znakiem #
. Ostatecznie, gdy podstawimy naszą funkcję w list comprehension
, otrzymamy listę gotowych kolorów do użycia!
Pozostało użyć naszej listy barw przy kolorowaniu kwadratów podczas renderowania widoku.
Test mrówki!
Czas sprawdzić, czy kroki poczynione przez nas do tej pory pozwolą zobaczyć oczekiwane efekty. Aby przetestować więcej niż dwa kolory, postanowiłem pokazać, jak zachowa się mrówka przy algorytmie poruszania RLR
. Jak widać poniżej, nasz kod wciąż działa poprawnie, obsługuje już inne, rozszerzone algorytmy i odpowiednio je prezentuje poprzez różnicę kolorów między stanami.
Stopniowe Rysowanie
Do tego momentu nasza aplikacja wyświetlała ostateczny wynik działania algorytmu. A co gdybyśmy wymusili na niej rysowanie kolejnych etapów?
Aby tego dokonać, przenieśmy kod służący do wyświetlania stanu planszy do funkcji. Na końcu tej funkcji wyląduje użycie metody canvas.update()
do aktualizowania naszej kanwy.
Następnie podstawiamy ją tam, gdzie uprzednio używaliśmy tego skrawka kodu.
Pozostała jeszcze krokowa aktualizacja stanu. Zrobimy to w naszej pętli, która robi zmiany na naszej planszy. Na początek, aby przyspieszyć druk, będziemy odświeżać kanwę co 1000 kroków.
Po uruchomieniu naszej aplikacji zacznie ona renderować kolejne etapy przemieszczania się mrówki, co tysiąc kroków.
Przyspieszenie Rysowania
Działa to dość wolno. Spowodowane jest to rysowaniem kolejnych kwadratów na już istniejących kwadratach. Zapychamy przez to pamięć i kanwę, która staje się coraz cięższa. Są różne metody jak poradzić sobie z tym problemem. Ja zrobię to dość prosto, poprzez dwie zmiany.
Po pierwsze, aktualnie nasza plansza nie ma podstawowej barwy, która nas interesuje. Dlatego też, przy tworzeniu obiektu klasy Canvas
zdefiniujemy kolor tła.
Następnie przed każdym rysowaniem nowego stanu planszy będziemy pozbywać się już istniejących elementów, i utworzymy je na nowo!
Poniższa animacja udowadnia, że nasz skrypt zdecydowanie przyspieszył! Ale da się zrobić to jeszcze szybciej!
Kolejne przyspieszenie
Wciąż robimy pewną operację, która na ten moment jest nam niepotrzebna. Jest nią rysowanie kwadratów o kolorze białym! Skoro nasza canva ma już ustawiony podstawowy kolor, wystarczy omijać rysowanie tych kwadratów.
Aby ukazać, jak szybko teraz renderuje się mrówka, postanowiłem nie zmieniać żadnych zmiennych dotąd używanych. Różnica jest kolosalna między iteracjami naszego rozwiązania!
Pełny Zakres
Ostatni element, który jest nam potrzebny po tych zmianach, to pozwolenie mrówce działać, dopóki nie zderzy się ze ścianą. Uczynimy to poprzez zamknięcie naszego kodu w try except
, dzięki czemu nasz skrypt będzie pracować wyznaczoną ilość kroków, albo do momentu zetknięcia się ze ścianą.
Dzięki tej zmianie możemy zaobserwować jaką drogą podąża mrówka Langtona przy podstawowym algorytmie RL
.
Dodatkowo dzięki tej zmianie, udało się zaobserwować błąd w naszym kodzie, który jest związany z logiką Pythona. O ile nie można wykraczać poza zakresy list, o tyle Python bez problemu obsłuży indeksy poniżej zera, z tego powodu nasza mrówka przeszła na drugą stronę! Brzmi jak fajny feature
, ale by nie zakłócać wyników pozwolimy sobie go usunąć.
Ściana
Jak już wyżej opisałem, powyższy problem związany jest z możliwością poruszania się po wartościach ujemnych. Gdybyśmy chcieli, aby nasz kod wspierał takie sytuacje, należałoby wartości x
oraz y
przepuścić odpowiednio przez modulo. W naszym przypadku zabezpieczymy się przed tym przypadkiem, poprzez podnoszenie wyjątku IndexError
, gdy któraś z wartości x
lub y
będzie poniżej 0.
Przeniesienie zmiennych
Na koniec wyciągnijmy przydatne zmienne w jedno miejsce, aby móc łatwiej korzystać z naszego kodu!
Zabawa Mrówką Langtona!
Zobaczmy jakie efekty możemy osiągnąć z aktualnym kodem! Dla rozmiaru planszy 300x300
, oraz algorytmu RRLLLRLLLRRR
prześledźmy maksymalnie 150_000
kroków, aktualizując nasz widok co 10_000
kroków.
Stopka
Czy da się wycisnąć więcej z mrówki langtona? Zdecydowanie tak! Według propozycji Wikipedii mamy jeszcze dwa ciekawe przypadki do obsłużenia. Możemy dodać kolejne mrówki, oraz poruszać się po hexie! Dodatkowo wciąż wprowadzamy zmiany w kodzie, czas najwyższy przenieść sterowanie do okna aplikacji.