From 74e95e614b7a1fe733ea2b381382e394af1aec7c Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Sun, 27 Sep 2015 13:57:21 -0700 Subject: [PATCH] Add CraftingPlan to compute resource requirements --- .../controllers/full_recipe_controller.coffee | 2 +- .../models/crafting/crafting_node.coffee | 90 ++++--- .../models/crafting/crafting_plan.coffee | 69 +++--- .../models/crafting/crafting_step.coffee | 29 +++ .../models/crafting/graph_builder.coffee | 35 ++- .../models/crafting/inventory_node.coffee | 7 +- src/coffee/models/crafting/item_node.coffee | 12 +- .../models/crafting/plan_builder.coffee | 55 +++-- src/coffee/models/crafting/recipe_node.coffee | 12 +- src/coffee/models/crafting_plan.coffee | 233 ------------------ src/coffee/models/recipe.coffee | 72 ++---- src/coffee/models/step.coffee | 2 +- test/crafting/crafting_plan.test.coffee | 56 ++++- test/crafting/fixtures.coffee | 9 +- test/crafting/plan_builder.test.coffee | 26 +- .../mod_version_parser_v1.test.coffee | 5 + 16 files changed, 280 insertions(+), 434 deletions(-) create mode 100644 src/coffee/models/crafting/crafting_step.coffee delete mode 100644 src/coffee/models/crafting_plan.coffee diff --git a/src/coffee/controllers/full_recipe_controller.coffee b/src/coffee/controllers/full_recipe_controller.coffee index f5fd14873..18c49f9ca 100644 --- a/src/coffee/controllers/full_recipe_controller.coffee +++ b/src/coffee/controllers/full_recipe_controller.coffee @@ -71,7 +71,7 @@ module.exports = class FullRecipeController extends BaseController inputs.clear() if @model? - @model.eachInputStack (stack)-> + for stack in @model.input inputs.add stack.itemSlug, stack.quantity diff --git a/src/coffee/models/crafting/crafting_node.coffee b/src/coffee/models/crafting/crafting_node.coffee index bf070fde8..50566dbb6 100644 --- a/src/coffee/models/crafting/crafting_node.coffee +++ b/src/coffee/models/crafting/crafting_node.coffee @@ -32,13 +32,13 @@ module.exports = class CraftingNode # Public Methods ############################################################################### expand: (queue=[])-> + return unless @valid return queue if @children.length > 0 for child in @_createChildren() child.parent = this - if child.valid - @_children.push child - queue.push child + @_children.push child + queue.push child return queue acceptVisitor: (visitor)-> @@ -65,52 +65,46 @@ module.exports = class CraftingNode # Property Methods ############################################################################# - getChildren: -> - return @_children - - isComplete: -> - if not @_complete? - @_complete = @_checkCompleteness() - return @_complete - - getCompleteText: -> - return if @complete then "✓" else "✗" - - getDepth: -> - maxDepth = 1 - for child in @children - maxDepth = Math.max maxDepth, child.getDepth() + 1 - return maxDepth - - getId: -> - return @_id - - getRotations: -> - return @_rotations - - getSize: -> - size = 1 - for child in @children - size += child.size - return size - - isValid: -> - return @_valid if @_valid? - - valid = @_checkValidity() - @_valid = false if not valid - - return valid - Object.defineProperties @prototype, - children: { get:@prototype.getChildren } - complete: { get:@prototype.isComplete } - completeText: { get:@prototype.getCompleteText } - depth: { get:@prototype.getDepth } - id: { get:@prototype.getId } - rotations: { get:@prototype.getRotations } - size: { get:@prototype.getSize } - valid: { get:@prototype.isValid } + + children: + get: -> return @_children + + complete: + get: -> + if not @_complete? + @_complete = @_checkCompleteness() + return @_complete + + completeText: + get: -> if @complete then "◼︎" else "◻︎" + + depth: + get: -> + maxDepth = 1 + for child in @children + maxDepth = Math.max maxDepth, child.depth + 1 + return maxDepth + + id: + get: -> return @_id + + rotations: + get: -> return @_rotations + + size: + get: -> + size = 1 + for child in @children + size += child.size + return size + + valid: + get: -> @_checkValidity() + + validText: + get: -> if @valid then "✓" else "✗" + # Virtual Methods ############################################################################## diff --git a/src/coffee/models/crafting/crafting_plan.coffee b/src/coffee/models/crafting/crafting_plan.coffee index 1161cb909..cd4dc6c58 100644 --- a/src/coffee/models/crafting/crafting_plan.coffee +++ b/src/coffee/models/crafting/crafting_plan.coffee @@ -39,12 +39,24 @@ module.exports = class CraftingPlan # Object Overrides ############################################################################# toString: -> - return "#{@constructor.name}{ - wanted:#{@wanted}, - required:#{@required}, - steps:[#{(step.toString() for step in @steps).join(',')}], - produced:#{@produced} - }" + result = ["To Make:"] + @_wanted.each (stack)-> + result.push " #{stack}" + + result.push "Start with:" + @_required.each (stack)-> + result.push " #{stack}" + + result.push "Use these recipes:" + for step in @_steps + result.push " #{step}" + + result.push "To produce:" + @_produced.each (stack)-> + result.push " #{stack}" + + return result.join '\n' + # Private Methods ############################################################################## @@ -52,35 +64,32 @@ module.exports = class CraftingPlan @_required.addInventory @_wanted for i in [@_steps.length-1..0] by -1 - recipe = @_steps[i] + step = @_steps[i] - for stack in recipe.output + for stack in step.recipe.output while @_required.quantityOf(stack.itemSlug) > 0 - @_executeRecipe recipe + @_executeStep step @_produced.addInventory @_wanted - _executeRecipe: (recipe)-> - #console.log "executing: #{recipe}" + _executeStep: (step)-> + step.repeat += 1 + recipe = step.recipe + for stack in recipe.input - @_use stack + available = @_produced.quantityOf stack.itemSlug + required = recipe.getQuantityRequired stack.itemSlug + consumed = Math.min required, available + deficit = required - consumed + + @_produced.remove stack.itemSlug, consumed + @_required.add stack.itemSlug, deficit + for stack in recipe.output - @_produce stack + deficit = @_required.quantityOf stack.itemSlug + created = recipe.getQuantityProduced stack.itemSlug + replenished = Math.min deficit, created + surplus = created - replenished - _produce: (stack)-> - deficit = @_required.quantityOf stack.itemSlug - replenished = Math.min deficit, stack.quantity - surplus = stack.quantity - replenished - - #console.log "producing #{stack.itemSlug}, surplus: #{surplus}, replenished:#{replenished}" - @_produced.add stack.itemSlug, surplus - @_required.remove stack.itemSlug, replenished - - _use: (stack)-> - available = @_produced.quantityOf stack.itemSlug - consumed = Math.min stack.quantity, available - deficit = stack.quantity - consumed - - #console.log "using #{stack.itemSlug}, consumed: #{consumed}, deficit:#{deficit}" - @_produced.remove stack.itemSlug, consumed - @_required.add stack.itemSlug, deficit + @_produced.add stack.itemSlug, surplus + @_required.remove stack.itemSlug, replenished diff --git a/src/coffee/models/crafting/crafting_step.coffee b/src/coffee/models/crafting/crafting_step.coffee new file mode 100644 index 000000000..75ee5866c --- /dev/null +++ b/src/coffee/models/crafting/crafting_step.coffee @@ -0,0 +1,29 @@ +### +Crafting Guide - crafting_step.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +######################################################################################################################## + +module.exports = class CraftingStep + + constructor: (recipe, repeat=0)-> + if not recipe? then throw new Error 'recipe is required' + if repeat < 0 then throw new Error 'repeat must be at least 1' + + @_recipe = recipe + @repeat = repeat + + # Property Methods ############################################################################# + + Object.defineProperties @prototype, + + recipe: + get: -> @_recipe + + # Object Overrides ############################################################################# + + toString: -> + return "#{@repeat}x #{@recipe.slug}" diff --git a/src/coffee/models/crafting/graph_builder.coffee b/src/coffee/models/crafting/graph_builder.coffee index 02ef2b822..e859549e7 100644 --- a/src/coffee/models/crafting/graph_builder.coffee +++ b/src/coffee/models/crafting/graph_builder.coffee @@ -44,26 +44,23 @@ module.exports = class GraphBuilder # Property Methods ############################################################################# - isComplete: -> - return false unless @_rootNode? - return false unless @_queue? - return false unless @_queue.length is 0 - return true - - getRootNode: -> - return @_rootNode - - getStepCount: -> - return @_stepCount - - getWanted: -> - return @_wanted - Object.defineProperties @prototype, - complete: { get:@prototype.isComplete } - rootNode: { get:@prototype.getRootNode } - stepCount: { get:@prototype.getStepCount } - wanted: { get:@prototype.getWanted } + + complete: + get: -> + return false unless @_rootNode? + return false unless @_queue? + return false unless @_queue.length is 0 + return true + + rootNode: + get: -> @_rootNode + + stepCount: + get: -> @_stepCount + + wanted: + get: -> @_wanted # Object Overrides ############################################################################ diff --git a/src/coffee/models/crafting/inventory_node.coffee b/src/coffee/models/crafting/inventory_node.coffee index e0a00102d..e945c4411 100644 --- a/src/coffee/models/crafting/inventory_node.coffee +++ b/src/coffee/models/crafting/inventory_node.coffee @@ -33,12 +33,12 @@ module.exports = class InventoryNode extends CraftingNode _checkCompleteness: -> for child in @children - return false unless child.isComplete + return false unless child.complete return true _checkValidity: -> for child in @children - return false unless child.isValid + return false unless child.valid return true # Object Overrides ############################################################################# @@ -47,8 +47,7 @@ module.exports = class InventoryNode extends CraftingNode options.indent ?= '' options.recursive ?= true - completeText = if @complete then 'complete' else 'incomplete' - parts = ["#{options.indent}#{@completeText} InventoryNode for #{@inventory}"] + parts = ["#{options.indent}#{@completeText} #{@validText} InventoryNode for #{@inventory}"] nextIndent = options.indent + ' ' if options.recursive for child in @children diff --git a/src/coffee/models/crafting/item_node.coffee b/src/coffee/models/crafting/item_node.coffee index 0065ef683..51e54d26f 100644 --- a/src/coffee/models/crafting/item_node.coffee +++ b/src/coffee/models/crafting/item_node.coffee @@ -46,7 +46,10 @@ module.exports = class ItemNode extends CraftingNode return [] unless recipes.length > 0 for recipe in recipes - result.push new RecipeNode modPack:@modPack, recipe:recipe + child = new RecipeNode modPack:@modPack, recipe:recipe + child.parent = this + if child.valid + result.push child return result @@ -55,13 +58,13 @@ module.exports = class ItemNode extends CraftingNode return false unless @children? for child in @children - return true if child.isComplete + return true if child.complete return false _checkValidity: -> return true unless @children.length > 0 for child in @children - return true if child.isValid + return true if child.valid return false # Object Overrides ############################################################################# @@ -70,8 +73,7 @@ module.exports = class ItemNode extends CraftingNode options.indent ?= '' options.recursive ?= true - completeText = if @complete then 'complete' else 'incomplete' - parts = ["#{options.indent}#{@completeText} ItemNode for #{@item.name}"] + parts = ["#{options.indent}#{@completeText} #{@validText} ItemNode for #{@item.name}"] nextIndent = options.indent + ' ' if options.recursive for child in @children diff --git a/src/coffee/models/crafting/plan_builder.coffee b/src/coffee/models/crafting/plan_builder.coffee index 185f2f424..83648dde3 100644 --- a/src/coffee/models/crafting/plan_builder.coffee +++ b/src/coffee/models/crafting/plan_builder.coffee @@ -7,6 +7,7 @@ All rights reserved. CraftingNode = require './crafting_node' CraftingPlan = require './crafting_plan' +CraftingStep = require './crafting_step' Inventory = require '../inventory' ######################################################################################################################## @@ -30,37 +31,34 @@ module.exports = class PlanBuilder producePlans: (maxPlans=null)-> maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE - while @plans.length is 0 or (@plans.length < maxPlans and not @complete) + while not @complete and (@plans.length < maxPlans) plan = @_captureCurrentPlan() - @plans.push plan + if plan? + @plans.push plan + @_incrementChoiceNodes() return @_plans # Property Methods ############################################################################# - isComplete: -> - return @_complete - - getPlans: -> - return @_plans - - getWanted: -> - return @_wanted - - setWanted: (wanted)-> - @_wanted = wanted or new Inventory - Object.defineProperties @prototype, - complete: { get:@prototype.isComplete } - plans: { get:@prototype.getPlans } - wanted: { get:@prototype.getWanted, set:@prototype.setWanted } + + complete: + get: -> @_complete + + plans: + get: -> @_plans + + wanted: + get: -> @_wanted + set: (wanted)-> @_wanted = wanted or new Inventory # Private Methods ############################################################################## _captureCurrentPlan: -> toVisit = [@_rootNode] - steps = [] + stepNodes = [] while toVisit.length > 0 node = toVisit.shift() @@ -70,14 +68,31 @@ module.exports = class PlanBuilder else if node.TYPE is CraftingNode::TYPES.ITEM toVisit.push node.children[0] if node.children.length > 0 else if node.TYPE is CraftingNode::TYPES.RECIPE - steps.push node.recipe + stepNodes.push node toVisit.push(c) for c in node.children - steps.reverse() + steps = [] + seenRecipes = {} + index = stepNodes.length - 1 + while index >= 0 + node = stepNodes[index] + index -= 1 + return null unless node.valid and node.complete + + recipeSlug = node.recipe.slug + continue if seenRecipes[recipeSlug]? + + seenRecipes[recipeSlug] = true + steps.push new CraftingStep node.recipe + plan = new CraftingPlan steps, @_wanted return plan _incrementChoiceNodes: -> + if @_choiceNodes.length is 0 + @_complete = true + return + index = @_choiceNodes.length - 1 while true if index is -1 diff --git a/src/coffee/models/crafting/recipe_node.coffee b/src/coffee/models/crafting/recipe_node.coffee index 1bf1cce9c..1145c7a53 100644 --- a/src/coffee/models/crafting/recipe_node.coffee +++ b/src/coffee/models/crafting/recipe_node.coffee @@ -34,14 +34,15 @@ module.exports = class RecipeNode extends CraftingNode _checkCompleteness: -> for child in @children - return false unless child.isComplete + return false unless child.complete return true _checkValidity: -> - for child in @children - return false unless child.isValid - return false if @_isRepeatedRecipe() + + for child in @children + return false unless child.valid + return true # Private Methods ############################################################################## @@ -60,8 +61,7 @@ module.exports = class RecipeNode extends CraftingNode options.indent ?= '' options.recursive ?= true - completeText = if @complete then 'complete' else 'incomplete' - parts = ["#{options.indent}#{@completeText} RecipeNode for #{@recipe.slug}"] + parts = ["#{options.indent}#{@completeText} #{@validText} RecipeNode for #{@recipe.slug}"] nextIndent = options.indent + ' ' if options.recursive for child in @children diff --git a/src/coffee/models/crafting_plan.coffee b/src/coffee/models/crafting_plan.coffee deleted file mode 100644 index 43dcb75f3..000000000 --- a/src/coffee/models/crafting_plan.coffee +++ /dev/null @@ -1,233 +0,0 @@ -### -Crafting Guide - crafting_plan.coffee - -Copyright (c) 2014-2015 by Redwood Labs -All rights reserved. -### - -BaseModel = require './base_model' -Inventory = require './inventory' -ItemSlug = require './item_slug' -Step = require './step' -_ = require 'underscore' -{Event} = require '../constants' - -######################################################################################################################## - -module.exports = class CraftingPlan extends BaseModel - - constructor: (attributes={}, options={})-> - if not attributes.modPack then throw new Error 'modPack is required' - attributes.includingTools ?= false - super attributes, options - - @have = new Inventory modPack:@modPack - @want = new Inventory modPack:@modPack - @need = new Inventory modPack:@modPack - @result = new Inventory modPack:@modPack - - recraft = _.debounce (=> @craft()), 100 - for inventory in [@have, @want] - inventory.on Event.change, recraft - - @on Event.change + ':includingTools', recraft - - @clear() - - # Public Methods ############################################################################### - - clear: (options={})-> - @steps = [] - @need.clear() - @result.clear() - - @trigger 'change', this - return this - - craft: -> - toolsMessage = if @includingTools then ' (including tools)' else '' - haveMessage = if @have.isEmpty then '' else " starting with #{@have.unparse()}" - logger.info => "crafting #{@want.unparse()}#{toolsMessage}#{haveMessage}" - - @clear() - @have.localize() - @want.localize() - - @result.addInventory @have - - @steps = {} - @want.each (stack)=> - @_findSteps stack.itemSlug, {}, ignoreGatherable:true - item = @modPack.findItem stack.itemSlug - @need.add item.slug, stack.quantity - - @steps = (step for recipeSlug, step of @steps) - @_resolveNeeds() - @_removeExtraSteps() - - @result.addInventory @want - - @need.trigger 'change', @need - @result.trigger 'change', @result - @trigger 'change', this - - removeUncraftableItems: -> - toRemove = [] - @want.each (stack)=> - item = @modPack.findItem stack.itemSlug - if not item? then toRemove.push stack.itemSlug - - for itemSlug in toRemove - @want.remove itemSlug - - # Event Methods ################################################################################ - - onIncludingToolsChanged: -> - @storage.setItem 'includingTools', "#{@includingTools}" - @craft() - - # Object Overrides ############################################################################# - - toString: -> - return "#{@constructor.name} { - have:#{@have}, - want:#{@want}, - need:#{@need}, - result:#{@result}, - steps:#{@steps} - }" - - # Private Methods ############################################################################## - - _findRecipes: (item)-> - recipes = @modPack.findRecipes item.slug - return null unless recipes? and recipes.length > 0 - - recipeOptions = [] - for recipe in recipes - if recipe.tools.length is 0 - recipeOptions.push recipe:recipe, missingTools:0 - else - missingTools = [] - for tool in recipe.tools - if not @have.hasAtLeast tool.itemSlug - missingTools.push tool - recipeOptions.push recipe:recipe, missingTools:missingTools.length - - recipeOptions.sort (a, b)-> - return 0 if a.missingTools is b.missingTools - return if a.missingTools < b.missingTools then -1 else +1 - - return (option.recipe for option in recipeOptions) - - _findSteps: (itemSlug, parentSteps={})-> - item = @modPack.findItem itemSlug - return unless item? - return unless item.isCraftable - - ignoreGatherable = @want.hasAtLeast itemSlug, 1 - if (not item.isGatherable) or ignoreGatherable - recipes = @_findRecipes item - recipes ?= [] - - if parentSteps[item.slug]? - logger.verbose -> "found cycle at #{item.slug}" - throw new Error 'invalid recipe path' - parentSteps[item.slug] = item - - logger.verbose -> "exploring: #{item.slug}" - logger.indent() - - currentSteps = _.clone @steps - foundValidRecipe = false - for i in [0...recipes.length] by 1 - recipe = recipes[i] - logger.verbose -> "trying recipe #{i+1} of #{recipes.length}: #{recipe.slug}" - if @steps[recipe.slug]? - logger.verbose -> "already accepted this recipe" - foundValidRecipe = true - break - - try - if @includingTools - for toolStack in recipe.tools - if not @_hasStep toolStack.itemSlug - @_findSteps toolStack.itemSlug, parentSteps - - for inputStack in recipe.input - @_findSteps inputStack.itemSlug, parentSteps - - logger.verbose -> "adding step for: #{recipe.slug}" - @steps[recipe.slug] = new Step outputItemSlug:item.slug, recipe:recipe - foundValidRecipe = true - break - catch error - logger.verbose -> "recipe didn't work out: #{recipe.slug}" - if error.message isnt 'invalid recipe path' then throw error - @steps = _.clone currentSteps - - delete parentSteps[item.slug] - logger.outdent() - - if not (foundValidRecipe or item.isGatherable) - logger.verbose -> "could not find a valid recipe for #{item.slug}" - throw new Error 'invalid recipe path' - - _hasStep: (itemSlug)-> - for recipeSlug, step of @steps - return true if step.recipe.produces itemSlug - return false - - _qualifyItemSlug: (itemSlug)-> - item = @modPack.findItem itemSlug - return item.slug if item? - return itemSlug - - _removeExtraSteps: -> - result = [] - - number = 1 - for step in @steps - if step.multiplier > 0 - result.push step - step.number = number - number += 1 - - @steps = result - - _resolveNeeds: -> - for i in [@steps.length-1..0] by -1 - step = @steps[i] - step.number = i + 1 - - recipe = step.recipe - outputQuantity = recipe.getQuantityProducedOf step.outputItemSlug - - step.multiplier = Math.ceil(@need.quantityOf(step.outputItemSlug) / outputQuantity) - - if @includingTools - recipe.eachToolStack (stack)=> - itemSlug = @_qualifyItemSlug stack.itemSlug - available = @result.quantityOf(itemSlug) + @need.quantityOf(itemSlug) - needed = Math.max 0, stack.quantity - available - - @need.add itemSlug, needed - @result.add itemSlug, needed - - recipe.eachInputStack (stack)=> - itemSlug = @_qualifyItemSlug stack.itemSlug - needed = step.multiplier * stack.quantity - consumed = Math.min needed, @result.quantityOf itemSlug - remaining = needed - consumed - - @result.remove itemSlug, consumed - @need.add itemSlug, remaining - - recipe.eachOutputStack (stack)=> - itemSlug = @_qualifyItemSlug stack.itemSlug - created = stack.quantity * step.multiplier - consumed = Math.min created, @need.quantityOf itemSlug - remaining = created - consumed - - @result.add itemSlug, remaining - @need.remove itemSlug, consumed diff --git a/src/coffee/models/recipe.coffee b/src/coffee/models/recipe.coffee index caac27642..40b79e4fa 100644 --- a/src/coffee/models/recipe.coffee +++ b/src/coffee/models/recipe.coffee @@ -6,6 +6,7 @@ All rights reserved. ### BaseModel = require './base_model' +ItemSlug = require './item_slug' Stack = require './stack' {Event} = require '../constants' {StringBuilder} = require 'crafting-guide-common' @@ -46,50 +47,15 @@ module.exports = class Recipe extends BaseModel return -1 if aValue return +1 if bValue - aValue = a.getQuantityProducedOf itemSlug - bValue = b.getQuantityProducedOf itemSlug + aValue = a.getQuantityProduced itemSlug + bValue = b.getQuantityProduced itemSlug if aValue isnt bValue return if aValue > bValue then -1 else +1 - aValue = a.getOutputCount() - bValue = b.getOutputCount() - if aValue isnt bValue - return if aValue > bValue then -1 else +1 - - aValue = a.tools.length - bValue = b.tools.length - if aValue isnt bValue - return if aValue < bValue then -1 else +1 - - aValue = a.getInputCount() - bValue = b.getInputCount() - if aValue isnt bValue - return if aValue < bValue then -1 else +1 - return 0 # Public Methods ############################################################################### - eachInputStack: (callback)-> - for i in [0...@pattern.length] - stack = @getStackAtSlot(i) - continue unless stack? - callback stack - - eachOutputStack: (callback)-> - for stack in @output - callback stack - - eachToolStack: (callback)-> - for stack in @tools - callback stack - - getInputCount: -> - result = 0 - for stack in @input - result += stack.quantity - return result - getStackAtSlot: (patternSlot)-> trueIndex = 0:0, 1:1, 2:2, 3:4, 4:5, 5:6, 6:8, 7:9, 8:10 patternDigit = @pattern[trueIndex[patternSlot]] @@ -101,13 +67,7 @@ module.exports = class Recipe extends BaseModel return stack - getOutputCount: -> - result = 0 - for stack in @output - result += stack.quantity - return result - - getQuantityProducedOf: (itemSlug)-> + getQuantityProduced: (itemSlug)-> total = 0 for stack in @output if stack.itemSlug.matches itemSlug @@ -115,6 +75,16 @@ module.exports = class Recipe extends BaseModel return total + getQuantityRequired: (itemSlug)-> + total = 0 + for i in [0...@pattern.length] + stack = @getStackAtSlot i + continue unless stack? + + if ItemSlug.equal stack.itemSlug, itemSlug + total += stack.quantity + return total + hasAllTools: (modPack)-> modPack ?= @modVersion?.mod?.modPack return true unless modPack @@ -179,7 +149,7 @@ module.exports = class Recipe extends BaseModel if not @_slug? builder = new StringBuilder delimiterNeeded = false - @eachInputStack (stack)-> + for stack in @input if delimiterNeeded then builder.push ',' delimiterNeeded = true @@ -187,12 +157,12 @@ module.exports = class Recipe extends BaseModel builder.push stack.itemSlug.qualified builder.push '>' - @eachToolStack (stack)-> + for stack in @tools builder.push stack.itemSlug.qualified builder.push '>' delimiterNeeded = false - @eachOutputStack (stack)-> + for stack in @output if delimiterNeeded then builder.push ',' delimiterNeeded = true @@ -215,7 +185,9 @@ module.exports = class Recipe extends BaseModel needsDelimiter = false for stack in @input if needsDelimiter then result.push ', ' - result.push stack.toString() + result.push @getQuantityRequired stack.itemSlug + result.push ' ' + result.push stack.itemSlug needsDelimiter = true result.push ']' @@ -223,7 +195,9 @@ module.exports = class Recipe extends BaseModel needsDelimiter = false for stack in @output if needsDelimiter then result.push ', ' - result.push stack.toString() + result.push @getQuantityProduced stack.itemSlug + result.push ' ' + result.push stack.itemSlug needsDelimiter = true result.push ']' diff --git a/src/coffee/models/step.coffee b/src/coffee/models/step.coffee index 087b6cac1..71a5e80c6 100644 --- a/src/coffee/models/step.coffee +++ b/src/coffee/models/step.coffee @@ -31,5 +31,5 @@ module.exports = class Step extends BaseModel _computeInventory: -> @inventory.clear() - @recipe.eachInputStack (stack)=> + for stack in @recipe.input @inventory.add stack.itemSlug, stack.quantity * @multiplier diff --git a/test/crafting/crafting_plan.test.coffee b/test/crafting/crafting_plan.test.coffee index 3c729eec1..7271a6287 100644 --- a/test/crafting/crafting_plan.test.coffee +++ b/test/crafting/crafting_plan.test.coffee @@ -7,6 +7,7 @@ All rights reserved. CraftingPlan = require '../../src/coffee/models/crafting/crafting_plan' fixtures = require './fixtures' +ItemSlug = require '../../src/coffee/models/item_slug' ######################################################################################################################## @@ -20,30 +21,75 @@ describe 'crafting_plan.coffee', -> plan.required.unparse().should.equal 'coal' plan.produced.unparse().should.equal 'coal' - it 'can compute a single item with a single step', -> + it 'can compute a single item with one single step plan', -> plans = fixtures.makePlans [1, 'test__charcoal'] plans.length.should.equal 1 plan = plans[0] plan.required.unparse().should.equal 'coal:8.oak_wood' plan.produced.unparse().should.equal '8.charcoal' + (s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>>8 test__charcoal'] - it 'can compute a large quantity of a single item with a single step', -> + it 'can compute a large quantity of a single item with one single step plan', -> plans = fixtures.makePlans [15, 'test__charcoal'] plans.length.should.equal 1 plan = plans[0] plan.required.unparse().should.equal '2.coal:16.oak_wood' plan.produced.unparse().should.equal '16.charcoal' + (s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>>8 test__charcoal'] - it 'can compute a single item with multiple steps', -> + it 'can compute a single item with multiple plans', -> plans = fixtures.makePlans [1, 'test__iron_ingot'] plans.length.should.equal 2 plan = plans[0] plan.required.unparse().should.equal 'coal:8.iron_ore:8.oak_wood' - plan.produced.unparse().should.equal '7.charcoal:iron_ingot' + plan.produced.unparse().should.equal '7.charcoal:8.iron_ingot' + (s.toString() for s in plan.steps).should.eql [ + '1x 8 test__oak_wood,test__coal>>8 test__charcoal' + '1x 8 test__iron_ore,test__charcoal>test__furnace>8 test__iron_ingot' + ] plan = plans[1] plan.required.unparse().should.equal 'coal:8.iron_ore' - plan.produced.unparse().should.equal 'iron_ingot' + plan.produced.unparse().should.equal '8.iron_ingot' + (s.toString() for s in plan.steps).should.eql [ + '1x 8 test__iron_ore,test__coal>test__furnace>8 test__iron_ingot' + ] + + it 'can compute multiple items with multiple plans', -> + plans = fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword'] + plans.length.should.equal 4 + + for plan in plans + plan.required.unparse().should.match /16.copper_ore.*8.iron_ore/ + plan.produced.unparse().should.match /copper_block.*:iron_sword/ + + plan = plans[0] + plan.required.unparse().should.match /3.coal.*9.oak_wood/ + plan.produced.unparse().should.match /7.charcoal/ + "#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__charcoal>test__furnace>8 test__iron_ingot' + "#{plan.steps[4]}".should.equal '2x 8 test__copper_ore,test__coal>test__furnace>8 test__copper_ingot' + plan.steps.length.should.equal 7 + + plan = plans[1] + plan.required.unparse().should.match /3.coal.*:oak_wood/ + plan.produced.unparse().should.not.match /charcoal/ + "#{plan.steps[2]}".should.equal '1x 8 test__iron_ore,test__coal>test__furnace>8 test__iron_ingot' + "#{plan.steps[3]}".should.equal '2x 8 test__copper_ore,test__coal>test__furnace>8 test__copper_ingot' + plan.steps.length.should.equal 6 + + plan = plans[2] + plan.required.unparse().should.match /^coal.*9.oak_wood/ + plan.produced.unparse().should.match /5.charcoal/ + "#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__charcoal>test__furnace>8 test__iron_ingot' + "#{plan.steps[4]}".should.equal '2x 8 test__copper_ore,test__charcoal>test__furnace>8 test__copper_ingot' + plan.steps.length.should.equal 7 + + plan = plans[3] + plan.required.unparse().should.match /2.coal.*9.oak_wood/ + plan.produced.unparse().should.match /6.charcoal/ + "#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__coal>test__furnace>8 test__iron_ingot' + "#{plan.steps[4]}".should.equal '2x 8 test__copper_ore,test__charcoal>test__furnace>8 test__copper_ingot' + plan.steps.length.should.equal 7 diff --git a/test/crafting/fixtures.coffee b/test/crafting/fixtures.coffee index 8093685e3..c1e025d45 100644 --- a/test/crafting/fixtures.coffee +++ b/test/crafting/fixtures.coffee @@ -41,16 +41,19 @@ MOD_VERSION_FILE = item: Copper Ingot recipe: - input: Copper Ore, Coal + input: 8 Copper Ore, Coal pattern: .0. ... .1. + quantity: 8 tools: Furnace recipe: - input: Copper Ore, Charcoal + input: 8 Copper Ore, Charcoal pattern: .0. ... .1. + quantity: 8 tools: Furnace recipe: input: Copper Block pattern: ... .0. ... + quantity: 9 item: Copper Ore @@ -66,10 +69,12 @@ MOD_VERSION_FILE = recipe: input: 8 Iron Ore, Charcoal pattern: .0. ... .1. + quantity: 8 tools: Furnace recipe: input: 8 Iron Ore, Coal pattern: .0. ... .1. + quantity: 8 tools: Furnace item: Iron Sword diff --git a/test/crafting/plan_builder.test.coffee b/test/crafting/plan_builder.test.coffee index 8d3aca511..f33d8e3b9 100644 --- a/test/crafting/plan_builder.test.coffee +++ b/test/crafting/plan_builder.test.coffee @@ -13,27 +13,31 @@ PlanBuilder = require '../../src/coffee/models/crafting/plan_builder' describe 'plan_builder.coffee', -> printPlan = (plan)-> - return ((s.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > ' + return '' unless plan? + return ((s.recipe.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > ' it 'generates an empty plan for a gatherable item', -> - builder = new PlanBuilder fixtures.makeTree 'test__oak_wood' - plans = builder.producePlans 100 + plans = fixtures.makePlans [1, 'test__oak_wood'] plans.length.should.equal 1 plans[0].length.should.equal 0 - builder.complete.should.be.true it 'can find a multi-step plan', -> - builder = new PlanBuilder fixtures.makeTree 'test__lever' - plans = builder.producePlans 100 + plans = fixtures.makePlans [1, 'test__lever'] printPlan(plans[0]).should.equal '4 test__oak_planks > 4 test__stick > test__lever' plans.length.should.equal 1 - builder.complete.should.be.true it 'can find multiple plans', -> - builder = new PlanBuilder fixtures.makeTree 'test__iron_ingot' - plans = builder.producePlans 100 + plans = fixtures.makePlans [1, 'test__iron_ingot'] - printPlan(plans[0]).should.equal '8 test__charcoal > test__iron_ingot' - printPlan(plans[1]).should.equal 'test__iron_ingot' + printPlan(plans[0]).should.equal '8 test__charcoal > 8 test__iron_ingot' + printPlan(plans[1]).should.equal '8 test__iron_ingot' + plans.length.should.equal 2 + + it 'ignores invalid plans', -> + plans = fixtures.makePlans [1, 'test__copper_block'] + + printPlan(plans[0]).should.equal '8 test__copper_ingot > test__copper_block' + printPlan(plans[1]).should.equal '8 test__charcoal > 8 test__copper_ingot > test__copper_block' + plans.length.should.equal 2 diff --git a/test/parser_versions/mod_version_parser_v1.test.coffee b/test/parser_versions/mod_version_parser_v1.test.coffee index 611e57caf..545e5a4fd 100644 --- a/test/parser_versions/mod_version_parser_v1.test.coffee +++ b/test/parser_versions/mod_version_parser_v1.test.coffee @@ -91,6 +91,11 @@ describe 'mod_version_parser_v1.coffee', -> modVersion = parser.parse baseText + 'recipe:; input:Delta, Echo, Foxtrot; pattern:...012...' (s.item for s in modVersion._slugs).should.eql ['charlie', 'delta', 'echo', 'foxtrot'] + it 'correctly handles recipes which use the same input multiple times', -> + modVersion = parser.parse baseText + 'recipe:; input:Alpha; pattern:.0..0....' + recipe = _.values(modVersion._recipes)[0] + recipe.getQuantityRequired(ItemSlug.slugify('alpha')).should.equal 2 + it 'allows a quantity for each input', -> modVersion = parser.parse baseText + 'recipe:; input: 12 Delta, 3 Echo; pattern:... 0.1 ...' recipe = _.values(modVersion._recipes)[0]