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.
Files changed (35) hide show
  1. data/.gitignore +4 -0
  2. data/.travis.yml +7 -0
  3. data/Gemfile +4 -0
  4. data/README.md +187 -0
  5. data/Rakefile +8 -0
  6. data/bracket_tree.gemspec +20 -0
  7. data/lib/bracket_tree.rb +9 -0
  8. data/lib/bracket_tree/bracket.rb +12 -0
  9. data/lib/bracket_tree/bracket/base.rb +228 -0
  10. data/lib/bracket_tree/bracket/double_elimination.rb +7 -0
  11. data/lib/bracket_tree/bracket/single_elimination.rb +7 -0
  12. data/lib/bracket_tree/match.rb +24 -0
  13. data/lib/bracket_tree/node.rb +23 -0
  14. data/lib/bracket_tree/template.rb +70 -0
  15. data/lib/bracket_tree/templates/double_elimination.rb +11 -0
  16. data/lib/bracket_tree/templates/double_elimination/128.json +3695 -0
  17. data/lib/bracket_tree/templates/double_elimination/16.json +107 -0
  18. data/lib/bracket_tree/templates/double_elimination/32.json +202 -0
  19. data/lib/bracket_tree/templates/double_elimination/4.json +26 -0
  20. data/lib/bracket_tree/templates/double_elimination/64.json +400 -0
  21. data/lib/bracket_tree/templates/double_elimination/8.json +50 -0
  22. data/lib/bracket_tree/templates/single_elimination.rb +11 -0
  23. data/lib/bracket_tree/templates/single_elimination/128.json +1917 -0
  24. data/lib/bracket_tree/templates/single_elimination/16.json +56 -0
  25. data/lib/bracket_tree/templates/single_elimination/2.json +11 -0
  26. data/lib/bracket_tree/templates/single_elimination/32.json +351 -0
  27. data/lib/bracket_tree/templates/single_elimination/4.json +17 -0
  28. data/lib/bracket_tree/templates/single_elimination/64.json +703 -0
  29. data/lib/bracket_tree/templates/single_elimination/8.json +29 -0
  30. data/spec/bracket_spec.rb +189 -0
  31. data/spec/double_elimination_spec.rb +5 -0
  32. data/spec/match_spec.rb +30 -0
  33. data/spec/spec_helper.rb +3 -0
  34. data/spec/template_spec.rb +58 -0
  35. metadata +112 -0
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ notifications:
5
+ email:
6
+ recipients:
7
+ - anordman@majorleaguegaming.com
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in bracket_tree.gemspec
4
+ gemspec
@@ -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)
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+
5
+ task :default => :spec
6
+
7
+ desc "Run specs"
8
+ RSpec::Core::RakeTask.new(:spec)
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'json'
2
+ require 'bracket_tree/match'
3
+ require 'bracket_tree/bracket'
4
+ require 'bracket_tree/template'
5
+ require 'bracket_tree/templates/double_elimination'
6
+ require 'bracket_tree/templates/single_elimination'
7
+
8
+ module BracketTree
9
+ end
@@ -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