]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/quiz.rb
quiz plugin: sometimes cmd_hint was not busting even when it should have
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / quiz.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # Plugin for the Ruby IRC bot (http://linuxbrit.co.uk/rbot/)
5 #
6 # A trivia quiz game. Fast paced, featureful and fun.
7 #
8 # Author:: Mark Kretschmann <markey@web.de>
9 # Author:: Jocke Andersson <ajocke@gmail.com>
10 # Author:: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
11 # Author:: Yaohan Chen <yaohan.chen@gmail.com>
12 #
13 # (c) 2006 Mark Kretschmann, Jocke Andersson, Giuseppe Bilotta
14 # (c) 2007 Giuseppe Bilotta, Yaohan Chen
15 #
16 # Licensed under GPL V2.
17
18 # FIXME interesting fact: in the Quiz class, @registry.has_key? seems to be
19 # case insensitive. Although this is all right for us, this leads to rank vs
20 # registry mismatches. So we have to make the @rank_table comparisons case
21 # insensitive as well. For the moment, redefine everything to downcase before
22 # matching the nick.
23 #
24 # TODO define a class for the rank table. We might also need it for scoring in
25 # other games.
26 #
27 # TODO when Ruby 2.0 gets out, fix the FIXME 2.0 UTF-8 workarounds
28
29 # Class for storing question/answer pairs
30 QuizBundle = Struct.new( "QuizBundle", :question, :answer )
31
32 # Class for storing player stats
33 PlayerStats = Struct.new( "PlayerStats", :score, :jokers, :jokers_time )
34 # Why do we still need jokers_time? //Firetech
35
36 # Maximum number of jokers a player can gain
37 Max_Jokers = 3
38
39 # Control codes
40 Color = "\003"
41 Bold = "\002"
42
43
44 #######################################################################
45 # CLASS QuizAnswer
46 # Abstract an answer to a quiz question, by providing self as a string
47 # and a core that can be answered as an alternative. It also provides
48 # a boolean that tells if the core is numeric or not
49 #######################################################################
50 class QuizAnswer
51   attr_writer :info
52
53   def initialize(str)
54     @string = str.strip
55     @core = nil
56     if @string =~ /#(.+)#/
57       @core = $1
58       @string.gsub!('#', '')
59     end
60     raise ArgumentError, "empty string can't be a valid answer!" if @string.empty?
61     raise ArgumentError, "empty core can't be a valid answer!" if @core and @core.empty?
62
63     @numeric = (core.to_i.to_s == core) || (core.to_f.to_s == core)
64     @info = nil
65   end
66
67   def core
68     @core || @string
69   end
70
71   def numeric?
72     @numeric
73   end
74
75   def valid?(str)
76     str.downcase == core.downcase || str.downcase == @string.downcase
77   end
78
79   def to_str
80     [@string, @info].join
81   end
82   alias :to_s :to_str
83
84
85 end
86
87
88 #######################################################################
89 # CLASS Quiz
90 # One Quiz instance per channel, contains channel specific data
91 #######################################################################
92 class Quiz
93   attr_accessor :registry, :registry_conf, :questions,
94     :question, :answers, :canonical_answer, :answer_array,
95     :first_try, :hint, :hintrange, :rank_table, :hinted, :has_errors
96
97   def initialize( channel, registry )
98     if !channel
99       @registry = registry.sub_registry( 'private' )
100     else
101       @registry = registry.sub_registry( channel.downcase )
102     end
103     @has_errors = false
104     @registry.each_key { |k|
105       unless @registry.has_key?(k)
106         @has_errors = true
107         error "Data for #{k} is NOT ACCESSIBLE! Database corrupt?"
108       end
109     }
110     if @has_errors
111       debug @registry.to_a.map { |a| a.join(", ")}.join("\n")
112     end
113
114     @registry_conf = @registry.sub_registry( "config" )
115
116     # Per-channel list of sources. If empty, the default one (quiz/quiz.rbot)
117     # will be used. TODO
118     @registry_conf["sources"] = [] unless @registry_conf.has_key?( "sources" )
119
120     # Per-channel copy of the global questions table. Acts like a shuffled queue
121     # from which questions are taken, until empty. Then we refill it with questions
122     # from the global table.
123     @registry_conf["questions"] = [] unless @registry_conf.has_key?( "questions" )
124
125     # Autoask defaults to true
126     @registry_conf["autoask"] = true unless @registry_conf.has_key?( "autoask" )
127
128     # Autoask delay defaults to 0 (instantly)
129     @registry_conf["autoask_delay"] = 0 unless @registry_conf.has_key?( "autoask_delay" )
130
131     @questions = @registry_conf["questions"]
132     @question = nil
133     @answers = []
134     @canonical_answer = nil
135     # FIXME 2.0 UTF-8
136     @answer_array = []
137     @first_try = false
138     # FIXME 2.0 UTF-8
139     @hint = []
140     @hintrange = nil
141     @hinted = false
142
143     # We keep this array of player stats for performance reasons. It's sorted by score
144     # and always synced with the registry player stats hash. This way we can do fast
145     # rank lookups, without extra sorting.
146     @rank_table = @registry.to_a.sort { |a,b| b[1].score<=>a[1].score }
147   end
148 end
149
150
151 #######################################################################
152 # CLASS QuizPlugin
153 #######################################################################
154 class QuizPlugin < Plugin
155   BotConfig.register BotConfigBooleanValue.new('quiz.dotted_nicks',
156     :default => true,
157     :desc => "When true, nicks in the top X scores will be camouflaged to prevent IRC hilighting")
158
159   BotConfig.register BotConfigArrayValue.new('quiz.sources',
160     :default => ['quiz.rbot'],
161     :desc => "List of files and URLs that will be used to retrieve quiz questions")
162
163   def initialize()
164     super
165
166     @questions = Array.new
167     @quizzes = Hash.new
168     @waiting = Hash.new
169     @ask_mutex = Mutex.new
170   end
171
172   # Function that returns whether a char is a "separator", used for hints
173   #
174   def is_sep( ch )
175     return ch !~ /^\w$/u
176   end
177
178
179   # Fetches questions from the data sources, which can be either local files
180   # (in quiz/) or web pages.
181   #
182   def fetch_data( m )
183     # Read the winning messages file 
184     @win_messages = Array.new
185     if File.exists? "#{@bot.botclass}/quiz/win_messages"
186       IO.foreach("#{@bot.botclass}/quiz/win_messages") { |line| @win_messages << line.chomp }
187     else
188       warning( "win_messages file not found!" )
189       # Fill the array with a least one message or code accessing it would fail
190       @win_messages << "<who> guessed right! The answer was <answer>"
191     end
192
193     m.reply "Fetching questions ..."
194
195     # TODO Per-channel sources
196
197     data = ""
198     @bot.config['quiz.sources'].each { |p|
199       if p =~ /^https?:\/\//
200         # Wiki data
201         begin
202           serverdata = @bot.httputil.get_cached( URI.parse( p ) ) # "http://amarok.kde.org/amarokwiki/index.php/Rbot_Quiz"
203           serverdata = serverdata.split( "QUIZ DATA START\n" )[1]
204           serverdata = serverdata.split( "\nQUIZ DATA END" )[0]
205           serverdata = serverdata.gsub( /&nbsp;/, " " ).gsub( /&amp;/, "&" ).gsub( /&quot;/, "\"" )
206           data << "\n\n" << serverdata
207         rescue
208           m.reply "Failed to download questions from #{p}, ignoring sources"
209         end
210       else
211         path = "#{@bot.botclass}/quiz/#{p}"
212         debug "Fetching from #{path}"
213
214         # Local data
215         begin
216           datafile = File.new( path, File::RDONLY )
217           data << "\n\n" << datafile.read
218         rescue
219           m.reply "Failed to read from local database file #{p}, skipping."
220         end
221       end
222     }
223
224     @questions.clear
225
226     # Fuse together and remove comments, then split
227     entries = data.strip.gsub( /^#.*$/, "" ).split( /(?:^|\n+)Question: / )
228
229     entries.each do |e|
230       p = e.split( "\n" )
231       # We'll need at least two lines of data
232       unless p.size < 2
233         # Check if question isn't empty
234         if p[0].length > 0
235           while p[1].match( /^Answer: (.*)$/ ) == nil and p.size > 2
236             # Delete all lines between the question and the answer
237             p.delete_at(1)
238           end
239           p[1] = p[1].gsub( /Answer: /, "" ).strip
240           # If the answer was found
241           if p[1].length > 0
242             # Add the data to the array
243             b = QuizBundle.new( p[0], p[1] )
244             @questions << b
245           end
246         end
247       end
248     end
249
250     m.reply "done, #{@questions.length} questions loaded."
251   end
252
253
254   # Returns new Quiz instance for channel, or existing one
255   #
256   def create_quiz( channel )
257     unless @quizzes.has_key?( channel )
258       @quizzes[channel] = Quiz.new( channel, @registry )
259     end
260
261     if @quizzes[channel].has_errors
262       return nil
263     else
264       return @quizzes[channel]
265     end
266   end
267
268
269   def say_score( m, nick )
270     chan = m.channel
271     q = create_quiz( chan )
272     if q.nil?
273       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
274       return
275     end
276
277     if q.registry.has_key?( nick )
278       score = q.registry[nick].score
279       jokers = q.registry[nick].jokers
280
281       rank = 0
282       q.rank_table.each_index { |rank| break if nick.downcase == q.rank_table[rank][0].downcase }
283       rank += 1
284
285       m.reply "#{nick}'s score is: #{score}    Rank: #{rank}    Jokers: #{jokers}"
286     else
287       m.reply "#{nick} does not have a score yet. Lamer."
288     end
289   end
290
291
292   def help( plugin, topic="" )
293     if topic == "admin"
294       "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)."
295     else
296       urls = @bot.config['quiz.sources'].select { |p| p =~ /^https?:\/\// }
297       "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(', ')}")
298     end
299   end
300
301
302   # Updates the per-channel rank table, which is kept for performance reasons.
303   # This table contains all players sorted by rank.
304   #
305   def calculate_ranks( m, q, nick )
306     if q.registry.has_key?( nick )
307       stats = q.registry[nick]
308
309       # Find player in table
310       found_player = false
311       i = 0
312       q.rank_table.each_index do |i|
313         if nick.downcase == q.rank_table[i][0].downcase
314           found_player = true
315           break
316         end
317       end
318
319       # Remove player from old position
320       if found_player
321         old_rank = i
322         q.rank_table.delete_at( i )
323       else
324         old_rank = nil
325       end
326
327       # Insert player at new position
328       inserted = false
329       q.rank_table.each_index do |i|
330         if stats.score > q.rank_table[i][1].score
331           q.rank_table[i,0] = [[nick, stats]]
332           inserted = true
333           break
334         end
335       end
336
337       # If less than all other players' scores, append to table 
338       unless inserted
339         i += 1 unless q.rank_table.empty?
340         q.rank_table << [nick, stats]
341       end
342
343       # Print congratulations/condolences if the player's rank has changed
344       unless old_rank.nil?
345         if i < old_rank
346           m.reply "#{nick} ascends to rank #{i + 1}. Congratulations :)"
347         elsif i > old_rank
348           m.reply "#{nick} slides down to rank #{i + 1}. So Sorry! NOT. :p"
349         end
350       end
351     else
352       q.rank_table << [[nick, PlayerStats.new( 1 )]]
353     end
354   end
355
356
357   # Reimplemented from Plugin
358   #
359   def listen( m )
360     return unless m.kind_of?(PrivMessage)
361
362     chan = m.channel
363     return unless @quizzes.has_key?( chan )
364     q = @quizzes[chan]
365
366     return if q.question == nil
367
368     message = m.message.downcase.strip
369
370     nick = m.sourcenick.to_s 
371
372     # Support multiple alternate answers and cores
373     answer = q.answers.find { |ans| ans.valid?(message) }
374     if answer
375       # List canonical answer which the hint was based on, to avoid confusion
376       # FIXME display this more friendly
377       answer.info = " (hints were for alternate answer #{q.canonical_answer.core})" if answer != q.canonical_answer and q.hinted
378
379       points = 1
380       if q.first_try
381         points += 1
382         reply = "WHOPEEE! #{nick} got it on the first try! That's worth an extra point. Answer was: #{answer}"
383       elsif q.rank_table.length >= 1 and nick.downcase == q.rank_table[0][0].downcase
384         reply = "THE QUIZ CHAMPION defends his throne! Seems like #{nick} is invicible! Answer was: #{answer}"
385       elsif q.rank_table.length >= 2 and nick.downcase == q.rank_table[1][0].downcase
386         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}"
387       elsif    q.rank_table.length >= 3 and nick.downcase == q.rank_table[2][0].downcase
388         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}"
389       else
390         reply = @win_messages[rand( @win_messages.length )].dup
391         reply.gsub!( "<who>", nick )
392         reply.gsub!( "<answer>", answer )
393       end
394
395       m.reply reply
396
397       player = nil
398       if q.registry.has_key?(nick)
399         player = q.registry[nick]
400       else
401         player = PlayerStats.new( 0, 0, 0 )
402       end
403
404       player.score = player.score + points
405
406       # Reward player with a joker every X points
407       if player.score % 15 == 0 and player.jokers < Max_Jokers
408         player.jokers += 1
409         m.reply "#{nick} gains a new joker. Rejoice :)"
410       end
411
412       q.registry[nick] = player
413       calculate_ranks( m, q, nick)
414
415       q.question = nil
416       if q.registry_conf["autoask"]
417         delay = q.registry_conf["autoask_delay"]
418         if delay > 0
419           m.reply "#{Bold}#{Color}03Next question in #{Bold}#{delay}#{Bold} seconds"
420           timer = @bot.timer.add_once(delay) {
421             @ask_mutex.synchronize do
422               @waiting.delete(chan)
423             end
424             cmd_quiz( m, nil)
425           }
426           @waiting[chan] = timer
427         else
428           cmd_quiz( m, nil )
429         end
430       end
431     else
432       # First try is used, and it wasn't the answer.
433       q.first_try = false
434     end
435   end
436
437
438   # Stretches an IRC nick with dots, simply to make the client not trigger a hilight,
439   # which is annoying for those not watching. Example: markey -> m.a.r.k.e.y
440   #
441   def unhilight_nick( nick )
442     return nick unless @bot.config['quiz.dotted_nicks']
443     return nick.split(//).join(".")
444   end
445
446
447   #######################################################################
448   # Command handling
449   #######################################################################
450   def cmd_quiz( m, params )
451     fetch_data( m ) if @questions.empty?
452     chan = m.channel
453
454     @ask_mutex.synchronize do
455       if @waiting.has_key?(chan)
456         m.reply "Next quiz question will be automatically asked soon, have patience"
457         return
458       end
459     end
460
461     q = create_quiz( chan )
462     if q.nil?
463       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
464       return
465     end
466
467     if q.question
468       m.reply "#{Bold}#{Color}03Current question: #{Color}#{Bold}#{q.question}"
469       m.reply "Hint: #{q.hint}" if q.hinted
470       return
471     end
472
473     # Fill per-channel questions buffer
474     if q.questions.empty?
475       q.questions = @questions.sort_by { rand }
476     end
477
478     # pick a question and delete it (delete_at returns the deleted item)
479     picked = q.questions.delete_at( rand(q.questions.length) )
480
481     q.question = picked.question
482     q.answers = picked.answer.split(/\s+\|\|\s+/).map { |ans| QuizAnswer.new(ans) }
483
484     # Check if any core answer is numerical and tell the players so, if that's the case
485     # The rather obscure statement is needed because to_i and to_f returns 99(.0) for "99 red balloons", and 0 for "balloon"
486     #
487     # The "canonical answer" is also determined here, defined to be the first found numerical answer, or
488     # the first core.
489     numeric = q.answers.find { |ans| ans.numeric? }
490     if numeric
491         q.question += "#{Color}07 (Numerical answer)#{Color}"
492         q.canonical_answer = numeric
493     else
494         q.canonical_answer = q.answers.first
495     end
496
497     q.first_try = true
498
499     # FIXME 2.0 UTF-8
500     q.hint = []
501     q.answer_array.clear
502     q.canonical_answer.core.scan(/./u) { |ch|
503       if is_sep(ch)
504         q.hint << ch
505       else
506         q.hint << "^"
507       end
508       q.answer_array << ch
509     }
510     q.hinted = false
511
512     # Generate array of unique random range
513     q.hintrange = (0..q.hint.length-1).sort_by{ rand }
514
515     m.reply "#{Bold}#{Color}03Question: #{Color}#{Bold}" + q.question
516   end
517
518
519   def cmd_solve( m, params )
520     chan = m.channel
521
522     return unless @quizzes.has_key?( chan )
523     q = @quizzes[chan]
524
525     m.reply "The correct answer was: #{q.canonical_answer}"
526
527     q.question = nil
528
529     cmd_quiz( m, nil ) if q.registry_conf["autoask"]
530   end
531
532
533   def cmd_hint( m, params )
534     chan = m.channel
535     nick = m.sourcenick.to_s
536
537     return unless @quizzes.has_key?(chan)
538     q = @quizzes[chan]
539
540     if q.question == nil
541       m.reply "#{nick}: Get a question first!"
542     else
543       num_chars = case q.hintrange.length    # Number of characters to reveal
544       when 25..1000 then 7
545       when 20..1000 then 6
546       when 16..1000 then 5
547       when 12..1000 then 4
548       when  8..1000 then 3
549       when  5..1000 then 2
550       when  1..1000 then 1
551       end
552
553       debug "Hintrange before: #{q.hintrange.inspect}"
554
555       # FIXME 2.0 UTF-8
556       num_chars.times do
557         begin
558           index = q.hintrange.pop
559           # New hint char until the char isn't a "separator" (space etc.)
560         end while is_sep(q.answer_array[index])
561         q.hint[index] = q.answer_array[index]
562       end
563       m.reply "Hint: #{q.hint}"
564       q.hinted = true
565
566       debug "Hintrange before: #{q.hintrange.inspect}"
567
568       # FIXME 2.0 UTF-8
569       if q.hint == q.answer_array
570         m.reply "#{Bold}#{Color}04BUST!#{Color}#{Bold} This round is over. #{Color}04Minus one point for #{nick}#{Color}."
571
572         stats = nil
573         if q.registry.has_key?( nick )
574           stats = q.registry[nick]
575         else
576           stats = PlayerStats.new( 0, 0, 0 )
577         end
578
579         stats["score"] = stats.score - 1
580         q.registry[nick] = stats
581
582         calculate_ranks( m, q, nick)
583
584         q.question = nil
585         cmd_quiz( m, nil ) if q.registry_conf["autoask"]
586       end
587     end
588   end
589
590
591   def cmd_skip( m, params )
592     chan = m.channel
593     return unless @quizzes.has_key?(chan)
594     q = @quizzes[chan]
595
596     q.question = nil
597     cmd_quiz( m, params )
598   end
599
600
601   def cmd_joker( m, params )
602     chan = m.channel
603     nick = m.sourcenick.to_s
604     q = create_quiz(chan)
605     if q.nil?
606       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
607       return
608     end
609
610     if q.question == nil
611       m.reply "#{nick}: There is no open question."
612       return
613     end
614
615     if q.registry[nick].jokers > 0
616       player = q.registry[nick]
617       player.jokers -= 1
618       player.score += 1
619       q.registry[nick] = player
620
621       calculate_ranks( m, q, nick )
622
623       if player.jokers != 1
624         jokers = "jokers"
625       else
626         jokers = "joker"
627       end
628       m.reply "#{Bold}#{Color}12JOKER!#{Color}#{Bold} #{nick} draws a joker and wins this round. You have #{player.jokers} #{jokers} left."
629       m.reply "The answer was: #{q.canonical_answer}."
630
631       q.question = nil
632       cmd_quiz( m, nil ) if q.registry_conf["autoask"]
633     else
634       m.reply "#{nick}: You don't have any jokers left ;("
635     end
636   end
637
638
639   def cmd_fetch( m, params )
640     fetch_data( m )
641   end
642
643
644   def cmd_refresh( m, params )
645     q = create_quiz ( m.channel )
646     q.questions.clear
647     fetch_data ( m )
648     cmd_quiz( m, params )
649   end
650
651
652   def cmd_top5( m, params )
653     chan = m.channel
654     q = create_quiz( chan )
655     if q.nil?
656       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
657       return
658     end
659
660     if q.rank_table.empty?
661       m.reply "There are no scores known yet!"
662       return
663     end
664
665     m.reply "* Top 5 Players for #{chan}:"
666
667     [5, q.rank_table.length].min.times do |i|
668       player = q.rank_table[i]
669       nick = player[0]
670       score = player[1].score
671       m.reply "    #{i + 1}. #{unhilight_nick( nick )} (#{score})"
672     end
673   end
674
675
676   def cmd_top_number( m, params )
677     num = params[:number].to_i
678     return if num < 1 or num > 50
679     chan = m.channel
680     q = create_quiz( chan )
681     if q.nil?
682       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
683       return
684     end
685
686     if q.rank_table.empty?
687       m.reply "There are no scores known yet!"
688       return
689     end
690
691     ar = []
692     m.reply "* Top #{num} Players for #{chan}:"
693     n = [ num, q.rank_table.length ].min
694     n.times do |i|
695       player = q.rank_table[i]
696       nick = player[0]
697       score = player[1].score
698       ar << "#{i + 1}. #{unhilight_nick( nick )} (#{score})"
699     end
700     m.reply ar.join(" | ")
701   end
702
703
704   def cmd_stats( m, params )
705     fetch_data( m ) if @questions.empty?
706
707     m.reply "* Total Number of Questions:"
708     m.reply "    #{@questions.length}"
709   end
710
711
712   def cmd_score( m, params )
713     nick = m.sourcenick.to_s
714     say_score( m, nick )
715   end
716
717
718   def cmd_score_player( m, params )
719     say_score( m, params[:player] )
720   end
721
722
723   def cmd_autoask( m, params )
724     chan = m.channel
725     q = create_quiz( chan )
726     if q.nil?
727       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
728       return
729     end
730
731     case params[:enable].downcase
732     when "on", "true"
733       q.registry_conf["autoask"] = true
734       m.reply "Enabled autoask mode."
735       cmd_quiz( m, nil ) if q.question == nil
736     when "off", "false"
737       q.registry_conf["autoask"] = false
738       m.reply "Disabled autoask mode."
739     else
740       m.reply "Invalid autoask parameter. Use 'on' or 'off'."
741     end
742   end
743
744   def cmd_autoask_delay( m, params )
745     chan = m.channel
746     q = create_quiz( chan )
747     if q.nil?
748       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
749       return
750     end
751
752     delay = params[:time].to_i
753     q.registry_conf["autoask_delay"] = delay
754     m.reply "Autoask delay now #{q.registry_conf['autoask_delay']} seconds"
755   end
756
757
758   def cmd_transfer( m, params )
759     chan = m.channel
760     q = create_quiz( chan )
761     if q.nil?
762       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
763       return
764     end
765
766     debug q.rank_table.inspect
767
768     source = params[:source]
769     dest = params[:dest]
770     transscore = params[:score].to_i
771     transjokers = params[:jokers].to_i
772     debug "Transferring #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
773
774     if q.registry.has_key?(source)
775       sourceplayer = q.registry[source]
776       score = sourceplayer.score
777       if transscore == -1
778         transscore = score
779       end
780       if score < transscore
781         m.reply "#{source} only has #{score} points!"
782         return
783       end
784       jokers = sourceplayer.jokers
785       if transjokers == -1
786         transjokers = jokers
787       end
788       if jokers < transjokers
789         m.reply "#{source} only has #{jokers} jokers!!"
790         return
791       end
792       if q.registry.has_key?(dest)
793         destplayer = q.registry[dest]
794       else
795         destplayer = PlayerStats.new(0,0,0)
796       end
797
798       if sourceplayer == destplayer
799         m.reply "Source and destination are the same, I'm not going to touch them"
800         return
801       end
802
803       sourceplayer.score -= transscore
804       destplayer.score += transscore
805       sourceplayer.jokers -= transjokers
806       destplayer.jokers += transjokers
807
808       q.registry[source] = sourceplayer
809       calculate_ranks(m, q, source)
810
811       q.registry[dest] = destplayer
812       calculate_ranks(m, q, dest)
813
814       m.reply "Transferred #{transscore} points and #{transjokers} jokers from #{source} to #{dest}"
815     else
816       m.reply "#{source} doesn't have any points!"
817     end
818   end
819
820
821   def cmd_del_player( m, params )
822     chan = m.channel
823     q = create_quiz( chan )
824     if q.nil?
825       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
826       return
827     end
828
829     debug q.rank_table.inspect
830
831     nick = params[:nick]
832     if q.registry.has_key?(nick)
833       player = q.registry[nick]
834       score = player.score
835       if score != 0
836         m.reply "Can't delete player #{nick} with score #{score}."
837         return
838       end
839       jokers = player.jokers
840       if jokers != 0
841         m.reply "Can't delete player #{nick} with #{jokers} jokers."
842         return
843       end
844       q.registry.delete(nick)
845
846       player_rank = nil
847       q.rank_table.each_index { |rank|
848         if nick.downcase == q.rank_table[rank][0].downcase
849           player_rank = rank
850           break
851         end
852       }
853       q.rank_table.delete_at(player_rank)
854
855       m.reply "Player #{nick} deleted."
856     else
857       m.reply "Player #{nick} isn't even in the database."
858     end
859   end
860
861
862   def cmd_set_score(m, params)
863     chan = m.channel
864     q = create_quiz( chan )
865     if q.nil?
866       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
867       return
868     end
869     debug q.rank_table.inspect
870
871     nick = params[:nick]
872     val = params[:score].to_i
873     if q.registry.has_key?(nick)
874       player = q.registry[nick]
875       player.score = val
876     else
877       player = PlayerStats.new( val, 0, 0)
878     end
879     q.registry[nick] = player
880     calculate_ranks(m, q, nick)
881     m.reply "Score for player #{nick} set to #{val}."
882   end
883
884
885   def cmd_set_jokers(m, params)
886     chan = m.channel
887     q = create_quiz( chan )
888     if q.nil?
889       m.reply "Sorry, the quiz database for #{chan} seems to be corrupt"
890       return
891     end
892     debug q.rank_table.inspect
893
894     nick = params[:nick]
895     val = [params[:jokers].to_i, Max_Jokers].min
896     if q.registry.has_key?(nick)
897       player = q.registry[nick]
898       player.jokers = val
899     else
900       player = PlayerStats.new( 0, val, 0)
901     end
902     q.registry[nick] = player
903     m.reply "Jokers for player #{nick} set to #{val}."
904   end
905 end
906
907
908
909 plugin = QuizPlugin.new
910 plugin.default_auth( 'edit', false )
911
912 # Normal commands
913 plugin.map 'quiz',                  :action => 'cmd_quiz'
914 plugin.map 'quiz solve',            :action => 'cmd_solve'
915 plugin.map 'quiz hint',             :action => 'cmd_hint'
916 plugin.map 'quiz skip',             :action => 'cmd_skip'
917 plugin.map 'quiz joker',            :action => 'cmd_joker'
918 plugin.map 'quiz score',            :action => 'cmd_score'
919 plugin.map 'quiz score :player',    :action => 'cmd_score_player'
920 plugin.map 'quiz fetch',            :action => 'cmd_fetch'
921 plugin.map 'quiz refresh',          :action => 'cmd_refresh'
922 plugin.map 'quiz top5',             :action => 'cmd_top5'
923 plugin.map 'quiz top :number',      :action => 'cmd_top_number'
924 plugin.map 'quiz stats',            :action => 'cmd_stats'
925
926 # Admin commands
927 plugin.map 'quiz autoask :enable',  :action => 'cmd_autoask', :auth_path => 'edit'
928 plugin.map 'quiz autoask delay :time',  :action => 'cmd_autoask_delay', :auth_path => 'edit', :requirements => {:time => /\d+/}
929 plugin.map 'quiz transfer :source :dest :score :jokers', :action => 'cmd_transfer', :auth_path => 'edit', :defaults => {:score => '-1', :jokers => '-1'}
930 plugin.map 'quiz deleteplayer :nick', :action => 'cmd_del_player', :auth_path => 'edit'
931 plugin.map 'quiz setscore :nick :score', :action => 'cmd_set_score', :auth_path => 'edit'
932 plugin.map 'quiz setjokers :nick :jokers', :action => 'cmd_set_jokers', :auth_path => 'edit'