crafting_table 0.3.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 (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