]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/quiz.rb
Plugin header boilerplating.
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / quiz.rb
index 17daae1e8d356f819c7f84f4afbdcba09bc0e7a3..7e2c0f83b1c201251e3093798de6fa1a4d900699 100644 (file)
@@ -1,12 +1,30 @@
-# Plugin for the Ruby IRC bot (http://linuxbrit.co.uk/rbot/)
+#-- vim:sw=2:et
+#++
 #
-# A trivia quiz game. Fast paced, featureful and fun.
+# :title: Quiz plugin for rbot
+#
+# Author:: Mark Kretschmann <markey@web.de>
+# Author:: Jocke Andersson <ajocke@gmail.com>
+# Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+# Author:: Yaohan Chen <yaohan.chen@gmail.com>
+#
+# Copyright:: (C) 2006 Mark Kretschmann, Jocke Andersson, Giuseppe Bilotta
+# Copyright:: (C) 2007 Giuseppe Bilotta, Yaohan Chen
+#
+# License:: GPL v2
 #
-# (c) 2006 Mark Kretschmann <markey@web.de>
-# (c) 2006 Jocke Andersson <ajocke@gmail.com>
-# (c) 2006 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
-# Licensed under GPL V2.
+# A trivia quiz game. Fast paced, featureful and fun.
 
+# FIXME:: interesting fact: in the Quiz class, @registry.has_key? seems to be
+#         case insensitive. Although this is all right for us, this leads to
+#         rank vs registry mismatches. So we have to make the @rank_table
+#         comparisons case insensitive as well. For the moment, redefine
+#         everything to downcase before matching the nick.
+#
+# TODO:: define a class for the rank table. We might also need it for scoring
+#        in other games.
+#
+# TODO:: when Ruby 2.0 gets out, fix the FIXME 2.0 UTF-8 workarounds
 
 # Class for storing question/answer pairs
 QuizBundle = Struct.new( "QuizBundle", :question, :answer )
@@ -23,18 +41,82 @@ Color = "\003"
 Bold = "\002"
 
 
+#######################################################################
+# CLASS QuizAnswer
+# Abstract an answer to a quiz question, by providing self as a string
+# and a core that can be answered as an alternative. It also provides
+# a boolean that tells if the core is numeric or not
+#######################################################################
+class QuizAnswer
+  attr_writer :info
+
+  def initialize(str)
+    @string = str.strip
+    @core = nil
+    if @string =~ /#(.+)#/
+      @core = $1
+      @string.gsub!('#', '')
+    end
+    raise ArgumentError, "empty string can't be a valid answer!" if @string.empty?
+    raise ArgumentError, "empty core can't be a valid answer!" if @core and @core.empty?
+
+    @numeric = (core.to_i.to_s == core) || (core.to_f.to_s == core)
+    @info = nil
+  end
+
+  def core
+    @core || @string
+  end
+
+  def numeric?
+    @numeric
+  end
+
+  def valid?(str)
+    str.downcase == core.downcase || str.downcase == @string.downcase
+  end
+
+  def to_str
+    [@string, @info].join
+  end
+  alias :to_s :to_str
+
+
+end
+
+
 #######################################################################
 # CLASS Quiz
 # One Quiz instance per channel, contains channel specific data
 #######################################################################
 class Quiz
-  attr_accessor :registry, :registry_conf, :questions, :question, :answer, :answer_core,
-  :first_try, :hint, :hintrange, :rank_table, :hinted
+  attr_accessor :registry, :registry_conf, :questions,
+    :question, :answers, :canonical_answer, :answer_array,
+    :first_try, :hint, :hintrange, :rank_table, :hinted, :has_errors
 
   def initialize( channel, registry )
-    @registry = registry.sub_registry( channel )
+    if !channel
+      @registry = registry.sub_registry( 'private' )
+    else
+      @registry = registry.sub_registry( channel.downcase )
+    end
+    @has_errors = false
+    @registry.each_key { |k|
+      unless @registry.has_key?(k)
+        @has_errors = true
+        error "Data for #{k} is NOT ACCESSIBLE! Database corrupt?"
+      end
+    }
+    if @has_errors
+      debug @registry.to_a.map { |a| a.join(", ")}.join("\n")
+    end
+
     @registry_conf = @registry.sub_registry( "config" )
 
+    # Per-channel list of sources. If empty, the default one (quiz/quiz.rbot)
+    # will be used. TODO
+    @registry_conf["sources"] = [] unless @registry_conf.has_key?( "sources" )
+
     # Per-channel copy of the global questions table. Acts like a shuffled queue
     # from which questions are taken, until empty. Then we refill it with questions
     # from the global table.
@@ -43,12 +125,18 @@ class Quiz
     # Autoask defaults to true
     @registry_conf["autoask"] = true unless @registry_conf.has_key?( "autoask" )
 
+    # Autoask delay defaults to 0 (instantly)
+    @registry_conf["autoask_delay"] = 0 unless @registry_conf.has_key?( "autoask_delay" )
+
     @questions = @registry_conf["questions"]
     @question = nil
-    @answer = nil
-    @answer_core = nil
+    @answers = []
+    @canonical_answer = nil
+    # FIXME 2.0 UTF-8
+    @answer_array = []
     @first_try = false
-    @hint = nil
+    # FIXME 2.0 UTF-8
+    @hint = []
     @hintrange = nil
     @hinted = false
 
@@ -64,31 +152,32 @@ end
 # CLASS QuizPlugin
 #######################################################################
 class QuizPlugin < Plugin
+  BotConfig.register BotConfigBooleanValue.new('quiz.dotted_nicks',
+    :default => true,
+    :desc => "When true, nicks in the top X scores will be camouflaged to prevent IRC hilighting")
+
+  BotConfig.register BotConfigArrayValue.new('quiz.sources',
+    :default => ['quiz.rbot'],
+    :desc => "List of files and URLs that will be used to retrieve quiz questions")
+
   def initialize()
     super
 
     @questions = Array.new
     @quizzes = Hash.new
+    @waiting = Hash.new
+    @ask_mutex = Mutex.new
   end
 
   # Function that returns whether a char is a "separator", used for hints
   #
   def is_sep( ch )
-    return case ch
-    when " " then true
-    when "." then true
-    when "," then true
-    when "-" then true
-    when "'" then true
-    when "&" then true
-    when "\"" then true
-    else false
-    end
+    return ch !~ /^\w$/u
   end
 
 
-  # Fetches questions from a file on the server and from the wiki, then merges
-  # and transforms the questions and fills the global question table.
+  # Fetches questions from the data sources, which can be either local files
+  # (in quiz/) or web pages.
   #
   def fetch_data( m )
     # Read the winning messages file 
@@ -97,44 +186,45 @@ class QuizPlugin < Plugin
       IO.foreach("#{@bot.botclass}/quiz/win_messages") { |line| @win_messages << line.chomp }
     else
       warning( "win_messages file not found!" )
+      # Fill the array with a least one message or code accessing it would fail
+      @win_messages << "<who> guessed right! The answer was <answer>"
     end
 
-    # TODO: Make this configurable, and add support for more than one file (there's a size limit in linux too ;) )
-    path = "#{@bot.botclass}/quiz/quiz.rbot"
-    debug "Fetching from #{path}"
+    m.reply "Fetching questions ..."
 
-    m.reply "Fetching questions from local database and wiki.."
+    # TODO Per-channel sources
 
-    # Local data
-    begin
-      datafile = File.new( path, File::RDONLY )
-      localdata = datafile.read
-    rescue
-      m.reply "Failed to read local database file. oioi."
-      localdata = nil
-    end
+    data = ""
+    @bot.config['quiz.sources'].each { |p|
+      if p =~ /^https?:\/\//
+        # Wiki data
+        begin
+          serverdata = @bot.httputil.get_cached( URI.parse( p ) ) # "http://amarok.kde.org/amarokwiki/index.php/Rbot_Quiz"
+          serverdata = serverdata.split( "QUIZ DATA START\n" )[1]
+          serverdata = serverdata.split( "\nQUIZ DATA END" )[0]
+          serverdata = serverdata.gsub( /&nbsp;/, " " ).gsub( /&amp;/, "&" ).gsub( /&quot;/, "\"" )
+          data << "\n\n" << serverdata
+        rescue
+          m.reply "Failed to download questions from #{p}, ignoring sources"
+        end
+      else
+        path = "#{@bot.botclass}/quiz/#{p}"
+        debug "Fetching from #{path}"
 
-    # Wiki data
-    begin
-      serverdata = @bot.httputil.get_cached( URI.parse( "http://amarok.kde.org/amarokwiki/index.php/Rbot_Quiz" ) )
-      serverdata = serverdata.split( "QUIZ DATA START\n" )[1]
-      serverdata = serverdata.split( "\nQUIZ DATA END" )[0]
-      serverdata = serverdata.gsub( /&nbsp;/, " " ).gsub( /&amp;/, "&" ).gsub( /&quot;/, "\"" )
-    rescue
-      m.reply "Failed to download wiki questions. oioi."
-      if localdata == nil
-        m.reply "No questions loaded, aborting."
-        return
+        # Local data
+        begin
+          datafile = File.new( path, File::RDONLY )
+          data << "\n\n" << datafile.read
+        rescue
+          m.reply "Failed to read from local database file #{p}, skipping."
+        end
       end
-    end
+    }
 
-    @questions = []
+    @questions.clear
 
     # Fuse together and remove comments, then split
-    data = "\n\n#{localdata}\n\n#{serverdata}".gsub( /^#.*$/, "" )
-    entries = data.split( "\nQuestion: " )
-    #First entry will be empty.
-    entries.delete_at(0)
+    entries = data.strip.gsub( /^#.*$/, "" ).split( /(?:^|\n+)Question: / )
 
     entries.each do |e|
       p = e.split( "\n" )
@@ -168,19 +258,28 @@ class QuizPlugin < Plugin
       @quizzes[channel] = Quiz.new( channel, @registry )
     end
 
-    return @quizzes[channel]
+    if @quizzes[channel].has_errors
+      return nil
+    else
+      return @quizzes[channel]
+    end
   end
 
 
   def say_score( m, nick )
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
 
     if q.registry.has_key?( nick )
       score = q.registry[nick].score
       jokers = q.registry[nick].jokers
 
       rank = 0
-      q.rank_table.each_index { |rank| break if nick == q.rank_table[rank][0] }
+      q.rank_table.each_index { |rank| break if nick.downcase == q.rank_table[rank][0].downcase }
       rank += 1
 
       m.reply "#{nick}'s score is: #{score}    Rank: #{rank}    Jokers: #{jokers}"
@@ -192,9 +291,10 @@ class QuizPlugin < Plugin
 
   def help( plugin, topic="" )
     if topic == "admin"
-      "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)."
+      "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)."
     else
-      "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"
+      urls = @bot.config['quiz.sources'].select { |p| p =~ /^https?:\/\// }
+      "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(', ')}")
     end
   end
 
@@ -210,7 +310,7 @@ class QuizPlugin < Plugin
       found_player = false
       i = 0
       q.rank_table.each_index do |i|
-        if nick == q.rank_table[i][0]
+        if nick.downcase == q.rank_table[i][0].downcase
           found_player = true
           break
         end
@@ -257,35 +357,46 @@ class QuizPlugin < Plugin
   # Reimplemented from Plugin
   #
   def listen( m )
-    return unless @quizzes.has_key?( m.target.to_s )
-    q = @quizzes[m.target.to_s]
+    return unless m.kind_of?(PrivMessage)
+
+    chan = m.channel
+    return unless @quizzes.has_key?( chan )
+    q = @quizzes[chan]
 
     return if q.question == nil
 
     message = m.message.downcase.strip
 
-    if message == q.answer.downcase or message == q.answer_core.downcase
+    nick = m.sourcenick.to_s 
+
+    # Support multiple alternate answers and cores
+    answer = q.answers.find { |ans| ans.valid?(message) }
+    if answer
+      # List canonical answer which the hint was based on, to avoid confusion
+      # FIXME display this more friendly
+      answer.info = " (hints were for alternate answer #{q.canonical_answer.core})" if answer != q.canonical_answer and q.hinted
+
       points = 1
       if q.first_try
         points += 1
-        reply = "WHOPEEE! #{m.sourcenick.to_s} got it on the first try! That's worth an extra point. Answer was: #{q.answer}"
-      elsif q.rank_table.length >= 1 and m.sourcenick.to_s == q.rank_table[0][0]
-        reply = "THE QUIZ CHAMPION defends his throne! Seems like #{m.sourcenick.to_s} is invicible! Answer was: #{q.answer}"
-      elsif q.rank_table.length >= 2 and m.sourcenick.to_s == q.rank_table[1][0]
-        reply = "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}"
-      elsif    q.rank_table.length >= 3 and m.sourcenick.to_s == q.rank_table[2][0]
-        reply = "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}"
+        reply = "WHOPEEE! #{nick} got it on the first try! That's worth an extra point. Answer was: #{answer}"
+      elsif q.rank_table.length >= 1 and nick.downcase == q.rank_table[0][0].downcase
+        reply = "THE QUIZ CHAMPION defends his throne! Seems like #{nick} is invicible! Answer was: #{answer}"
+      elsif q.rank_table.length >= 2 and nick.downcase == q.rank_table[1][0].downcase
+        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}"
+      elsif    q.rank_table.length >= 3 and nick.downcase == q.rank_table[2][0].downcase
+        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}"
       else
         reply = @win_messages[rand( @win_messages.length )].dup
-        reply.gsub!( "<who>", m.sourcenick )
-        reply.gsub!( "<answer>", q.answer )
+        reply.gsub!( "<who>", nick )
+        reply.gsub!( "<answer>", answer )
       end
 
       m.reply reply
 
       player = nil
-      if q.registry.has_key?( m.sourcenick.to_s )
-        player = q.registry[m.sourcenick.to_s]
+      if q.registry.has_key?(nick)
+        player = q.registry[nick]
       else
         player = PlayerStats.new( 0, 0, 0 )
       end
@@ -295,14 +406,28 @@ class QuizPlugin < Plugin
       # Reward player with a joker every X points
       if player.score % 15 == 0 and player.jokers < Max_Jokers
         player.jokers += 1
-        m.reply "#{m.sourcenick.to_s} gains a new joker. Rejoice :)"
+        m.reply "#{nick} gains a new joker. Rejoice :)"
       end
 
-      q.registry[m.sourcenick.to_s] = player
-      calculate_ranks( m, q, m.sourcenick.to_s )
+      q.registry[nick] = player
+      calculate_ranks( m, q, nick)
 
       q.question = nil
-      cmd_quiz( m, nil ) if q.registry_conf["autoask"]
+      if q.registry_conf["autoask"]
+        delay = q.registry_conf["autoask_delay"]
+        if delay > 0
+          m.reply "#{Bold}#{Color}03Next question in #{Bold}#{delay}#{Bold} seconds"
+          timer = @bot.timer.add_once(delay) {
+            @ask_mutex.synchronize do
+              @waiting.delete(chan)
+            end
+            cmd_quiz( m, nil)
+          }
+          @waiting[chan] = timer
+        else
+          cmd_quiz( m, nil )
+        end
+      end
     else
       # First try is used, and it wasn't the answer.
       q.first_try = false
@@ -314,14 +439,8 @@ class QuizPlugin < Plugin
   # which is annoying for those not watching. Example: markey -> m.a.r.k.e.y
   #
   def unhilight_nick( nick )
-    new_nick = ""
-
-    0.upto( nick.length - 1 ) do |i|
-      new_nick += nick[i, 1]
-      new_nick += "." unless i == nick.length - 1
-    end
-
-    return new_nick
+    return nick unless @bot.config['quiz.dotted_nicks']
+    return nick.split(//).join(".")
   end
 
 
@@ -330,7 +449,20 @@ class QuizPlugin < Plugin
   #######################################################################
   def cmd_quiz( m, params )
     fetch_data( m ) if @questions.empty?
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+
+    @ask_mutex.synchronize do
+      if @waiting.has_key?(chan)
+        m.reply "Next quiz question will be automatically asked soon, have patience"
+        return
+      end
+    end
+
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
 
     if q.question
       m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
@@ -340,56 +472,57 @@ class QuizPlugin < Plugin
 
     # Fill per-channel questions buffer
     if q.questions.empty?
-      temp = @questions.dup
-
-      temp.length.times do
-        i = rand( temp.length )
-        q.questions << temp[i]
-        temp.delete_at( i )
-      end
+      q.questions = @questions.sort_by { rand }
     end
 
-    i = rand( q.questions.length )
-    q.question = q.questions[i].question
-    q.answer     = q.questions[i].answer.gsub( "#", "" )
+    # pick a question and delete it (delete_at returns the deleted item)
+    picked = q.questions.delete_at( rand(q.questions.length) )
 
-    begin
-      q.answer_core = /(#)(.*)(#)/.match( q.questions[i].answer )[2]
-    rescue
-      q.answer_core = nil
-    end
-    q.answer_core = q.answer.dup if q.answer_core == nil
+    q.question = picked.question
+    q.answers = picked.answer.split(/\s+\|\|\s+/).map { |ans| QuizAnswer.new(ans) }
 
-    # Check if core answer is numerical and tell the players so, if that's the case
+    # Check if any core answer is numerical and tell the players so, if that's the case
     # The rather obscure statement is needed because to_i and to_f returns 99(.0) for "99 red balloons", and 0 for "balloon"
-    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
-
-    q.questions.delete_at( i )
+    #
+    # The "canonical answer" is also determined here, defined to be the first found numerical answer, or
+    # the first core.
+    numeric = q.answers.find { |ans| ans.numeric? }
+    if numeric
+        q.question += "#{Color}07 (Numerical answer)#{Color}"
+        q.canonical_answer = numeric
+    else
+        q.canonical_answer = q.answers.first
+    end
 
     q.first_try = true
 
-    q.hint = ""
-    (0..q.answer_core.length-1).each do |index|
-      if is_sep(q.answer_core[index,1])
-        q.hint << q.answer_core[index]
+    # FIXME 2.0 UTF-8
+    q.hint = []
+    q.answer_array.clear
+    q.canonical_answer.core.scan(/./u) { |ch|
+      if is_sep(ch)
+        q.hint << ch
       else
         q.hint << "^"
       end
-    end
+      q.answer_array << ch
+    }
     q.hinted = false
 
     # Generate array of unique random range
-    q.hintrange = (0..q.answer_core.length-1).sort_by{rand}
+    q.hintrange = (0..q.hint.length-1).sort_by{ rand }
 
     m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
   end
 
 
   def cmd_solve( m, params )
-    return unless @quizzes.has_key?( m.target.to_s )
-    q = @quizzes[m.target.to_s]
+    chan = m.channel
 
-    m.reply "The correct answer was: #{q.answer}"
+    return unless @quizzes.has_key?( chan )
+    q = @quizzes[chan]
+
+    m.reply "The correct answer was: #{q.canonical_answer}"
 
     q.question = nil
 
@@ -398,11 +531,14 @@ class QuizPlugin < Plugin
 
 
   def cmd_hint( m, params )
-    return unless @quizzes.has_key?( m.target.to_s )
-    q = @quizzes[m.target.to_s]
+    chan = m.channel
+    nick = m.sourcenick.to_s
+
+    return unless @quizzes.has_key?(chan)
+    q = @quizzes[chan]
 
     if q.question == nil
-      m.reply "#{m.sourcenick.to_s}: Get a question first!"
+      m.reply "#{nick}: Get a question first!"
     else
       num_chars = case q.hintrange.length    # Number of characters to reveal
       when 25..1000 then 7
@@ -414,30 +550,32 @@ class QuizPlugin < Plugin
       when  1..1000 then 1
       end
 
+      # FIXME 2.0 UTF-8
       num_chars.times do
         begin
           index = q.hintrange.pop
           # New hint char until the char isn't a "separator" (space etc.)
-        end while is_sep(q.answer_core[index,1])
-        q.hint[index] = q.answer_core[index]
+        end while is_sep(q.answer_array[index])
+        q.hint[index] = q.answer_array[index]
       end
       m.reply "Hint: #{q.hint}"
       q.hinted = true
 
-      if q.hint == q.answer_core
-        m.reply "#{Bold}#{Color}04BUST!#{Color}#{Bold} This round is over. #{Color}04Minus one point for #{m.sourcenick.to_s}#{Color}."
+      # FIXME 2.0 UTF-8
+      if q.hint == q.answer_array
+        m.reply "#{Bold}#{Color}04BUST!#{Color}#{Bold} This round is over. #{Color}04Minus one point for #{nick}#{Color}."
 
         stats = nil
-        if q.registry.has_key?( m.sourcenick.to_s )
-          stats = q.registry[m.sourcenick.to_s]
+        if q.registry.has_key?( nick )
+          stats = q.registry[nick]
         else
           stats = PlayerStats.new( 0, 0, 0 )
         end
 
         stats["score"] = stats.score - 1
-        q.registry[m.sourcenick.to_s] = stats
+        q.registry[nick] = stats
 
-        calculate_ranks( m, q, m.sourcenick.to_s )
+        calculate_ranks( m, q, nick)
 
         q.question = nil
         cmd_quiz( m, nil ) if q.registry_conf["autoask"]
@@ -447,8 +585,9 @@ class QuizPlugin < Plugin
 
 
   def cmd_skip( m, params )
-    return unless @quizzes.has_key?( m.target.to_s )
-    q = @quizzes[m.target.to_s]
+    chan = m.channel
+    return unless @quizzes.has_key?(chan)
+    q = @quizzes[chan]
 
     q.question = nil
     cmd_quiz( m, params )
@@ -456,33 +595,39 @@ class QuizPlugin < Plugin
 
 
   def cmd_joker( m, params )
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    nick = m.sourcenick.to_s
+    q = create_quiz(chan)
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
 
     if q.question == nil
-      m.reply "#{m.sourcenick.to_s}: There is no open question."
+      m.reply "#{nick}: There is no open question."
       return
     end
 
-    if q.registry[m.sourcenick.to_s].jokers > 0
-      player = q.registry[m.sourcenick.to_s]
+    if q.registry[nick].jokers > 0
+      player = q.registry[nick]
       player.jokers -= 1
       player.score += 1
-      q.registry[m.sourcenick.to_s] = player
+      q.registry[nick] = player
 
-      calculate_ranks( m, q, m.sourcenick.to_s )
+      calculate_ranks( m, q, nick )
 
       if player.jokers != 1
         jokers = "jokers"
       else
         jokers = "joker"
       end
-      m.reply "#{Bold}#{Color}12JOKER!#{Color}#{Bold} #{m.sourcenick.to_s} draws a joker and wins this round. You have #{player.jokers} #{jokers} left."
-      m.reply "The answer was: #{q.answer}."
+      m.reply "#{Bold}#{Color}12JOKER!#{Color}#{Bold} #{nick} draws a joker and wins this round. You have #{player.jokers} #{jokers} left."
+      m.reply "The answer was: #{q.canonical_answer}."
 
       q.question = nil
       cmd_quiz( m, nil ) if q.registry_conf["autoask"]
     else
-      m.reply "#{m.sourcenick.to_s}: You don't have any jokers left ;("
+      m.reply "#{nick}: You don't have any jokers left ;("
     end
   end
 
@@ -492,14 +637,28 @@ class QuizPlugin < Plugin
   end
 
 
+  def cmd_refresh( m, params )
+    q = create_quiz ( m.channel )
+    q.questions.clear
+    fetch_data ( m )
+    cmd_quiz( m, params )
+  end
+
+
   def cmd_top5( m, params )
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
+
     if q.rank_table.empty?
       m.reply "There are no scores known yet!"
       return
     end
 
-    m.reply "* Top 5 Players for #{m.target.to_s}:"
+    m.reply "* Top 5 Players for #{chan}:"
 
     [5, q.rank_table.length].min.times do |i|
       player = q.rank_table[i]
@@ -512,15 +671,21 @@ class QuizPlugin < Plugin
 
   def cmd_top_number( m, params )
     num = params[:number].to_i
-    return unless 1..50 === num
-    q = create_quiz( m.target.to_s )
+    return if num < 1 or num > 50
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
+
     if q.rank_table.empty?
       m.reply "There are no scores known yet!"
       return
     end
 
     ar = []
-    m.reply "* Top #{num} Players for #{m.target.to_s}:"
+    m.reply "* Top #{num} Players for #{chan}:"
     n = [ num, q.rank_table.length ].min
     n.times do |i|
       player = q.rank_table[i]
@@ -541,7 +706,8 @@ class QuizPlugin < Plugin
 
 
   def cmd_score( m, params )
-    say_score( m, m.sourcenick.to_s )
+    nick = m.sourcenick.to_s
+    say_score( m, nick )
   end
 
 
@@ -551,13 +717,19 @@ class QuizPlugin < Plugin
 
 
   def cmd_autoask( m, params )
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
 
-    if params[:enable].downcase == "on"
+    case params[:enable].downcase
+    when "on", "true"
       q.registry_conf["autoask"] = true
       m.reply "Enabled autoask mode."
       cmd_quiz( m, nil ) if q.question == nil
-    elsif params[:enable].downcase == "off"
+    when "off", "false"
       q.registry_conf["autoask"] = false
       m.reply "Disabled autoask mode."
     else
@@ -565,9 +737,27 @@ class QuizPlugin < Plugin
     end
   end
 
+  def cmd_autoask_delay( m, params )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
+
+    delay = params[:time].to_i
+    q.registry_conf["autoask_delay"] = delay
+    m.reply "Autoask delay now #{q.registry_conf['autoask_delay']} seconds"
+  end
+
 
   def cmd_transfer( m, params )
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
 
     debug q.rank_table.inspect
 
@@ -601,6 +791,11 @@ class QuizPlugin < Plugin
         destplayer = PlayerStats.new(0,0,0)
       end
 
+      if sourceplayer == destplayer
+        m.reply "Source and destination are the same, I'm not going to touch them"
+        return
+      end
+
       sourceplayer.score -= transscore
       destplayer.score += transscore
       sourceplayer.jokers -= transjokers
@@ -620,7 +815,13 @@ class QuizPlugin < Plugin
 
 
   def cmd_del_player( m, params )
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
+
     debug q.rank_table.inspect
 
     nick = params[:nick]
@@ -640,7 +841,7 @@ class QuizPlugin < Plugin
 
       player_rank = nil
       q.rank_table.each_index { |rank|
-        if nick == q.rank_table[rank][0]
+        if nick.downcase == q.rank_table[rank][0].downcase
           player_rank = rank
           break
         end
@@ -655,7 +856,12 @@ class QuizPlugin < Plugin
 
 
   def cmd_set_score(m, params)
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
     debug q.rank_table.inspect
 
     nick = params[:nick]
@@ -673,7 +879,13 @@ class QuizPlugin < Plugin
 
 
   def cmd_set_jokers(m, params)
-    q = create_quiz( m.target.to_s )
+    chan = m.channel
+    q = create_quiz( chan )
+    if q.nil?
+      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+      return
+    end
+    debug q.rank_table.inspect
 
     nick = params[:nick]
     val = [params[:jokers].to_i, Max_Jokers].min
@@ -702,12 +914,14 @@ plugin.map 'quiz joker',            :action => 'cmd_joker'
 plugin.map 'quiz score',            :action => 'cmd_score'
 plugin.map 'quiz score :player',    :action => 'cmd_score_player'
 plugin.map 'quiz fetch',            :action => 'cmd_fetch'
+plugin.map 'quiz refresh',          :action => 'cmd_refresh'
 plugin.map 'quiz top5',             :action => 'cmd_top5'
 plugin.map 'quiz top :number',      :action => 'cmd_top_number'
 plugin.map 'quiz stats',            :action => 'cmd_stats'
 
 # Admin commands
 plugin.map 'quiz autoask :enable',  :action => 'cmd_autoask', :auth_path => 'edit'
+plugin.map 'quiz autoask delay :time',  :action => 'cmd_autoask_delay', :auth_path => 'edit', :requirements => {:time => /\d+/}
 plugin.map 'quiz transfer :source :dest :score :jokers', :action => 'cmd_transfer', :auth_path => 'edit', :defaults => {:score => '-1', :jokers => '-1'}
 plugin.map 'quiz deleteplayer :nick', :action => 'cmd_del_player', :auth_path => 'edit'
 plugin.map 'quiz setscore :nick :score', :action => 'cmd_set_score', :auth_path => 'edit'