// Ciąg dalszy wykorzystania mechanizmów obiektowych w przykładzie
// wykorzystującym klasę Runner. Program prezentuje dziedziczenie,
// wykorzystanie pól chronionych, ponowdą definicję funkcji
// składowych w klasie pochodnej.
//
// Prosty program prezentujący klasę Runner dziedziczącą po klasie Point.
// Program zawiera również klasę PlayBoard, reprezentującą planszę gry,
// oraz klasę Game. Klasa Game jest klasą nadrzędną, jest właścicielem
// obiektu klasy Runner oraz obiektu klasy PlayBoard. Na planszy znaki
// '#' reprezentują ściany, znaki '$' prezentują gotówkę, którą może zbierać
// obiekt klasy Runnner przemieszczając się po planszy.
//
// Zachęcam do przeanalizowania mechaniki tej prostej "gierki", i jej rozwoju.
// Np. poprzez wprowadzenie innych elementów, które może zbierać gracz, elementy
// mogą też zabierać gotówkę (np. salon gier), można też się pokusić o wprowadzenie
// elementów ruchomych (duszki "dobre" i "złe").
//
// Program stanowi uzupełnienie wykładu z programowania w językach
// C/C++. Koncepcja klasy Runner została omówiona w materiałach wykładowych
//
// Autor: Roman Siminski

// Uwaga! Implementacja tylko dla Windows, obsługa konsoli realizowana za
// pośrednictwem WinApi. Najlepiej kompilować używając MinGW (środowiska
// Code::Blocka, DevC++) lub kompilatorów w środowisku C++ Builder. Proszę
// unikać środowisk VisualC++, CLion, QT, będą stwarzać problemy.

#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <conio.h>

#if defined(__MINGW32__) || defined(_MSC_VER)
#include <windows.h>
#endif

// Kody klawiszy sterujących kursorem i klawiszy specjalnych
enum KEY_CODES
{
#ifdef __BORLANDC__
	KEY_BLANK = 0x0000,
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	KEY_BLANK = 0x00e0,
#endif
	KEY_UP = 0x4800,
	KEY_DOWN = 0x5000,
	KEY_LEFT = 0x4b00,
	KEY_RIGHT = 0x4d00,
	KEY_ESC = 0x001b
};

// Prototypy funkcji obsługi konsoli
int getKey();
void clearScreen();
void writeCharXY(int x, int y, char c);
void writeStrXY(int x, int y, char s[]);
void writeIntXY(int x, int y, int i);
void writeDoubleXY(int x, int y, double d);
void cursorOff();
void cursorOn();

// Rozmiary ekranu konsoli, nie uwzględniają ostatniego wiersza
// ten jest wierszem statusu i zawiera informacje o stanie gry
const int NUM_OF_COLS = 80;
const int NUM_OF_ROWS = 24;

// Klasa reprezentacji informacji o położeniu punktu
// w przestrzeni 2D, bez żadnych ograniczeń w sensie
// wartości współrzędnych
class Point
{
  public:
    Point(int startX = 0, int startY = 0);
    void setX(int newX);
	void setY(int newY);
	int  getX();
	int  getY();
  protected:
	int  x, y;
};

// Definicje funkcji składowych poza deklaracją klasy
Point::Point(int startX, int startY) : x(startX), y(startY)
{
}

void Point::setX(int newX)
{
  x = newX;
}

void Point::setY(int newY)
{
  y = newY;
}

int Point::getX()
{
  return x;
}

int Point::getY()
{
  return y;
}

// Klasa Runner, dziedziczenie pozycji ekranowej po klasie Point.
// Obiekt klasy Runner będzie zatem posiadał pola X, y oraz dostęp
// do nieprywatnych funkcji składowych. Uwaga -- funkcje ustawiania
// wartości x i y w klasie Point nie kontrolują wartości wstawianych
// do tych pól. W klasie runner dbamy o kontrole, nie pozwalamy ustawić
// wartości nie mieszczących się na ekranie konsoli. Dlatego w tej
// implementacji klasy Runner funkcje skłądowe setX oraz setY zostały
// zdefiniowane ponownie
class Runner : public Point
{
  public:
	Runner(int startX = 1, int startY = 1, char startShape = '*');
	void setShape(char newShape);
	char getShape();

    // Ponowna definicja funkcji ustawiania pozycji
    void setX(int newX);
    void setY(int newY);

	void show();
	void hide();

	void moveUp();
	void moveDown();
	void moveLeft();
	void moveRight();

  private:
	char shape; // Znak określający wygląd elementu
	// Pomocnicze funkcje weryfikacji pozycji ekranowej
	bool isXOnScreen(int x);
	bool isYOnScreen(int y);
};

// Definicje funkcji składowych poza deklaracją klasy
Runner::Runner(int startX, int startY, char startShape)
{
  // Wartości x, y i shape mogłyby być ustawione na liście inicjalizacyjnej.
  // Ale wartości parametrów konstruktora mogły być nieprawidłowe, dlatego
  // są ustawione w ciele konstruktora z wykorzystaniem odpowiednich funkcji
  // ustawiających
  setX(startX);
  setY(startY);
  setShape(startShape);
}

void Runner::setShape(char newShape)
{
  shape = (newShape > 32 && newShape <= 127)
          ? newShape : '*';
}

void Runner::setX(int newX)
{
  // Wykorzystujemy możliwość bezpośredniego dostępu
  // do pola chronionego x klasy Point
  x = isXOnScreen(newX) ? newX : 1;

  // Alternatywnie, gdyby pole x było prywatne:
  // Point::setX(isXOnScreen(newX) ? newX : 1);
  // No, można napisać też jak w przedszkolu:
  // if(isXOnScreen(newX))
  //   Point::setX(newX);
  // else
  //   Point::setX(1);
}

void Runner::setY(int newY)
{
  // Wykorzystujemy możliwość bezpośredniego dostępu
  // do pola chronionego y klasy Point
  y = isYOnScreen(newY) ? newY : 1;
}

void Runner::show()
{
  writeCharXY(x, y, shape);
}

void Runner::hide()
{
  writeCharXY(x, y, ' ');
}

char Runner::getShape()
{
  return shape;
}

void Runner::moveUp()
{
  hide();
  if(y > 1)
    --y;
  show();
}

void Runner::moveDown()
{
  hide();
  if(y < NUM_OF_ROWS)
    ++y;
  show();
}

void Runner::moveLeft()
{
  hide();
  if(x > 1)
    --x;
  show();
}

void Runner::moveRight()
{
  hide();
  if(x < NUM_OF_COLS)
    ++x;
  show();
}

bool Runner::isXOnScreen(int newX)
{
 return (newX > 0 && newX <= NUM_OF_COLS);
}

bool Runner::isYOnScreen(int newY)
{
  return (newY > 0 && newY <= NUM_OF_ROWS);
}

// Klasa reprezentująca planszę gry. Funkcje publiczne pozwalają na testowanie
// czy na zadanej pozycji planszy są ściany labiryntu (znak #) oraz czy na
// zadanej pozycji jest gotówka (znak $)
class PlayBoard
{
  public:
    PlayBoard();
    bool isWallOnXY(Point p);
    bool isCashOnXY(Point p);
    void clearCashOnXY(Point p);
    void show();
  private:
    char playBoardMap[NUM_OF_ROWS][NUM_OF_COLS+1];
};

// Definicje funkcji składowych poza deklaracją klasy
PlayBoard::PlayBoard()
{
  // Drobna sztuczka: ze względu na problemy związane z inicjalizacją tablicowych
  // pól klasy (do wersji C++11), pole playBoardMap będące tablicą dwuwymiarową
  // jest inicjowane w konstruktorze zawartością statycznej tablicy initialMap.
  // Tę ostatnią można dowolnie edytować ustalając zawartość planszy.
  static char initialMap[NUM_OF_ROWS][NUM_OF_COLS+1] =
  {
      // Liczby poniżej ułatwiają orientacje w tablicy, uwaga wymiary muszą się zgadzać
      //01234567890123456789012345678901234567890123456789012345678901234567890123456789
      "################################################################################",
      "#                                                     $    #    $    #    $ $  #",
      "#         #                                           #    #    #    #    # $  #",
      "#         #                                           #    $    #    $    # $  #",
      "#         ####################  ################  ##############################",
      "#         #       # $  $ #                                                     #",
      "#         #       #      #                                                     #",
      "# #########  $    #### #########################################################",
      "#  $ #            #      #          #              #$$$$                       #",
      "#  $ #                   #          #       #      ##########################  #",
      "# ##### ##################          #       #      #                           #",
      "#    $#                             #       #      ############# ###############",
      "#     #                                     #                                  #",
      "#  ###########  ######################################################## #######",
      "#                                  # $                                      #$ #",
      "######################## #################### ############################# #  #",
      "#                                                                              #",
      "#                                                   ############################",
      "#                                                   #            #$$$$$$$$$$$$$#",
      "# #########################                         #  ########  #             #",
      "#                     #                                #      #  #             #",
      "#                     ##################################      #  #             #",
      "#                                 $$$#                                         #",
      "################################################################################"
    };
    // Kopiowane kolejnych wierszy z mapy statycznej do playBoardMap
    for(int i = 0; i < 24; ++i)
      strcpy(playBoardMap[i], initialMap[i]);
}

void PlayBoard::show()
{
  for(int i = 0; i < 24; ++i)
      writeStrXY(1, i + 1, playBoardMap[i]);
}

bool PlayBoard::isWallOnXY(Point p)
{
  // Pozycja ekranowa liczona jest od 1, indeksy w tablicy od 0
  return playBoardMap[p.getY() - 1][p.getX() - 1] == '#';
}

bool PlayBoard::isCashOnXY(Point p)
{
  // Pozycja ekranowa liczona jest od 1, indeksy w tablicy od 0
  return playBoardMap[p.getY() - 1][p.getX() - 1] == '$';
}

void PlayBoard::clearCashOnXY(Point p)
{
  // Pozycja ekranowa liczona jest od 1, indeksy w tablicy od 0
   playBoardMap[p.getY() - 1][p.getX() - 1] = ' ';
}

// Klasa zarządzania grą. Połączona z klasą PlayBoard i Runner związkiem
// kompozycji (całość-część). Klasa realizuje w funkcji run() "aktywne"
// przepytywanie klawiatury. Jeżeli żaden klawisz nie został naciśnięty
// wywoływana jest procedura backgroundProcess(). W tej wersji realizuje
// ona wyświetlanie zegara i aktualizacjê pozycji runner'a oraz kwoty
// zebranej gotówki. Docelowo w tej procedurze można zrealizować animowanie
// ruchomych obiektów gry.
// Każde naciśnięcie klawisza skutkuje jego obsługą (funkcja processKey(int))
// i powrotem do aktywnego przepytywania klawiatury.
class Game
{
  public:
	Game();
	~Game();
	void run();
  private:
	Runner runner;
	PlayBoard playBoard;
	int cash;
	void backgroundProcess();
	void processKey(int key);
	void updateGameStatus();
	void displayClock();
};

// Definicje funkcji składowych poza deklaracją klasy

// Uwaga! lista inicjalizacyjna przeznaczona jest (oprócz aktywowania
// konstruktorów klas bazowych) do inicjowana pól obiektów danej klasy.
// Pole cash (typ prosty int) jest inicjowane wartością zero. Pola runner
// i playBoard są obiektami. Obiekty inicjuje się poprzez aktywowanie ich
// konstruktorów. Umieszczenie na liście inicjalizacyjnej pole będącego
// obiektem powoduje aktywowanie odpowiedniego konstruktora dla obiektu.
// Jeżeli obiekt nie wystąpi na liście inicjalizacyjnej, kompilator
// automatycznie uaktywni dla niego konstruktor domyślny przed wejściem
// do ciała konstruktora klasy nadrzędnej. Ważne -- kolejność aktywowania
// konstruktorów dla pól obiektowych jest zgodna z ich występowaniem
// w deklaracji klasy a nie w kolejności umieszczania tych pól na liście
// inicjalizacyjnej
Game::Game() : runner(40, 12, '*'), playBoard(), cash(0)
{
  clearScreen();
  cursorOff();
}

Game::~Game()
{
  clearScreen();
  cursorOn();
}

void Game::updateGameStatus()
{
  char info[128];
  // Przygotowanie łańcucha znaków z informacjami i wyświetlenie
  sprintf(info, "X=%-2d Y=%-2d", runner.getX(), runner.getY());
  writeStrXY(1, 25, info);
  // Przygotowanie łańcucha znaków z informacjami i wyświetlenie
  sprintf(info, "Gotowka=%-4d", cash);
  writeStrXY(20, 25, info);
}

void Game::displayClock()
{
  char info[128];

  time_t t = time(NULL); // Pobranie aktualnego czasu
  struct tm *ptm = localtime(&t); // "Rozpakowanie" informacji o czasie

  // Przygotowanie łańcucha znaków z informacjami o czasie i wyświetlenie
  sprintf( info, "%02d:%02d:%02d", ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
  writeStrXY(72, 25, info);
}

void Game::backgroundProcess()
{
  updateGameStatus();
  displayClock();
}

void Game::processKey(int key)
{
  // Zapamietujemy aktualną pozycję runner'a
  Point newPos = runner;
  switch(key)
  {
      case KEY_UP:    // Przewidywana nowa pozycja
                      newPos.setY(runner.getY()-1);
                      // Czy można się przemieścić na tę pozycję?
                      if(!playBoard.isWallOnXY(newPos))
                        runner.moveUp();
		              break;
	  case KEY_DOWN:  newPos.setY(runner.getY()+1);
	                  if(!playBoard.isWallOnXY(newPos))
                        runner.moveDown();
		              break;
	  case KEY_LEFT:  newPos.setX(runner.getX()-1);
                      if(!playBoard.isWallOnXY(newPos))
	                    runner.moveLeft();
		              break;
	  case KEY_RIGHT: newPos.setX(runner.getX()+1);
	                  if(!playBoard.isWallOnXY(newPos))
	                    runner.moveRight();
		              break;
  }
  // Zebranie gotówki z nowej pozycji, o ile na niej jest
  if(playBoard.isCashOnXY(runner))
  {
     cash += 100;
     playBoard.clearCashOnXY(runner);
  }
}

void Game::run()
{
  int key = 0;

  playBoard.show();
  runner.show();

  // Aktywne przepytywanie klawiatury. Rezultatem funkcji _kbhit()
  // jest true jeżeli w buforze klawiatury oczekuje nieodczytany znak
  do
  {
    if(!_kbhit())
	  backgroundProcess();
    else
      processKey(key = getKey());
  }
  while(key != KEY_ESC);
}

int main()
{
  Game game;
  game.run();
  return EXIT_SUCCESS;
}

// Funkcje obsługi konsoli, sprawa techniczna, w sensie merytorycznym nieistotna
int getKey()
{
	int key = _getch();
	return (key == KEY_BLANK) ? _getch() << 8 : key;
}

void clearScreen()
{
#ifdef __BORLANDC__
	clrscr();
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	COORD leftTop = { 0, 0 };
	CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
	int numOfCells = 80 * 25;
	DWORD writtenItems;
	HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
	if (GetConsoleScreenBufferInfo(consoleHandle, &consoleInfo))
		numOfCells = consoleInfo.dwSize.X * consoleInfo.dwSize.Y;

	FillConsoleOutputAttribute(consoleHandle, 0xf,
		numOfCells, leftTop, &writtenItems);
	FillConsoleOutputCharacter(consoleHandle, ' ',
		numOfCells, leftTop, &writtenItems);
#else
#error "Nieobslugiwana platforma"
#endif
}

void writeCharXY(int x, int y, char c) {
#ifdef __BORLANDC__
	gotoxy(x, y);
	putch(c);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	COORD cursorPos;
	DWORD written;

	cursorPos.X = x - 1;
	cursorPos.Y = y - 1;

	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cursorPos);
	WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), &c, 1, &written, 0);
#else
#error "Nieobslugiwana platforma"
#endif
}

void writeStrXY(int x, int y, char s[])
{
#ifdef __BORLANDC__
	gotoxy(x, y);
	cputs(s);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	COORD cursorPos;
	DWORD written;

	cursorPos.X = x - 1;
	cursorPos.Y = y - 1;

	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cursorPos);
	WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), s, strlen(s), &written, 0);
#else
#error "Nieobslugiwana platforma"
#endif
}

void writeIntXY(int x, int y, int i)
{
	char s[80];
#if defined(_MSC_VER)
	sprintf_s(s, 80, "%d", i);
#else
	sprintf(s, "%d", i);
#endif
	writeStrXY(x, y, s);
}

void writeDoubleXY(int x, int y, double d)
{
	char s[80];
#if defined(_MSC_VER)
	sprintf_s(s, 80, "%g", d);
#else
	sprintf(s, "%g", d);
#endif
	writeStrXY(x, y, s);
}

void cursorOff()
{
#ifdef __BORLANDC__
	_setcursortype(_NOCURSOR);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	CONSOLE_CURSOR_INFO cursorInfo;
	HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleCursorInfo(consoleHandle, &cursorInfo);
	cursorInfo.bVisible = false;
	SetConsoleCursorInfo(consoleHandle, &cursorInfo);
#else
#error "Nieobslugiwana platforma"
#endif
}

void cursorOn()
{
#ifdef __BORLANDC__
	_setcursortype(_NORMALCURSOR);
#elif (defined(__GNUC__) && defined(__MINGW32__)) || defined(_MSC_VER)
	CONSOLE_CURSOR_INFO cursorInfo;
	HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
	GetConsoleCursorInfo(consoleHandle, &cursorInfo);
	cursorInfo.bVisible = true;
	SetConsoleCursorInfo(consoleHandle, &cursorInfo);
#else
#error "Nieobslugiwana platforma"
#endif
}

