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