crafting_table 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.asc +11 -0
  3. data.tar.gz.asc +11 -0
  4. data/.gitignore +0 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +41 -0
  7. data/LICENSE +14 -0
  8. data/Rakefile +0 -0
  9. data/crafting_table.gemspec +19 -0
  10. data/data/items/thermal_expansion.yml +64 -0
  11. data/data/items/vanilla.yml +1152 -0
  12. data/data/recipes/thermal_expansion.yml +97 -0
  13. data/data/recipes/vanilla.yml +25 -0
  14. data/data/test/items/vanilla.yml +1149 -0
  15. data/data/test/recipes/vanilla.yml +25 -0
  16. data/examples/interactive_table.rb +98 -0
  17. data/features/item_differentiation.feature +18 -0
  18. data/features/loading_items.feature +10 -0
  19. data/features/resolving_recipes.feature +26 -0
  20. data/features/searching_items.feature +83 -0
  21. data/features/searching_recipes.feature +42 -0
  22. data/features/step_definitions/item_manager_steps.rb +88 -0
  23. data/features/step_definitions/item_steps.rb +23 -0
  24. data/features/step_definitions/recipe_manager_steps.rb +42 -0
  25. data/features/support/env.rb +29 -0
  26. data/lib/crafting_table.rb +19 -0
  27. data/lib/crafting_table/item.rb +61 -0
  28. data/lib/crafting_table/item_manager.rb +171 -0
  29. data/lib/crafting_table/recipe.rb +36 -0
  30. data/lib/crafting_table/recipe_manager.rb +216 -0
  31. data/lib/crafting_table/search/damage_search.rb +51 -0
  32. data/lib/crafting_table/search/fuzzy_name_search.rb +39 -0
  33. data/lib/crafting_table/search/input_search.rb +47 -0
  34. data/lib/crafting_table/search/item_id_search.rb +51 -0
  35. data/lib/crafting_table/search/name_search.rb +71 -0
  36. data/lib/crafting_table/search/output_search.rb +47 -0
  37. data/lib/crafting_table/search/search_builder.rb +85 -0
  38. data/spec/crafting_table/item_manager_spec.rb +421 -0
  39. data/spec/crafting_table/item_spec.rb +67 -0
  40. data/spec/crafting_table/recipe_manager_spec.rb +353 -0
  41. data/spec/crafting_table/recipe_spec.rb +25 -0
  42. data/spec/crafting_table/search/search_builder.rb +105 -0
  43. data/spec/crafting_table/search/search_spec.rb +383 -0
  44. data/spec/spec_helper.rb +6 -0
  45. metadata +145 -0
  46. metadata.gz.asc +11 -0
@@ -0,0 +1,29 @@
1
+ #coding: utf-8
2
+
3
+ require_relative '../../lib/crafting_table'
4
+
5
+ ITEM_FILE = 'data/test/items/vanilla.yml'
6
+ RECIPE_FILE = 'data/test/recipes/vanilla.yml'
7
+
8
+ def string_to_boolean(string)
9
+
10
+ if string.downcase =~ /yes|y/
11
+ true
12
+ elsif string.downcase =~ /no|n/
13
+ false
14
+ else
15
+ raise ArgumentError
16
+ end
17
+
18
+ end
19
+
20
+ def string_to_range_or_integer(string)
21
+ if string =~ /^[0-9]+$/
22
+ string.to_i
23
+ elsif string =~ /^[0-9]+\.\.[0-9]+$/
24
+ Range.new(*string.split('..').map(&:to_i))
25
+ else
26
+ fail "Could not convert #{ string.inspect } to range or integer."
27
+ end
28
+ end
29
+
@@ -0,0 +1,19 @@
1
+ #coding: utf-8
2
+
3
+ require_relative 'crafting_table/item_manager'
4
+ require_relative 'crafting_table/item'
5
+
6
+ require_relative 'crafting_table/recipe_manager'
7
+ require_relative 'crafting_table/recipe'
8
+
9
+ require_relative 'crafting_table/search/search_builder'
10
+ require_relative 'crafting_table/search/name_search'
11
+ require_relative 'crafting_table/search/fuzzy_name_search'
12
+ require_relative 'crafting_table/search/damage_search'
13
+ require_relative 'crafting_table/search/item_id_search'
14
+ require_relative 'crafting_table/search/input_search'
15
+ require_relative 'crafting_table/search/output_search'
16
+
17
+ module CraftingTable
18
+
19
+ end
@@ -0,0 +1,61 @@
1
+ #coding: utf-8
2
+
3
+ module CraftingTable
4
+
5
+ # A class representing a single item.
6
+ # An item, in this case, can be any block, tool, loot etc.
7
+ #
8
+ # @author Michael Senn <morrolan@morrolan.ch>
9
+ # @since 0.1
10
+ class Item
11
+ attr_reader :name, :item_id, :damage_value
12
+
13
+ # Create a new Item.
14
+ #
15
+ # @example Spruce Wood
16
+ # item = Item.new('Spruce Wood', 17, 1)
17
+ #
18
+ # @example Glass
19
+ # item = Item.new('Glass', 20, :any)
20
+ #
21
+ # @param [String] name The item's name.
22
+ # @param [Integer, Symbol] damage_value The item's damage / meta value.
23
+ # @param [Integer] item_id The item's ID.
24
+ def initialize(name, item_id, damage_value = 0)
25
+ @name, @item_id, @damage_value = name, item_id, damage_value
26
+ end
27
+
28
+ # Compare two items for equality.
29
+ #
30
+ # Two items are considered equal, if their name, item ID and damage value are equal.
31
+ #
32
+ # @param [Item] other Item which to compare for equality.
33
+ # @return [Boolean] Whether the two items are equal.
34
+ def ==(other)
35
+ name == other.name && item_id == other.item_id && damage_value == other.damage_value
36
+ end
37
+
38
+ alias_method(:eql?, :==)
39
+
40
+ # Return reasonable hash value of this item.
41
+ #
42
+ # @since 0.2
43
+ #
44
+ # @return [Integer] Hash of item's name, id and damage value.
45
+ def hash
46
+ [name, item_id, damage_value].hash
47
+ end
48
+
49
+ # Return a unique identifier of this item.
50
+ # The identifier is an array, containing its ID and damage value
51
+ #
52
+ # @since 0.2
53
+ #
54
+ # @return [Array<Integer>] Unique identifier of this item.
55
+ def identifier
56
+ [item_id, damage_value]
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,171 @@
1
+ #coding: utf-8
2
+
3
+ require 'yaml'
4
+
5
+ module CraftingTable
6
+
7
+ # A class which contains items, and allows to search through them.
8
+ #
9
+ # @author Michael Senn <morrolan@morrolan.ch>
10
+ # @since 0.1
11
+ class ItemManager
12
+ attr_reader :items
13
+
14
+ # Create a new ItemManager
15
+ #
16
+ # @param [Array] items Array of items with which to initialize the item manager.
17
+ def initialize(items = [])
18
+ @items = items.to_ary
19
+ end
20
+
21
+ # Add a new item to the internal collection.
22
+ #
23
+ # @param [Item] item Item which to add to the collection.
24
+ # @return [void]
25
+ def add(item)
26
+ @items << item
27
+ end
28
+
29
+ # Add new items by reading them from a YAML file.
30
+ #
31
+ # @param [String] path The path to the file from which to read items from.
32
+ # @return [void]
33
+ def add_from_file(path)
34
+ YAML.load_file(path).each do |hash|
35
+ add(Item.new(hash['name'], hash['id'], hash['damage']))
36
+ end
37
+ end
38
+
39
+ # Clear the internal collection of items.
40
+ # @return [void]
41
+ def clear
42
+ @items.clear
43
+ end
44
+
45
+ # Find items by their name.
46
+ #
47
+ # @deprecated Use {#find} instead
48
+ #
49
+ # @example Search using default parameters.
50
+ # manager.find_by_name('Stone').map(&:name) #=> ['Stone']
51
+ #
52
+ # @example Case-insensitive search, non-exact matching.
53
+ # manager.find_by_name('stone', exact: false, case_sensitive: false).map(&:name) #=> ['Stone', 'Sandstone', '...']
54
+ #
55
+ # @param [String] name The name for which to search.
56
+ # @param [Hash] options Options which influence the search.
57
+ # @option options [Boolean] :exact (true) Whether to match names exactly.
58
+ # @option options [Boolean] :case_sensitive (true) Whether to search case-sensitively.
59
+ # @return [Array<Item>] Collection of items which matched the search condition.
60
+ def find_by_name(name, options = {})
61
+ default_options = { exact: true, case_sensitive: true }
62
+ default_options.update(options)
63
+
64
+ if default_options[:case_sensitive]
65
+ if default_options[:exact]
66
+ items.select { |item| item.name == name }
67
+ else
68
+ items.select { |item| item.name.include? name }
69
+ end
70
+ else
71
+ if default_options[:exact]
72
+ items.select { |item| item.name.downcase == name.downcase }
73
+ else
74
+ items.select { |item| item.name.downcase.include? name.downcase }
75
+ end
76
+ end
77
+ end
78
+
79
+ # Find items by their ID.
80
+ #
81
+ # @deprecated Use {#find} instead
82
+ #
83
+ # @example Searching for single ID.
84
+ # manager.find_by_item_id(17).first.name #=> 'Wood'
85
+ #
86
+ # @example Searching for range of IDs.
87
+ # manager.find_by_item_id(14..16).map(&:name) #=> ['Gold Ore', 'Iron Ore', 'Coal Ore']
88
+ #
89
+ # @example Searching for collection of IDs.
90
+ # manager.find_by_item_id([1, 3, 24]).map(&:name) #=> ['Stone', 'Dirt', 'Sandstone']
91
+ #
92
+ # @param [#include?, Integer] id A collection of IDs, or a single ID, for which to search.
93
+ # @return [Array<Item>] Collection of items which matched the search condition.
94
+ def find_by_item_id(id)
95
+ if id.respond_to? :include?
96
+ items.select { |item| id.include? item.item_id }
97
+ else
98
+ items.select { |item| item.item_id == id }
99
+ end
100
+ end
101
+
102
+ # Find items.
103
+ #
104
+ # @since 0.3
105
+ #
106
+ # @example Searching for name and damage value
107
+ # results = manager.find do |search|
108
+ # search.name = 'Wood'
109
+ # search.exact = false
110
+ # search.damage_value = 3
111
+ # end
112
+ # results.first.name #=> 'Jungle Wood'
113
+ #
114
+ # @example Searching for range of item IDs and two specific damage values
115
+ # results = manager.find do |search|
116
+ # search.item_id = 1..20
117
+ # search.damage_value = [1, 3]
118
+ # end
119
+ # results.map(&:name) #=> ['Spruce Sapling', 'Jungle Sapling', 'Spruce Wood', 'Jungle Wood', 'Spruce Leaves', 'Jungle Leaves']
120
+ #
121
+ # @yieldparam builder [SearchBuilder]
122
+ # An instance of the SearchBuilder class which allows to easily specify
123
+ # multiple search conditions.
124
+ #
125
+ # @return [Array<Item>] Items which matched the search conditions.
126
+ def find(&block)
127
+ builder = Search::SearchBuilder.new
128
+ yield builder
129
+
130
+ builder.searches.inject(items) { |items, search| search.apply_to(items) }
131
+ end
132
+
133
+ # Find items by their damage value.
134
+ #
135
+ # @deprecated Use {#find} instead
136
+ #
137
+ # @example Searching for single damage value.
138
+ # manager.find_by_damage_value(2).map(&:name) #=> ['Birch Wood', '...']
139
+ #
140
+ # @example Searching for range of damage values.
141
+ # manager.find_by_damage_value(1..2).map(&:name) #=> ['Spruce Wood', 'Birch Wood', '...']
142
+ #
143
+ # @example Searching for collection of damage values.
144
+ # manager.find_by_damage_value([1, 3]).map(&:name) #=> ['Spruce Wood', 'Jungle Wood', '...']
145
+ #
146
+ # @param [#include?, Integer] id A collection of damage values, or a single damage value, for which to search.
147
+ # @return [Array<Item>] Collection of items which matched the search condition.
148
+ def find_by_damage_value(damage)
149
+ if damage.is_a?(Symbol) || !damage.respond_to?(:include?)
150
+ items.select { |item| item.damage_value == damage }
151
+ else
152
+ items.select { |item| damage.include? item.damage_value }
153
+ end
154
+ end
155
+
156
+ # Find items by their identifier.
157
+ #
158
+ # @see Item#identifier
159
+ # @since 0.2
160
+ #
161
+ # @param [Array<Integer>] identifier Identifier for which to
162
+ #search.
163
+ # @return [Array<Item>] Collection of items which matched
164
+ # the search condition.
165
+ def find_by_identifier(identifier)
166
+ items.select { |item| item.identifier == identifier }
167
+ end
168
+
169
+ end
170
+
171
+ end
@@ -0,0 +1,36 @@
1
+ #coding: utf-8
2
+
3
+ module CraftingTable
4
+
5
+ # A class representing a single recipe.
6
+ # A recipe has a name, one to n inputs, and one to n outputs.
7
+ #
8
+ # @author Michael Senn <morrolan@morrolan.ch>
9
+ # @since 0.2
10
+ class Recipe
11
+ attr_reader :name, :input, :output
12
+
13
+ # Create a new recipe.
14
+ #
15
+ # @example Torch
16
+ # recipe = Recipe.new('Torch',
17
+ # { Item.new('Coal', 263, 0) => 1,
18
+ # Item.new('Stick', 280, 0) => 1 },
19
+ # { Item.new('Torch', 50, 0) => 4 })
20
+ #
21
+ # @param [String] name The recipe's name
22
+ # @param [Hash{Item => Integer}] input
23
+ # A hash of items and their amounts which are required to craft
24
+ # the recipe.
25
+ # @param [Hash{Item => Integer}] output
26
+ # A hash of items and their amounts which you get when crafting
27
+ # the recipe.
28
+ def initialize(name, input, output)
29
+ @name, @input, @output = name, input, output
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
36
+
@@ -0,0 +1,216 @@
1
+ #coding: utf-8
2
+
3
+ module CraftingTable
4
+
5
+ # A class which contains recipes, and allows to search through them.
6
+ #
7
+ # @author Michael Senn <morrolan@morrolan.ch>
8
+ # @since 0.2
9
+ class RecipeManager
10
+ attr_reader :recipes
11
+ attr_reader :item_manager
12
+
13
+ # Create a new RecipeManager
14
+ #
15
+ # @param [ItemManager] item_manager ItemManager which contains the items
16
+ # on which this manager's recipes are based.
17
+ def initialize(item_manager)
18
+ @item_manager = item_manager
19
+ @recipes = []
20
+ end
21
+
22
+ # Add a new recipe to the internal collection.
23
+ #
24
+ # @param [Recipe] recipe Recipe which to add to the collection.
25
+ # @return [void]
26
+ def add(recipe)
27
+ @recipes << recipe
28
+ end
29
+
30
+ # Add new recipes by reading them from a YAML file.
31
+ #
32
+ # @param [String] path The path to the file from which to read
33
+ # recipes from.
34
+ # @return [void]
35
+ def add_from_file(path)
36
+ YAML.load_file(path).each do |recipe_hash|
37
+ raw_input = recipe_hash.fetch('input', {})
38
+ raw_output = recipe_hash.fetch('output', {})
39
+ name = recipe_hash.fetch('name', 'UNKNOWN')
40
+
41
+ input = {}
42
+ output = {}
43
+ raw_input.each do |identifier_string, amount|
44
+ identifier = identifier_string.split(':').map(&:to_i)
45
+ item = item_manager.find_by_identifier(identifier).first
46
+ input[item] = amount
47
+ end
48
+
49
+ raw_output.each do |identifier_string, amount|
50
+ identifier = identifier_string.split(':').map(&:to_i)
51
+ item = item_manager.find_by_identifier(identifier).first
52
+ output[item] = amount
53
+ end
54
+
55
+ @recipes << Recipe.new(name, input, output)
56
+ end
57
+
58
+ end
59
+
60
+ # Clear the internal collection of recipes.
61
+ # @return [void]
62
+ def clear
63
+ @recipes.clear
64
+ end
65
+
66
+ # Find recipes.
67
+ #
68
+ # @since 0.3
69
+ #
70
+ # @example Searching for name.
71
+ # results = manager.find do |search|
72
+ # search.name = 'slab'
73
+ # search.exact = false
74
+ # search.case_sensitive = false
75
+ # end
76
+ # results.map(&:name)
77
+ # #=> ['Cobblestone slab', 'Wooden slab', 'Stone slab', '...']
78
+ #
79
+ # @example Searching for recipes which result in torch(es).
80
+ # manager.find do |search|
81
+ # manager.output = Item.new('Torch', 50)
82
+ # end
83
+ #
84
+ # @example Searching for recipes which require oak planks.
85
+ # results = manager.find do |search|
86
+ # manager.input = Item.new('Oak Wood Planks', 5)
87
+ # end
88
+ # results.map(&:name)
89
+ # #=> ['Sticks', 'Crafting Table', 'Chest', 'Bed', '...']
90
+ #
91
+ # @yieldparam builder [SearchBuilder]
92
+ # An instance of the SearchBuilder class which allows to easily specify
93
+ # multiple search conditions.
94
+ #
95
+ # @return [Array<Recipe>] Recipes which matched the search conditions.
96
+ def find(&block)
97
+ builder = Search::SearchBuilder.new
98
+ yield builder
99
+
100
+ builder.searches.inject(recipes) { |recipes, search| search.apply_to(recipes) }
101
+ end
102
+
103
+ # Find recipes by their name.
104
+ #
105
+ # @deprecated Use {#find} instead.
106
+ #
107
+ # @example Search using default parameters.
108
+ # manager.find_by_name('Torch').first.input.map(&:name) #=> ['Wooden Planks', 'Coal']
109
+ #
110
+ # @example Case-insensitive search, non-exact matching.
111
+ # manager.find_by_name('slab').map(&:name) #=> ['Cobblestone Slab', 'Wooden slab', 'Stone slab', '...']
112
+ #
113
+ # @param [String] name The name for which to search.
114
+ # @param [Hash] options Options which influence the search.
115
+ # @option options [Boolean] :exact (true) Whether to match names exactly.
116
+ # @option options [Boolean] :case_sensitive (true) Whether to search case-sensitively.
117
+ # @return [Array<Recipe>] Collection of recipes which matched the search condition.
118
+ def find_by_name(name, options = {})
119
+ default_options = { exact: true, case_sensitive: true }
120
+ default_options.update(options)
121
+
122
+ if default_options[:case_sensitive]
123
+ if default_options[:exact]
124
+ recipes.select { |recipe| recipe.name == name }
125
+ else
126
+ recipes.select { |recipe| recipe.name.include? name }
127
+ end
128
+ else
129
+ if default_options[:exact]
130
+ recipes.select { |recipe| recipe.name.downcase == name.downcase }
131
+ else
132
+ recipes.select { |recipe| recipe.name.downcase.include? name.downcase }
133
+ end
134
+ end
135
+ end
136
+
137
+ # Find recipes by their input.
138
+ #
139
+ # @deprecated Use {#find} instead.
140
+ #
141
+ # @example Searching for recipes which require planks.
142
+ # manager.find_by_input(Item.new('Oak Wood Planks', 5, 0)).map(&:name)
143
+ # #=> ['Sticks', 'Crafting Table', 'Chest', 'Bed', '...']
144
+ #
145
+ # @param [Item] item Item for which to search.
146
+ # @return [Array<Recipe>] Collection of recipes which matched the search condition.
147
+ def find_by_input(item)
148
+ recipes.select { |recipe| recipe.input.key? item }
149
+ end
150
+
151
+ # Find recipes by their output.
152
+ #
153
+ # @deprecated Use {#find} instead.
154
+ #
155
+ # @example Searching for recipes which result in Coal.
156
+ # manager.find_by_output(Item.new('Coal', 263, 0)).map(&:name)
157
+ # #=> ['Coal']
158
+ # @return [Array<Recipe>]
159
+ # Collection of recipes which matched the search condition.
160
+ def find_by_output(item)
161
+ recipes.select { |recipe| recipe.output.key? item }
162
+ end
163
+
164
+ # Resolve a recipe into its base components.
165
+ #
166
+ # @since 0.3
167
+ #
168
+ # @example Components required to craft 50 torches.
169
+ # torch = recipe_manager.find_by_name('Torch').first
170
+ # components = recipe_manager.resolve_recipe(recipe_torch, 50)
171
+ # components.map { |item, amount| item.name => amount }
172
+ # #=> { "Wood" => 2, "Coal" => 13 }
173
+ #
174
+ # @param [Recipe] recipe
175
+ # The recipe which to resolve to its components.
176
+ # @param [Integer] amount Desired amount of the recipe.
177
+ # @return [Hash{Item => Integer}]
178
+ # A hash mapping the base components
179
+ # to the required amount of each.
180
+ def resolve_recipe(recipe, amount)
181
+ # Todo: Allow to specify arbitrary outputs.
182
+ # Todo: Fail if the specified output is not part of the recipe.
183
+ desired_output = recipe.output.keys.first
184
+ amount_per_iteration = recipe.output[desired_output]
185
+ # If we get four items per iteration, and want 21 items in
186
+ # total, we'll need 6 iterations.
187
+ iterations = (amount.to_f / amount_per_iteration).ceil
188
+
189
+ requirements = Hash.new(0)
190
+
191
+ recipe.input.each do |input, amount|
192
+ # Finding potential recipes for the input.
193
+ recipes_for_input = find_by_output(input)
194
+
195
+ # Todo: Allow for other criteria where the recipe should be ignored.
196
+ if recipes_for_input.empty?
197
+ requirements[input] += amount * iterations
198
+ else
199
+ recipe_for_input = recipes_for_input.first
200
+ requirements_for_input = resolve_recipe(recipe_for_input,
201
+ amount * iterations)
202
+
203
+ requirements.merge!(requirements_for_input) do |key, old_amount, new_amount|
204
+ old_amount + new_amount
205
+ end
206
+
207
+ end
208
+ end
209
+
210
+ requirements
211
+
212
+ end
213
+
214
+ end
215
+
216
+ end