| 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 |
|---|
| 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 | :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 |
|---|
| 152 | end |
|---|
| 153 | |
|---|
| 154 | |
|---|
| 155 | ####################################################################### |
|---|
| 156 | # CLASS QuizPlugin |
|---|
| 157 | ####################################################################### |
|---|
| 158 | class 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( / /, " " ).gsub( /&/, "&" ).gsub( /"/, "\"" ) |
|---|
| 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 | |
|---|
| 935 | end |
|---|
| 936 | |
|---|
| 937 | |
|---|
| 938 | |
|---|
| 939 | plugin = QuizPlugin.new |
|---|
| 940 | plugin.default_auth( 'edit', false ) |
|---|
| 941 | |
|---|
| 942 | # Normal commands |
|---|
| 943 | plugin.map 'quiz', :action => 'cmd_quiz' |
|---|
| 944 | plugin.map 'quiz solve', :action => 'cmd_solve' |
|---|
| 945 | plugin.map 'quiz hint', :action => 'cmd_hint' |
|---|
| 946 | plugin.map 'quiz skip', :action => 'cmd_skip' |
|---|
| 947 | plugin.map 'quiz joker', :action => 'cmd_joker' |
|---|
| 948 | plugin.map 'quiz score', :action => 'cmd_score' |
|---|
| 949 | plugin.map 'quiz score :player', :action => 'cmd_score_player' |
|---|
| 950 | plugin.map 'quiz fetch', :action => 'cmd_fetch' |
|---|
| 951 | plugin.map 'quiz refresh', :action => 'cmd_refresh' |
|---|
| 952 | plugin.map 'quiz top5', :action => 'cmd_top5' |
|---|
| 953 | plugin.map 'quiz top :number', :action => 'cmd_top_number' |
|---|
| 954 | plugin.map 'quiz stats', :action => 'cmd_stats' |
|---|
| 955 | |
|---|
| 956 | # Admin commands |
|---|
| 957 | plugin.map 'quiz autoask :enable', :action => 'cmd_autoask', :auth_path => 'edit' |
|---|
| 958 | plugin.map 'quiz autoask delay :time', :action => 'cmd_autoask_delay', :auth_path => 'edit', :requirements => {:time => /\d+/} |
|---|
| 959 | plugin.map 'quiz transfer :source :dest :score :jokers', :action => 'cmd_transfer', :auth_path => 'edit', :defaults => {:score => '-1', :jokers => '-1'} |
|---|
| 960 | plugin.map 'quiz deleteplayer :nick', :action => 'cmd_del_player', :auth_path => 'edit' |
|---|
| 961 | plugin.map 'quiz setscore :nick :score', :action => 'cmd_set_score', :auth_path => 'edit' |
|---|
| 962 | plugin.map 'quiz setjokers :nick :jokers', :action => 'cmd_set_jokers', :auth_path => 'edit' |
|---|
| 963 | plugin.map 'quiz cleanup', :action => 'cmd_cleanup', :auth_path => 'edit' |
|---|