]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/games/quiz.rb
quiz: autoask cleanup
[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   # Announce errors if a message is passed as second parameter
262   #
263   def create_quiz(channel, m=nil)
264     unless @quizzes.has_key?( channel )
265       @quizzes[channel] = Quiz.new( channel, @registry )
266     end
267
268     if @quizzes[channel].has_errors
269       m.reply _("Sorry, the quiz database for %{chan} seems to be corrupt") % {
270         :chan => channel
271       } if m
272       return nil
273     else
274       return @quizzes[channel]
275     end
276   end
277
278
279   def say_score( m, nick )
280     chan = m.channel
281     q = create_quiz( chan, m )
282     return unless q
283
284     if q.registry.has_key?( nick )
285       score = q.registry[nick].score
286       jokers = q.registry[nick].jokers
287
288       rank = 0
289       q.rank_table.each do |place|
290         rank += 1
291         break if nick.downcase == place[0].downcase
292       end
293
294       m.reply "#{nick}'s score is: #{score}    Rank: #{rank}    Jokers: #{jokers}"
295     else
296       m.reply "#{nick} does not have a score yet. Lamer."
297     end
298   end
299
300
301   def help( plugin, topic="" )
302     if topic == "admin"
303       _("Quiz game aministration commands (requires authentication): ") + [
304         _("'quiz autoask <on/off>' => enable/disable autoask mode"),
305         _("'quiz autoask delay <secs>' => delay next quiz by <secs> seconds when in autoask mode"),
306         _("'quiz transfer <source> <dest> [score] [jokers]' => transfer [score] points and [jokers] jokers from <source> to <dest> (default is entire score and all jokers)"),
307         _("'quiz setscore <player> <score>' => set <player>'s score to <score>"),
308         _("'quiz setjokers <player> <jokers>' => set <player>'s number of jokers to <jokers>"),
309         _("'quiz deleteplayer <player>' => delete one player from the rank table (only works when score and jokers are set to 0)"),
310         _("'quiz cleanup' => remove players with no points and no jokers")
311       ].join(". ")
312     else
313       urls = @bot.config['quiz.sources'].select { |p| p =~ /^https?:\/\// }
314       "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(', ')}")
315     end
316   end
317
318
319   # Updates the per-channel rank table, which is kept for performance reasons.
320   # This table contains all players sorted by rank.
321   #
322   def calculate_ranks( m, q, nick )
323     if q.registry.has_key?( nick )
324       stats = q.registry[nick]
325
326       # Find player in table
327       old_rank = nil
328       q.rank_table.each_with_index do |place, i|
329         if nick.downcase == place[0].downcase
330           old_rank = i
331           break
332         end
333       end
334
335       # Remove player from old position
336       if old_rank
337         q.rank_table.delete_at( old_rank )
338       end
339
340       # Insert player at new position
341       new_rank = nil
342       q.rank_table.each_with_index do |place, i|
343         if stats.score > place[1].score
344           q.rank_table[i,0] = [[nick, stats]]
345           new_rank = i
346           break
347         end
348       end
349
350       # If less than all other players' scores, append to table
351       unless new_rank
352         new_rank = q.rank_table.length
353         q.rank_table << [nick, stats]
354       end
355
356       # Print congratulations/condolences if the player's rank has changed
357       if old_rank
358         if new_rank < old_rank
359           m.reply "#{nick} ascends to rank #{new_rank + 1}. Congratulations :)"
360         elsif new_rank > old_rank
361           m.reply "#{nick} slides down to rank #{new_rank + 1}. So Sorry! NOT. :p"
362         end
363       end
364     else
365       q.rank_table << [[nick, PlayerStats.new( 1 )]]
366     end
367   end
368
369
370   # Reimplemented from Plugin
371   #
372   def message(m)
373     chan = m.channel
374     return unless @quizzes.has_key?( chan )
375     q = @quizzes[chan]
376
377     return if q.question == nil
378
379     message = m.message.downcase.strip
380
381     nick = m.sourcenick.to_s
382
383     # Support multiple alternate answers and cores
384     answer = q.answers.find { |ans| ans.valid?(message) }
385     if answer
386       # List canonical answer which the hint was based on, to avoid confusion
387       # FIXME display this more friendly
388       answer.info = " (hints were for alternate answer #{q.canonical_answer.core})" if answer != q.canonical_answer and q.hinted
389
390       points = 1
391       if q.first_try
392         points += 1
393         reply = "WHOPEEE! #{nick} got it on the first try! That's worth an extra point. Answer was: #{answer}"
394       elsif q.rank_table.length >= 1 and nick.downcase == q.rank_table[0][0].downcase
395         reply = "THE QUIZ CHAMPION defends his throne! Seems like #{nick} is invicible! Answer was: #{answer}"
396       elsif q.rank_table.length >= 2 and nick.downcase == q.rank_table[1][0].downcase
397         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}"
398       elsif    q.rank_table.length >= 3 and nick.downcase == q.rank_table[2][0].downcase
399         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}"
400       else
401         reply = @win_messages[rand( @win_messages.length )].dup
402         reply.gsub!( "<who>", nick )
403         reply.gsub!( "<answer>", answer )
404       end
405
406       m.reply reply
407
408       player = nil
409       if q.registry.has_key?(nick)
410         player = q.registry[nick]
411       else
412         player = PlayerStats.new( 0, 0, 0 )
413       end
414
415       player.score = player.score + points
416
417       # Reward player with a joker every X points
418       if player.score % 15 == 0 and player.jokers < @bot.config['quiz.max_jokers']
419         player.jokers += 1
420         m.reply "#{nick} gains a new joker. Rejoice :)"
421       end
422
423       q.registry[nick] = player
424       calculate_ranks( m, q, nick)
425
426       q.question = nil
427       if q.registry_conf["autoask"]
428         delay = q.registry_conf["autoask_delay"]
429         if delay > 0
430           m.reply "#{Bold}#{Color}03Next question in #{Bold}#{delay}#{Bold} seconds"
431           timer = @bot.timer.add_once(delay) {
432             @ask_mutex.synchronize do
433               @waiting.delete(chan)
434             end
435             cmd_quiz( m, nil)
436           }
437           @waiting[chan] = timer
438         else
439           cmd_quiz( m, nil )
440         end
441       end
442     else
443       # First try is used, and it wasn't the answer.
444       q.first_try = false
445     end
446   end
447
448
449   # Stretches an IRC nick with dots, simply to make the client not trigger a hilight,
450   # which is annoying for those not watching. Example: markey -> m.a.r.k.e.y
451   #
452   def unhilight_nick( nick )
453     return nick unless @bot.config['quiz.dotted_nicks']
454     return nick.split(//).join(".")
455   end
456
457
458   #######################################################################
459   # Command handling
460   #######################################################################
461   def cmd_quiz( m, params )
462     fetch_data( m ) if @questions.empty?
463     chan = m.channel
464
465     @ask_mutex.synchronize do
466       if @waiting.has_key?(chan)
467         m.reply "Next quiz question will be automatically asked soon, have patience"
468         return
469       end
470     end
471
472     q = create_quiz( chan, m )
473     return unless q
474
475     if q.question
476       m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
477       m.reply "Hint: #{q.hint}" if q.hinted
478       return
479     end
480
481     # Fill per-channel questions buffer
482     if q.questions.empty?
483       q.questions = @questions.sort_by { rand }
484     end
485
486     # pick a question and delete it (delete_at returns the deleted item)
487     picked = q.questions.delete_at( rand(q.questions.length) )
488
489     q.question = picked.question
490     q.answers = picked.answer.split(/\s+\|\|\s+/).map { |ans| QuizAnswer.new(ans) }
491
492     # Check if any core answer is numerical and tell the players so, if that's the case
493     # The rather obscure statement is needed because to_i and to_f returns 99(.0) for "99 red balloons", and 0 for "balloon"
494     #
495     # The "canonical answer" is also determined here, defined to be the first found numerical answer, or
496     # the first core.
497     numeric = q.answers.find { |ans| ans.numeric? }
498     if numeric
499         q.question += "#{Color}07 (Numerical answer)#{Color}"
500         q.canonical_answer = numeric
501     else
502         q.canonical_answer = q.answers.first
503     end
504
505     q.first_try = true
506
507     # FIXME 2.0 UTF-8
508     q.hint = []
509     q.answer_array.clear
510     q.canonical_answer.core.scan(/./u) { |ch|
511       if is_sep(ch)
512         q.hint << ch
513       else
514         q.hint << "^"
515       end
516       q.answer_array << ch
517     }
518     q.all_seps = false
519     # It's possible that an answer is entirely done by separators,
520     # in which case we'll hide everything
521     if q.answer_array == q.hint
522       q.hint.map! { |ch|
523         "^"
524       }
525       q.all_seps = true
526     end
527     q.hinted = false
528
529     # Generate array of unique random range
530     q.hintrange = (0..q.hint.length-1).sort_by{ rand }
531
532     m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
533   end
534
535
536   def cmd_solve( m, params )
537     chan = m.channel
538
539     return unless @quizzes.has_key?( chan )
540     q = @quizzes[chan]
541
542     m.reply "The correct answer was: #{q.canonical_answer}"
543
544     q.question = nil
545
546     cmd_quiz( m, nil ) if q.registry_conf["autoask"]
547   end
548
549
550   def cmd_hint( m, params )
551     chan = m.channel
552     nick = m.sourcenick.to_s
553
554     return unless @quizzes.has_key?(chan)
555     q = @quizzes[chan]
556
557     if q.question == nil
558       m.reply "#{nick}: Get a question first!"
559     else
560       num_chars = case q.hintrange.length    # Number of characters to reveal
561       when 25..1000 then 7
562       when 20..1000 then 6
563       when 16..1000 then 5
564       when 12..1000 then 4
565       when  8..1000 then 3
566       when  5..1000 then 2
567       when  1..1000 then 1
568       end
569
570       # FIXME 2.0 UTF-8
571       num_chars.times do
572         begin
573           index = q.hintrange.pop
574           # New hint char until the char isn't a "separator" (space etc.)
575         end while is_sep(q.answer_array[index]) and not q.all_seps
576         q.hint[index] = q.answer_array[index]
577       end
578       m.reply "Hint: #{q.hint}"
579       q.hinted = true
580
581       # FIXME 2.0 UTF-8
582       if q.hint == q.answer_array
583         m.reply "#{Bold}#{Color}04BUST!#{Color}#{Bold} This round is over. #{Color}04Minus one point for #{nick}#{Color}."
584
585         stats = nil
586         if q.registry.has_key?( nick )
587           stats = q.registry[nick]
588         else
589           stats = PlayerStats.new( 0, 0, 0 )
590         end
591
592         stats["score"] = stats.score - 1
593         q.registry[nick] = stats
594
595         calculate_ranks( m, q, nick)
596
597         q.question = nil
598         cmd_quiz( m, nil ) if q.registry_conf["autoask"]
599       end
600     end
601   end
602
603
604   def cmd_skip( m, params )
605     chan = m.channel
606     return unless @quizzes.has_key?(chan)
607     q = @quizzes[chan]
608
609     q.question = nil
610     cmd_quiz( m, params )
611   end
612
613
614   def cmd_joker( m, params )
615     chan = m.channel
616     nick = m.sourcenick.to_s
617     q = create_quiz(chan, m)
618     return unless q
619
620     if q.question == nil
621       m.reply "#{nick}: There is no open question."
622       return
623     end
624
625     if q.registry[nick].jokers > 0
626       player = q.registry[nick]
627       player.jokers -= 1
628       player.score += 1
629       q.registry[nick] = player
630
631       calculate_ranks( m, q, nick )
632
633       if player.jokers != 1
634         jokers = "jokers"
635       else
636         jokers = "joker"
637       end
638       m.reply "#{Bold}#{Color}12JOKER!#{Color}#{Bold} #{nick} draws a joker and wins this round. You have #{player.jokers} #{jokers} left."
639       m.reply "The answer was: #{q.canonical_answer}."
640
641       q.question = nil
642       cmd_quiz( m, nil ) if q.registry_conf["autoask"]
643     else
644       m.reply "#{nick}: You don't have any jokers left ;("
645     end
646   end
647
648
649   def cmd_fetch( m, params )
650     fetch_data( m )
651   end
652
653
654   def cmd_refresh( m, params )
655     q = create_quiz(m.channel)
656     q.questions.clear
657     fetch_data(m)
658     cmd_quiz( m, params )
659   end
660
661
662   def cmd_top5( m, params )
663     chan = m.channel
664     q = create_quiz( chan, m )
665     return unless q
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, m )
688     return unless q
689
690     if q.rank_table.empty?
691       m.reply "There are no scores known yet!"
692       return
693     end
694
695     ar = []
696     m.reply "* Top #{num} Players for #{chan}:"
697     n = [ num, q.rank_table.length ].min
698     n.times do |i|
699       player = q.rank_table[i]
700       nick = player[0]
701       score = player[1].score
702       ar << "#{i + 1}. #{unhilight_nick( nick )} (#{score})"
703     end
704     m.reply ar.join(" | "), :split_at => /\s+\|\s+/
705   end
706
707
708   def cmd_stats( m, params )
709     fetch_data( m ) if @questions.empty?
710
711     m.reply "* Total Number of Questions:"
712     m.reply "    #{@questions.length}"
713   end
714
715
716   def cmd_score( m, params )
717     nick = m.sourcenick.to_s
718     say_score( m, nick )
719   end
720
721
722   def cmd_score_player( m, params )
723     say_score( m, params[:player] )
724   end
725
726
727   def cmd_autoask( m, params )
728     chan = m.channel
729     q = create_quiz( chan, m )
730     return unless q
731
732     params[:enable] ||= 'status'
733
734     reg = q.registry_conf
735
736     case params[:enable].downcase
737     when "on", "true"
738       reg["autoask"] = true
739       m.reply "Enabled autoask mode."
740       reg["autoask_delay"] = 0 unless reg.has_key("autoask_delay")
741       cmd_quiz( m, nil ) if q.question == nil
742     when "off", "false"
743       reg["autoask"] = false
744       m.reply "Disabled autoask mode."
745     when "status"
746       if reg.has_key? "autoask"
747         m.reply _("autoask is %{status}, the delay is %{time}") % {
748           :status => reg["autoask"],
749           :time => Utils.secs_to_string(reg["autoask_delay"]),
750         }
751       else
752         m.reply _("autoask is not configured here")
753       end
754     else
755       m.reply "Invalid autoask parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
756     end
757   end
758
759   def cmd_autoask_delay( m, params )
760     chan = m.channel
761     q = create_quiz( chan, m )
762     return unless q
763
764     delay = params[:time].to_i
765     q.registry_conf["autoask_delay"] = delay
766     m.reply "autoask delay now #{q.registry_conf['autoask_delay']} seconds"
767   end
768
769   def cmd_transfer( m, params )
770     chan = m.channel
771     q = create_quiz( chan, m )
772     return unless q
773
774     debug q.rank_table.inspect
775
776     source = params[:source]
777     dest = params[:dest]
778     transscore = params[:score].to_i
779     transjokers = params[:jokers].to_i
780     debug "Transferring #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
781
782     if q.registry.has_key?(source)
783       sourceplayer = q.registry[source]
784       score = sourceplayer.score
785       if transscore == -1
786         transscore = score
787       end
788       if score < transscore
789         m.reply "#{source} only has #{score} points!"
790         return
791       end
792       jokers = sourceplayer.jokers
793       if transjokers == -1
794         transjokers = jokers
795       end
796       if jokers < transjokers
797         m.reply "#{source} only has #{jokers} jokers!!"
798         return
799       end
800       if q.registry.has_key?(dest)
801         destplayer = q.registry[dest]
802       else
803         destplayer = PlayerStats.new(0,0,0)
804       end
805
806       if sourceplayer.object_id == destplayer.object_id
807         m.reply "Source and destination are the same, I'm not going to touch them"
808         return
809       end
810
811       sourceplayer.score -= transscore
812       destplayer.score += transscore
813       sourceplayer.jokers -= transjokers
814       destplayer.jokers += transjokers
815
816       q.registry[source] = sourceplayer
817       calculate_ranks(m, q, source)
818
819       q.registry[dest] = destplayer
820       calculate_ranks(m, q, dest)
821
822       m.reply "Transferred #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
823     else
824       m.reply "#{source} doesn't have any points!"
825     end
826   end
827
828
829   def cmd_del_player( m, params )
830     chan = m.channel
831     q = create_quiz( chan, m )
832     return unless q
833
834     debug q.rank_table.inspect
835
836     nick = params[:nick]
837     if q.registry.has_key?(nick)
838       player = q.registry[nick]
839       score = player.score
840       if score != 0
841         m.reply "Can't delete player #{nick} with score #{score}."
842         return
843       end
844       jokers = player.jokers
845       if jokers != 0
846         m.reply "Can't delete player #{nick} with #{jokers} jokers."
847         return
848       end
849       q.registry.delete(nick)
850
851       player_rank = nil
852       q.rank_table.each_index { |rank|
853         if nick.downcase == q.rank_table[rank][0].downcase
854           player_rank = rank
855           break
856         end
857       }
858       q.rank_table.delete_at(player_rank)
859
860       m.reply "Player #{nick} deleted."
861     else
862       m.reply "Player #{nick} isn't even in the database."
863     end
864   end
865
866
867   def cmd_set_score(m, params)
868     chan = m.channel
869     q = create_quiz( chan, m )
870     return unless q
871
872     debug q.rank_table.inspect
873
874     nick = params[:nick]
875     val = params[:score].to_i
876     if q.registry.has_key?(nick)
877       player = q.registry[nick]
878       player.score = val
879     else
880       player = PlayerStats.new( val, 0, 0)
881     end
882     q.registry[nick] = player
883     calculate_ranks(m, q, nick)
884     m.reply "Score for player #{nick} set to #{val}."
885   end
886
887
888   def cmd_set_jokers(m, params)
889     chan = m.channel
890     q = create_quiz( chan, m )
891     return unless q
892
893     debug q.rank_table.inspect
894
895     nick = params[:nick]
896     val = [params[:jokers].to_i, @bot.config['quiz.max_jokers']].min
897     if q.registry.has_key?(nick)
898       player = q.registry[nick]
899       player.jokers = val
900     else
901       player = PlayerStats.new( 0, val, 0)
902     end
903     q.registry[nick] = player
904     m.reply "Jokers for player #{nick} set to #{val}."
905   end
906
907
908   def cmd_cleanup(m, params)
909     chan = m.channel
910     q = create_quiz( chan, m )
911     return unless q
912
913     null_players = []
914     q.registry.each { |nick, player|
915       null_players << nick if player.jokers == 0 and player.score == 0
916     }
917     debug "Cleaning up by removing #{null_players * ', '}"
918     null_players.each { |nick|
919       cmd_del_player(m, :nick => nick)
920     }
921
922   end
923
924   def stop(m, params)
925     unless m.public?
926       m.reply 'you must be on some channel to use this command'
927       return
928     end
929     if @quizzes.delete m.channel
930       @ask_mutex.synchronize do
931         t = @waiting.delete(m.channel)
932         @bot.timer.remove t if t
933       end
934       m.okay
935     else
936       m.reply(_("there is no active quiz on #{m.channel}"))
937     end
938   end
939
940 end
941
942 plugin = QuizPlugin.new
943 plugin.default_auth( 'edit', false )
944
945 # Normal commands
946 plugin.map 'quiz',                  :action => 'cmd_quiz'
947 plugin.map 'quiz solve',            :action => 'cmd_solve'
948 plugin.map 'quiz hint',             :action => 'cmd_hint'
949 plugin.map 'quiz skip',             :action => 'cmd_skip'
950 plugin.map 'quiz joker',            :action => 'cmd_joker'
951 plugin.map 'quiz score',            :action => 'cmd_score'
952 plugin.map 'quiz score :player',    :action => 'cmd_score_player'
953 plugin.map 'quiz fetch',            :action => 'cmd_fetch'
954 plugin.map 'quiz refresh',          :action => 'cmd_refresh'
955 plugin.map 'quiz top5',             :action => 'cmd_top5'
956 plugin.map 'quiz top :number',      :action => 'cmd_top_number'
957 plugin.map 'quiz stats',            :action => 'cmd_stats'
958 plugin.map 'quiz stop', :action => :stop
959
960 # Admin commands
961 plugin.map 'quiz autoask [:enable]',  :action => 'cmd_autoask', :auth_path => 'edit'
962 plugin.map 'quiz autoask delay :time',  :action => 'cmd_autoask_delay', :auth_path => 'edit', :requirements => {:time => /\d+/}
963 plugin.map 'quiz transfer :source :dest :score :jokers', :action => 'cmd_transfer', :auth_path => 'edit', :defaults => {:score => '-1', :jokers => '-1'}
964 plugin.map 'quiz deleteplayer :nick', :action => 'cmd_del_player', :auth_path => 'edit'
965 plugin.map 'quiz setscore :nick :score', :action => 'cmd_set_score', :auth_path => 'edit'
966 plugin.map 'quiz setjokers :nick :jokers', :action => 'cmd_set_jokers', :auth_path => 'edit'
967 plugin.map 'quiz cleanup', :action => 'cmd_cleanup', :auth_path => 'edit'