define_structure :PlayerStats, :score, :jokers, :jokers_time
# Why do we still need jokers_time? //Firetech
-# Maximum number of jokers a player can gain
-Max_Jokers = 3
-
# Control codes
Color = "\003"
Bold = "\002"
:default => ['quiz.rbot'],
:desc => "List of files and URLs that will be used to retrieve quiz questions")
+
+ Config.register Config::IntegerValue.new('quiz.max_jokers',
+ :default => 3,
+ :desc => "Maximum number of jokers a player can gain")
+
def initialize()
super
@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 )
# Local data
begin
- file = File.new( path, File::RDONLY )
- data << "\n\n" << file.read
+ data << "\n\n" << File.read(path)
rescue
m.reply "Failed to read from local database file #{p}, skipping."
end
# 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
+ m.reply _("Sorry, the quiz database for %{chan} seems to be corrupt") % {
+ :chan => channel
+ } if m
return nil
else
return @quizzes[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
jokers = q.registry[nick].jokers
rank = 0
- q.rank_table.each_index { |rank| break if nick.downcase == q.rank_table[rank][0].downcase }
- rank += 1
+ q.rank_table.each do |place|
+ rank += 1
+ break if nick.downcase == place[0].downcase
+ end
m.reply "#{nick}'s score is: #{score} Rank: #{rank} Jokers: #{jokers}"
else
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(', ')}")
stats = q.registry[nick]
# Find player in table
- found_player = false
- i = 0
- q.rank_table.each_index do |i|
- if nick.downcase == q.rank_table[i][0].downcase
- found_player = true
+ old_rank = nil
+ q.rank_table.each_with_index do |place, i|
+ if nick.downcase == place[0].downcase
+ old_rank = i
break
end
end
# Remove player from old position
- if found_player
- old_rank = i
- q.rank_table.delete_at( i )
- else
- old_rank = nil
+ if old_rank
+ q.rank_table.delete_at( old_rank )
end
# Insert player at new position
- inserted = false
- q.rank_table.each_index do |i|
- if stats.score > q.rank_table[i][1].score
+ new_rank = nil
+ q.rank_table.each_with_index do |place, i|
+ if stats.score > place[1].score
q.rank_table[i,0] = [[nick, stats]]
- inserted = true
+ new_rank = i
break
end
end
# If less than all other players' scores, append to table
- unless inserted
- i += 1 unless q.rank_table.empty?
+ unless new_rank
+ new_rank = q.rank_table.length
q.rank_table << [nick, stats]
end
# Print congratulations/condolences if the player's rank has changed
- unless old_rank.nil?
- if i < old_rank
- m.reply "#{nick} ascends to rank #{i + 1}. Congratulations :)"
- elsif i > old_rank
- m.reply "#{nick} slides down to rank #{i + 1}. So Sorry! NOT. :p"
+ if old_rank
+ if new_rank < old_rank
+ m.reply "#{nick} ascends to rank #{new_rank + 1}. Congratulations :)"
+ elsif new_rank > old_rank
+ m.reply "#{nick} slides down to rank #{new_rank + 1}. So Sorry! NOT. :p"
end
end
else
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)
# 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
player.score = player.score + points
# Reward player with a joker every X points
- if player.score % 15 == 0 and player.jokers < Max_Jokers
+ if player.score % 15 == 0 and player.jokers < @bot.config['quiz.max_jokers']
player.jokers += 1
m.reply "#{nick} gains a new joker. Rejoice :)"
end
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.
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
- 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}"
- m.reply "Hint: #{q.hint}" if q.hinted
+ m.reply "Hint: #{q.hint.join}" if q.hinted
return
end
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
+ @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]
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 while is_sep(q.answer_array[index]) and not q.all_seps
q.hint[index] = q.answer_array[index]
end
- m.reply "Hint: #{q.hint}"
+ m.reply "Hint: #{q.hint.join}"
q.hinted = true
# FIXME 2.0 UTF-8
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]
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."
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!"
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!"
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'
+
+ reg = q.registry_conf
case params[:enable].downcase
when "on", "true"
- q.registry_conf["autoask"] = true
+ reg["autoask"] = true
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"
- q.registry_conf["autoask"] = false
+ reg["autoask"] = false
m.reply "Disabled autoask mode."
+ when "status"
+ 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'."
+ m.reply "Invalid autoask parameter. Use 'on' or 'off' to set it, 'status' to check the current status."
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"
+ 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
- delay = params[:time].to_i
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
- def cmd_transfer( m, params )
+ def cmd_autoskip( 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
+
+ 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
+ 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
+ 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]
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
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]
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]
- val = [params[:jokers].to_i, Max_Jokers].min
+ val = [params[:jokers].to_i, @bot.config['quiz.max_jokers']].min
if q.registry.has_key?(nick)
player = q.registry[nick]
player.jokers = val
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|
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
plugin.map 'quiz stop', :action => :stop
# 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 [:enable]', :action => 'cmd_autoask', :auth_path => 'edit'
+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'