killer_queen_scene_scoring 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d33202ac0d276376a6f6a5b044ff7c7780599eba401c8046359390cca7b0677e
4
+ data.tar.gz: e435962c1fd937190dc49cf950b5bfea1dd54d0aeab5f6431892330fe265ab9d
5
+ SHA512:
6
+ metadata.gz: 9f0190a641d7e30c18d8c4a7d653d48360df7b45337ccc4266115192889c48509296374ebcb51f938108c6d4fc72f52f1d9bca88017fb93d4311247a1d41aba1
7
+ data.tar.gz: b39af761fbb5a74829b228b7bdcae7d732a0e99fa5a9b8cdfa6aec7bf66e28f6faf71fef3411479f3458143c85d30f208960cfc538391111fa92b381ef8a4951
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor/
10
+ *.gem
11
+ .env
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.0
7
+ before_install: gem install bundler -v 1.17.2
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+ ruby "2.6.0"
3
+
4
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5
+
6
+ # Specify your gem's dependencies in killer_queen_scene_scoring.gemspec
7
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ killer_queen_scene_scoring (0.1.0)
5
+ dotenv (~> 2.7)
6
+ json (~> 2.2)
7
+ rest-client (~> 2.0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ domain_name (0.5.20180417)
13
+ unf (>= 0.0.5, < 1.0.0)
14
+ dotenv (2.7.1)
15
+ http-cookie (1.0.3)
16
+ domain_name (~> 0.5)
17
+ json (2.2.0)
18
+ mime-types (3.2.2)
19
+ mime-types-data (~> 3.2015)
20
+ mime-types-data (3.2018.0812)
21
+ minitest (5.11.3)
22
+ netrc (0.11.0)
23
+ rake (10.5.0)
24
+ rest-client (2.0.2)
25
+ http-cookie (>= 1.0.2, < 2.0)
26
+ mime-types (>= 1.16, < 4.0)
27
+ netrc (~> 0.8)
28
+ unf (0.1.4)
29
+ unf_ext
30
+ unf_ext (0.0.7.5)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ bundler (~> 1.17)
37
+ killer_queen_scene_scoring!
38
+ minitest (~> 5.0)
39
+ rake (~> 10.0)
40
+
41
+ RUBY VERSION
42
+ ruby 2.6.0p0
43
+
44
+ BUNDLED WITH
45
+ 1.17.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Michael Dunn
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.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # KillerQueenSceneScoring
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/killer_queen_scene_scoring`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'killer_queen_scene_scoring'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install killer_queen_scene_scoring
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/killer_queen_scene_scoring.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "killer_queen_scene_scoring"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "killer_queen_scene_scoring"
4
+
5
+ id = ARGV[0]
6
+ api_key = ENV["CHALLONGE_API_KEY"] || ARGV[1]
7
+
8
+ unless id && api_key
9
+ puts <<~EOS
10
+ Usage: #{File.basename __FILE__} <bracket_id> <api_key>
11
+
12
+ bracket_id is the slug or numeric ID of the first bracket in the tournament.
13
+ api_key is your Challonge API key.
14
+
15
+ If you don't have a tournament ready, you can try one of these, which have
16
+ already been set up: tvtpeasf clonekqxxv bb3wc
17
+
18
+ For convenience, you can create a .env file in the current directory and
19
+ store your API key in that file. The file should have this line:
20
+ CHALLONGE_API_KEY=Your_API_key_here
21
+ EOS
22
+
23
+ return
24
+ end
25
+
26
+ t = KillerQueenSceneScoring::Tournament.new(id: id, api_key: api_key)
27
+ t.load
28
+ t.calculate_points
29
+ puts t.scene_scores.sort
@@ -0,0 +1,43 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require "date"
5
+ require "killer_queen_scene_scoring/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "killer_queen_scene_scoring"
9
+ spec.version = KillerQueenSceneScoring::VERSION
10
+ spec.authors = ["Michael Dunn"]
11
+ spec.email = ["acidhelm@gmail.com"]
12
+
13
+ spec.summary = "Scene-wide scoring for Killer Queen tournaments."
14
+ spec.description = "Classes that implement scene-wide scoring for Killer Queen tournaments."
15
+ spec.homepage = "https://github.com/acidhelm/killer_queen_scene_scoring"
16
+ spec.license = "MIT"
17
+
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
23
+ else
24
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
25
+ end
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(/^(test|spec|features)\//) }
30
+ end
31
+
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(/^exe\//) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_development_dependency "bundler", "~> 1.17"
37
+ spec.add_development_dependency "minitest", "~> 5.0"
38
+ spec.add_development_dependency "rake", "~> 10.0"
39
+
40
+ spec.add_runtime_dependency "dotenv", "~> 2.7"
41
+ spec.add_runtime_dependency "json", "~> 2.2"
42
+ spec.add_runtime_dependency "rest-client", "~> 2.0"
43
+ end
@@ -0,0 +1,18 @@
1
+ require "dotenv/load"
2
+ require "json"
3
+ require "rest-client"
4
+ require "killer_queen_scene_scoring/bracket.rb"
5
+ require "killer_queen_scene_scoring/config"
6
+ require "killer_queen_scene_scoring/match"
7
+ require "killer_queen_scene_scoring/player"
8
+ require "killer_queen_scene_scoring/scene"
9
+ require "killer_queen_scene_scoring/team"
10
+ require "killer_queen_scene_scoring/tournament"
11
+ require "killer_queen_scene_scoring/version"
12
+
13
+ module KillerQueenSceneScoring
14
+ # Creates a hash whose values are arrays.
15
+ def self.hash_of_arrays
16
+ Hash.new { |h, k| h[k] = [] }
17
+ end
18
+ end
@@ -0,0 +1,334 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KillerQueenSceneScoring
4
+
5
+ class Bracket
6
+ attr_reader :players, :config
7
+
8
+ # `id` can be the slug or the challonge ID of the bracket. If you pass a
9
+ # and the bracket is owned by an organization, it must be of the form
10
+ # "<org name>-<slug>". `api_key` is your Challonge API key.
11
+ def initialize(id:, api_key:)
12
+ @id = id
13
+ @api_key = api_key
14
+ @loaded = false
15
+ @state == ""
16
+ end
17
+
18
+ def complete?
19
+ @state == "complete"
20
+ end
21
+
22
+ # Reads the Challonge bracket with the ID that was passed to the constructor,
23
+ # and fills in all the data structures that represent that bracket.
24
+ # Returns a boolean indicating whether the bracket was loaded.
25
+ def load
26
+ url = "https://api.challonge.com/v1/tournaments/#{@id}.json"
27
+ params = { include_matches: 1, include_participants: 1 }
28
+
29
+ begin
30
+ response = send_get_request(url, params)
31
+ rescue RestClient::NotFound
32
+ # Bail out if we got a 404 error. The bracket doesn't exist on
33
+ # Challonge right now, but it might be created in the future.
34
+ # TODO: Rails.logger.warn "The bracket does not exist."
35
+ return false
36
+ end
37
+
38
+ # Set `@challonge_bracket` to the data structure that represents the
39
+ # bracket, its teams, and its matches. We also keep the bracket's
40
+ # state separately, for convenience.
41
+ @challonge_bracket = OpenStruct.new(response[:tournament])
42
+ @state = @challonge_bracket.state
43
+
44
+ # Bail out if the bracket hasn't started yet. This lets the tournament
45
+ # organizer set the `next_bracket` value to a bracket that has been
46
+ # created on Challonge, but which will be started in the future. For
47
+ # example, the organizer can create a wild card bracket and a finals
48
+ # bracket, and set `next_bracket` in the wild card bracket to the ID
49
+ # of the finals bracket before the wild card bracket has finished.
50
+ if @challonge_bracket.started_at.nil?
51
+ # TODO: Rails.logger.warn "The bracket has not been started yet."
52
+ return false
53
+ end
54
+
55
+ # Read all the things.
56
+ read_config
57
+ read_teams
58
+ read_matches
59
+ read_players
60
+
61
+ @loaded = true
62
+ true
63
+ end
64
+
65
+ # Calculates how many points each player has earned in this bracket. If the
66
+ # bracket is not yet complete, the point values are the mininum number of
67
+ # points that the player can earn based on their current position in
68
+ # the bracket.
69
+ # On exit, `@players` contains a hash. The keys are the Challonge IDs of
70
+ # the teams in the bracket. The values are arrays of `Player` objects
71
+ # representing the players on the team.
72
+ # The caller must call `load`, and `load` must succeed, before calling this
73
+ # function.
74
+ def calculate_points
75
+ raise_error "The bracket was not loaded" if !@loaded
76
+
77
+ calculate_team_points
78
+ calculate_player_points
79
+ end
80
+
81
+ protected
82
+
83
+ def finalizable?
84
+ @state == "awaiting_review"
85
+ end
86
+
87
+ def raise_error(msg)
88
+ # TODO: Rails.logger.error "ERROR: #{msg}"
89
+ raise msg
90
+ end
91
+
92
+ # Reads the config file from the bracket.
93
+ def read_config
94
+ # Find the match that has the config file attached to it. By convention,
95
+ # the file is attached to the first match, although we don't enforce that.
96
+ # We just look for a match with exactly one attachment. We do require
97
+ # that exactly one match have exactly one attachment.
98
+ first_match = @challonge_bracket.matches.select do |match|
99
+ match[:match][:attachment_count] == 1
100
+ end
101
+
102
+ raise_error "No matches with one attachment were found in the bracket" if first_match.empty?
103
+ raise_error "Multiple matches have one attachment" if first_match.size > 1
104
+
105
+ # Read the options from the config file that's attached to that match.
106
+ url = "https://api.challonge.com/v1/tournaments/#{@id}/matches/" \
107
+ "#{first_match[0][:match][:id]}/attachments.json"
108
+
109
+ attachment_list = send_get_request(url)
110
+ asset_url = attachment_list[0][:match_attachment][:asset_url]
111
+
112
+ raise_error "Couldn't find the config file attachment" if asset_url.nil?
113
+
114
+ uri = URI(asset_url)
115
+
116
+ # The attachment URLs that Challonge returns don't have a scheme, and
117
+ # instead start with "//". Default to HTTPS.
118
+ uri.scheme ||= "https"
119
+
120
+ # TODO: Rails.logger.debug "Reading the config file from #{uri}"
121
+
122
+ # Read the config file from the attchment.
123
+ config = send_get_request(uri.to_s)
124
+
125
+ # Ensure that the required values are in the config file.
126
+ %i(base_point_value max_players_to_count match_values).each do |key|
127
+ raise_error "The config file is missing \"#{key}\"" unless config.key?(key)
128
+ end
129
+
130
+ @config = Config.new(config)
131
+ end
132
+
133
+ # Reads the teams that are in this bracket, and sets `@teams` to an array
134
+ # of `Team` objects.
135
+ def read_teams
136
+ @teams = []
137
+
138
+ # Make a `Team` for each participant in the bracket.
139
+ @challonge_bracket.participants.each do |team|
140
+ @teams << Team.new(team[:participant])
141
+ end
142
+
143
+ # TODO: Rails.logger.info "#{@teams.size} teams are in the bracket: " +
144
+ # @teams.sort_by(&:name).map { |t| %("#{t.name}") }.join(", ")
145
+
146
+ # Check that all of the teams in the bracket are also in the config file.
147
+ # We do case-insensitive name comparisons to allow for different
148
+ # capitalizations of prepositions and articles. This is fine, because
149
+ # two teams' names will never be the same except for case.
150
+ missing_teams = []
151
+ config_team_names = @config.teams.map { |t| t[:name] }
152
+
153
+ @teams.each do |team|
154
+ if config_team_names.none? { |name| name.casecmp?(team.name) }
155
+ missing_teams << team.name
156
+ end
157
+ end
158
+
159
+ if missing_teams.any?
160
+ raise_error "These teams are in the bracket but not the config file: " +
161
+ missing_teams.join(", ")
162
+ end
163
+ end
164
+
165
+ # Reads the matches that are in this bracket, and sets `@matches` to an array
166
+ # of `Match` objects.
167
+ def read_matches
168
+ # Check that `match_values` in the config file is the right size.
169
+ # The size must normally equal the number of matches. However, if the
170
+ # bracket is complete (finalized, or not finalized but all matches have
171
+ # been played) and it is double-elimination, then the array size is
172
+ # allowed to be one more than the number of matches, to account for a grand
173
+ # final that was only one match long.
174
+ #
175
+ # TODO: Also check that grand_finals_modifier is not set.
176
+ #
177
+ # If this is a two-stage bracket, the matches in the first stage have
178
+ # `suggested_play_order` set to nil, so don't consider those matches.
179
+ # If there is a match for 3rd place, its `suggested_play_order` is nil.
180
+ # We also ignore that match, and instead, we award points to the 3rd-place
181
+ # and 4th-place teams after the bracket has finished.
182
+ @matches = []
183
+ elim_stage_matches =
184
+ @challonge_bracket.matches.select { |m| m[:match][:suggested_play_order] }
185
+ num_matches = elim_stage_matches.size
186
+ config_array_size = @config.match_values.size
187
+
188
+ if num_matches != config_array_size
189
+ if !(complete? || finalizable?) ||
190
+ @challonge_bracket.tournament_type != "double elimination" ||
191
+ config_array_size != num_matches + 1
192
+ raise_error "match_values in the config file is the wrong size." \
193
+ " The size is #{config_array_size}, expected #{num_matches}."
194
+ end
195
+ end
196
+
197
+ # Make a `Match` for each match in the bracket.
198
+ elim_stage_matches.each do |match|
199
+ @matches << Match.new(match[:match], @config.match_values)
200
+ end
201
+ end
202
+
203
+ # Reads the players that are in this bracket, and sets `@players` to a hash.
204
+ # Each key is a Challonge ID of a team, and each value is an array of
205
+ # `Player` objects for the players on that team.
206
+ def read_players
207
+ @players = KillerQueenSceneScoring::hash_of_arrays
208
+
209
+ # Read the team list from the config file and create structs for each
210
+ # player on each team.
211
+ @config.teams.each do |team|
212
+ # Look up the team in the `@teams` array. This is how we associate a
213
+ # team in the config file with its Challonge ID.
214
+ team_obj = @teams.find { |t| t.name.casecmp?(team[:name]) }
215
+
216
+ # If the `find` call failed, then there is a team in the team list that
217
+ # isn't in the bracket. We allow this so that multiple brackets can
218
+ # use the same master team list during a tournament.
219
+ if team_obj.nil?
220
+ # TODO: Rails.logger.info "Skipping a team that isn't in the bracket: #{team[:name]}"
221
+ next
222
+ end
223
+
224
+ team[:players].each do |player|
225
+ @players[team_obj.id] << Player.new(player)
226
+ end
227
+
228
+ # TODO: Rails.logger.info "#{team[:name]} (ID #{team_obj.id}) has: " +
229
+ # @players[team_obj.id].map { |p| "#{p.name} (#{p.scene})" }.join(", ")
230
+ end
231
+
232
+ # Bail out if any team doesn't have exactly 5 players.
233
+ # TODO: Do this in `read_config` instead.
234
+ invalid_teams = @players.select do |_, team|
235
+ team.size != 5
236
+ end.each_key.map do |team_id|
237
+ @teams.find { |t| t.id == team_id }.name
238
+ end
239
+
240
+ if invalid_teams.any?
241
+ raise_error "These teams don't have 5 players: #{invalid_teams.join(', ')}"
242
+ end
243
+ end
244
+
245
+ # Calculates how many points each team has earned in this bracket. If the
246
+ # bracket is not yet complete, the values are the mininum number of points
247
+ # that the team can receive based on their current position in the bracket.
248
+ # Sets the `points` member of each object in `@teams` to the number of
249
+ # points that each team member has earned.
250
+ def calculate_team_points
251
+ # If the bracket is complete, we can calculate points based on the
252
+ # teams' `final_rank`s.
253
+ if complete?
254
+ calculate_team_points_by_final_rank
255
+ return
256
+ end
257
+
258
+ # For each team, look at the matches that it is in, look at the point
259
+ # values of those matches, and take the maximum point value. That's the
260
+ # number of points that the team has earned so far in the bracket.
261
+ base_point_value = @config.base_point_value
262
+
263
+ @teams.each do |team|
264
+ matches_with_team = @matches.select { |match| match.has_team?(team.id) }
265
+
266
+ # TODO: Rails.logger.info "Team #{team.name} was in #{matches_with_team.size} matches"
267
+
268
+ points_earned = matches_with_team.max_by(&:points).points
269
+
270
+ # TODO: Rails.logger.info "The largest point value of those matches is #{points_earned}" \
271
+ # "#{" + #{base_point_value} base" if base_point_value > 0}"
272
+
273
+ team.points = points_earned + base_point_value
274
+ end
275
+ end
276
+
277
+ # Calculates how many points each player has earned in the tournament, and
278
+ # sets the `points` member of each object in `@players` to that value.
279
+ def calculate_player_points
280
+ # Iterate over the teams in descending order of their scores. This way,
281
+ # the debug output will follow the teams' finishing order, which will be
282
+ # easier to read.
283
+ @teams.sort_by(&:points).reverse_each do |team|
284
+ # TODO: Rails.logger.info "Awarding #{team.points} points to #{team.name}: " +
285
+ # @players[team.id].map(&:to_s).join(", ")
286
+
287
+ @players[team.id].each do |player|
288
+ player.points = team.points
289
+ end
290
+ end
291
+ end
292
+
293
+ # Calculates how many points each team earned in this bracket.
294
+ # Sets the `points` member of each object in `@teams` to the number of
295
+ # points that each team member has earned.
296
+ def calculate_team_points_by_final_rank
297
+ # Calculate how many points to award to each rank. When multiple teams
298
+ # have the same rank (e.g., two teams tie for 5th place), those teams
299
+ # get the average of the points available to those ranks. For example,
300
+ # in a 6-team bracket, the teams in 1st through 4th place get 6 through 3
301
+ # points respectively. The two teams in 5th get 1.5, the average of 2 and 1.
302
+ sorted_teams = @teams.sort_by(&:final_rank)
303
+ num_teams = sorted_teams.size.to_f
304
+ final_rank_points = KillerQueenSceneScoring::hash_of_arrays
305
+
306
+ sorted_teams.each_with_index do |team, idx|
307
+ final_rank_points[team.final_rank] << num_teams - idx
308
+ end
309
+
310
+ base_point_value = @config.base_point_value
311
+
312
+ sorted_teams.each do |team|
313
+ points_earned = final_rank_points[team.final_rank].sum /
314
+ final_rank_points[team.final_rank].size
315
+
316
+ # TODO: Rails.logger.info "#{team.name} finished in position #{team.final_rank}" \
317
+ # " and gets #{points_earned} points" \
318
+ # "#{" + #{base_point_value} base" if base_point_value > 0}"
319
+
320
+ team.points = points_earned + base_point_value
321
+ end
322
+ end
323
+
324
+ # Sends a GET request to `url`, treats the returned data as JSON, parses it
325
+ # into an object, and returns that object.
326
+ def send_get_request(url, params = {})
327
+ params[:api_key] = @api_key
328
+ resp = RestClient.get(url, params: params)
329
+
330
+ JSON.parse(resp, symbolize_names: true)
331
+ end
332
+ end
333
+
334
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KillerQueenSceneScoring
4
+
5
+ class Config
6
+ attr_reader :base_point_value, :next_bracket, :max_players_to_count,
7
+ :match_values, :teams
8
+
9
+ # `config_obj` is a hash that contains the data from the config file.
10
+ def initialize(config_obj)
11
+ @base_point_value = config_obj[:base_point_value]
12
+ @next_bracket = config_obj[:next_bracket]
13
+ @max_players_to_count = config_obj[:max_players_to_count]
14
+ @match_values = config_obj[:match_values]
15
+ @teams = config_obj[:teams]
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KillerQueenSceneScoring
4
+
5
+ class Match
6
+ attr_reader :points
7
+
8
+ # `challonge_obj` is the Challonge data for this match. `match_values` is
9
+ # the array from the config file that holds how many points are awarded to
10
+ # the teams in that match.
11
+ def initialize(challonge_obj, match_values)
12
+ @team1_id = challonge_obj[:player1_id]
13
+ @team2_id = challonge_obj[:player2_id]
14
+
15
+ play_order = challonge_obj[:suggested_play_order]
16
+ @points = match_values[play_order - 1]
17
+ end
18
+
19
+ # Returns whether the team with the Challonge ID `team_id` is in this match.
20
+ def has_team?(team_id)
21
+ @team1_id == team_id || @team2_id == team_id
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KillerQueenSceneScoring
4
+
5
+ class Player
6
+ attr_reader :name, :scene
7
+ attr_accessor :points
8
+
9
+ # `config_obj` is a hash that contains the player's data from the config file.
10
+ def initialize(config_obj)
11
+ @name = config_obj[:name]
12
+ @scene = config_obj[:scene]
13
+ @points = 0.0
14
+ end
15
+
16
+ def hash
17
+ to_s.hash
18
+ end
19
+
20
+ def to_s
21
+ "#{@name} (#{@scene})"
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KillerQueenSceneScoring
4
+
5
+ class Scene
6
+ attr_accessor :name, :score, :num_players
7
+
8
+ # `name` is the name of the scene. `player_scores` is an array that holds
9
+ # the points that were awarded to each player in the scene.
10
+ def initialize(name, player_scores)
11
+ @name = name
12
+ @score = player_scores.sum
13
+ @num_players = player_scores.size
14
+ end
15
+
16
+ def to_s
17
+ "#{@name}: #{@score} points from #{@num_players} players"
18
+ end
19
+
20
+ def <=>(rhs)
21
+ # Sort by score in descending order so the largest scores come first.
22
+ @score != rhs.score ? rhs.score <=> @score : @name <=> rhs.name
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KillerQueenSceneScoring
4
+
5
+ class Team
6
+ attr_reader :players, :id, :name, :final_rank
7
+ attr_accessor :points
8
+
9
+ # `challonge_obj` is the Challonge data for this team.
10
+ def initialize(challonge_obj)
11
+ @id = challonge_obj[:id]
12
+ @name = challonge_obj[:name]
13
+ @final_rank = challonge_obj[:final_rank]
14
+ @points = 0.0
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KillerQueenSceneScoring
4
+
5
+ class Tournament
6
+ attr_reader :scene_scores, :complete
7
+
8
+ # `id` can be the slug or the challonge ID of the first bracket in the
9
+ # tournament. If you pass a slug, and the bracket is owned by an organization,
10
+ # it must be of the form "<org name>-<slug>".
11
+ # `api_key` is your Challonge API key.
12
+ def initialize(id:, api_key:)
13
+ @brackets = []
14
+ @scene_scores = []
15
+ @loaded = false
16
+ @complete = false
17
+ @id = id
18
+ @api_key = api_key
19
+ end
20
+
21
+ # Reads the Challonge bracket with the ID that was passed to the constructor,
22
+ # and fills in all the data structures that represent that bracket and any
23
+ # later brackets in the tournament. `@complete` is also set to indicate
24
+ # whether the entire tournament is complete.
25
+ # Returns true if at least one bracket was loaded, and false otherwise.
26
+ def load
27
+ @loaded = false
28
+ tournament_id = @id
29
+ all_brackets_loaded = true
30
+
31
+ while tournament_id
32
+ # TODO: Rails.logger.debug "Reading the bracket \"#{tournament_id}\""
33
+
34
+ # Load the next bracket in the chain. Bail out if we can't load it.
35
+ bracket = Bracket.new(id: tournament_id, api_key: @api_key)
36
+
37
+ if !bracket.load
38
+ all_brackets_loaded = false
39
+ break
40
+ end
41
+
42
+ # Store that bracket.
43
+ @brackets << bracket
44
+
45
+ # For debugging purposes, log the players in each scene ->
46
+ scenes = KillerQueenSceneScoring::hash_of_arrays
47
+
48
+ bracket.players.each_value do |team|
49
+ team.each do |player|
50
+ scenes[player.scene] << player
51
+ end
52
+ end
53
+
54
+ scene_list = scenes.map do |scene, players|
55
+ "#{scene} has #{players.size} players: " +
56
+ players.map(&:name).join(", ")
57
+ end
58
+
59
+ # TODO: Rails.logger.info scene_list.join("\n")
60
+ # <- end debug logging
61
+
62
+ # Go to the next bracket in the chain.
63
+ tournament_id = bracket.config.next_bracket
64
+ end
65
+
66
+ return false if @brackets.empty?
67
+
68
+ # Check that all the config files have the same `max_players_to_count`.
69
+ values = @brackets.map { |b| b.config.max_players_to_count }
70
+
71
+ if values.count(values[0]) != values.size
72
+ msg = "ERROR: All brackets must have the same \"max_players_to_count\"."
73
+ # TODO: Rails.logger.error msg
74
+ raise msg
75
+ end
76
+
77
+ # If we loaded all the brackets in the list of brackets, set our
78
+ # `complete` member based on the completed state of the last bracket.
79
+ # We only check the last bracket because previous brackets in the
80
+ # sequence are not guaranteed to be marked as complete on Challonge.
81
+ # For an example, see "bb3wc". The BB3 wild card bracket was not
82
+ # marked as complete because play stopped once it got down to 4 teams.
83
+ @complete = all_brackets_loaded && @brackets.last.complete?
84
+
85
+ @loaded = true
86
+ true
87
+ end
88
+
89
+ # Calculates the score for each scene in the tournament, and sets
90
+ # `@scene_scores` to an array of `Scene` objects.
91
+ # The caller must call `load`, and `load` must succeed, before calling this
92
+ # function.
93
+ def calculate_points
94
+ raise "The tournament was not loaded" unless @loaded
95
+
96
+ @brackets.each(&:calculate_points)
97
+ calculate_scene_points
98
+ end
99
+
100
+ protected
101
+
102
+ # Calculates how many points each scene has earned in the tournament.
103
+ # Sets `@scene_scores` to an array of `Scene` objects.
104
+ def calculate_scene_points
105
+ # Collect the scores of all players from the same scene. Since a player
106
+ # may be in multiple brackets, we find their greatest score across
107
+ # all brackets.
108
+ # `player_scores` is a hash from a `Player` object's hash to the `Player`
109
+ # object. This is a hash to make lookups easier; the keys aren't used
110
+ # after# this loop.
111
+ player_scores = @brackets.each_with_object({}) do |bracket, scores|
112
+ bracket.players.each_value do |team_players|
113
+ team_players.each do |player|
114
+ key = player.hash
115
+
116
+ if !scores.key?(key) || player.points > scores[key].points
117
+ scores[key] = player
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ # Assemble the scores from the players in each scene.
124
+ # `scene_players_scores` is a hash from a scene name to an array that
125
+ # holds the scores of all the players in that scene.
126
+ scene_players_scores = KillerQueenSceneScoring::hash_of_arrays
127
+
128
+ player_scores.each_value do |player|
129
+ scene_players_scores[player.scene] << player.points
130
+ end
131
+
132
+ @scene_scores = scene_players_scores.map do |scene, scores|
133
+ # If a scene has more players than the max number of players whose
134
+ # scores can be counted, drop the extra players' scores.
135
+ # Sort the scores for each scene in descending order, so we only
136
+ # keep the highest scores.
137
+ max_players_to_count = @brackets[0].config.max_players_to_count
138
+ scores.sort!.reverse!
139
+
140
+ if scores.size > max_players_to_count
141
+ dropped = scores.slice!(max_players_to_count..-1)
142
+
143
+ # TODO: Rails.logger.info "Dropping the #{dropped.size} lowest scores from #{scene}:" +
144
+ # dropped.join(", ")
145
+ end
146
+
147
+ # Add up the scores for this scene.
148
+ Scene.new(scene, scores)
149
+ end
150
+ end
151
+ end
152
+
153
+ end
@@ -0,0 +1,3 @@
1
+ module KillerQueenSceneScoring
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: killer_queen_scene_scoring
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Dunn
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-03-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.17'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.17'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rest-client
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
97
+ description: Classes that implement scene-wide scoring for Killer Queen tournaments.
98
+ email:
99
+ - acidhelm@gmail.com
100
+ executables:
101
+ - killer_queen_scene_scoring
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - Gemfile.lock
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - exe/killer_queen_scene_scoring
115
+ - killer_queen_scene_scoring.gemspec
116
+ - lib/killer_queen_scene_scoring.rb
117
+ - lib/killer_queen_scene_scoring/bracket.rb
118
+ - lib/killer_queen_scene_scoring/config.rb
119
+ - lib/killer_queen_scene_scoring/match.rb
120
+ - lib/killer_queen_scene_scoring/player.rb
121
+ - lib/killer_queen_scene_scoring/scene.rb
122
+ - lib/killer_queen_scene_scoring/team.rb
123
+ - lib/killer_queen_scene_scoring/tournament.rb
124
+ - lib/killer_queen_scene_scoring/version.rb
125
+ homepage: https://github.com/acidhelm/killer_queen_scene_scoring
126
+ licenses:
127
+ - MIT
128
+ metadata:
129
+ allowed_push_host: https://rubygems.org
130
+ homepage_uri: https://github.com/acidhelm/killer_queen_scene_scoring
131
+ source_code_uri: https://github.com/acidhelm/killer_queen_scene_scoring
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubygems_version: 3.0.1
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: Scene-wide scoring for Killer Queen tournaments.
151
+ test_files: []