bracket_graph 0.0.2
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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +64 -0
- data/Rakefile +1 -0
- data/bracket_graph.gemspec +26 -0
- data/lib/bracket_graph.rb +12 -0
- data/lib/bracket_graph/double_elimination_graph.rb +122 -0
- data/lib/bracket_graph/graph.rb +89 -0
- data/lib/bracket_graph/loser_graph.rb +56 -0
- data/lib/bracket_graph/seat.rb +60 -0
- data/lib/bracket_graph/team_seeder.rb +57 -0
- data/lib/bracket_graph/version.rb +3 -0
- data/spec/lib/bracket_graph/double_elimination_graph_spec.rb +153 -0
- data/spec/lib/bracket_graph/graph_spec.rb +165 -0
- data/spec/lib/bracket_graph/loser_graph_spec.rb +142 -0
- data/spec/lib/bracket_graph/seat_spec.rb +101 -0
- data/spec/lib/bracket_graph/team_seeder_spec.rb +41 -0
- data/spec/spec_helper.rb +1 -0
- metadata +127 -0
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
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|