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