bracket_graph 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|