БлогNot. Hunt the Wumpus forever или Охота на Вампуса по-русски

Hunt the Wumpus forever или Охота на Вампуса по-русски

Какая компьютерная игра была первой игрой на карте-графе, первой текстовой и вообще первой приключенческой игрой на свете? Наверное, всё-таки Вампус.

Программисты до сих пор любят её, боюсь, что нужно быть программистом, чтобы понять, почему :)

А вот найти классические консольно-текстовые реализации с исходниками уже непросто, кажется, проще сделать заново, ну и пусть будет с русскоязычным интерфейсом заодно.

Проверил в консоли Studio 2019, для запуска игры код нужно скомпилировать и выполнить, а для этого хватит вставки листинга в единственный файл нового пустого проекта C++. Никаких цветных пятен, правильная чёрно-белая классика :) В исходнике есть специфичный для Studio код (см. строчку подключения файла windows.h).

#include <iostream>
#include <cstdlib>   /* srand, rand */
#include <ctime>     /* time */
#include <clocale>   /* setlocale */
#include <windows.h> /* SetConsoleCP, SetConsoleOutputCP */
using namespace std;

const static int adjacentRooms[20][3] = { // Додекаэдр комнат
 {1, 4, 7},   {0, 2, 9},   {1, 3, 11},   {2, 4, 13},    {0, 3, 5},
 {4, 6, 14},  {5, 7, 16},    {0, 6, 8},   {7, 9, 17},   {1, 8, 10},
 {9, 11, 18}, {2, 10, 12}, {11, 13, 19},  {3, 12, 14},  {5, 13, 15},
 {14, 16, 19}, {6, 15, 17},  {8, 16, 18}, {10, 17, 19}, {12, 15, 18}
};

class WumpusGame {
private:
 int numberOfRooms, currentRoom, startingPosition, 
     wumpusRoom, batRoom1, batRoom2, pitRoom1, pitRoom2, //где вампус, мыши, ямы
     wumpusStart, bat1Start, bat2Start; //кто в какой комнате
 bool playerAlive, wumpusAlive; //кто жив
 int numArrows; //стрелы
 void PlacePits();
 void PlaceBats();
 void PlaceWumpus();
 void PlacePlayer();
 bool IsValidMove (int);
 bool IsRoomAdjacent (int, int);
 int Move(int);
 void InspectCurrentRoom();
 void PerformAction(int);
 void MoveStartledWumpus(int);
 void PlayGame();
 void PlayAgain();
 void PrintInstructions();
public:
 WumpusGame();
 void StartGame();
 static void clearInput() { cin.clear(); cin.ignore(10000, '\n'); }
};

WumpusGame::WumpusGame() { numberOfRooms = 20; }

void WumpusGame::PrintInstructions() {
 char wait;
 cout << endl << "Игра 'Охота на Вампуса', оригинальный (С) Gregory Yob, 1972" << endl << endl << 
 "Вампус живет в пещере из 20 комнат. В каждой комнате есть 3 туннеля, ведущих в" << endl <<
 "соседние комнаты. Посмотрите на додекаэдр, чтобы увидеть, как это устроено." << endl <<
 "В двух комнатах есть бездонные ямы. Если вы войдёте туда,  вы упадёте  в яму и" << endl <<
 "проиграете." << endl <<
 "В двух других комнатах есть летучие мыши.  Если  вы войдёте туда, мышь хватает" << endl <<
 "вас и уносит в другую случайно выбранную комнату. Сама мышь  после этого  тоже" << endl <<
 "перемещается в случайное место на карте." << endl <<
 "У Вампуса ноги на присосках  и он слишком велик для того, чтобы мыши могли его" << endl <<
 "побеспокоить. Обычно он спит. Его могут разбудить две вещи:  если  вы стреляте" << endl <<
 "или же входите в его комнату. Если Вампус проснется, то есть 3 шанса из 4, что" << endl <<
 "он перейдёт в соседнюю комнату и 1 шанс из 4, что он останется на месте." << endl <<
 "Если вы окажетесь в одной комнате с Вампусом, он съест вас :(" << endl <<
 "За один ход вы можете  перейти в смежную комнату или  выстрелить в неё.  У вас" << endl <<
 "всего 3 стрелы. Вы проиграете, когда они закончатся.  Если  указать комнату, в" << endl <<
 "которую невозмоно выстрелить, то стрела пропадёт зря." << endl <<
 "Когда вы находитесь в смежной комнате с опасностью, выводятся предупреждения:" << endl <<
 "\"Я чувствую отвратительный запах\" (если рядом Вампус)" << endl <<
 "\"Летучие мыши рядом\"" << endl <<
 "\"Я чувствую сквозняк\" (если рядом - бездонная яма)" << endl <<
 "Введите что-нибудь, чтобы вернуться в главное меню.";
 cin >> wait;
}

void WumpusGame::PlaceBats() {
 bool validRoom = false;
 while (!validRoom) {
  batRoom1 = rand() % numberOfRooms + 1;
  if (batRoom1 != wumpusRoom) validRoom = true;
 }
 validRoom = false;
 while (!validRoom) {
  batRoom2 = rand() % numberOfRooms + 1;
  if (batRoom2 != wumpusRoom && batRoom2 != batRoom1) validRoom = true;
 }
 bat1Start = batRoom1; bat2Start = batRoom2;
}

void WumpusGame::PlacePits() {
 pitRoom1 = rand() % 20 + 1;
 pitRoom2 = rand() % 20 + 1;
}

void WumpusGame::PlaceWumpus() {
 int randomRoom = rand() % 20 + 1;
 wumpusRoom = randomRoom;
 wumpusStart = wumpusRoom;
}

void WumpusGame::PlacePlayer() {
 startingPosition = 0;
 currentRoom = Move(0);
}

bool WumpusGame::IsValidMove(int roomID) {
 if (roomID < 0) return false;
 if (roomID > numberOfRooms) return false;
 if (!IsRoomAdjacent(currentRoom, roomID)) return false;
 return true;
}

bool WumpusGame::IsRoomAdjacent(int roomA, int roomB) {
 for (int j = 0; j < 3; j++) {
  if (adjacentRooms[roomA][j] == roomB) return true;
 }
 return false;
}

int WumpusGame::Move (int newRoom) { return newRoom; }

void WumpusGame::InspectCurrentRoom() {
 if (currentRoom == wumpusRoom) {
  cout << endl << "Вампус съел вас, игра окончена :(" << endl;
  PlayAgain();
 }
 else if (currentRoom == batRoom1 || currentRoom == batRoom2) {
  int roomBatsLeft = currentRoom;
  bool validNewBatRoom = false;
  bool isBatRoom = false;
  cout << endl << "Вы схвачены летучей мышью!" << endl;
  if (currentRoom == pitRoom1 || currentRoom == pitRoom2)
   cout << endl << "К счастью, летучая мышь спасла вас от бездонной ямы" << endl;
  while (!isBatRoom) {
   currentRoom = Move(rand() % numberOfRooms + 1);
   if (currentRoom != batRoom1 && currentRoom != batRoom2) isBatRoom = true;
  }
  cout << endl << "Летучая мышь перенесла вас в комнату " << currentRoom << endl;
  InspectCurrentRoom();
  if (roomBatsLeft == batRoom1) {
   while (!validNewBatRoom) {
    batRoom1 = rand() % (numberOfRooms-1) + 1;
    if (batRoom1 != wumpusRoom && batRoom1 != currentRoom) validNewBatRoom = true;
   }
  }
  else {
   while (!validNewBatRoom) {
    batRoom2 = rand() % (numberOfRooms-1) + 1;
    if (batRoom2 != wumpusRoom && batRoom2 != currentRoom) validNewBatRoom = true;
   }
  }
 }
 else if (currentRoom == pitRoom1 || currentRoom == pitRoom2) {
  cout << endl << "Вы свалились в бездонную яму, игра окончена :(" << endl;
  PlayAgain();
 }
 else {
  cout << endl << "Вы находитесь в комнате " << currentRoom << endl;
  if (IsRoomAdjacent(currentRoom, wumpusRoom)) {
   cout << endl << "Я чувствую отвратительный запах..." << endl;
  }
  if (IsRoomAdjacent(currentRoom, batRoom1) || IsRoomAdjacent(currentRoom, batRoom2)) {
   cout << endl << "Летучие мыши рядом..." << endl;
  }
  if (IsRoomAdjacent(currentRoom, pitRoom1) || IsRoomAdjacent(currentRoom, pitRoom2)) {
   cout << endl << "Я чувствую сквозняк..." << endl;
  }
  cout << endl << "Туннели ведут в комнаты ";
  for (int j = 0; j < 3; j++) cout << adjacentRooms[currentRoom][j] << (j < 2 ? ", " : "");
  cout << endl;
 }
}

void WumpusGame::PerformAction(int cmd) {
 int newRoom;
 switch (cmd) {
  case 1:
   cout << endl << "В какую комнату пойдете? ";
   try {
    cin >> newRoom;
    if (cin.fail()) throw invalid_argument ("Вы должны ввести число!");
    if (IsValidMove(newRoom)) {
     currentRoom = Move(newRoom);
     InspectCurrentRoom();
    }
    else {
     cout << endl << "Вы не можете сейчас перейти в эту комнату :(" << endl;
    }
   }
   catch (invalid_argument& error)  {
    WumpusGame::clearInput();
    cerr << endl << error.what() << endl;
   }
  break;
  case 2:
   if (numArrows > 0) {
    cout << endl << "В какую комнату будете стрелять? " << endl;
    try {
     cin >> newRoom;
     if (cin.fail()) throw invalid_argument("Вы должны ввести число!");
     if (IsValidMove(newRoom)) {
      numArrows--;
      if (newRoom == wumpusRoom) {
       wumpusAlive = false;
       cout << endl << "Примите поздравления! Вы убили Вампуса и победили!" << endl;
       cout << endl << "Введите что-нибудь, чтобы вернуться в главное меню" << endl;
       cin >> newRoom;
       WumpusGame::clearInput();
      }
      else {
       cout << endl << "Мимо! Но вы побеспокоили Вампуса..." << endl;
       MoveStartledWumpus(wumpusRoom);
       cout << endl << "Осталось стрел: " << numArrows << endl;
       if (wumpusRoom == currentRoom) {
        cout << endl << "Вампус ворвался в вашу комнату и сожрал вас, игра окончена :(" << endl;
        PlayAgain();
       }
      }
     }
     else {
      cout << endl << "Вы не можете стрелять в эту комнату :(" << endl;
     }
    }
    catch (invalid_argument& error) {
     WumpusGame::clearInput();
     cerr << endl << error.what() << endl;
    }
   }
   else  {
    cout << endl << "У вас больше нет ни одной стрелы :(" << endl;
   }
  break;
  case 3:
   cout << endl << "Выходим из текущей игры..." << endl;
   playerAlive = false;
  break;
  default:
   cout << endl << "Пожалуйста, выберите правильное действие в меню" << endl;
  break;
 }
}

void WumpusGame::MoveStartledWumpus(int roomNum) {
 int rando = rand() % 3;
 if (rando != 3) wumpusRoom = adjacentRooms[roomNum][rando];
}

void WumpusGame::PlayAgain() {
 char reply;
 cout << endl << "Хотите сыграть заново на этой же карте? Введите Y для новой игры" << endl;
 cin >> reply;
 if (reply == 'y' || reply == 'Y') {
  currentRoom = startingPosition;
  wumpusRoom = wumpusStart;
  batRoom1 = bat1Start;
  batRoom2 = bat2Start;
  cout << endl << "Начата новая игра" << endl;
  InspectCurrentRoom();
 }
 else {
  playerAlive = false;
 }
}

void WumpusGame::PlayGame() {
 int choice;
 bool validChoice = false;
 cout << endl << "Запускаем игру..." << endl;
 PlaceWumpus();
 PlaceBats();
 PlacePits();
 PlacePlayer();
 playerAlive = true;
 wumpusAlive = true;
 numArrows = 3;
 InspectCurrentRoom();
 while (playerAlive && wumpusAlive) { //Игровой цикл
  cout << endl << "Выберите действие" << endl << endl <<
   "1) Идти" << endl <<
   "2) Стрельба" << endl <<
   "3) Выход";
  do {
   validChoice = true;
   cout << endl << "Пожалуйста, выберите пункт меню: ";
   try {
    cin >> choice;
    switch (choice) {
     case 1: PerformAction(1); break;
     case 2: PerformAction(2); break;
     case 3: PerformAction(3); break;
     default:
      validChoice = false;
      cout << endl << "Неверный выбор, попробуем ещё раз" << endl;
      WumpusGame::clearInput();
     break;
    }
   }
   catch (...) {
    validChoice = false;
    cout << endl << "Неверный выбор, попробуем ещё раз" << endl;
    WumpusGame::clearInput();
   }
  } while (validChoice == false);
 }
}

void WumpusGame::StartGame() {
 srand(time(NULL));
 int choice;
 bool validChoice;
 bool keepPlaying;
 wumpusStart = bat1Start = bat2Start = -1;
 do {
  keepPlaying = true;
  cout << endl << "Добро пожаловать в \"Охоту на Вампуса\"" << endl << endl <<
   "1) Новая игра" << endl <<
   "2) Помощь" << endl <<
   "3) Выход" << endl;
  do {
   validChoice = true;
   cout << endl << "Пожалуйста, выберите пункт меню: ";
   try {
    cin >> choice;
    switch (choice) {
     case 1: PlayGame(); break;
     case 2: PrintInstructions(); break;
     case 3: 
      cout << endl << "Выполняем выход..." << endl;
      keepPlaying = false;
     break;
     default:
      validChoice = false;
      cout << endl << "Неверный выбор, попробуем ещё раз" << endl;
      WumpusGame::clearInput();
     break;
    }
   }
   catch (...) {
    validChoice = false;
    cout << endl << "Неверный выбор, попробуем ещё раз" << endl;
    WumpusGame::clearInput();
   }
  } while (validChoice == false);
 } while (keepPlaying);
}

int main() {
 setlocale(LC_ALL, "Rus"); 
 SetConsoleCP(1251); SetConsoleOutputCP(1251);
 WumpusGame game;
 game.StartGame();
 return 0;
}
Скриншот запущенной игры Вампус
Скриншот запущенной игры Вампус
Как может выглядеть простой игровой цикл в консоли?

Чтобы не делать отдельной заметки, пусть будет ещё вот такая простейшая консольная "Змейка" исходником для примера.

Замечу, что даже если задача достаточно проста, всё равно хорошо, когда в проекте есть классы, представляющие основные сущности (у нас это "поле", "пища" и "змея"), логика приложения реализована отдельно, а оформление - отдельно, игровой цикл наглядно читается, понятно, как потом можно дальше с этим работать и модифицировать. Просто "набор функций", не связанных функционалом классов, увы, быстро превращается в хаотичную вещь, уж поверьте моему опыту.

#include <iostream>
#include <windows.h>

using namespace std;

struct position {
 int x, y;
};


class fieldClass {
 static const int height;
 static const int width;
 char** field;
 fieldClass(const fieldClass&);
 fieldClass operator=(const fieldClass&);
public:
 fieldClass() {
  field = new char* [fieldClass::height];
  for (int c = 0; c < fieldClass::height; ++c) {
   field[c] = new char[fieldClass::width];
  }
 }
 ~fieldClass() {
  for (int c = 0; c < fieldClass::height; ++c) {
   delete[] field[c];
  }
  delete[] field;
 }

 void print() {
  for (int c = 0; c < height; ++c) {
   for (int r = 0; r < width; ++r) {
    cout << field[c][r];
   }
   cout << endl;
  }
 }

 void clear() {
  for (int c = 0; c < height; ++c) {
   for (int r = 0; r < width; ++r) {
    field[c][r] = ' ';
   }
  }
 }

 int get_width() const { return width; }
 int get_height() const { return height; }


 void draw(int y, int x, char what) {
  y = (y < 0) ? 0 : (y >= height ? height - 1 : y);
  x = (x < 0) ? 0 : (x >= width ? width - 1 : x);
  field[y][x] = what;
 }

} field;


class foodClass {
 position pos;
 char symbol;
public:
 foodClass() : symbol('X'), pos() {
  pos.x = pos.y = -1;
 }

 void set_pos(int x, int y) {
  pos.x = x;
  pos.y = y;
 }

 void reposition(const fieldClass& field) {
  pos.x = rand() % field.get_width();
  pos.y = rand() % field.get_height();
 }

 int get_x() const { return pos.x; }
 int get_y() const { return pos.y; }
 char get_symbol() const { return symbol; }
} food;

class snakeClass {
 enum { UP, DOWN, LEFT, RIGHT } dir;
 char symbol, head_symbol;
 position pos[100];
 position& head;
 int speed;
 int size;
 bool can_turn;
public:
 snakeClass(int x, int y) :
  symbol('#'), head_symbol('@'), pos(),
  speed(1), size(1), dir(RIGHT),
  head(pos[0]), can_turn(true)
 {
  pos[0].x = x;
  pos[0].y = y;
 }

 bool check_food(const foodClass& food) {
  if (food.get_x() == head.x && food.get_y() == head.y) {
   if (size < 100) size++;
   return true;
  }
  return false;
 }

 void get_input(const fieldClass& field) {
  if (GetAsyncKeyState(VK_UP) && dir != DOWN) {
   dir = UP;
  }
  if (GetAsyncKeyState(VK_DOWN) && dir != UP) {
   dir = DOWN;
  }
  if (GetAsyncKeyState(VK_LEFT) && dir != RIGHT) {
   dir = LEFT;
  }
  if (GetAsyncKeyState(VK_RIGHT) && dir != LEFT) {
   dir = RIGHT;
  }
 }

 void move(const fieldClass& field) {
  position next = { 0, 0 };
  switch (dir) {
  case UP:
   next.y = -speed;
   break;
  case DOWN:
   next.y = speed;
   break;
  case LEFT:
   next.x = -speed;
   break;
  case RIGHT:
   next.x = speed;
  }
  for (int c = size - 1; c > 0; --c) {
   pos[c] = pos[c - 1];
  }
  head.x += next.x;
  head.y += next.y;

  if (head.x < 0 || head.y < 0 || head.x >= field.get_width() || head.y >= field.get_height()) {
   throw "DEAD :(";
  }
 }

 void draw(fieldClass& field) {
  for (int c = 0; c < size; ++c) {
   if (c == 0) {
    field.draw(pos[c].y, pos[c].x, head_symbol);
   }
   else {
    field.draw(pos[c].y, pos[c].x, symbol);
   }
  }
 }

 int get_x() const { return head.x; }
 int get_y() const { return head.y; }
 char get_symbol() const { return symbol; }
} snake(1, 1);


const int fieldClass::height = 24;
const int fieldClass::width = 79;

void hidecursor() {
 HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE);
 CONSOLE_CURSOR_INFO info;
 info.dwSize = 100;
 info.bVisible = FALSE;
 SetConsoleCursorInfo(consoleHandle, &info);
}

int main() {
 system("mode con cols=80 lines=25");
 hidecursor();

 field.clear();
 food.set_pos(5, 5);

 while (1) { //игровой цикл
  field.clear();

  snake.get_input(field);
  try {
   snake.move(field);
  }
  catch (const char* er) {
   field.clear();
   cerr << er << endl;
   system("pause");
   return -1;
  }
  snake.draw(field);
  field.draw(food.get_y(), food.get_x(), food.get_symbol());

  if (snake.check_food(food)) {
   food.reposition(field);
  }

  field.print();

  Sleep(1000 / 30);
  system("cls");
 }

 return 0;
}

13.06.2020, 08:16 [623 просмотра]


теги: c++ random программирование ретро игра