]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/games/quiz.rb
ensures the path reported by gems does exists
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / games / quiz.rb
index e1e544f4c9f1b2b244cef94ad7ba21d03db6e877..b139c66a2fad25eb2ec8c9c0e31900ce2f0307a3 100644 (file)
@@ -175,6 +175,22 @@ class QuizPlugin < Plugin
     @ask_mutex = Mutex.new
   end
 
     @ask_mutex = Mutex.new
   end
 
+  def cleanup
+    @ask_mutex.synchronize do
+      # purge all waiting timers
+      @waiting.each do |chan, t|
+        @bot.timer.remove t.first
+        @bot.say chan, _("stopped quiz timer")
+      end
+      @waiting.clear
+    end
+    chans = @quizzes.keys
+    @quizzes.clear
+    chans.each do |chan|
+      @bot.say chan, _("quiz stopped")
+    end
+  end
+
   # Function that returns whether a char is a "separator", used for hints
   #
   def is_sep( ch )
   # Function that returns whether a char is a "separator", used for hints
   #
   def is_sep( ch )
@@ -258,13 +274,17 @@ class QuizPlugin < Plugin
 
 
   # Returns new Quiz instance for channel, or existing one
 
 
   # Returns new Quiz instance for channel, or existing one
+  # Announce errors if a message is passed as second parameter
   #
   #
-  def create_quiz( channel )
+  def create_quiz(channel, m=nil)
     unless @quizzes.has_key?( channel )
       @quizzes[channel] = Quiz.new( channel, @registry )
     end
 
     if @quizzes[channel].has_errors
     unless @quizzes.has_key?( channel )
       @quizzes[channel] = Quiz.new( channel, @registry )
     end
 
     if @quizzes[channel].has_errors
+      m.reply _("Sorry, the quiz database for %{chan} seems to be corrupt") % {
+        :chan => channel
+      } if m
       return nil
     else
       return @quizzes[channel]
       return nil
     else
       return @quizzes[channel]
@@ -274,11 +294,8 @@ class QuizPlugin < Plugin
 
   def say_score( m, nick )
     chan = m.channel
 
   def say_score( m, nick )
     chan = m.channel
-    q = create_quiz( chan )
-    if q.nil?
-      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
-      return
-    end
+    q = create_quiz( chan, m )
+    return unless q
 
     if q.registry.has_key?( nick )
       score = q.registry[nick].score
 
     if q.registry.has_key?( nick )
       score = q.registry[nick].score
@@ -299,7 +316,17 @@ class QuizPlugin < Plugin
 
   def help( plugin, topic="" )
     if topic == "admin"
 
   def help( plugin, topic="" )
     if topic == "admin"
-      "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). 'quiz cleanup' => remove players with no points and no jokers."
+      _("Quiz game aministration commands (requires authentication): ") + [
+        _("'quiz autoask <on/off>' => enable/disable autoask mode"),
+        _("'quiz autoask delay <time>' => delay next quiz by <time> when in autoask mode"),
+        _("'quiz autoskip <on/off>' => enable/disable autoskip mode (autoskip implies autoask)"),
+        _("'quiz autoskip delay <time>' => wait <time> before skipping to next quiz when in autoskip 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 cleanup' => remove players with no points and no jokers")
+      ].join(". ")
     else
       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(', ')}")
     else
       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(', ')}")
@@ -358,6 +385,23 @@ class QuizPlugin < Plugin
   end
 
 
   end
 
 
+  def setup_ask_timer(m, q)
+    chan = m.channel
+    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, :ask]
+    else
+      cmd_quiz( m, nil )
+    end
+  end
+
   # Reimplemented from Plugin
   #
   def message(m)
   # Reimplemented from Plugin
   #
   def message(m)
@@ -374,6 +418,15 @@ class QuizPlugin < Plugin
     # Support multiple alternate answers and cores
     answer = q.answers.find { |ans| ans.valid?(message) }
     if answer
     # Support multiple alternate answers and cores
     answer = q.answers.find { |ans| ans.valid?(message) }
     if answer
+
+      # purge the autoskip timer
+      @ask_mutex.synchronize do
+        if @waiting.key? chan and @waiting[chan].last == :skip
+          @bot.timer.remove(@waiting[chan].first)
+          @waiting.delete(chan)
+        end
+      end
+
       # 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
       # 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
@@ -415,20 +468,9 @@ class QuizPlugin < Plugin
       calculate_ranks( m, q, nick)
 
       q.question = nil
       calculate_ranks( m, q, nick)
 
       q.question = nil
-      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
+
+      if q.registry_conf['autoskip'] or q.registry_conf["autoask"]
+        setup_ask_timer(m, q)
       end
     else
       # First try is used, and it wasn't the answer.
       end
     else
       # First try is used, and it wasn't the answer.
@@ -454,17 +496,14 @@ class QuizPlugin < Plugin
     chan = m.channel
 
     @ask_mutex.synchronize do
     chan = m.channel
 
     @ask_mutex.synchronize do
-      if @waiting.has_key?(chan)
+      if @waiting.has_key?(chan) and @waiting[chan].last == :ask
         m.reply "Next quiz question will be automatically asked soon, have patience"
         return
       end
     end
 
         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
+    q = create_quiz( chan, m )
+    return unless q
 
     if q.question
       m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
 
     if q.question
       m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
@@ -524,12 +563,34 @@ class QuizPlugin < Plugin
     q.hintrange = (0..q.hint.length-1).sort_by{ rand }
 
     m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
     q.hintrange = (0..q.hint.length-1).sort_by{ rand }
 
     m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
+
+    if q.registry_conf.key? 'autoskip'
+      delay = q.registry_conf['autoskip_delay']
+      timer = @bot.timer.add_once(delay) do
+        m.reply _("Nobody managed to answer in %{time}! Skipping to the next question ...") % {
+          :time => Utils.secs_to_string(delay)
+        }
+        q.question = nil
+        @ask_mutex.synchronize do
+          @waiting.delete(chan)
+        end
+        setup_ask_timer(m, q)
+      end
+      @waiting[chan] = [timer, :skip]
+    end
   end
 
 
   def cmd_solve( m, params )
     chan = m.channel
 
   end
 
 
   def cmd_solve( m, params )
     chan = m.channel
 
+    @ask_mutex.synchronize do
+      if @waiting.has_key?(chan) and @waiting[chan].last == :skip
+        m.reply _("you can't make me solve a quiz in autoskip mode, sorry")
+        return
+      end
+    end
+
     return unless @quizzes.has_key?( chan )
     q = @quizzes[chan]
 
     return unless @quizzes.has_key?( chan )
     q = @quizzes[chan]
 
@@ -537,7 +598,7 @@ class QuizPlugin < Plugin
 
     q.question = nil
 
 
     q.question = nil
 
-    cmd_quiz( m, nil ) if q.registry_conf["autoask"]
+    cmd_quiz( m, nil ) if q.registry_conf["autoask"] or q.registry_conf["autoskip"]
   end
 
 
   end
 
 
@@ -597,6 +658,14 @@ class QuizPlugin < Plugin
 
   def cmd_skip( m, params )
     chan = m.channel
 
   def cmd_skip( m, params )
     chan = m.channel
+
+    @ask_mutex.synchronize do
+      if @waiting.has_key?(chan) and @waiting[chan].last == :skip
+        m.reply _("I'll skip to the next question as soon as the timeout expires, not now")
+        return
+      end
+    end
+
     return unless @quizzes.has_key?(chan)
     q = @quizzes[chan]
 
     return unless @quizzes.has_key?(chan)
     q = @quizzes[chan]
 
@@ -608,11 +677,8 @@ class QuizPlugin < Plugin
   def cmd_joker( m, params )
     chan = m.channel
     nick = m.sourcenick.to_s
   def cmd_joker( m, params )
     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
+    q = create_quiz(chan, m)
+    return unless q
 
     if q.question == nil
       m.reply "#{nick}: There is no open question."
 
     if q.question == nil
       m.reply "#{nick}: There is no open question."
@@ -658,11 +724,8 @@ class QuizPlugin < Plugin
 
   def cmd_top5( m, params )
     chan = m.channel
 
   def cmd_top5( 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
+    q = create_quiz( chan, m )
+    return unless q
 
     if q.rank_table.empty?
       m.reply "There are no scores known yet!"
 
     if q.rank_table.empty?
       m.reply "There are no scores known yet!"
@@ -684,11 +747,8 @@ class QuizPlugin < Plugin
     num = params[:number].to_i
     return if num < 1 or num > 50
     chan = m.channel
     num = params[:number].to_i
     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
+    q = create_quiz( chan, m )
+    return unless q
 
     if q.rank_table.empty?
       m.reply "There are no scores known yet!"
 
     if q.rank_table.empty?
       m.reply "There are no scores known yet!"
@@ -729,27 +789,31 @@ class QuizPlugin < Plugin
 
   def cmd_autoask( m, params )
     chan = m.channel
 
   def cmd_autoask( 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
+    q = create_quiz( chan, m )
+    return unless q
 
     params[:enable] ||= 'status'
 
 
     params[:enable] ||= 'status'
 
+    reg = q.registry_conf
+
     case params[:enable].downcase
     when "on", "true"
     case params[:enable].downcase
     when "on", "true"
-      q.registry_conf["autoask"] = true
+      reg["autoask"] = true
       m.reply "Enabled autoask mode."
       m.reply "Enabled autoask mode."
+      reg["autoask_delay"] = 0 unless reg.has_key("autoask_delay")
       cmd_quiz( m, nil ) if q.question == nil
     when "off", "false"
       cmd_quiz( m, nil ) if q.question == nil
     when "off", "false"
-      q.registry_conf["autoask"] = false
+      reg["autoask"] = false
       m.reply "Disabled autoask mode."
     when "status"
       m.reply "Disabled autoask mode."
     when "status"
-      m.reply _("Autoask is %{status}, the delay is %{time}") % {
-        :status => q.registry_conf["autoask"],
-        :time => Utils.secs_to_string(q.registry_conf["autoask_delay"]),
-      }
+      if reg.has_key? "autoask"
+        m.reply _("autoask is %{status}, the delay is %{time}") % {
+          :status => reg["autoask"],
+          :time => Utils.secs_to_string(reg["autoask_delay"]),
+        }
+      else
+        m.reply _("autoask is not configured here")
+      end
     else
       m.reply "Invalid autoask parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
     end
     else
       m.reply "Invalid autoask parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
     end
@@ -757,25 +821,104 @@ class QuizPlugin < Plugin
 
   def cmd_autoask_delay( m, params )
     chan = m.channel
 
   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"
+    q = create_quiz( chan, m )
+    return unless q
+
+    time = params[:time].to_s
+    if time =~ /^-?\d+$/
+      delay = time.to_i
+    else
+      begin
+        delay = Utils.parse_time_offset(time)
+      rescue RuntimeError
+        m.reply _("I couldn't understand that delay expression, sorry")
+        return
+      end
+    end
+
+    if delay < 0
+      m.reply _("wait, you want me to ask the next question %{abs} BEFORE the previous one gets solved?") % {
+        :abs => Utils.secs_to_string(-delay)
+      }
       return
     end
 
       return
     end
 
-    delay = params[:time].to_i
     q.registry_conf["autoask_delay"] = delay
     q.registry_conf["autoask_delay"] = delay
-    m.reply "Autoask delay now #{q.registry_conf['autoask_delay']} seconds"
+    m.reply "autoask delay now #{q.registry_conf['autoask_delay']} seconds"
   end
 
   end
 
-  def cmd_transfer( m, params )
+
+  def cmd_autoskip( m, params )
+    chan = m.channel
+    q = create_quiz( chan, m )
+    return unless q
+
+    params[:enable] ||= 'status'
+
+    reg = q.registry_conf
+
+    case params[:enable].downcase
+    when "on", "true"
+      reg["autoskip"] = true
+      m.reply "Enabled autoskip mode."
+      # default: 1 minute (TODO customize with a global config key)
+      reg["autoskip_delay"] = 60 unless reg.has_key("autoskip_delay")
+      # also set a default autoask delay
+      reg["autoask_delay"] = 0 unless reg.has_key("autoask_delay")
+    when "off", "false"
+      reg["autoskip"] = false
+      m.reply "Disabled autoskip mode."
+    when "status"
+      if reg.has_key? "autoskip"
+        m.reply _("autoskip is %{status}, the delay is %{time}") % {
+          :status => reg["autoskip"],
+          :time => Utils.secs_to_string(reg["autoskip_delay"]),
+        }
+      else
+        m.reply _("autoskip is not configured here")
+      end
+    else
+      m.reply "Invalid autoskip parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
+    end
+  end
+
+  def cmd_autoskip_delay( m, params )
     chan = m.channel
     chan = m.channel
-    q = create_quiz( chan )
-    if q.nil?
-      m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
+    q = create_quiz( chan, m )
+    return unless q
+
+    time = params[:time].to_s
+    if time =~ /^-?\d+$/
+      delay = time.to_i
+    else
+      begin
+        delay = Utils.parse_time_offset(time)
+      rescue RuntimeError
+        m.reply _("I couldn't understand that delay expression, sorry")
+        return
+      end
+    end
+
+    if delay < 0
+      m.reply _("wait, you want me to skip to the next question %{abs} BEFORE the previous one?") % {
+        :abs => Utils.secs_to_string(-delay)
+      }
+      return
+    elsif delay == 0
+      m.reply _("sure, I'll ask all the questions at the same time! </sarcasm>")
       return
     end
 
       return
     end
 
+    q.registry_conf["autoskip_delay"] = delay
+    m.reply "autoskip delay now #{q.registry_conf['autoskip_delay']} seconds"
+  end
+
+
+  def cmd_transfer( m, params )
+    chan = m.channel
+    q = create_quiz( chan, m )
+    return unless q
+
     debug q.rank_table.inspect
 
     source = params[:source]
     debug q.rank_table.inspect
 
     source = params[:source]
@@ -833,11 +976,8 @@ class QuizPlugin < Plugin
 
   def cmd_del_player( m, params )
     chan = m.channel
 
   def cmd_del_player( 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
+    q = create_quiz( chan, m )
+    return unless q
 
     debug q.rank_table.inspect
 
 
     debug q.rank_table.inspect
 
@@ -874,11 +1014,9 @@ class QuizPlugin < Plugin
 
   def cmd_set_score(m, params)
     chan = m.channel
 
   def cmd_set_score(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
+    q = create_quiz( chan, m )
+    return unless q
+
     debug q.rank_table.inspect
 
     nick = params[:nick]
     debug q.rank_table.inspect
 
     nick = params[:nick]
@@ -897,11 +1035,9 @@ class QuizPlugin < Plugin
 
   def cmd_set_jokers(m, params)
     chan = m.channel
 
   def cmd_set_jokers(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
+    q = create_quiz( chan, m )
+    return unless q
+
     debug q.rank_table.inspect
 
     nick = params[:nick]
     debug q.rank_table.inspect
 
     nick = params[:nick]
@@ -919,11 +1055,8 @@ class QuizPlugin < Plugin
 
   def cmd_cleanup(m, params)
     chan = m.channel
 
   def cmd_cleanup(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
+    q = create_quiz( chan, m )
+    return unless q
 
     null_players = []
     q.registry.each { |nick, player|
 
     null_players = []
     q.registry.each { |nick, player|
@@ -944,7 +1077,7 @@ class QuizPlugin < Plugin
     if @quizzes.delete m.channel
       @ask_mutex.synchronize do
         t = @waiting.delete(m.channel)
     if @quizzes.delete m.channel
       @ask_mutex.synchronize do
         t = @waiting.delete(m.channel)
-        @bot.timer.remove t if t
+        @bot.timer.remove t.first if t
       end
       m.okay
     else
       end
       m.okay
     else
@@ -974,7 +1107,9 @@ plugin.map 'quiz stop', :action => :stop
 
 # Admin commands
 plugin.map 'quiz autoask [:enable]',  :action => 'cmd_autoask', :auth_path => 'edit'
 
 # 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 autoask delay *time',  :action => 'cmd_autoask_delay', :auth_path => 'edit'
+plugin.map 'quiz autoskip [:enable]',  :action => 'cmd_autoskip', :auth_path => 'edit'
+plugin.map 'quiz autoskip delay *time',  :action => 'cmd_autoskip_delay', :auth_path => 'edit'
 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'
 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'