├── .ruby-version ├── .ruby-gemset ├── .gitignore ├── run.sh ├── warmup.sh ├── player_x.rb ├── spec ├── spec_helper.rb └── extreme_startup │ ├── general_knowledge_question_spec.rb │ ├── warmup_question_spec.rb │ ├── scoreboard_spec.rb │ ├── power_question_spec.rb │ ├── subtraction_question_spec.rb │ ├── scrabble_question_spec.rb │ ├── maximum_question_spec.rb │ ├── anagram_question_spec.rb │ ├── slashdot_rate_controller_spec.rb │ ├── primes_spec.rb │ ├── fib_question_spec.rb │ ├── square_and_cube_spec.rb │ ├── rate_controller_spec.rb │ └── question_factory_spec.rb ├── Dockerfile ├── web_server.rb ├── player_y.rb ├── features ├── step_definitions │ ├── warm_up_steps.rb │ ├── steps.rb │ └── team_steps.rb ├── withdraw.feature ├── add_team.feature ├── support │ ├── env.rb │ └── testing_api.rb ├── warm_up.feature ├── penalty_rules.feature └── basic_scoring.feature ├── Rakefile ├── Gemfile ├── views ├── player_added.haml ├── no_such_player.haml ├── add_player.haml ├── scores.haml ├── leaderboard.haml ├── controlpanel.haml ├── metrics_index.haml └── personal_page.haml ├── LICENSE.md ├── lib └── extreme_startup │ ├── game_state.rb │ ├── anagrams.yaml │ ├── player.rb │ ├── scoreboard.rb │ ├── quiz_master.rb │ ├── web_server.rb │ └── question_factory.rb ├── public ├── js │ ├── controlpanel.js │ ├── leaderboard.js │ ├── smoothie.js │ └── jquery-1.6.2.min.js └── css │ └── style.css ├── Gemfile.lock └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 1.9.3 2 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | extreme_startup 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | *.swp 3 | .project 4 | .buildpath 5 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run -d -p 80:3000 extreme_startup 4 | -------------------------------------------------------------------------------- /warmup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run -d -p 80:3000 -e WARMUP=1 extreme_startup 4 | -------------------------------------------------------------------------------- /player_x.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | 4 | get '/' do 5 | "x" 6 | end 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:1.9-onbuild 2 | 3 | EXPOSE 3000 4 | CMD /usr/local/bin/ruby web_server.rb 5 | -------------------------------------------------------------------------------- /web_server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | require_relative 'lib/extreme_startup/web_server' 4 | ExtremeStartup::WebServer.run! 5 | -------------------------------------------------------------------------------- /player_y.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra' 3 | 4 | configure do 5 | set :port, 4568 6 | end 7 | 8 | get '/' do 9 | "4" 10 | end -------------------------------------------------------------------------------- /features/step_definitions/warm_up_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^the server is running in warm\-up mode$/ do 2 | app.question_factory = ExtremeStartup::WarmupQuestionFactory.new 3 | end 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :cucumber do 2 | sh "cucumber -f progress" 3 | end 4 | 5 | task :rspec do 6 | sh "rspec spec" 7 | end 8 | 9 | task :default => [:rspec, :cucumber] 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | gem 'eventmachine' 5 | gem 'thin' 6 | gem 'sinatra' 7 | gem 'httparty' 8 | gem 'uuid' 9 | gem 'haml' 10 | gem 'rspec' 11 | gem 'cucumber' 12 | gem 'rack-test' 13 | gem 'capybara' 14 | gem 'json' 15 | 16 | -------------------------------------------------------------------------------- /views/player_added.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %body 4 | %h1 Player added - Thanks 5 | 6 | %p 7 | You can view your personal log page by 8 | %a{ :href => "/players/#{playerid}" } following this link 9 | don't close that tab! 10 | -------------------------------------------------------------------------------- /views/no_such_player.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %title Extreme Startup - No Such Player 4 | %link(rel="stylesheet" href="/css/style.css") 5 | %body 6 | %h1 No Such Player 7 | %p 8 | No player is registered with that id. Perhaps the game was restarted? You may need to re-register. 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution-NonCommercial 3.0 Unported License. 2 | To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/3.0/ 3 | or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. 4 | -------------------------------------------------------------------------------- /lib/extreme_startup/game_state.rb: -------------------------------------------------------------------------------- 1 | module ExtremeStartup 2 | 3 | class GameState 4 | def initialize 5 | @paused = false 6 | end 7 | 8 | def is_running? 9 | ! @paused 10 | end 11 | 12 | def pause 13 | @paused = true 14 | end 15 | 16 | def resume 17 | @paused = false 18 | end 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /spec/extreme_startup/general_knowledge_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | 4 | module ExtremeStartup 5 | describe GeneralKnowledgeQuestion do 6 | let(:question) { GeneralKnowledgeQuestion.new(Player.new) } 7 | 8 | it "converts to a string" do 9 | question.as_text.should =~ /wh.+/ 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /features/withdraw.feature: -------------------------------------------------------------------------------- 1 | Feature: A team can withdraw from the game 2 | In order to be considered as big loosers 3 | As a team 4 | I want to be able to withdraw from the game 5 | 6 | Scenario: Withdraw 7 | Given I am playing 8 | When I withdraw 9 | Then my name should not be on the leaderboard anymore 10 | And my player page should give a nice error 11 | And the game master should not send me anymore questions -------------------------------------------------------------------------------- /public/js/controlpanel.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | $('#advance').click(function(){ 4 | $.post('/advance_round', function(data) { 5 | location.reload(); 6 | }); 7 | }); 8 | 9 | $('#pause').click(function(){ 10 | $.post('/pause', function(data) { 11 | location.reload(); 12 | }); 13 | }); 14 | 15 | $('#resume').click(function(){ 16 | $.post('/resume', function(data) { 17 | location.reload(); 18 | }); 19 | }); 20 | 21 | }); -------------------------------------------------------------------------------- /views/add_player.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %body 4 | %h1 Add a new player 5 | 6 | %form{:action => "/players", :method => "post"} 7 | %label{ :for => "name" }Name: 8 | %input{ :type => "text", :id => "name", :name => "name" } 9 | 10 | %label{ :for => "name" }URL: 11 | %input{ :type => "text", :id => "url", :name => "url", :placeholder => "http://..."} 12 | 13 | %input{ :type => "submit", :value => "Submit"} 14 | 15 | -------------------------------------------------------------------------------- /lib/extreme_startup/anagrams.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | anagram: listen 4 | correct: inlets 5 | incorrect: 6 | - enlists 7 | - google 8 | - banana 9 | - 10 | anagram: listen 11 | correct: silent 12 | incorrect: 13 | - enlists 14 | - google 15 | - banana 16 | - 17 | anagram: dictionary 18 | correct: indicatory 19 | incorrect: 20 | - abdication 21 | - incendiary 22 | - butterfly 23 | - 24 | anagram: admirer 25 | correct: married 26 | incorrect: 27 | - dairy 28 | - random 29 | - border 30 | - forgot 31 | -------------------------------------------------------------------------------- /views/scores.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %title Extreme Startup - Score Graph 4 | %link(rel="stylesheet" href="/css/style.css") 5 | %script{:type => "text/javascript", :src => "/js/smoothie.js"} 6 | %script{:type => "text/javascript", :src => "/js/jquery-1.6.2.min.js"} 7 | %script{:type => "text/javascript", :src => "/js/leaderboard.js"} 8 | 9 | %body 10 | .left 11 | %canvas{:id => "mycanvas", :width => 500, :height => 500 } 12 | .right 13 | %h1 Leader Board 14 | %ul{:id => "scoreboard" } 15 | -------------------------------------------------------------------------------- /views/leaderboard.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %title Extreme Startup - Leaderboard 4 | %link(rel="stylesheet" href="/css/style.css") 5 | %body 6 | %a{ :href => '/players' } 7 | I want to play! 8 | %h1 Leaderboard 9 | %ul 10 | - leaderboard.each do |entry| 11 | %li.player 12 | %div{:class => ["ranking", "name"]} 13 | &= entry.playername 14 | %div{:class => ["ranking", "points"]} #{entry.score} 15 | 16 | :javascript 17 | refresh = function() { window.location.reload(); }; 18 | setTimeout(refresh, 10000); 19 | -------------------------------------------------------------------------------- /views/controlpanel.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %title Extreme Startup - Control Panel 4 | %link(rel="stylesheet" href="/css/style.css") 5 | %script{:type => "text/javascript", :src => "/js/jquery-1.6.2.min.js"} 6 | %script{:type => "text/javascript", :src => "/js/controlpanel.js"} 7 | 8 | %body 9 | %h1 Current Round: #{round} 10 | %form 11 | %input#advance{:type => "button", :value => "Advance Round"} 12 | - if (game_state.is_running?) 13 | %input#pause{:type => "button", :value => "Pause Game"} 14 | - else 15 | %input#resume{:type => "button", :value => "Resume Game"} 16 | -------------------------------------------------------------------------------- /views/metrics_index.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %title Extreme Startup - Metrics Index 4 | %link(rel="stylesheet" href="/css/style.css") 5 | %body 6 | %h1 Metrics 7 | %p 8 | The following metrics are available for your player 9 | 10 | %ul 11 | %li 12 | %a(href="metrics/score") 13 | Current score 14 | %li 15 | %a(href="metrics/correct") 16 | Total number of queries answered correctly 17 | %li 18 | %a(href="metrics/incorrect") 19 | Total number of queries answered incorrectly 20 | %li 21 | %a(href="metrics/requestcount") 22 | Total number of queries received -------------------------------------------------------------------------------- /views/personal_page.haml: -------------------------------------------------------------------------------- 1 | %html 2 | %head 3 | %title Extreme Startup - #{name} 4 | %link(rel="stylesheet" href="/css/style.css") 5 | %body 6 | %h1 7 | &= "Hello, #{name}!" 8 | 9 | %h2 10 | Your score is: 11 | %span.score #{score} 12 | 13 | %a{ :href => "/withdraw/#{playerid}" } 14 | Withdraw 15 | 16 | %h2 Log: 17 | %ul 18 | - log.each do |logline| 19 | %li 20 | %div{:class => ["logline", "id", logline.result]} #{logline.id} 21 | %div{:class => ["logline", "result", logline.result]} result: #{logline.result} 22 | %div{:class => ["logline", "points", logline.result]} points awarded: #{logline.points} 23 | -------------------------------------------------------------------------------- /features/add_team.feature: -------------------------------------------------------------------------------- 1 | Feature: A team can register to the game 2 | In order to show everyone that we are the best 3 | As a team 4 | I want to be able to register my team 5 | 6 | Scenario: Add team 7 | Given I want to play the game 8 | When I submit my team name and the url of my computer 9 | Then I should see that my team was added 10 | And I should see my team on the leaderboard 11 | 12 | Scenario: When added team receives a link to its log 13 | Given I submitted my team info 14 | Then I should receive a link to my market requests log 15 | 16 | Scenario: Start receiving questions 17 | Given I am playing 18 | Then the game master should start sending me question -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | h1 {color:blue} 2 | 3 | .score {color:blue} 4 | 5 | .left { float: left; width: 500px; } 6 | .right{ float: right; width: 500px; } 7 | 8 | li { display: block; } 9 | 10 | .logline {margin:2px; padding:3px; display:inline-block} 11 | .logline.id {width:100} 12 | .logline.result {width:200} 13 | .logline.points {width:200} 14 | .logline.correct {background-color:#7cfc00} 15 | .logline.pass {background-color:#eed5b7} 16 | .logline.wrong {background-color:#ffaa00} 17 | .logline.no_server_response {background-color:#ff4500} 18 | .logline.error_response {background-color:#ff4500} 19 | 20 | .ranking {margin:2px; padding:3px; display:inline-block; background-color:#eed5b7} 21 | .ranking.name {width:200} 22 | .ranking.points {width:200} 23 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | $: << File.expand_path(File.dirname(__FILE__) + '/../../lib') 2 | require 'extreme_startup/web_server' 3 | 4 | require 'rack/test' 5 | require 'capybara/cucumber' 6 | Capybara.app = ExtremeStartup::WebServer 7 | 8 | module RackTestWorld 9 | include Rack::Test::Methods 10 | 11 | def app 12 | ExtremeStartup::WebServer 13 | end 14 | end 15 | World(RackTestWorld) 16 | 17 | $silence_logging = true 18 | 19 | Before do 20 | app.players = Hash.new 21 | app.players_threads = Hash.new 22 | app.scoreboard = ExtremeStartup::Scoreboard.new(false) 23 | end 24 | 25 | After do 26 | app.players_threads.each do |uuid, thread| 27 | thread.exit 28 | end 29 | app.question_factory = ExtremeStartup::QuestionFactory.new 30 | end 31 | -------------------------------------------------------------------------------- /spec/extreme_startup/warmup_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/player' 3 | 4 | module ExtremeStartup 5 | describe Question do 6 | let(:player) { Player.new({"name" => "S\u00E9bastian".force_encoding("UTF-8"), 7 | "url" => "http://127.0.0.1:8080", }) } 8 | let(:question) { 9 | question = WarmupQuestion.new(player) 10 | question.answer = "S\u00E9bastian".force_encoding("ASCII-8BIT") 11 | question 12 | } 13 | 14 | context "when displaying response with an answer encoded into ASCII-8BIT" do 15 | it "should not raise error" do 16 | lambda { "For player #{:player}\n#{question.display_result}" }.should_not raise_error 17 | end 18 | end 19 | 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /spec/extreme_startup/scoreboard_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/scoreboard' 3 | 4 | module ExtremeStartup 5 | describe Scoreboard do 6 | let(:scoreboard) { Scoreboard.new(false) } 7 | 8 | describe "#leaderboard_position" do 9 | let(:player_a) { double(:uuid => 'a') } 10 | let(:player_b) { double(:uuid => 'b') } 11 | 12 | it "when none of the players have any points, it sorts by the order they were added" do 13 | scoreboard.new_player player_a 14 | scoreboard.new_player player_b 15 | scoreboard.leaderboard_position(player_a).should == 1 16 | scoreboard.leaderboard_position(player_b).should == 2 17 | end 18 | 19 | it "it sorts by the number of points" do 20 | end 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /spec/extreme_startup/power_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe PowerQuestion do 7 | let(:question) { PowerQuestion.new(Player.new) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /what is \d+ to the power of \d+/i 11 | end 12 | 13 | context "when the numbers are known" do 14 | let(:question) { PowerQuestion.new(Player.new, 2,3) } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /what is 2 to the power of 3/i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("8").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("9").should be_false 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/extreme_startup/subtraction_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe SubtractionQuestion do 7 | let(:question) { SubtractionQuestion.new(Player.new) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /what is \d+ minus \d+/i 11 | end 12 | 13 | context "when the numbers are known" do 14 | let(:question) { SubtractionQuestion.new(Player.new, 2,3) } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /what is 2 minus 3/i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("-1").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("77").should be_false 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/extreme_startup/scrabble_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe ScrabbleQuestion do 7 | let(:question) { ScrabbleQuestion.new(Player.new) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /what is the english scrabble score of \w+/i 11 | end 12 | 13 | context "when the words are known" do 14 | let(:question) { ScrabbleQuestion.new(Player.new, "spaceman") } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /what is the english scrabble score of spaceman/i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("14").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("23").should be_false 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /features/warm_up.feature: -------------------------------------------------------------------------------- 1 | Feature: Warm up 2 | 3 | Background: 4 | Given the server is running in warm-up mode 5 | 6 | Scenario: 7 | Given a player "bob" who plays like this: 8 | """ 9 | get '/' do 10 | 'bob' 11 | end 12 | """ 13 | When the player is entered 14 | And the game is played for 1 second 15 | Then the scores should be: 16 | | player | score | 17 | | bob | 10 | 18 | And the log for bob should show: 19 | | result: correct | points awarded: 10 | 20 | 21 | Scenario: 22 | Given a player "Sébastian" who plays like this: 23 | """ 24 | get '/' do 25 | 'Sébastian' 26 | end 27 | """ 28 | When the player is entered 29 | And the game is played for 1 second 30 | Then the scores should be: 31 | | player | score | 32 | | Sébastian | 10 | 33 | And the log for Sébastian should show: 34 | | result: correct | points awarded: 10 | 35 | -------------------------------------------------------------------------------- /lib/extreme_startup/player.rb: -------------------------------------------------------------------------------- 1 | require 'uuid' 2 | 3 | module ExtremeStartup 4 | 5 | class LogLine 6 | attr_reader :id, :result, :points 7 | def initialize(id, result, points) 8 | @id = id 9 | @result = result 10 | @points = points 11 | end 12 | 13 | def to_s 14 | "#{@id}: #{@result} - points awarded: #{@points}" 15 | end 16 | end 17 | 18 | class Player 19 | attr_reader :name, :url, :uuid, :log 20 | 21 | class << self 22 | def generate_uuid 23 | @uuid_generator ||= UUID.new 24 | @uuid_generator.generate.to_s[0..7] 25 | end 26 | end 27 | 28 | def initialize(params = {}) 29 | @name = params['name'] 30 | @url = params['url'] 31 | @uuid = Player.generate_uuid 32 | @log = [] 33 | end 34 | 35 | def log_result(id, msg, points) 36 | @log.unshift(LogLine.new(id, msg, points)) 37 | end 38 | 39 | def to_s 40 | "#{name} (#{url})" 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /spec/extreme_startup/maximum_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe MaximumQuestion do 7 | let(:question) { MaximumQuestion.new(Player.new) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /which of the following numbers is the largest: (\d+, )+(\d+)/i 11 | end 12 | 13 | context "when the numbers and known" do 14 | let(:question) { MaximumQuestion.new(Player.new, 2,4,3) } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /which of the following numbers is the largest: 2, 4, 3/i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("4").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("3").should be_false 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/extreme_startup/anagram_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe AnagramQuestion do 7 | let(:question) { AnagramQuestion.new(Player.new) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /which of the following is an anagram of "\w+":/i 11 | end 12 | 13 | context "when the words are known" do 14 | let(:question) { AnagramQuestion.new(Player.new, "listen", "inlets", "enlists", "google", "banana") } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /which of the following is an anagram of "listen": /i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("inlets").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("enlists").should be_false 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /features/penalty_rules.feature: -------------------------------------------------------------------------------- 1 | Feature: Penalty Rules 2 | 3 | Penalty for failure depends on your league position. 4 | 5 | If you get a question wrong, your score is reduced by X, where X is the potential value of the question, divided by your current leaderboard position. So, the higher you are on the leaderboard, the more penalty you suffer for getting the same question wrong. 6 | 7 | i.e. 8 | 9 | X = Value / Position 10 | 11 | When the game starts, initial leaderboard position is determined by the order in which you entered. 12 | 13 | Scenario: One player plays another 14 | 15 | Given a player "always-right" who plays like this: 16 | """ 17 | get('/') { '0' } 18 | """ 19 | Given a player "always-wrong" who plays like this: 20 | """ 21 | get('/') { 'PASS' } 22 | """ 23 | And the correct answer to every question is '0' worth 10 points 24 | When the players are entered 25 | And the game is played for 1 second 26 | Then the scores should be: 27 | | player | score | 28 | | always-right | 10 | 29 | | always-wrong | -5 | -------------------------------------------------------------------------------- /features/step_definitions/steps.rb: -------------------------------------------------------------------------------- 1 | Given /^a player "([^"]*)" who plays like this:$/ do |player_name, content| 2 | create_player player_name, content 3 | end 4 | 5 | Given /^the correct answer to every question is '(\d+)'$/ do |answer| 6 | stub_correct_answer_to_be answer 7 | end 8 | 9 | Given /^the correct answer to every question is '(\d+)' worth (\d+) points$/ do |answer, points| 10 | stub_correct_answer_to_be answer, points.to_i 11 | end 12 | 13 | When /^the (?:player is|players are) entered$/ do 14 | players.each do |player| 15 | player.start 16 | enter_player player 17 | end 18 | end 19 | 20 | When /^the game is played for (\d+) second$/ do |num_seconds| 21 | sleep num_seconds.to_i 22 | end 23 | 24 | Then /^the scores should be:$/ do |table| 25 | table.hashes.each do |row| 26 | score_for(row['player']).should == row['score'].to_i 27 | end 28 | end 29 | 30 | Then /^the log for (.+) should show:$/ do |player_name, table| 31 | player = players.find{ |p| p.name == player_name } 32 | visit player.personal_page 33 | actual = page.all('li').map do |li| 34 | li.all('div').map do |div| 35 | div.text 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/extreme_startup/slashdot_rate_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/quiz_master' 3 | 4 | module ExtremeStartup 5 | describe SlashdotRateController do 6 | let(:controller) { SlashdotRateController.new } 7 | 8 | it "makes the inter-request delay very small" do 9 | controller.delay_before_next_request(FakeAnswer.new(:correct)).should == 0.02 10 | end 11 | 12 | it "increases the delay slightly with each request, up to 5 seconds" do 13 | old_delay = 0 14 | 200.times do 15 | delay = controller.delay_before_next_request(FakeAnswer.new(:correct)) 16 | delay.should be < 4 17 | delay.should be > old_delay 18 | old_delay = delay 19 | end 20 | end 21 | 22 | it "reverts to standard RateController once spike subsides" do 23 | 500.times do 24 | delay = controller.delay_before_next_request(FakeAnswer.new(:correct)) 25 | end 26 | controller.update_algorithm_based_on_score(1).should be_instance_of RateController 27 | end 28 | end 29 | 30 | class FakeAnswer 31 | def initialize(result) 32 | @result = result 33 | end 34 | def was_answered_correctly 35 | @result == :correct 36 | end 37 | def was_answered_wrongly 38 | @result == :wrong 39 | end 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /features/basic_scoring.feature: -------------------------------------------------------------------------------- 1 | Feature: Basic Scoring 2 | 3 | Scenario: Player is correct 4 | Given a player "bob" who plays like this: 5 | """ 6 | get '/' do 7 | '4' 8 | end 9 | """ 10 | And the correct answer to every question is '4' worth 10 points 11 | When the player is entered 12 | And the game is played for 1 second 13 | Then the scores should be: 14 | | player | score | 15 | | bob | 10 | 16 | 17 | Scenario: Player is wrong 18 | Given a player "charlie" who plays like this: 19 | """ 20 | get '/' do 21 | '2' 22 | end 23 | """ 24 | And the correct answer to every question is '4' worth 10 points 25 | When the player is entered 26 | And the game is played for 1 second 27 | Then the scores should be: 28 | | player | score | 29 | | charlie | -10 | 30 | 31 | Scenario: Player causes error 32 | Given a player "ernie" who plays like this: 33 | """ 34 | get '/' do 35 | 1 / 0 36 | end 37 | """ 38 | And the correct answer to every question is '4' 39 | When the player is entered 40 | And the game is played for 1 second 41 | Then the scores should be: 42 | | player | score | 43 | | ernie | -50 | -------------------------------------------------------------------------------- /spec/extreme_startup/primes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe PrimesQuestion do 7 | let(:question) { PrimesQuestion.new(Player.new) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /which of the following numbers are primes: (\d+, )+(\d+)/i 11 | end 12 | 13 | context "when the numbers are known" do 14 | let(:question) { PrimesQuestion.new(Player.new, 3, 4, 6) } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /which of the following numbers are primes: 3, 4, 6/i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("3").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("4").should be_false 26 | end 27 | end 28 | 29 | context "when there are multiple numbers in the answer" do 30 | let(:question) { PrimesQuestion.new(Player.new, 3, 4, 5) } 31 | 32 | it "identifies a correct answer" do 33 | question.answered_correctly?("3, 5").should be_true 34 | end 35 | 36 | it "identifies an incorrect answer" do 37 | question.answered_correctly?("3, 4").should be_false 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/extreme_startup/fib_question_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe FibonacciQuestion do 7 | let(:question) { FibonacciQuestion.new(Player.new, 18, nil) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /what is the 22nd number in the Fibonacci sequence/i 11 | end 12 | 13 | context "when the numbers are known" do 14 | let(:question) { FibonacciQuestion.new(Player.new, 2, nil) } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /what is the 6th number in the Fibonacci sequence/i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("8").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("5").should be_false 26 | end 27 | end 28 | 29 | context "when the numbers are large" do 30 | let(:question) { FibonacciQuestion.new(Player.new, 200, nil) } 31 | 32 | it "converts to the right string" do 33 | question.as_text.should =~ /what is the 204th number in the Fibonacci sequence/i 34 | end 35 | 36 | it "identifies a correct answer" do 37 | question.answered_correctly?("1923063428480944139667114773918309212080528").should be_true 38 | end 39 | 40 | it "identifies an incorrect answer" do 41 | question.answered_correctly?("1923063428480957210245803843555347568525312").should be_false 42 | end 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /features/step_definitions/team_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^I want to play the game$/ do 2 | visit '/players' 3 | end 4 | 5 | When /^I submit my team name and the url of my computer$/ do 6 | fill_in "name", :with => "Jedi masters" 7 | fill_in "url", :with => "http://deathstar.local" 8 | click_on "Submit" 9 | end 10 | 11 | Then /^I should see my team on the leaderboard$/ do 12 | visit '/' 13 | page.should have_content("Jedi masters") 14 | end 15 | 16 | Then /^I should see that my team was added$/ do 17 | page.should have_content("Player added - Thanks") 18 | end 19 | 20 | Given /^I submitted my team info$/ do 21 | steps %Q{ 22 | Given I want to play the game 23 | When I submit my team name and the url of my computer 24 | } 25 | end 26 | 27 | Then /^I should receive a link to my market requests log$/ do 28 | page.should have_selector("a") 29 | page.first("a").click 30 | page.should have_content("Hello, Jedi masters") 31 | end 32 | 33 | Given /^I am playing$/ do 34 | steps %Q{ 35 | Given I submitted my team info 36 | } 37 | end 38 | 39 | Then /^the game master should start sending me question$/ do 40 | player_uuid = app.players.keys.first 41 | app.players_threads[player_uuid].should be_alive 42 | end 43 | 44 | When /^I withdraw$/ do 45 | player_uuid = app.players.keys.first 46 | visit "/players/#{player_uuid}" 47 | click_link 'Withdraw' 48 | end 49 | 50 | Then /^my name should not be on the leaderboard anymore$/ do 51 | page.should_not have_content "Jedi masters" 52 | end 53 | 54 | Then /^the game master should not send me anymore questions$/ do 55 | app.players_threads.should be_empty 56 | end 57 | 58 | Then /^my player page should give a nice error$/ do 59 | visit '/players/no-longer-exists' 60 | page.should have_content("No player is registered with that id") 61 | end -------------------------------------------------------------------------------- /spec/extreme_startup/square_and_cube_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe SquareCubeQuestion do 7 | let(:question) { SquareCubeQuestion.new(Player.new) } 8 | 9 | it "converts to a string" do 10 | question.as_text.should =~ /which of the following numbers is both a square and a cube: (\d+, )+(\d+)/i 11 | end 12 | 13 | context "when the numbers are known" do 14 | let(:question) { SquareCubeQuestion.new(Player.new, 62, 63, 64) } 15 | 16 | it "converts to the right string" do 17 | question.as_text.should =~ /which of the following numbers is both a square and a cube: 62, 63, 64/i 18 | end 19 | 20 | it "identifies a correct answer" do 21 | question.answered_correctly?("64").should be_true 22 | end 23 | 24 | it "identifies an incorrect answer" do 25 | question.answered_correctly?("63").should be_false 26 | end 27 | end 28 | 29 | context "if one of the numbers is zero" do 30 | let(:question) { SquareCubeQuestion.new(Player.new, 0, 5) } 31 | 32 | it "doesn't crash with divide by zero" do 33 | question.answered_correctly?("0").should be_true 34 | question.answered_correctly?("5").should be_false 35 | end 36 | end 37 | 38 | context "when there are multiple numbers in the answer" do 39 | let(:question) { SquareCubeQuestion.new(Player.new, 64, 65, 729) } 40 | 41 | it "identifies a correct answer" do 42 | question.answered_correctly?("64, 729").should be_true 43 | end 44 | 45 | it "identifies an incorrect answer" do 46 | question.answered_correctly?("64").should be_false 47 | end 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | builder (3.2.2) 5 | capybara (1.0.0) 6 | mime-types (>= 1.16) 7 | nokogiri (>= 1.3.3) 8 | rack (>= 1.0.0) 9 | rack-test (>= 0.5.4) 10 | selenium-webdriver (~> 0.2.0) 11 | xpath (~> 0.1.4) 12 | childprocess (0.1.9) 13 | ffi (~> 1.0.6) 14 | cucumber (1.3.10) 15 | builder (>= 2.1.2) 16 | diff-lcs (>= 1.1.3) 17 | gherkin (~> 2.12) 18 | multi_json (>= 1.7.5, < 2.0) 19 | multi_test (>= 0.0.2) 20 | daemons (1.1.9) 21 | diff-lcs (1.2.5) 22 | eventmachine (1.0.3) 23 | ffi (1.0.9) 24 | gherkin (2.12.2) 25 | multi_json (~> 1.3) 26 | haml (4.0.5) 27 | tilt 28 | httparty (0.11.0) 29 | multi_json (~> 1.0) 30 | multi_xml (>= 0.5.2) 31 | json (1.8.1) 32 | json_pure (1.5.2) 33 | macaddr (1.6.5) 34 | systemu (~> 2.6.2) 35 | mime-types (1.16) 36 | multi_json (1.8.4) 37 | multi_test (0.0.3) 38 | multi_xml (0.5.5) 39 | nokogiri (1.4.5) 40 | rack (1.5.2) 41 | rack-test (0.6.2) 42 | rack (>= 1.0) 43 | rspec (2.14.1) 44 | rspec-core (~> 2.14.0) 45 | rspec-expectations (~> 2.14.0) 46 | rspec-mocks (~> 2.14.0) 47 | rspec-core (2.14.8) 48 | rspec-expectations (2.14.5) 49 | diff-lcs (>= 1.1.3, < 2.0) 50 | rspec-mocks (2.14.6) 51 | rubyzip (0.9.4) 52 | selenium-webdriver (0.2.1) 53 | childprocess (>= 0.1.7) 54 | ffi (>= 1.0.7) 55 | json_pure 56 | rubyzip 57 | sinatra (1.2.6) 58 | rack (~> 1.1) 59 | tilt (>= 1.2.2, < 2.0) 60 | systemu (2.6.3) 61 | thin (1.5.1) 62 | daemons (>= 1.0.9) 63 | eventmachine (>= 0.12.6) 64 | rack (>= 1.0.0) 65 | tilt (1.3.2) 66 | uuid (2.3.7) 67 | macaddr (~> 1.0) 68 | xpath (0.1.4) 69 | nokogiri (~> 1.3) 70 | 71 | PLATFORMS 72 | ruby 73 | 74 | DEPENDENCIES 75 | capybara 76 | cucumber 77 | eventmachine 78 | haml 79 | httparty 80 | json 81 | rack-test 82 | rspec 83 | sinatra 84 | thin 85 | uuid 86 | -------------------------------------------------------------------------------- /public/js/leaderboard.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | var colourTable = {} 3 | 4 | var Graph = function(canvas) { 5 | var timeSeries = {}; 6 | var smoothie = new SmoothieChart({millisPerPixel: 500}); 7 | smoothie.streamTo(canvas, 1000); 8 | var randomRgbValue = function () { 9 | return Math.floor(Math.random() * 156 + 99); 10 | } 11 | var randomColour = function () { 12 | return 'rgb(' + [randomRgbValue(), randomRgbValue(), randomRgbValue()].join(',') + ')'; 13 | } 14 | this.updateWith = function (leaderboard) { 15 | for (var i=0; i < leaderboard.length; i += 1) { 16 | var entry = leaderboard[i]; 17 | var series = timeSeries[entry.playerid]; 18 | if (!series) { 19 | series = timeSeries[entry.playerid] = new TimeSeries(); 20 | colourTable[entry.playerid] = randomColour(); 21 | smoothie.addTimeSeries(series, { strokeStyle:colourTable[entry.playerid], lineWidth:3 }); 22 | } 23 | series.append(new Date().getTime(), entry.score); 24 | smoothie.start(); 25 | } 26 | }; 27 | this.pause = function() { 28 | smoothie.stop(); 29 | } 30 | }; 31 | 32 | var ScoreBoard = function(div) { 33 | this.updateWith = function (leaderboard) { 34 | var list = $(''); 35 | for (var i=0; i < leaderboard.length; i += 1) { 36 | var entry = leaderboard[i]; 37 | list.append( 38 | $('
').append( 39 | $('
  • ', {class: "player"}) 40 | .append($('
    ' + entry.playername + '
    ').addClass("ranking name").css("background-color", colourTable[entry.playerid])) 41 | .append($('
    ' + entry.score + '
    ').addClass("ranking points").css("background-color", colourTable[entry.playerid])) 42 | .append($('Withdraw').attr("href", "/withdraw/" + entry.playerid)))); 43 | } 44 | $("#scoreboard").replaceWith(list); 45 | } 46 | }; 47 | 48 | var graph = new Graph($('#mycanvas')[0]); // get DOM object from jQuery object 49 | var scoreboard = new ScoreBoard($('#scoreboard')); 50 | 51 | setInterval(function() { 52 | $.ajax({ 53 | url: '/scores', 54 | success: function( data ) { 55 | var leaderboard = JSON.parse(data); 56 | if (leaderboard.inplay) { 57 | graph.updateWith(leaderboard.entries); 58 | scoreboard.updateWith(leaderboard.entries); 59 | } else { 60 | graph.pause(); 61 | } 62 | } 63 | }); 64 | }, 1000); 65 | } 66 | ); -------------------------------------------------------------------------------- /spec/extreme_startup/rate_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/quiz_master' 3 | 4 | module ExtremeStartup 5 | describe RateController do 6 | let(:controller) { RateController.new } 7 | 8 | it "reduces delays after successive correct answers" do 9 | controller.delay_before_next_request(FakeAnswer.new(:correct)).should == 4.9 10 | controller.delay_before_next_request(FakeAnswer.new(:correct)).should == 4.8 11 | controller.delay_before_next_request(FakeAnswer.new(:correct)).should == 4.7 12 | end 13 | 14 | it "enforces minimum delay between successive correct answers is one second" do 15 | 1000.times { controller.delay_before_next_request(FakeAnswer.new(:correct)) } 16 | controller.delay_before_next_request(FakeAnswer.new(:correct)).should == 1 17 | end 18 | 19 | it "increases delays after successive wrong answers" do 20 | controller.delay_before_next_request(FakeAnswer.new(:wrong)).should == 5.1 21 | controller.delay_before_next_request(FakeAnswer.new(:wrong)).should == 5.2 22 | controller.delay_before_next_request(FakeAnswer.new(:correct)).should == 5.1 23 | controller.delay_before_next_request(FakeAnswer.new(:wrong)).should == 5.2 24 | controller.delay_before_next_request(FakeAnswer.new(:wrong)).should == 5.3 25 | end 26 | 27 | it "enforces maximum delay between successive wrong answers is 20 seconds" do 28 | 1000.times { controller.delay_before_next_request(FakeAnswer.new(:wrong)) } 29 | controller.delay_before_next_request(FakeAnswer.new(:wrong)).should == 20 30 | end 31 | 32 | it "delays 20 seconds after error responses" do 33 | controller.delay_before_next_request(FakeAnswer.new(:error)).should == 20 34 | end 35 | 36 | it "occasionally switches a SlashdotRateController if score is above 2000" do 37 | class ExtremeStartup::RateController 38 | def slashdot_probability_percent 39 | 100 40 | end 41 | end 42 | controller.update_algorithm_based_on_score(100).should == controller 43 | controller.update_algorithm_based_on_score(2001).should be_instance_of SlashdotRateController 44 | 45 | end 46 | 47 | end 48 | 49 | class FakeAnswer 50 | def initialize(result) 51 | @result = result 52 | end 53 | def was_answered_correctly 54 | @result == :correct 55 | end 56 | def was_answered_wrongly 57 | @result == :wrong 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/extreme_startup/question_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'extreme_startup/question_factory' 3 | require 'extreme_startup/player' 4 | 5 | module ExtremeStartup 6 | describe QuestionFactory do 7 | let(:player) { Player.new("player one") } 8 | let(:factory) { QuestionFactory.new } 9 | 10 | context "in the first round" do 11 | it "creates both AdditionQuestions and SquareCubeQuestion" do 12 | questions = 10.times.map { factory.next_question(player) } 13 | questions.any? { |q| q.is_a?(AdditionQuestion) }.should be_true 14 | questions.any? { |q| q.is_a?(MaximumQuestion) }.should be_true 15 | questions.all? { |q| [AdditionQuestion, MaximumQuestion].include? q.class } 16 | end 17 | end 18 | 19 | context "in the second round" do 20 | before(:each) do 21 | factory.advance_round 22 | end 23 | 24 | it "creates four different types of question" do 25 | questions = 20.times.map { factory.next_question(player) } 26 | questions.any? { |q| q.is_a?(AdditionQuestion) }.should be_true 27 | questions.any? { |q| q.is_a?(MaximumQuestion) }.should be_true 28 | questions.any? { |q| q.is_a?(MultiplicationQuestion) }.should be_true 29 | questions.any? { |q| q.is_a?(SquareCubeQuestion) }.should be_true 30 | questions.all? { |q| [AdditionQuestion, MaximumQuestion, MultiplicationQuestion, SquareCubeQuestion, ].include? q.class } 31 | end 32 | 33 | end 34 | 35 | context "in the third round" do 36 | before(:each) do 37 | factory.advance_round 38 | factory.advance_round 39 | end 40 | 41 | it "moves a sliding window forward, keeping 5 question types, so AdditionQuestions no longer appear" do 42 | questions = 30.times.map { factory.next_question(player) } 43 | questions.any? { |q| q.is_a?(AdditionQuestion) }.should be_false 44 | questions.any? { |q| q.is_a?(MaximumQuestion) }.should be_true 45 | questions.any? { |q| q.is_a?(MultiplicationQuestion) }.should be_true 46 | questions.any? { |q| q.is_a?(SquareCubeQuestion) }.should be_true 47 | questions.any? { |q| q.is_a?(MultiplicationQuestion) }.should be_true 48 | questions.any? { |q| q.is_a?(SquareCubeQuestion) }.should be_true 49 | questions.all? { |q| [MaximumQuestion, MultiplicationQuestion, SquareCubeQuestion, GeneralKnowledgeQuestion, PrimesQuestion].include? q.class } 50 | end 51 | 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/extreme_startup/scoreboard.rb: -------------------------------------------------------------------------------- 1 | module ExtremeStartup 2 | class Scoreboard 3 | attr_reader :scores 4 | 5 | def initialize(lenient) 6 | @lenient = lenient 7 | @scores = Hash.new { 0 } 8 | @correct_tally = Hash.new { 0 } 9 | @incorrect_tally = Hash.new { 0 } 10 | @request_counts = Hash.new { 0 } 11 | end 12 | 13 | def increment_score_for(player, question) 14 | increment = score(question, leaderboard_position(player)) 15 | @scores[player.uuid] += increment 16 | if (increment > 0) 17 | @correct_tally[player.uuid] += 1 18 | elsif (increment < 0) 19 | @incorrect_tally[player.uuid] += 1 20 | end 21 | puts "added #{increment} to player #{player.name}'s score. It is now #{@scores[player.uuid]}" 22 | player.log_result(question.id, question.result, increment) 23 | end 24 | 25 | def record_request_for(player) 26 | @request_counts[player.uuid] += 1 27 | end 28 | 29 | def new_player(player) 30 | @scores[player.uuid] = 0 31 | end 32 | 33 | def delete_player(player) 34 | @scores.delete(player.uuid) 35 | end 36 | 37 | def current_score(player) 38 | @scores[player.uuid] 39 | end 40 | 41 | def current_total_correct(player) 42 | @correct_tally[player.uuid] 43 | end 44 | 45 | def current_total_not_correct(player) 46 | @incorrect_tally[player.uuid] 47 | end 48 | 49 | def total_requests_for(player) 50 | @request_counts[player.uuid] 51 | end 52 | 53 | def leaderboard 54 | @scores.sort{|a,b| b[1]<=>a[1]} 55 | end 56 | 57 | def leaderboard_position(player) 58 | leaderboard.index do |uuid, score| 59 | uuid == player.uuid 60 | end + 1 61 | end 62 | 63 | def score(question, leaderboard_position) 64 | case question.result 65 | when "correct" then question.points 66 | when "wrong" then @lenient ? allow_passes(question, leaderboard_position) : penalty(question, leaderboard_position) 67 | when "error_response" then -50 68 | when "no_server_response" then -20 69 | else puts "!!!!! unrecognized result '#{question.result}' from #{question.inspect} in Scoreboard#score" 70 | end 71 | end 72 | 73 | def allow_passes(question, leaderboard_position) 74 | (question.answer == "") ? 0 : penalty(question, leaderboard_position) 75 | end 76 | 77 | def penalty(question, leaderboard_position) 78 | -1 * question.points / leaderboard_position 79 | end 80 | 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /features/support/testing_api.rb: -------------------------------------------------------------------------------- 1 | require 'thin' 2 | Thin::Logging.silent = true # uncomment this if things are going wrong on the player 3 | 4 | module TestingApi 5 | 6 | class TestablePlayer 7 | attr_reader :url, :name 8 | attr_accessor :personal_page 9 | 10 | def initialize(name, content) 11 | @name, @content = name, content 12 | @port = next_free_port 13 | @app = Class.new(Sinatra::Base) do 14 | eval content 15 | 16 | get('/ping') { 'OK' } 17 | end 18 | end 19 | 20 | def start 21 | app = @app 22 | port = @port 23 | @thread = Thread.new do 24 | Thin::Server.start('localhost', port) do 25 | map('/') { run app.new } 26 | end 27 | end 28 | @url = "http://localhost:#{port}" 29 | Timeout.timeout(2) do 30 | until responsive?;end 31 | end 32 | end 33 | 34 | private 35 | 36 | def responsive? 37 | response = Net::HTTP.start('localhost', @port) { |http| http.get('/ping') } 38 | response.body == 'OK' 39 | rescue Errno::ECONNREFUSED, Errno::EBADF 40 | false 41 | end 42 | 43 | def next_free_port 44 | server = TCPServer.new('127.0.0.1', 0) 45 | server.addr[1] 46 | ensure 47 | server.close if server 48 | end 49 | end 50 | 51 | attr_reader :players 52 | 53 | def create_player(name, content) 54 | @players ||= [] 55 | player = TestablePlayer.new(name, content) 56 | @players << player 57 | player 58 | end 59 | 60 | def enter_player(player) 61 | post '/players', :name => player.name, :url => player.url 62 | doc = Nokogiri.parse(last_response.body) 63 | personal_page_link = doc.css('a').first 64 | player.personal_page = personal_page_link['href'] 65 | end 66 | 67 | def stub_correct_answer_to_be(correct_answer, points_awarded = 1) 68 | ::ExtremeStartup::AdditionQuestion.class_eval do 69 | define_method(:answered_correctly?) do |actual_answer| 70 | actual_answer.to_s == correct_answer 71 | end 72 | end 73 | 74 | ::ExtremeStartup::AdditionQuestion.class_eval do 75 | define_method(:points) do 76 | points_awarded 77 | end 78 | end 79 | 80 | ::ExtremeStartup::MaximumQuestion.class_eval do 81 | define_method(:answered_correctly?) do |actual_answer| 82 | actual_answer.to_s == correct_answer 83 | end 84 | end 85 | 86 | ::ExtremeStartup::MaximumQuestion.class_eval do 87 | define_method(:points) do 88 | points_awarded 89 | end 90 | end 91 | end 92 | 93 | def score_for(player_name) 94 | visit '/' 95 | find('.player .name', :text => player_name).find(:xpath, '..').find('.points').text.to_i 96 | end 97 | end 98 | 99 | World(TestingApi) 100 | 101 | -------------------------------------------------------------------------------- /lib/extreme_startup/quiz_master.rb: -------------------------------------------------------------------------------- 1 | require_relative 'question_factory' 2 | require 'uri' 3 | require 'bigdecimal' 4 | 5 | module ExtremeStartup 6 | 7 | class RateController 8 | 9 | MIN_REQUEST_INTERVAL_SECS = BigDecimal.new("1") 10 | MAX_REQUEST_INTERVAL_SECS = BigDecimal.new("20") 11 | REQUEST_DELTA_SECS = BigDecimal.new("0.1") 12 | 13 | SLASHDOT_THRESHOLD_SCORE = 2000 14 | 15 | def initialize 16 | @delay = BigDecimal.new("5") 17 | end 18 | 19 | def wait_for_next_request(question) 20 | sleep delay_before_next_request(question) 21 | end 22 | 23 | def delay_before_next_request(question) 24 | if (question.was_answered_correctly) 25 | if (@delay > MIN_REQUEST_INTERVAL_SECS) 26 | @delay = @delay - REQUEST_DELTA_SECS 27 | end 28 | elsif (question.was_answered_wrongly) 29 | if (@delay < MAX_REQUEST_INTERVAL_SECS) 30 | @delay = @delay + REQUEST_DELTA_SECS 31 | end 32 | else 33 | #error response 34 | if (@delay < 10) 35 | @delay = BigDecimal.new("10") 36 | end 37 | return BigDecimal.new("20") 38 | end 39 | @prev_question = question 40 | @delay.to_f 41 | end 42 | 43 | def slashdot_probability_percent 44 | 0.1 45 | end 46 | 47 | def update_algorithm_based_on_score(score) 48 | if (score > SLASHDOT_THRESHOLD_SCORE && (rand(1000) < (10 * slashdot_probability_percent))) 49 | return SlashdotRateController.new 50 | end 51 | self 52 | end 53 | end 54 | 55 | class SlashdotRateController < RateController 56 | 57 | def initialize 58 | @delay = BigDecimal.new("0.02") 59 | end 60 | 61 | def delay_before_next_request(question) 62 | result = @delay.to_f 63 | @delay = @delay * BigDecimal.new("1.022") 64 | result 65 | end 66 | 67 | def update_algorithm_based_on_score(score) 68 | if (@delay > 3.5) 69 | return RateController.new 70 | end 71 | self 72 | end 73 | end 74 | 75 | class QuizMaster 76 | def initialize(player, scoreboard, question_factory, game_state) 77 | @player = player 78 | @scoreboard = scoreboard 79 | @question_factory = question_factory 80 | @game_state = game_state 81 | @rate_controller = RateController.new 82 | end 83 | 84 | def player_passed?(response) 85 | response.to_s.downcase.strip == "pass" 86 | end 87 | 88 | def start 89 | while true 90 | if (@game_state.is_running?) 91 | question = @question_factory.next_question(@player) 92 | question.ask(@player) 93 | puts "For player #{@player}\n#{question.display_result}" 94 | @scoreboard.record_request_for(@player) 95 | @scoreboard.increment_score_for(@player, question) 96 | @rate_controller.wait_for_next_request(question) 97 | @rate_controller = @rate_controller.update_algorithm_based_on_score(@scoreboard.current_score(@player)) 98 | else 99 | sleep 2 100 | end 101 | end 102 | end 103 | 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Welcome 2 | ======= 3 | This is Extreme Startup. This software supports a workshop where teams can compete to build a software product that satisfies market demand. 4 | 5 | NB don't show the players the code for this project until after the workshop as otherwise they can cheat. 6 | 7 | Getting started 8 | --------------- 9 | * Install Ruby 1.9.3 and rubygems 10 | * (For Windows) 11 | * Install [Ruby DevKit](http://rubyinstaller.org/downloads/) 12 | * Extract to (e.g.) c:\devkit 13 | * cd c:\devkit 14 | * ruby dk.rb init 15 | * Edit the file config.yml (Add the locations where ruby is installed e.g. c:\Ruby193) 16 | * ruby dk.rb install 17 | * (For Ubuntu 12.04 onwards) 18 | * Remove existing installation of Ruby and ruby related packages (do not use sudo or Ubuntu Software centre or any other Ubuntu package manager to install Ruby or any of its components) 19 | * Remove rvm and related package from Ubuntu 20 | * Install RVM using the instructions on https://rvm.io/ 21 | * In case RVM is broken it can be fixed by going to http://stackoverflow.com/questions/9056008/installed-ruby-1-9-3-with-rvm-but-command-line-doesnt-show-ruby-v/9056395#9056395 22 | * Install Ruby and Rubygems using RVM only (for Rubygems use: 'rvm rubygems current' or 'rvm rubygems latest') 23 | * See [Installing Nokogiri](http://nokogiri.org/tutorials/installing_nokogiri.html) for installing requirement 24 | * sudo apt-get install libxslt-dev libxml2-dev 25 | * (For Mac (Xcode 5.1 onwards)) 26 | * In the install instructions below you may need to supply an additional argument to ensure that Xcode does not treat an incorrect command line argument as a fatal error when installing Nokogiri. 27 | * The argument is: `ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future` and can be prepended to the install commands. 28 | * Read more here: https://developer.apple.com/library/ios/releasenotes/DeveloperTools/RN-Xcode/Introduction/Introduction.html 29 | 30 | * Install dependencies: 31 | 32 | ```` 33 | cd ../ 34 | gem install bundler 35 | bundle install 36 | ```` 37 | 38 | * Start the game server 39 | 40 | ```` 41 | ruby web_server.rb 42 | ```` 43 | 44 | * Or the docker way 45 | 46 | ```` 47 | #The first time 48 | docker build -t extreme_startup . 49 | docker run -p 3000:3000 extreme_startup 50 | ```` 51 | and for warmup round 52 | ```` 53 | docker run -p 3000:3000 -e WARMUP=1 extreme_startup 54 | ```` 55 | 56 | For more information on How to manage docker read [Docker user guide](https://docs.docker.com/userguide/) 57 | 58 | Notes for facilitators 59 | ---------------------- 60 | 61 | * Run the server on your machine. It's a Sinatra app that by default runs on port 3000. 62 | * Everyone needs a computer connected to the same network, so that they can communicate. Check that everyone can see the leaderboard page served by the webapp running on your machine. Depending on the situation, we have used a local/ad-hoc network and that is ok for the game. 63 | * We have had trouble with things like firewalls, especially on some Windows laptops, so if there are problems, make sure you can ping clients from the server and vice versa. 64 | 65 | * Warmup round: run the web server with the `WARMUP` environment variable set (note that the result of running with `WARMUP=0` is undefined): 66 | 67 | ```` 68 | $ WARMUP=1 ruby web_server.rb 69 | ```` 70 | 71 | * In the warmup round, just make sure that everyone has something technologically working, you just get the same request repeatedly. @bodil has provided some [nice sample players in different languages](https://github.com/steria/extreme_startup_servers). 72 | 73 | * Real game: revert to using the full QuizMaster, and restart the server. This will clear any registered players, but that's ok. 74 | * As the game progresses, you can introduce new question types by moving to the next round. Visit /controlpanel and press the "Advance round" button. Do this when you feel like some of the teams are making good progress in the current round. Typically we've found this to be about every 10 mins. But you can go faster/slower as you like. There are 6 rounds available. 75 | * In case you want to 'stop the world' and reflect with the players 76 | during the game, you can use the "Pause Game" button in /controlpanel. 77 | * Set a time limit so you know when to stop the game, declare the winner, and retrospect. 78 | 79 | 80 | -- Robert Chatley and Matt Wynne 2011. 81 | 82 | People Who've Run Extreme Startup Sessions 83 | ------------------------------------------ 84 | 85 | * http://chatley.com/posts/05-27-2011/extreme-startup/ 86 | * http://johannesbrodwall.com/2011/06/22/real-time-coding-competition-with-extreme-startup/ 87 | * http://www.nilswloka.com/2011/08/17/code-dojo-extreme.html 88 | * http://blog.xebia.fr/2012/07/19/extreme-startup-chez-xebia/ 89 | * https://blog.codecentric.de/en/2015/06/extreme-startup-at-codecentric/ 90 | 91 | If you run this workshop, please write it up on the internet and send us a link to add to this list. 92 | -------------------------------------------------------------------------------- /lib/extreme_startup/web_server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'httparty' 3 | require 'uuid' 4 | require 'haml' 5 | require 'socket' 6 | require 'json' 7 | require_relative 'game_state' 8 | require_relative 'scoreboard' 9 | require_relative 'player' 10 | require_relative 'quiz_master' 11 | 12 | Thread.abort_on_exception = true 13 | 14 | module ExtremeStartup 15 | 16 | class WebServer < Sinatra::Base 17 | 18 | set :port, 3000 19 | set :static, true 20 | set :public, 'public' 21 | set :players, Hash.new 22 | set :players_threads, Hash.new 23 | set :scoreboard, Scoreboard.new(ENV['LENIENT']) 24 | set :question_factory, ENV['WARMUP'] ? WarmupQuestionFactory.new : QuestionFactory.new 25 | set :game_state, GameState.new 26 | 27 | get '/' do 28 | haml :leaderboard, :locals => { 29 | :leaderboard => LeaderBoard.new(scoreboard, players, game_state), 30 | :players => players } 31 | end 32 | 33 | get '/scores' do 34 | LeaderBoard.new(scoreboard, players, game_state).to_json 35 | end 36 | 37 | class LeaderBoard 38 | def initialize(scoreboard, players, game_state) 39 | @entries = [] 40 | scoreboard.leaderboard.each do |entry| 41 | @entries << LeaderBoardEntry.new(entry[0], players[entry[0]], entry[1]) 42 | end 43 | @inplay = game_state.is_running?; 44 | end 45 | 46 | def each(&block) 47 | @entries.each &block 48 | end 49 | 50 | def to_json(*a) 51 | {'entries' => @entries, 'inplay' => @inplay }.to_json(*a) 52 | end 53 | end 54 | 55 | class LeaderBoardEntry 56 | attr_reader :playerid, :playername, :score 57 | def initialize(id, name, score) 58 | @playerid = id; 59 | @playername = name; 60 | @score=score; 61 | end 62 | 63 | def to_json(*a) 64 | { 65 | 'playerid' => playerid, 66 | 'playername' => playername, 67 | 'score' => score 68 | }.to_json(*a) 69 | end 70 | end 71 | 72 | get '/graph' do 73 | haml :scores 74 | end 75 | 76 | get '/controlpanel' do 77 | haml :controlpanel, :locals => { 78 | :game_state => game_state, 79 | :round => question_factory.round.to_s 80 | } 81 | end 82 | 83 | get %r{/players/([\w]+)/metrics/score} do |uuid| 84 | if (players[uuid] == nil) 85 | haml :no_such_player 86 | else 87 | return "#{scoreboard.scores[uuid]}" 88 | end 89 | end 90 | 91 | get %r{/players/([\w]+)/metrics/correct} do |uuid| 92 | if (players[uuid] == nil) 93 | haml :no_such_player 94 | else 95 | return "#{scoreboard.current_total_correct(players[uuid])}" 96 | end 97 | end 98 | 99 | get %r{/players/([\w]+)/metrics/incorrect} do |uuid| 100 | if (players[uuid] == nil) 101 | haml :no_such_player 102 | else 103 | return "#{scoreboard.current_total_not_correct(players[uuid])}" 104 | end 105 | end 106 | 107 | get %r{/players/([\w]+)/metrics/requestcount} do |uuid| 108 | if (players[uuid] == nil) 109 | haml :no_such_player 110 | else 111 | return "#{scoreboard.total_requests_for(players[uuid])}" 112 | end 113 | end 114 | 115 | get %r{/players/([\w]+)/metrics} do |uuid| 116 | haml :metrics_index 117 | end 118 | 119 | get %r{/players/([\w]+)} do |uuid| 120 | if (players[uuid] == nil) 121 | haml :no_such_player 122 | else 123 | haml :personal_page, :locals => { 124 | :name => players[uuid].name, 125 | :playerid => uuid, 126 | :score => scoreboard.scores[uuid], 127 | :log => players[uuid].log[0..25] } 128 | end 129 | end 130 | 131 | get '/players' do 132 | haml :add_player 133 | end 134 | 135 | post '/advance_round' do 136 | question_factory.advance_round.to_s 137 | end 138 | 139 | post '/pause' do 140 | game_state.pause 141 | end 142 | 143 | post '/resume' do 144 | game_state.resume 145 | end 146 | 147 | get %r{/withdraw/([\w]+)} do |uuid| 148 | scoreboard.delete_player(players[uuid]) 149 | players.delete(uuid) 150 | players_threads[uuid].kill 151 | players_threads.delete(uuid) 152 | redirect '/' 153 | end 154 | 155 | post '/players' do 156 | player = Player.new(params) 157 | scoreboard.new_player(player) 158 | players[player.uuid] = player 159 | 160 | player_thread = Thread.new do 161 | QuizMaster.new(player, scoreboard, question_factory, game_state).start 162 | end 163 | players_threads[player.uuid] = player_thread 164 | 165 | haml :player_added, :locals => { :playerid => player.uuid } 166 | end 167 | 168 | private 169 | 170 | def local_ip 171 | UDPSocket.open {|s| s.connect("64.233.187.99", 1); s.addr.last} 172 | end 173 | 174 | [:players, :players_threads, :scoreboard, :question_factory, :game_state].each do |setting| 175 | define_method(setting) do 176 | settings.send(setting) 177 | end 178 | end 179 | 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/extreme_startup/question_factory.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'prime' 3 | 4 | module ExtremeStartup 5 | class Question 6 | class << self 7 | def generate_uuid 8 | @uuid_generator ||= UUID.new 9 | @uuid_generator.generate.to_s[0..7] 10 | end 11 | end 12 | 13 | def ask(player) 14 | url = player.url + '?q=' + URI.escape(self.to_s) 15 | puts "GET: " + url 16 | begin 17 | response = get(url) 18 | if (response.success?) then 19 | self.answer = response.to_s 20 | else 21 | @problem = "error_response" 22 | end 23 | rescue => exception 24 | puts exception 25 | @problem = "no_server_response" 26 | end 27 | end 28 | 29 | def get(url) 30 | HTTParty.get(url) 31 | end 32 | 33 | def result 34 | if @answer && self.answered_correctly?(answer) 35 | "correct" 36 | elsif @answer 37 | "wrong" 38 | else 39 | @problem 40 | end 41 | end 42 | 43 | def delay_before_next 44 | case result 45 | when "correct" then 5 46 | when "wrong" then 10 47 | else 20 48 | end 49 | end 50 | 51 | def was_answered_correctly 52 | result == "correct" 53 | end 54 | 55 | def was_answered_wrongly 56 | result == "wrong" 57 | end 58 | 59 | def display_result 60 | "\tquestion: #{self.to_s}\n\tanswer: #{answer}\n\tresult: #{result}" 61 | end 62 | 63 | def id 64 | @id ||= Question.generate_uuid 65 | end 66 | 67 | def to_s 68 | "#{id}: #{as_text}" 69 | end 70 | 71 | def answer=(answer) 72 | @answer = answer.force_encoding("UTF-8") 73 | end 74 | 75 | def answer 76 | @answer && @answer.downcase.strip 77 | end 78 | 79 | def answered_correctly?(answer) 80 | correct_answer.to_s.downcase.strip == answer 81 | end 82 | 83 | def points 84 | 10 85 | end 86 | end 87 | 88 | class BinaryMathsQuestion < Question 89 | def initialize(player, *numbers) 90 | if numbers.any? 91 | @n1, @n2 = *numbers 92 | else 93 | @n1, @n2 = rand(20), rand(20) 94 | end 95 | end 96 | end 97 | 98 | class TernaryMathsQuestion < Question 99 | def initialize(player, *numbers) 100 | if numbers.any? 101 | @n1, @n2, @n3 = *numbers 102 | else 103 | @n1, @n2, @n3 = rand(20), rand(20), rand(20) 104 | end 105 | end 106 | end 107 | 108 | class SelectFromListOfNumbersQuestion < Question 109 | def initialize(player, *numbers) 110 | if numbers.any? 111 | @numbers = *numbers 112 | else 113 | size = rand(2) 114 | @numbers = random_numbers[0..size].concat(candidate_numbers.shuffle[0..size]).shuffle 115 | end 116 | end 117 | 118 | def random_numbers 119 | randoms = Set.new 120 | loop do 121 | randoms << rand(1000) 122 | return randoms.to_a if randoms.size >= 5 123 | end 124 | end 125 | 126 | def correct_answer 127 | @numbers.select do |x| 128 | should_be_selected(x) 129 | end.join(', ') 130 | end 131 | end 132 | 133 | class MaximumQuestion < SelectFromListOfNumbersQuestion 134 | def as_text 135 | "which of the following numbers is the largest: " + @numbers.join(', ') 136 | end 137 | def points 138 | 40 139 | end 140 | private 141 | def should_be_selected(x) 142 | x == @numbers.max 143 | end 144 | 145 | def candidate_numbers 146 | (1..100).to_a 147 | end 148 | end 149 | 150 | class AdditionQuestion < BinaryMathsQuestion 151 | def as_text 152 | "what is #{@n1} plus #{@n2}" 153 | end 154 | private 155 | def correct_answer 156 | @n1 + @n2 157 | end 158 | end 159 | 160 | class SubtractionQuestion < BinaryMathsQuestion 161 | def as_text 162 | "what is #{@n1} minus #{@n2}" 163 | end 164 | private 165 | def correct_answer 166 | @n1 - @n2 167 | end 168 | end 169 | 170 | class MultiplicationQuestion < BinaryMathsQuestion 171 | def as_text 172 | "what is #{@n1} multiplied by #{@n2}" 173 | end 174 | private 175 | def correct_answer 176 | @n1 * @n2 177 | end 178 | end 179 | 180 | class AdditionAdditionQuestion < TernaryMathsQuestion 181 | def as_text 182 | "what is #{@n1} plus #{@n2} plus #{@n3}" 183 | end 184 | def points 185 | 30 186 | end 187 | private 188 | def correct_answer 189 | @n1 + @n2 + @n3 190 | end 191 | end 192 | 193 | class AdditionMultiplicationQuestion < TernaryMathsQuestion 194 | def as_text 195 | "what is #{@n1} plus #{@n2} multiplied by #{@n3}" 196 | end 197 | def points 198 | 60 199 | end 200 | private 201 | def correct_answer 202 | @n1 + @n2 * @n3 203 | end 204 | end 205 | 206 | class MultiplicationAdditionQuestion < TernaryMathsQuestion 207 | def as_text 208 | "what is #{@n1} multiplied by #{@n2} plus #{@n3}" 209 | end 210 | def points 211 | 50 212 | end 213 | private 214 | def correct_answer 215 | @n1 * @n2 + @n3 216 | end 217 | end 218 | 219 | class PowerQuestion < BinaryMathsQuestion 220 | def as_text 221 | "what is #{@n1} to the power of #{@n2}" 222 | end 223 | def points 224 | 20 225 | end 226 | private 227 | def correct_answer 228 | @n1 ** @n2 229 | end 230 | end 231 | 232 | class SquareCubeQuestion < SelectFromListOfNumbersQuestion 233 | def as_text 234 | "which of the following numbers is both a square and a cube: " + @numbers.join(', ') 235 | end 236 | def points 237 | 60 238 | end 239 | private 240 | def should_be_selected(x) 241 | is_square(x) and is_cube(x) 242 | end 243 | 244 | def candidate_numbers 245 | square_cubes = (1..100).map { |x| x ** 3 }.select{ |x| is_square(x) } 246 | squares = (1..50).map { |x| x ** 2 } 247 | square_cubes.concat(squares) 248 | end 249 | 250 | def is_square(x) 251 | if (x ==0) 252 | return true 253 | end 254 | (x % (Math.sqrt(x).round(4))) == 0 255 | end 256 | 257 | def is_cube(x) 258 | if (x ==0) 259 | return true 260 | end 261 | (x % (Math.cbrt(x).round(4))) == 0 262 | end 263 | end 264 | 265 | class PrimesQuestion < SelectFromListOfNumbersQuestion 266 | def as_text 267 | "which of the following numbers are primes: " + @numbers.join(', ') 268 | end 269 | def points 270 | 60 271 | end 272 | private 273 | def should_be_selected(x) 274 | Prime.prime? x 275 | end 276 | 277 | def candidate_numbers 278 | Prime.take(100) 279 | end 280 | end 281 | 282 | class FibonacciQuestion < BinaryMathsQuestion 283 | def as_text 284 | n = @n1 + 4 285 | if (n > 20 && n % 10 == 1) 286 | return "what is the #{n}st number in the Fibonacci sequence" 287 | end 288 | if (n > 20 && n % 10 == 2) 289 | return "what is the #{n}nd number in the Fibonacci sequence" 290 | end 291 | return "what is the #{n}th number in the Fibonacci sequence" 292 | end 293 | def points 294 | 50 295 | end 296 | private 297 | def correct_answer 298 | n = @n1 + 4 299 | a, b = 0, 1 300 | n.times { a, b = b, a + b } 301 | a 302 | end 303 | end 304 | 305 | class GeneralKnowledgeQuestion < Question 306 | class << self 307 | def question_bank 308 | [ 309 | ["who is the Prime Minister of Great Britain", "David Cameron"], 310 | ["which city is the Eiffel tower in", "Paris"], 311 | ["what currency did Spain use before the Euro", "peseta"], 312 | ["what colour is a banana", "yellow"], 313 | ["who played James Bond in the film Dr No", "Sean Connery"] 314 | ] 315 | end 316 | end 317 | 318 | def initialize(player) 319 | question = GeneralKnowledgeQuestion.question_bank.sample 320 | @question = question[0] 321 | @correct_answer = question[1] 322 | end 323 | 324 | def as_text 325 | @question 326 | end 327 | 328 | def correct_answer 329 | @correct_answer 330 | end 331 | end 332 | 333 | require 'yaml' 334 | class AnagramQuestion < Question 335 | def as_text 336 | possible_words = [@anagram["correct"]] + @anagram["incorrect"] 337 | %Q{which of the following is an anagram of "#{@anagram["anagram"]}": #{possible_words.shuffle.join(", ")}} 338 | end 339 | 340 | def initialize(player, *words) 341 | if words.any? 342 | @anagram = {} 343 | @anagram["anagram"], @anagram["correct"], *@anagram["incorrect"] = words 344 | else 345 | anagrams = YAML.load_file(File.join(File.dirname(__FILE__), "anagrams.yaml")) 346 | @anagram = anagrams.sample 347 | end 348 | end 349 | 350 | def correct_answer 351 | @anagram["correct"] 352 | end 353 | end 354 | 355 | class ScrabbleQuestion < Question 356 | def as_text 357 | "what is the english scrabble score of #{@word}" 358 | end 359 | 360 | def initialize(player, word=nil) 361 | if word 362 | @word = word 363 | else 364 | @word = ["banana", "september", "cloud", "zoo", "ruby", "buzzword"].sample 365 | end 366 | end 367 | 368 | def correct_answer 369 | @word.chars.inject(0) do |score, letter| 370 | score += scrabble_scores[letter.downcase] 371 | end 372 | end 373 | 374 | private 375 | 376 | def scrabble_scores 377 | scores = {} 378 | %w{e a i o n r t l s u}.each {|l| scores[l] = 1 } 379 | %w{d g}.each {|l| scores[l] = 2 } 380 | %w{b c m p}.each {|l| scores[l] = 3 } 381 | %w{f h v w y}.each {|l| scores[l] = 4 } 382 | %w{k}.each {|l| scores[l] = 5 } 383 | %w{j x}.each {|l| scores[l] = 8 } 384 | %w{q z}.each {|l| scores[l] = 10 } 385 | scores 386 | end 387 | end 388 | 389 | class QuestionFactory 390 | attr_reader :round 391 | 392 | def initialize 393 | @round = 1 394 | @question_types = [ 395 | AdditionQuestion, 396 | MaximumQuestion, 397 | MultiplicationQuestion, 398 | SquareCubeQuestion, 399 | GeneralKnowledgeQuestion, 400 | PrimesQuestion, 401 | SubtractionQuestion, 402 | FibonacciQuestion, 403 | PowerQuestion, 404 | AdditionAdditionQuestion, 405 | AdditionMultiplicationQuestion, 406 | MultiplicationAdditionQuestion, 407 | AnagramQuestion, 408 | ScrabbleQuestion 409 | ] 410 | end 411 | 412 | def next_question(player) 413 | window_end = (@round * 2 - 1) 414 | window_start = [0, window_end - 4].max 415 | available_question_types = @question_types[window_start..window_end] 416 | available_question_types.sample.new(player) 417 | end 418 | 419 | def advance_round 420 | @round += 1 421 | end 422 | 423 | end 424 | 425 | class WarmupQuestion < Question 426 | def initialize(player) 427 | @player = player 428 | end 429 | 430 | def correct_answer 431 | @player.name 432 | end 433 | 434 | def as_text 435 | "what is your name" 436 | end 437 | end 438 | 439 | class WarmupQuestionFactory 440 | def next_question(player) 441 | WarmupQuestion.new(player) 442 | end 443 | 444 | def advance_round 445 | raise("please just restart the server") 446 | end 447 | end 448 | 449 | end 450 | -------------------------------------------------------------------------------- /public/js/smoothie.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2011, Joe Walnes 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * Smoothie Charts - http://smoothiecharts.org/ 25 | * (c) 2010, Joe Walnes 26 | * 27 | * v1.0: Main charting library, by Joe Walnes 28 | * v1.1: Auto scaling of axis, by Neil Dunn 29 | * v1.2: fps (frames per second) option, by Mathias Petterson 30 | * v1.3: Fix for divide by zero, by Paul Nikitochkin 31 | * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds 32 | * v1.5: Set default frames per second to 50... smoother. 33 | * .start(), .stop() methods for conserving CPU, by Dmitry Vyal 34 | * options.iterpolation = 'bezier' or 'line', by Dmitry Vyal 35 | * options.maxValue to fix scale, by Dmitry Vyal 36 | */ 37 | 38 | function TimeSeries(options) { 39 | options = options || {}; 40 | options.resetBoundsInterval = options.resetBoundsInterval || 3000; // Reset the max/min bounds after this many milliseconds 41 | options.resetBounds = options.resetBounds || true; // Enable or disable the resetBounds timer 42 | this.options = options; 43 | this.data = []; 44 | 45 | this.maxValue = Number.NaN; // The maximum value ever seen in this time series. 46 | this.minValue = Number.NaN; // The minimum value ever seen in this time series. 47 | 48 | // Start a resetBounds Interval timer desired 49 | if (options.resetBounds) { 50 | this.boundsTimer = setInterval(function(thisObj) { thisObj.resetBounds(); }, options.resetBoundsInterval, this); 51 | } 52 | } 53 | 54 | // Reset the min and max for this timeseries so the graph rescales itself 55 | TimeSeries.prototype.resetBounds = function() { 56 | this.maxValue = Number.NaN; 57 | this.minValue = Number.NaN; 58 | for (var i = 0; i < this.data.length; i++) { 59 | this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, this.data[i][1]) : this.data[i][1]; 60 | this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, this.data[i][1]) : this.data[i][1]; 61 | } 62 | }; 63 | 64 | TimeSeries.prototype.append = function(timestamp, value) { 65 | this.data.push([timestamp, value]); 66 | this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, value) : value; 67 | this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, value) : value; 68 | }; 69 | 70 | function SmoothieChart(options) { 71 | // Defaults 72 | options = options || {}; 73 | options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 }; 74 | options.millisPerPixel = options.millisPerPixel || 20; 75 | options.fps = options.fps || 50; 76 | options.maxValueScale = options.maxValueScale || 1; 77 | options.minValue = options.minValue; 78 | options.maxValue = options.maxValue; 79 | options.labels = options.labels || { fillStyle:'#ffffff' }; 80 | options.interpolation = options.interpolation || "bezier"; 81 | this.options = options; 82 | this.seriesSet = []; 83 | } 84 | 85 | SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { 86 | this.seriesSet.push({timeSeries: timeSeries, options: options || {}}); 87 | }; 88 | 89 | SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { 90 | this.seriesSet.splice(this.seriesSet.indexOf(timeSeries), 1); 91 | }; 92 | 93 | SmoothieChart.prototype.streamTo = function(canvas, delay) { 94 | var self = this; 95 | this.render_on_tick = function() { 96 | self.render(canvas, new Date().getTime() - (delay || 0)); 97 | }; 98 | 99 | this.start(); 100 | }; 101 | 102 | SmoothieChart.prototype.start = function() { 103 | if (!this.timer) 104 | this.timer = setInterval(this.render_on_tick, 1000/this.options.fps); 105 | }; 106 | 107 | SmoothieChart.prototype.stop = function() { 108 | if (this.timer) { 109 | clearInterval(this.timer); 110 | this.timer = undefined; 111 | } 112 | }; 113 | 114 | SmoothieChart.prototype.render = function(canvas, time) { 115 | var canvasContext = canvas.getContext("2d"); 116 | var options = this.options; 117 | var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight}; 118 | 119 | // Save the state of the canvas context, any transformations applied in this method 120 | // will get removed from the stack at the end of this method when .restore() is called. 121 | canvasContext.save(); 122 | 123 | // Round time down to pixel granularity, so motion appears smoother. 124 | time = time - time % options.millisPerPixel; 125 | 126 | // Move the origin. 127 | canvasContext.translate(dimensions.left, dimensions.top); 128 | 129 | // Create a clipped rectangle - anything we draw will be constrained to this rectangle. 130 | // This prevents the occasional pixels from curves near the edges overrunning and creating 131 | // screen cheese (that phrase should neeed no explanation). 132 | canvasContext.beginPath(); 133 | canvasContext.rect(0, 0, dimensions.width, dimensions.height); 134 | canvasContext.clip(); 135 | 136 | // Clear the working area. 137 | canvasContext.save(); 138 | canvasContext.fillStyle = options.grid.fillStyle; 139 | canvasContext.fillRect(0, 0, dimensions.width, dimensions.height); 140 | canvasContext.restore(); 141 | 142 | // Grid lines.... 143 | canvasContext.save(); 144 | canvasContext.lineWidth = options.grid.lineWidth || 1; 145 | canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff'; 146 | // Vertical (time) dividers. 147 | if (options.grid.millisPerLine > 0) { 148 | for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) { 149 | canvasContext.beginPath(); 150 | var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel)); 151 | canvasContext.moveTo(gx, 0); 152 | canvasContext.lineTo(gx, dimensions.height); 153 | canvasContext.stroke(); 154 | canvasContext.closePath(); 155 | } 156 | } 157 | 158 | // Horizontal (value) dividers. 159 | for (var v = 1; v < options.grid.verticalSections; v++) { 160 | var gy = Math.round(v * dimensions.height / options.grid.verticalSections); 161 | canvasContext.beginPath(); 162 | canvasContext.moveTo(0, gy); 163 | canvasContext.lineTo(dimensions.width, gy); 164 | canvasContext.stroke(); 165 | canvasContext.closePath(); 166 | } 167 | // Bounding rectangle. 168 | canvasContext.beginPath(); 169 | canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height); 170 | canvasContext.closePath(); 171 | canvasContext.restore(); 172 | 173 | // Calculate the current scale of the chart, from all time series. 174 | var maxValue = Number.NaN; 175 | var minValue = Number.NaN; 176 | 177 | for (var d = 0; d < this.seriesSet.length; d++) { 178 | // TODO(ndunn): We could calculate / track these values as they stream in. 179 | var timeSeries = this.seriesSet[d].timeSeries; 180 | if (!isNaN(timeSeries.maxValue)) { 181 | maxValue = !isNaN(maxValue) ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue; 182 | } 183 | 184 | if (!isNaN(timeSeries.minValue)) { 185 | minValue = !isNaN(minValue) ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue; 186 | } 187 | } 188 | 189 | if (isNaN(maxValue) && isNaN(minValue)) { 190 | return; 191 | } 192 | 193 | // Scale the maxValue to add padding at the top if required 194 | if (options.maxValue != null) 195 | maxValue = options.maxValue; 196 | else 197 | maxValue = maxValue * options.maxValueScale; 198 | // Set the minimum if we've specified one 199 | if (options.minValue != null) 200 | minValue = options.minValue; 201 | var valueRange = maxValue - minValue; 202 | 203 | // For each data set... 204 | for (var d = 0; d < this.seriesSet.length; d++) { 205 | canvasContext.save(); 206 | var timeSeries = this.seriesSet[d].timeSeries; 207 | var dataSet = timeSeries.data; 208 | var seriesOptions = this.seriesSet[d].options; 209 | 210 | // Delete old data that's moved off the left of the chart. 211 | // We must always keep the last expired data point as we need this to draw the 212 | // line that comes into the chart, but any points prior to that can be removed. 213 | while (dataSet.length >= 2 && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) { 214 | dataSet.splice(0, 1); 215 | } 216 | 217 | // Set style for this dataSet. 218 | canvasContext.lineWidth = seriesOptions.lineWidth || 1; 219 | canvasContext.fillStyle = seriesOptions.fillStyle; 220 | canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff'; 221 | // Draw the line... 222 | canvasContext.beginPath(); 223 | // Retain lastX, lastY for calculating the control points of bezier curves. 224 | var firstX = 0, lastX = 0, lastY = 0; 225 | for (var i = 0; i < dataSet.length; i++) { 226 | // TODO: Deal with dataSet.length < 2. 227 | var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel)); 228 | var value = dataSet[i][1]; 229 | var offset = maxValue - value; 230 | var scaledValue = valueRange ? Math.round((offset / valueRange) * dimensions.height) : 0; 231 | var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart. 232 | 233 | if (i == 0) { 234 | firstX = x; 235 | canvasContext.moveTo(x, y); 236 | } 237 | // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/B�zier_curve#Quadratic_curves 238 | // 239 | // Assuming A was the last point in the line plotted and B is the new point, 240 | // we draw a curve with control points P and Q as below. 241 | // 242 | // A---P 243 | // | 244 | // | 245 | // | 246 | // Q---B 247 | // 248 | // Importantly, A and P are at the same y coordinate, as are B and Q. This is 249 | // so adjacent curves appear to flow as one. 250 | // 251 | else { 252 | switch (options.interpolation) { 253 | case "line": 254 | canvasContext.lineTo(x,y); 255 | break; 256 | case "bezier": 257 | default: 258 | canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop 259 | Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) 260 | Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) 261 | x, y); // endPoint (B) 262 | break; 263 | } 264 | } 265 | 266 | lastX = x, lastY = y; 267 | } 268 | if (dataSet.length > 0 && seriesOptions.fillStyle) { 269 | // Close up the fill region. 270 | canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); 271 | canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); 272 | canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); 273 | canvasContext.fill(); 274 | } 275 | canvasContext.stroke(); 276 | canvasContext.closePath(); 277 | canvasContext.restore(); 278 | } 279 | 280 | // Draw the axis values on the chart. 281 | if (!options.labels.disabled) { 282 | canvasContext.fillStyle = options.labels.fillStyle; 283 | var maxValueString = maxValue.toFixed(2); 284 | var minValueString = minValue.toFixed(2); 285 | canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10); 286 | canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2); 287 | } 288 | 289 | canvasContext.restore(); // See .save() above. 290 | } -------------------------------------------------------------------------------- /public/js/jquery-1.6.2.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery JavaScript Library v1.6.2 3 | * http://jquery.com/ 4 | * 5 | * Copyright 2011, John Resig 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://jquery.org/license 8 | * 9 | * Includes Sizzle.js 10 | * http://sizzlejs.com/ 11 | * Copyright 2011, The Dojo Foundation 12 | * Released under the MIT, BSD, and GPL Licenses. 13 | * 14 | * Date: Thu Jun 30 14:16:56 2011 -0400 15 | */ 16 | (function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"":"")+""),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z])/ig,x=function(a,b){return b.toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!A){A=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||D.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c
    a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0},m&&f.extend(p,{position:"absolute",left:-1e3,top:-1e3});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
    ",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
    t
    ",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]||i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=w:v&&c!=="className"&&(f.nodeName(a,"form")||u.test(c))&&(i=v)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}},value:{get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return f.prop(a,c)?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.attrHooks.title=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i. 17 | shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function(c){var d=c.target,e,g;if(!!y.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=I(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

    ";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
    ";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/",""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]};bf.optgroup=bf.option,bf.tbody=bf.tfoot=bf.colgroup=bf.caption=bf.thead,bf.th=bf.td,f.support.htmlSerialize||(bf._default=[1,"div
    ","
    "]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!bf[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j 18 | )}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bi(a,d),e=bj(a),g=bj(d);for(h=0;e[h];++h)bi(e[h],g[h])}if(b){bh(a,d);if(c){e=bj(a),g=bj(d);for(h=0;e[h];++h)bh(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!ba.test(k))k=b.createTextNode(k);else{k=k.replace(Z,"<$1>");var l=($.exec(k)||["",""])[1].toLowerCase(),m=bf[l]||bf._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=_.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Y.test(k)&&o.insertBefore(b.createTextNode(Y.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bo.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bn.test(g)?g.replace(bn,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bB=/%20/g,bC=/\[\]$/,bD=/\r?\n/g,bE=/#.*$/,bF=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bG=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bH=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bI=/^(?:GET|HEAD)$/,bJ=/^\/\//,bK=/\?/,bL=/)<[^<]*)*<\/script>/gi,bM=/^(?:select|textarea)/i,bN=/\s+/,bO=/([?&])_=[^&]*/,bP=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bQ=f.fn.load,bR={},bS={},bT,bU;try{bT=e.href}catch(bV){bT=c.createElement("a"),bT.href="",bT=bT.href}bU=bP.exec(bT.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bQ)return bQ.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
    ").append(c.replace(bL,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bM.test(this.nodeName)||bG.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bD,"\r\n")}}):{name:b.name,value:c.replace(bD,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bT,isLocal:bH.test(bU[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bW(bR),ajaxTransport:bW(bS),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?bZ(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b$(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bF.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bE,"").replace(bJ,bU[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bN),d.crossDomain==null&&(r=bP.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bU[1]&&r[2]==bU[2]&&(r[3]||(r[1]==="http:"?80:443))==(bU[3]||(bU[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bX(bR,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bI.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bK.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bO,"$1_="+x);d.url=y+(y===d.url?(bK.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bX(bS,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bB,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn,co=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cr("show",3),a,b,c);for(var g=0,h=this.length;g=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b
    ";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cu.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cu.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cv(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cv(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); --------------------------------------------------------------------------------