3.6.5. Strategia podstawowa

3.6.5.1. Przykład robota

Kod nr
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import rg

class Robot:

    def act(self, game):
        # jeżeli jesteś w środku, broń się
        if self.location == rg.CENTER_POINT:
            return ['guard']

        # jeżeli wokół są przeciwnicy, atakuj
        for poz, robot in game.robots.iteritems():
            if robot.player_id != self.player_id:
                if rg.dist(poz, self.location) <= 1:
                    return ['attack', poz]

        # idź do środka planszy
        return ['move', rg.toward(self.location, rg.CENTER_POINT)]

Z powyższego kodu wynikają trzy zasady:

  • broń się, jeżeli jesteś w środku planszy;
  • atakuj przeciwnika, jeżeli jest obok;
  • idź do środka.

To pozwala nam rozpocząć grę, ale wiele możemy ulepszyć. Większość usprawnień (ang. feature), które zostaną omówione, to rozszerzenia wersji podstawowej. Konstruując robota, można je stosować wybiórczo.

3.6.5.2. Kolejne reguły

Rozbudujemy przykład podstawowy. Oto lista reguł, które warto rozważyć:

  • Reguła 1: Opuść punkt wejścia.

Pozostawanie w punkcie wejścia nie jest dobre. Sprawdźmy, czy jesteśmy w punkcie wejścia i czy powinniśmy z niego wyjść. Nawet wtedy, gdy jest ktoś do zaatakowania, ponieważ nie chcemy zostać zamknięci w pułapce wejścia.

  • Reguła 2: Uciekaj, jeśli masz zginąć.

Przykładowy robot atakuje aż do śmierci. Ponieważ jednak wygrana zależy od liczby pozostałych robotów, a nie ich zdrowia, bardziej opłaca się zachować robota niż poświęcać go, żeby zadał dodakowe obrażenia przeciwnikowi. Jeżeli więc jesteśmy zagrożeni śmiercią, uciekamy, a nie giniemy na próżno.

  • Reguła 3: Atakuje przeciwnika o dwa kroki od ciebie.

Przyjrzyj się grającemu wg reguł robotowi, zauważysz, że kiedy wchodzi na pole atakowane przez przeciwnika, odnosi obrażenia. Dlatego, jeśli prawdopodobne jest, że przeciwnik może znaleźć się w naszym sąsiedztwie, trzeba go zatakować. Dzięki temu nit się do nas bezkarnie nie zbliży.

Informacja

Połączenie ucieczki i ataku w kierunku przeciwnika naprawdę jest skuteczne. Każdy agresywny wróg zanim nas zaatakuje, sam spotyka się z atakiem. Jeżeli w porę odskoczysz, zanim się zbliży, działanie takie możesz powtórzyć. Technika ta nazywana jest w grach kiting, a jej działanie ilustruje poniższa animacja:

../../_images/kiting.gif

Zwróć uwagę na słabego robota ze zdrowiem 8 HP, który podchodzi do mocnego robota z 50 HP, a następnie ucieka. Zbliżając się atakuje pole, na które wchodzi przeciwnik, ucieka i ponawia działanie. Trwa to do momentu, kiedy silniejszy robot popełni samobójstwo (co w tym wypadku jest mało przydatne). Wszystko bez uszczerbku na zdrowiu słabszego robota.

  • Reguła 4: Wchodź tylko na wolne pola.

Przykładowy robot idzie do środka planszy, ale w wielu wypadkach lepiej zrobić coś innego. Np. iść tam, gdzie jest bezpiecznie, zamiast narażać się na bezużyteczne niebezpieczeństwo. Co jest bowiem ryzykowne? Po wejściu na planszę ruch na pole przeciwnika lub wchodzenie w jego sąsiedztwo. Wiadomo też, że nie możemy wchodzić na zajęte pola i że możemy zmniejszyć ilość kolizji, nie wchodząc na pola zajęte przez naszą drużynę.

  • Reguła 5: Idź na wroga, jeżeli go nie ma w zasięgu dwóch kroków.

Po co iść do środka, skoro mamy inne bezpieczne możliwości? Wprawdzie stanie w punkcie wejścia jest złe, ale to nie znaczy, że środek planszy jest dobry. Lepszym wyborem jest ruch w kierunku, ale nie na pole, przeciwnika. W połączeniu z atakiem daje nam to lepszą kontrolę nad planszą. Później przekonamy się jeszcze, że są sytuacje, kiedy wejście na potencjalnie niebezpieczne pole warte jest ryzyka, ale na razie poprzestańmy na tym, co ustaliliśmy.

3.6.5.3. Łączenie ulepszeń

Zapiszmy wszystkie reguły w pseudokodzie. Możemy użyć do tego jednej rozbudowanej instrukcji warunkowej if/else.

jeżeli jesteś w punkcie wejścia:
    rusz się bezpiecznie (np. poza wejście)
jeżeli jeddnak mamy przeciwnika o krok dalej:
    jeżeli możemy umrzeć:
        ruszamy się w bezpieczne miejsce
    w przeciwnym razie:
        atakujemy przeciwnika
jeżeli jednak mamy przeciwnika o dwa kroki dalej:
    atakujemy w jego kierunku
jeżeli mamy bezpieczny ruch (i nikogo wokół siebie):
    ruszamy się bezpiecznie, ale w kierunku przeciwnika
w przeciwnym razie:
    bronimy się w miejscu, bo nie ma gdzie ruszyć się lub atakować

3.6.5.4. Implementacja

Do zakodowania omówionej logiki potrzebujemy struktury danych gry z jej ustawieniami i kilku funkcji. Pamiętajmy, że jest wiele sobosobów na zapisanie kodu w Pythonie. Poniższy w żdanym razie nie jest optymalny, ale działa jako przykład.

3.6.5.5. Zbiory zamiast list

Dla ułatwienia użyjemy pythonowych zbiorów razem z funkcją set() i wyrażeniami zbiorów (ang. set comprehensions).

Informacja

Zbiory i operacje na nich omówiono w dokumentacji zbiorów, podobnie przykłady wyrażeń listowych i odpowiadających im pętli.

Podstawowe operacje na zbiorach, których użyjemy to:

  • | lub suma – zwraca zbiór wszystkich elementów zbiorów;
  • - lub różnica – zbiór elementów obecnych tylko w pierwszym zbiorze;
  • & lub iloczyn – zwraca zbiór elementów występujących w obydwu zbiorach.

Załóżmy, że zaczniemy od wygenerowania następujących list: drużyna – członkowie drużyny, wrogowie – przeciwnicy, wejścia – punkty wejścia oraz przeszkody – położenia zablokowane, tzn. szare kwadraty.

3.6.5.6. Zbiory pól

Aby ułatwić implementację omówionych ulepszeń, przygotujemy kilka zbiorów reprezentujących pola różnych kategorii na planszy gry. W tym celu używamy wyrażeń listowych (ang. list comprehensions).

# zbiory pól na planszy

# wszystkie pola
wszystkie = {(x, y) for x in xrange(19) for y in xrange(19)}

# punkty wejścia (spawn)
wejscia = {loc for loc in wszystkie if 'spawn' in rg.loc_types(loc)}

# pola zablokowane (obstacle)
zablokowane = {loc for loc in wszystkie if 'obstacle' in rg.loc_types(loc)}

# pola zajęte przez nasze roboty
przyjaciele = {loc for loc in game.robots if game.robots[loc].player_id == self.player_id}

# pola zajęte przez wrogów
wrogowie = set(game.robots) - przyjaciele

Warto zauważyć, że zbiór wrogich robotów otrzymujemy jako różnicę zbioru wszystkich robotów i tych z naszej drużyny.

3.6.5.7. Wykorzystanie zbiorów

Przy poruszaniu się i atakowaniu mamy tylko cztery możliwe kierunki, które zwraca funkcja rg.locs_around. Możemy wykluczyć położenia zablokowane (ang. obstacle), ponieważ nigdy ich nie zajmujemy i nie atakujemy. Iloczyn zbiorów sasiednie & wrogowie da nam zbiór przeciwników w sąsiedztwie:

# pola sąsiednie
sasiednie = set(rg.locs_around(self.location)) - zablokowane

# pola sąsiednie zajęte przez wrogów
wrogowie_obok = sasiednie & wrogowie

Aby odnaleźć wrogów oddalonych o dwa kroki, szukamy przyległych kwadratów, obok których są przeciwnicy. Wyłączamy sąsiednie pola zajęte przez członków drużyny.

# pola zajęte przez wrogów w odległości 2 kroków
wrogowie_obok2 = {loc for loc in sasiednie if (set(rg.locs_around(loc)) & wrogowie)} - przyjaciele

Teraz musimy sprawdzić, które z położeń są bezpieczne. Usuwamy pola zajmowane przez przeciwników w odległości 1 i 2 kroków. Pozbywamy się także punktów wejścia, nie chcemy na nie wracać. Podobnie, aby zmniejszyć możliwość kolizji, wyrzucamy pola zajmowane przez drużynę. W miarę komplikowania logiki będzie można zastąpić to ograniczenie dodatkowym warunkiem, ale na razie to najlepsze, co możemy zrobić.

bezpieczne = sasiednie - wrogowie_obok - wrogowie_obok2 - wejscia - przyjaciele

Potrzebujemy funkcji, która wybierze ze zbioru położeń pole najbliższe podanego. Możemy użyć tej funkcji do znajdowania najbliższego wroga, jak również do wyboru pola z bezpiecznej listy. Możemy więc wybrać ruch najbardziej przybliżający nas do założonego celu.

def mindist(bots, loc):
    return min(bots, key=lambda x: rg.dist(x, loc))

Możemy użyć metody pop() zbioru, aby pobrać jego dowolny element, np. przeciwnika, którego zaatakujemy. Żeby dowiedzieć się, czy jesteśmy zagrożeni śmiercią, możemy pomnożyć liczbę sąsiadujących przeciwników przez średni poziom uszkodzeń (9 punktów HP) i sprawdzić, czy mamy więcej siły.

Ze względu na sposób napisania funkcji minidist() trzeba pamiętać o przekazywaniu jej niepustych zbiorów. Jeśli np. zbiór przeciwników będzie pusty, funkcja zwróci błąd.

3.6.5.8. Składamy wszystko razem

Po złożeniu wszystkich kawałków kodu razem otrzymujemy przykładową implemetację robota wyposażonego we wszystkie założone wyżej właściwości:

Kod nr
 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import rg

class Robot:

    def act(self, game):

        wszystkie = {(x, y) for x in xrange(19) for y in xrange(19)}
        wejscia = {poz for poz in wszystkie if 'spawn' in rg.loc_types(poz)}
        zablokowane = {poz for poz in wszystkie if 'obstacle' in rg.loc_types(poz)}
        przyjaciele = {poz for poz in game.robots if game.robots[poz].player_id == self.player_id}
        wrogowie = set(game.robots) - przyjaciele

        sasiednie = set(rg.locs_around(self.location)) - zablokowane
        wrogowie_obok = sasiednie & wrogowie
        wrogowie_obok2 = {poz for poz in sasiednie if (set(rg.locs_around(poz)) & wrogowie)} - przyjaciele
        bezpieczne = sasiednie - wrogowie_obok - wrogowie_obok2 - wejscia - przyjaciele

        def mindist(bots, poz):
            return min(bots, key=lambda x: rg.dist(x, poz))

        if wrogowie:
            najblizszy_wrog = mindist(wrogowie,self.location)
        else:
            najblizszy_wrog = rg.CENTER_POINT

        # działanie domyślne:
        ruch = ['guard']

        if self.location in wejscia:
            if bezpieczne:
                ruch = ['move', mindist(bezpieczne, rg.CENTER_POINT)]
        elif wrogowie_obok:
            if 9*len(wrogowie_obok) >= self.hp:
                if bezpieczne:
                    ruch = ['move', mindist(bezpieczne, rg.CENTER_POINT)]
            else:
                ruch = ['attack', wrogowie_obok.pop()]
        elif wrogowie_obok2:
            ruch = ['attack', wrogowie_obok2.pop()]
        elif bezpieczne:
            ruch = ['move', mindist(bezpieczne, najblizszy_wrog)]

        return ruch

Informacja

Niniejsza dokumentacja jest swobodnym i nieautoryzowanym tłumaczeniem materiałów dostępnych na stonie Robotgame basic strategy.


Licencja Creative Commons Materiały Python 101 udostępniane przez Centrum Edukacji Obywatelskiej na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.

Utworzony:2022-05-22 o 19:52 w Sphinx 1.5.3
Autorzy:Patrz plik “Autorzy”