diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/pairing/PairingAlgorithm.rb | 6 | ||||
-rw-r--r-- | lib/playing/.keep | 0 | ||||
-rw-r--r-- | lib/sampling/README.md | 28 | ||||
-rw-r--r-- | lib/sampling/double_blind.rb | 35 | ||||
-rw-r--r-- | lib/sampling/manual.rb | 36 | ||||
-rw-r--r-- | lib/sampling/riot_api.rb | 170 | ||||
-rw-r--r-- | lib/scheduling/README.md | 13 | ||||
-rw-r--r-- | lib/scheduling/elimination.rb | 144 | ||||
-rw-r--r-- | lib/scheduling/roundrobin.rb | 74 | ||||
-rw-r--r-- | lib/scoring/README.md | 10 | ||||
-rw-r--r-- | lib/scoring/fibonacci_peer_with_blowout.rb | 22 | ||||
-rw-r--r-- | lib/scoring/marginal_peer.rb | 15 | ||||
-rw-r--r-- | lib/scoring/winner_takes_all.rb | 20 | ||||
-rw-r--r-- | lib/seeding/.keep | 0 | ||||
-rw-r--r-- | lib/seeding/README.md | 4 | ||||
-rw-r--r-- | lib/seeding/early_bird_seeding.rb | 20 | ||||
-rw-r--r-- | lib/seeding/fair_ranked_seeding.rb | 40 | ||||
-rw-r--r-- | lib/seeding/random_seeding.rb | 20 | ||||
-rw-r--r-- | lib/throttled_api_request.rb | 25 |
19 files changed, 682 insertions, 0 deletions
diff --git a/lib/pairing/PairingAlgorithm.rb b/lib/pairing/PairingAlgorithm.rb new file mode 100644 index 0000000..81e4df6 --- /dev/null +++ b/lib/pairing/PairingAlgorithm.rb @@ -0,0 +1,6 @@ +module Pairing + class PairingAlgorithm + def self.pair(matches, players) + end + end +end diff --git a/lib/playing/.keep b/lib/playing/.keep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/playing/.keep diff --git a/lib/sampling/README.md b/lib/sampling/README.md new file mode 100644 index 0000000..28c603e --- /dev/null +++ b/lib/sampling/README.md @@ -0,0 +1,28 @@ +Files in this directory should be modules implementing the following +interface: + + - `works_with?(Game) => Boolean` + Returns whether or not this sampling method works with the + specified game. + + - `uses_remote?() => Boolean` + Return whether or not this sampling method requires remote IDs for + users. + - `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. + - `get_remote_name(Object)` + When given an object from `RemoteUsername#value`, give back a + human-readable/editable name to display. + + - `sampling_start(Match)` + Fetch the statistics for a match. + - `sampling_done?(Match) => Boolean` + Returns whether or not statistics have been completely collected + yet. + + - `render_user_interaction(Match, User) => String` + Returns HTML to render on a page. + - `handle_user_interaction(Match, User, Hash params)` + Handles params from the form generated by + `#user_interaction_render`. diff --git a/lib/sampling/double_blind.rb b/lib/sampling/double_blind.rb new file mode 100644 index 0000000..409da10 --- /dev/null +++ b/lib/sampling/double_blind.rb @@ -0,0 +1,35 @@ +module Sampling + module DoubleBlind + def self.works_with?(game) + return true + 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) + # TODO + end + + def self.sampling_done?(match) + # 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.rb b/lib/sampling/manual.rb new file mode 100644 index 0000000..a1bf9a5 --- /dev/null +++ b/lib/sampling/manual.rb @@ -0,0 +1,36 @@ +module Sampling + module Manual + def self.works_with?(game) + return true + 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) + # TODO + end + + def self.sampling_done?(match) + # TODO + end + + def self.render_user_interaction(match, user) + # TODO + end + + def self.handle_user_interaction(match, user, sampling_params) + # TODO + #match.statistics.create(user: nil, name: "blowout", + end + end +end diff --git a/lib/sampling/riot_api.rb b/lib/sampling/riot_api.rb new file mode 100644 index 0000000..d5e9c72 --- /dev/null +++ b/lib/sampling/riot_api.rb @@ -0,0 +1,170 @@ +module Sampling + module 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}" + 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 + class Job < ThrottledApiRequest + def initialize(request, args={}) + @url = Sampling::RiotApi::url(request, args) + super(api_name, 10.seconds, 10) + end + + def perform + response = open(@url) + status = response.status + data = JSON::parse(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 + self.handle(data) + end + + def handle(data) + 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 + + ## + # 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 => 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) + + normalized_summoner_name = data.keys.first + remote_data = { + :id => data[normalized_summoner_name]["id"], + :name => data[normalized_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 + + ## + # Fetch all the statistics for a match. + public + def self.sampling_start(match) + @match.teams.each do |team| + team.users.each do |user| + Delayed::Job.enqueue(MatchJob.new(user, match), :queue => api_name) + end + end + end + protected + class FetchStatisticsJob < Job + def initialize(user, match) + @user_id = user.id + @match_id = match.id + # Get the summoner id + summoner = user.get_remote_username(match.tournament_stage.tournament.game) + if summoner.nil? + raise "Someone didn't enter their summoner name" + end + # 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) + Statistic.create(user: user, match: match, value: TODO) + end + end + + public + def self.sampling_done?(match) + # TODO + end + + public + def self.render_user_interaction(match, user) + return "" + end + + public + def self.handle_user_interaction(match, user) + end + end +end diff --git a/lib/scheduling/README.md b/lib/scheduling/README.md new file mode 100644 index 0000000..173b7be --- /dev/null +++ b/lib/scheduling/README.md @@ -0,0 +1,13 @@ +Files in this directory should implement the following interface: + + - `initialize(tournament_stage)` + 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
\ No newline at end of file diff --git a/lib/scheduling/elimination.rb b/lib/scheduling/elimination.rb new file mode 100644 index 0000000..4518cff --- /dev/null +++ b/lib/scheduling/elimination.rb @@ -0,0 +1,144 @@ + +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(status: 0, submitted_peer_evaluations: 0) + end + + match_num = num_matches-1 + team_num = 0 + + tournament.players.shuffle + + # for each grouping of min_players_per_team + tournament.players.each_slice(tournament.min_players_per_team) do |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) + match_num -= 1 + team_num = 1 + else + team_num += 1 + end + # create a new team in the current match + tournament_stage.matches[match_num].teams.push(Team.create(users: team_members)) + end + end + + def finish_match(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/2]) + 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; + + 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 + puts matchDepth + if matchDepth > Math.log(base*(logBase-1), logBase).floor+1 + pBase = base + base = i + end + puts base + rh = 100 / (logBase**(depth-1)+1) - 100/height; + puts rh + rw = 100/(depth+1) - 5 + puts rw + rx = 50/(depth+1) + 100/(depth+1)*(depth-matchDepth) + puts rx + ry = 100/(logBase**(matchDepth-1)+1) * (i-base+1) - rh/2 + puts ry + + 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 < @tournament_stage.tournament.min_teams_per_match + str += ' stroke="red"' + str += ' fill-opacity="0.6"' + else + str += ' stroke="green"' + end + when 1 + str += ' stroke="orange"' + when 2 + str += ' stroke="yellow"' + 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=\"200%\">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=\"200%\"> 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/roundrobin.rb b/lib/scheduling/roundrobin.rb new file mode 100644 index 0000000..7a9e257 --- /dev/null +++ b/lib/scheduling/roundrobin.rb @@ -0,0 +1,74 @@ +module Scheduling + class RoundRobin + 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/2)*(num_teams-1) + for i in 1..num_matches + tournament_stage.matches.create(status: 0, submitted_peer_evaluations: 0) + end + end + + def finish_match(match) + #declare winner of match, and store that somehow + rotate + return "totes worked\n" + end + + def graph(current_user) + end + + private + + def create_round_array + #round robin should look like this. + #NOTE: I DO NOT KNOW IF THIS IS HOW TO PROPERLY POPULATE THE ROUND ROBIN ARRAY WITH TEAMS + @team_pairs = Array.new(num_matches) + for i in 0..@match.teams.size + @team_pairs.push(@match.teams[i]) + #if there is an odd number of teams, add a dummy for byes + if @match.teams.size % 2 != 0 && i == @match.teams.size-1 + dummy = Team.create + @team_pairs.push(dummy) + end + end + end + + def tournament_stage + @tournament_stage + end + + def tournament + tournament_stage.tournament + end + + def rotate + #this is called when a round has completed + + #remove first team + hold = @team_pairs.shift + #rotate by 1 element + @team_pairs.rotate! + #place first team the front of the array + @team_pairs.unshift(hold) + end + + def mother_fuckin_winner + scores = {} + @teams_pairs.each do |team| + scores[team] = team.matches. + where(:tournament_stage => tournament_stage). + collect{|match|match.winner==team} + end + weiner = scores.index(scores.max) + scores[weiner] + end + + + end +end diff --git a/lib/scoring/README.md b/lib/scoring/README.md new file mode 100644 index 0000000..95dd5e0 --- /dev/null +++ b/lib/scoring/README.md @@ -0,0 +1,10 @@ +Files in this directory should be modules implementing the following +interface: + + - `stats_needed() => Array[i]=Symbol` + 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..21ffab1 --- /dev/null +++ b/lib/scoring/fibonacci_peer_with_blowout.rb @@ -0,0 +1,22 @@ +module Scoring + module FibonacciPeerWithBlowout + def self.stats_needed + return [:votes] + end + + def self.score(match) + scores = {} + match.players.each do |player| + scores[player] = self.score_user(match.statistics.where(user: player, name: :votes).first, match.win?(player), match.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 + end +end diff --git a/lib/scoring/marginal_peer.rb b/lib/scoring/marginal_peer.rb new file mode 100644 index 0000000..13e1796 --- /dev/null +++ b/lib/scoring/marginal_peer.rb @@ -0,0 +1,15 @@ +module Scoring + module MarginalPeer + def stats_needed + return [:rating] + end + + def score(match, interface) + scores = {} + match.players.each do |player| + scores[player.user_name] = interface.get_statistic(match, player, :rating) + 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..517dfd6 --- /dev/null +++ b/lib/scoring/winner_takes_all.rb @@ -0,0 +1,20 @@ +module Scoring + module WinnerTakesAll + def stats_needed + return [] + end + + def score(match, interface) + scores = {} + match.players.each do |player| + scores[player.user_name] = score_user(match.win?(player)) + end + scores + end + + private + def score_user(win) + win.nil? ? 0.5 : win ? 1 : 0 + 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..67fc19b --- /dev/null +++ b/lib/seeding/README.md @@ -0,0 +1,4 @@ +Files in this directory should implement the following interface: + +- seed_matches(tournament) + take the matches of a tournament and the players in a tournament, assign players to teams, and teams to matches (all must exist)
\ No newline at end of file diff --git a/lib/seeding/early_bird_seeding.rb b/lib/seeding/early_bird_seeding.rb new file mode 100644 index 0000000..30e15fc --- /dev/null +++ b/lib/seeding/early_bird_seeding.rb @@ -0,0 +1,20 @@ +module Seeding + class EarlyBirdSeeding + def seed_matches(tournament) + matches = tournament.current_stage.matches + match = matches.first + match_num = 0 + teams = 0 + tournament.players.each_slice(tournament.min_players_per_team) do |slice| + if teams < tournament.min_teams_per_match + match.teams[teams].players += slice + teams += 1 + else + match_num += 1 + match = matches[match_num] + teams = 0 + end + end + end + end +end
\ No newline at end of file diff --git a/lib/seeding/fair_ranked_seeding.rb b/lib/seeding/fair_ranked_seeding.rb new file mode 100644 index 0000000..4eb8c26 --- /dev/null +++ b/lib/seeding/fair_ranked_seeding.rb @@ -0,0 +1,40 @@ +module Seeding + class FairRankedSeeding + def seed_matches(tournament) + matches = tournament.current_stage.matches + match = matches.first + match_num = 0 + players_used = 0 + 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 best_first(tournament) + tournament.players.sort {|a, b| better(a, b, tournament) } + end + + def better(player1, player2, tournament) + ps1 = previousScore(player1, tournament) + ps2 = previousScore(player2, tournament) + ps1 <=> ps2 + end + + def previousScore(player, tournament) + score = tournament.statistics.getStatistic(player.matches.last, player, :score) + if score.nil? + return 0 + end + score + end + end +end
\ No newline at end of file diff --git a/lib/seeding/random_seeding.rb b/lib/seeding/random_seeding.rb new file mode 100644 index 0000000..ec39e61 --- /dev/null +++ b/lib/seeding/random_seeding.rb @@ -0,0 +1,20 @@ +module Seeding + class RandomSeeding + def seed_matches(tournament) + matches = tournament.current_stage.matches + match = matches.first + match_num = 0 + teams = 0 + tournament.players.shuffle.each_slice(tournament.min_players_per_team) do |slice| + if teams < tournament.min_teams_per_match + match.teams[teams].players += slice + teams += 1 + else + match_num += 1 + match = matches[match_num] + teams = 0 + end + end + end + end +end
\ No newline at end of file diff --git a/lib/throttled_api_request.rb b/lib/throttled_api_request.rb new file mode 100644 index 0000000..3f30c56 --- /dev/null +++ b/lib/throttled_api_request.rb @@ -0,0 +1,25 @@ +class ThrottledApiRequest < Struct.new(:api_name, :unit_time, :requests_per) + def before(job) + loop do + sleep_for = -1 + ActiveRecord::Base.transaction do + ApiRequests.create(:api_name => self.api_name) + recent_requests = ApiRequets. + where(:api_name => self.api_name). + where("updated_at > ?", Time.now.utc - self.unit_time). + order(:updated_at) + if (recent_requests.count > self.requests_per) + sleep_for = Time.now.utc - recent_requests[recent_requests.count-self.requests_per].updated_at + raise ActiveRecord::Rollback + else + sleep_for = -1 + end + end + if sleep_for != -1 + sleep(sleep_for) + else + break + end + end + end +end |