Prosty algorytm do generacji labiryntów z wizualizacją w SDL2

7

Zamiast oglądania seriali i rozwiązywania krzyżówek przez weekend, dobrze czasem coś napisać.
Przy okazji, pobawiłem się trochę kolorami i rozdzielczością, wyszedł przyjemny generator mapy archipelagu wysp.

https://github.com/c3r/mazegen

3
Point (*dir_func[ALL_WALL])(Point& p) = ...
bool  (*bfunc[ALL_WALL])(Point& p)    = ...

Te nazwy niewiele mówią o tym, co funkcje robią.

  for (uint16_t x = 0; x < k_MazeWidth; x++)
    for (uint16_t y = 0; y < k_MazeHeight; y++)

Nie jest wcale powiedziane, że na mniejszym typie procesor będzie operował szybciej. Typy o określonej szerokości mają uzasadnienie w strukturach, które przekazujesz pomiędzy bibliotekami, ale w tego typu wypadkach lepiej dać zwykły int albo unsigned int.

      maze[At({ x, y })] = ( N_WALL | E_WALL | S_WALL | W_WALL );

Masz zdefiniowany constexpr ALL_WALL, dlaczego nie użyć go tutaj?

  uint32_t visited;
[- 10 linii przerwy -]
  visited = 1;

Trzymaj deklaracje zmiennych możliwie blisko ich inicjalizacji.

      if (dir == N_WALL) { *dest &= ~S_WALL; }
      if (dir == E_WALL) { *curr &= ~E_WALL; }
      if (dir == S_WALL) { *curr &= ~S_WALL; }
      if (dir == W_WALL) { *dest &= ~E_WALL; }

Użyj switch, albo dodaj else pomiędzy poszczególnymi warunkami.

    Point dest_point;
[- parę linii niżej -]
    if (!neighbours.empty())
    {
      dir = neighbours.at(rand() % neighbours.size());
      Point dest_point = dir_func[dir](curr_point);

Zmienna w wewnętrznym scope przesłania tę z zewnętrznego. Poza tym, zmienna z zewnętrznego scope jest nieużywana - nie jest do niej przypisywana żadna wartość ani nie bierze udziału w porównaniach.


mg_draw.h zawiera definicje funkcji, co jest złą praktyką - pliki .h powinny zawierać tylko deklaracje, a ciała funkcji powinny znajdować się w plikach .cpp. (Osobiście uważam też, że dobrze jest stosować .hpp dla odróżnienia headerów C od headerów C++.) Zgaduję, że użyłeś takiego rozwiązania, by móc łatwo kompilować program "z palca" z wiersza poleceń - zalecałbym w takim wypadku użycie Make albo CMake, aby móc budować projekt w sposób zautomatyzowany. Polecam także używanie -Wall oraz -Wextra przy kompilacji - część z rzeczy, które wypisałem, kompilator jest w stanie wykryć oraz o nich powiadomić.

2

Czy jest jakiś powód dla użycia switcha zamiast drabinki ifów, oprócz zasady zachowania najmniejszego zaskoczenia dla czytającego?

Jeżeli robisz switch na wartości typu enum, kompilator jest w stanie przeanalizować pokryte wartości i wyrzucić ostrzeżenie typu "obsługujesz ENUM_VALUE_A, ENUM_VALUE_C, ale pominąłeś ENUM_VALUE_B".

W mg_draw.h deklaracje umieściłem z czystego lenistwa, do kodowania używam VS 2019 Community. Dlaczego jest to zła praktyka?

Najprostsza odpowiedź brzmi "bo tak się nie robi". :p

Trochę dłuższa odpowiedź: bo jeżeli ten sam plik .h / .hpp, zawierający implementację funkcji, będziesz include'ował w kilku plikach .c / .cpp, w wynikowym programie będziesz miał, zamiast jednego kodu funkcji, który jest wywoływany z różnych miejsc, kilka kopii tej funkcji. Aktualnie unikasz tego problemu, mając tylko jeden plik .cpp.

Bardziej teoretyczna odpowiedź: gdy kompilujesz program, tworzysz tzw. pliki obiektowe (object file, .o). Te pliki zawierają kod oraz listę symboli - czyli taką listę "w tym obiekcie jest funkcja X oraz zmienna globalna Y; ten obiekt woła funkcję Z oraz dobiera się do zmiennej globalnej W". Gdy program składa się z wielu plików .c / .cpp, zazwyczaj każdy z nich kompiluje się do osobnego pliku .o i dopiero na sam koniec używa linkera (zwanego także konsolidatorem), który łączy pliki obiektowe w wynikowy program i zajmuje się rozwiązywaniem ("podpinaniem") symboli - tzn. sprawdza, który obiekt co eksportuje, a co sam chce importować, i łączy jedno z drugim.

Zatem, gdy masz plik .c / .cpp i include'ujesz do niego plik .h / .hpp, który zawiera wyłącznie definicje funkcji / zmiennych, jedynie powiadamiasz kompilator, że "gdzieś tam będzie istnieć funkcja X / zmienna globalna Z", kompilator tworzy plik obiektowy "wołający" o te symbole, linker to potem łączy, wszystko ładnie. Jeżeli jednak w pliku .h / .hpp masz też implementację funkcji, to rezultat jest taki sam, jakbyś tę funkcję copy-paste'nął - będzie ona istnieć zarówno w obiekcie AAA.o, jak i BBB.o. Ponieważ ten sama nazwa będzie eksportowana przez kilka plików obiektowych jednocześnie, linker nie będzie w stanie rozwiązać zależności i program się nie zbuduje.

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