]> git.somenet.org - irc/bugbot.git/blob - BotModules/Quiz.bm
some old base
[irc/bugbot.git] / BotModules / Quiz.bm
1 # -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*-
2 ################################
3 # Quiz Module                  #
4 ################################
5 # some of these ideas are stolen from moxquizz (an eggdrop module)
6 # see http://www.meta-x.de/moxquizz/
7
8 package BotModules::Quiz;
9 use vars qw(@ISA);
10 @ISA = qw(BotModules);
11 1;
12
13 # XXX high score table
14 # XXX do something with level
15 # XXX make bot able to self-abort if no-one is taking part
16 # XXX implement feature so that users that can be quiz admins in certain channels
17 # XXX accept user submission
18 # XXX README for database format (for now see http://www.meta-x.de/moxquizz/README.database)
19 # XXX pause doesn't stop count of how long answer takes to answer
20 # XXX different quiz formats, e.g. university challenge, weakest link (maybe implement by inheritance?)
21 # XXX stats, e.g. number of questions skipped
22 # XXX category filtering
23
24 sub Help {
25     my $self = shift;
26     my($event) = @_;
27     my $help = {
28         '' => "Runs quizzes. Start a quiz with the $self->{'prefix'}ask command.",
29         $self->{'prefix'}.'ask' => 'Starts a quiz.',
30         $self->{'prefix'}.'pause' => "Pauses the current quiz. Resume with $self->{'prefix'}resume.",
31         $self->{'prefix'}.'resume' => 'Resumes the current quiz.',
32         $self->{'prefix'}.'repeat' => 'Repeats the current question.',
33         $self->{'prefix'}.'endquiz' => 'Ends the current quiz.',
34         $self->{'prefix'}.'next' => 'Jump to the next question (at least half of the active participants have to say this for the question to be skipped).',
35         $self->{'prefix'}.'score' => 'Show the current scores for the round.',
36     };
37     if ($self->isAdmin($event)) {
38         $help->{'reload'} = 'To just reload the quiz data files instead of the whole module, use: reload Quiz Data';
39     }
40     return $help;
41 }
42
43 # RegisterConfig - Called when initialised, should call registerVariables
44 sub RegisterConfig {
45     my $self = shift;
46     $self->SUPER::RegisterConfig(@_);
47     $self->registerVariables(
48       # [ name, save?, settable? ]
49         ['questionSets', 1, 1, ['trivia.en']], # the list of files to read (from the Quiz/ directory)
50         ['questions', 0, 0, []], # the list of questions (hashes)
51         ['categories', 0, 0, {}], # hash of arrays whose values are indexes into questions
52         ['questionsPerRound', 1, 1, -1], # how many questions per round (-1 = infinite)
53         ['currentQuestion', 1, 0, {}], # the active question (per-channel hash)
54         ['questionIndex', 1, 0, 0], # where to start when picking the next question
55         ['skipMargin', 1, 1, 10], # maximum number of questions to skip at a time
56         ['remainingQuestions', 1, 0, {}], # how many more questions this round (per-channel hash)
57         ['questionsTime', 1, 0, {}], # when the question was asked
58         ['quizTime', 1, 0, {}], # when the quiz was started
59         ['paused', 1, 0, {}], # if the game is paused
60         ['totalScores', 1, 1, {}], # user => score
61         ['quizScores', 1, 0, {}], # channel => "user score"
62         ['skip', 1, 0, {}], # channel => "user 1"
63         ['players', 1, 0, {}], # channel => "user last time"
64         ['tip', 1, 0, {}], # which tip should next be given on this channel
65         ['tipDelay', 1, 1, 10], # seconds to wait before giving a tip
66         ['timeout', 1, 1, 120], # seconds to wait before giving up
67         ['skipFractionRequired', 1, 1, 0.5], # fraction of players that must say !skip to skip
68         ['askDelay', 1, 1, 2], # how long to wait between answer and question
69         ['prefix', 1, 1, '!'], # the prefix to have at the start of commands
70     );
71 }
72
73 sub Schedule {
74     my $self = shift;
75     my($event) = @_;
76     $self->reloadData($event);
77     my $fakeEvent = {%$event};
78     foreach my $channel (keys %{$self->{'currentQuestion'}}) {
79         $fakeEvent->{'channel'} = $channel;
80         $fakeEvent->{'target'} = $channel;
81         $self->debug("Restarting quiz in $channel... (qid $self->{'questionsTime'}->{$channel})");
82         $self->schedule($fakeEvent, \$self->{'tipDelay'}, 1, 'tip', $self->{'questionsTime'}->{$channel});
83         $self->schedule($fakeEvent, \$self->{'timeout'}, 1, 'timeout', $self->{'questionsTime'}->{$channel});
84         if ($self->{'questionsTime'}->{$event->{'channel'}} == 0) {
85             $self->schedule($event, \$self->{'askDelay'}, 1, 'ask');
86         }
87     }
88     $self->SUPER::Schedule($event);
89 }
90
91 sub Told {
92     my $self = shift;
93     my($event, $message) = @_;
94     if ($message =~ /^\s*reload\s+quiz\s+data\s*$/osi and $self->isAdmin($event)) {
95         my $count = $self->reloadData($event);
96         $self->say($event, "$count questions loaded");
97     } elsif ($message =~ /^\s*status[?\s]*$/osi) {
98         my $questions = @{$self->{'questions'}};
99         my $quizzes = keys %{$self->{'currentQuestion'}};
100         $self->say($event, "$event->{'from'}: I have $questions questions and am running $quizzes quizzes.", 1); # XXX 1 quizzes
101     } elsif (not $self->DoQuizCheck($event, $message, 1)) {
102         return $self->SUPER::Told(@_);
103     }
104     return 0; # we've dealt with it, no need to do anything else.
105 }
106
107 sub Baffled {
108     my $self = shift;
109     my($event, $message) = @_;
110     if (not $self->quizAnswer($event, $message)) {
111         return $self->SUPER::Baffled(@_);
112     }
113     return 0; # we've dealt with it, no need to do anything else.
114 }
115
116 sub Heard {
117     my $self = shift;
118     my($event, $message) = @_;
119     if (not $self->DoQuizCheck($event, $message, 0) and
120         not $self->quizAnswer($event, $message)) {
121         return $self->SUPER::Heard(@_);
122     }
123     return 0; # we've dealt with it, no need to do anything else.
124 }
125
126 sub DoQuizCheck {
127     my $self = shift;
128     my($event, $message, $direct) = @_;
129     if ($message =~ /^\s*\Q$self->{'prefix'}\Eask\s*$/si) {
130         $self->quizStart($event);
131     } elsif ($message =~ /^\s*\Q$self->{'prefix'}\Epause\s*$/si) {
132         $self->quizPause($event);
133     } elsif ($message =~ /^\s*\Q$self->{'prefix'}\E(?:resume|unpause)\s*$/si) {
134         $self->quizResume($event);
135     } elsif ($message =~ /^\s*\Q$self->{'prefix'}\Erepeat\s*$/si) {
136         $self->quizRepeat($event);
137     } elsif ($message =~ /^\s*\Q$self->{'prefix'}\E(?:end|stop|strivia|exit)(?:quiz)?\s*$/si) {
138         $self->quizEnd($event);
139     } elsif ($message =~ /^\s*\Q$self->{'prefix'}\E(?:dunno|skip|next)\s*$/si) {
140         $self->quizSkip($event);
141     } elsif ($message =~ /^\s*\Q$self->{'prefix'}\E(?:scores)\s*$/si) {
142         $self->quizScores($event);
143     } else {
144         return 0;
145     }
146     return 1;
147 }
148
149 sub reloadData {
150     my $self = shift;
151     my($event) = @_;
152     $self->{'questions'} = [];
153     $self->{'categories'} = {};
154     $self->debug('Loading quiz data...');
155     foreach my $set (@{$self->{'questionSets'}}) {
156         if ($set =~ m/^[a-zA-Z0-9-][a-zA-Z0-9.-]*$/os) {
157             local *FILE;
158             if (not open(FILE, "<BotModules/Quiz/$set")) { # XXX what if the directory has changed?
159                 $self->debug("  * $set (Not loaded; $!)");
160                 next;
161             }
162             $self->debug("  * $set");
163             my $category;
164             my $question = {'tip' => []};
165             while (defined($_ = <FILE>)) {
166                 chomp;
167                 next if m/^\#/os; # skip comment lines
168                 next if m/^\s*$/os; # skip blank lines
169                 if (m/^Category:\s*(.*?)\s*$/os) {
170                     #  Category?                              (should always be on top!)
171                     $category = $1;
172                     if (not defined($self->{'categories'}->{$category})) {
173                         $self->{'categories'}->{$category} = [];
174                     }
175                 } elsif (m/^Question:\s*(.*?)\s*$/os) {
176                     #  Question                               (should always stand after Category)
177                     $question = {'question' => $1, 'tip' => []};
178                     if (defined($category)) {
179                         $question->{'category'} = $category;
180                         undef($category);
181                     }
182                     push(@{$self->{'questions'}}, $question);
183                     push(@{$self->{'categories'}->{$category}}, $#{$self->{'questions'}});
184                 } elsif (m/^Answer:\s*(?:(.*?)\#(.*?)\#(.*?)|(.*?))\s*$/os) {
185                     #  Answer                                 (will be matched if no regexp is provided)
186                     if (defined($1)) {
187                         $question->{'answer-long'} = "$1$2$3";
188                         $question->{'answer-short'} = $2;
189                     } else {
190                         $question->{'answer-long'} = $4;
191                         $question->{'answer-short'} = $4;
192                     }
193                 } elsif (m/^Regexp:\s*(.*?)\s*$/os) {
194                     #  Regexp?                                (use UNIX-style expressions)
195                     $question->{'answer-regexp'} = $1;
196                 } elsif (m/^Author:\s*(.*?)\s*$/os) {
197                     #  Author?                                (the brain behind this question)
198                     $question->{'author'} = $1;
199                 } elsif (m/^Level:\s*(.*?)\s*$/os) {
200                     #  Level? [baby|easy|normal|hard|extreme] (difficulty)
201                     $question->{'level'} = $1;
202                 } elsif (m/^Comment:\s*(.*?)\s*$/os) {
203                     #  Comment?                               (comment line)
204                     $question->{'comment'} = $1;
205                 } elsif (m/^Score:\s*(.*?)\s*$/os) {
206                     #  Score? [#]                             (credits for answering this question)
207                     $question->{'score'} = $1;
208                 } elsif (m/^Tip:\s*(.*?)\s*$/os) {
209                     #  Tip*                                   (provide one or more hints)
210                     push(@{$question->{'tip'}}, $1);
211                 } elsif (m/^TipCycle:\s*(.*?)\s*$/os) {
212                     #  TipCycle? [#]                          (Specify number of generated tips)
213                     $question->{'tip-cycle'} = $1;
214                 } else {
215                     # XXX error handling
216                 }
217             }
218             close(FILE);
219         } # else XXX invalid filename, ignore it
220     }
221     # if no more questions, abort running quizes.
222     if (not @{$self->{'questions'}}) {
223         foreach my $channel (keys %{$self->{'currentQuestion'}}) {
224             local $event->{'channel'} = $channel;
225             $self->say($event, 'There are no more questions.');
226             $self->quizEnd($event);
227         }
228     }
229     return scalar(@{$self->{'questions'}});
230 }
231
232
233 # game implementation
234
235 sub Scheduled {
236     my $self = shift;
237     my($event, @data) = @_;
238     if ($data[0] eq 'tip') {
239         if ($self->{'questionsTime'}->{$event->{'channel'}} == $data[1] and
240             defined($self->{'currentQuestion'}->{$event->{'channel'}})) {
241             # $self->debug('time for a tip');
242             if ($self->{'paused'}->{$event->{'channel'}} or
243                 $self->quizTip($event)) {
244                 $self->schedule($event, \$self->{'tipDelay'}, 1, @data);
245             }
246         }
247     } elsif ($data[0] eq 'timeout') {
248         if ($self->{'questionsTime'}->{$event->{'channel'}} == $data[1] and
249             defined($self->{'currentQuestion'}->{$event->{'channel'}})) {
250             if ($self->{'paused'}->{$event->{'channel'}}) {
251                 $self->schedule($event, \$self->{'timeout'}, 1, @data);
252             } else {
253                 my $answer = $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-long'};
254                 $self->say($event, "Too late! The answer was: $answer");
255                 $self->quizQuestion($event);
256             }
257         }
258     } elsif ($data[0] eq 'ask') {
259         if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) {
260             $self->quizQuestion($event);
261         }
262     } else {
263         $self->SUPER::Scheduled($event, @data);
264     }
265 }
266
267 sub quizStart { # called by user
268     my $self = shift;
269     my($event) = @_;
270     if ($event->{'channel'} ne '' and
271         not defined($self->{'currentQuestion'}->{$event->{'channel'}})) {
272         if (@{$self->{'questions'}} == 0) {
273             # if no questions, complain.
274             $self->say($event, 'I cannot run a quiz with no questions!');
275         } else {
276             # no game in progress, start one
277             $self->{'remainingQuestions'}->{$event->{'channel'}} = $self->{'questionsPerRound'};
278             $self->{'paused'}->{$event->{'channel'}} = 0;
279             $self->{'quizTime'}->{$event->{'channel'}} = $event->{'time'};
280             $self->{'quizScores'}->{$event->{'channel'}} = '';
281             $self->{'players'}->{$event->{'channel'}} = '';
282             $self->quizQuestion($event);
283         }
284     }
285 }
286
287 sub quizQuestion { # called from quizStart or delayed from quizAnswer
288     my $self = shift;
289     my($event) = @_;
290     if ($event->{'channel'} ne '' and # in channel
291         not $self->{'paused'}->{$event->{'channel'}}) { # quiz not paused
292         if ($self->{'remainingQuestions'}->{$event->{'channel'}} != 0) {
293             $self->{'remainingQuestions'}->{$event->{'channel'}}--;
294             my $category = $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'category'};
295             my $try = 0;
296             my $questionCount = scalar keys %{$self->{'questions'}};
297             while ($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'category'} eq $category
298                    and $try++ < $questionCount) {
299                 $self->{'currentQuestion'}->{$event->{'channel'}} = $self->pickQuestion($event);
300             }
301             $self->{'questionsTime'}->{$event->{'channel'}} = $event->{'time'};
302             $self->{'tip'}->{$event->{'channel'}} = 0;
303             $self->{'skip'}->{$event->{'channel'}} = '';
304             $self->schedule($event, \$self->{'tipDelay'}, 1, 'tip', $self->{'questionsTime'}->{$event->{'channel'}});
305             $self->schedule($event, \$self->{'timeout'}, 1, 'timeout', $self->{'questionsTime'}->{$event->{'channel'}});
306             $self->say($event, "Question: $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'question'}");
307             $self->debug("Question: $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'question'}");
308             $self->debug("Answer: $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-long'}");
309             $self->saveConfig();
310         } else {
311             $self->quizEnd($event);
312         }
313     }
314 }
315
316 sub quizAnswer { # called by user
317     my $self = shift;
318     my($event, $message) = @_;
319     if ($event->{'channel'} ne '' and # in channel
320         defined($self->{'currentQuestion'}->{$event->{'channel'}}) and # in quiz
321         $self->{'questionsTime'}->{$event->{'channel'}} and # not answered
322         not $self->{'paused'}->{$event->{'channel'}}) { # quiz not paused
323         $self->stringHash(\$self->{'players'}->{$event->{'channel'}}, $event->{'from'}, $event->{'time'});
324         if (lc($message) eq lc($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-long'}) or
325             (defined($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-short'}) and
326              lc($message) eq lc($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-short'})) or
327             (defined($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-regexp'}) and
328              $message =~ /$self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-regexp'}/si)) {
329             # they got it right
330             my $who = $event->{'from'};
331             my $answer = $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-long'};
332             my $score = $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'score'};
333             if (not defined($score)) {
334                 $score = 1; # use difficulty XXX
335             }
336             my $time = $event->{'time'} - $self->{'questionsTime'}->{$event->{'channel'}};
337             my $total = $self->score($event, $who, $score);
338             $self->debug("Answered by: $who");
339             $self->say($event, "$who got the right answer in $time seconds (+$score points giving $total). The answer was: $answer");
340             $self->saveConfig();
341             $self->{'questionsTime'}->{$event->{'channel'}} = 0;
342             $self->schedule($event, \$self->{'askDelay'}, 1, 'ask');
343         }
344     }
345 }
346
347 sub quizTip { # called by timer, only during game
348     my $self = shift;
349     my($event) = @_;
350     my $tip;
351     if (defined($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'tips'}) and
352         $self->{'tip'}->{$event->{'channel'}} < @{$self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'tips'}}) {
353         $tip = $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'tips'}->[$self->{'tip'}->{$event->{'channel'}}];
354     } else {
355         if (not defined($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'tips'}) and
356             (not defined($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'tipCycle'}) or
357              $self->{'tip'}->{$event->{'channel'}} < $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'tipCycle'})) {
358             $tip = $self->generateTip($self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-long'},
359                                       $self->{'tip'}->{$event->{'channel'}},
360                                       $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'tipCycle'});
361         }
362     }
363     if (defined($tip)) {
364         $self->{'tip'}->{$event->{'channel'}} += 1;
365         $self->say($event, "Hint: $tip...");
366         $self->saveConfig();
367         return 1;
368     } else {
369         return 0;
370     }
371 }
372
373 sub quizPause { # called by user
374     my $self = shift;
375     my($event) = @_;
376     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) { # game in progress
377         if (not $self->{'paused'}->{$event->{'channel'}}) { # not paused
378             # pause game
379             $self->{'paused'}->{$event->{'channel'}} = 1;
380             $self->saveConfig();
381             $self->say($event, "Quiz paused. Use $self->{'prefix'}resume to continue.");
382         } else {
383             $self->say($event, "Quiz already paused. Use $self->{'prefix'}resume to continue.");
384         }
385     } else {
386         $self->say($event, "No quiz in progress, use $self->{'prefix'}ask to start one.");
387     }
388 }
389
390 sub quizResume { # called by user
391     my $self = shift;
392     my($event) = @_;
393     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) { # game in progress
394         if ($self->{'paused'}->{$event->{'channel'}}) { # paused
395             # unpause game
396             $self->{'paused'}->{$event->{'channel'}} = 0;
397             $self->saveConfig();
398             $self->say($event, "Quiz resumed. Question: $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'question'}");
399         } else {
400             $self->say($event, "Quiz already in progress. Use $self->{'prefix'}repeat to be told the question again, and $self->{'prefix'}pause to pause the quiz.");
401         }
402     } else {
403         $self->say($event, "No quiz in progress, use $self->{'prefix'}ask to start one.");
404     }
405 }
406
407 sub quizRepeat { # called by user
408     my $self = shift;
409     my($event) = @_;
410     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) { # game in progress
411         $self->say($event, "Question: $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'question'}");
412     } else {
413         $self->say($event, "No quiz in progress, use $self->{'prefix'}ask to start one.");
414     }
415 }
416
417 sub quizEnd { # called by question and user
418     my $self = shift;
419     my($event) = @_;
420     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) {
421         # get the scores for each player that player in the game
422         my @scores = $self->getScores($event, sub {
423                                           my($event, $score) = @_;
424                                           # XXX this means that a user has to be there till the end
425                                           # of the game to get points added to his high score table.
426                                           # XXX it also means a user can get better simply by
427                                           # playing more games.
428                                           $self->{'totalScores'}->{$score->[1]} += $score->[2];
429                                       });
430         # print them
431         if (@scores) {
432             local $" = ', ';
433             $self->say($event, "Quiz Ended. Scores: @scores");
434         } else {
435             $self->say($event, 'Quiz Ended. No questions were answered.');
436         }
437         delete($self->{'currentQuestion'}->{$event->{'channel'}});
438         $self->saveConfig();
439     }
440 }
441
442 sub quizScores { # called by user
443     my $self = shift;
444     my($event) = @_;
445     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) {
446         # get the scores for each player that player in the game
447         my @scores = $self->getScores($event, sub {});
448         # get other stats
449         my $remaining = '';
450         if ($self->{'remainingQuestions'}->{$event->{'channel'}} > 0) {
451             $remaining = " There are $self->{'remainingQuestions'}->{$event->{'channel'}} more questions to go.";
452         }
453         # print them
454         if (@scores) {
455             local $" = ', ';
456             $self->say($event, "Current Scores: @scores$remaining");
457         } else {
458             $self->say($event, "No questions have been answered yet.$remaining");
459         }
460     } else {
461         $self->say($event, "No quiz in progress, use $self->{'prefix'}ask to start one.");
462     }
463 }
464
465 sub quizSkip { # called by user
466     my $self = shift;
467     my($event) = @_;
468     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) { # game in progress
469         if (not $self->{'paused'}->{$event->{'channel'}}) { # not paused
470             if ($self->{'questionsTime'}->{$event->{'channel'}}) { # question asked and not answered
471                 # XXX should only let players skip (at the moment even someone who has not tried to answer any question can skip)
472                 # Get number of users who have said !skip (and set current user)
473                 my(undef, $skipCount) = $self->stringHash(\$self->{'skip'}->{$event->{'channel'}}, $event->{'from'}, 1);
474                 # Get number of users who are playing
475                 my $playerCount = $self->getActivePlayers($event);
476                 if ($skipCount >= $playerCount * $self->{'skipFractionRequired'}) {
477                     my $answer = $self->{'questions'}->[$self->{'currentQuestion'}->{$event->{'channel'}}]->{'answer-long'};
478                     $self->say($event, "$skipCount players wanted to skip. Moving to next question. The answer was: $answer");
479                     $self->quizQuestion($event);
480                 }
481             } # else drop it
482         } else {
483             $self->say($event, "Quiz paused. Use $self->{'prefix'}resume to continue the quiz.");
484         }
485     } else {
486         $self->say($event, "No quiz in progress, use $self->{'prefix'}ask to start one.");
487     }
488 }
489
490 sub pickQuestion {
491     my $self = shift;
492     my($event) = @_;
493     $self->{'questionIndex'} += 1 + $event->{'time'} % $self->{'skipMargin'};
494     $self->{'questionIndex'} %= @{$self->{'questions'}};
495     return $self->{'questionIndex'};
496 }
497
498 sub score {
499     my $self = shift;
500     my($event, $who, $score) = @_;
501     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) {
502         my($score, undef) = $self->stringHash(\$self->{'quizScores'}->{$event->{'channel'}}, $who, $score, 1);
503         $self->saveConfig();
504         return $score;
505     }
506 }
507
508 sub getScores {
509     my $self = shift;
510     my($event, $perUser) = @_;
511     my @scores;
512     foreach my $player ($self->getActivePlayers($event)) {
513         my($score, undef) = $self->stringHash(\$self->{'quizScores'}->{$event->{'channel'}}, $player);
514         if (defined($score)) {
515             push(@scores, ["$player: $score", $player, $score]);
516         }
517     }
518     # sort the scores by number
519     @scores = sort {$a->[2] <=> $b->[2]} @scores;
520     foreach my $score (@scores) {
521         &$perUser($event, $score);
522         $score = $score->[0];
523     }
524     return @scores;
525 }
526
527 sub generateTip {
528     my $self = shift;
529     my($answer, $tipID, $maxTips) = @_;
530     if (length($answer) > $tipID+1) {
531         return substr($answer, 0, $tipID+1);
532     } else {
533         return undef;
534     }
535 }
536
537 sub getActivePlayers {
538     my $self = shift;
539     my($event) = @_;
540     my @players;
541     if (defined($self->{'currentQuestion'}->{$event->{'channel'}})) { # game in progress
542         my $start = $self->{'quizTime'}->{$event->{'channel'}};
543         my %players = split(' ', $self->{'players'}->{$event->{'channel'}});
544         foreach my $player (keys %players) {
545             if ($players{$player} > $start) {
546                 push(@players, $player);
547             }
548         }
549     }
550     return @players;
551 }
552
553 sub stringHash {
554     my $self = shift;
555     my($string, $key, $value, $multiple) = @_;
556     my %hash = split(' ', $$string);
557     my @hash;
558     if (defined($value)) {
559         if (defined($multiple)) {
560             $hash{$key} = $hash{$key} * $multiple + $value;
561         } else {
562             $hash{$key} = $value;
563         }
564         local $" = ' ';
565         @hash = %hash;
566         $$string = "@hash";
567     } else {
568         @hash = %hash;
569     }
570     return ($hash{$key}, scalar(@hash) / 2);
571 }