Jak poprawnie zaprojektować wywołania zwrotne?

Odpowiedz Nowy wątek
2019-08-20 17:12
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.


edytowany 14x, ostatnio: Silv, 2019-08-20 17:35
Sam nie wiem, czy to bardziej nie nadaje się do działu "Inżynieria oprogramowania". - Silv 2019-08-20 17:22

Pozostało 580 znaków

2019-08-20 17:37
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.

Pozostało 580 znaków

2019-08-20 17:39
0

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


Pozostało 580 znaków

2019-08-20 17:47
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.

edytowany 1x, ostatnio: Maciej Cąderek, 2019-08-20 17:53

Pozostało 580 znaków

2019-08-20 17:51
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.


edytowany 3x, ostatnio: Silv, 2019-08-20 17:53

Pozostało 580 znaków

2019-08-20 18:09
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.

Pozostało 580 znaków

2019-08-20 21:38
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.


Pokaż pozostałe 23 komentarze
@Silv: czy ty sypiasz po 2h dziennie? - baant 2019-08-21 05:39
Poza ID segment ma pozycję, kierunek oraz znak do wyświetlania. Ta trójka nie musi jednak wyznaczać segmentu, jeśli, przykładowo, jeden wejdzie na drugi. W ten sposób zabronione byłoby porównywanie segmentów Nie wiem, czy dobrze rozumiem twój tok rozumowania, ale jeżeli robisz Węża działającego jak klasyczny Snake, to każdy kolejny segment idzie na sztywno tą samą drogą, którą przebyła wcześniej głowa i jeśli ona nie weszła wcześniej z niczym w kolizję, to każdy kolejny segment podążający jej ścieżką też ma zagwarantowaną bezkolizyjność. - Freja Draco 2019-08-21 11:55
A jutro przyjdzie Fre i wszystkie moje inżynierskie rozważania podsumuje jednym optymalizującym stwierdzeniem a proszę: KISS - https://pl.wikipedia.org/wiki/KISS_(regu%C5%82a) ;) - Freja Draco 2019-08-21 12:00
@baant: Nie, sypiam tak jak większość. Po prostu w innych godzinach. ;) - Silv 2019-08-21 14:19
Oczywiście, o KISS można zapomnieć. Dzięki, Fre. - Silv 2019-08-21 14:20

Pozostało 580 znaków

2019-08-20 22:44
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.


edytowany 7x, ostatnio: Silv, 2019-08-20 23:13
Pokaż pozostałe 7 komentarzy
Z tą kolizją to dzielisz włos na czworo - to standardowa terminologia w grach. Obojętnie czy uderzasz w ścianę, wychodzisz poza planszę czy wchodzisz w chmurę dymu. No ale oczywiście możesz używać własnych, bardziej specyficznych określeń, ja się tu nie czepiam ;) - Maciej Cąderek 2019-08-20 23:30
Możesz mieć rację, ale... zmiany powinny następować, by uzyskać (wyższą) wartość dodaną. Przynajmniej tak bym to widział u siebie... <idealista> - Silv 2019-08-20 23:33
Wyjście poza planszę nie jest kolizją w moim rozumieniu, bo "kolizja" musi zawsze następować z czymś. Poza planszą nie ma nic I tym sposobem wchodzimy w dziedzinę filozofii programistycznej i stawiamy ważkie pytania na miarę: Dlaczego jest raczej coś niż nic ;) - Freja Draco 2019-08-21 12:04
@Freja Draco: :D Trafnie. Niemniej to jest jeszcze poziom języka, a zbiór pozycji jest w mojej aplikacji nieskończony (dlatego to ma u mnie sens). - Silv 2019-08-21 14:27
PS. Plansza jedynie nakłada na niego ograniczenia. - Silv 2019-08-21 14:27

Pozostało 580 znaków

2019-08-20 23:50
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ą.

Prawda! Mój błąd. - Silv 2019-08-20 23:50
Nieintuicyjne pierwszym wrażeniem, ale optymalizacja może się przydać... - Silv 2019-08-21 00:02

Pozostało 580 znaków

2019-08-21 02:12
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.


Pozostało 580 znaków

2019-08-21 02:20
1

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


edytowany 1x, ostatnio: Silv, 2019-08-21 02:21
Pokaż pozostałe 4 komentarze
A, canvas... nie pomyślałem. :) Na razie robię w konsoli. - Silv 2019-08-21 14:57
Tzn. zostanę na konsoli. - Silv 2019-08-21 14:57
E... w konsoli przeglądarki? - Freja Draco 2019-08-21 14:59
Nie, nie, tworzę w Node.js. - Silv 2019-08-21 15:03
Jakoś mi web przestał podchodzić. - Silv 2019-08-21 15:06

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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