bracket_graph 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a8a094e616df6e13af2293847ab029e6b492d41a
4
+ data.tar.gz: a5288ecdb4a357a7dadb8e4f83ed16ff99d873bf
5
+ SHA512:
6
+ metadata.gz: 7ecaa3807b3900d97ecb09d9b48c018d84a78426d4ee1a0aa1a1154bc418bdb74ea78b34163be71a26863a88f25c7baa97befa608a45e9f37a529832fb790a9e
7
+ data.tar.gz: 49fa2118ba3892ee6f950bad065364a4f1a8d3ff5dd38794bda78c6dcf12ce46aac425ae5f0cfbeb021b84ac49552458bdd584f10a97c7c80c8996299201bbe9
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bracket_graph.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nicola Racco
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # BracketGraph
2
+
3
+ Bracket Graph Library.
4
+
5
+ It helps managing a graph for a single elimination bracket where each seat leads to a match that leads to a winner seat.
6
+
7
+ ## Single Elimination Bracket
8
+
9
+ ```ruby
10
+ BracketGraph::Graph.new(bracket_size)
11
+ ```
12
+
13
+ ## About the Graph object
14
+
15
+ ```ruby
16
+ graph.root # => BracketGraph::Seat for the final match
17
+ graph.seats # => Array[BracketGraph::Seat] all nodes
18
+ graph.starting_seats # => Array[BracketGraph::Seat] all starting nodes
19
+ graph[12] # => BracketGraph::Seat with id/position 12
20
+ graph.seed(teams) # => seeds each item in the given array to a starting node
21
+ graph.seed(teams, shuffle: true) # => seeds teams after shuffle
22
+ ```
23
+
24
+ ## About the Graph nodes
25
+
26
+ ```ruby
27
+ seat.from # Array[BracketGraph::Seat] source nodes. Empty array for a starting node
28
+ seat.to # parent node. nil for the final node
29
+ seat.position # node position id
30
+ seat.payload # custom payload that can be also seeded via BracketGraph::Graph#seed
31
+ ```
32
+
33
+ ## Double Elimination Bracket
34
+
35
+ ```ruby
36
+ BracketGraph::DoubleEliminationGraph.new(bracket_size)
37
+ ```
38
+
39
+ ## About the Graph objects
40
+
41
+ ```ruby
42
+ graph.root # => BracketGraph::Seat for the final match
43
+ graph.winner_graph # => BracketGraph::Graph for the the winner bracket
44
+ graph.loser_graph # => BracketGraph::LoserGraph for the the loser bracket
45
+ graph[12] # => BracketGraph::Seat with id/position 12
46
+ graph.seed(teams) # => seeds each item in the given array to a starting node in the winner_graph
47
+ graph.seed(teams, shuffle: true) # => seeds teams after shuffle
48
+ ```
49
+
50
+ ### Winner Graph object
51
+
52
+ ```ruby
53
+ graph.winner_root # => BracketGraph::Seat for the final match of the winner bracket
54
+ graph.winner_seats # => Array[BracketGraph::Seat] all nodes of the winner bracket
55
+ graph.winner_starting_seats # => Array[BracketGraph::Seat] all starting nodes of the winner bracket
56
+ ```
57
+
58
+ ### Loser Graph object
59
+
60
+ ```ruby
61
+ graph.loser_root # => BracketGraph::Seat for the final match of the loser bracket
62
+ graph.loser_seats # => Array[BracketGraph::Seat] all nodes of the loser bracket
63
+ graph.loser_starting_seats # => Array[BracketGraph::Seat] all starting nodes of the loser bracket
64
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bracket_graph/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bracket_graph"
8
+ spec.version = BracketGraph::VERSION
9
+ spec.authors = ["Nicola Racco"]
10
+ spec.email = ["nicola@nicolaracco.com"]
11
+ spec.summary = %q{Amazing brackets JSON representations.}
12
+ spec.description = %q{Amazing, more or less, brackets in JSON.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activesupport"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.5"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "guard-rspec"
26
+ end
@@ -0,0 +1,12 @@
1
+ require "active_support/all"
2
+
3
+ require "bracket_graph/version"
4
+ require "bracket_graph/team_seeder"
5
+ require "bracket_graph/graph"
6
+ require "bracket_graph/loser_graph"
7
+ require "bracket_graph/seat"
8
+ require "bracket_graph/double_elimination_graph"
9
+
10
+ module BracketGraph
11
+ # Your code goes here...
12
+ end
@@ -0,0 +1,122 @@
1
+ module BracketGraph
2
+ class DoubleEliminationGraph
3
+ attr_reader :root
4
+ attr_reader :winner_graph, :loser_graph
5
+
6
+ def initialize root_or_size
7
+ if root_or_size.is_a? Seat
8
+ @root = root_or_size
9
+ @winner_graph = Graph.new @root.from[0]
10
+ @loser_graph = LoserGraph.new @root.from[1]
11
+ else
12
+ @winner_graph = Graph.new root_or_size
13
+ @loser_graph = LoserGraph.new root_or_size
14
+ sync_winner_rounds
15
+ sync_loser_rounds
16
+ build_final_seat
17
+ assign_loser_links
18
+ end
19
+ end
20
+
21
+ def [](position)
22
+ return root if position == root.position
23
+ if position < root.position
24
+ winner_graph[position]
25
+ else
26
+ loser_graph[position]
27
+ end
28
+ end
29
+
30
+ def size
31
+ winner_graph.size
32
+ end
33
+
34
+ %w(winner loser).each do |type|
35
+ define_method "#{type}_starting_seats" do
36
+ send("#{type}_graph").starting_seats
37
+ end
38
+
39
+ define_method "#{type}_seats" do
40
+ send("#{type}_graph").seats
41
+ end
42
+
43
+ define_method "#{type}_root" do
44
+ send("#{type}_graph").root
45
+ end
46
+ end
47
+
48
+ def seats
49
+ [root] + winner_seats + loser_seats
50
+ end
51
+
52
+ def starting_seats
53
+ winner_starting_seats + loser_starting_seats
54
+ end
55
+
56
+ def seed *args
57
+ winner_graph.seed *args
58
+ end
59
+
60
+ def as_json options={}
61
+ @root.as_json options
62
+ end
63
+
64
+ private
65
+
66
+ def build_final_seat
67
+ @root = Seat.new size * 2, round: loser_graph.root.round + 1
68
+ @root.from.concat [winner_graph.root, loser_graph.root]
69
+ @root.from.each { |s| s.to = @root }
70
+ end
71
+
72
+ def sync_winner_rounds
73
+ seats_by_round = winner_seats.inject({}) do |memo, seat|
74
+ memo[seat.round] ||= []
75
+ memo[seat.round] << seat
76
+ memo
77
+ end
78
+ 3.upto(winner_root.round) do |round|
79
+ seats_by_round[round].each do |r|
80
+ r.instance_variable_set '@round', 2 + (round - 2) * 2
81
+ end
82
+ end
83
+ end
84
+
85
+ def sync_loser_rounds
86
+ loser_graph.seats.each do |s|
87
+ s.instance_variable_set '@round', s.round + 1
88
+ end
89
+ end
90
+
91
+ def winner_matches_by_round
92
+ winner_graph.seats.
93
+ reject(&:starting?).
94
+ sort_by(&:position).
95
+ inject({}) do |memo, seat|
96
+ memo[seat.round] ||= []
97
+ memo[seat.round] << seat
98
+ memo
99
+ end
100
+ end
101
+
102
+ def loser_starting_seats_by_round
103
+ loser_graph.starting_seats.sort_by(&:position).inject({}) do |memo, seat|
104
+ memo[seat.round] ||= []
105
+ memo[seat.round] << seat
106
+ memo
107
+ end
108
+ end
109
+
110
+ def assign_loser_links
111
+ winner_matches = winner_matches_by_round
112
+ loser_candidates = loser_starting_seats_by_round
113
+ winner_matches.each do |round, matches|
114
+ candidates = loser_candidates[round]
115
+ candidates.reverse! if round.even?
116
+ matches.each do |match|
117
+ match.loser_to = candidates.pop
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,89 @@
1
+ module BracketGraph
2
+ class Graph
3
+ attr_reader :root
4
+ attr_reader :starting_seats, :seats
5
+
6
+ # Builds a new graph.
7
+ # The graph will be composed by a root seat and a match with two seats pointing to the root seat
8
+ # Each seat will then follows the same template (seat -> match -> 2 seats) until the generated
9
+ # last level seats (the starting seats) is equal to `size`.
10
+ #
11
+ # @param size [Fixnum|Seat] The number of orphan seats to generate, or the root node
12
+ # @raise [ArgumentError] if size is not a power of 2
13
+ def initialize root_or_size
14
+ if root_or_size.is_a? Seat
15
+ @root = root_or_size
16
+ update_references
17
+ else
18
+ raise ArgumentError, 'the given size is not a power of 2' if Math.log2(root_or_size) % 1 != 0
19
+ build_tree root_or_size
20
+ end
21
+ end
22
+
23
+ def [](position)
24
+ seats.detect { |s| s.position == position }
25
+ end
26
+
27
+ # Number of the starting seats
28
+ def size
29
+ starting_seats.size
30
+ end
31
+
32
+ # Fills the starting seats with the given `teams`
33
+ #
34
+ # @param teams [Array] Teams to place as payload in the starting seats
35
+ # @param shuffle [true, false] Indicates if teams shoud be shuffled
36
+ # @raise [ArgumentError] if `teams.count` is greater then `#size`
37
+ def seed teams, shuffle: false
38
+ raise ArgumentError, "Only a maximum of #{size} teams is allowed" if teams.size > size
39
+ slots = TeamSeeder.new(teams, size, shuffle: shuffle).slots
40
+ starting_seats.sort_by(&:position).each do |seat|
41
+ seat.payload = slots.shift
42
+ end
43
+ end
44
+
45
+ def as_json *attrs
46
+ @root.as_json *attrs
47
+ end
48
+
49
+ private
50
+
51
+ def build_tree size
52
+ build_tree! size
53
+ update_references
54
+ end
55
+
56
+ def build_tree! size
57
+ @root = Seat.new size, round: Math.log2(size).to_i
58
+ # Math.log2(size) indicates the graph depth
59
+ Math.log2(size).to_i.times.inject [root] do |seats|
60
+ seats.inject [] do |memo, seat|
61
+ memo.concat create_children_of seat
62
+ end
63
+ end
64
+
65
+ end
66
+
67
+ def update_references
68
+ @seats = [root]
69
+ @starting_seats = []
70
+ nodes = [root]
71
+ while nodes.any?
72
+ @seats.concat nodes = nodes.map(&:from).flatten
73
+ end
74
+ @starting_seats = @seats.select { |s| s.from.empty? }
75
+ end
76
+
77
+ # Builds a match as a source of this seat
78
+ # @raise [NoMethodError] if a source match has already been set
79
+ def create_children_of seat
80
+ raise NoMethodError, 'children already built' if seat.from.any?
81
+ parent_position = seat.to ? seat.to.position : 0
82
+ relative_position_halved = ((seat.position - parent_position) / 2).abs
83
+ seat.from.concat [
84
+ Seat.new(seat.position - relative_position_halved, to: seat),
85
+ Seat.new(seat.position + relative_position_halved, to: seat)
86
+ ]
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,56 @@
1
+ require "bracket_graph/graph"
2
+
3
+ module BracketGraph
4
+ class LoserGraph < Graph
5
+ class IdGenerator
6
+ attr_reader :current
7
+
8
+ def initialize starting_id = 0
9
+ @current = starting_id
10
+ end
11
+
12
+ def next
13
+ @current += 1
14
+ end
15
+ end
16
+
17
+ def initialize root_or_size
18
+ raise ArgumentError, 'a loser graph require at least 4 participants' if root_or_size.is_a?(Fixnum) && root_or_size < 4
19
+ super
20
+ end
21
+
22
+ def size
23
+ starting_seats.count + 1
24
+ end
25
+
26
+ private
27
+
28
+ def build_tree size
29
+ id_generator = IdGenerator.new size * 2 + 1
30
+ @root = Seat.new id_generator.next, round: 2 * Math.log2(size).to_i - 2
31
+ expected_rounds = 2 * (Math.log2(size).to_i - 1)
32
+ expected_rounds.times.inject [root] do |seats, round|
33
+ seats.each_with_index.inject [] do |memo, (seat, index)|
34
+ children = create_children_of seat, id_generator
35
+ side_count = (seats.count / 2.0).ceil
36
+ if round.even?
37
+ memo << children[index % side_count >= (side_count / 2.0).ceil ? 0 : 1]
38
+ else
39
+ memo.concat children
40
+ end
41
+ end
42
+ end
43
+ update_references
44
+ end
45
+
46
+ # Builds a match as a source of this seat
47
+ # @raise [NoMethodError] if a source match has already been set
48
+ def create_children_of seat, id_generator
49
+ raise NoMethodError, 'children already built' if seat.from.any?
50
+ seat.from.concat [
51
+ Seat.new(id_generator.next, to: seat),
52
+ Seat.new(id_generator.next, to: seat)
53
+ ]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,60 @@
1
+ module BracketGraph
2
+ class Seat
3
+ # Source match for this seat
4
+ attr_reader :from
5
+ # Seat position in the graph. It acts like an Id
6
+ attr_reader :position
7
+ # Seat payload. It should be used to keep track of the player and of its status in this seat
8
+ attr_accessor :payload
9
+ # Destination match of this seat.
10
+ attr_accessor :to
11
+ # Destination match of this seat for the loser participant.
12
+ attr_accessor :loser_to
13
+
14
+ # Creates a new seat for the bracket graph.
15
+ #
16
+ # @param position [Fixnum] Indicates the Seat position in the graph and acts like an Id
17
+ # @param to [BracketGraph::Match] The destination match. By default it's nil (and this node will act like the root node)
18
+ def initialize position, to: nil, round: nil
19
+ round ||= to.round - 1 if to
20
+ @position, @to, @round = position, to, round
21
+ @from = []
22
+ end
23
+
24
+ def starting?
25
+ from.nil? || from.empty?
26
+ end
27
+
28
+ def final?
29
+ to.nil?
30
+ end
31
+
32
+ # Graph depth until this level. If there is no destination it will return 0, otherwise it will return the destionation depth
33
+ def depth
34
+ @depth ||= to && to.depth + 1 || 0
35
+ end
36
+
37
+ # Round is the opposite of depth. While depth is 0 in the root node and Math.log2(size) at the lower level
38
+ # round is 0 at the lower level and Math.log2(size) in the root node
39
+ # While depth is memoized, round is calculated each time. If the seat has a source, it's the source round + 1, otherwise it's 0
40
+ def round
41
+ @round || (to ? to.round - 1 : 0)
42
+ end
43
+
44
+ def as_json options = {}
45
+ data = { position: position, round: round }
46
+ data.update payload: payload if payload
47
+ data.update loser_to: loser_to.position if loser_to
48
+ from && data.update(from: from.map(&:as_json)) || data
49
+ end
50
+
51
+ def inspect
52
+ """#<BracketGraph::Seat:#{position}
53
+ @from=#{from.map(&:position).inspect}
54
+ @round=#{round}
55
+ @to=#{(to && to.position || nil).inspect}
56
+ @loser_to=#{(loser_to && loser_to.position || nil).inspect}
57
+ @payload=#{payload.inspect}>"""
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,57 @@
1
+ class TeamSeeder
2
+ attr_reader :size
3
+
4
+ def initialize teams, size, shuffle: false
5
+ @teams = shuffle && teams.shuffle || teams.dup
6
+ @size = size
7
+ end
8
+
9
+ def slots
10
+ return @slots if @slots
11
+ @slots = [true] * size
12
+ seed_byes
13
+ seed_teams
14
+ @slots
15
+ end
16
+
17
+ private
18
+
19
+ def seed_byes
20
+ byes_to_seed = size - @teams.length
21
+ return if byes_to_seed == 0
22
+ @slots[0] = nil
23
+ seed_byes_by_partition byes_to_seed - 1 if byes_to_seed > 1
24
+ end
25
+
26
+ def seed_teams
27
+ @slots.each_with_index do |slot_value, index|
28
+ @slots[index] = @teams.shift if slot_value == true
29
+ end
30
+ end
31
+
32
+ def seed_byes_by_partition byes
33
+ partition = nil
34
+ byes.times do
35
+ partition = largest_bye_partition
36
+ mid_index = partition.min + ((partition.max.to_f - partition.min) / 2).ceil
37
+ @slots[mid_index] = nil
38
+ end
39
+ end
40
+
41
+ def largest_bye_partition
42
+ start_index, end_index = nil, nil
43
+ prev_index = 0
44
+ @slots.each_with_index do |value, index|
45
+ next if index > 0 && index < @slots.count - 1 && value
46
+ if start_index.nil?
47
+ start_index = index
48
+ elsif end_index.nil?
49
+ end_index = index
50
+ elsif index - prev_index > end_index - start_index
51
+ start_index, end_index = prev_index, index
52
+ end
53
+ prev_index = index
54
+ end
55
+ Range.new start_index, end_index
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module BracketGraph
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,153 @@
1
+ require 'spec_helper'
2
+
3
+ describe BracketGraph::DoubleEliminationGraph do
4
+ it 'creates a graph composed by winner and loser graphs' do
5
+ subject = described_class.new 8
6
+ expect(subject.winner_graph).to be_a BracketGraph::Graph
7
+ expect(subject.loser_graph).to be_a BracketGraph::LoserGraph
8
+ end
9
+
10
+ it 'creates both the sub-graphs with the same size' do
11
+ subject = described_class.new 8
12
+ expect(subject.winner_graph.size).to eq 8
13
+ expect(subject.loser_graph.size).to eq 8
14
+ end
15
+
16
+ it 'creates a real final node' do
17
+ subject = described_class.new 8
18
+ expect(subject.root).to be_a BracketGraph::Seat
19
+ end
20
+
21
+ it 'binds both the sub-graph roots as children of the real final node' do
22
+ subject = described_class.new 8
23
+ expect(subject.winner_graph.root.to).to eq subject.root
24
+ expect(subject.loser_graph.root.to).to eq subject.root
25
+ expect(subject.root.from).to eq [subject.winner_graph.root, subject.loser_graph.root]
26
+ end
27
+
28
+ it 'creates the final node with doubled size as position' do
29
+ subject = described_class.new 8
30
+ expect(subject.root.position).to eq 16
31
+ end
32
+
33
+ it 'creates the final node in the last round' do
34
+ subject = described_class.new 8
35
+ expect(subject.root.round).to eq 6
36
+ end
37
+
38
+ it 'syncs the rounds of the winner bracket' do
39
+ subject = described_class.new 16
40
+ memo = subject.winner_graph.seats.inject(Hash.new { |h, k| h[k] = [] }) do |m, s|
41
+ m[s.round] << s
42
+ m
43
+ end
44
+ expect(memo[0].count).to eq 16
45
+ expect(memo[1].count).to eq 8
46
+ expect(memo[2].count).to eq 4
47
+ expect(memo[3].count).to be_zero
48
+ expect(memo[4].count).to eq 2
49
+ expect(memo[5].count).to be_zero
50
+ expect(memo[6].count).to eq 1
51
+ end
52
+
53
+ it 'syncs the rounds of the loser bracket' do
54
+ subject = described_class.new 16
55
+ memo = subject.loser_graph.seats.inject(Hash.new { |h, k| h[k] = [] }) do |m, s|
56
+ m[s.round] << s
57
+ m
58
+ end
59
+ expect(memo[0].count).to be_zero
60
+ expect(memo[1].count).to eq 8
61
+ expect(memo[2].count).to eq 8
62
+ expect(memo[3].count).to eq 4
63
+ expect(memo[4].count).to eq 4
64
+ expect(memo[5].count).to eq 2
65
+ expect(memo[6].count).to eq 2
66
+ expect(memo[7].count).to eq 1
67
+ end
68
+
69
+ it 'after the sync the winner final is one round behind the real final' do
70
+ subject = described_class.new 16
71
+ expect(subject.loser_graph.root.round).to eq 7
72
+ expect(subject.winner_graph.root.round).to eq 6
73
+ end
74
+
75
+ describe '#size' do
76
+ it 'returns the right size' do
77
+ subject = described_class.new 8
78
+ expect(subject.size).to eq 8
79
+ end
80
+ end
81
+
82
+ describe '#starting_seats' do
83
+ it 'returns the sum of starting seats' do
84
+ subject = described_class.new 8
85
+ expect(subject.starting_seats).to match_array subject.winner_graph.starting_seats + subject.loser_graph.starting_seats
86
+ end
87
+ end
88
+
89
+ describe '#seats' do
90
+ it 'returns the sum of seats' do
91
+ subject = described_class.new 8
92
+ expect(subject.seats).to match_array [subject.root] + subject.winner_graph.seats + subject.loser_graph.seats
93
+ end
94
+
95
+ it 'returns the root node too' do
96
+ subject = described_class.new 8
97
+ expect(subject.seats).to include subject.root
98
+ end
99
+ end
100
+
101
+ describe '#seed' do
102
+ it 'delegates to the winner graph' do
103
+ subject = described_class.new 8
104
+ allow(subject.winner_graph).to receive(:seed).and_return 'foo'
105
+ expect(subject.seed).to eq 'foo'
106
+ end
107
+ end
108
+
109
+ it 'correctly dumps to json' do
110
+ subject = described_class.new(4).as_json
111
+ expect(subject).to be_a Hash
112
+ expect(subject[:from]).to be_a Array
113
+ end
114
+
115
+ it 'correctly saves and restores' do
116
+ data = Marshal::dump described_class.new(4)
117
+ subject = Marshal::load data
118
+ expect(subject.starting_seats.count).to eq 7
119
+ end
120
+
121
+ it 'assigns a loser to to each match' do
122
+ subject = described_class.new 16
123
+ candidates = subject.winner_seats - subject.winner_starting_seats
124
+ expect(candidates.select(&:loser_to).count).to eq candidates.count
125
+ end
126
+
127
+ it 'assigns only loser starting seats in the loser relationship' do
128
+ subject = described_class.new 16
129
+ candidates = subject.winner_seats - subject.winner_starting_seats
130
+ expect(candidates.map(&:loser_to)).to match_array subject.loser_starting_seats
131
+ end
132
+
133
+ it 'assigns each loser_to to a different seat' do
134
+ subject = described_class.new 16
135
+ candidates = subject.winner_seats - subject.winner_starting_seats
136
+ expect(candidates.map(&:loser_to).uniq.count).to eq candidates.count
137
+ end
138
+
139
+ it 'assigns loser_to links in a different order using the round oddity' do
140
+ subject = described_class.new 16
141
+ candidates = (subject.winner_seats - subject.winner_starting_seats).sort_by &:position
142
+ (1..subject.winner_root.round).each do |round|
143
+ round_candidates_positions = candidates.
144
+ select { |s| s.round == round }.
145
+ map { |c| c.loser_to.position }
146
+ if round.odd?
147
+ expect(round_candidates_positions).to eq round_candidates_positions.sort.reverse
148
+ else
149
+ expect(round_candidates_positions).to eq round_candidates_positions.sort
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,165 @@
1
+ require 'spec_helper'
2
+
3
+ describe BracketGraph::Graph do
4
+ describe 'constructor' do
5
+ it 'creates instance with a certain size' do
6
+ expect { described_class.new 128 }.to_not raise_error
7
+ end
8
+
9
+ it 'raises error if size is not a power of 2' do
10
+ expect { described_class.new 3 }.to raise_error ArgumentError
11
+ end
12
+
13
+ it 'creates a root seat node' do
14
+ subject = described_class.new 4
15
+ expect(subject.root).to be_a BracketGraph::Seat
16
+ end
17
+
18
+ it 'appends two seats as input in the root match node' do
19
+ subject = described_class.new 4
20
+ expect(subject.root.from.map(&:class)).to eq [BracketGraph::Seat,BracketGraph::Seat]
21
+ end
22
+
23
+ it 'follows this pattern until the last level children count is equal to the graph size' do
24
+ subject = described_class.new 128
25
+ nodes = 7.times.inject([subject.root]) do |current_nodes|
26
+ current_nodes.inject([]) do |memo, node|
27
+ expect(node.from.count).to eq 2
28
+ memo.concat node.from
29
+ end
30
+ end
31
+ expect(nodes.count).to eq 128
32
+ nodes.each { |node| expect(node.from).to be_empty }
33
+ end
34
+
35
+ it 'sets depths starting from 0' do
36
+ subject = described_class.new 128
37
+ expect(subject.root.depth).to eq 0
38
+ end
39
+
40
+ it 'sets depths ending to log2 of size' do
41
+ subject = described_class.new 128
42
+ expect(subject.starting_seats.map(&:depth).uniq).to eq [7]
43
+ end
44
+
45
+ it 'sets rounds starting from log2 of size' do
46
+ subject = described_class.new 128
47
+ expect(subject.root.round).to eq 7
48
+ end
49
+
50
+ it 'sets rounds ending to 0' do
51
+ subject = described_class.new 128
52
+ expect(subject.starting_seats.map(&:round).uniq).to eq [0]
53
+ end
54
+
55
+ it 'sets root position to size' do
56
+ subject = described_class.new 64
57
+ expect(subject.root.position).to eq 64
58
+ end
59
+
60
+ it 'sets source seats (through the match) to size - (size / 2) and size + (size / 2)' do
61
+ subject = described_class.new 64
62
+ children = subject.root.from
63
+ expect(children.map(&:position).sort).to eq [32,96]
64
+ end
65
+
66
+ it 'does not duplicate positions' do
67
+ subject = described_class.new 128
68
+ expect(subject.seats.map(&:position).uniq.count).to eq subject.seats.count
69
+ end
70
+
71
+ it 'creates a graph given its root node' do
72
+ existing = described_class.new 4
73
+ subject = described_class.new existing.root
74
+ expect(subject.starting_seats).to eq existing.starting_seats
75
+ end
76
+
77
+ it 'always sets the children by position order' do
78
+ subject = described_class.new 32
79
+ positions_groups = subject.seats.map { |s| s.from.map &:position }
80
+ positions_groups.each do |position_group|
81
+ expect(position_group).to eq position_group.sort
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#starting_seats' do
87
+ it 'returns a collection of the given size' do
88
+ subject = described_class.new 128
89
+ expect(subject.starting_seats.count).to eq 128
90
+ end
91
+
92
+ it 'returns a collection of seats' do
93
+ subject = described_class.new 8
94
+ expect(subject.starting_seats.map(&:class).uniq).to eq [BracketGraph::Seat]
95
+ end
96
+
97
+ it 'returns the last level seats' do
98
+ subject = described_class.new 8
99
+ subject.starting_seats.each do |seat|
100
+ expect(seat.from).to be_empty
101
+ end
102
+ end
103
+ end
104
+
105
+ describe '#seed' do
106
+ it 'raises error if there are more teams than starting seats' do
107
+ subject = described_class.new 4
108
+ expect { subject.seed [1,2,3,4,5] }.to raise_error ArgumentError
109
+ end
110
+
111
+ it 'assigns the given teams to the starting seats' do
112
+ subject = described_class.new 4
113
+ subject.seed [1,2,3,4]
114
+ expect(subject.starting_seats.map(&:payload)).to match_array [1,2,3,4]
115
+ end
116
+
117
+ it 'does not change the given array' do
118
+ subject = described_class.new 4
119
+ array = [1,2,3,4]
120
+ expect { subject.seed array }.to_not change array, :count
121
+ end
122
+
123
+ it 'fills seats by position' do
124
+ subject = described_class.new 4
125
+ subject.seed [1,2,3,4]
126
+ expect(subject.starting_seats.sort_by(&:position).map(&:payload)).to eq [1,2,3,4]
127
+ end
128
+
129
+ it 'leaves remaining seats with a nil payload' do
130
+ subject = described_class.new 4
131
+ subject.seed [1,2,3]
132
+ expect(subject.starting_seats.sort_by(&:position).map(&:payload)).to eq [nil,1,2,3]
133
+ end
134
+
135
+ it 'uses the TeamSeeder' do
136
+ subject = described_class.new 4
137
+ expect_any_instance_of(TeamSeeder).to receive(:slots).and_return []
138
+ subject.seed [1,2,3,4], shuffle: true
139
+ end
140
+ end
141
+
142
+ describe '#[]' do
143
+ it 'return the seat with the given position' do
144
+ subject = described_class.new 8
145
+ expect(subject[6].position).to eq 6
146
+ end
147
+ end
148
+
149
+ describe '#seats' do
150
+ it 'returns seats' do
151
+ subject = described_class.new 8
152
+ expect(subject.seats.map(&:class).uniq).to eq [BracketGraph::Seat]
153
+ end
154
+
155
+ it 'returns all generated seats' do
156
+ subject = described_class.new 8
157
+ expect(subject.seats.count).to eq 15
158
+ end
159
+
160
+ it 'returns the root node too' do
161
+ subject = described_class.new 8
162
+ expect(subject.seats).to include subject.root
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe BracketGraph::LoserGraph do
4
+ describe 'constructor' do
5
+ it 'creates instance with a certain size' do
6
+ expect { described_class.new 128 }.to_not raise_error
7
+ end
8
+
9
+ it 'raises error if size is not a power of 2' do
10
+ expect { described_class.new 5 }.to raise_error ArgumentError
11
+ end
12
+
13
+ it 'raises error if size is lower than 4' do
14
+ expect { described_class.new 2 }.to raise_error ArgumentError
15
+ end
16
+
17
+ it 'creates a root seat node' do
18
+ subject = described_class.new 4
19
+ expect(subject.root).to be_a BracketGraph::Seat
20
+ end
21
+
22
+ it 'appends two seats as input in the root match node' do
23
+ subject = described_class.new 4
24
+ expect(subject.root.from.map(&:class)).to eq [BracketGraph::Seat,BracketGraph::Seat]
25
+ end
26
+
27
+ it 'one of the root children is a starting seat' do
28
+ subject = described_class.new 4
29
+ expect(subject.root.from.map(&:from)).to include []
30
+ end
31
+
32
+ it 'the children of the match child of root are starting seats' do
33
+ subject = described_class.new 4
34
+ expect(subject.root.from.map(&:from).flatten.map(&:from).flatten).to be_empty
35
+ end
36
+
37
+ it 'follows this pattern until the last level children count is equal to the graph size' do
38
+ subject = described_class.new 128
39
+ nodes = 6.times.inject([subject.root]) do |current_nodes|
40
+ current_nodes.inject([]) do |memo, node|
41
+ expect(node.from.count).to eq 2
42
+ sub_children = node.from.map &:from
43
+ expect(sub_children).to include []
44
+ expect(sub_children.flatten.count).to eq 2
45
+ memo.concat sub_children.flatten
46
+ end
47
+ end
48
+ expect(nodes.count).to eq 64
49
+ nodes.each { |node| expect(node.from).to be_empty }
50
+ end
51
+
52
+ it 'sets depths starting from 0' do
53
+ subject = described_class.new 128
54
+ expect(subject.root.depth).to eq 0
55
+ end
56
+
57
+ it 'contains starting seats in even seats' do
58
+ subject = described_class.new 128
59
+ expect(subject.starting_seats.map(&:depth).uniq).to eq [1,3,5,7,9,11,12]
60
+ end
61
+
62
+ it 'sets rounds starting from 2 per log2 of size - 1' do
63
+ subject = described_class.new 128
64
+ expect(subject.root.round).to eq 12
65
+ end
66
+
67
+ it 'contains starting seats in odd seats' do
68
+ subject = described_class.new 128
69
+ expect(subject.starting_seats.map(&:round).uniq).to eq [11,9,7,5,3,1,0]
70
+ end
71
+
72
+ it 'sets root position to doubled size + 2' do
73
+ subject = described_class.new 64
74
+ expect(subject.root.position).to eq 130
75
+ end
76
+
77
+ it 'sets the position of root children to root position +1 and +2' do
78
+ subject = described_class.new 64
79
+ expect(subject.root.from.map(&:position)).to match_array [131,132]
80
+ end
81
+
82
+ it 'sets source seats (through the match) positions' do
83
+ subject = described_class.new 64
84
+ children = subject.root.from[1].from
85
+ expect(children.map(&:position).sort).to eq [133, 134]
86
+ end
87
+
88
+ it 'does not duplicate positions' do
89
+ subject = described_class.new 16
90
+ expect(subject.seats.map(&:position).uniq.count).to eq subject.seats.count
91
+ end
92
+
93
+ it 'creates a graph given its root node' do
94
+ existing = described_class.new 4
95
+ subject = described_class.new existing.root
96
+ expect(subject.starting_seats).to eq existing.starting_seats
97
+ end
98
+ end
99
+
100
+ describe '#starting_seats' do
101
+ it 'returns a collection of the given size - 1' do
102
+ subject = described_class.new 128
103
+ expect(subject.starting_seats.count).to eq 127
104
+ end
105
+
106
+ it 'returns a collection of seats' do
107
+ subject = described_class.new 8
108
+ expect(subject.starting_seats.map(&:class).uniq).to eq [BracketGraph::Seat]
109
+ end
110
+
111
+ it 'returns the last level seats' do
112
+ subject = described_class.new 8
113
+ subject.starting_seats.each do |seat|
114
+ expect(seat.from).to be_empty
115
+ end
116
+ end
117
+ end
118
+
119
+ describe '#[]' do
120
+ it 'return the seat with the given position' do
121
+ subject = described_class.new 8
122
+ expect(subject[22].position).to eq 22
123
+ end
124
+ end
125
+
126
+ describe '#seats' do
127
+ it 'returns seats' do
128
+ subject = described_class.new 8
129
+ expect(subject.seats.map(&:class).uniq).to eq [BracketGraph::Seat]
130
+ end
131
+
132
+ it 'returns all generated seats' do
133
+ subject = described_class.new 8
134
+ expect(subject.seats.count).to eq 13
135
+ end
136
+
137
+ it 'returns the root node too' do
138
+ subject = described_class.new 8
139
+ expect(subject.seats).to include subject.root
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ describe BracketGraph::Seat do
4
+ let(:subject_class) { BracketGraph::Seat }
5
+ let(:subject) { subject_class.new 10 }
6
+
7
+ describe 'constructor' do
8
+ it 'raises error if position is nil' do
9
+ expect { subject_class.new }.to raise_error ArgumentError
10
+ end
11
+
12
+ it 'requires position' do
13
+ subject = subject_class.new 10
14
+ expect(subject.position).to eq 10
15
+ end
16
+
17
+ it 'accepts the destination' do
18
+ dest = subject_class.new 12
19
+ expect(subject_class.new(10, to: dest).to).to eq dest
20
+ end
21
+
22
+ it 'allows the destination to not be set' do
23
+ expect { subject_class.new 10 }.to_not raise_error
24
+ end
25
+ end
26
+
27
+ describe '#depth' do
28
+ it 'is 0 when the seat has no destination' do
29
+ expect(subject_class.new(10).depth).to eq 0
30
+ end
31
+
32
+ it 'equals destination_depth + 1when destination is set' do
33
+ destination = double 11, depth: 10, round: 9
34
+ expect(subject_class.new(10, to: destination).depth).to eq 11
35
+ end
36
+ end
37
+
38
+ describe '#round' do
39
+ it 'returns 0 if seat has no source' do
40
+ expect(subject_class.new(10).round).to be_zero
41
+ end
42
+
43
+ it 'returns parent round - 1 if a parent exists' do
44
+ allow(subject).to receive(:to) { double(round: 10) }
45
+ expect(subject.round).to eq 9
46
+ end
47
+ end
48
+
49
+ describe '#as_json' do
50
+ subject { described_class.new 10, round: 10 }
51
+
52
+ it 'returns a json representation' do
53
+ expect { subject.as_json }.to_not raise_error
54
+ end
55
+
56
+ it 'returns position' do
57
+ expect(subject.as_json[:position]).to eq 10
58
+ end
59
+
60
+ it 'returns source matches' do
61
+ subject.from[0] = described_class.new 11, to: subject
62
+ expect(subject.as_json.key? :from).to be_truthy
63
+ end
64
+
65
+ it 'returns payload' do
66
+ subject.payload = { id: 9 }
67
+ expect(subject.as_json[:payload]).to eq :id => 9
68
+ end
69
+ end
70
+
71
+ describe '#starting?' do
72
+ subject { described_class.new 10, round: 10 }
73
+
74
+ it 'returns true if there are no children' do
75
+ expect(subject).to be_starting
76
+ end
77
+
78
+ it 'returns false if there are children' do
79
+ subject.from << double
80
+ expect(subject).not_to be_starting
81
+ end
82
+
83
+ it 'returns true if #from is nil' do
84
+ subject.instance_variable_set '@from', nil
85
+ expect(subject).to be_starting
86
+ end
87
+ end
88
+
89
+ describe '#final?' do
90
+ subject { described_class.new 10, round: 10 }
91
+
92
+ it 'returns true if there is no parent seat' do
93
+ expect(subject).to be_final
94
+ end
95
+
96
+ it 'returns false if there is a parent seat' do
97
+ subject.to = double
98
+ expect(subject).not_to be_final
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe TeamSeeder do
4
+ it 'shuffles teams if shuffle is true' do
5
+ subject = TeamSeeder.new ['a','b','c'], 4, shuffle: true
6
+ teams = subject.instance_variable_get '@teams'
7
+ expect(teams).to_not eq ['a','b','c']
8
+ end
9
+
10
+ it 'does not shuffle teams if shuffle is false' do
11
+ subject = TeamSeeder.new ['a','b','c'], 4
12
+ teams = subject.instance_variable_get '@teams'
13
+ expect(teams).to eq ['a','b','c']
14
+ end
15
+
16
+ describe '#slots' do
17
+ let(:teams) { %w(a b c d e f g h i j k l m) }
18
+ subject { TeamSeeder.new teams, 16 }
19
+
20
+ it 'returns an array' do
21
+ expect(subject.slots).to be_a Array
22
+ end
23
+
24
+ it 'returns an array of {slots} length' do
25
+ expect(subject.slots.length).to eq 16
26
+ end
27
+
28
+ it 'inserts {slots} - {teams} byes' do
29
+ expect(subject.slots.select(&:nil?).count).to eq 3
30
+ end
31
+
32
+ it 'inserts the first bye at the first position' do
33
+ expect(subject.slots[0]).to be_nil
34
+ end
35
+
36
+ it 'inserts byes using the mid point of the large partition recursively' do
37
+ expect(subject.slots).to eq [nil, 'a', 'b', 'c', nil, 'd', 'e', 'f', nil,
38
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm']
39
+ end
40
+ end
41
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path '../../lib/bracket_graph', __FILE__
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bracket_graph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Nicola Racco
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Amazing, more or less, brackets in JSON.
70
+ email:
71
+ - nicola@nicolaracco.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - Gemfile
79
+ - Guardfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - bracket_graph.gemspec
84
+ - lib/bracket_graph.rb
85
+ - lib/bracket_graph/double_elimination_graph.rb
86
+ - lib/bracket_graph/graph.rb
87
+ - lib/bracket_graph/loser_graph.rb
88
+ - lib/bracket_graph/seat.rb
89
+ - lib/bracket_graph/team_seeder.rb
90
+ - lib/bracket_graph/version.rb
91
+ - spec/lib/bracket_graph/double_elimination_graph_spec.rb
92
+ - spec/lib/bracket_graph/graph_spec.rb
93
+ - spec/lib/bracket_graph/loser_graph_spec.rb
94
+ - spec/lib/bracket_graph/seat_spec.rb
95
+ - spec/lib/bracket_graph/team_seeder_spec.rb
96
+ - spec/spec_helper.rb
97
+ homepage: ''
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.4.7
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Amazing brackets JSON representations.
121
+ test_files:
122
+ - spec/lib/bracket_graph/double_elimination_graph_spec.rb
123
+ - spec/lib/bracket_graph/graph_spec.rb
124
+ - spec/lib/bracket_graph/loser_graph_spec.rb
125
+ - spec/lib/bracket_graph/seat_spec.rb
126
+ - spec/lib/bracket_graph/team_seeder_spec.rb
127
+ - spec/spec_helper.rb