bracket_tree 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://secure.travis-ci.org/agoragames/bracket_tree.png)](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
|