While surfing the net the other day, I stumbled across a quiz that I decided to take. You know the kind – you are asked a question and you may choose one of typically four alternative answers. After a specific number of questions, you are told how well you performed. The thing about this particular quiz was that the response time was a complete disaster – after selecting an answer, I typically had to wait about a minute before seeing the result. Needless to say, I soon gave up. It made me think about how to implement a quiz system, trying to keep response times as low as possible, also in case of a slow network connection.
With long experience in object-oriented design and implementation in Delphi, I decided that such a system would be small enough, while still being usable, for its analysis, design and implementation to fit in a series of three posts. I have decided on a few guidelines for the design:
- The design should not restrict how questions and answers are stored – we will create an abstract base class, which may be overridden to read questions from e.g. text files, XML files or a database. To illustrate this, we will create one subclass containing hard-coded questions and answers, and another subclass that reads information from an ini file.
- In addition to the classes that read questions and answers from some kind of storage medium, we will also create a class that holds information about a game in progress, keeping track of the number of correct and incorrect answers etc. This first post will describe the implementation of the quiz classes, and suitable test projects that validate their behavior.
- We will create our own system for session management, used in the web server. This system will be the sole subject of the second post.
- The server will be implemented as a socket server, interpreting incoming HTTP requests and returning responses. This will be described in the third article.
The quiz classes are separated into two categories – the classes (TQuizManager and its descendants) that hold static information about the different quiz games available and the TQuizGame class which holds dynamic information about a game in progress. I will start by explaining the static quiz classes, and we will get back to TQuizGame later.
The static quiz classes
TQuizManager exposes an interface to its user (in our case TQuizGame, as we shall see later) that allows access to the underlying quizzes:
TQuizManager = class public procedure GetQuizNames(const QuizNames: TStrings); function GetNumberOfQuestions(const QuizName: String): Integer; function GetQuestion(const QuizName: String; QuestionIndex: Integer; out Question, Alt1, Alt2, Alt3, Alt4: String; out Correct: Integer): Boolean; end;
The GetQuizNames method fills the Names string list with the names of all available quizzes. The GetNumberOfQuestions method returns the number of questions for the quiz with given name. The GetQuestion method retrieves the question, alternatives and correct alternative for the question with given index in the quiz with given name. Using this class is rather straight-forward and we will later see how it is done by TQuizGame. We must note, though, that TQuizManager does not load any quiz information by itself; we must derive classes from it. These derived classes should know how to access quiz information, and typically we will create a specific such sub-class, that reads information from e.g. the database of our choice.
For this article we will create two sub-classes, each describing one of two typical ways to use the methods in the base class. If we want the quiz server to be as quick as possible, it makes sense to read all information into memory when starting it, so we don’t have to access e.g. a database whenever we should serve a request. This is feasible for quiz systems of moderate size – with today’s amount of RAM available, we could keep all quizzes in memory all the time. If, on the other hand, we have a large system, with multiple quizzes, each with a lot of questions, this may not be possible.
The THardcodedQuizManager class takes the first approach, which is to load all data into memory as soon as the object is created. To do this, it uses two protected methods supplied by the base class; AddQuiz and AddQuestion. All specialized code is in this case placed in the derived object’s constructor, which makes the implementation of this object very simple.
The TIniFileQuizManager takes the second approach, which means that it accesses quiz data from an external source (in this case an ini file), when that information is requested by a user. This conserves RAM but sacrifices performance. Also, the implementation of TIniFileQuizManager is not as straight-forward as that of THardCodedQuizManager. In this case, instead of simply calling two methods implemented by the base class, we override the three methods InternalGetQuizNames, InternalGetNumberOfQuestions and InternalGetQuestion. Instead of keeping information in memory, these methods are now implemented to read data from an ini file. The ini file used in this case, should have one section per quiz and a number of keys, named “Q1”, “Q2” etc. The value for such a key should be a string consisting of the question, all four alternatives and the index of the correct answer, separated by the pipe character. The following extract indicates the structure of the user file:
[Geography] Q0=What is the capital of Ireland?|London|Dublin|Oslo|Copenhagen|2 Q1=What is the capital of Denmark?|London|Dublin|Oslo|Copenhagen|4
The following code describes how InternalGetQuestion reads a requested question, along with its alternatives and the correct answer:
function TIniFileQuizManager.InternalGetQuestion(const QuizName: String; QuestionIndex: Integer; out Question, Alt1, Alt2, Alt3, Alt4: String; out Correct: Integer): Boolean; var Params: TStrings; begin with TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'Quizes.ini') do begin if SectionExists(QuizName) then begin Params := TStringList.Create; try Params.Delimiter := '|'; Params.StrictDelimiter := True; Params.DelimitedText := ReadString(QuizName, Format('Q%d', [QuestionIndex]), ''); if Params.Count > 5 then begin Question := Params; Alt1 := Params; Alt2 := Params; Alt3 := Params; Alt4 := Params; Correct := StrToIntDef(Params, 0); Result := True; end else Result := False; finally Params.Free; end; end else Result := False; Free; end; end;
The two approaches selected may of course be combined; if you implement a system with a lot of available quizzes, you could for instance keep track of which quizzes that are the most popular, and keep them in memory, while loading information for the more infrequently used quizzes on demand.
The dynamic quiz class
The TQuizGame class holds information about a game in progress. For each game, an instance of this type is created and initialized. When initializing the instance, you supply a TQuizManager descendant, a quiz ID and the number of questions you want. After that, you may ask for a new question (questions will be asked in random order, and questions will be asked only once) and answer the previously received question. The object also contains read-only properties letting you ask for the total number of questions, the number of correct answers, the number of incorrect answers, and whether the game is finished.
TQuizGame = class public property Done: Boolean read GetDone; property NumberOfQuestions: Integer read FNumberOfQuestions; property NumberOfCorrectAnswers: Integer read FNumberOfCorrectAnswers; property NumberOfIncorrectAnswers: Integer read FNumberOfIncorrectAnswers; function GetNextQuestion(out Question, Alt1, Alt2, Alt3, Alt4: String; out Number: Integer): Boolean; function Respond(Answer: Integer; out CorrectAnswer: Integer): Boolean; function Initialize(aQuiz: TQuizManager; const aQuizName: String; aNumberOfQuestions: Integer): Boolean; end;
A TQuizGame object holds an array of Boolean values, initially all set to false. When a question is asked, the corresponding entry is set to true. When a client calls GetNextQuestion, it generates a random number, in the range supported by the used quiz. If the question corresponding to that number is already used, it generates a new one etc. Once a new question has been retrieved, the expected answer is stored and the corresponding entry in the array of Boolean values is set to true.
Together, the static and dynamic quiz classes represent all that is needed to write a simple quiz game:
In the downloadable code, the WinQuizGame project contains a simple Windows application that implements a quiz game using these classes. In the next post, I will design a system for managing sessions in the web server, something I will need when it is time to create the actual server in the third post.