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 }; // the missing tile
    Tile tile4{ 1 };

    std::cout << "0123456789ABCDEF\n"; // to make it easy to see how many spaces are in the next line
    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:

// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };

// Your code goes here

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:

// Your code goes here

// Note: save your main() from the prior step, as you'll need it again in the next step
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