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