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