bracket_tree 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/README.md +187 -0
- data/Rakefile +8 -0
- data/bracket_tree.gemspec +20 -0
- data/lib/bracket_tree.rb +9 -0
- data/lib/bracket_tree/bracket.rb +12 -0
- data/lib/bracket_tree/bracket/base.rb +228 -0
- data/lib/bracket_tree/bracket/double_elimination.rb +7 -0
- data/lib/bracket_tree/bracket/single_elimination.rb +7 -0
- data/lib/bracket_tree/match.rb +24 -0
- data/lib/bracket_tree/node.rb +23 -0
- data/lib/bracket_tree/template.rb +70 -0
- data/lib/bracket_tree/templates/double_elimination.rb +11 -0
- data/lib/bracket_tree/templates/double_elimination/128.json +3695 -0
- data/lib/bracket_tree/templates/double_elimination/16.json +107 -0
- data/lib/bracket_tree/templates/double_elimination/32.json +202 -0
- data/lib/bracket_tree/templates/double_elimination/4.json +26 -0
- data/lib/bracket_tree/templates/double_elimination/64.json +400 -0
- data/lib/bracket_tree/templates/double_elimination/8.json +50 -0
- data/lib/bracket_tree/templates/single_elimination.rb +11 -0
- data/lib/bracket_tree/templates/single_elimination/128.json +1917 -0
- data/lib/bracket_tree/templates/single_elimination/16.json +56 -0
- data/lib/bracket_tree/templates/single_elimination/2.json +11 -0
- data/lib/bracket_tree/templates/single_elimination/32.json +351 -0
- data/lib/bracket_tree/templates/single_elimination/4.json +17 -0
- data/lib/bracket_tree/templates/single_elimination/64.json +703 -0
- data/lib/bracket_tree/templates/single_elimination/8.json +29 -0
- data/spec/bracket_spec.rb +189 -0
- data/spec/double_elimination_spec.rb +5 -0
- data/spec/match_spec.rb +30 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/template_spec.rb +58 -0
- metadata +112 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
# BracketTree [](http://travis-ci.org/agoragames/bracket_tree)
|
2
|
+
|
3
|
+
BracketTree is a bracketing system built around the BracketTree Data Specification,
|
4
|
+
which uses a three-section data structure built on top of JSON to convey the visual
|
5
|
+
representation, progression logic, and seed mapping in a serializable format. For
|
6
|
+
more information on the data specification, please read the
|
7
|
+
[BracketTree Data Specification](https://github.com/agoragames/bracket_tree/wiki/BracketTree-Data-Specification).
|
8
|
+
|
9
|
+
BracketTree builds upon the specification by providing Ruby classes for programmatically
|
10
|
+
generating templates for brackets and generating brackets from those templates. It
|
11
|
+
also contains a number of common bracket template types like Single Elimination and
|
12
|
+
Double Elimination, with the ability to put your own extensions on their logic and
|
13
|
+
representation.
|
14
|
+
|
15
|
+
BracketTree is broken into two fundamental components: Templates and Brackets.
|
16
|
+
|
17
|
+
## BracketTree Templates
|
18
|
+
|
19
|
+
Templates in BracketTree are the instructions on how a Bracket is to be constructed,
|
20
|
+
containing the three components of a BracketTree specification:
|
21
|
+
|
22
|
+
- `starting_seats`
|
23
|
+
- `seats`
|
24
|
+
- `nodes`
|
25
|
+
|
26
|
+
BracketTree comes with a number of default templates included. To access one, call
|
27
|
+
the `by_size` method on the template class. For example, to generate an eight-player,
|
28
|
+
double-elimination bracket template:
|
29
|
+
|
30
|
+
```
|
31
|
+
template = BracketTree::Template::DoubleElimination.by_size(8)
|
32
|
+
```
|
33
|
+
|
34
|
+
The resulting BracketTree::Template::DoubleElimination object contains the necessary
|
35
|
+
details to create a bracket using its information.
|
36
|
+
|
37
|
+
If you need to make customizations, you can manipulate the template per object, like
|
38
|
+
in this example where we reverse the seed order of the template:
|
39
|
+
|
40
|
+
```
|
41
|
+
template = BracketTree::Template::DoubleElimination.by_size(8)
|
42
|
+
template.starting_seats # => [1,3,5,7,9,11,13,15]
|
43
|
+
template.starting_seats = [15,13,11,9,7,5,3,1]
|
44
|
+
```
|
45
|
+
|
46
|
+
However, you may wish to generate your own Template class. To do so, subclass the
|
47
|
+
`BracketTree::Template::Base` class and define `location` to be the location of the
|
48
|
+
JSON files that conform to the [BracketTree Data Specification](https://github.com/agoragames/bracket_tree/wiki/BracketTree-Data-Specification).
|
49
|
+
In this example, we create a class for the MLG Double Elimination format, where the
|
50
|
+
templates are located in the `mlg_double` directory:
|
51
|
+
|
52
|
+
```
|
53
|
+
class BracketTree::Template::MLGDouble < BracketTree::Template::Base
|
54
|
+
def location ; File.join File.dirname(__FILE__), 'mlg_double' ; end
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
If you happen to have the JSON already stored as a hash and want to create a Template
|
59
|
+
from that, you can use the `from_json` method to generate a new template:
|
60
|
+
|
61
|
+
```
|
62
|
+
hash = {
|
63
|
+
'startingSeats' => [1,3,5,7],
|
64
|
+
'seats' => [
|
65
|
+
{ 'position' => 4 },
|
66
|
+
{ 'position' => 2 },
|
67
|
+
{ 'position' => 6 },
|
68
|
+
{ 'position' => 1 },
|
69
|
+
{ 'position' => 3 },
|
70
|
+
{ 'position' => 5 },
|
71
|
+
{ 'position' => 7 }
|
72
|
+
],
|
73
|
+
'nodes' => []
|
74
|
+
}
|
75
|
+
|
76
|
+
template = BracketTree::Template::Base.from_json hash
|
77
|
+
```
|
78
|
+
|
79
|
+
## BracketTree Brackets
|
80
|
+
|
81
|
+
Brackets are derived from BracketTree::Bracket::Base or its sub-classes. BracketTree
|
82
|
+
provides two subclasses, `BracketTree::Bracket::SingleElimination` and
|
83
|
+
`BracketTree::Bracket::DoubleElimination`, to quickly generate blank brackets based on
|
84
|
+
the popular standard formats:
|
85
|
+
|
86
|
+
```
|
87
|
+
single_elim_bracket = BracketTree::Bracket::SingleElimination.by_size 4
|
88
|
+
double_elim_bracket = BracketTree::Bracket::DoubleElimination.by_size 64
|
89
|
+
```
|
90
|
+
|
91
|
+
For those who wish to create a custom bracket Template class, doing so is straightforward.
|
92
|
+
Create a subclass of `BracketTree::Bracket::Base` and use the `template` method to
|
93
|
+
specify your template class, like the below `MLGDouble` bracket:
|
94
|
+
|
95
|
+
```
|
96
|
+
class MLGDoubleTemplate < BracketTree::Template::Base
|
97
|
+
def self.location
|
98
|
+
File.join File.dirname(__FILE__), 'templates', 'mlg_double'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class MLGDouble < BracketTree::Bracket::Base
|
103
|
+
template MLGDoubleTemplate
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
Once we've generated a bracket from a template, we're able to start populating and
|
108
|
+
controlling the bracket information. All bracket objects derive from
|
109
|
+
`BracketTree::Bracket::Base`. If you generate a bracket from this class, it will
|
110
|
+
have a blank binary tree. If you happen to know the math involved in hand-crafting a
|
111
|
+
binary tree reflective of your particular tournament type, then you could use `add`
|
112
|
+
to start adding nodes in the bracket:
|
113
|
+
|
114
|
+
```
|
115
|
+
bracket = BracketTree::Bracket::Base.new
|
116
|
+
bracket.add 2, { player: 'player1' }
|
117
|
+
bracket.add 1, { player: 'player1' }
|
118
|
+
bracket.add 3, { player: 'player2' }
|
119
|
+
```
|
120
|
+
|
121
|
+
While this is not the most difficult thing to do on a small scale, doing this for
|
122
|
+
larger tournaments is extremely cumbersome, so we generate Brackets from Templates
|
123
|
+
instead. Please review 'BracketTree Templates' for more information on this.
|
124
|
+
|
125
|
+
When you generate a blank bracket from a template, it adds empty hashes as
|
126
|
+
placeholders for all of the seats in the Bracket. To replace these placeholders, use
|
127
|
+
the `replace` method with the seat position and the object you would like to replace
|
128
|
+
it with.
|
129
|
+
|
130
|
+
In this example, we handle seeding a two-player, single elimination bracket:
|
131
|
+
|
132
|
+
```
|
133
|
+
bracket = BracketTree::Bracket::SingleElimination.by_size 2
|
134
|
+
bracket.replace 1, { player: 'player1' }
|
135
|
+
bracket.replace 3, { player: 'player3' }
|
136
|
+
```
|
137
|
+
|
138
|
+
Again, this is not the most difficult thing to do, but seeding is a pretty common
|
139
|
+
thing. For actions like this, use the `seed` method.
|
140
|
+
|
141
|
+
Under the hood, each seat position in the Bracket is held as the payload of a `Node`
|
142
|
+
object. This contains the binary tree traversal controls, as well as a `payload`
|
143
|
+
property that contains the object being stored at the node. When using any iterator
|
144
|
+
methods from `Enumerable` on a bracket, know that they are in the context of a `Node`
|
145
|
+
rather than whatever you have chosen to store inside the `Node`. This allows the
|
146
|
+
following:
|
147
|
+
|
148
|
+
```
|
149
|
+
bracket = BracketTree::Bracket::SingleElimination.by_size 2
|
150
|
+
bracket.replace 2, { player: 'player1 }
|
151
|
+
|
152
|
+
node = bracket.at(2)
|
153
|
+
node.payload # => { player: 'player1' }
|
154
|
+
node.payload[:seed_value] = 3
|
155
|
+
```
|
156
|
+
|
157
|
+
## Example
|
158
|
+
|
159
|
+
Below is an example of creating a 32-player, double elimination tournament bracket
|
160
|
+
template, generating a blank bracket, seeding from an array of players, and filling
|
161
|
+
some results from round 1:
|
162
|
+
|
163
|
+
```
|
164
|
+
players = []
|
165
|
+
32.times do |n|
|
166
|
+
players << { login: "Player#{n}", seed_value: n }
|
167
|
+
end
|
168
|
+
|
169
|
+
bracket = BracketTree::Bracket::DoubleElimination.by_size 32
|
170
|
+
bracket.seed players
|
171
|
+
|
172
|
+
# Player1 wins Rd 1
|
173
|
+
bracket.match_winner(1)
|
174
|
+
bracket.at(1).payload[:winner] = true
|
175
|
+
|
176
|
+
# Player3 wins Rd 1
|
177
|
+
bracket.match_winner(5)
|
178
|
+
bracket.at(1).payload[:winner] = true
|
179
|
+
```
|
180
|
+
|
181
|
+
## Contributions
|
182
|
+
|
183
|
+
Contributions are awesome. Feature branch pull requests are the preferred method.
|
184
|
+
|
185
|
+
## Author
|
186
|
+
|
187
|
+
Written by [Andrew Nordman](http://github.com/cadwallion)
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "bracket_tree"
|
6
|
+
s.version = '0.1.0'
|
7
|
+
s.authors = ["Andrew Nordman"]
|
8
|
+
s.email = ["anordman@majorleaguegaming.com"]
|
9
|
+
s.homepage = "https://github.com/agoragames/bracket_tree"
|
10
|
+
s.summary = %q{Binary Tree based bracketing system}
|
11
|
+
s.description = %q{Binary Tree based bracketing system}
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_development_dependency "rspec"
|
19
|
+
s.add_development_dependency "rake"
|
20
|
+
end
|
data/lib/bracket_tree.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'bracket_tree/node'
|
2
|
+
require 'bracket_tree/bracket/base'
|
3
|
+
module BracketTree
|
4
|
+
module Bracket
|
5
|
+
class NoSeedOrderError < Exception ; end
|
6
|
+
class SeedLimitExceededError < Exception ; end
|
7
|
+
|
8
|
+
autoload :Base, 'bracket_tree/bracket/base'
|
9
|
+
autoload :SingleElimination, 'bracket_tree/bracket/single_elimination'
|
10
|
+
autoload :DoubleElimination, 'bracket_tree/bracket/double_elimination'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
module BracketTree
|
2
|
+
module Bracket
|
3
|
+
# Basic bracketing functionality. If you wish to create a custom bracket type,
|
4
|
+
# inherit from this class to provide easy access to bracketing.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
# class MLGDouble < BracketTree::Bracket::Base
|
8
|
+
# template BracketTree::Template::DoubleElimination
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# bracket = MLGDouble.by_size 8
|
12
|
+
#
|
13
|
+
# This creates a bracket based on the standard double elimination template class.
|
14
|
+
# The template parameter can be any class that inherits from BracketTree::Template::Base,
|
15
|
+
# though.
|
16
|
+
#
|
17
|
+
# class MLGDoubleTemplate < BracketTree::Template::Base
|
18
|
+
# def self.location
|
19
|
+
# File.join File.dirname(__FILE__), 'templates', 'mlg_double'
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# class MLGDouble < BracketTree::Bracket::Base
|
24
|
+
# template MLGDoubleTemplate
|
25
|
+
# end
|
26
|
+
class Base
|
27
|
+
class NoTemplateError < Exception ; end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def by_size size, options = {}
|
31
|
+
generate_from_template @template, size
|
32
|
+
end
|
33
|
+
|
34
|
+
def template class_name
|
35
|
+
@template = class_name
|
36
|
+
end
|
37
|
+
|
38
|
+
# Generates a blank bracket object from the passed Template class for the
|
39
|
+
# passed size
|
40
|
+
#
|
41
|
+
# @param BracketTree::Template::Base template - the Template the bracket is
|
42
|
+
# based on
|
43
|
+
# @param Fixnum size - bracket size
|
44
|
+
# @return BracketTree::Bracket::Base bracket - a blank bracket with hash placeholders
|
45
|
+
def generate_from_template template, size
|
46
|
+
template = template.by_size size
|
47
|
+
bracket = new(matches: template.matches, seed_order: template.seed_order)
|
48
|
+
|
49
|
+
template.seats.each do |position|
|
50
|
+
bracket.add position, {}
|
51
|
+
end
|
52
|
+
|
53
|
+
bracket
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
include Enumerable
|
58
|
+
attr_accessor :root, :seed_order, :matches, :insertion_order
|
59
|
+
|
60
|
+
def initialize options = {}
|
61
|
+
@insertion_order = []
|
62
|
+
@matches = []
|
63
|
+
|
64
|
+
if options[:matches]
|
65
|
+
options[:matches].each do |m|
|
66
|
+
@matches << Match.new(m)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@seed_order = options[:seed_order] if options[:seed_order]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Adds a Node at the given position, setting the data as the payload. Maps to
|
74
|
+
# binary tree under the hood. The `data` can be any serializable object.
|
75
|
+
#
|
76
|
+
# @param Fixnum position - Seat position to add
|
77
|
+
# @param Object data - the player object to store in the Seat position
|
78
|
+
def add position, data
|
79
|
+
node = Node.new position, data
|
80
|
+
@insertion_order << position
|
81
|
+
|
82
|
+
if @root.nil?
|
83
|
+
@root = node
|
84
|
+
else
|
85
|
+
current = @root
|
86
|
+
loop do
|
87
|
+
if node.position < current.position
|
88
|
+
if current.left.nil?
|
89
|
+
current.left = node
|
90
|
+
break
|
91
|
+
else
|
92
|
+
current = current.left
|
93
|
+
end
|
94
|
+
elsif node.position > current.position
|
95
|
+
if current.right.nil?
|
96
|
+
current.right = node
|
97
|
+
break
|
98
|
+
else
|
99
|
+
current = current.right
|
100
|
+
end
|
101
|
+
else
|
102
|
+
break
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Replaces the data at a given node position with new payload. This is useful
|
109
|
+
# for updating bracket data, replacing placeholders with actual data, seeding,
|
110
|
+
# etc..
|
111
|
+
#
|
112
|
+
# @param [Fixnum] position - the node position to replace
|
113
|
+
# @param payload - the new payload object to replace
|
114
|
+
def replace position, payload
|
115
|
+
node = at position
|
116
|
+
if node
|
117
|
+
node.payload = payload
|
118
|
+
true
|
119
|
+
else
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Seeds bracket based on `seed_order` value of bracket. Provide an iterator
|
125
|
+
# with players that will be inserted in the appropriate location. Will raise a
|
126
|
+
# SeedLimitExceededError if too many players are sent, and a NoSeedOrderError if
|
127
|
+
# the `seed_order` attribute is nil
|
128
|
+
#
|
129
|
+
# @param [Enumerable] players - players to be seeded
|
130
|
+
def seed players
|
131
|
+
if @seed_order.nil?
|
132
|
+
raise NoSeedOrderError, 'Bracket does not have a seed order.'
|
133
|
+
elsif players.size > @seed_order.size
|
134
|
+
raise SeedLimitExceededError, 'cannot seed more players than seed order list.'
|
135
|
+
else
|
136
|
+
@seed_order.each do |position|
|
137
|
+
replace position, players.shift
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def winner
|
143
|
+
@root.payload
|
144
|
+
end
|
145
|
+
|
146
|
+
def each(&block)
|
147
|
+
in_order(@root, block)
|
148
|
+
end
|
149
|
+
|
150
|
+
def to_h
|
151
|
+
@root.to_h
|
152
|
+
end
|
153
|
+
|
154
|
+
# Array of Seats mapping to the individual positions of the bracket tree. The
|
155
|
+
# order of the nodes is important, as insertion in this order maintains the
|
156
|
+
# binary tree
|
157
|
+
#
|
158
|
+
# @return Array seats
|
159
|
+
def seats
|
160
|
+
entries.sort_by { |node| @insertion_order.index(node.position) }
|
161
|
+
end
|
162
|
+
|
163
|
+
alias_method :to_a, :seats
|
164
|
+
|
165
|
+
def at position
|
166
|
+
find { |n| n.position == position }
|
167
|
+
end
|
168
|
+
|
169
|
+
alias_method :size, :count
|
170
|
+
|
171
|
+
# Progresses the bracket by using the stored `matches` to copy data to the winning
|
172
|
+
# and losing seats. This facilitates match progression without manually
|
173
|
+
# manipulating bracket positions
|
174
|
+
#
|
175
|
+
# @param Fixnum seat - winning seat position
|
176
|
+
# @return Boolean result - result of progression
|
177
|
+
def match_winner seat
|
178
|
+
match = @matches.find { |m| m.include? seat }
|
179
|
+
|
180
|
+
if match
|
181
|
+
losing_seat = match.seats.find { |s| s != seat }
|
182
|
+
|
183
|
+
if match.winner_to
|
184
|
+
replace match.winner_to, at(seat).payload
|
185
|
+
end
|
186
|
+
|
187
|
+
if match.loser_to
|
188
|
+
replace match.loser_to, at(losing_seat).payload
|
189
|
+
end
|
190
|
+
|
191
|
+
return true
|
192
|
+
else
|
193
|
+
return false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Inverse of `match_winner`, progresses the bracket based on seat. See `match_winner`
|
198
|
+
# for more details
|
199
|
+
#
|
200
|
+
# @param Fixnum seat - losing seat position
|
201
|
+
# @return Boolean result - result of progression
|
202
|
+
def match_loser seat
|
203
|
+
match = @matches.find { |m| m.include? seat }
|
204
|
+
|
205
|
+
if match
|
206
|
+
winning_seat = match.seats.find { |s| s != seat }
|
207
|
+
match_winner winning_seat
|
208
|
+
else
|
209
|
+
return false
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def in_order(node, block)
|
214
|
+
if node
|
215
|
+
unless node.left.nil?
|
216
|
+
in_order(node.left, block)
|
217
|
+
end
|
218
|
+
|
219
|
+
block.call(node)
|
220
|
+
|
221
|
+
unless node.right.nil?
|
222
|
+
in_order(node.right, block)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|