root/data/rbot/plugins/games/quiz.rb @ a7b2718fb0ee7309fa73458b660112e046cf957e

Revision a7b2718fb0ee7309fa73458b660112e046cf957e, 27.9 KB (checked in by Tom Gilbert <tom@…>, 3 years ago)

fix "warning: don't put space before argument parentheses"

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