Dyspozytor zdarzeń - gra Mario

Piszesz grę platformową wzorowaną na świecie Mario. Postać, którą kieruje gracz, może zderzyć się z różnymi ruchomymi obiektami na planszy, taki jak moneta, żółw (Mario ginie), czy wisienka (supermoc). Aktualnie Twój kod prezentuje się następująco:

 1game_actors = [(20, 5, "Turtle"), (24, 8, "Coin"),
 2                      (26, 8, "Coin"), (30, 2, "Cherry")]
 3
 4
 5def process_collisions(mario, other_actors):
 6    for a in other_actors:
 7        # check if X and Y match; continue if it's not the case
 8        if a[0] != mario[0] or a[1] != mario[1]: continue
 9        # yes, we have a collision!
10        if a[3] == "Coin":
11            pass # update the number of coins collected by a player
12        if a[3] == "Cherry":
13            pass # superpower Mario!
14        if a[3] == "Turtle":
15            pass # no_lives -= 1
16
17if __name__ == "__main__":
18    # in the main loop of the game you update Mario's position

Powyższy kod jest nieoptymalny z wielu powodów:

  1. Reprezentowanie aktorów gry (moneta, żółw) jako tuple trójelementowe: (x, y, nazwa) jest proszeniem się o kłopoty: kod jest nieczytelny i łatwo pomylić indeks [0] z [1]. Dodatkowo rozbudowywanie aktorów o dodatkowe zmienne (np liczba monet zebranych przez Mario) stwarza dodatkowe problemy z indeksowaniem.

  2. instrukcja wielokrotnego wyboru, która podejmuje decyzję o reakcji na zdarzenie jest trudna w utrzymaniu / rozbudowie

Przepisz powyższy kod tak, aby:

  • każdy z aktorów dziedziczył po abstrakcyjnej klasie Actor, zawierającej jego położenie na planszy

  • klasa Mario powinna zawierać też aktualną liczbę żyć gracza, liczbę zebranych monet oraz flagę logiczną włączającą supermoce (po zderzeniu z wisienką)

  • dla każdej z tych klas napisz też klasę-akcję, która modyfikuje obiekt Mario odpowiednio do zdarzenia; klasy te powinny dziedziczyć po abstrakcyjnej klasie bazowej AbstractAction

  • zaimplementu dyspozytor, który każdej z możliwych klas Actor przypisze odpowiedni obiekt AbstractAction

Pierwsze dwie kropki z powyżej listy rozwiązują problem (1) - obiektową reprezentację aktorów. Dwie kolejne kropki powinny wyeliminować instrukcję wielokrotnego wyboru (punkt (2)). Poprawnie napisany kod powinien działać z poniższą sekcją __main__:

 1game_actors = [Turtle(20, 5), Coin(24, 8),
 2                      Coin(26, 8), Cherry(30, 2)]
 3
 4if __name__ == "__main__":
 5
 6    # in the main loop of the game you update Mario's position
 7    mario = Mario(0, 0)
 8    # register collision action in a dispatch:
 9    collisions = CollisionDispatch()
10    collisions.register(Coin.class, MarioHitsCoin)
11
12    # check for collisions:
13    for actor in game_actors:
14        # dispatch actions accordingly to the actor's type
15        if mario.if_crashes_into(actor):
16            collisions.dispatch(actor)

Rozwiązanie zadania znajduje się poniżej:

(rozwiń kod rozwiązania)
 1from abc import ABC, abstractmethod
 2from math import fabs
 3
 4class Actor:
 5    def __init__(self, x, y, width, height):
 6        self.x = x
 7        self.y = y
 8        self.width = width
 9        self.height = height
10
11class Mario(Actor):
12    def __init__(self, x, y):
13        super().__init__(x, y, 20, 20)
14        self.coins = 0
15        self.lifes = 3
16        self.speed = 1.0
17
18    def if_crashes_into(self, actor: Actor):
19        dx = fabs(actor.x - self.x)
20        dy = fabs(actor.y - self.y)
21        return dx < (self.width+actor.width)/2.0 and dy < (self.height+actor.height)/2.0
22
23class Coin(Actor):
24    def __init__(self, x, y):
25        super(Coin, self).__init__(x, y, 10, 10)
26
27class Turtle(Actor):
28    def __init__(self, x, y):
29        super().__init__(x, y, 30, 10)
30
31class Cherry(Actor):
32    def __init__(self, x, y):
33        super().__init__(x, y, 10, 10)
34
35game_actors = [Turtle(20, 5), Coin(24, 8),
36                      Coin(26, 8), Cherry(30, 2)]
37
38class CollisionAction(ABC):
39    @abstractmethod
40    def dispatch(self, a_mario: Mario): pass
41
42class MarioHitsTurtle(CollisionAction):
43    def dispatch(self, a_mario: Mario): a_mario.lifes -= 1
44
45
46class MarioHitsCoin(CollisionAction):
47    def dispatch(self, a_mario: Mario): a_mario.coins += 1
48
49class MarioHitsCherry(CollisionAction):
50    def dispatch(self, a_mario: Mario): a_mario.speed *= 1.5
51
52class CollisionDispatch:
53    def __init__(self):
54        self.__dict = {}
55
56    def register(self, cls_name: str, action: CollisionAction):
57        self.__dict[cls_name] = action
58
59if __name__ == "__main__":
60
61    # in the main loop of the game you update Mario's position
62    mario = Mario(0, 0)
63    # register collision action in a dispatch:
64    collisions = CollisionDispatch()
65    collisions.register(Coin.__class__.__name__, MarioHitsCoin())
66    collisions.register(Turtle.__class__.__name__, MarioHitsTurtle())
67    collisions.register(Cherry.__class__.__name__, MarioHitsCherry())
68
69    # check for collisions:
70    for actor in game_actors:
71        # dispatch actions accordingly to the actor's type
72        if mario.if_crashes_into(actor):
73            collisions.dispatch(actor)
74