// Suggestions and hints: // - Read the 'main' function to understand how the code is used. // - Don't worry too much about rewriting everything. Find a few // places where you can demonstrate your knowledge and only change // those places. // - The wishlists are just hints/suggestions. You don't have to // follow them if you don't want to. // - It is more important to understand how the code is used rather // than what it does. // - To help you understand the code it might be helpful to make small // changes in the code and then see what effect it has on the // output. // - It is ok if the behaviour of the code changes, as long as it // stays approximately the same. Use your own judgement. // - There are a lot of comments in this file, read them. #include #include #include #include #include // This enum represents the different types of questions that can be // used in this quiz framework. // - Single: a question with multiple alternative answers where only // one of the alternatives is correct. // - Multi : a question with multiple alternative answers where one of // more of the alternatives are correct. For the user the // get the - correct answer they must pick exactly those // alternatives that are - correct, no more and no less. // - Text : a question where the user have to enter their answer as // text, so no alternatives are available. The answer must be // a single word (no spaces allowed). enum Question_Type { Single, Multi, Text }; // A struct representing a question in the quiz. A question can be one // of three different types (see above). // 'description' is used for all the different types: it is a string // that contains the actual question. // 'alternatives' is used if 'type' is either Single or Multi // (i.e. when there are multiple alternatives to choose from). // 'single_answer' is used when 'type' is Single. It contains a number // representing which alternative is correct (it is like an index in // the 'alternatives' vector, but it starts at 1 instead of 0). // 'multi_answer' is used when 'type' is Multi. It is like // 'single_answer', but it can contain more than one alternative. // 'text_answer' is used when 'type' is Text. It contains the string // which is the correct answer. // Wishlist: // - It would be a good idea to represent the 'type' in some other way. // - It should be easy to add other types of questions. // - Would be nice if the text question could be other types, not just // string answers. // - Can we change the representation of the alternatives and // 'multi_answer' to make comparing them easier (see // 'equal_answers')? struct Question { Question_Type type; std::string description; std::vector alternatives; int single_answer; std::vector multi_answer; std::string text_answer; }; // Function used to create a Single question Question* create_single(std::string description, std::vector alternatives, int answer) { Question* result { new Question{} }; result->type = Single; result->description = description; result->alternatives = alternatives; result->single_answer = answer; return result; } // Function used to create a Multi question Question* create_multi(std::string description, std::vector alternatives, std::vector answer) { Question* result { new Question{} }; result->type = Multi; result->description = description; result->alternatives = alternatives; result->multi_answer = answer; return result; } // Function used to create a Text question // Wishlist: // - would be nice if we could generalize Text so that it works with more types than strings specifically Question* create_text(std::string description, std::string answer) { Question* result { new Question{} }; result->type = Text; result->description = description; result->text_answer = answer; return result; } // This function prints the visual presentation of the question. It // will print the description. If there are alternatives, it presents // them in a numbered list starting from 1. // Wishlist: // - Would be nice if we didn't have to check the type of the question. void print(std::ostream& os, Question& question) { os << question.description << '\n' << std::endl; if (question.type == Single || question.type == Multi) { os << "Alternatives:" << std::endl; for (int i { 0 }; i < question.alternatives.size(); ++i) { os << (i + 1) << ". " << question.alternatives[i] << std::endl; } } } // Check if the elements in 'user' are present in 'answer' bool contains(std::vector user, std::vector answer) { // go through each alternative in 'user' and make sure that // alternative is present in 'answer' for (int i { 0 }; i < user.size(); ++i) { bool found { false }; for (int j { 0 }; j < answer.size(); ++j) { if (answer[j] == user[i]) { found = true; break; } } // The current value in 'user' doesn't exist in 'answer' if (!found) { return false; } } return true; } // Helper function that is used to check if two vectors of integers // contain the exact same integers (different orders and duplicates // are allowed so for example: { 1, 1, 3, 2 } is the same as { 3, 3, // 2, 1, 2, 2 } since they both uniquely contains { 1, 2, 3 }). // This function is used to check if the alternatives that the user // selected in a Multi question correspond to the correct answer. // Wishlist: // - Maybe this can be done in a more efficient way? // - Could the representation of 'user' and 'answer' help us make this // better? bool equal_answers(std::vector user, std::vector answer) { return contains(user, answer) && contains(answer, user); } // This function prints the question (and any potential alternatives) // and then allows the user to enter their answer. This function will // return 'true' if the answer was correct and 'false' otherwise. The // method for answering the question differs depending on the type of // question it is: // - Single: Allows the user to enter exactly one number. // - Multi : Allows the user to enter a space separated list of // numbers. // - Text : Allows the user to enter one word as a string. // Note that this function doesn't contain any error handling except // for some basic buffer handling. // The parameter 'os' indicates which stream the question should be // printed to and 'is' indicates which stream the user will input // their answer from. // Wishlist: // - Would be nice to split this function in such a way that we don't // need the enumeration of the type variable. bool handle(Question& question, std::ostream& os, std::istream& is) { print(os, question); std::string line; if (question.type == Single) { int index; os << "Select a single alternative: "; is >> index; is.ignore(1000, '\n'); return index == question.single_answer; } else if (question.type == Multi) { std::vector indices; os << "Select one or more alternatives: "; std::getline(is, line); int index; std::istringstream iss { line }; while (iss >> index) { indices.push_back(index); } return equal_answers(indices, question.multi_answer); } else if (question.type == Text) { std::string answer; os << "Enter your answer: "; is >> answer; is.ignore(1000, '\n'); return answer == question.text_answer; } return false; } // Helper function used to calculate scores in the 'evaluate' function int add_score(int score, int) { return score + 1; } // Helper function used to calculate scores in the 'evaluate' function std::string append_score(std::string score, int index) { return score + std::to_string(index + 1) + " "; } // Struct representing a Quiz, which is simply a collection of questions. // Wishlist: // - It would be nice if we could index the questions on either // numbers or names (strings). Right now vector only allows us to // access questions based on their index. // - It shouldn't be possible to modify 'questions' in any way except // with the 'add' function below. // - This object might cause memory leaks. That should probably be fixed. struct Quiz { std::vector questions; }; // 'evaluate' is used to present each question in the quiz in // sequence. Once the user have given their answer to a question this // function will tell the user if they gave the correct answer or not // and then move on to the next question. // It will present the information on the 'os' stream and it will // accept answers from the 'is' stream. // The score is added to the 'score' variable which is referenced in this function. // The score is calculated with a function passed to the // 'score_counter' parameter. // 'score_counter' expects a function that takes two parameters: the // first parameter is the current value of 'score' and the second // parameter is the index of the current question (you can change this // second parameter to something else if you wish). // There are two versions of evaluate, one version where the score is // an integer and another where the score is a string. // Wishlist: // - The two versions of evaluate have the exact same body, can they // be merged somehow? // - Would be nice if we could pass any callable object to // 'score_counter' parameter // - Maybe it should be possible for the programmer to decide what // type the score should be and not just restrict them to either // integer or string. void evaluate(Quiz& quiz, std::ostream& os, std::istream& is, int& score, int(*score_counter)(int, int)) { for (int i { 0 }; i < quiz.questions.size(); ++i) { // print the current questions header os << "=== Question #" << (i + 1) << " ===\n" << std::endl; // handle the question, and if the user gave the correct // answer we will add to the score with 'score_counter' and // print 'Correct!', otherwise it will print 'Wrong...' if (handle(*quiz.questions[i], os, is)) { os << "\nCorrect!\n" << std::endl; score = score_counter(score, i); } else { os << "\nWrong...\n" << std::endl; } } } void evaluate(Quiz& quiz, std::ostream& os, std::istream& is, std::string& score, std::string(*score_counter)(std::string, int)) { for (int i { 0 }; i < quiz.questions.size(); ++i) { // print the current questions header os << "=== Question #" << (i + 1) << " ===\n" << std::endl; // handle the question, and if the user gave the correct // answer we will add to the score with 'score_counter' and // print 'Correct!', otherwise it will print 'Wrong...' if (handle(*quiz.questions[i], os, is)) { os << "\nCorrect!\n" << std::endl; score = score_counter(score, i); } else { os << "\nWrong...\n" << std::endl; } } } // This function adds a question to the quiz void add(Quiz& quiz, Question* question) { quiz.questions.push_back(question); } // Function used to cleanup the quiz and remove all questions. // Wishlist: // - This still leaks memory. Should be fixed... // - It would be nice if this function was automatically called by the // compiler once we are done with the Quiz object. void destroy(Quiz& quiz) { quiz.questions.clear(); } int main() { Quiz quiz{}; // Create some test questions add(quiz, create_single("Alternative 1 is correct.", { "A", "B", "C" }, 1)); add(quiz, create_multi("Alternative 1 and 2 are correct.", { "A", "B", "C" }, { 1, 2 })); add(quiz, create_text("Write 'correct'.", "correct")); // This block tests if 'evaluate' has the correct behaviour when // the score is a string. This block should not produce any output // what-so-ever. { std::ostringstream oss {}; std::istringstream iss { "1\n1 2\ncorrect\n" }; std::string result { }; evaluate(quiz, oss, iss, result, append_score); assert(result == "1 2 3 "); } // This block tests if 'evaluate' has the correct behaviour when // the score is an integer. This block should not produce any // output what-so-ever. { std::ostringstream oss {}; std::istringstream iss { "1\n1 3\nincorrect\n" }; int score {}; evaluate(quiz, oss, iss, score, add_score); assert(score == 1); } // Here we test that the interactive part of the quiz works as expected. This block will run the quiz { int score { }; evaluate(quiz, std::cout, std::cin, score, add_score); // would be nice if I could do this: // evaluate(quiz, score, [](int score, int index) { return score * (index + 1); }); std::cout << "Your score was: " << score << std::endl; } destroy(quiz); }