О чем я хочу рассказать
Я студент-программист, и пока не дошёл до того уровня, чтобы делать что-то серьезное, а делать банальные задачки для практики уже как-то надоело. И вот, как и, скорее всего, большинству студентов, мне захотелось сделать что-то своё. Мой выбор остановился на простецкой игре - виселице, запускаемой в консоли. C++ я выбрал по тому, что лучше всего его знаю. Здесь я хочу рассказать об основных моментах написания кода, и кратенько его разобрать.
Краткое описание игры
Виселица (Hangman) - очень простая игра, цель которой угадать слово за ограниченное количество попыток. Вариаций этой игры много, они различаются как наличием или отсутствием различных тем, так и разным количеством попыток. В моем же варианте будет всего пять тем ( но ничего не мешает добавить еще), и семь попыток на то, чтобы угадать слово.
Работа с консолью Windows
Рас мы работаем с консолью, нам нужно как-то получить к ней доступ. В этом нам помогут несколько библиотек и Windows API:
#include <windows.h>
#include <conio.h>
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
#include <windows.h> - это заголовочный файл для работы с функциями операционной системы Windows. В нем содержатся объявления и определения функций для работы с окнами, файлами и другими ресурсами операционной системы .
#include <conio.h> - это заголовочный файл, используемый для работы с функциями ввода-вывода в консоль. В нем содержатся функции для чтения символов с клавиатуры, вывода текста на экран и управления курсором.
"HANDLE" - это тип данных, используемый для представления дескриптора объекта.
"hStdOut" - переменная, которой присваивается дескриптор.
"GetStdHandle(STD_OUTPUT_HANDLE)" - это функция, которая возвращает дескриптор стандартного выходного потока. С помощью этого дескриптора программы могут выполнять операции записи или вывода на стандартный выходной поток.
Координаты и цвет
Доступ к консоли мы получили, теперь нужно как-то ориентироваться на 'полотне'.
void SetXY(short X, short Y){
COORD coord = { X, Y };
SetConsoleCursorPosition(hStdOut, coord);
Я решил сделать это с помощью функции void, передавая в неё две координаты (X,Y).
"COORD" - структура, которая определяет координаты символьного знакоместа в экранном буфере консоли. Начало системы координат (0,0) - верхняя левая ячейка. X увеличивается как все и привыкли - в правую сторону, а вот Y увеличивается вниз.
"coord" - переменная которая представляет собой структуру с двумя полями: X и Y.
"SetConsoleCursorPosition" - функция, которая устанавливает положение курсора в указанном буфере экрана консоли .
Мы разобрались с тем, как выводить что-либо на экран. А что на счет цвета? Здесь приведены основные основные цвета, при нужде можно погуглить и все остальные.
enum ConsoleColor
{
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
LightMagenta = 13,
Yellow = 14,
White = 15
};
"enum" - это один из способов определения типов, отличительная особенность которого заключается в том, что перечисления содержат набор числовых констант.
"ConsoleColor" - это перечисление, которое определяет константы, определяющие основной цвет и цвет фона консоли.
Для установки цвета я так же использовал функцию void.
void SetColor(ConsoleColor text, ConsoleColor background){
SetConsoleTextAttribute(hStdOut, (WORD)((background) | text));
}
"SetConsoleTextAttribute" это функция для настройки цвета
"WORD" - тип данных для хранения значений символов, цветов и других атрибутов консоли в Windows.
"text" и "background" - переменные для хранения значений цвета.
Вывод данных и очистка
SetColor(DarkGray, Black); SetXY(0,0); cout << "0" <<endl;
В основном весь вывод выглядит так. Сначала устанавливается свет текста и фона, после устанавливается положение курсора и с помощью cout выводится то, что нам надо. При этом SetColor распространяется на все последующие выводы, и если нужно вывести несколько строчек или символов одного цвета подряд, то SetColor нужно написать только при первом выводе.
SetColor(Green, Black); SetXY(0,0); cout << "00000" <<endl;
SetXY(1,1); cout << "11111" <<endl;
Мы вывели несколько цифр на экран. Теперь нам нужно их стереть. Сделать это можно несколькими способами.
1) Можно вывести что-то другое на те же координаты. Но это способ сработает только в тех случаях, когда длина новой строки либо равна либо больше предыдущей. Если новая строка будет короче предыдущей, например на место "CARROT" вывести "apple", то получится наложение только на одинаковую длину, "appleT". Можно добавить пробел, если длина отличается на несколько символов, и не помешает вам в дальнейшем. На место "CARROT" выводим "apple " и получаем "apple ". В этом случае символ заменяется на пробел, и если background установлен такой же, то это будек как бы "пустота".
2) ANSI Escape-последовательности - специальные управляющие последовательности символов, которые используются для управления форматированием текста и курсором в терминалах и консолях. Можно использовать "cout << "\x1B[2K\r"". Здесь \x1B - это эквивалент символа ESC, который используется для управления ANSI Escape-последовательностями, 2K - это команда для очистки всей строки, а \r - перенос курсора в начало строки. В большинстве случаев это должно работать на Unix-подобных системах (Linux, macOS), но на Windows может потребоваться дополнительная настройка или использование сторонних библиотек.
3) Полная очистка терминала. Это можно использовать когда угодно, в моем же случае при переходе из меню в игру и обратно.
...
void clearScreen() {
system("cls");
}
...
int main(){
...
clearScreen();
...
}
Этот способ работает безупречно, но использование "system("cls")" может считаться не самым эффективным способом очистки экрана, так как это вызывает выполнение команды оболочки системы, что может повлечь за собой некоторые нежелательные побочные эффекты или уязвимости. Так же важно учитывать, что это является зависимым от операционной системы и может не работать на всех платформах.
Вместо "system("cls")" можно использовать "cout << "\x1B[2J"", чем я не пользовался, так как решил обойтись без ANSI Escape-последовательностей.
Я использовал функцию void, так как мне так удобней, но ничего не мешает использовать просто "system("cls")" или "cout << "\x1B[2J"" прямо в функции main() или любом другом месте.
Обработка нажатий на клавиатуру в реальном времени
int main() {
int key;
while (true) {
if (_kbhit()) { // Проверяем, была ли нажата клавиша
key = _getch(); // Получаем нажатую клавишу
cout << "Нажата клавиша: " << key << endl;
if (key == 27) { // 27 - ASCII-код клавиши "Esc"
break; // Выход из цикла при нажатии клавиши "Esc"
}
}
}
}
Для хранения информации о нажатой клавише обычно используют переменную "key".
Цикл "while(true)" используется для создания бесконечного цикла. Когда условие в скобках равно true, цикл будет выполняться бесконечно, пока не будет прерван внутри цикла. В данном случае он используется для непрерывного отслеживания нажатых клавиш. Программа будет ожидать нажатия клавиши и выводить ее код ASCII в консоль. Если будет нажата клавиша "Esc", программа завершит свою работу с помощью оператора break.
"_kbhit()" - это функция, которая проверяет, была ли нажата какая-либо клавиша на клавиатуре. Она возвращает ненулевое значение, если нет нажатой клавиши, и возвращать значение, отличное от нуля (обычно 1) если клавиша была нажата.
"_getch()" - это функция, которая считывает следующий символ из консоли без ожидания нажатия клавиши Enter. Она возвращает ASCII-код символа, который был нажат на клавиатуре. Обычно её используют после "_kbhit()", чтобы получить символ, который был нажат.
В моем же коде это выглядит так:
int main() {
...
while (true) {
...
while (key != 's' && key != 'S' && key != 27) { // Ждем нажатие "S" или "Esc"
if (_kbhit()) {
key = _getch();
clearScreen();
}
}
if (key == 27) { // Если "Esc" была нажата, выход из программы
break;
}
}
}
Здесь, внутренний цикл while проверяет какая именно клавиша была нажата, и при нажатии "S", в моём случае означающей "Start", очищается экран и цикл игры начинается заново, а при нажатии на "Esc" прерывает цикл, тем самым закрытая программу.
Выбор темы
#include <ctime>
...
vector <int> wordsKey = {1, 2 ,3 ,4 ,5 };
vector <string> wordsA = {...};
...
while (!(key == 'a' || key == 'A' ...)) {
if (_kbhit()) {
key = _getch();
if (key == 'r' || key == 'R') {
key = wordsKey[rand() % wordsKey.size()];
break;
}
}
}
...
void ChooseWord(){
if (key == 'a' || key == 'A' || key == 1){
word = wordsA[rand() % wordsA.size()];
}
...
}
...
int main(){
srand(time(NULL));
...
}
В моём случае тем пять, и цикл ожидает нажатие одной из соответствующих клавиш. Плюсом можно выбрать случайную тему нажатием на "R" - "key = wordsKey[rand() % wordsKey.size()]" в этом случае переменная key будет равняться случайному числу из вектора "wordsKey".
Случайный выбор был сделан с помощью "srand(time(NULL))" - эта функция используется для инициализации генератора псевдослучайных чисел. "time(NULL)" возвращает текущее время в секундах. При использовании "time(NULL)" в качестве аргумента для "srand", мы инициализируем генератор псевдослучайных чисел новым стартовым значением, которое зависит от текущего времени.
"word = wordsA[rand() % wordsA.size()]". В этой строке мы выбираем случайный элемент вектора, номер которого равняется остатку от деления псевдослучайного числа на длину вектора.
Ввод слова
int attemptsLeft = 7;
int enterX = ...;
char guess;
...
SetXY(X,Y); cout << "Введите букву: ";
...
while (attemptsLeft > 0 && !isWordGuessed()){
...
printGuessedWord();
enterX++;
SetXY(enterX,Y);
cin >> guess;
...
}
Переменная "attemptsLeft" содержит в себе число попыток (ошибок), в моем случае это семь.
Переменная "enterX" в моём случае содержит в себе число - позицию курсора по оси X, на этом месте и отображаются введенный нами буквы.
Переменная "guess" записывает в себя введенную букву, чтобы после проверить её наличие в загаданном слове.
После каждой введенной буквы, к координате X курсора прибавляется единица "enterX++", тем самым, при следующем срабатывании цикла while курсор передвинется вправо на единицу.
Важно учитывать, что переменная типа char может содержать в себе лишь одну букву, и в случае, когда мы введем сразу несколько букв, цикл while последовательно отработает с каждой буквой по очереди.
string guessedLetters;
...
if (word.find(guess) != string::npos) {
guessedLetters += guess;
} else {
attemptsLeft--;
}
Здесь же if проверяет, содержится ли буква из переменной "guess" в слове "word". Метод find() ищет первое вхождение подстроки "guess" в строке "word". Если найденное значение не равно "string::npos", то это означает, что буква была угадана и добавляется к переменной "guessedLetters" которая хранит все угаданные буквы.
Если же буква не была угдана, то мы уменьшаем количество попыток "attemptsLeft--".
"string::npos" представляет собой статическую константу в классе string, которая используется для обозначения отсутствия значения.
bool isWordGuessed() {
for (char c : word) {
if (guessedLetters.find(c) == string::npos) {
return false;
}
}
return true;
}
Эта функция проверяет, были ли все буквы из переменной word угаданы, находятся ли они в переменной "guessedLetters". Если хотя бы одна буква из загаданного слова не была угадана, функция вернет false, иначе вернет true.
int length = word.length();
...
void printGuessedWord() {
int m = (Xx - length/2);
SetXY(m,Y);
for (char c : word) {
if (guessedLetters.find(c) != string::npos) {
cout << c;
} else
{
cout << "_";
}
}
}
Этот код представляет функцию "printGuessedWord()", которая используется для вывода угаданных букв и пропусков в слове word на экране.
Если все слова будут выводится в одно месте, это бы выглядело не особо красиво и правильно, ведь они все имеют разную длину. Я же решил выводить слово, привязав его центр к определенной координате. Тем самым слова будет выводится в одном месте, но координата начала слова будет зависеть от его длины.
Переменная "length" содержит в себе длину загаданного слова.
Переменная "m" вычисляет начальную позицию по оси X для вывода слова. Она рассчитывается как разность между значением Xx - определенной координате, на которой будет находится центр слова и половиной длины слова "length/2".
Затем цикл for перебирает каждую букву c в загаданном слове. Если буква содержится в "guessedLetters", т.е. если она была угадана, то она выводится на экран на той позиции, на которой она находится в загаданном слове. Изначально вместо слова выводятся "_" во всю его длину, тем самым мы можем понять, сколько букв в слове.
Заключение
Считаю, что рассказал о всём, о чём хотел. Опыта в написании статей у меня нет, пишу в первый раз, и надеюсь что получилось читабельно и понятно. Так же надеюсь, что то, о чём я тут написал, может кому-то пригодиться. Ниже я оставлю полный код получившейся игры.
Если вдруг где-то ошибка, описка или неточность, или если вы хотите что-то дополнить, то добро пожаловать в комментарии. Комментарии читаю, и если нужно, то всё вышеперечисленное исправляю. Спасибо что прочитали!
#include <iostream>
#include <string>
#include <vector>
#include <ctime>
#include <windows.h>
#include <conio.h>
using namespace std;
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
vector <int> wordsKey = {1, 2 ,3 ,4 ,5 };
vector <string> wordsA = {"fish", "dog", "cat", "elephant", "lion", "tiger", "monkey", "zebra", "giraffe", "bear", "kangaroo", "koala", "penguin", "seal", "hippo", "chiken", "wolf", "panda", "frog", "owl", "bunny", "ladybug", "fox", "dragonfly", "sheep", "octopus", "butterfly", "whale", "sheep", "horse", "goat", "pig", "snake"};
vector <string> wordsP = {"flower", "tree", "grass", "sunflower", "rose", "cactus", "lily", "dandelion", "tulip", "oak", "birch", "pine", "iris", "lotus", "violet", "dandelion"};
vector <string> wordsF = {"apple", "peach", "banana", "orange", "strawberry", "grapefruit", "carrot", "potato", "sandwich", "chicken", "beef", "salad", "cake"};
vector <string> wordsC = {"dress", "shirt", "pants", "skirt", "jeans", "sweater", "jacket", "hat", "shoes", "socks", "gloves", "scarf", "bra", "panties", "boots", "junper", "blouse"};
vector <string> wordsI = {"pencil", "book", "chair", "table", "computer", "desk", "notebook", "pen", "calculator", "clock", "backpack", "ruler", "scissors", "marker", "board"};
string word, guessedLetters;
int length, key;
void SetXY(short X, short Y){
COORD coord = { X, Y };
SetConsoleCursorPosition(hStdOut, coord);
}
enum ConsoleColor{
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
White = 15
};
void SetColor(ConsoleColor text, ConsoleColor background){
SetConsoleTextAttribute(hStdOut, (WORD)((background) | text));
}
void Start() {
SetColor(Cyan, Black); SetXY(12,3); cout << "To start the game, select a theme:" << endl;
SetColor(LightCyan, Black);
SetXY(12,5); cout << "Animals - 'A'. Plants - 'P'." << endl;
SetXY(24,7); cout << "Food - 'F'." << endl;
SetXY(12,9); cout << "Clothes - 'C'. Items - 'I'."<< endl;
SetXY(20,11); cout << "Random theme - 'R'." << endl;
SetColor(DarkGray, Black); SetXY(10,13); cout << "(To select, press the appropriate key)" << endl;
while (!(key == 'a' || key == 'A' || key == 'p' || key == 'P' || key == 'f' || key == 'F' || key == 'c' || key == 'C' || key == 'i' || key == 'I')) {
if (_kbhit()) {
key = _getch();
if (key == 'r' || key == 'R') {
key = wordsKey[rand() % wordsKey.size()];
break;
}
}
}
}
void ChooseWord(){
if (key == 'a' || key == 'A' || key == 1){
word = wordsA[rand() % wordsA.size()];
SetColor(Cyan , Black); SetXY(3,17); cout << "Theme: animals." << endl;
}
else if (key == 'p' || key == 'P' || key == 2){
word = wordsP[rand() % wordsP.size()];
SetColor(Cyan , Black); SetXY(3,17); cout << "Theme: plants." << endl;
}
else if (key == 'f' || key == 'F' || key == 3){
word = wordsF[rand() % wordsF.size()];
SetColor(Cyan , Black); SetXY(3,17); cout << "Theme: food." << endl;
}
else if (key == 'c' || key == 'C' || key == 4){
word = wordsC[rand() % wordsC.size()];
SetColor(Cyan , Black); SetXY(3,17); cout << "Theme: clothes." << endl;
}
else if (key == 'i' || key == 'I' || key == 5){
word = wordsI[rand() % wordsI.size()];
SetColor(Cyan , Black); SetXY(3,17); cout << "Theme: item." << endl;
}
}
void Walls() {
SetColor(Blue, Black);
for (int m = 15; m < 31; m++) {
SetXY(m, 5); cout << "*";
SetXY(m, 14); cout << "*";
}
for (int m = 6; m < 14; m++) {
SetXY(15, m); cout << "*";
SetXY(30, m); cout << "*";
}
}
bool isWordGuessed() {
for (char c : word) {
if (guessedLetters.find(c) == string::npos) {
return false;
}
}
return true;
}
void printHangman(int attemptsLeft) {
SetColor(Brown , Black);
if (attemptsLeft == 7) {
for(int m = 19; m < 26 ; m++) {
SetXY(m,6); cout <<"_";
}
for(int m = 7; m < 13 ; m++) {
SetXY(18,m); cout <<"|";
}
} else if (attemptsLeft == 6) {
SetColor(DarkGray, Black); SetXY(26,7); cout << "|"<<endl;
} else if (attemptsLeft == 5) {
SetColor(White , Black); SetXY(26,8); cout << "0"<<endl;
} else if (attemptsLeft == 4) {
SetColor(White , Black); SetXY(26,9); cout << "|"<<endl;
} else if (attemptsLeft == 3) {
SetColor(White , Black); SetXY(25,9); cout << "/"<<endl;
} else if (attemptsLeft == 2) {
SetColor(White , Black); SetXY(27,9); cout << "\\"<<endl;
} else if (attemptsLeft == 1) {
SetColor(White , Black); SetXY(25,10); cout << "/"<<endl;
} else if (attemptsLeft == 0) {
SetColor(White , Black); SetXY(27,10); cout << "\\"<<endl;
}
}
void printGuessedWord() {
SetColor(LightCyan, Black);
int m = (23 - length/2);
SetXY(m,15);
for (char c : word) {
if (guessedLetters.find(c) != string::npos) {
cout << c;
} else { cout << "_"; }
}
}
void clearScreen() {
system("cls");
}
int main() {
srand(time(NULL));
SetColor(LightGreen, Black); SetXY(19,1); cout <<"Welcome to Hangman!" << endl;
while (true) {
Start();
clearScreen();
Walls();
SetColor(LightRed, Black); SetXY(3,50); cout << "The author of this something - Mustafaev Emil aka DarkmMoonDm.";
ChooseWord();
length = word.length();
int attemptsLeft = 7;
SetXY(3,18); cout << "Enter the letter: ";
int enterX = 20;
char guess;
while (attemptsLeft > 0 && !isWordGuessed()){
SetColor(Cyan, Black); SetXY(5,2); cout << "You have attempts to guess the word.";
SetXY(14,2); cout << attemptsLeft;
printHangman(attemptsLeft);
SetColor(Cyan , Black);
printGuessedWord();
enterX++;
SetColor(LightCyan, Black); SetXY(enterX,18);
cin >> guess;
if (word.find(guess) != string::npos) {
SetColor(Green, Black); SetXY(19,4); cout << "Correct!" << endl;
guessedLetters += guess;
} else {
SetColor(Red, Black); SetXY(19,4);cout << " Wrong! " << endl;
attemptsLeft--;
}
}
printHangman(attemptsLeft);
if (isWordGuessed()) {
int m = (23 - length/2);
SetColor(LightCyan, Black); SetXY(m,15); cout << word;
m = 58-((39+length)/2);
SetColor(Green, Black); SetXY(m,8); cout << "Congratulations, you guessed the word: " << word << '.' << endl;
} else
{
SetColor(Cyan, Black); SetXY(14,2); cout << "0";
int m = 58-((30+length)/2);
SetColor(Red, Black); SetXY(m,8) ;cout << "You couldn't guess the word: " << word << '.'<< endl;
}
SetColor(White, Black);
SetXY(43,10); cout << "To start a new game, press'S'." << endl;
SetXY(48,11); cout << "To exit, press 'Esc'." << endl;
while (key != 's' && key != 'S' && key != 27) {
if (_kbhit()) {
key = _getch();
clearScreen();
}
}
if (key == 27) {break;}
length, key = 0;
word = "";
guessedLetters = "";
}
}