/** * @file * @brief A simple [Memory Game](https://en.wikipedia.org/wiki/Matching_game) * with **3 different sizes** and multiple letters. * @details * The game consists on finding **the pair** of all the given letters depending * on the table size. Once all of the instances are all found, the game will end * and will ask you if you'd like to play again or not. * * It provides **3 different sizes** available that the user can choose (4x2, * 5x2, 7x2). 7x2 being the biggest table size and hardest mode. The bigger the * size, **the more letters are available**. * * @author [David Leal](https://github.com/Panquesito7) */ #include /// for std::shuffle() #include /// for std::srand() #include /// for std::time() #include /// for IO operations #include /// for std::mt19937 #include /// for std::vector // `Sleep` is only available in Windows in milliseconds. // However, on Unix/Linux systems it is `sleep`, in seconds. #ifdef _WIN32 #include /// for Sleep() template constexpr typename std::enable_if::value, void>::type SLEEP( T milliseconds) { Sleep(milliseconds * 1000); } #else #include /// for sleep() template constexpr T SLEEP(T seconds) { return sleep(seconds); } #endif /** * @namespace * @brief (Mini)game implementations. */ namespace games { /** * @namespace * @brief Functions for the [Memory * Game](https://en.wikipedia.org/wiki/Matching_game) implementation */ namespace memory_game { /** * @brief Utility function to verify if the given input is a number or not. * This is very useful to prevent the program being stuck in a loop. * @tparam T The type of the input * @param input The input to check. * @returns false if the input IS empty or if it contains a non-digit character * @returns true if the input is NOT empty and if it contains only digit * characters */ template bool is_number(const T &input) { if (std::cin.fail()) { std::cin.clear(); std::cin.ignore(256, '\n'); return false; } return true; } /** * @brief Initializes the table with the letters. * @tparam T The type of the table. * @param table The table to initialize. * @returns void */ template void init(std::vector *table) { std::vector letters(7); // Decrease / increase the number of letters depending on the size. if ((*table).size() == 10) { // 5x2 letters = {'A', 'E', 'Z', 'P', 'D'}; } else if ((*table).size() == 8) { // 4x2 letters = {'A', 'E', 'Z', 'D'}; } else if ((*table).size() == 14) { // 7x2 letters = {'A', 'E', 'Z', 'P', 'D', 'B', 'M'}; } std::vector pairs; for (char letter : letters) { pairs.push_back(letter); pairs.push_back(letter); } std::shuffle(pairs.begin(), pairs.end(), std::mt19937(std::random_device()())); for (int i = 0; i < (*table).size(); i++) { (*table)[i] = pairs[i]; } std::cout << "All available types are: "; for (int i = 0; i < letters.size(); i++) { if (i == letters.size() - 1) { std::cout << "and " << letters[i] << ".\n\n"; } else { std::cout << letters[i] << ", "; } } } /** * @brief Utility function to print the table. * @tparam T The type of the table. * @param table The table to print. * @returns void */ template void print_table(const std::vector &table) { std::cout << "| "; std::vector table_print(table.size()); for (int i = 0; i < table.size(); i++) { table_print[i] = ' '; if (table[i] != 0) { table_print[i] = table[i]; } } for (int i = 0; i < table.size(); i++) { if (i % 5 == 0 && i != 0) { std::cout << "\n| "; } std::cout << table_print[i] << " | "; } } // Prototype function. This is needed as `ask_data` calls `reset_data`, and // `reset_data` calls `ask_data`. template void reset_data(const std::vector &, int *, int *, int *); /** * @brief Function that asks the user for their * input in the table they previously chose. * @tparam T The type of the table. * @param table The table that's used to get the user's input and data. * @param answer The user's answer. * @param old_answer The user's previous answer. * @param memory_count A counter to check if the user has already answered two * values. * @returns void */ template void ask_data(const std::vector &table, int *answer, int *old_answer, int *memory_count) { (*old_answer) = (*answer); print_table(table); std::cout << "\n\nType your response here (number index):\n"; std::cin >> (*answer); if (!is_number((*answer))) { std::cout << "\nYou must enter a valid number.\n\n"; reset_data(table, answer, old_answer, memory_count); } // Increase the memory count, which will be later on used for checking if // the user has already answered two values. (*memory_count)++; if (((*answer) > table.size()) || ((*answer) < 1)) { std::cout << "\nYou can't check a value that doesn't exist (or an " "invalid number).\n\n"; reset_data(table, answer, old_answer, memory_count); } if ((*old_answer) == (*answer)) { std::cout << "\nYou can't check the same value twice.\n\n"; reset_data(table, answer, old_answer, memory_count); } // If two matches are answered already, but the user checkes a non-answered // and an answered value, the program will mark it as no match, however, we // must not allow the user to check the same value twice. if ((table[(*answer) - 1] != 0) && ((table[(*old_answer)] == 0) || (table[(*old_answer)] != 0))) { std::cout << "\nYou can't check the same value twice.\n\n"; reset_data(table, answer, old_answer, memory_count); } } /** * @brief Utility function that resets the data if the user enters an invalid * value. * @tparam T The type of the table. * @param table The table that will be used to call `ask_data()`. * @param answer The user's answer. * @param old_answer The user's previous answer. * @param memory_count A counter to check if the user has already answered two * values. * @returns void */ template void reset_data(const std::vector &table, int *answer, int *old_answer, int *memory_count) { (*answer) = (*old_answer); (*memory_count)--; ask_data(table, answer, old_answer, memory_count); } /** * @brief Checks if the two values given by the user match. * @tparam T The type of the table. * @param table_empty The table with no values, slowly assigned from `table` * depending on the user's input. * @param table The table with the original values. * @param answer The user's answer. * @param first_time A boolean to check if the user has already answered a * value. * @param old_answer The user's previous answer. * @param memory_count A counter to check if the user has already answered two * values. * @returns true IF the values given by the user match * @returns false if the values given by the user do NOT match */ template bool match(const std::vector &table, std::vector *table_empty, const int &answer, bool *first_time, int *old_answer, int *memory_count) { if ((*first_time) == true) { return true; } // Search across the whole table and if the two values match, keep results, // otherwise, hide 'em up. for (int i = 0; i < table.size() + 1; i++) { if (i == answer) { if (table[i - 1] == table[(*old_answer) - 1]) { (*first_time) = true; (*memory_count) = 0; (*old_answer) = 0; return true; } else { std::cout << "\nNo match (value was " << table[i - 1] << ", index is " << i << ").\n\n"; (*table_empty)[(*old_answer) - 1] = 0; (*table_empty)[answer - 1] = 0; (*first_time) = true; (*memory_count) = 0; (*old_answer) = 0; return false; } } } return false; } /** * @brief Function to assign the results to the table. * * Also checkes if the user has answered all the values already, as well as * verify if the user made a match or not. * @tparam T The type of the tables. * @param table_empty The table with no values, slowly assigned from `table` * depending on the user's input. * @param table The table with the original values. * @param answer The user's answer. * @param first_time A boolean to check if the user has already answered a * value. * @param old_answer The user's previous answer. * @param memory_count A counter to check if the user has already answered two * values. * @returns void */ template void assign_results(std::vector *table_empty, std::vector *table, int *answer, bool *first_time, int *old_answer, int *memory_count) { // Search through the entire table and if the answer matches the index, show // the value. If it doesn't match, hide both the values. Don't forget to // keep older values already answered. for (int i = 0; i < (*table).size() + 1; i++) { if (i == (*answer)) { if (match((*table), table_empty, (*answer), first_time, old_answer, memory_count) == true) { (*table_empty)[i - 1] = (*table)[i - 1]; (*first_time) = true; } } } if ((*memory_count) == 1) { (*first_time) = false; (*memory_count) = 0; } char try_again = 'n'; // Has the user finished the game? Use a `for` loop, and if the table is // full, ask the user if he wants to play again. for (int i = 0; i < (*table).size() + 1; i++) { if ((*table_empty)[i] == 0) { break; } else if (i == (*table).size() - 1) { print_table((*table)); std::cout << "\n\nYou won. Congratulations! Do you want to play " "again? (y/n)\n"; std::cout << "Size " << (*table).size() << " will be used. This can be changed by re-running the game."; std::cin >> try_again; if (try_again == 'y') { // This is needed when checking if the user has two matches // already. for (int i = 0; i < (*table_empty).size(); i++) { (*table_empty)[i] = 0; } init(table); } else if (try_again == 'n') { std::cout << "\nThanks for playing the game!\n"; SLEEP(3); exit(0); } else { std::cout << "\nInvalid input (exitting...).\n"; SLEEP(3); exit(0); } } } // Ask data again. ask_data((*table_empty), answer, old_answer, memory_count); assign_results(table_empty, table, answer, first_time, old_answer, memory_count); } } // namespace memory_game } // namespace games /** * @brief Main function * @returns 0 on exit */ int main() { // Start randomizer. This changes the values every time. std::srand(std::time(nullptr)); int size = 0; ///< Size of the table. int selection = 0; ///< Selection of the size (4x2, 5x2, 7x2). int response = 0; ///< The answer (number index) that the user chose. int old_answer = 0; ///< Previous answer (number index). int memory_count = 0; ///< Counter to check if the user has already answered two values. bool first_time = true; ///< Whether the user has answered 1 value or not ///< (previous answered values do not count). std::cout << "\tMEMORY GAME\n"; std::cout << std::boolalpha; std::cout << std::is_literal_type::value; do { std::cout << "\n1. 4x2 (1)"; std::cout << "\n2. 5x2 (2)"; std::cout << "\n3. 7x2 (3)\n"; std::cout << "\nChoose table size: "; std::cin >> selection; } while ((selection < 1 || selection > 3) && (!games::memory_game::is_number(selection))); switch (selection) { case 1: size = 8; break; case 2: size = 10; break; case 3: size = 14; break; default: size = 10; break; } std::vector table(size); std::vector table_empty(size); std::cout << "\n"; games::memory_game::init(&table); games::memory_game::ask_data(table_empty, &response, &old_answer, &memory_count); games::memory_game::assign_results(&table_empty, &table, &response, &first_time, &old_answer, &memory_count); return 0; }