X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=data%2Frbot%2Fplugins%2Fquiz.rb;h=7e2c0f83b1c201251e3093798de6fa1a4d900699;hb=7b7f1309e8c3dbc3bb4408d56489ae5fba77d57a;hp=d84295a417d58aaa7d68ef3c0063c850d15e4a0d;hpb=ed4d4a6901683105e75b3704dfbbf494e1def84b;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/data/rbot/plugins/quiz.rb b/data/rbot/plugins/quiz.rb index d84295a4..7e2c0f83 100644 --- a/data/rbot/plugins/quiz.rb +++ b/data/rbot/plugins/quiz.rb @@ -1,20 +1,30 @@ -# Plugin for the Ruby IRC bot (http://linuxbrit.co.uk/rbot/) +#-- vim:sw=2:et +#++ +# +# :title: Quiz plugin for rbot +# +# Author:: Mark Kretschmann +# Author:: Jocke Andersson +# Author:: Giuseppe Bilotta +# Author:: Yaohan Chen +# +# Copyright:: (C) 2006 Mark Kretschmann, Jocke Andersson, Giuseppe Bilotta +# Copyright:: (C) 2007 Giuseppe Bilotta, Yaohan Chen +# +# License:: 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. # -# (c) 2006 Mark Kretschmann -# (c) 2006 Jocke Andersson -# (c) 2006 Giuseppe Bilotta -# 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 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 ) @@ -31,12 +41,57 @@ 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, + 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 ) @@ -75,10 +130,13 @@ class Quiz @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 @@ -114,16 +172,7 @@ class QuizPlugin < Plugin # Function that returns whether a char is a "separator", used for hints # def is_sep( ch ) - 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 @@ -175,9 +224,7 @@ class QuizPlugin < Plugin @questions.clear # Fuse together and remove comments, then split - entries = data.gsub( /^#.*$/, "" ).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" ) @@ -247,7 +294,7 @@ class QuizPlugin < Plugin "Quiz game aministration commands (requires authentication): 'quiz autoask ' => enable/disable autoask mode. 'quiz autoask delay ' => delay next quiz by seconds when in autoask mode. 'quiz transfer [score] [jokers]' => transfer [score] points and [jokers] jokers from to (default is entire score and all jokers). 'quiz setscore ' => set 's score to . 'quiz setjokers ' => set 's number of jokers to . 'quiz deleteplayer ' => delete one player from the rank table (only works when score and jokers are set to 0)." 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 ' => show top players (max 50). 'quiz stats' => show some statistics. 'quiz fetch' => refetch questions from databases." + (urls.empty? ? "" : "\nYou can add new questions at #{urls.join(', ')}") + "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 ' => show top 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 @@ -322,21 +369,27 @@ class QuizPlugin < Plugin nick = m.sourcenick.to_s - if message == q.answer.downcase or message == q.answer_core.downcase + # 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! #{nick} got it on the first try! That's worth an extra point. 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: #{q.answer}" + 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: #{q.answer}" + 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: #{q.answer}" + 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!( "", nick ) - reply.gsub!( "", q.answer ) + reply.gsub!( "", answer ) end m.reply reply @@ -419,46 +472,45 @@ 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 @@ -470,7 +522,7 @@ class QuizPlugin < Plugin 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 @@ -498,17 +550,19 @@ 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 + # 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 @@ -568,7 +622,7 @@ class QuizPlugin < Plugin jokers = "joker" end 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.answer}." + m.reply "The answer was: #{q.canonical_answer}." q.question = nil cmd_quiz( m, nil ) if q.registry_conf["autoask"] @@ -583,6 +637,14 @@ 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 ) chan = m.channel q = create_quiz( chan ) @@ -852,6 +914,7 @@ 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'