+#-- vim:sw=2:et
+#++
+#
# Plugin for the Ruby IRC bot (http://linuxbrit.co.uk/rbot/)
#
# A trivia quiz game. Fast paced, featureful and fun.
#
-# (c) 2006 Mark Kretschmann <markey@web.de>
-# (c) 2006 Jocke Andersson <ajocke@gmail.com>
-# (c) 2006 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
+# 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>
+#
+# (c) 2006 Mark Kretschmann, Jocke Andersson, Giuseppe Bilotta
+# (c) 2007 Giuseppe Bilotta, Yaohan Chen
+#
# Licensed under GPL V2.
+# 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 )
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.
# 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
# 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 )
- # TODO: Make this configurable, and add support for more than one file (there's a size limit in linux too ;) )
# Read the winning messages file
@win_messages = Array.new
if File.exists? "#{@bot.botclass}/quiz/win_messages"
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
- 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( / /, " " ).gsub( /&/, "&" ).gsub( /"/, "\"" )
+ 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( 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( / /, " " ).gsub( /&/, "&" ).gsub( /"/, "\"" )
- 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 = "#{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" )
@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}"
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
- # Updates the per-channel rank table, which is kept for performance reasons
+ # Updates the per-channel rank table, which is kept for performance reasons.
+ # This table contains all players sorted by rank.
#
def calculate_ranks( m, q, nick )
if q.registry.has_key?( nick )
stats = q.registry[nick]
# Find player in table
+ found_player = false
i = 0
q.rank_table.each_index do |i|
- break if nick == q.rank_table[i][0]
+ if nick.downcase == q.rank_table[i][0].downcase
+ found_player = true
+ break
+ end
end
- old_rank = i
- q.rank_table.delete_at( i )
+ # Remove player from old position
+ if found_player
+ old_rank = i
+ q.rank_table.delete_at( i )
+ else
+ old_rank = nil
+ end
# Insert player at new position
inserted = false
q.rank_table.each_index do |i|
- if stats.score >= q.rank_table[i][1].score
+ if stats.score > q.rank_table[i][1].score
q.rank_table[i,0] = [[nick, stats]]
inserted = true
break
end
end
- # If less than all other players' scores, append at the end
+ # If less than all other players' scores, append to table
unless inserted
+ i += 1 unless q.rank_table.empty?
q.rank_table << [nick, stats]
- i += 1
end
- 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"
+ # 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"
+ end
end
else
q.rank_table << [[nick, PlayerStats.new( 1 )]]
end
- debug q.rank_table.inspect
end
# 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
# 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
# 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
#######################################################################
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}"
# 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
+
+ return unless @quizzes.has_key?( chan )
+ q = @quizzes[chan]
- m.reply "The correct answer was: #{q.answer}"
+ m.reply "The correct answer was: #{q.canonical_answer}"
q.question = nil
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
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.hintrange.length == 0
+ 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
+ stats["score"] = stats.score - 1
+ 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"]
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 )
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
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
- debug q.rank_table.inspect
+ 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]
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
- debug q.rank_table.inspect
+ 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]
score = player[1].score
ar << "#{i + 1}. #{unhilight_nick( nick )} (#{score})"
end
- str = ar.join(" | ")
-
- if str.empty?
- m.reply "Noone in #{m.target.to_s} has a score!"
- else
- m.reply str
- end
+ m.reply ar.join(" | ")
end
def cmd_score( m, params )
- say_score( m, m.sourcenick.to_s )
+ nick = m.sourcenick.to_s
+ say_score( m, nick )
end
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
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
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
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]
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
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]
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
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'