').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="