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