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