1 # Plugin for the Ruby IRC bot (http://linuxbrit.co.uk/rbot/)
3 # A trivia quiz game. Fast paced, featureful and fun.
5 # (c) 2006 Mark Kretschmann <markey@web.de>
6 # (c) 2006 Jocke Andersson <ajocke@gmail.com>
7 # (c) 2006 Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
8 # Licensed under GPL V2.
11 # Class for storing question/answer pairs
12 QuizBundle = Struct.new( "QuizBundle", :question, :answer )
14 # Class for storing player stats
15 PlayerStats = Struct.new( "PlayerStats", :score, :jokers, :jokers_time )
16 # Why do we still need jokers_time? //Firetech
18 # Maximum number of jokers a player can gain
26 #######################################################################
28 # One Quiz instance per channel, contains channel specific data
29 #######################################################################
31 attr_accessor :registry, :registry_conf, :questions, :question, :answer, :answer_core,
32 :first_try, :hint, :hintrange, :rank_table, :hinted
34 def initialize( channel, registry )
35 @registry = registry.sub_registry( channel )
36 @registry_conf = @registry.sub_registry( "config" )
38 # Per-channel copy of the global questions table. Acts like a shuffled queue
39 # from which questions are taken, until empty. Then we refill it with questions
40 # from the global table.
41 @registry_conf["questions"] = [] unless @registry_conf.has_key?( "questions" )
43 # Autoask defaults to true
44 @registry_conf["autoask"] = true unless @registry_conf.has_key?( "autoask" )
46 @questions = @registry_conf["questions"]
55 # We keep this array of player stats for performance reasons. It's sorted by score
56 # and always synced with the registry player stats hash. This way we can do fast
57 # rank lookups, without extra sorting.
58 @rank_table = @registry.to_a.sort { |a,b| b[1].score<=>a[1].score }
60 # # Convert old PlayerStats to new. Can be removed later on
61 # @registry.each_key do |player|
63 # j = @registry[player].joker
65 # @registry[player] = PlayerStats.new( @registry[player].score, 0, 0 )
72 #######################################################################
74 #######################################################################
75 class QuizPlugin < Plugin
79 @questions = Array.new
83 # Function that returns whether a char is a "separator", used for hints
99 # Fetches questions from a file on the server and from the wiki, then merges
100 # and transforms the questions and fills the global question table.
103 # TODO: Make this configurable, and add support for more than one file (there's a size limit in linux too ;) )
104 path = "#{@bot.botclass}/quiz/quiz.rbot"
105 debug "Fetching from #{path}"
107 m.reply "Fetching questions from local database and wiki.."
111 datafile = File.new( path, File::RDONLY )
112 localdata = datafile.read
114 m.reply "Failed to read local database file. oioi."
120 serverdata = @bot.httputil.get( URI.parse( "http://amarok.kde.org/amarokwiki/index.php/Rbot_Quiz" ) )
121 serverdata = serverdata.split( "QUIZ DATA START\n" )[1]
122 serverdata = serverdata.split( "\nQUIZ DATA END" )[0]
123 serverdata = serverdata.gsub( / /, " " ).gsub( /&/, "&" ).gsub( /"/, "\"" )
125 m.reply "Failed to download wiki questions. oioi."
127 m.reply "No questions loaded, aborting."
134 # Fuse together and remove comments, then split
135 data = "#{localdata}\n\n#{serverdata}".gsub( /^#.*$/, "" )
136 entries = data.split( "\nQuestion: " )
137 #First entry will be empty.
142 # We'll need at least two lines of data
144 # Check if question isn't empty
146 while p[1].match( /^Answer: (.*)$/ ) == nil and p.size > 2
147 # Delete all lines between the question and the answer
150 p[1] = p[1].gsub( /Answer: /, "" ).strip
151 # If the answer was found
153 # Add the data to the array
154 b = QuizBundle.new( p[0], p[1] )
161 m.reply "done, #{@questions.length} questions loaded."
165 # Returns new Quiz instance for channel, or existing one
167 def create_quiz( channel )
168 unless @quizzes.has_key?( channel )
169 @quizzes[channel] = Quiz.new( channel, @registry )
172 return @quizzes[channel]
176 def say_score( m, nick )
177 q = create_quiz( m.target.to_s )
179 if q.registry.has_key?( nick )
180 score = q.registry[nick].score
181 jokers = q.registry[nick].jokers
184 q.rank_table.each_index { |rank| break if nick == q.rank_table[rank][0] }
187 m.reply "#{nick}'s score is: #{score} Rank: #{rank} Jokers: #{jokers}"
189 m.reply "#{nick} does not have a score yet. Lamer."
194 def help( plugin, topic="" )
196 "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)."
198 "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"
203 # Updates the per-channel rank table, which is kept for performance reasons
205 def calculate_ranks( m, q, nick )
206 if q.registry.has_key?( nick )
207 stats = q.registry[nick]
209 # Find player in table
211 q.rank_table.each_index do |i|
212 break if nick == q.rank_table[i][0]
216 q.rank_table.delete_at( i )
218 # Insert player at new position
220 q.rank_table.each_index do |i|
221 if stats.score >= q.rank_table[i][1].score
222 q.rank_table[i,0] = [[nick, stats]]
228 # If less than all other players' scores, append at the end
230 q.rank_table << [nick, stats]
235 m.reply "#{nick} ascends to rank #{i + 1}. Congratulations :)"
237 m.reply "#{nick} slides down to rank #{i + 1}. So Sorry! NOT. :p"
240 q.rank_table << [[nick, PlayerStats.new( 1 )]]
242 debug q.rank_table.inspect
246 # Reimplemented from Plugin
249 return unless @quizzes.has_key?( m.target.to_s )
250 q = @quizzes[m.target.to_s]
252 return if q.question == nil
254 message = m.message.downcase.strip
256 if message == q.answer.downcase or message == q.answer_core.downcase
262 replies << "WHOPEEE! #{m.sourcenick.to_s} got it on the first try! That's worth an extra point. Answer was: #{q.answer}"
263 elsif q.rank_table.length >= 1 and m.sourcenick.to_s == q.rank_table[0][0]
264 replies << "THE QUIZ CHAMPION defends his throne! Seems like #{m.sourcenick.to_s} is invicible! Answer was: #{q.answer}"
265 elsif q.rank_table.length >= 2 and m.sourcenick.to_s == q.rank_table[1][0]
266 replies << "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}"
267 elsif q.rank_table.length >= 3 and m.sourcenick.to_s == q.rank_table[2][0]
268 replies << "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}"
270 replies << "BINGO!! #{m.sourcenick.to_s} got it right. The answer was: #{q.answer}"
271 replies << "OMG!! PONIES!! #{m.sourcenick.to_s} is the cutest. The answer was: #{q.answer}"
272 replies << "HUZZAAAH! #{m.sourcenick.to_s} did it again. The answer was: #{q.answer}"
273 replies << "YEEEHA! Cowboy #{m.sourcenick.to_s} scored again. The answer was: #{q.answer}"
274 replies << "STRIKE! #{m.sourcenick.to_s} pwned you all. The answer was: #{q.answer}"
275 replies << "YAY :)) #{m.sourcenick.to_s} is totally invited to my next sleepover. The answer was: #{q.answer}"
276 replies << "And the crowd GOES WILD for #{m.sourcenick.to_s}. The answer was: #{q.answer}"
277 replies << "GOOOAAALLLL! That was one fine strike by #{m.sourcenick.to_s}. The answer was: #{q.answer}"
278 replies << "HOO-RAY, #{m.sourcenick.to_s} deserves a medal! Only #{m.sourcenick.to_s} could have known the answer: #{q.answer}"
279 replies << "OKAY, #{m.sourcenick.to_s} is officially a spermatologist! Answer was: #{q.answer}"
280 replies << "WOOO, I bet that #{m.sourcenick.to_s} knows where the word 'trivia' comes from too! Answer was: #{q.answer}"
283 m.reply replies[rand( replies.length )]
286 if q.registry.has_key?( m.sourcenick.to_s )
287 player = q.registry[m.sourcenick.to_s]
289 player = PlayerStats.new( 0, 0, 0 )
292 player.score = player.score + points
294 # Reward player with a joker every X points
295 if player.score % 15 == 0 and player.jokers < Max_Jokers
297 m.reply "#{m.sourcenick.to_s} gains a new joker. Rejoice :)"
300 q.registry[m.sourcenick.to_s] = player
301 calculate_ranks( m, q, m.sourcenick.to_s )
304 cmd_quiz( m, nil ) if q.registry_conf["autoask"]
306 # First try is used, and it wasn't the answer.
312 # Stretches an IRC nick with dots, simply to make the client not trigger a hilight,
313 # which is annoying for those not watching. Example: markey -> m.a.r.k.e.y
315 def unhilight_nick( nick )
318 0.upto( nick.length - 1 ) do |i|
319 new_nick += nick[i, 1]
320 new_nick += "." unless i == nick.length - 1
327 #######################################################################
329 #######################################################################
330 def cmd_quiz( m, params )
331 # if m.target.to_s == "#amarok"
332 # m.reply "Please join #amarok.gaming for quizzing! :)"
336 fetch_data( m ) if @questions.empty?
338 q = create_quiz( m.target.to_s )
341 m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
342 m.reply "Hint: #{q.hint}" if q.hinted
346 # Fill per-channel questions buffer
347 if q.questions.empty?
348 temp = @questions.dup
351 i = rand( temp.length )
352 q.questions << temp[i]
357 i = rand( q.questions.length )
358 q.question = q.questions[i].question
359 q.answer = q.questions[i].answer.gsub( "#", "" )
362 q.answer_core = /(#)(.*)(#)/.match( q.questions[i].answer )[2]
366 q.answer_core = q.answer.dup if q.answer_core == nil
368 # Check if core answer is numerical and tell the players so, if that's the case
369 # The rather obscure statement is needed because to_i and to_f returns 99(.0) for "99 red balloons", and 0 for "balloon"
370 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
372 q.questions.delete_at( i )
377 (0..q.answer_core.length-1).each do |index|
378 if is_sep(q.answer_core[index,1])
379 q.hint << q.answer_core[index]
386 # Generate array of unique random range
387 q.hintrange = (0..q.answer_core.length-1).sort_by{rand}
389 m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
393 def cmd_solve( m, params )
394 return unless @quizzes.has_key?( m.target.to_s )
395 q = @quizzes[m.target.to_s]
397 m.reply "The correct answer was: #{q.answer}"
401 cmd_quiz( m, nil ) if q.registry_conf["autoask"]
405 def cmd_hint( m, params )
406 return unless @quizzes.has_key?( m.target.to_s )
407 q = @quizzes[m.target.to_s]
410 m.reply "#{m.sourcenick.to_s}: Get a question first!"
412 num_chars = case q.hintrange.length # Number of characters to reveal
424 index = q.hintrange.pop
425 # New hint char until the char isn't a "separator" (space etc.)
426 end while is_sep(q.answer_core[index,1])
427 q.hint[index] = q.answer_core[index]
429 m.reply "Hint: #{q.hint}"
432 if q.hint == q.answer_core
433 m.reply "#{Bold}#{Color}04BUST!#{Color}#{Bold} This round is over. #{Color}04Minus one point for #{m.sourcenick.to_s}#{Color}."
436 if q.registry.has_key?( m.sourcenick.to_s )
437 stats = q.registry[m.sourcenick.to_s]
439 stats = PlayerStats.new( 0, 0, 0 )
442 stats["score"] = stats.score - 1
443 q.registry[m.sourcenick.to_s] = stats
445 calculate_ranks( m, q, m.sourcenick.to_s )
448 cmd_quiz( m, nil ) if q.registry_conf["autoask"]
454 def cmd_skip( m, params )
455 return unless @quizzes.has_key?( m.target.to_s )
456 q = @quizzes[m.target.to_s]
459 cmd_quiz( m, params )
463 def cmd_joker( m, params )
464 q = create_quiz( m.target.to_s )
467 m.reply "#{m.sourcenick.to_s}: There is no open question."
471 if q.registry[m.sourcenick.to_s].jokers > 0
472 player = q.registry[m.sourcenick.to_s]
475 q.registry[m.sourcenick.to_s] = player
477 calculate_ranks( m, q, m.sourcenick.to_s )
479 if player.jokers != 1
484 m.reply "#{Bold}#{Color}12JOKER!#{Color}#{Bold} #{m.sourcenick.to_s} draws a joker and wins this round. You have #{player.jokers} #{jokers} left."
485 m.reply "The answer was: #{q.answer}."
488 cmd_quiz( m, nil ) if q.registry_conf["autoask"]
490 m.reply "#{m.sourcenick.to_s}: You don't have any jokers left ;("
495 def cmd_fetch( m, params )
500 def cmd_top5( m, params )
501 q = create_quiz( m.target.to_s )
503 debug q.rank_table.inspect
505 m.reply "* Top 5 Players for #{m.target.to_s}:"
507 [5, q.rank_table.length].min.times do |i|
508 player = q.rank_table[i]
510 score = player[1].score
511 m.reply " #{i + 1}. #{unhilight_nick( nick )} (#{score})"
516 def cmd_top_number( m, params )
517 num = params[:number].to_i
518 return unless 1..50 === num
519 q = create_quiz( m.target.to_s )
521 debug q.rank_table.inspect
524 m.reply "* Top #{num} Players for #{m.target.to_s}:"
525 n = [ num, q.rank_table.length ].min
527 player = q.rank_table[i]
529 score = player[1].score
530 ar << "#{i + 1}. #{unhilight_nick( nick )} (#{score})"
535 m.reply "Noone in #{m.target.to_s} has a score!"
542 def cmd_stats( m, params )
543 fetch_data( m ) if @questions.empty?
545 m.reply "* Total Number of Questions:"
546 m.reply " #{@questions.length}"
550 def cmd_score( m, params )
551 say_score( m, m.sourcenick.to_s )
555 def cmd_score_player( m, params )
556 say_score( m, params[:player] )
560 def cmd_autoask( m, params )
561 q = create_quiz( m.target.to_s )
563 if params[:enable].downcase == "on"
564 q.registry_conf["autoask"] = true
565 m.reply "Enabled autoask mode."
566 cmd_quiz( m, nil ) if q.question == nil
567 elsif params[:enable].downcase == "off"
568 q.registry_conf["autoask"] = false
569 m.reply "Disabled autoask mode."
571 m.reply "Invalid autoask parameter. Use 'on' or 'off'."
576 def cmd_transfer( m, params )
577 q = create_quiz( m.target.to_s )
579 debug q.rank_table.inspect
581 source = params[:source]
583 transscore = params[:score].to_i
584 transjokers = params[:jokers].to_i
585 debug "Transferring #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
587 if q.registry.has_key?(source)
588 sourceplayer = q.registry[source]
589 score = sourceplayer.score
593 if score < transscore
594 m.reply "#{source} only has #{score} points!"
597 jokers = sourceplayer.jokers
601 if jokers < transjokers
602 m.reply "#{source} only has #{jokers} jokers!!"
605 if q.registry.has_key?(dest)
606 destplayer = q.registry[dest]
608 destplayer = PlayerStats.new(0,0,0)
611 sourceplayer.score -= transscore
612 destplayer.score += transscore
613 sourceplayer.jokers -= transjokers
614 destplayer.jokers += transjokers
616 q.registry[source] = sourceplayer
617 calculate_ranks(m, q, source)
619 q.registry[dest] = destplayer
620 calculate_ranks(m, q, dest)
622 m.reply "Transferred #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
624 m.reply "#{source} doesn't have any points!"
629 def cmd_del_player( m, params )
630 q = create_quiz( m.target.to_s )
631 debug q.rank_table.inspect
634 if q.registry.has_key?(nick)
635 player = q.registry[nick]
638 m.reply "Can't delete player #{nick} with score #{score}."
641 jokers = player.jokers
643 m.reply "Can't delete player #{nick} with #{jokers} jokers."
646 q.registry.delete(nick)
649 q.rank_table.each_index { |rank|
650 if nick == q.rank_table[rank][0]
655 q.rank_table.delete_at(player_rank)
657 m.reply "Player #{nick} deleted."
659 m.reply "Player #{nick} isn't even in the database."
664 def cmd_set_score(m, params)
665 q = create_quiz( m.target.to_s )
666 debug q.rank_table.inspect
669 val = params[:score].to_i
670 if q.registry.has_key?(nick)
671 player = q.registry[nick]
674 player = PlayerStats.new( val, 0, 0)
676 q.registry[nick] = player
677 calculate_ranks(m, q, nick)
678 m.reply "Score for player #{nick} set to #{val}."
682 def cmd_set_jokers(m, params)
683 q = create_quiz( m.target.to_s )
686 val = [params[:jokers].to_i, Max_Jokers].min
687 if q.registry.has_key?(nick)
688 player = q.registry[nick]
691 player = PlayerStats.new( 0, val, 0)
693 q.registry[nick] = player
694 m.reply "Jokers for player #{nick} set to #{val}."
700 plugin = QuizPlugin.new
701 plugin.default_auth( 'edit', false )
704 plugin.map 'quiz', :action => 'cmd_quiz'
705 plugin.map 'quiz solve', :action => 'cmd_solve'
706 plugin.map 'quiz hint', :action => 'cmd_hint'
707 plugin.map 'quiz skip', :action => 'cmd_skip'
708 plugin.map 'quiz joker', :action => 'cmd_joker'
709 plugin.map 'quiz score', :action => 'cmd_score'
710 plugin.map 'quiz score :player', :action => 'cmd_score_player'
711 plugin.map 'quiz fetch', :action => 'cmd_fetch'
712 plugin.map 'quiz top5', :action => 'cmd_top5'
713 plugin.map 'quiz top :number', :action => 'cmd_top_number'
714 plugin.map 'quiz stats', :action => 'cmd_stats'
717 plugin.map 'quiz autoask :enable', :action => 'cmd_autoask', :auth_path => 'edit'
718 plugin.map 'quiz transfer :source :dest :score :jokers', :action => 'cmd_transfer', :auth_path => 'edit', :defaults => {:score => '-1', :jokers => '-1'}
719 plugin.map 'quiz deleteplayer :nick', :action => 'cmd_del_player', :auth_path => 'edit'
720 plugin.map 'quiz setscore :nick :score', :action => 'cmd_set_score', :auth_path => 'edit'
721 plugin.map 'quiz setjokers :nick :jokers', :action => 'cmd_set_jokers', :auth_path => 'edit'