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:

  1. Zaimplementowałem pętlę, która wykonuje się dopóki każdy jej przebieg odsłonił jakiekolwiek pole
  2. W pętli odsłaniam wszystkie pola które są puste i które sąsiadują z odsłoniętym polem
  3. 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ą:

  1. Pole 2×3 (i przyległe cyfry) ponieważ sąsiaduje z klikniętym polem
  2. Pola 2×2, 1×3 (i przyległe cyfry) ponieważ sąsiadują z polem z poprzedniego punktu
  3. Pola 2×1, 1×2, 1×4 (i przyległe cyfry) ponieważ sąsiadują z polami z poprzedniego punktu
  4. (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:

  1. Gra zresetowana – zawartość mapy nie jest jeszcze wygenerowana, czekamy na pierwsze kliknięcie
  2. W trakcie gry – mapa wygenerowana, odkryte co najmniej jedno pole
  3. Gra zakończona wygraną – kliknięcie resetuje grę
  4. 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.

 

Udostępnij

Skomentuj