21.y — Projekt rozdziału 21

Ukłon dla czytelnika Avtem za pomysł i współpracę przy tym projekcie.

Projekt czas

Zaimplementujmy klasyczną grę 15 Puzzle!

W 15 Puzzle zaczynasz od losowej siatki płytek 4×4. 15 płytek ma numery od 1 do 15. Brakuje jednej płytki.

Na przykład:

     15   1   4
  2   5   9  12
  7   8  11  14
 10  13   6   3

W tej łamigłówce brakująca płytka znajduje się w lewym górnym rogu.

W każdej turze gry wybierasz jedną z płytek sąsiadującą z brakującą płytką i wsuwasz ją w miejsce zajmowane przez brakującą płytkę.

Celem gry jest przesuwanie płytek, aż ułożą się w kolejności numerycznej, z brakującą płytką kafelek w prawym dolnym rogu:

  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15   

Możesz rozegrać kilka rund na tej stronie. Pomoże Ci to zrozumieć, jak działa ta gra i jak należy ją wdrożyć.

W naszej wersji gry w każdej turze użytkownik będzie wprowadzał pojedynczą literę polecenia. Istnieje 5 prawidłowych poleceń:

  • w - przesuń płytkę w górę
  • a - przesuń płytkę w lewo
  • s - przesuń płytkę w dół
  • d - przesuń płytkę w prawo
  • q - wyjdź z gry

Ponieważ to będzie dłuższy program, będziemy go rozwijać etapami.

Jeszcze jedno rzecz: na każdym kroku przedstawimy dwie rzeczy: cel i zadania. Cel określa wynik, który ma zostać osiągnięty na danym etapie, wraz z wszelkimi dodatkowymi istotnymi informacjami. Zadania zawierają szczegółowe informacje i wskazówki dotyczące realizacji celu.

Zadania będą początkowo ukryte, aby zachęcić Cię do sprawdzenia, czy możesz ukończyć każdy krok, korzystając wyłącznie z celu i przykładowych wyników lub przykładowego programu. Jeśli nie masz pewności, jak zacząć lub czujesz, że utknąłeś, możesz odkryć zadania. Powinny pomóc Ci w dalszym rozwoju.

> Krok #1

Ponieważ będzie to większy program, zacznijmy od ćwiczenia projektowego.

Nota autora

Jeśli nie masz dużego doświadczenia w projektowaniu programów od początku, może to być nieco trudne. Tego można się spodziewać. Mniej ważne jest, abyś zrobił to dobrze, a ważniejsze, abyś brał udział i uczył się.

W kolejnych krokach omówimy wszystkie te elementy bardziej szczegółowo, więc jeśli czujesz się całkowicie zagubiony, możesz pominąć ten krok.

Cel: Udokumentuj kluczowe wymagania tego programu i zaplanuj, jak Twój program będzie zorganizowany na wysokim poziomie. Zrobimy to w trzech częściach.

A) Jakie najważniejsze elementy musi wykonać Twój program? Oto kilka wskazówek na początek:

Elementy planszy:

  • Wyświetl planszę
  • …

Rzeczy użytkownika:

  • Uzyskaj polecenia od użytkownika
  • …

Pokaż rozwiązanie

B) Co podstawowe klas lub przestrzeni nazw będziesz używać do implementowania elementów opisanych w kroku 1? Co też zrobi twoja funkcja main()?

Możesz utworzyć diagram lub użyć dwóch takich tabel:

Klasa podstawowa/przestrzeń nazw/mainImplementuje elementy najwyższego poziomuCzłonkowie
Plansza klasyWyświetl planszę
…
…
funkcję mainGłówna pętla logiczna gry
…
…

Pokaż rozwiązanie

C) (dodatkowe punkty) Czy przychodzą Ci do głowy jakieś klasy pomocnicze lub możliwości, dzięki którym wdrożenie powyższego będzie łatwiejsze lub bardziej spójne?

Pokaż rozwiązanie

Jeśli miałeś trudności z tym ćwiczeniem, nie ma problemu. Celem było głównie skłonienie Cię do zastanowienia się nad tym, co zamierzasz zrobić, zanim zaczniesz to robić.

Teraz czas na wdrożenie!

> Krok #2

Cel: Być w stanie wyświetlić poszczególne kafelki na ekranie.

Nasza plansza do gry to siatka płytek 4×4, które można przesuwać. Dlatego przydatne będzie posiadanie Tile klasa reprezentująca jedną z ponumerowanych płytek na siatce 4×4 lub brakującą płytkę. Każda płytka powinna umożliwiać:

  • Otrzymasz numer lub zostaniesz ustawiony jako brakujący kafelek
  • Ustal, czy jest to brakująca płytka.
  • Rysuj do konsoli z zachowaniem odpowiednich odstępów (aby kafelki ułożyły się w jednej linii po wyświetleniu planszy). Zobacz przykładowe dane wyjściowe poniżej, aby zapoznać się z przykładem wskazującym, jak powinny być rozmieszczone płytki.

Pokaż zadania

Następujący kod powinien się skompilować i wygenerować wynik widoczny poniżej:

int main()
{
    Tile tile1{ 10 };
    Tile tile2{ 8 };
    Tile tile3{ 0 }; // brakująca płytka
    Tile tile4{ 1 };

    std::cout << "0123456789ABCDEF\n"; // aby ułatwić sprawdzenie, ile spacji znajduje się w następnym wierszu
    std::cout << tile1 << tile2 << tile3 << tile4 << '\n';
    
    std::cout << std::boolalpha << tile1.isEmpty() << ' ' << tile3.isEmpty() << '\n';
    std::cout << "Tile 2 has number: " << tile2.getNum() << "\nTile 4 has number: " << tile4.getNum() << '\n';
    
    return 0;
}

Oczekiwany wynik (zwróć uwagę na białe spacje):

0123456789ABCDEF
 10   8       1 
false true
Tile 2 has number: 8
Tile 4 has number: 1

Pokaż rozwiązanie

> Krok #3

Cel: Utwórz ułożoną planszę (siatka płytek 4×4) i wyświetl ją na ekranie.

Zdefiniuj Board klasę, która będzie reprezentować siatkę płytek 4×4. Nowo utworzony Board obiekt powinien być w stanie rozwiązanym. Aby wyświetlić tablicę, najpierw wydrukuj g_consoleLines (zdefiniowane we fragmencie kodu poniżej) puste linie, a następnie wydrukuj samą tablicę. Dzięki temu wszelkie wcześniejsze dane wyjściowe zostaną usunięte z pola widzenia i na konsoli będzie widoczna tylko bieżąca płytka.

Po co inicjować płytkę w stanie rozwiązanym? Kiedy kupujesz fizyczną wersję tych łamigłówek, zazwyczaj zaczynają się one od ułożonego stanu — musisz je ręcznie wymieszać (przesuwając kafelki), zanim spróbujesz je ułożyć. Będziemy naśladować ten proces w naszym programie (w następnym kroku zajmiemy się mieszaniem). Klasa

Pokaż zadania

Powinien uruchomić się następujący program:

// Zwiększ liczbę nowych linii, jeśli twoja tablica nie jest
// na samym konsoli
constexpr int g_consoleLines{ 25 };

// Twój kod będzie tutaj

int main()
{
    Board board{};
    std::cout << board;

    return 0;
}

i wypisze następujący komunikat:


























  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15

Pokaż rozwiązanie

> Krok 4

Cel: W tym kroku umożliwimy użytkownikowi wielokrotne wprowadzanie gry polecenia, obsłuż nieprawidłowe dane wejściowe i zaimplementuj polecenie zakończenia gry.

Oto 5 poleceń, które będzie obsługiwała nasza gra (z których każde zostanie wprowadzone jako pojedynczy znak):

  • „w” - przesuń płytkę w górę
  • „a” - przesuń płytkę w lewo
  • 's' - przesuń płytkę w dół
  • „d” - przesuń płytkę prawo
  • „q” - wyjście z gry

Gdy użytkownik uruchomi grę, powinna nastąpić następująca sytuacja:

  • (Rozwiązana) plansza powinna zostać wydrukowana na konsoli.
  • Program powinien wielokrotnie otrzymywać od użytkownika prawidłowe polecenia gry. Jeśli użytkownik wprowadzi nieprawidłowe polecenie lub dodatkowe dane, zignoruj je.

Dla każdego prawidłowego polecenia gry:

  • Wydrukuj "Valid command: " oraz znak wprowadzony przez użytkownika.
  • Jeśli poleceniem jest polecenie zakończenia, wydrukuj również "\n\nBye!\n\n" a następnie zamknij aplikację.

Ponieważ nasze procedury wprowadzania danych przez użytkownika nie muszą utrzymywać żadnych stanu, zaimplementuj je w przestrzeni nazw o nazwie UserInput.

Pokaż zadania

Wyjście programu powinno być zgodne z następującymi:


























  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15
w
Valid command: w
a
Valid command: a
s
Valid command: s
d
Valid command: d
f
g
h
Valid command: q


Bye!


Pokaż rozwiązanie

> Krok #5

Cel: Zaimplementuj klasę pomocniczą, która ułatwi nam obsługę poleceń kierunkowych.

Po wykonaniu poprzedniego kroku możemy akceptować polecenia od użytkownika (w postaci znaków „w”, „a”, „s”, „d” i „q”). Znaki te są w zasadzie magicznymi liczbami w naszym kodzie. Chociaż obsługa tych poleceń w naszej UserInput przestrzeni nazw i funkcji main() jest w porządku, nie chcemy propagować ich w całym programie. Na przykład klasa Board nie powinna wiedzieć, co oznacza „s”.

Zaimplementuj klasę pomocniczą o nazwie Direction, która pozwoli nam tworzyć obiekty reprezentujące kierunki kardynalne (w górę, w lewo, w dół lub w prawo). operator- powinna zwracać kierunek przeciwny i operator<< powinna wypisać kierunek do konsoli. Będziemy także potrzebować funkcji członkowskiej, która zwróci obiekt Direction zawierający losowy kierunek. Na koniec dodaj funkcję do przestrzeni nazw UserInput , która konwertuje kierunkowe polecenie gry („w”, „a”, „s” lub „d”) na obiekt Direction.

Im częściej możemy używać Direction zamiast kierunkowych poleceń gry, tym łatwiej będzie odczytać i zrozumieć nasz kod.

Pokaż zadania

Na koniec zmodyfikuj program, który napisałeś w poprzednim kroku, tak aby dane wyjściowe były zgodne z następującymi:


























  1   2   3   4
  5   6   7   8
  9  10  11  12
 13  14  15
Generating random direction... up
Generating random direction... down
Generating random direction... up
Generating random direction... left

Enter a command: w
You entered direction: up
a
You entered direction: left
s
You entered direction: down
d
You entered direction: right
q


Bye!


Pokaż rozwiązanie

> Krok 6

Cel: Zaimplementuj klasę pomocniczą, która ułatwi nam indeksowanie płytek na naszej planszy.

Nasza plansza to siatka 4×4 Tile, którą przechowujemy w dwuwymiarowej tablicy członek m_tiles klasy Board . Do danego kafelka dostaniemy się korzystając z jego współrzędnych {x, y}. Na przykład lewy górny kafelek ma współrzędne {0, 0}. Płytka po prawej stronie ma współrzędne {1, 0} (x staje się 1, y pozostaje 0). Kafelek niżej ma współrzędne {1, 1}.

Ponieważ będziemy dużo pracować ze współrzędnymi, utwórz klasę pomocniczą o nazwie Point , która przechowuje parę współrzędnych {x, y}. Powinniśmy być w stanie porównać dwa obiekty Point pod kątem równości i nierówności. Zaimplementuj także funkcję składową o nazwie getAdjacentPoint , która przyjmuje obiekt Direction jako parametr i zwraca Point w tym kierunku. Na przykład Point{1, 1}.getAdjacentPoint(Direction::right) == Point{2, 1}.

Pokaż zadania

Zapisz main() funkcję z poprzedniego kroku, ponieważ będziesz jej potrzebować ponownie w następnym kroku.

Następujący kod powinien zostać uruchomiony i wydrukowany true dla każdego przypadek testowy:

// Twój kod będzie tutaj

// Uwaga: zapisz funkcję main() z poprzedniego kroku, ponieważ będzie ona potrzebna ponownie w następnym kroku
int main()
{
    std::cout << std::boolalpha;
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::up)    == Point{ 1, 0 }) << '\n';
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::down)  == Point{ 1, 2 }) << '\n';
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::left)  == Point{ 0, 1 }) << '\n';
    std::cout << (Point{ 1, 1 }.getAdjacentPoint(Direction::right) == Point{ 2, 1 }) << '\n';
    std::cout << (Point{ 1, 1 } != Point{ 2, 1 }) << '\n';
    std::cout << (Point{ 1, 1 } != Point{ 1, 2 }) << '\n';
    std::cout << !(Point{ 1, 1 } != Point{ 1, 1 }) << '\n';

    return 0;
}

Pokaż rozwiązanie

> Krok 7

Cel: Dodaj możliwość przesuwania płytek po planszy przez graczy.

Najpierw powinniśmy przyjrzeć się bliżej, jak faktycznie działa przesuwanie płytek:

Biorąc pod uwagę stan układanki wyglądający tak:

     15   1   4
  2   5   9  12
  7   8  11  14
 10  13   6   3

Kiedy użytkownik wprowadzi „w” na klawiaturze, jedyną płytką, która może się podnieść, jest kafelek 2.

Po przesunięciu płytki plansza wygląda następująco:

  2  15   1   4
      5   9  12
  7   8  11  14
 10  13   6   3

A więc zasadniczo stało się tak, że zamieniliśmy pustą płytkę na płytkę 2.

Uogólnijmy tę procedurę. Kiedy użytkownik wprowadzi polecenie kierunkowe, musimy:

  • Zlokalizować pustą płytkę.
  • Z pustej płytki znaleźć sąsiadującą płytkę, która jest w kierunku przeciwnym do kierunku wprowadzonego przez użytkownika.
  • Jeśli sąsiednia płytka jest prawidłowa (nie jest poza siatką), zamień pustą płytkę z sąsiednią płytką.
  • Jeśli sąsiednia płytka jest nieprawidłowa, wykonaj nic.

Zaimplementuj to, dodając funkcję składową moveTile(Direction) do klasy Board. Dodaj to do swojej pętli gry z kroku 5. Jeśli użytkownikowi pomyślnie przesuniesz płytkę, gra powinna przerysować zaktualizowaną planszę.

Pokaż zadania

Pokaż rozwiązanie

> Krok #8

Cel: W tym kroku zakończymy naszą grę. Losuj początkowy stan planszy. Wykryj także, kiedy użytkownik wygra, abyśmy mogli wydrukować wiadomość o wygranej i zakończyć grę.

Musimy uważać, jak losowo dobieramy losowość naszej łamigłówki, ponieważ nie każdą łamigłówkę da się ułożyć. Na przykład nie ma sposobu na rozwiązanie tej zagadki:

  1   2   3   4 
  5   6   7   8
  9  10  11  12
 13  15  14

Jeśli po prostu na ślepo losujemy liczby w łamigłówce, jest szansa, że ​​wygenerujemy taką nierozwiązywalną łamigłówkę. W przypadku fizycznej wersji łamigłówki losowaliśmy łamigłówkę, przesuwając kafelki w przypadkowych kierunkach, aż do ich wystarczającego wymieszania. Rozwiązaniem takiej losowej łamigłówki jest przesunięcie każdej płytki w przeciwnym kierunku, w jakim została przesunięta, aby w pierwszej kolejności ją losować. Zatem losowanie łamigłówek w ten sposób zawsze generuje łamigłówkę dającą się rozwiązać.

Możemy pozwolić naszemu programowi losować planszę w ten sam sposób.

Gdy użytkownik rozwiąże łamigłówkę, program powinien wydrukować "\n\nYou won!\n\n" a następnie normalnie zakończyć.

Pokaż zadania

Oto pełne rozwiązanie naszej 15-łamigłówki:

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
153 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze