]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/games/quiz.rb
quiz: autoask status query
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / games / quiz.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Quiz plugin for rbot
5 #
6 # Author:: Mark Kretschmann <markey@web.de>
7 # Author:: Jocke Andersson <ajocke@gmail.com>
8 # Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
9 # Author:: Yaohan Chen <yaohan.chen@gmail.com>
10 #
11 # Copyright:: (C) 2006 Mark Kretschmann, Jocke Andersson, Giuseppe Bilotta
12 # Copyright:: (C) 2007 Giuseppe Bilotta, Yaohan Chen
13 #
14 # License:: GPL v2
15 #
16 # A trivia quiz game. Fast paced, featureful and fun.
17
18 # FIXME:: interesting fact: in the Quiz class, @registry.has_key? seems to be
19 #         case insensitive. Although this is all right for us, this leads to
20 #         rank vs registry mismatches. So we have to make the @rank_table
21 #         comparisons case insensitive as well. For the moment, redefine
22 #         everything to downcase before matching the nick.
23 #
24 # TODO:: define a class for the rank table. We might also need it for scoring
25 #        in other games.
26 #
27 # TODO:: when Ruby 2.0 gets out, fix the FIXME 2.0 UTF-8 workarounds
28
29 # Class for storing question/answer pairs
30 define_structure :QuizBundle, :question, :answer
31
32 # Class for storing player stats
33 define_structure :PlayerStats, :score, :jokers, :jokers_time
34 # Why do we still need jokers_time? //Firetech
35
36 # Control codes
37 Color = "\003"
38 Bold = "\002"
39
40
41 #######################################################################
42 # CLASS QuizAnswer
43 # Abstract an answer to a quiz question, by providing self as a string
44 # and a core that can be answered as an alternative. It also provides
45 # a boolean that tells if the core is numeric or not
46 #######################################################################
47 class QuizAnswer
48   attr_writer :info
49
50   def initialize(str)
51     @string = str.strip
52     @core = nil
53     if @string =~ /#(.+)#/
54       @core = $1
55       @string.gsub!('#', '')
56     end
57     raise ArgumentError, "empty string can't be a valid answer!" if @string.empty?
58     raise ArgumentError, "empty core can't be a valid answer!" if @core and @core.empty?
59
60     @numeric = (core.to_i.to_s == core) || (core.to_f.to_s == core)
61     @info = nil
62   end
63
64   def core
65     @core || @string
66   end
67
68   def numeric?
69     @numeric
70   end
71
72   def valid?(str)
73     str.downcase == core.downcase || str.downcase == @string.downcase
74   end
75
76   def to_str
77     [@string, @info].join
78   end
79   alias :to_s :to_str
80
81
82 end
83
84
85 #######################################################################
86 # CLASS Quiz
87 # One Quiz instance per channel, contains channel specific data
88 #######################################################################
89 class Quiz
90   attr_accessor :registry, :registry_conf, :questions,
91     :question, :answers, :canonical_answer, :answer_array,
92     :first_try, :hint, :hintrange, :rank_table, :hinted, :has_errors,
93     :all_seps
94
95   def initialize( channel, registry )
96     if !channel
97       @registry = registry.sub_registry( 'private' )
98     else
99       @registry = registry.sub_registry( channel.downcase )
100     end
101     @has_errors = false
102     @registry.each_key { |k|
103       unless @registry.has_key?(k)
104         @has_errors = true
105         error "Data for #{k} is NOT ACCESSIBLE! Database corrupt?"
106       end
107     }
108     if @has_errors
109       debug @registry.to_a.map { |a| a.join(", ")}.join("\n")
110     end
111
112     @registry_conf = @registry.sub_registry( "config" )
113
114     # Per-channel list of sources. If empty, the default one (quiz/quiz.rbot)
115     # will be used. TODO
116     @registry_conf["sources"] = [] unless @registry_conf.has_key?( "sources" )
117
118     # Per-channel copy of the global questions table. Acts like a shuffled queue
119     # from which questions are taken, until empty. Then we refill it with questions
120     # from the global table.
121     @registry_conf["questions"] = [] unless @registry_conf.has_key?( "questions" )
122
123     # Autoask defaults to true
124     @registry_conf["autoask"] = true unless @registry_conf.has_key?( "autoask" )
125
126     # Autoask delay defaults to 0 (instantly)
127     @registry_conf["autoask_delay"] = 0 unless @registry_conf.has_key?( "autoask_delay" )
128
129     @questions = @registry_conf["questions"]
130     @question = nil
131     @answers = []
132     @canonical_answer = nil
133     # FIXME 2.0 UTF-8
134     @answer_array = []
135     @first_try = false
136     # FIXME 2.0 UTF-8
137     @hint = []
138     @hintrange = nil
139     @hinted = false
140
141     # True if the answers is entirely done by separators
142     @all_seps = false
143
144     # We keep this array of player stats for performance reasons. It's sorted by score
145     # and always synced with the registry player stats hash. This way we can do fast
146     # rank lookups, without extra sorting.
147     @rank_table = @registry.to_a.sort { |a,b| b[1].score<=>a[1].score }
148   end
149 end
150
151
152 #######################################################################
153 # CLASS QuizPlugin
154 #######################################################################
155 class QuizPlugin < Plugin
156   Config.register Config::BooleanValue.new('quiz.dotted_nicks',
157     :default => true,
158     :desc => "When true, nicks in the top X scores will be camouflaged to prevent IRC hilighting")
159
160   Config.register Config::ArrayValue.new('quiz.sources',
161     :default => ['quiz.rbot'],
162     :desc => "List of files and URLs that will be used to retrieve quiz questions")
163
164
165   Config.register Config::IntegerValue.new('quiz.max_jokers',
166     :default => 3,
167     :desc => "Maximum number of jokers a player can gain")
168
169   def initialize()
170     super
171
172     @questions = Array.new
173     @quizzes = Hash.new
174     @waiting = Hash.new
175     @ask_mutex = Mutex.new
176   end
177
178   # Function that returns whether a char is a "separator", used for hints
179   #
180   def is_sep( ch )
181     return ch !~ /^\w$/u
182   end
183
184
185   # Fetches questions from the data sources, which can be either local files
186   # (in quiz/) or web pages.
187   #
188   def fetch_data( m )
189     # Read the winning messages file
190     @win_messages = Array.new
191     winfile = datafile 'win_messages'
192     if File.exists? winfile
193       IO.foreach(winfile) { |line| @win_messages << line.chomp }
194     else
195       warning( "win_messages file not found!" )
196       # Fill the array with a least one message or code accessing it would fail
197       @win_messages << "<who> guessed right! The answer was <answer>"
198     end
199
200     m.reply "Fetching questions ..."
201
202     # TODO Per-channel sources
203
204     data = ""
205     @bot.config['quiz.sources'].each { |p|
206       if p =~ /^https?:\/\//
207         # Wiki data
208         begin
209           serverdata = @bot.httputil.get(p) # "http://amarok.kde.org/amarokwiki/index.php/Rbot_Quiz"
210           serverdata = serverdata.split( "QUIZ DATA START\n" )[1]
211           serverdata = serverdata.split( "\nQUIZ DATA END" )[0]
212           serverdata = serverdata.gsub( /&nbsp;/, " " ).gsub( /&amp;/, "&" ).gsub( /&quot;/, "\"" )
213           data << "\n\n" << serverdata
214         rescue
215           m.reply "Failed to download questions from #{p}, ignoring sources"
216         end
217       else
218         path = datafile p
219         debug "Fetching from #{path}"
220
221         # Local data
222         begin
223           data << "\n\n" << File.read(path)
224         rescue
225           m.reply "Failed to read from local database file #{p}, skipping."
226         end
227       end
228     }
229
230     @questions.clear
231
232     # Fuse together and remove comments, then split
233     entries = data.strip.gsub( /^#.*$/, "" ).split( /(?:^|\n+)Question: / )
234
235     entries.each do |e|
236       p = e.split( "\n" )
237       # We'll need at least two lines of data
238       unless p.size < 2
239         # Check if question isn't empty
240         if p[0].length > 0
241           while p[1].match( /^Answer: (.*)$/ ) == nil and p.size > 2
242             # Delete all lines between the question and the answer
243             p.delete_at(1)
244           end
245           p[1] = p[1].gsub( /Answer: /, "" ).strip
246           # If the answer was found
247           if p[1].length > 0
248             # Add the data to the array
249             b = QuizBundle.new( p[0], p[1] )
250             @questions << b
251           end
252         end
253       end
254     end
255
256     m.reply "done, #{@questions.length} questions loaded."
257   end
258
259
260   # Returns new Quiz instance for channel, or existing one
261   #
262   def create_quiz( channel )
263     unless @quizzes.has_key?( channel )
264       @quizzes[channel] = Quiz.new( channel, @registry )
265     end
266
267     if @quizzes[channel].has_errors
268       return nil
269     else
270       return @quizzes[channel]
271     end
272   end
273
274
275   def say_score( m, nick )
276     chan = m.channel
277     q = create_quiz( chan )
278     if q.nil?
279       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
280       return
281     end
282
283     if q.registry.has_key?( nick )
284       score = q.registry[nick].score
285       jokers = q.registry[nick].jokers
286
287       rank = 0
288       q.rank_table.each do |place|
289         rank += 1
290         break if nick.downcase == place[0].downcase
291       end
292
293       m.reply "#{nick}'s score is: #{score}    Rank: #{rank}    Jokers: #{jokers}"
294     else
295       m.reply "#{nick} does not have a score yet. Lamer."
296     end
297   end
298
299
300   def help( plugin, topic="" )
301     if topic == "admin"
302       "Quiz game aministration commands (requires authentication): 'quiz autoask <on/off>' => enable/disable autoask mode. 'quiz autoask delay <secs>' => delay next quiz by <secs> seconds when in autoask mode. 'quiz transfer <source> <dest> [score] [jokers]' => transfer [score] points and [jokers] jokers from <source> to <dest> (default is entire score and all jokers). 'quiz setscore <player> <score>' => set <player>'s score to <score>. 'quiz setjokers <player> <jokers>' => set <player>'s number of jokers to <jokers>. 'quiz deleteplayer <player>' => delete one player from the rank table (only works when score and jokers are set to 0). 'quiz cleanup' => remove players with no points and no jokers."
303     else
304       urls = @bot.config['quiz.sources'].select { |p| p =~ /^https?:\/\// }
305       "A multiplayer trivia quiz. 'quiz' => ask a question. 'quiz hint' => get a hint. 'quiz solve' => solve this question. 'quiz skip' => skip to next question. 'quiz joker' => draw a joker to win this round. 'quiz score [player]' => show score for [player] (default is yourself). 'quiz top5' => show top 5 players. 'quiz top <number>' => show top <number> players (max 50). 'quiz stats' => show some statistics. 'quiz fetch' => refetch questions from databases. 'quiz refresh' => refresh the question pool for this channel." + (urls.empty? ? "" : "\nYou can add new questions at #{urls.join(', ')}")
306     end
307   end
308
309
310   # Updates the per-channel rank table, which is kept for performance reasons.
311   # This table contains all players sorted by rank.
312   #
313   def calculate_ranks( m, q, nick )
314     if q.registry.has_key?( nick )
315       stats = q.registry[nick]
316
317       # Find player in table
318       old_rank = nil
319       q.rank_table.each_with_index do |place, i|
320         if nick.downcase == place[0].downcase
321           old_rank = i
322           break
323         end
324       end
325
326       # Remove player from old position
327       if old_rank
328         q.rank_table.delete_at( old_rank )
329       end
330
331       # Insert player at new position
332       new_rank = nil
333       q.rank_table.each_with_index do |place, i|
334         if stats.score > place[1].score
335           q.rank_table[i,0] = [[nick, stats]]
336           new_rank = i
337           break
338         end
339       end
340
341       # If less than all other players' scores, append to table
342       unless new_rank
343         new_rank = q.rank_table.length
344         q.rank_table << [nick, stats]
345       end
346
347       # Print congratulations/condolences if the player's rank has changed
348       if old_rank
349         if new_rank < old_rank
350           m.reply "#{nick} ascends to rank #{new_rank + 1}. Congratulations :)"
351         elsif new_rank > old_rank
352           m.reply "#{nick} slides down to rank #{new_rank + 1}. So Sorry! NOT. :p"
353         end
354       end
355     else
356       q.rank_table << [[nick, PlayerStats.new( 1 )]]
357     end
358   end
359
360
361   # Reimplemented from Plugin
362   #
363   def message(m)
364     chan = m.channel
365     return unless @quizzes.has_key?( chan )
366     q = @quizzes[chan]
367
368     return if q.question == nil
369
370     message = m.message.downcase.strip
371
372     nick = m.sourcenick.to_s
373
374     # Support multiple alternate answers and cores
375     answer = q.answers.find { |ans| ans.valid?(message) }
376     if answer
377       # List canonical answer which the hint was based on, to avoid confusion
378       # FIXME display this more friendly
379       answer.info = " (hints were for alternate answer #{q.canonical_answer.core})" if answer != q.canonical_answer and q.hinted
380
381       points = 1
382       if q.first_try
383         points += 1
384         reply = "WHOPEEE! #{nick} got it on the first try! That's worth an extra point. Answer was: #{answer}"
385       elsif q.rank_table.length >= 1 and nick.downcase == q.rank_table[0][0].downcase
386         reply = "THE QUIZ CHAMPION defends his throne! Seems like #{nick} is invicible! Answer was: #{answer}"
387       elsif q.rank_table.length >= 2 and nick.downcase == q.rank_table[1][0].downcase
388         reply = "THE SECOND CHAMPION is on the way up! Hurry up #{nick}, you only need #{q.rank_table[0][1].score - q.rank_table[1][1].score - 1} points to beat the king! Answer was: #{answer}"
389       elsif    q.rank_table.length >= 3 and nick.downcase == q.rank_table[2][0].downcase
390         reply = "THE THIRD CHAMPION strikes again! Give it all #{nick}, with #{q.rank_table[1][1].score - q.rank_table[2][1].score - 1} more points you'll reach the 2nd place! Answer was: #{answer}"
391       else
392         reply = @win_messages[rand( @win_messages.length )].dup
393         reply.gsub!( "<who>", nick )
394         reply.gsub!( "<answer>", answer )
395       end
396
397       m.reply reply
398
399       player = nil
400       if q.registry.has_key?(nick)
401         player = q.registry[nick]
402       else
403         player = PlayerStats.new( 0, 0, 0 )
404       end
405
406       player.score = player.score + points
407
408       # Reward player with a joker every X points
409       if player.score % 15 == 0 and player.jokers < @bot.config['quiz.max_jokers']
410         player.jokers += 1
411         m.reply "#{nick} gains a new joker. Rejoice :)"
412       end
413
414       q.registry[nick] = player
415       calculate_ranks( m, q, nick)
416
417       q.question = nil
418       if q.registry_conf["autoask"]
419         delay = q.registry_conf["autoask_delay"]
420         if delay > 0
421           m.reply "#{Bold}#{Color}03Next question in #{Bold}#{delay}#{Bold} seconds"
422           timer = @bot.timer.add_once(delay) {
423             @ask_mutex.synchronize do
424               @waiting.delete(chan)
425             end
426             cmd_quiz( m, nil)
427           }
428           @waiting[chan] = timer
429         else
430           cmd_quiz( m, nil )
431         end
432       end
433     else
434       # First try is used, and it wasn't the answer.
435       q.first_try = false
436     end
437   end
438
439
440   # Stretches an IRC nick with dots, simply to make the client not trigger a hilight,
441   # which is annoying for those not watching. Example: markey -> m.a.r.k.e.y
442   #
443   def unhilight_nick( nick )
444     return nick unless @bot.config['quiz.dotted_nicks']
445     return nick.split(//).join(".")
446   end
447
448
449   #######################################################################
450   # Command handling
451   #######################################################################
452   def cmd_quiz( m, params )
453     fetch_data( m ) if @questions.empty?
454     chan = m.channel
455
456     @ask_mutex.synchronize do
457       if @waiting.has_key?(chan)
458         m.reply "Next quiz question will be automatically asked soon, have patience"
459         return
460       end
461     end
462
463     q = create_quiz( chan )
464     if q.nil?
465       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
466       return
467     end
468
469     if q.question
470       m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
471       m.reply "Hint: #{q.hint}" if q.hinted
472       return
473     end
474
475     # Fill per-channel questions buffer
476     if q.questions.empty?
477       q.questions = @questions.sort_by { rand }
478     end
479
480     # pick a question and delete it (delete_at returns the deleted item)
481     picked = q.questions.delete_at( rand(q.questions.length) )
482
483     q.question = picked.question
484     q.answers = picked.answer.split(/\s+\|\|\s+/).map { |ans| QuizAnswer.new(ans) }
485
486     # Check if any core answer is numerical and tell the players so, if that's the case
487     # The rather obscure statement is needed because to_i and to_f returns 99(.0) for "99 red balloons", and 0 for "balloon"
488     #
489     # The "canonical answer" is also determined here, defined to be the first found numerical answer, or
490     # the first core.
491     numeric = q.answers.find { |ans| ans.numeric? }
492     if numeric
493         q.question += "#{Color}07 (Numerical answer)#{Color}"
494         q.canonical_answer = numeric
495     else
496         q.canonical_answer = q.answers.first
497     end
498
499     q.first_try = true
500
501     # FIXME 2.0 UTF-8
502     q.hint = []
503     q.answer_array.clear
504     q.canonical_answer.core.scan(/./u) { |ch|
505       if is_sep(ch)
506         q.hint << ch
507       else
508         q.hint << "^"
509       end
510       q.answer_array << ch
511     }
512     q.all_seps = false
513     # It's possible that an answer is entirely done by separators,
514     # in which case we'll hide everything
515     if q.answer_array == q.hint
516       q.hint.map! { |ch|
517         "^"
518       }
519       q.all_seps = true
520     end
521     q.hinted = false
522
523     # Generate array of unique random range
524     q.hintrange = (0..q.hint.length-1).sort_by{ rand }
525
526     m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
527   end
528
529
530   def cmd_solve( m, params )
531     chan = m.channel
532
533     return unless @quizzes.has_key?( chan )
534     q = @quizzes[chan]
535
536     m.reply "The correct answer was: #{q.canonical_answer}"
537
538     q.question = nil
539
540     cmd_quiz( m, nil ) if q.registry_conf["autoask"]
541   end
542
543
544   def cmd_hint( m, params )
545     chan = m.channel
546     nick = m.sourcenick.to_s
547
548     return unless @quizzes.has_key?(chan)
549     q = @quizzes[chan]
550
551     if q.question == nil
552       m.reply "#{nick}: Get a question first!"
553     else
554       num_chars = case q.hintrange.length    # Number of characters to reveal
555       when 25..1000 then 7
556       when 20..1000 then 6
557       when 16..1000 then 5
558       when 12..1000 then 4
559       when  8..1000 then 3
560       when  5..1000 then 2
561       when  1..1000 then 1
562       end
563
564       # FIXME 2.0 UTF-8
565       num_chars.times do
566         begin
567           index = q.hintrange.pop
568           # New hint char until the char isn't a "separator" (space etc.)
569         end while is_sep(q.answer_array[index]) and not q.all_seps
570         q.hint[index] = q.answer_array[index]
571       end
572       m.reply "Hint: #{q.hint}"
573       q.hinted = true
574
575       # FIXME 2.0 UTF-8
576       if q.hint == q.answer_array
577         m.reply "#{Bold}#{Color}04BUST!#{Color}#{Bold} This round is over. #{Color}04Minus one point for #{nick}#{Color}."
578
579         stats = nil
580         if q.registry.has_key?( nick )
581           stats = q.registry[nick]
582         else
583           stats = PlayerStats.new( 0, 0, 0 )
584         end
585
586         stats["score"] = stats.score - 1
587         q.registry[nick] = stats
588
589         calculate_ranks( m, q, nick)
590
591         q.question = nil
592         cmd_quiz( m, nil ) if q.registry_conf["autoask"]
593       end
594     end
595   end
596
597
598   def cmd_skip( m, params )
599     chan = m.channel
600     return unless @quizzes.has_key?(chan)
601     q = @quizzes[chan]
602
603     q.question = nil
604     cmd_quiz( m, params )
605   end
606
607
608   def cmd_joker( m, params )
609     chan = m.channel
610     nick = m.sourcenick.to_s
611     q = create_quiz(chan)
612     if q.nil?
613       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
614       return
615     end
616
617     if q.question == nil
618       m.reply "#{nick}: There is no open question."
619       return
620     end
621
622     if q.registry[nick].jokers > 0
623       player = q.registry[nick]
624       player.jokers -= 1
625       player.score += 1
626       q.registry[nick] = player
627
628       calculate_ranks( m, q, nick )
629
630       if player.jokers != 1
631         jokers = "jokers"
632       else
633         jokers = "joker"
634       end
635       m.reply "#{Bold}#{Color}12JOKER!#{Color}#{Bold} #{nick} draws a joker and wins this round. You have #{player.jokers} #{jokers} left."
636       m.reply "The answer was: #{q.canonical_answer}."
637
638       q.question = nil
639       cmd_quiz( m, nil ) if q.registry_conf["autoask"]
640     else
641       m.reply "#{nick}: You don't have any jokers left ;("
642     end
643   end
644
645
646   def cmd_fetch( m, params )
647     fetch_data( m )
648   end
649
650
651   def cmd_refresh( m, params )
652     q = create_quiz(m.channel)
653     q.questions.clear
654     fetch_data(m)
655     cmd_quiz( m, params )
656   end
657
658
659   def cmd_top5( m, params )
660     chan = m.channel
661     q = create_quiz( chan )
662     if q.nil?
663       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
664       return
665     end
666
667     if q.rank_table.empty?
668       m.reply "There are no scores known yet!"
669       return
670     end
671
672     m.reply "* Top 5 Players for #{chan}:"
673
674     [5, q.rank_table.length].min.times do |i|
675       player = q.rank_table[i]
676       nick = player[0]
677       score = player[1].score
678       m.reply "    #{i + 1}. #{unhilight_nick( nick )} (#{score})"
679     end
680   end
681
682
683   def cmd_top_number( m, params )
684     num = params[:number].to_i
685     return if num < 1 or num > 50
686     chan = m.channel
687     q = create_quiz( chan )
688     if q.nil?
689       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
690       return
691     end
692
693     if q.rank_table.empty?
694       m.reply "There are no scores known yet!"
695       return
696     end
697
698     ar = []
699     m.reply "* Top #{num} Players for #{chan}:"
700     n = [ num, q.rank_table.length ].min
701     n.times do |i|
702       player = q.rank_table[i]
703       nick = player[0]
704       score = player[1].score
705       ar << "#{i + 1}. #{unhilight_nick( nick )} (#{score})"
706     end
707     m.reply ar.join(" | "), :split_at => /\s+\|\s+/
708   end
709
710
711   def cmd_stats( m, params )
712     fetch_data( m ) if @questions.empty?
713
714     m.reply "* Total Number of Questions:"
715     m.reply "    #{@questions.length}"
716   end
717
718
719   def cmd_score( m, params )
720     nick = m.sourcenick.to_s
721     say_score( m, nick )
722   end
723
724
725   def cmd_score_player( m, params )
726     say_score( m, params[:player] )
727   end
728
729
730   def cmd_autoask( m, params )
731     chan = m.channel
732     q = create_quiz( chan )
733     if q.nil?
734       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
735       return
736     end
737
738     params[:enable] ||= 'status'
739
740     case params[:enable].downcase
741     when "on", "true"
742       q.registry_conf["autoask"] = true
743       m.reply "Enabled autoask mode."
744       cmd_quiz( m, nil ) if q.question == nil
745     when "off", "false"
746       q.registry_conf["autoask"] = false
747       m.reply "Disabled autoask mode."
748     when "status"
749       m.reply _("Autoask is %{status}, the delay is %{time}") % {
750         :status => q.registry_conf["autoask"],
751         :time => Utils.secs_to_string(q.registry_conf["autoask_delay"]),
752       }
753     else
754       m.reply "Invalid autoask parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
755     end
756   end
757
758   def cmd_autoask_delay( m, params )
759     chan = m.channel
760     q = create_quiz( chan )
761     if q.nil?
762       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
763       return
764     end
765
766     delay = params[:time].to_i
767     q.registry_conf["autoask_delay"] = delay
768     m.reply "Autoask delay now #{q.registry_conf['autoask_delay']} seconds"
769   end
770
771   def cmd_transfer( m, params )
772     chan = m.channel
773     q = create_quiz( chan )
774     if q.nil?
775       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
776       return
777     end
778
779     debug q.rank_table.inspect
780
781     source = params[:source]
782     dest = params[:dest]
783     transscore = params[:score].to_i
784     transjokers = params[:jokers].to_i
785     debug "Transferring #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
786
787     if q.registry.has_key?(source)
788       sourceplayer = q.registry[source]
789       score = sourceplayer.score
790       if transscore == -1
791         transscore = score
792       end
793       if score < transscore
794         m.reply "#{source} only has #{score} points!"
795         return
796       end
797       jokers = sourceplayer.jokers
798       if transjokers == -1
799         transjokers = jokers
800       end
801       if jokers < transjokers
802         m.reply "#{source} only has #{jokers} jokers!!"
803         return
804       end
805       if q.registry.has_key?(dest)
806         destplayer = q.registry[dest]
807       else
808         destplayer = PlayerStats.new(0,0,0)
809       end
810
811       if sourceplayer.object_id == destplayer.object_id
812         m.reply "Source and destination are the same, I'm not going to touch them"
813         return
814       end
815
816       sourceplayer.score -= transscore
817       destplayer.score += transscore
818       sourceplayer.jokers -= transjokers
819       destplayer.jokers += transjokers
820
821       q.registry[source] = sourceplayer
822       calculate_ranks(m, q, source)
823
824       q.registry[dest] = destplayer
825       calculate_ranks(m, q, dest)
826
827       m.reply "Transferred #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
828     else
829       m.reply "#{source} doesn't have any points!"
830     end
831   end
832
833
834   def cmd_del_player( m, params )
835     chan = m.channel
836     q = create_quiz( chan )
837     if q.nil?
838       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
839       return
840     end
841
842     debug q.rank_table.inspect
843
844     nick = params[:nick]
845     if q.registry.has_key?(nick)
846       player = q.registry[nick]
847       score = player.score
848       if score != 0
849         m.reply "Can't delete player #{nick} with score #{score}."
850         return
851       end
852       jokers = player.jokers
853       if jokers != 0
854         m.reply "Can't delete player #{nick} with #{jokers} jokers."
855         return
856       end
857       q.registry.delete(nick)
858
859       player_rank = nil
860       q.rank_table.each_index { |rank|
861         if nick.downcase == q.rank_table[rank][0].downcase
862           player_rank = rank
863           break
864         end
865       }
866       q.rank_table.delete_at(player_rank)
867
868       m.reply "Player #{nick} deleted."
869     else
870       m.reply "Player #{nick} isn't even in the database."
871     end
872   end
873
874
875   def cmd_set_score(m, params)
876     chan = m.channel
877     q = create_quiz( chan )
878     if q.nil?
879       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
880       return
881     end
882     debug q.rank_table.inspect
883
884     nick = params[:nick]
885     val = params[:score].to_i
886     if q.registry.has_key?(nick)
887       player = q.registry[nick]
888       player.score = val
889     else
890       player = PlayerStats.new( val, 0, 0)
891     end
892     q.registry[nick] = player
893     calculate_ranks(m, q, nick)
894     m.reply "Score for player #{nick} set to #{val}."
895   end
896
897
898   def cmd_set_jokers(m, params)
899     chan = m.channel
900     q = create_quiz( chan )
901     if q.nil?
902       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
903       return
904     end
905     debug q.rank_table.inspect
906
907     nick = params[:nick]
908     val = [params[:jokers].to_i, @bot.config['quiz.max_jokers']].min
909     if q.registry.has_key?(nick)
910       player = q.registry[nick]
911       player.jokers = val
912     else
913       player = PlayerStats.new( 0, val, 0)
914     end
915     q.registry[nick] = player
916     m.reply "Jokers for player #{nick} set to #{val}."
917   end
918
919
920   def cmd_cleanup(m, params)
921     chan = m.channel
922     q = create_quiz( chan )
923     if q.nil?
924       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
925       return
926     end
927
928     null_players = []
929     q.registry.each { |nick, player|
930       null_players << nick if player.jokers == 0 and player.score == 0
931     }
932     debug "Cleaning up by removing #{null_players * ', '}"
933     null_players.each { |nick|
934       cmd_del_player(m, :nick => nick)
935     }
936
937   end
938
939   def stop(m, params)
940     unless m.public?
941       m.reply 'you must be on some channel to use this command'
942       return
943     end
944     if @quizzes.delete m.channel
945       @ask_mutex.synchronize do
946         t = @waiting.delete(m.channel)
947         @bot.timer.remove t if t
948       end
949       m.okay
950     else
951       m.reply(_("there is no active quiz on #{m.channel}"))
952     end
953   end
954
955 end
956
957 plugin = QuizPlugin.new
958 plugin.default_auth( 'edit', false )
959
960 # Normal commands
961 plugin.map 'quiz',                  :action => 'cmd_quiz'
962 plugin.map 'quiz solve',            :action => 'cmd_solve'
963 plugin.map 'quiz hint',             :action => 'cmd_hint'
964 plugin.map 'quiz skip',             :action => 'cmd_skip'
965 plugin.map 'quiz joker',            :action => 'cmd_joker'
966 plugin.map 'quiz score',            :action => 'cmd_score'
967 plugin.map 'quiz score :player',    :action => 'cmd_score_player'
968 plugin.map 'quiz fetch',            :action => 'cmd_fetch'
969 plugin.map 'quiz refresh',          :action => 'cmd_refresh'
970 plugin.map 'quiz top5',             :action => 'cmd_top5'
971 plugin.map 'quiz top :number',      :action => 'cmd_top_number'
972 plugin.map 'quiz stats',            :action => 'cmd_stats'
973 plugin.map 'quiz stop', :action => :stop
974
975 # Admin commands
976 plugin.map 'quiz autoask [:enable]',  :action => 'cmd_autoask', :auth_path => 'edit'
977 plugin.map 'quiz autoask delay :time',  :action => 'cmd_autoask_delay', :auth_path => 'edit', :requirements => {:time => /\d+/}
978 plugin.map 'quiz transfer :source :dest :score :jokers', :action => 'cmd_transfer', :auth_path => 'edit', :defaults => {:score => '-1', :jokers => '-1'}
979 plugin.map 'quiz deleteplayer :nick', :action => 'cmd_del_player', :auth_path => 'edit'
980 plugin.map 'quiz setscore :nick :score', :action => 'cmd_set_score', :auth_path => 'edit'
981 plugin.map 'quiz setjokers :nick :jokers', :action => 'cmd_set_jokers', :auth_path => 'edit'
982 plugin.map 'quiz cleanup', :action => 'cmd_cleanup', :auth_path => 'edit'