Jak poprawnie zaprojektować wywołania zwrotne?

0

Gra "Snake" (mniej lub bardziej klasyczna wersja). Sytuacja wyjściowa jest taka:

  • Wąż to obiekt snake – przechowuje zbiór segmentów węża.
  • Segment węża to obiekt snakeSegment – składa się z aktualnej pozycji oraz aktualnego kierunku (tzn. takiego, "w którym idzie").
  • "Pozycja skrętu" jest to obiekt breakPoint (nie gańcie mnie za tę roboczą nazwę) – składa się z pary (pozycja, kierunek). Oznacza pozycję, w której dany segment węża musi skręcić w określonym kierunku (ponieważ został naciśnięty klawisz).
  • Pozycje skrętu są przechowywane w obiekcie breakPoints (zawiera ich tablicę).
  • Wąż może zbierać punkty – feed – które są przechowywane w obiekcie feeds (zawiera ich tablicę). UPDATE: Obiekt feed (ten bez "s") składa się z pozycji oraz wartości (ile punktów jest do zebrania w tym punkcie... eee... wiadomo, o co chodzi).

Podczas gry:

  • Wybór nowej pozycji jest ograniczony wielkością planszy oraz pozycjami innych segmentów węża.
  • Wybór (czy "wytyczenie"?) nowego kierunku dla każdego z segmentów jest ograniczony tym, czy istnieje dla danej pozycji segmentu punkt skrętu.

I teraz kod ruchu w obiekcie węża wygląda tak:

exports.snake = {
    ...,
    move: function (
        isPositionForbidden, // wywołanie zwrotne; parametr – position (tzn. "pozycja węża")
        doWhenPositionForbidden, // wywołanie zwrotne; parametr – position (tzn. "pozycja węża")
        isBreakPointPosition, // wywołanie zwrotne; parametr – segmentPosition (tzn. pozycja tego segmentu węża, dla którego ta funkcja jest wywoływana)
        getNewDirectionWhenBreakPointPosition, // wywołanie zwrotne; parametr – segmentPosition (tzn. pozycja tego segmentu węża, dla którego ta funkcja jest wywoływana)
        isFeedPosition, // wywołanie zwrotne; parametr – position (tzn. "pozycja węża")
        doWhenFeedPosition // wywołanie zwrotne; parametr – position (tzn. "pozycja węża")
    ) {
        ...
    },
    ...
};

Wywoływane jest to w głównej funkcji programu. Jak widać, są tu trzy grupy:

  1. sprawdzanie, czy wąż wyszedł poza tablicę lub wszedł na samego siebie, oraz obsługa sytuacji, jeśli tak;
  2. sprawdzanie, czy poszczególne segmenty natrafiły na pozycję skrętu, oraz obsługa sytuacji, jeśli tak;
  3. sprawdzanie, czy wąż natrafił na punkt, oraz obsługa sytuacji, jeśli tak.

Założeniem jest, że wąż nie wie nic o planszy, pozycjach skrętu ani punktach do zebrania. Zna jedynie swoje segmenty. Podobnież, segment nie wie nic o tych trzech rzeczach – zna jedynie swoją pozycję oraz kierunek. Dopiero na poziomie pozycji można określić, czy jest ona "niedozwolona" (forbidden), czy jest pozycją skrętu, oraz jest na niej punkt do zebrania.

Problemem jest to, że jest to 6 wywołań zwrotnych zawierających trzy grupy. Próbowałem, ale nie mam pomysłu, jak to można zapisać, hm... w bardziej skonsolidowany sposób? Inaczej? Przenieść może jakąś część logiki dokądś?


PS. Przy czym, jak widać, funkcja obsługi pozycji skrętu musi zwracać nowy kierunek – co powiększa problem, ponieważ burzy jednolitość tych trzech grup.


PS2: Pytajcie, jak coś niejasno napisałem. Nad taką logiką myślałem kilka dni, więc jest ona, patrząc z mojej strony, sophisticated.

1

Może nie składaj wszystkiego w metodzie .move(), skoro masz jak piszesz trzy "grupy",gdzie pierwszy element grupy to w zasadzie warunek, to możesz go użyć wewnątrz drugiego elementu. Masz wtedy trzy argumenty zamiast sześciu.

0

@Maciej Cąderek: tak, myślałem nad tym, ale jakbyś w takim razie nazwał je?

0

No na podstwaie tego co widzę to Ci nie powiem, bo teraz niektóre nazwy są imo skopane - nie mówią co funkcja robi, tylko kiedy to robi (doWhenPositionForbidden, doWhenFeedPosition). Ale strzelam, że coś w stylu:

handleCollision()
handleDirectionChange()
handleFeeding()

?

Edit:
W sumie pasuje do punktów 1, 2, 3 w opisie.

0

Myślałem nad takimi, i doszedłem do wniosku, że bardziej jest już intuicyjne rozdzielenie logiki sprawdzania i obsługi. handleCollision (czy też raczej handleForbiddenPosition, bo wyjście poza planszę nie jest kolizją) nie oznacza dla mnie, że sprawdza pozycję, tylko że to już jest pewne, że pozycja jest taka a taka. Nie byłoby w tym nic złego, i pewnie przyjąłbym takie rozwiązanie, gdyby sprawdzenie pozycji zależało od węża. Problem jednak w tym, że zarówno sprawdzenie danej pozycji, jak i jej obsługa nie należy do logiki węża, więc obie rzeczy muszą być podane jako wywołania zwrotne.


PS: Może jako jedno wywołanie, może dwa, może nawet więcej, ale nie mogę w nazwie zakładać, że wąż sam sprawdzi pozycję – bo nie może.

0

wyjście poza planszę nie jest kolizją

Jak kończy grę (?) to czemu niby nie jest kolizją?

handleCollision [...] nie oznacza dla mnie, że sprawdza pozycję, tylko że to już jest pewne, że pozycja jest taka a taka.

Wiesz, zawsze możesz nazwać to handlePotentialCollision czy coś w tym stylu, ale wiele to nie zmienia.

obie rzeczy muszą być podane jako wywołania zwrotne

W jakim sensie muszą? Technicznie czy "filozoficznie"?
Ogólnie to w taki sposób metoda .move() robi kilka rzeczy na raz - co też idealne nie jest.

4
Silv napisał(a):
  • "Pozycja skrętu" jest to obiekt breakPoint (nie gańcie mnie za tę roboczą nazwę) – składa się z pary (pozycja, kierunek). Oznacza pozycję, w której dany segment węża musi skręcić w określonym kierunku (ponieważ został naciśnięty klawisz).

Po grzyba w ogóle przechowujesz coś takiego dla każdego segmentu? W zupełności wystarczy ci zmienna przechowująca aktualny kierunek poruszania się węża i tablica przechowująca współrzędne jego segmentów. W każdym kolejnym kroku:

  • dodajesz nowy element do tablicy o współrzędnych zależnych od aktualnego kierunku poruszania się,
  • zdejmujesz ostatni element tablicy (koniec ogona węża).

Ew. w sytuacji, gdy wąż w danym kroku je, pomijasz usuwanie ogniwa z ogona.

0

@Maciej Cąderek: Wyjście poza planszę nie jest kolizją w moim rozumieniu, bo "kolizja" musi zawsze następować z czymś. Poza planszą nie ma nic, więc nie ma z czym kolidować. Można to nazwać, i tak to nazwałem, "pozycją zabronioną niedozwoloną"; nazwa ta wynika po części z logiki gry – najpierw wąż się porusza, a potem sprawdzam, czy pozycja jest zabroniona niedozwolona, a nie odwrotnie. Może mógłbym zrobić odwrotnie (i jakoś jeszcze inaczej nazwać)... ale nie rozpatrywałem tego i nie mam ochoty przepisywać gry na nowo (już raz przepisałem na inną logikę).

Wiesz, zawsze możesz nazwać to handlePotentialCollision czy coś w tym stylu, ale wiele to nie zmienia.

O to chodzi, to nic nie zmienia. W moim odczuciu nazwy takie jak doIfOK czy doIfFailed byłyby najlepsze, ale na razie nie widzę, jak by można było je wykorzystać...

W jakim sensie muszą? Technicznie czy "filozoficznie"?

Obie rzeczy muszą być podane jako wywołania zwrotne w sensie raczej filozoficznym. Czy logika gry Snake jako takiej (poza moją implementacją) pozwoliłaby na inną filozofię (mniej wywołań zwrotnych)? Być może tak, ale nie wiem, jak by to miało wyglądać. Nie wiem, czy byłoby lepsze od tego podejścia – "lepsze" na przykład w sensie posiadania mniejszej liczby zależności (looser coupling).

Ogólnie to w taki sposób metoda .move() robi kilka rzeczy na raz - co też idealne nie jest.

Właśnie dziś stojąc na przystanku o tym sobie myślałem. Być może nie jest to idealne, i być może zmienię coś innego w tej metodzie niż liczba wywołań zwrotnych.


@Freja Draco: nie myślałem o takim podejściu. Obecnie obliczam współrzędne dla każdego segmentu, a Ty proponujesz, abym nie tylko liczył, ale również usuwał i dodawał segmenty (przy dodawaniu licząc pozycję). Dzięki! Pomyślę nad tym. Tak na szybko – nie wydaje się to ani gorsze, ani lepsze; ale każde nowe podejście (jak nowe maksimum lokalne) będzie lepsze niż międlenie starego (jak stare minimum lokalne).


UPDATE: @Maciej Cąderek, metoda move nie robi aż tyle chyba, co Ty masz na myśli – to są wywołania zwrotne, powinny one działać podobnie jak promises – wywołuję je po zakończeniu danej metody. Tzn. aktualnie w metodzie move jest to trochę pomieszane, ale kod przekazanych do niej funkcji jest wywoływany w większości "po" wykonaniu jej logiki.

2

Ty proponujesz, abym nie tylko liczył, ale również usuwał i dodawał segmenty (przy dodawaniu licząc pozycję).

Nie do końca, w podejściu @Freja Draco nie ma właśnie za wiele liczenia, obliczasz tylko nową pozycję głowy, nie przejmujesz się resztą.

2

Tak jeszcze odnośnie detekcji kolizji. Nie wiem, na ile da się to zrealizować w twoim przypadku, ale przy wyświetlaniu czegoś takiego w trybie znakowym, nie trzeba nawet sprawdzać kolizji przelatując tablicę wszystkich ogniw węża, żeby sprawdzić, czy wąż sam siebie nie ugryzie, bo wystarczy odczytać jaki znak siedzi aktualnie w polu na które głowa chce wejść. W trybie graficznym też w sumie da się odczytać kolor piksela. No i masz jeden test, zamiast kilkudziesięciu/kilkuset przy dłuższym wężu.

1

@Freja Draco: dzięki, niemniej tak robię właśnie. :) Ale po co odczytywać kolor piksela...?

1

Jak już się bawimy w filozofowanie i kombinacje, to mam jeszcze jeden pomysł na realizację Węża. Całość opiera się na następujących zmiennych:

  • tablicy komórek, każda komórka może zawierać:
    • pustkę: 0,
    • ziarenko do zjedzenia: -1,
    • ogniwo węża: liczba milisekund uniksowych opisujących czas narodzin danej komórki,
  • współrzędnych głowy i ogona,
  • aktualnym kierunku poruszania.

W każdym kolejnym kroku:

  • w oparciu o współrzędne głowy i kierunek poruszania

    • wyliczasz gdzie przesunie się głowa
    • testujesz ew. kolizje,
    • wypełniasz kolejną komórkę ogniwem węża,
  • w oparciu o współrzędne ogona:

    • zdejmujesz z planszy czubek ogona,
    • wyszukujesz współrzędne nowego czubka ogona, sprawdzając 8 komórek sąsiadujących z właśnie wyczyszczoną, wybierasz tą, która zawiera najmniejszą liczbę większą od zera.

Przy czym wskazane byłoby odwrócić kolejność i najpierw usuwać ogon i dopiero wtedy testować kolizje i rysować głowę.

Trochę cudactwo, ale jeśli i tak chcesz tam robić jakąś tablicę komórek, to nie potrzebujesz drugiej osobnej na samego węża :)

1

Ostatecznie przemodelowałem architekturę programu, niemniej wywołania zwrotne zostały zmienione jedynie lekko.

Zdecydowałem się jednak porzucić projekt. W związku z tym wątek pozostawiam bez zaakceptowanej odpowiedzi.

Niemniej, bardzo pomogła mi wskazówka @Freja Draco w tym poście: https://4programmers.net/Forum/1613705

1

Najprościej to po prostu w każdym ruchu wstawić nową głowę i usunąć ogon. Jeśli wąż zeżarł jabłko to tylko wstawić głowę. Wszystkie dane w segmentach inne niż współrzędne (dla głowy dodatkowo kierunek) oraz czynności inne niż opisane w pierwszym zdaniu są absolutnie zbędne. ;)

Kiedyś robiłem konsolowego węża, ale z bardziej skomplikowaną mechaniką. Oprócz zwykłego poruszania się i zjadania jabłek, można było przechodzić przez segmenty, w których znajdowało się jabłko (były renderowane jaśniejszym kolorem, żeby było wiadomo gdzie są). Obsługa punktów przecięcia węża znacznie uatrakcyjniała rozgrywkę.

1 użytkowników online, w tym zalogowanych: 0, gości: 1