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