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:
Rzeczy użytkownika:
- Uzyskaj polecenia od użytkownika
- …
Pokaż rozwiązanie
Elementy planszy:
- Wyświetl planszę
- Wyświetl pojedynczy kafelek
- Losuj stan początkowy
- Przesuń płytki
- Określ, czy warunek wygranej został osiągnięty
Rzeczy użytkownika:
- Uzyskaj polecenia od użytkownika
- Obsłuż nieprawidłowe dane wejściowe
- Pozwól użytkownikowi wyjść przed wygraną
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/main | Implementuje elementy najwyższego poziomu | Członkowie |
|---|
| Plansza klasy | Wyświetl planszę … | … |
| funkcję main | Główna pętla logiczna gry … | … |
Pokaż rozwiązanie
| Klasa podstawowa/Przestrzeń nazw/główna | Implementuje elementy najwyższego poziomu | Członkowie (typ) |
|---|
| Plansza klasy | Wyświetl planszę Losuj stan początkowy Przesuń płytki Określ, czy warunek wygranej został osiągnięty | 2d tablica płytek |
| Płytka klasy | Wyświetl pojedynczy kafelek | int numer wyświetlacza |
| Dane wejściowe użytkownika przestrzeni nazw | Uzyskaj polecenia od użytkownika Obsłuż nieprawidłowe dane wejściowe | nic |
| funkcja główna() | Główna pętla logiczna gry Pozwól użytkownikowi wyjść przed wygraną | nic |
Oto uzasadnienie powyższych wyborów.
class Board: Nasza gra to siatka płytek 4×4. Głównym celem tej klasy jest przechowywanie i zarządzanie dwuwymiarową tablicą płytek. Klasa ta odpowiada również za losowanie, przesuwanie płytek, eksponowanie planszy i sprawdzanie, czy nasza plansza jest ułożona.class Tile: Ta klasa reprezentuje pojedynczą płytkę na planszy. Użycie tutaj klasy pozwala nam przeciążyć operator wyjścia, aby wyprowadzić kafelek w żądanym formacie. Pozwala nam również mieć dobrze nazwane funkcje członkowskie, które zwiększą czytelność kodu związanego z pojedynczym kafelkiem.namespace UserInput: Ta przestrzeń nazw zawiera funkcje umożliwiające uzyskanie danych wejściowych od użytkownika, sprawdzenie, czy dane wprowadzone przez użytkownika są prawidłowe oraz obsługę nieprawidłowych danych wejściowych. Ponieważ nie ma to żadnego stanu, nie potrzebujemy tutaj klasy.function main(): Tutaj zostanie napisana główna pętla gry. Zajmie się to konfiguracją planszy do gry, koordynacją pobierania danych wejściowych użytkownika i przetwarzaniem poleceń oraz obsługą warunków wyjścia (kiedy użytkownik wygra lub wprowadzi polecenie zakończenia).
Wykorzystamy także dwie klasy pomocnicze. Potrzeba takich zajęć może nie być oczywista na pierwszy rzut oka, więc nie martw się, jeśli nie wpadłeś na nic podobnego. Często potrzeba (lub korzyści) klas pomocniczych nie jest oczywista, gdy posuwasz się dalej w realizację swojego programu.
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
| Klasa pomocnicza/przestrzeń nazw | Na co to pomaga? | Członkowie (typ) |
|---|
| Punkt klasowy | Indeksowanie płytek planszy do gry | int współrzędne osi X i Y |
| Kierunek klasy | Spraw, aby praca z poleceniami kierunkowymi była łatwiejsza i bardziej intuicyjna | kierunek wyliczeniowy |
class Point: Dostęp do konkretnego kafelka w naszej dwuwymiarowej tablicy kafelków będzie wymagał 2 indeksów. Możemy o nich myśleć jak o parach indeksów {oś x, oś y}. Ta klasa Point implementuje taką parę indeksów, aby ułatwić przekazywanie lub zwracanie pary indeksów.class Direction: użytkownik będzie wprowadzał na klawiaturze polecenia jednoliterowe (znakowe), aby przesuwać kafelki w kierunkach kardynalnych (np. 'w'=w górę, 'a'=po lewej). Konwersja tych poleceń char na obiekt Direction (reprezentujący kierunek kardynalny) sprawi, że nasz kod będzie bardziej intuicyjny i zapobiegnie zaśmiecaniu go literałami znaków (Direction::left ma większe znaczenie niż 'a').
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
Nasz Tile klasa powinna mieć tę funkcjonalność:
- Domyślny konstruktor.
- Konstruktor pozwalający nam utworzyć Tile z wyświetlaną wartością. Ponieważ nie używamy
0 jako wartości wyświetlanej, możemy użyć wartości 0 w celu zidentyfikowania brakującego kafelka. - A
getNum() funkcja dostępu, która zwraca wartość przechowywaną w kafelku. - An
isEmpty() funkcja składowa, która zwraca wartość logiczną wskazującą, czy bieżący kafelek jest brakującym kafelkiem. - Przeciążony
operator<< która wyświetli wartość przechowywaną przez kafelek.
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
#include <iostream>
class Tile
{
public:
Tile() = default;
explicit Tile(int number)
:m_num(number)
{
}
bool isEmpty() const
{
return m_num == 0;
}
int getNum() const { return m_num; }
private:
int m_num { 0 };
};
std::ostream& operator<<(std::ostream& stream, Tile tile)
{
if (tile.getNum() > 9) // if two digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() > 0) // if one digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() == 0) // if empty spot
stream << " ";
return stream;
}
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;
}
> 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
Klasa Board powinna posiadać następującą funkcjonalność:
- A
constexpr stała symboliczna, ustawiana na wielkość siatki (można założyć, że siatka jest kwadratowa). - Dwuwymiarowa tablica
Tile obiektów, w których będą znajdować się nasze 16 liczb. Powinny one zaczynać się od rozwiązanego stanu. - Domyślny konstruktor.
- Przeciążony
operator<< który wypisze N pustych linii (gdzie N = wartość g_consoleLines), a następnie przeciągnie planszę do konsoli.
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
#include <iostream>
// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };
class Tile
{
public:
Tile() = default;
explicit Tile(int number)
:m_num(number)
{
}
bool isEmpty() const
{
return m_num == 0;
}
int getNum() const { return m_num; }
private:
int m_num { 0 };
};
std::ostream& operator<<(std::ostream& stream, Tile tile)
{
if (tile.getNum() > 9) // if two digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() > 0) // if one digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() == 0) // if empty spot
stream << " ";
return stream;
}
class Board
{
public:
Board() = default;
friend std::ostream& operator<<(std::ostream& stream, const Board& board)
{
// Before drawing always print some empty lines
// so that only one board appears at a time
// and it's always shown at the bottom of the window
// because console window scrolls automatically when there is no
// enough space.
for (int i = 0; i < g_consoleLines; ++i)
std::cout << '\n';
for (int y = 0; y < s_size; ++y)
{
for (int x = 0; x < s_size; ++x)
stream << board.m_tiles[y][x];
stream << '\n';
}
return stream;
}
private:
static constexpr int s_size { 4 };
Tile m_tiles[s_size][s_size]{
Tile{ 1 }, Tile { 2 }, Tile { 3 } , Tile { 4 },
Tile { 5 } , Tile { 6 }, Tile { 7 }, Tile { 8 },
Tile { 9 }, Tile { 10 }, Tile { 11 }, Tile { 12 },
Tile { 13 }, Tile { 14 }, Tile { 15 }, Tile { 0 } };
};
int main()
{
Board board{};
std::cout << board;
return 0;
}
> 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
Zaimplementuj UserInput przestrzeń nazw:
- Utwórz funkcję o nazwie
getCommandFromUser(). Przeczytaj pojedynczy znak użytkownika. Jeśli postać nie jest prawidłowym poleceniem gry, usuń wszelkie dodatkowe, zewnętrzne dane wejściowe i wczytaj inny znak od użytkownika. Powtarzaj, aż zostanie wprowadzone prawidłowe polecenie gry. Zwróć prawidłowe polecenie wywołującemu. - Utwórz tyle funkcji pomocniczych, ile potrzebujesz.
In main():
- Zaimplementuj nieskończoną pętlę. Wewnątrz pętli pobierz prawidłowe polecenie gry, a następnie obsłuż je zgodnie z powyższymi wymaganiami.
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
#include <iostream>
#include <limits>
// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };
namespace UserInput
{
bool isValidCommand(char ch)
{
return ch == 'w'
|| ch == 'a'
|| ch == 's'
|| ch == 'd'
|| ch == 'q';
}
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
char getCharacter()
{
char operation{};
std::cin >> operation;
ignoreLine(); // remove any extraneous input
return operation;
}
char getCommandFromUser()
{
char ch{};
while (!isValidCommand(ch))
ch = getCharacter();
return ch;
}
};
class Tile
{
public:
Tile() = default;
explicit Tile(int number)
:m_num(number)
{
}
bool isEmpty() const
{
return m_num == 0;
}
int getNum() const { return m_num; }
private:
int m_num { 0 };
};
std::ostream& operator<<(std::ostream& stream, Tile tile)
{
if (tile.getNum() > 9) // if two digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() > 0) // if one digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() == 0) // if empty spot
stream << " ";
return stream;
}
class Board
{
public:
Board() = default;
friend std::ostream& operator<<(std::ostream& stream, const Board& board)
{
// Before drawing always print some empty lines
// so that only one board appears at a time
// and it's always shown at the bottom of the window
// because console window scrolls automatically when there is no
// enough space.
for (int i = 0; i < g_consoleLines; ++i)
std::cout << '\n';
for (int y = 0; y < s_size; ++y)
{
for (int x = 0; x < s_size; ++x)
stream << board.m_tiles[y][x];
stream << '\n';
}
return stream;
}
private:
static constexpr int s_size { 4 };
Tile m_tiles[s_size][s_size]{
Tile{ 1 }, Tile { 2 }, Tile { 3 } , Tile { 4 },
Tile { 5 } , Tile { 6 }, Tile { 7 }, Tile { 8 },
Tile { 9 }, Tile { 10 }, Tile { 11 }, Tile { 12 },
Tile { 13 }, Tile { 14 }, Tile { 15 }, Tile { 0 } };
};
int main()
{
Board board{};
std::cout << board;
while (true)
{
char ch{ UserInput::getCommandFromUser() };
// If we reach the line below, "ch" will ALWAYS be a correct command!
std::cout << "Valid command: " << ch << '\n';
// Handle non-direction commands
if (ch == 'q')
{
std::cout << "\n\nBye!\n\n";
return 0;
}
}
return 0;
}
> 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
Zaimplementuj klasa Direction, która zawiera:
- Publiczne zagnieżdżone wyliczenie o nazwie
Type z modułami wyliczającymi up, down, left, right, I maxDirections. - Prywatny element przechowujący rzeczywisty kierunek.
- Konstruktor z jednym argumentem, który pozwala nam zainicjować
Direction z Type inicjator. - Przeciążony
operator-, który przyjmuje kierunek i zwraca przeciwny kierunek. - Przeciążony
operator<<, który wysyła nazwę kierunku do konsoli. - Funkcja statyczna, która zwraca kierunek z losową
Type. Możesz użyć funkcji Random::get() z nagłówka „Random.h” , aby wygenerować liczbę losową.
Ponadto w przestrzeni nazw UserInput dodaj następujące polecenie:
- Funkcja, która przekonwertuje kierunkowe polecenie gry (znak) na Kierunek obiekt.
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
#include <cassert>
#include <iostream>
#include <limits>
#include "Random.h"
// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };
class Direction
{
public:
enum Type
{
up,
down,
left,
right,
maxDirections,
};
Direction(Type type)
:m_type(type)
{
}
Type getType() const
{
return m_type;
}
Direction operator-() const
{
switch (m_type)
{
case up: return Direction{ down };
case down: return Direction{ up };
case left: return Direction{ right };
case right: return Direction{ left };
default: break;
}
assert(0 && "Unsupported direction was passed!");
return Direction{ up };
}
static Direction getRandomDirection()
{
Type random{ static_cast<Type>(Random::get(0, Type::maxDirections - 1)) };
return Direction{ random };
}
private:
Type m_type{};
};
std::ostream& operator<<(std::ostream& stream, Direction dir)
{
switch (dir.getType())
{
case Direction::up: return (stream << "up");
case Direction::down: return (stream << "down");
case Direction::left: return (stream << "left");
case Direction::right: return (stream << "right");
default: break;
}
assert(0 && "Unsupported direction was passed!");
return (stream << "unknown direction");
}
namespace UserInput
{
bool isValidCommand(char ch)
{
return ch == 'w'
|| ch == 'a'
|| ch == 's'
|| ch == 'd'
|| ch == 'q';
}
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
char getCharacter()
{
char operation{};
std::cin >> operation;
ignoreLine(); // remove any extraneous input
return operation;
}
char getCommandFromUser()
{
char ch{};
while (!isValidCommand(ch))
ch = getCharacter();
return ch;
}
Direction charToDirection(char ch)
{
switch (ch)
{
case 'w': return Direction{ Direction::up };
case 's': return Direction{ Direction::down };
case 'a': return Direction{ Direction::left };
case 'd': return Direction{ Direction::right };
}
assert(0 && "Unsupported direction was passed!");
return Direction{ Direction::up };
}
};
class Tile
{
public:
Tile() = default;
explicit Tile(int number)
:m_num(number)
{
}
bool isEmpty() const
{
return m_num == 0;
}
int getNum() const { return m_num; }
private:
int m_num { 0 };
};
std::ostream& operator<<(std::ostream& stream, Tile tile)
{
if (tile.getNum() > 9) // if two digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() > 0) // if one digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() == 0) // if empty spot
stream << " ";
return stream;
}
class Board
{
public:
Board() = default;
static void printEmptyLines(int count)
{
for (int i = 0; i < count; ++i)
std::cout << '\n';
}
friend std::ostream& operator<<(std::ostream& stream, const Board& board)
{
// Before drawing always print some empty lines
// so that only one board appears at a time
// and it's always shown at the bottom of the window
// because console window scrolls automatically when there is no
// enough space.
for (int i = 0; i < g_consoleLines; ++i)
std::cout << '\n';
for (int y = 0; y < s_size; ++y)
{
for (int x = 0; x < s_size; ++x)
stream << board.m_tiles[y][x];
stream << '\n';
}
return stream;
}
private:
static constexpr int s_size { 4 };
Tile m_tiles[s_size][s_size]{
Tile{ 1 }, Tile { 2 }, Tile { 3 } , Tile { 4 },
Tile { 5 } , Tile { 6 }, Tile { 7 }, Tile { 8 },
Tile { 9 }, Tile { 10 }, Tile { 11 }, Tile { 12 },
Tile { 13 }, Tile { 14 }, Tile { 15 }, Tile { 0 } };
};
int main()
{
Board board{};
std::cout << board;
std::cout << "Generating random direction... " << Direction::getRandomDirection() << '\n';
std::cout << "Generating random direction... " << Direction::getRandomDirection() << '\n';
std::cout << "Generating random direction... " << Direction::getRandomDirection() << '\n';
std::cout << "Generating random direction... " << Direction::getRandomDirection() << "\n\n";
std::cout << "Enter a command: ";
while (true)
{
char ch{ UserInput::getCommandFromUser() };
// Handle non-direction commands
if (ch == 'q')
{
std::cout << "\n\nBye!\n\n";
return 0;
}
// Handle direction commands
Direction dir{ UserInput::charToDirection(ch) };
std::cout << "You entered direction: " << dir << '\n';
}
return 0;
}
> 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
Zaimplementuj strukturę o nazwie Point. Powinno to zawierać:
- Dwa publiczne elementy danych do przechowywania współrzędnych osi x i y.
- Przeciążony
operator== i operator!= do porównywania dwóch zestawów współrzędnych. - Funkcja elementu stałego
Point getAdjacentPoint(Direction) , która zwraca Punkt w kierunku parametru Kierunek. Nie musimy tutaj przeprowadzać żadnego sprawdzania poprawności.
Używamy tutaj struktury zamiast klasy, ponieważ Point to prosty pakiet danych, który niewiele by zyskał na enkapsulacji.
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
#include <array>
#include <cassert>
#include <iostream>
#include <limits>
#include "Random.h"
// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };
class Direction
{
public:
enum Type
{
up,
down,
left,
right,
maxDirections,
};
Direction(Type type)
:m_type(type)
{
}
Type getType() const
{
return m_type;
}
Direction operator-() const
{
switch (m_type)
{
case up: return Direction{ down };
case down: return Direction{ up };
case left: return Direction{ right };
case right: return Direction{ left };
default: break;
}
assert(0 && "Unsupported direction was passed!");
return Direction{ up };
}
static Direction getRandomDirection()
{
Type random{ static_cast<Type>(Random::get(0, Type::maxDirections - 1)) };
return Direction{ random };
}
private:
Type m_type{};
};
std::ostream& operator<<(std::ostream& stream, Direction dir)
{
switch (dir.getType())
{
case Direction::up: return (stream << "up");
case Direction::down: return (stream << "down");
case Direction::left: return (stream << "left");
case Direction::right: return (stream << "right");
default: break;
}
assert(0 && "Unsupported direction was passed!");
return (stream << "unknown direction");
}
struct Point
{
int x{};
int y{};
friend bool operator==(Point p1, Point p2)
{
return p1.x == p2.x && p1.y == p2.y;
}
friend bool operator!=(Point p1, Point p2)
{
return !(p1 == p2);
}
Point getAdjacentPoint(Direction dir) const
{
switch (dir.getType())
{
case Direction::up: return Point{ x, y - 1 };
case Direction::down: return Point{ x, y + 1 };
case Direction::left: return Point{ x - 1, y };
case Direction::right: return Point{ x + 1, y };
default: break;
}
assert(0 && "Unsupported direction was passed!");
return *this;
}
};
namespace UserInput
{
bool isValidCommand(char ch)
{
return ch == 'w'
|| ch == 'a'
|| ch == 's'
|| ch == 'd'
|| ch == 'q';
}
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
char getCharacter()
{
char operation{};
std::cin >> operation;
ignoreLine(); // remove any extraneous input
return operation;
}
char getCommandFromUser()
{
char ch{};
while (!isValidCommand(ch))
ch = getCharacter();
return ch;
}
Direction charToDirection(char ch)
{
switch (ch)
{
case 'w': return Direction{ Direction::up };
case 's': return Direction{ Direction::down };
case 'a': return Direction{ Direction::left };
case 'd': return Direction{ Direction::right };
}
assert(0 && "Unsupported direction was passed!");
return Direction{ Direction::up };
}
};
class Tile
{
public:
Tile() = default;
explicit Tile(int number)
:m_num(number)
{
}
bool isEmpty() const
{
return m_num == 0;
}
int getNum() const { return m_num; }
private:
int m_num { 0 };
};
std::ostream& operator<<(std::ostream& stream, Tile tile)
{
if (tile.getNum() > 9) // if two digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() > 0) // if one digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() == 0) // if empty spot
stream << " ";
return stream;
}
class Board
{
public:
Board() = default;
static void printEmptyLines(int count)
{
for (int i = 0; i < count; ++i)
std::cout << '\n';
}
friend std::ostream& operator<<(std::ostream& stream, const Board& board)
{
// Before drawing always print some empty lines
// so that only one board appears at a time
// and it's always shown at the bottom of the window
// because console window scrolls automatically when there is no
// enough space.
for (int i = 0; i < g_consoleLines; ++i)
std::cout << '\n';
for (int y = 0; y < s_size; ++y)
{
for (int x = 0; x < s_size; ++x)
stream << board.m_tiles[y][x];
stream << '\n';
}
return stream;
}
private:
static constexpr int s_size { 4 };
Tile m_tiles[s_size][s_size]{
Tile{ 1 }, Tile { 2 }, Tile { 3 } , Tile { 4 },
Tile { 5 } , Tile { 6 }, Tile { 7 }, Tile { 8 },
Tile { 9 }, Tile { 10 }, Tile { 11 }, Tile { 12 },
Tile { 13 }, Tile { 14 }, Tile { 15 }, Tile { 0 } };
};
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;
}
> 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
Zaimplementuj następujące funkcje członkowskie w naszej Board klasę:
- Funkcji, która zwraca wartość bool wskazującą, czy dany Punkt jest ważny (w obrębie naszej tablicy).
- Funkcja, która znajduje i zwraca pozycję pustej płytki jako a
Point. Moglibyśmy po prostu śledzić, gdzie znajduje się pusta płytka, ale wprowadza to niezmiennik klasy, a znalezienie pustej płytki, kiedy tylko jej potrzebujemy, nie jest aż tak drogie. - Funkcja, która zamieni dwie płytki ze względu na ich indeksy punktowe.
- A
moveTile(Direction dir) Funkcja, która spróbuje przesunąć płytkę w danym kierunku i zwróci true jeśli się powiedzie. Funkcja ta powinna implementować procedurę opisaną powyżej.
Zmodyfikuj main() z kroku 5 tak, aby moveTile() było wywoływane w przypadku wprowadzenia polecenia kierunkowego. Jeśli ruch się udał, przerysuj planszę.
Pokaż rozwiązanie
#include <array>
#include <cassert>
#include <iostream>
#include <limits>
#include "Random.h"
// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };
class Direction
{
public:
enum Type
{
up,
down,
left,
right,
maxDirections,
};
Direction(Type type)
:m_type(type)
{
}
Type getType() const
{
return m_type;
}
Direction operator-() const
{
switch (m_type)
{
case up: return Direction{ down };
case down: return Direction{ up };
case left: return Direction{ right };
case right: return Direction{ left };
default: break;
}
assert(0 && "Unsupported direction was passed!");
return Direction{ up };
}
static Direction getRandomDirection()
{
Type random{ static_cast<Type>(Random::get(0, Type::maxDirections - 1)) };
return Direction{ random };
}
private:
Type m_type{};
};
std::ostream& operator<<(std::ostream& stream, Direction dir)
{
switch (dir.getType())
{
case Direction::up: return (stream << "up");
case Direction::down: return (stream << "down");
case Direction::left: return (stream << "left");
case Direction::right: return (stream << "right");
default: break;
}
assert(0 && "Unsupported direction was passed!");
return (stream << "unknown direction");
}
struct Point
{
int x{};
int y{};
friend bool operator==(Point p1, Point p2)
{
return p1.x == p2.x && p1.y == p2.y;
}
friend bool operator!=(Point p1, Point p2)
{
return !(p1 == p2);
}
Point getAdjacentPoint(Direction dir) const
{
switch (dir.getType())
{
case Direction::up: return Point{ x, y - 1 };
case Direction::down: return Point{ x, y + 1 };
case Direction::left: return Point{ x - 1, y };
case Direction::right: return Point{ x + 1, y };
default: break;
}
assert(0 && "Unsupported direction was passed!");
return *this;
}
};
namespace UserInput
{
bool isValidCommand(char ch)
{
return ch == 'w'
|| ch == 'a'
|| ch == 's'
|| ch == 'd'
|| ch == 'q';
}
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
char getCharacter()
{
char operation{};
std::cin >> operation;
ignoreLine(); // remove any extraneous input
return operation;
}
char getCommandFromUser()
{
char ch{};
while (!isValidCommand(ch))
ch = getCharacter();
return ch;
}
Direction charToDirection(char ch)
{
switch (ch)
{
case 'w': return Direction{ Direction::up };
case 's': return Direction{ Direction::down };
case 'a': return Direction{ Direction::left };
case 'd': return Direction{ Direction::right };
}
assert(0 && "Unsupported direction was passed!");
return Direction{ Direction::up };
}
};
class Tile
{
public:
Tile() = default;
explicit Tile(int number)
:m_num(number)
{
}
bool isEmpty() const
{
return m_num == 0;
}
int getNum() const { return m_num; }
private:
int m_num { 0 };
};
std::ostream& operator<<(std::ostream& stream, Tile tile)
{
if (tile.getNum() > 9) // if two digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() > 0) // if one digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() == 0) // if empty spot
stream << " ";
return stream;
}
class Board
{
public:
Board() = default;
static void printEmptyLines(int count)
{
for (int i = 0; i < count; ++i)
std::cout << '\n';
}
friend std::ostream& operator<<(std::ostream& stream, const Board& board)
{
// Before drawing always print some empty lines
// so that only one board appears at a time
// and it's always shown at the bottom of the window
// because console window scrolls automatically when there is no
// enough space.
for (int i = 0; i < g_consoleLines; ++i)
std::cout << '\n';
for (int y = 0; y < s_size; ++y)
{
for (int x = 0; x < s_size; ++x)
stream << board.m_tiles[y][x];
stream << '\n';
}
return stream;
}
Point getEmptyTilePos() const
{
for (int y = 0; y < s_size; ++y)
for (int x = 0; x < s_size; ++x)
if (m_tiles[y][x].isEmpty())
return { x,y };
assert(0 && "There is no empty tile in the board!!!");
return { -1,-1 };
}
static bool isValidTilePos(Point pt)
{
return (pt.x >= 0 && pt.x < s_size)
&& (pt.y >= 0 && pt.y < s_size);
}
void swapTiles(Point pt1, Point pt2)
{
std::swap(m_tiles[pt1.y][pt1.x], m_tiles[pt2.y][pt2.x]);
}
// returns true if user moved successfully
bool moveTile(Direction dir)
{
Point emptyTile{ getEmptyTilePos() };
Point adj{ emptyTile.getAdjacentPoint(-dir) };
if (!isValidTilePos(adj))
return false;
swapTiles(adj, emptyTile);
return true;
}
private:
static const int s_size { 4 };
Tile m_tiles[s_size][s_size]{
Tile{ 1 }, Tile { 2 }, Tile { 3 } , Tile { 4 },
Tile { 5 } , Tile { 6 }, Tile { 7 }, Tile { 8 },
Tile { 9 }, Tile { 10 }, Tile { 11 }, Tile { 12 },
Tile { 13 }, Tile { 14 }, Tile { 15 }, Tile { 0 } };
};
int main()
{
Board board{};
std::cout << board;
std::cout << "Enter a command: ";
while (true)
{
char ch{ UserInput::getCommandFromUser() };
// Handle non-direction commands
if (ch == 'q')
{
std::cout << "\n\nBye!\n\n";
return 0;
}
// Handle direction commands
Direction dir{ UserInput::charToDirection(ch) };
bool userMoved { board.moveTile(dir) };
if (userMoved)
std::cout << board;
}
return 0;
}
> 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
- Dodaj
randomize() funkcję składową do klasy Board klasie, która losuje płytki na planszy. Wybierz losowy kierunek i jeśli sąsiadujący punkt jest prawidłowy, przesuń płytkę w tym kierunku. Wykonanie tej czynności 1000 razy powinno wystarczyć do pomieszania planszy. - Zaimplementuj operator== w klasie
Board , która porówna, czy płytki na dwóch podanych planszach są identyczne. - Dodaj
playerWon() funkcję składową do klasy Board , która zwróci wartość true, jeśli bieżąca plansza zostanie ułożona. Możesz użyć operator== zaimplementowanego, aby porównać aktualną planszę do gry z rozwiązaną planszą. Pamiętaj, że Board obiekty zaczynają się w stanie rozwiązanym, więc jeśli potrzebujesz rozwiązanej planszy, po prostu zainicjalizuj Board obiekt! - Zaktualizuj swoją funkcję main(), aby zintegrować randomize() i playerWon().
Oto pełne rozwiązanie naszej 15-łamigłówki:
Pokaż rozwiązanie
#include <array>
#include <cassert>
#include <iostream>
#include <limits>
#include "Random.h"
// Increase amount of new lines if your board isn't
// at the very bottom of the console
constexpr int g_consoleLines{ 25 };
class Direction
{
public:
enum Type
{
up,
down,
left,
right,
maxDirections,
};
Direction(Type type)
:m_type(type)
{
}
Type getType() const
{
return m_type;
}
Direction operator-() const
{
switch (m_type)
{
case up: return Direction{ down };
case down: return Direction{ up };
case left: return Direction{ right };
case right: return Direction{ left };
default: break;
}
assert(0 && "Unsupported direction was passed!");
return Direction{ up };
}
static Direction getRandomDirection()
{
Type random{ static_cast<Type>(Random::get(0, Type::maxDirections - 1)) };
return Direction{ random };
}
private:
Type m_type{};
};
std::ostream& operator<<(std::ostream& stream, Direction dir)
{
switch (dir.getType())
{
case Direction::up: return (stream << "up");
case Direction::down: return (stream << "down");
case Direction::left: return (stream << "left");
case Direction::right: return (stream << "right");
default: break;
}
assert(0 && "Unsupported direction was passed!");
return (stream << "unknown direction");
}
struct Point
{
int x{};
int y{};
friend bool operator==(Point p1, Point p2)
{
return p1.x == p2.x && p1.y == p2.y;
}
friend bool operator!=(Point p1, Point p2)
{
return !(p1 == p2);
}
Point getAdjacentPoint(Direction dir) const
{
switch (dir.getType())
{
case Direction::up: return Point{ x, y - 1 };
case Direction::down: return Point{ x, y + 1 };
case Direction::left: return Point{ x - 1, y };
case Direction::right: return Point{ x + 1, y };
default: break;
}
assert(0 && "Unsupported direction was passed!");
return *this;
}
};
namespace UserInput
{
bool isValidCommand(char ch)
{
return ch == 'w'
|| ch == 'a'
|| ch == 's'
|| ch == 'd'
|| ch == 'q';
}
void ignoreLine()
{
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
char getCharacter()
{
char operation{};
std::cin >> operation;
ignoreLine(); // remove any extraneous input
return operation;
}
char getCommandFromUser()
{
char ch{};
while (!isValidCommand(ch))
ch = getCharacter();
return ch;
}
Direction charToDirection(char ch)
{
switch (ch)
{
case 'w': return Direction{ Direction::up };
case 's': return Direction{ Direction::down };
case 'a': return Direction{ Direction::left };
case 'd': return Direction{ Direction::right };
}
assert(0 && "Unsupported direction was passed!");
return Direction{ Direction::up };
}
};
class Tile
{
public:
Tile() = default;
explicit Tile(int number)
:m_num(number)
{
}
bool isEmpty() const
{
return m_num == 0;
}
int getNum() const { return m_num; }
private:
int m_num { 0 };
};
std::ostream& operator<<(std::ostream& stream, Tile tile)
{
if (tile.getNum() > 9) // if two digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() > 0) // if one digit number
stream << " " << tile.getNum() << " ";
else if (tile.getNum() == 0) // if empty spot
stream << " ";
return stream;
}
class Board
{
public:
Board() = default;
static void printEmptyLines(int count)
{
for (int i = 0; i < count; ++i)
std::cout << '\n';
}
friend std::ostream& operator<<(std::ostream& stream, const Board& board)
{
// Before drawing always print some empty lines
// so that only one board appears at a time
// and it's always shown at the bottom of the window
// because console window scrolls automatically when there is no
// enough space.
for (int i = 0; i < g_consoleLines; ++i)
std::cout << '\n';
for (int y = 0; y < s_size; ++y)
{
for (int x = 0; x < s_size; ++x)
stream << board.m_tiles[y][x];
stream << '\n';
}
return stream;
}
Point getEmptyTilePos() const
{
for (int y = 0; y < s_size; ++y)
for (int x = 0; x < s_size; ++x)
if (m_tiles[y][x].isEmpty())
return { x,y };
assert(0 && "There is no empty tile in the board!!!");
return { -1,-1 };
}
static bool isValidTilePos(Point pt)
{
return (pt.x >= 0 && pt.x < s_size)
&& (pt.y >= 0 && pt.y < s_size);
}
void swapTiles(Point pt1, Point pt2)
{
std::swap(m_tiles[pt1.y][pt1.x], m_tiles[pt2.y][pt2.x]);
}
// Compare two boards to see if they are equal
friend bool operator==(const Board& f1, const Board& f2)
{
for (int y = 0; y < s_size; ++y)
for (int x = 0; x < s_size; ++x)
if (f1.m_tiles[y][x].getNum() != f2.m_tiles[y][x].getNum())
return false;
return true;
}
// returns true if user moved successfully
bool moveTile(Direction dir)
{
Point emptyTile{ getEmptyTilePos() };
Point adj{ emptyTile.getAdjacentPoint(-dir) };
if (!isValidTilePos(adj))
return false;
swapTiles(adj, emptyTile);
return true;
}
bool playerWon() const
{
static Board s_solved{}; // generate a solved board
return s_solved == *this; // player wins if current board == solved board
}
void randomize()
{
// Move empty tile randomly 1000 times
// (just like you would do in real life)
for (int i = 0; i < 1000; )
{
// If we are able to successfully move a tile, count this
if (moveTile(Direction::getRandomDirection()))
++i;
}
}
private:
static const int s_size { 4 };
Tile m_tiles[s_size][s_size]{
Tile{ 1 }, Tile { 2 }, Tile { 3 } , Tile { 4 },
Tile { 5 } , Tile { 6 }, Tile { 7 }, Tile { 8 },
Tile { 9 }, Tile { 10 }, Tile { 11 }, Tile { 12 },
Tile { 13 }, Tile { 14 }, Tile { 15 }, Tile { 0 } };
};
int main()
{
Board board{};
board.randomize();
std::cout << board;
while (!board.playerWon())
{
char ch{ UserInput::getCommandFromUser() };
// Handle non-direction commands
if (ch == 'q')
{
std::cout << "\n\nBye!\n\n";
return 0;
}
// Handle direction commands
Direction dir{ UserInput::charToDirection(ch) };
bool userMoved{ board.moveTile(dir) };
if (userMoved)
std::cout << board;
}
std::cout << "\n\nYou won!\n\n";
return 0;
}