Gra Saper w wersji izometrycznej w C++ i SDL2
Projekt
Od czasu do czasu robię sobie mały hackaton, polegający na napisaniu jakiejś prostej gry. Na blogu opisywałem już Catapult Moonshot napisany na GameJam a także grę w kółko i krzyżyk w widoku izometrycznym. Tym razem wziąłem na warsztat klasyczną grę Saper, w oryginale zwaną Minesweeper. Jako punkt wyjścia obrałem podobne rozwiązanie jak w przypadku gry w kółko i krzyżyk, czyli język C++, bibliotekę SDL2 oraz widok izometryczny.
W ciągu jednego popołudnia udało mi się napisać grywalną grę w uproszczonej wersji, bez stawiania flag prawym klawiszem myszy oraz bez odkrywania pól poprzez kliknięcie obu klawiszy myszy, gra również nie jest ograniczona czasowo.
Wyzwania
To co jest najciekawsze w tego rodzaju projektach to możliwość zmierzenia się z zagadnieniami, które znamy od strony końcowego rozwiązania a nie zawsze od strony kodu. Jak się okazało, implementacja gry w sapera stawia kilka ciekawych problemów do rozwiązania.
Nietrafianie na minę w pierwszym ruchu
Gra byłaby irytująca, gdyby już po pierwszym odsłonięciu pola gracz przegrywał, należy zatem zapewnić, że w pierwszym ruchu nigdy nie trafiamy na minę. W moim kodzie osiągnąłem to poprzez generowanie zawartości pola minowego po kliknięciu w pierwsze pole, nie wcześniej, dzięki temu wiem w jakim polu nie możemy postawić miny i pomijam to pole w generatorze. Sam generator to prosta pętla z losowaniem współrzędnych i sprawdzaniem czy w danym polu jeszcze nie umieszczono miny.
// Place mines int minesLeftToPlace = numberOfMines; while (minesLeftToPlace > 0) { int x = rand() % columns; int y = rand() % rows; if (minefield[y][x] != mineMarker && x != clickedField->x && y != clickedField->y) { minefield[y][x] = mineMarker; minesLeftToPlace--; } }
Odkrywanie pustych pól
Kiedy gracz klika w pole wokół którego nie ma żadnych min, odkrywa się puste pole, bez żadnej cyfry. Jednocześnie odkrywają się wszystkie puste pola otaczające to pole, oraz wszystkie pola z cyframi wokół wszystkich tych pustych pól które zostały odsłonięte. Tutaj algorytm wymagał nieco zastanowienia, rozwiązałem go następująco:
- Zaimplementowałem pętlę, która wykonuje się dopóki każdy jej przebieg odsłonił jakiekolwiek pole
- W pętli odsłaniam wszystkie pola które są puste i które sąsiadują z odsłoniętym polem
- Po odsłonięciu każdego pustego pola odkrywam wszystkie pola z numerami wokół niego.
Przykład
x to miny, 0 to puste pola, cyfry 1-8 to pola z cyframi
0 0 1 x x 1 0 0 0 0 1 3 3 3 1 1 0 0 0 1 x 2 x 1 0 1 1 2 1 2 1 1 0 1 x 1 0 0 1 1 0 1 2 2 1 0 2 x 0 0 2 x 3 1 3 x 0 0 2 x 3 x 2 1
Jeśli gracz kliknie w puste pole w pozycji 3×3 (trzecia kolumna, trzeci wiersz) to w kolejnych przebiegach pętli odsłonięte zostaną:
- Pole 2×3 (i przyległe cyfry) ponieważ sąsiaduje z klikniętym polem
- Pola 2×2, 1×3 (i przyległe cyfry) ponieważ sąsiadują z polem z poprzedniego punktu
- Pola 2×1, 1×2, 1×4 (i przyległe cyfry) ponieważ sąsiadują z polami z poprzedniego punktu
- (i tak dalej aż w którymś z kolei przebiegu pętli nie zostanie odkryte żadne pole)
W rezultacie, po zakończeniu pętli maska zasłaniająca pole minowe wygląda następująco:
0 0 0 1 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1 0 0 1 1 1 1 1 1 0 0 0 1 1 1 1 1 0 0 0 1 1 1 1 1 0 0 0 1 1 1 1 1
0 to odkryte pola, 1 to zakryte.
Natomiast w widoku gry wygląda to tak:
Struktura danych
Jak już zdradziłem w poprzednim punkcie, mapa gry jest zapisywana w dwuwymiarowej tablicy w formacie:
2 2 1 1 x 2 2 x x x 1 2 2 3 x 2 2 2 1 1 x 2 1 1 0 1 1 2 1 1 0 0 0 1 x 1 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 2 1 2 1 1 0 1 x 2 x 2 x 1
Zapisywana jest również maska określająca, które pola są odkryte, a które nie.
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1
Obie tablice przekazywane są do klasy Board, która przy pomocy biblioteki SDL2 rysuje widok gry.
Stan gry
Gra posiada cztery główne stany:
- Gra zresetowana – zawartość mapy nie jest jeszcze wygenerowana, czekamy na pierwsze kliknięcie
- W trakcie gry – mapa wygenerowana, odkryte co najmniej jedno pole
- Gra zakończona wygraną – kliknięcie resetuje grę
- Gra zakończona przegraną- kliknięcie resetuje grę
Maszyna stanów jest prosta, możliwe przejścia to oczywiście:
- 1 -> 2 -> 3 -> 1
- 1 -> 2 -> 4 -> 1
Dalszy rozwój
Grę staram się refaktoryzować i rozwijać. Jeśli chciałbyś dołożyć do niego coś od siebie, zapraszam do wystawiania Pull Requestów na GitHub, gdzie również dodam zadania dotyczące kolejnych usprawnień o których wspomniałem na początku artykułu.
Kod gry
Kod gry udostępniłem w tym repozytorium GitHub, zapraszam do kontrybucji.
Skomentuj