4 # :title: Quiz plugin for rbot
6 # Author:: Mark Kretschmann <markey@web.de>
7 # Author:: Jocke Andersson <ajocke@gmail.com>
8 # Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
9 # Author:: Yaohan Chen <yaohan.chen@gmail.com>
11 # Copyright:: (C) 2006 Mark Kretschmann, Jocke Andersson, Giuseppe Bilotta
12 # Copyright:: (C) 2007 Giuseppe Bilotta, Yaohan Chen
16 # A trivia quiz game. Fast paced, featureful and fun.
18 # FIXME:: interesting fact: in the Quiz class, @registry.has_key? seems to be
19 # case insensitive. Although this is all right for us, this leads to
20 # rank vs registry mismatches. So we have to make the @rank_table
21 # comparisons case insensitive as well. For the moment, redefine
22 # everything to downcase before matching the nick.
24 # TODO:: define a class for the rank table. We might also need it for scoring
27 # TODO:: when Ruby 2.0 gets out, fix the FIXME 2.0 UTF-8 workarounds
29 # Class for storing question/answer pairs
30 define_structure :QuizBundle, :question, :answer
32 # Class for storing player stats
33 define_structure :PlayerStats, :score, :jokers, :jokers_time
34 # Why do we still need jokers_time? //Firetech
41 #######################################################################
43 # Abstract an answer to a quiz question, by providing self as a string
44 # and a core that can be answered as an alternative. It also provides
45 # a boolean that tells if the core is numeric or not
46 #######################################################################
53 if @string =~ /#(.+)#/
55 @string.gsub!('#', '')
57 raise ArgumentError, "empty string can't be a valid answer!" if @string.empty?
58 raise ArgumentError, "empty core can't be a valid answer!" if @core and @core.empty?
60 @numeric = (core.to_i.to_s == core) || (core.to_f.to_s == core)
73 str.downcase == core.downcase || str.downcase == @string.downcase
85 #######################################################################
87 # One Quiz instance per channel, contains channel specific data
88 #######################################################################
90 attr_accessor :registry, :registry_conf, :questions,
91 :question, :answers, :canonical_answer, :answer_array,
92 :first_try, :hint, :hintrange, :rank_table, :hinted, :has_errors,
95 def initialize( channel, registry )
97 @registry = registry.sub_registry( 'private' )
99 @registry = registry.sub_registry( channel.downcase )
102 @registry.each_key { |k|
103 unless @registry.has_key?(k)
105 error "Data for #{k} is NOT ACCESSIBLE! Database corrupt?"
109 debug @registry.to_a.map { |a| a.join(", ")}.join("\n")
112 @registry_conf = @registry.sub_registry( "config" )
114 # Per-channel list of sources. If empty, the default one (quiz/quiz.rbot)
116 @registry_conf["sources"] = [] unless @registry_conf.has_key?( "sources" )
118 # Per-channel copy of the global questions table. Acts like a shuffled queue
119 # from which questions are taken, until empty. Then we refill it with questions
120 # from the global table.
121 @registry_conf["questions"] = [] unless @registry_conf.has_key?( "questions" )
123 # Autoask defaults to true
124 @registry_conf["autoask"] = true unless @registry_conf.has_key?( "autoask" )
126 # Autoask delay defaults to 0 (instantly)
127 @registry_conf["autoask_delay"] = 0 unless @registry_conf.has_key?( "autoask_delay" )
129 @questions = @registry_conf["questions"]
132 @canonical_answer = nil
141 # True if the answers is entirely done by separators
144 # We keep this array of player stats for performance reasons. It's sorted by score
145 # and always synced with the registry player stats hash. This way we can do fast
146 # rank lookups, without extra sorting.
147 @rank_table = @registry.to_a.sort { |a,b| b[1].score<=>a[1].score }
152 #######################################################################
154 #######################################################################
155 class QuizPlugin < Plugin
156 Config.register Config::BooleanValue.new('quiz.dotted_nicks',
158 :desc => "When true, nicks in the top X scores will be camouflaged to prevent IRC hilighting")
160 Config.register Config::ArrayValue.new('quiz.sources',
161 :default => ['quiz.rbot'],
162 :desc => "List of files and URLs that will be used to retrieve quiz questions")
165 Config.register Config::IntegerValue.new('quiz.max_jokers',
167 :desc => "Maximum number of jokers a player can gain")
172 @questions = Array.new
175 @ask_mutex = Mutex.new
179 @ask_mutex.synchronize do
180 # purge all waiting timers
181 @waiting.each do |chan, t|
182 @bot.timer.remove t.first
183 @bot.say chan, _("stopped quiz timer")
187 chans = @quizzes.keys
190 @bot.say chan, _("quiz stopped")
194 # Function that returns whether a char is a "separator", used for hints
201 # Fetches questions from the data sources, which can be either local files
202 # (in quiz/) or web pages.
205 # Read the winning messages file
206 @win_messages = Array.new
207 winfile = datafile 'win_messages'
208 if File.exists? winfile
209 IO.foreach(winfile) { |line| @win_messages << line.chomp }
211 warning( "win_messages file not found!" )
212 # Fill the array with a least one message or code accessing it would fail
213 @win_messages << "<who> guessed right! The answer was <answer>"
216 m.reply "Fetching questions ..."
218 # TODO Per-channel sources
221 @bot.config['quiz.sources'].each { |p|
222 if p =~ /^https?:\/\//
225 serverdata = @bot.httputil.get(p) # "http://amarok.kde.org/amarokwiki/index.php/Rbot_Quiz"
226 serverdata = serverdata.split( "QUIZ DATA START\n" )[1]
227 serverdata = serverdata.split( "\nQUIZ DATA END" )[0]
228 serverdata = serverdata.gsub( / /, " " ).gsub( /&/, "&" ).gsub( /"/, "\"" )
229 data << "\n\n" << serverdata
231 m.reply "Failed to download questions from #{p}, ignoring sources"
235 debug "Fetching from #{path}"
239 data << "\n\n" << File.read(path)
241 m.reply "Failed to read from local database file #{p}, skipping."
248 # Fuse together and remove comments, then split
249 entries = data.strip.gsub( /^#.*$/, "" ).split( /(?:^|\n+)Question: / )
253 # We'll need at least two lines of data
255 # Check if question isn't empty
257 while p[1].match( /^Answer: (.*)$/ ) == nil and p.size > 2
258 # Delete all lines between the question and the answer
261 p[1] = p[1].gsub( /Answer: /, "" ).strip
262 # If the answer was found
264 # Add the data to the array
265 b = QuizBundle.new( p[0], p[1] )
272 m.reply "done, #{@questions.length} questions loaded."
276 # Returns new Quiz instance for channel, or existing one
277 # Announce errors if a message is passed as second parameter
279 def create_quiz(channel, m=nil)
280 unless @quizzes.has_key?( channel )
281 @quizzes[channel] = Quiz.new( channel, @registry )
284 if @quizzes[channel].has_errors
285 m.reply _("Sorry, the quiz database for %{chan} seems to be corrupt") % {
290 return @quizzes[channel]
295 def say_score( m, nick )
297 q = create_quiz( chan, m )
300 if q.registry.has_key?( nick )
301 score = q.registry[nick].score
302 jokers = q.registry[nick].jokers
305 q.rank_table.each do |place|
307 break if nick.downcase == place[0].downcase
310 m.reply "#{nick}'s score is: #{score} Rank: #{rank} Jokers: #{jokers}"
312 m.reply "#{nick} does not have a score yet. Lamer."
317 def help( plugin, topic="" )
319 _("Quiz game aministration commands (requires authentication): ") + [
320 _("'quiz autoask <on/off>' => enable/disable autoask mode"),
321 _("'quiz autoask delay <time>' => delay next quiz by <time> when in autoask mode"),
322 _("'quiz autoskip <on/off>' => enable/disable autoskip mode (autoskip implies autoask)"),
323 _("'quiz autoskip delay <time>' => wait <time> before skipping to next quiz when in autoskip mode"),
324 _("'quiz transfer <source> <dest> [score] [jokers]' => transfer [score] points and [jokers] jokers from <source> to <dest> (default is entire score and all jokers)"),
325 _("'quiz setscore <player> <score>' => set <player>'s score to <score>"),
326 _("'quiz setjokers <player> <jokers>' => set <player>'s number of jokers to <jokers>"),
327 _("'quiz deleteplayer <player>' => delete one player from the rank table (only works when score and jokers are set to 0)"),
328 _("'quiz cleanup' => remove players with no points and no jokers")
331 urls = @bot.config['quiz.sources'].select { |p| p =~ /^https?:\/\// }
332 "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. 'quiz refresh' => refresh the question pool for this channel." + (urls.empty? ? "" : "\nYou can add new questions at #{urls.join(', ')}")
337 # Updates the per-channel rank table, which is kept for performance reasons.
338 # This table contains all players sorted by rank.
340 def calculate_ranks( m, q, nick )
341 if q.registry.has_key?( nick )
342 stats = q.registry[nick]
344 # Find player in table
346 q.rank_table.each_with_index do |place, i|
347 if nick.downcase == place[0].downcase
353 # Remove player from old position
355 q.rank_table.delete_at( old_rank )
358 # Insert player at new position
360 q.rank_table.each_with_index do |place, i|
361 if stats.score > place[1].score
362 q.rank_table[i,0] = [[nick, stats]]
368 # If less than all other players' scores, append to table
370 new_rank = q.rank_table.length
371 q.rank_table << [nick, stats]
374 # Print congratulations/condolences if the player's rank has changed
376 if new_rank < old_rank
377 m.reply "#{nick} ascends to rank #{new_rank + 1}. Congratulations :)"
378 elsif new_rank > old_rank
379 m.reply "#{nick} slides down to rank #{new_rank + 1}. So Sorry! NOT. :p"
383 q.rank_table << [[nick, PlayerStats.new( 1 )]]
388 def setup_ask_timer(m, q)
390 delay = q.registry_conf["autoask_delay"]
392 m.reply "#{Bold}#{Color}03Next question in #{Bold}#{delay}#{Bold} seconds"
393 timer = @bot.timer.add_once(delay) {
394 @ask_mutex.synchronize do
395 @waiting.delete(chan)
399 @waiting[chan] = [timer, :ask]
405 # Reimplemented from Plugin
409 return unless @quizzes.has_key?( chan )
412 return if q.question == nil
414 message = m.message.downcase.strip
416 nick = m.sourcenick.to_s
418 # Support multiple alternate answers and cores
419 answer = q.answers.find { |ans| ans.valid?(message) }
422 # purge the autoskip timer
423 @ask_mutex.synchronize do
424 if @waiting.key? chan and @waiting[chan].last == :skip
425 @bot.timer.remove(@waiting[chan].first)
426 @waiting.delete(chan)
430 # List canonical answer which the hint was based on, to avoid confusion
431 # FIXME display this more friendly
432 answer.info = " (hints were for alternate answer #{q.canonical_answer.core})" if answer != q.canonical_answer and q.hinted
437 reply = "WHOPEEE! #{nick} got it on the first try! That's worth an extra point. Answer was: #{answer}"
438 elsif q.rank_table.length >= 1 and nick.downcase == q.rank_table[0][0].downcase
439 reply = "THE QUIZ CHAMPION defends his throne! Seems like #{nick} is invicible! Answer was: #{answer}"
440 elsif q.rank_table.length >= 2 and nick.downcase == q.rank_table[1][0].downcase
441 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: #{answer}"
442 elsif q.rank_table.length >= 3 and nick.downcase == q.rank_table[2][0].downcase
443 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: #{answer}"
445 reply = @win_messages[rand( @win_messages.length )].dup
446 reply.gsub!( "<who>", nick )
447 reply.gsub!( "<answer>", answer )
453 if q.registry.has_key?(nick)
454 player = q.registry[nick]
456 player = PlayerStats.new( 0, 0, 0 )
459 player.score = player.score + points
461 # Reward player with a joker every X points
462 if player.score % 15 == 0 and player.jokers < @bot.config['quiz.max_jokers']
464 m.reply "#{nick} gains a new joker. Rejoice :)"
467 q.registry[nick] = player
468 calculate_ranks( m, q, nick)
472 if q.registry_conf['autoskip'] or q.registry_conf["autoask"]
473 setup_ask_timer(m, q)
476 # First try is used, and it wasn't the answer.
482 # Stretches an IRC nick with dots, simply to make the client not trigger a hilight,
483 # which is annoying for those not watching. Example: markey -> m.a.r.k.e.y
485 def unhilight_nick( nick )
486 return nick unless @bot.config['quiz.dotted_nicks']
487 return nick.split(//).join(".")
491 #######################################################################
493 #######################################################################
494 def cmd_quiz( m, params )
495 fetch_data( m ) if @questions.empty?
498 @ask_mutex.synchronize do
499 if @waiting.has_key?(chan) and @waiting[chan].last == :ask
500 m.reply "Next quiz question will be automatically asked soon, have patience"
505 q = create_quiz( chan, m )
509 m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
510 m.reply "Hint: #{q.hint.join}" if q.hinted
514 # Fill per-channel questions buffer
515 if q.questions.empty?
516 q.questions = @questions.sort_by { rand }
519 # pick a question and delete it (delete_at returns the deleted item)
520 picked = q.questions.delete_at( rand(q.questions.length) )
522 q.question = picked.question
523 q.answers = picked.answer.split(/\s+\|\|\s+/).map { |ans| QuizAnswer.new(ans) }
525 # Check if any core answer is numerical and tell the players so, if that's the case
526 # The rather obscure statement is needed because to_i and to_f returns 99(.0) for "99 red balloons", and 0 for "balloon"
528 # The "canonical answer" is also determined here, defined to be the first found numerical answer, or
530 numeric = q.answers.find { |ans| ans.numeric? }
532 q.question += "#{Color}07 (Numerical answer)#{Color}"
533 q.canonical_answer = numeric
535 q.canonical_answer = q.answers.first
543 q.canonical_answer.core.scan(/./u) { |ch|
552 # It's possible that an answer is entirely done by separators,
553 # in which case we'll hide everything
554 if q.answer_array == q.hint
562 # Generate array of unique random range
563 q.hintrange = (0..q.hint.length-1).sort_by{ rand }
565 m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
567 if q.registry_conf.key? 'autoskip'
568 delay = q.registry_conf['autoskip_delay']
569 timer = @bot.timer.add_once(delay) do
570 m.reply _("Nobody managed to answer in %{time}! Skipping to the next question ...") % {
571 :time => Utils.secs_to_string(delay)
574 @ask_mutex.synchronize do
575 @waiting.delete(chan)
577 setup_ask_timer(m, q)
579 @waiting[chan] = [timer, :skip]
584 def cmd_solve( m, params )
587 @ask_mutex.synchronize do
588 if @waiting.has_key?(chan) and @waiting[chan].last == :skip
589 m.reply _("you can't make me solve a quiz in autoskip mode, sorry")
594 return unless @quizzes.has_key?( chan )
597 m.reply "The correct answer was: #{q.canonical_answer}"
601 cmd_quiz( m, nil ) if q.registry_conf["autoask"] or q.registry_conf["autoskip"]
605 def cmd_hint( m, params )
607 nick = m.sourcenick.to_s
609 return unless @quizzes.has_key?(chan)
613 m.reply "#{nick}: Get a question first!"
615 num_chars = case q.hintrange.length # Number of characters to reveal
628 index = q.hintrange.pop
629 # New hint char until the char isn't a "separator" (space etc.)
630 end while is_sep(q.answer_array[index]) and not q.all_seps
631 q.hint[index] = q.answer_array[index]
633 m.reply "Hint: #{q.hint.join}"
637 if q.hint == q.answer_array
638 m.reply "#{Bold}#{Color}04BUST!#{Color}#{Bold} This round is over. #{Color}04Minus one point for #{nick}#{Color}."
641 if q.registry.has_key?( nick )
642 stats = q.registry[nick]
644 stats = PlayerStats.new( 0, 0, 0 )
647 stats["score"] = stats.score - 1
648 q.registry[nick] = stats
650 calculate_ranks( m, q, nick)
653 cmd_quiz( m, nil ) if q.registry_conf["autoask"]
659 def cmd_skip( m, params )
662 @ask_mutex.synchronize do
663 if @waiting.has_key?(chan) and @waiting[chan].last == :skip
664 m.reply _("I'll skip to the next question as soon as the timeout expires, not now")
669 return unless @quizzes.has_key?(chan)
673 cmd_quiz( m, params )
677 def cmd_joker( m, params )
679 nick = m.sourcenick.to_s
680 q = create_quiz(chan, m)
684 m.reply "#{nick}: There is no open question."
688 if q.registry[nick].jokers > 0
689 player = q.registry[nick]
692 q.registry[nick] = player
694 calculate_ranks( m, q, nick )
696 if player.jokers != 1
701 m.reply "#{Bold}#{Color}12JOKER!#{Color}#{Bold} #{nick} draws a joker and wins this round. You have #{player.jokers} #{jokers} left."
702 m.reply "The answer was: #{q.canonical_answer}."
705 cmd_quiz( m, nil ) if q.registry_conf["autoask"]
707 m.reply "#{nick}: You don't have any jokers left ;("
712 def cmd_fetch( m, params )
717 def cmd_refresh( m, params )
718 q = create_quiz(m.channel)
721 cmd_quiz( m, params )
725 def cmd_top5( m, params )
727 q = create_quiz( chan, m )
730 if q.rank_table.empty?
731 m.reply "There are no scores known yet!"
735 m.reply "* Top 5 Players for #{chan}:"
737 [5, q.rank_table.length].min.times do |i|
738 player = q.rank_table[i]
740 score = player[1].score
741 m.reply " #{i + 1}. #{unhilight_nick( nick )} (#{score})"
746 def cmd_top_number( m, params )
747 num = params[:number].to_i
748 return if num < 1 or num > 50
750 q = create_quiz( chan, m )
753 if q.rank_table.empty?
754 m.reply "There are no scores known yet!"
759 m.reply "* Top #{num} Players for #{chan}:"
760 n = [ num, q.rank_table.length ].min
762 player = q.rank_table[i]
764 score = player[1].score
765 ar << "#{i + 1}. #{unhilight_nick( nick )} (#{score})"
767 m.reply ar.join(" | "), :split_at => /\s+\|\s+/
771 def cmd_stats( m, params )
772 fetch_data( m ) if @questions.empty?
774 m.reply "* Total Number of Questions:"
775 m.reply " #{@questions.length}"
779 def cmd_score( m, params )
780 nick = m.sourcenick.to_s
785 def cmd_score_player( m, params )
786 say_score( m, params[:player] )
790 def cmd_autoask( m, params )
792 q = create_quiz( chan, m )
795 params[:enable] ||= 'status'
797 reg = q.registry_conf
799 case params[:enable].downcase
801 reg["autoask"] = true
802 m.reply "Enabled autoask mode."
803 reg["autoask_delay"] = 0 unless reg.has_key("autoask_delay")
804 cmd_quiz( m, nil ) if q.question == nil
806 reg["autoask"] = false
807 m.reply "Disabled autoask mode."
809 if reg.has_key? "autoask"
810 m.reply _("autoask is %{status}, the delay is %{time}") % {
811 :status => reg["autoask"],
812 :time => Utils.secs_to_string(reg["autoask_delay"]),
815 m.reply _("autoask is not configured here")
818 m.reply "Invalid autoask parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
822 def cmd_autoask_delay( m, params )
824 q = create_quiz( chan, m )
827 time = params[:time].to_s
832 delay = Utils.parse_time_offset(time)
834 m.reply _("I couldn't understand that delay expression, sorry")
840 m.reply _("wait, you want me to ask the next question %{abs} BEFORE the previous one gets solved?") % {
841 :abs => Utils.secs_to_string(-delay)
846 q.registry_conf["autoask_delay"] = delay
847 m.reply "autoask delay now #{q.registry_conf['autoask_delay']} seconds"
851 def cmd_autoskip( m, params )
853 q = create_quiz( chan, m )
856 params[:enable] ||= 'status'
858 reg = q.registry_conf
860 case params[:enable].downcase
862 reg["autoskip"] = true
863 m.reply "Enabled autoskip mode."
864 # default: 1 minute (TODO customize with a global config key)
865 reg["autoskip_delay"] = 60 unless reg.has_key("autoskip_delay")
866 # also set a default autoask delay
867 reg["autoask_delay"] = 0 unless reg.has_key("autoask_delay")
869 reg["autoskip"] = false
870 m.reply "Disabled autoskip mode."
872 if reg.has_key? "autoskip"
873 m.reply _("autoskip is %{status}, the delay is %{time}") % {
874 :status => reg["autoskip"],
875 :time => Utils.secs_to_string(reg["autoskip_delay"]),
878 m.reply _("autoskip is not configured here")
881 m.reply "Invalid autoskip parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
885 def cmd_autoskip_delay( m, params )
887 q = create_quiz( chan, m )
890 time = params[:time].to_s
895 delay = Utils.parse_time_offset(time)
897 m.reply _("I couldn't understand that delay expression, sorry")
903 m.reply _("wait, you want me to skip to the next question %{abs} BEFORE the previous one?") % {
904 :abs => Utils.secs_to_string(-delay)
908 m.reply _("sure, I'll ask all the questions at the same time! </sarcasm>")
912 q.registry_conf["autoskip_delay"] = delay
913 m.reply "autoskip delay now #{q.registry_conf['autoskip_delay']} seconds"
917 def cmd_transfer( m, params )
919 q = create_quiz( chan, m )
922 debug q.rank_table.inspect
924 source = params[:source]
926 transscore = params[:score].to_i
927 transjokers = params[:jokers].to_i
928 debug "Transferring #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
930 if q.registry.has_key?(source)
931 sourceplayer = q.registry[source]
932 score = sourceplayer.score
936 if score < transscore
937 m.reply "#{source} only has #{score} points!"
940 jokers = sourceplayer.jokers
944 if jokers < transjokers
945 m.reply "#{source} only has #{jokers} jokers!!"
948 if q.registry.has_key?(dest)
949 destplayer = q.registry[dest]
951 destplayer = PlayerStats.new(0,0,0)
954 if sourceplayer.object_id == destplayer.object_id
955 m.reply "Source and destination are the same, I'm not going to touch them"
959 sourceplayer.score -= transscore
960 destplayer.score += transscore
961 sourceplayer.jokers -= transjokers
962 destplayer.jokers += transjokers
964 q.registry[source] = sourceplayer
965 calculate_ranks(m, q, source)
967 q.registry[dest] = destplayer
968 calculate_ranks(m, q, dest)
970 m.reply "Transferred #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
972 m.reply "#{source} doesn't have any points!"
977 def cmd_del_player( m, params )
979 q = create_quiz( chan, m )
982 debug q.rank_table.inspect
985 if q.registry.has_key?(nick)
986 player = q.registry[nick]
989 m.reply "Can't delete player #{nick} with score #{score}."
992 jokers = player.jokers
994 m.reply "Can't delete player #{nick} with #{jokers} jokers."
997 q.registry.delete(nick)
1000 q.rank_table.each_index { |rank|
1001 if nick.downcase == q.rank_table[rank][0].downcase
1006 q.rank_table.delete_at(player_rank)
1008 m.reply "Player #{nick} deleted."
1010 m.reply "Player #{nick} isn't even in the database."
1015 def cmd_set_score(m, params)
1017 q = create_quiz( chan, m )
1020 debug q.rank_table.inspect
1022 nick = params[:nick]
1023 val = params[:score].to_i
1024 if q.registry.has_key?(nick)
1025 player = q.registry[nick]
1028 player = PlayerStats.new( val, 0, 0)
1030 q.registry[nick] = player
1031 calculate_ranks(m, q, nick)
1032 m.reply "Score for player #{nick} set to #{val}."
1036 def cmd_set_jokers(m, params)
1038 q = create_quiz( chan, m )
1041 debug q.rank_table.inspect
1043 nick = params[:nick]
1044 val = [params[:jokers].to_i, @bot.config['quiz.max_jokers']].min
1045 if q.registry.has_key?(nick)
1046 player = q.registry[nick]
1049 player = PlayerStats.new( 0, val, 0)
1051 q.registry[nick] = player
1052 m.reply "Jokers for player #{nick} set to #{val}."
1056 def cmd_cleanup(m, params)
1058 q = create_quiz( chan, m )
1062 q.registry.each { |nick, player|
1063 null_players << nick if player.jokers == 0 and player.score == 0
1065 debug "Cleaning up by removing #{null_players * ', '}"
1066 null_players.each { |nick|
1067 cmd_del_player(m, :nick => nick)
1074 m.reply 'you must be on some channel to use this command'
1077 if @quizzes.delete m.channel
1078 @ask_mutex.synchronize do
1079 t = @waiting.delete(m.channel)
1080 @bot.timer.remove t.first if t
1084 m.reply(_("there is no active quiz on #{m.channel}"))
1090 plugin = QuizPlugin.new
1091 plugin.default_auth( 'edit', false )
1094 plugin.map 'quiz', :action => 'cmd_quiz'
1095 plugin.map 'quiz solve', :action => 'cmd_solve'
1096 plugin.map 'quiz hint', :action => 'cmd_hint'
1097 plugin.map 'quiz skip', :action => 'cmd_skip'
1098 plugin.map 'quiz joker', :action => 'cmd_joker'
1099 plugin.map 'quiz score', :action => 'cmd_score'
1100 plugin.map 'quiz score :player', :action => 'cmd_score_player'
1101 plugin.map 'quiz fetch', :action => 'cmd_fetch'
1102 plugin.map 'quiz refresh', :action => 'cmd_refresh'
1103 plugin.map 'quiz top5', :action => 'cmd_top5'
1104 plugin.map 'quiz top :number', :action => 'cmd_top_number'
1105 plugin.map 'quiz stats', :action => 'cmd_stats'
1106 plugin.map 'quiz stop', :action => :stop
1109 plugin.map 'quiz autoask [:enable]', :action => 'cmd_autoask', :auth_path => 'edit'
1110 plugin.map 'quiz autoask delay *time', :action => 'cmd_autoask_delay', :auth_path => 'edit'
1111 plugin.map 'quiz autoskip [:enable]', :action => 'cmd_autoskip', :auth_path => 'edit'
1112 plugin.map 'quiz autoskip delay *time', :action => 'cmd_autoskip_delay', :auth_path => 'edit'
1113 plugin.map 'quiz transfer :source :dest :score :jokers', :action => 'cmd_transfer', :auth_path => 'edit', :defaults => {:score => '-1', :jokers => '-1'}
1114 plugin.map 'quiz deleteplayer :nick', :action => 'cmd_del_player', :auth_path => 'edit'
1115 plugin.map 'quiz setscore :nick :score', :action => 'cmd_set_score', :auth_path => 'edit'
1116 plugin.map 'quiz setjokers :nick :jokers', :action => 'cmd_set_jokers', :auth_path => 'edit'
1117 plugin.map 'quiz cleanup', :action => 'cmd_cleanup', :auth_path => 'edit'