diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/sampling/README.md | 50 | ||||
-rw-r--r-- | lib/sampling/double_blind.rb.bak | 35 | ||||
-rw-r--r-- | lib/sampling/manual.html.erb | 31 | ||||
-rw-r--r-- | lib/sampling/manual.rb | 62 | ||||
-rw-r--r-- | lib/sampling/peer_review.html.erb | 28 | ||||
-rw-r--r-- | lib/sampling/peer_review.rb | 91 | ||||
-rw-r--r-- | lib/sampling/riot_api.rb | 207 | ||||
-rw-r--r-- | lib/scheduling/README.md | 22 | ||||
-rw-r--r-- | lib/scheduling/elimination.rb | 146 | ||||
-rw-r--r-- | lib/scheduling/round_robin.rb | 70 | ||||
-rw-r--r-- | lib/scoring/README.md | 15 | ||||
-rw-r--r-- | lib/scoring/fibonacci_peer_with_blowout.rb | 33 | ||||
-rw-r--r-- | lib/scoring/marginal_peer.rb | 16 | ||||
-rw-r--r-- | lib/scoring/winner_takes_all.rb | 16 | ||||
-rw-r--r-- | lib/seeding/.keep | 0 | ||||
-rw-r--r-- | lib/seeding/README.md | 10 | ||||
-rw-r--r-- | lib/seeding/early_bird_seeding.rb | 20 | ||||
-rw-r--r-- | lib/seeding/fair_ranked_seeding.rb | 43 | ||||
-rw-r--r-- | lib/seeding/random_seeding.rb | 20 | ||||
-rw-r--r-- | lib/throttled_api_request.rb | 32 |
20 files changed, 947 insertions, 0 deletions
diff --git a/lib/sampling/README.md b/lib/sampling/README.md new file mode 100644 index 0000000..e4b3fbf --- /dev/null +++ b/lib/sampling/README.md @@ -0,0 +1,50 @@ +Sampling interface +================== + +Files in this directory should be _classes_ implementing the following +interface: + + - `self.works_with?(Game) => Boolean` + + Returns whether or not this sampling method works with the + specified game. + + - `self.can_get?(String setting_name) => Fixnum` + + Returns whether or not this sampling method can get a specifed + statistic; 0 means 'false', positive integers mean 'true', where + higher numbers are higher priority. + + - `self.uses_remote?() => Boolean` + + Return whether or not this sampling method requires remote IDs for + users. + + - `self.set_remote_name(User, Game, String)` + + Set the remote ID for a user for the specified game. It is safe to + assume that this sampling method `works_with?` that game. + + - `self.get_remote_name(Object)` + + When given an object from `RemoteUsername#value`, give back a + human-readable/editable name to display + +---- + + - `initialize(Match)` + + Construct new Sampling object for the specified match. + + - `start()` + + Begin fetching the statistics. + + - `render_user_interaction(User) => String` + + Returns HTML to render on a page. + + - `handle_user_interaction(User, Hash params)` + + Handles params from the form generated by + `#user_interaction_render`. diff --git a/lib/sampling/double_blind.rb.bak b/lib/sampling/double_blind.rb.bak new file mode 100644 index 0000000..6a30d57 --- /dev/null +++ b/lib/sampling/double_blind.rb.bak @@ -0,0 +1,35 @@ +module Sampling + module DoubleBlind + def self.works_with?(game) + return true + end + + def can_get?(setting_name) + return 1 + end + + def self.uses_remote? + return false + end + + def self.set_remote_name(user, game, value) + raise "This sampling method doesn't use remote usernames." + end + + def self.get_remote_name(value) + raise "This sampling method doesn't use remote usernames." + end + + def self.sampling_start(match, statistics) + # TODO + end + + def self.render_user_interaction(match, user) + # TODO + end + + def self.handle_user_interaction(match, user, sampling_params) + # TODO + end + end +end diff --git a/lib/sampling/manual.html.erb b/lib/sampling/manual.html.erb new file mode 100644 index 0000000..2bbd6da --- /dev/null +++ b/lib/sampling/manual.html.erb @@ -0,0 +1,31 @@ +<% if @tournament.hosts.include? @current_user %> + <fieldset><legend>Winner</legend> + <ul> + <% @match.teams.each do |team| %> + <li><label> + <input type="radio" name="manual[winner]" value="<%= team.id %>"> + Team <%= team.id %> + </label></li> + <% end %> + </ul> + </fieldset> + <% @match.teams.each do |team| %> + <fieldset><legend>Statistics for Team <%= team.id %></legend> + <% team.users.each do |user| %> + <fieldset><legend><%= user.name %></legend> + <% @stats.reject{|s|s=="win"}.each do |stat| %> + <p> + <label> + <%= stat.titleize %> + <input type="numeric" name="manual[statistics][<%= user.id %>][<%= stat %>]"> + </label> + </p> + <% end %> + </fieldset> + <% end %> + </fieldset> + <% end %> + <input type="submit", value="Finish match" > +<% else %> + <p>The match is running; the host has yet to post the scores of the match.</p> +<% end %> diff --git a/lib/sampling/manual.rb b/lib/sampling/manual.rb new file mode 100644 index 0000000..853516c --- /dev/null +++ b/lib/sampling/manual.rb @@ -0,0 +1,62 @@ +module Sampling + class Manual + def self.works_with?(game) + return true + end + + def self.can_get?(setting_name) + return 1 + end + + def self.uses_remote? + return false + end + + def self.set_remote_name(user, game, value) + raise "This sampling method doesn't use remote usernames." + end + + def self.get_remote_name(value) + raise "This sampling method doesn't use remote usernames." + end + + #### + + def initialize(match) + @match = match + end + + def start + # do nothing + end + + def render_user_interaction(user) + @tournament = @match.tournament_stage.tournament + @current_user = user + @stats = @match.stats_from(self.class) + + require 'erb' + erb_filename = File.join(__FILE__.sub(/\.rb$/, '.html.erb')) + erb = ERB.new(File.read(erb_filename)) + erb.filename = erb_filename + return erb.result(binding).html_safe + end + + def handle_user_interaction(user, params) + # => Save sampling_params as statistics + if (@match.tournament_stage.tournament.hosts.include? user) + manual_params = params.require(:manual) + winner = Team.find(manual_params[:winner]) + @match.users.each do |user| + Statistic.create(match: @match, user: user, + name: "win", value: winner.users.include?(user)) + @match.stats_from(self.class).reject{|s|s=="win"}.each do |stat| + Statistic.create(match: @match, user: user, + name: stat, value: manual_params[:statistics][user.id][stat].to_i) + end # stats + end # users + end # permission + end # def + + end +end diff --git a/lib/sampling/peer_review.html.erb b/lib/sampling/peer_review.html.erb new file mode 100644 index 0000000..a0b9c4d --- /dev/null +++ b/lib/sampling/peer_review.html.erb @@ -0,0 +1,28 @@ +<% if @feedbacks_missing.include? @user %> + <script type="text/javascript"> + function score_peers() { + var list = $('ol#peer_review_boxes'); + for(var i=0, var len=list.length; i < len; i++) { + if ( i == len-1) { + comma = ""; + } + $('peer_review').value += $('ol#peer_review_boxes:eq(' + i + ')').text() + comma; + } + } + </script> + <input type="hidden" id="peer_review" name="peer_review" value="" /> + <ol id="peer_review_boxes" class="sortable"> + <% @team.users.reject{|u|u==@user}.each do |user| %><li> + <%= user.user_name %> + <br> + <%# TODO: display more statistics %> + </li><% end %> + </ol> + <input type="submit" value="Submit peer evaluation", onsubmit="score_peers()") > +<% else %> + <p>Still waiting for peer feedback from the following users: + <ul><% @feedbacks_missing.each do |user| %> + <li><%= link_to user %></li> + <% end %></ul> + </p> +<% end %> diff --git a/lib/sampling/peer_review.rb b/lib/sampling/peer_review.rb new file mode 100644 index 0000000..7faa241 --- /dev/null +++ b/lib/sampling/peer_review.rb @@ -0,0 +1,91 @@ +module Sampling + class PeerReview + def self.works_with?(game) + return true + end + + def self.can_get?(setting_name) + return setting_name.start_with?("review_from_") ? 2 : 0 + end + + def self.uses_remote? + return false + end + + def self.set_remote_name(user, game, value) + raise "This sampling method doesn't use remote usernames." + end + + def self.get_remote_name(value) + raise "This sampling method doesn't use remote usernames." + end + + #### + + def initialize(match) + @match = match + end + + def start + # do nothing + end + + def render_user_interaction(user) + @user = user + @team = get_team(match) + @reviews_missing = get_reviews_missing(match) + + require 'erb' + erb_filename = File.join(__FILE__.sub(/\.rb$/, '.html.erb')) + erb = ERB.new(File.read(erb_filename)) + erb.filename = erb_filename + return erb.result(binding).html_safe + end + + def handle_user_interaction(reviewing_user, params) + i = 0 + params[:peer_review].to_s.split(',').each do |user_name| + reviewed_user = User.find_by_user_name(user_name) + reviewed_user.statistics.create(match: @match, name: "review_from_#{reviewing_user.user_name}", value: i) + i += 1 + end + end + + private + + def self.get_users(match) + users = [] + match.teams.each{|t| users.concat(t.users)} + return users + end + + def self.get_team(match) + match.teams.find{|t|t.users.include?(@user)} + end + + def self.get_reviews(match) + ret = {} + match.statistiscs.where("'name' LIKE 'review_from_%'").each do |statistic| + ret[statistic.user] ||= {} + ret[statistic.user][User.find_by_user_name(statistic.name.sub(/^review_from_/,''))] = statistic.value + end + return ret + end + + def self.get_reviews_missing(match) + require 'set' + ret = Set.new + + review = get_reviews(match) + users = get_users(match) + + review.each do |review| + (users - review.keys).each do |user| + ret.add(user) + end + end + + return ret + end + end +end diff --git a/lib/sampling/riot_api.rb b/lib/sampling/riot_api.rb new file mode 100644 index 0000000..4e72f91 --- /dev/null +++ b/lib/sampling/riot_api.rb @@ -0,0 +1,207 @@ +module Sampling + class RiotApi + protected + def self.api_name + "prod.api.pvp.net/api/lol" + end + + protected + def self.api_key + ENV["RIOT_API_KEY"] + end + + protected + def self.region + ENV["RIOT_API_REGION"] + end + + protected + def self.url(request, args={}) + "https://prod.api.pvp.net/api/lol/#{region}/#{request % args.merge(args){|k,v|url_escape(v)}}?api_key=#{api_key}" + end + + protected + def self.url_escape(string) + URI::escape(string.to_s, /[^a-zA-Z0-9._~!$&'()*+,;=:@-]/) + end + + protected + def self.standardize(summoner_name) + summoner_name.to_s.downcase.gsub(' ', '') + end + + protected + def self.stats_available + ["win", "numDeaths", "turretsKilled", "championsKilled", "minionsKilled", "assists"] + end + + protected + class Job < ThrottledApiRequest + def initialize(request, args={}) + @url = Sampling::RiotApi::url(request, args) + limits = [ + {:unit_time => 10.seconds, :requests_per => 10}, + {:unit_time => 10.minutes, :requests_per => 500}, + ] + super(RiotApi::api_name, limits) + end + + def perform + response = open(@url) + status = response.status + data = JSON::restore(response.read) + + # Error codes that RIOT uses: + # "400"=>"Bad request" + # "401"=>"Unauthorized" + # "429"=>"Rate limit exceeded" + # "500"=>"Internal server error" + # "503"=>"Service unavailable" + # "404"=>"Not found" + # Should probably handle these better + if status[0] != "200" + raise "GET #{@url} => #{status.join(" ")}" + end + return self.handle(data) + end + + def handle(data) + return true + end + end + + ######################################################################## + + ## + # Return whether or not this sampling method works with the specified game. + # Spoiler: It only works with League of Legends (or subclasses of it). + public + def self.works_with?(game) + if api_key.nil? or region.nil? + return false + end + if game.name == "League of Legends" + return true + end + unless game.parent.nil? + return works_with?(game.parent) + end + end + + ## + # Return whether or not the API can get a given statistic for + # a given user. + public + def self.can_get?(stat) + if stats_available.include?(stat) + return 2 + else + return 0 + end + end + + ## + # This sampling method uses remote IDs + public + def self.uses_remote? + return true + end + + ## + # When given a summoner name for a user, figure out the summoner ID. + public + def self.set_remote_name(user, game, summoner_name) + Delayed::Job.enqueue(UsernameJob.new(user, game, summoner_name), :queue => RiotApi::api_name) + end + protected + class UsernameJob < Job + def initialize(user, game, summoner_name) + @user_id = user.id + @game_id = game.id + # Escape any funny stuff + summoner_names = [summoner_name].map{|name|Sampling::RiotApi::standardize(name.gsub(',',''))} + # Generate the request + super("v1.3/summoner/by-name/%{summonerNames}", { :summonerNames => summoner_names.join(",") }) + end + def handle(data) + user = User.find(@user_id) + game = Game.find(@game_id) + + standardized_summoner_name = data.keys.first + remote_data = { + :id => data[standardized_summoner_name]["id"], + :name => data[standardized_summoner_name]["name"], + } + + user.set_remote_username(game, remote_data) + end + end + + ## + # When given data from RemoteUsername#value, give back a readable name to display. + # Here, this is the summoner name. + public + def self.get_remote_name(data) + data["name"] + end + + #### + + public + def initialize(match) + @match = match + end + + ## + # Fetch all the statistics for a match. + public + def start + @match.teams.each do |team| + team.users.each do |user| + #For demo purposes, we are hard coding in a league of legends game id. + Delayed::Job.enqueue(FetchStatisticsJob.new(user, @match, @match.stats_from(self.class), 10546), :queue => RiotApi::api_name) + end + end + end + protected + class FetchStatisticsJob < Job + def initialize(user, match, stats, last_game_id) + @user_id = user.id + @match_id = match.id + @stats = stats + @last_game_id = last_game_id + + # Get the summoner id + summoner = user.get_remote_username(match.tournament_stage.tournament.game) + # Generate the request + super("v1.3/game/by-summoner/%{summonerId}/recent", { :summonerId => summoner["id"] }) + end + def handle(data) + user = User.find(@user_id) + match = Match.find(@match_id) + if @last_game_id.nil? + Delayed::Job.enqueue(FetchStatisticsJob.new(user, match, data["games"][0]["gameId"]), :queue => RiotApi::api_name) + else + if @last_game_id == data["games"][0]["gameId"] + sleep(4.minutes) + Delayed::Job.enqueue(FetchStatisticsJob.new(user, match, @last_game_id), :queue => RiotApi::api_name) + else + @stats.each do |stat| + Statistic.create(user: user, match: match, name: stat, value: data["games"][0]["stats"][stat]) + end + end + end + end + end + + public + def render_user_interaction(user) + return "" + end + + public + def handle_user_interaction(user) + # do nothing + end + end +end diff --git a/lib/scheduling/README.md b/lib/scheduling/README.md new file mode 100644 index 0000000..fe6aba1 --- /dev/null +++ b/lib/scheduling/README.md @@ -0,0 +1,22 @@ +Scheduling interface +==================== + +Files in this directory should be _classes_ implementing the following +interface: + + - `initialize(TournamentStage)` + + Construct new Scheduling object from tournament_stage. + + - `create_matches` + + Creates all the matches of the current round. + + - `finish_match(Match)` + + Progresses the match through the schedule. + + - `graph` + + Returns a string representation of an svg image of the current + stage. diff --git a/lib/scheduling/elimination.rb b/lib/scheduling/elimination.rb new file mode 100644 index 0000000..a2ff989 --- /dev/null +++ b/lib/scheduling/elimination.rb @@ -0,0 +1,146 @@ + +module Scheduling + class Elimination + include Rails.application.routes.url_helpers + + def initialize(tournament_stage) + @tournament_stage = tournament_stage + end + + + def create_matches + num_teams = (tournament.players.count/tournament.min_players_per_team).floor + num_matches = (Float(num_teams - tournament.min_teams_per_match)/(tournament.min_teams_per_match - 1)).ceil + 1 + for i in 1..num_matches + tournament_stage.matches.create + end + + match_num = num_matches-1 + team_num = 1 + + tournament.players.shuffle + + # for each grouping of min_players_per_team + tournament.players.each_slice(tournament.min_players_per_team) do |team_members| + # create a new team in the current match + tournament_stage.matches.order(:id)[match_num].teams.push(Team.create(users: team_members)) + + # if the match is full, move to the next match, otherwise move to the next team + if (team_num == tournament.min_teams_per_match) + tournament_stage.matches[match_num].update(status: 1); + match_num -= 1 + team_num = 1 + else + team_num += 1 + end + end + end + + def finish_match(match) + logBase = match.tournament_stage.tournament.min_teams_per_match + matches = match.tournament_stage.matches_ordered + cur_match_num = matches.invert[match] + unless cur_match_num == 1 + match.winner.matches.push(matches[(cur_match_num+logBase-2)/logBase]) + end + if matches[(cur_match_num+logBase-2)/logBase].teams.count == match.tournament_stage.tournament.min_teams_per_match + matches[(cur_match_num+logBase-2)/logBase].update(status: 1) + end + end + + def graph(current_user) + matches = @tournament_stage.matches_ordered + numTeams = @tournament_stage.tournament.min_teams_per_match + logBase = numTeams + + # depth of SVG tree + depth = Math.log(matches.count*(logBase-1),logBase).floor+1; + + # height of SVG + matchHeight = 50*logBase; + height = [(matchHeight+50) * logBase**(depth-1) + 100, 500].max; + height = height/2; + + str = <<-STRING + <svg version="1.1" baseProfile="full" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + width="100%" height="#{height}"> + <defs> + <radialGradient id="gradMatch" cx="50%" cy="50%" r="80%" fx="50%" fy="50%"> + <stop offset="0%" style="stop-color:#fff; stop-opacity:1" /> + <stop offset="100%" style="stop-color:#ccc;stop-opacity:0" /> + </radialGradient> + </defs> + STRING + base = 1 + pBase = 1 + (1..matches.count).each do |i| + matchDepth = Math.log(i*(logBase-1), logBase).floor+1 + if matchDepth > Math.log(base*(logBase-1), logBase).floor+1 + pBase = base + base = i + end + rh = 100 / (logBase**(depth-1)+1) - 100/height; + rw = 100/(depth+1) - 5 + rx = 50/(depth+1) + 100/(depth+1)*(depth-matchDepth) + ry = 100/(logBase**(matchDepth-1)+1) * (i-base+1) - rh/2 + + str += "\t<a id=\"svg-match-#{i}\" xlink:href=\"#{match_path(matches[i])}\"><g>\n" + str += "\t\t<rect height=\"#{rh}%\" width=\"#{rw}%\" x=\"#{rx}%\" y=\"#{ry}%\" fill=\"url(#gradMatch)\" rx=\"5px\" stroke-width=\"2\"" + case matches[i].status + when 0 + if matches[i].teams.count == 0 + str += ' stroke="red"' + str += ' fill-opacity="0.6"' + else + str += 'stroke="orange"' + end + when 1 + str += ' stroke="green"' + when 2 + str += ' stroke="lightblue"' + when 3 + str += ' stroke="grey"' + end + + str += "/>\n" + + t = 1 + while t <= numTeams + color = (matches[i].teams[t-1] and matches[i].teams[t-1].users.include?(current_user)) ? "#5BC0DE" : "white" + str += "\t\t<rect width=\"#{rw-5}%\" height=\"#{rh*Float(30)/(matchHeight)}%\" x=\"#{rx + 2.5}%\" y=\"#{ry + (Float(t-1)/numTeams)*rh + 1 }%\" fill=\"#{color}\" />\n" + if matches[i].teams[t-1] + str += "\t\t<text x=\"#{rx + rw/4}%\" y=\"#{ry + (Float(t-1)/numTeams + Float(33)/(matchHeight))*rh}%\" font-size=\"120%\">Team #{matches[i].teams[t-1].id}</text>\n" + end + if (t < numTeams) + str += "\t\t<text x=\"#{rx + 1.3*rw/3}%\" y=\"#{ry + (Float(t)/numTeams)*rh + 1}%\" font-size=\"120%\"> VS </text>\n" + end + t = t + 1 + end + + if i > 1 + parent = (i+logBase-2)/logBase + pDepth = Math.log(parent*(logBase-1), logBase).floor+1 + lastrx = 50/(depth+1) + 100/(depth+1)*(depth-pDepth) + lastry = 100/(logBase**(pDepth-1)+1) * (parent-pBase+1) - rh/2 + str += "\t\t<line x1=\"#{rx+rw}%\" y1=\"#{ry+rh/2}%\" x2=\"#{lastrx}%\" y2=\"#{lastry+rh/2}%\" stroke=\"white\" stroke-width=\"2\" >\n" + end + str += "</g></a>\n" + end + str += '</svg>' + + return str + end + + private + + def tournament_stage + @tournament_stage + end + + def tournament + tournament_stage.tournament + end + end +end diff --git a/lib/scheduling/round_robin.rb b/lib/scheduling/round_robin.rb new file mode 100644 index 0000000..7ee617d --- /dev/null +++ b/lib/scheduling/round_robin.rb @@ -0,0 +1,70 @@ +# http://stackoverflow.com/questions/6648512/scheduling-algorithm-for-a-round-robin-tournament +module Scheduling + class RoundRobin + include Rails.application.routes.url_helpers + + def initialize(tournament_stage) + @tournament_stage = tournament_stage + end + + def create_matches + # => find the number of matches and teams to create + @num_teams = (tournament.players.count/tournament.min_players_per_team).floor + @matches_per_round = (@num_teams / tournament.min_teams_per_match).floor + + # => initialize data and status members + @team_pairs ||= Array.new + if @team_pairs.empty? + @matches_finished = 0 + end + + # => Create new matches + @matches_per_round.times do + tournament_stage.matches.create + end + + # => seed the first time + if @team_pairs.empty? + tournament_stage.seeding.seed(tournament_stage) + tournament_stage.matches.each {|match| match.teams.each {|team| @team_pairs.push team}} + else + # => Reorder the list of teams + top = @team_pairs.shift + @team_pairs.push @team_pairs.shift + @team_pairs.unshift top + + # => Add the teams to the matches + match = tournament_stage.matches[@matches_finished-1] + matches = 1 + (0..@team_pairs.count-1).each do |i| + match.teams += @team_pairs[i] + if @team_pairs.count.%(tournament.min_teams_per_match).zero? + match = tournament_stage.matches[@matches_finished-1 + matches] + matches += 1 + end + end + + end + + # => Set the match statuses to ready (1) + tournament_stage.matches.each {|match| match.update(status: 1)} + + end + + def finish_match(match) + @matches_finished += 1 + end + + def graph(current_user) + end + + private + def tournament_stage + @tournament_stage + end + + def tournament + tournament_stage.tournament + end + end +end diff --git a/lib/scoring/README.md b/lib/scoring/README.md new file mode 100644 index 0000000..efdc3cc --- /dev/null +++ b/lib/scoring/README.md @@ -0,0 +1,15 @@ +Scoring interface +================= + +Files in this directory should be _modules_ implementing the following +interface: + + - `stats_needed(Match) => Array[]=String` + + Returns which statistics need to be collected for this scoring + algorithm. + + - `score(Match) => Hash[User]=Integer` + + User scores for this match, assuming statistics have been + collected. diff --git a/lib/scoring/fibonacci_peer_with_blowout.rb b/lib/scoring/fibonacci_peer_with_blowout.rb new file mode 100644 index 0000000..a13d76c --- /dev/null +++ b/lib/scoring/fibonacci_peer_with_blowout.rb @@ -0,0 +1,33 @@ +module Scoring + module FibonacciPeerWithBlowout + def self.stats_needed(match) + return ["votes", "win", "blowout"] + match.users.map{|u|"review_from_#{u.user_name}"} + end + + def self.score(match) + scores = {} + match.users.each do |user| + stats = user.statistics.where(match: match) + votes = 0 + match.users.each do |u| + votes += convert_place_to_votes stats.where(name: "review_from_#{u.user_name}").first.value + end + win = stats.where(name: "win" ).first.value + blowout = stats.where(name: "blowout").first.value + scores[user] = self.score_user(votes, win, blowout) + end + scores + end + + protected + + def self.score_user(votes, win, blowout) + fibonacci = Hash.new { |h,k| h[k] = k < 2 ? k : h[k-1] + h[k-2] } + fibonacci[votes+3] + (win ? blowout ? 12 : 10 : blowout ? 5 : 7) + end + + def self.convert_place_to_votes(place) + (place == 0 or place == 1) ? 1 : 0 + end + end +end diff --git a/lib/scoring/marginal_peer.rb b/lib/scoring/marginal_peer.rb new file mode 100644 index 0000000..f2c0272 --- /dev/null +++ b/lib/scoring/marginal_peer.rb @@ -0,0 +1,16 @@ +module Scoring + module MarginalPeer + def self.stats_needed(match) + return ["rating", "win"] + end + + def self.score(match) + scores = {} + match.users.each do |user| + stats = Statistic.where(user: user, match: match) + scores[user] = stats.where(name: "rating").first.value + end + scores + end + end +end diff --git a/lib/scoring/winner_takes_all.rb b/lib/scoring/winner_takes_all.rb new file mode 100644 index 0000000..6cffb28 --- /dev/null +++ b/lib/scoring/winner_takes_all.rb @@ -0,0 +1,16 @@ +module Scoring + module WinnerTakesAll + def self.stats_needed(match) + return ["win"] + end + + def self.score(match) + scores = {} + match.users.each do |user| + stats = Statistic.where(user: user, match: match) + scores[user] = stats.where(name: "win").first.value ? 1 : 0 + end + scores + end + end +end diff --git a/lib/seeding/.keep b/lib/seeding/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/seeding/.keep diff --git a/lib/seeding/README.md b/lib/seeding/README.md new file mode 100644 index 0000000..d323b6d --- /dev/null +++ b/lib/seeding/README.md @@ -0,0 +1,10 @@ +Seeding interface +================= + +Files in this directory should be _modules_ implement the following +interface: + + - `seed(TournamentStage)` + + Take a tournament stage, assign players to teams and teams to + matches (matches must exist). diff --git a/lib/seeding/early_bird_seeding.rb b/lib/seeding/early_bird_seeding.rb new file mode 100644 index 0000000..bf7b3c2 --- /dev/null +++ b/lib/seeding/early_bird_seeding.rb @@ -0,0 +1,20 @@ +module Seeding + module EarlyBirdSeeding + def self.seed(tournament_stage) + matches = tournament_stage.matches + match = matches.first + match_num = 0 + teams = 0 + tournament_stage.tournament.players.each_slice(tournament_stage.tournament.min_players_per_team) do |slice| + if teams < tournament_stage.tournament.min_teams_per_match + match.teams.push Team.create(players: slice) + teams += 1 + else + match_num += 1 + match = matches[match_num] + teams = 0 + end + end + end + end +end diff --git a/lib/seeding/fair_ranked_seeding.rb b/lib/seeding/fair_ranked_seeding.rb new file mode 100644 index 0000000..870ebdd --- /dev/null +++ b/lib/seeding/fair_ranked_seeding.rb @@ -0,0 +1,43 @@ +module Seeding + module FairRankedSeeding + def self.seed(tournament_stage) + matches = tournament.current_stage.matches + match = matches.first + match_num = 0 + players_used = 0 + (tournament.players.count/tournament.min_players_per_team).floor.times do + match.teams.push Team.create() + end + best_first(tournament).each_slice(tournament.min_teams_per_match) do |slice| + (0..tournament.min_teams_per_match-1).each do |index| + match.teams[index].players += slice[index] + end + players_used += 1 + if players_used == tournament.min_players_per_team + match_num += 1 + match = matches[match_num] + players_used = 0 + end + end + end + + private + def self.best_first(tournament) + tournament.players.sort {|a, b| better(a, b, tournament) } + end + + def self.better(player1, player2, tournament) + ps1 = previous_score(player1, tournament) + ps2 = previous_score(player2, tournament) + ps1 <=> ps2 + end + + def self.previous_score(player, tournament) + score = tournament.statistics.where(match: player.matches.last, user: player, name: :score) + if score.nil? + return 0 + end + score + end + end +end diff --git a/lib/seeding/random_seeding.rb b/lib/seeding/random_seeding.rb new file mode 100644 index 0000000..ccdba11 --- /dev/null +++ b/lib/seeding/random_seeding.rb @@ -0,0 +1,20 @@ +module Seeding + module RandomSeeding + def self.seed(tournament_stage) + matches = tournament_stage.matches + match = matches.first + match_num = 0 + teams = 0 + tournament_stage.tournament.players.shuffle.each_slice(tournament_stage.tournament.min_players_per_team) do |slice| + if teams < tournament_stage.tournament.min_teams_per_match + match.teams.push Team.create(players: slice) + teams += 1 + else + match_num += 1 + match = matches[match_num] + teams = 0 + end + end + end + end +end diff --git a/lib/throttled_api_request.rb b/lib/throttled_api_request.rb new file mode 100644 index 0000000..c48a66d --- /dev/null +++ b/lib/throttled_api_request.rb @@ -0,0 +1,32 @@ +# limits is in the format: +# limits = [ +# {:unit_time => 10.seconds, :requests_per => 10}, +# {:unit_time => 10.minutes, :requests_per => 500}, +# ] +class ThrottledApiRequest < Struct.new(:api_name, :limits) + def before(job) + loop do + sleep_for = -1 + ActiveRecord::Base.transaction do + ApiRequest.create(:api_name => self.api_name) + self.limits.each do |limit| + recent_requests = ApiRequest. + where(:api_name => self.api_name). + where("updated_at > ?", Time.now.utc - limit[:unit_time]). + order(:updated_at) + if (recent_requests.count > limit[:requests_per]) + sleep_for = [sleep_for, Time.now.utc - recent_requests[recent_requests.count-limit[:requests_per]].updated_at].max + end + end + if sleep_for != -1 + raise ActiveRecord::Rollback + end + end + if sleep_for != -1 + sleep(sleep_for) + else + break + end + end + end +end |