bracket_tree 0.1.0

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