diff --git a/Gruntfile.coffee b/Gruntfile.coffee index 87946c40b..42cc2b10b 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -132,7 +132,7 @@ module.exports = (grunt)-> options: bail: true color: true - reporter: 'dot' + reporter: 'list' require: [ 'coffee-script/register' './src/test_helper.coffee' @@ -188,7 +188,7 @@ module.exports = (grunt)-> tasks: ['sass'] test: files: ['./src/**/*.coffee', './src/**/*.js', './test/**/*.coffee'] - tasks: ['test'] + tasks: ['script:clear', 'test'] # Compound Tasks ################################################################################################### @@ -277,7 +277,7 @@ module.exports = (grunt)-> grunt.registerTask 'script:clear', "clear the current terminal buffer", -> done = this.async() - grunt.util.spawn cmd:'clear', opts:{stdio:'inherit'}, (error)-> done(error) + grunt.util.spawn cmd:'./scripts/clear_buffer', opts:{stdio:'inherit'}, (error)-> done(error) grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", -> done = this.async() diff --git a/scripts/clear_buffer b/scripts/clear_buffer new file mode 100755 index 000000000..31cff086e --- /dev/null +++ b/scripts/clear_buffer @@ -0,0 +1,3 @@ +#!/bin/bash + +clear && printf '\e[3J' \ No newline at end of file diff --git a/src/client/models/crafting/crafting_node.coffee b/src/client/models/crafting/crafting_node.coffee deleted file mode 100644 index a54ec08cc..000000000 --- a/src/client/models/crafting/crafting_node.coffee +++ /dev/null @@ -1,134 +0,0 @@ -# -# Crafting Guide - crafting_node.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -######################################################################################################################## - -module.exports = class CraftingNode - - @::TYPES = - INVENTORY: 0 - ITEM: 1 - RECIPE: 2 - - @::TYPE = null - - constructor: (options={})-> - if not options.modPack? then throw new Error 'options.modPack is required' - - @modPack = options.modPack - - @_children = [] - @_complete = null - @_id = _.uniqueId 'node_' - @_rotations = 0 - @_valid = null - - # Public Methods ############################################################################### - - expand: (queue=[])-> - return unless @valid - return queue if @children.length > 0 - - for child in @_createChildren() - child.parent = this - @_children.push child - queue.push child - return queue - - acceptVisitor: (visitor)-> - enter = visitor[@ENTER_METHOD] - if enter? - enter.call visitor, this - else - enter = visitor['onEnterOtherNode'] - if enter? then enter.call visitor, this - - for child in @_children - child.acceptVisitor visitor - - leave = visitor[@LEAVE_METHOD] - if leave? - leave.call visitor, this - else - leave = visitor['onLeaveOtherNode'] - if leave? then leave.call visitor, this - - pruneInvalidChildren: -> - @_pruneInvalidChildren() - - removeChild: (index)-> - @_children.splice index, 1 - - rotateChildren: -> - @_children.push @_children.shift() - @_rotations += 1 - - # Property Methods ############################################################################# - - Object.defineProperties @prototype, - - 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 ############################################################################## - - # Subclasses must override this method to create such children as are appropriate for that kind of node. The new - # nodes should be appended to the given array. - _createChildren: (result)-> - throw new Error "#{@constructor.name} must override the _createChildren method" - - # Subclasses must override this method to indicate whether the node has at least one "complete" subtree rooted with - # itself. Completeness may be defined differently by different subclasses. - _checkCompleteness: -> - throw new Error "#{@constructor.name} must override the _checkCompletness method" - - # Subclasses must override this method to determine whether the subtree represented by a node should still be - # explored for having a useful crafting plan. - _checkValidity: -> - throw new Error "#{@constructor.name} must override the _checkValidity method" - - # Subclasses may override this method to remove any children determined to be invalid and therefore unable to be - # of any use in finding legitimate crafting plans. - _pruneInvalidChildren: -> - for child in @children - child.pruneInvalidChildren() diff --git a/src/client/models/crafting/crafting_node.test.coffee b/src/client/models/crafting/crafting_node.test.coffee deleted file mode 100644 index 189e719f5..000000000 --- a/src/client/models/crafting/crafting_node.test.coffee +++ /dev/null @@ -1,44 +0,0 @@ -# -# Crafting Guide - crafting_node.test.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -fixtures = require './fixtures.test' - -######################################################################################################################## - -root = null - -######################################################################################################################## - -describe 'crafting_node.coffee', -> - - describe 'acceptVisitor', -> - - it 'can provide a prefix, depth-first traversal', -> - root = fixtures.makeTree 'test__iron_ingot' - - visitor = - nodes: [] - onEnterOtherNode: (node)-> visitor.nodes.push node - - root.acceptVisitor visitor - - (v.constructor.name.replace('Node', '') for v in visitor.nodes).should.eql [ - "Inventory", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Item" - ] - - it 'can provide a postfix, depth-first traversal', -> - root = fixtures.makeTree 'test__iron_ingot' - - visitor = - nodes: [] - onLeaveOtherNode: (node)-> visitor.nodes.push node - - root.acceptVisitor visitor - - (n.constructor.name.replace('Node', '') for n in visitor.nodes).should.eql [ - "Item", "Item", "Item", "Recipe", "Item", "Recipe", "Item", "Item", "Recipe", "Item", "Inventory" - ] diff --git a/src/client/models/crafting/crafting_plan.coffee b/src/client/models/crafting/crafting_plan.coffee index 75535f14a..98f791f49 100644 --- a/src/client/models/crafting/crafting_plan.coffee +++ b/src/client/models/crafting/crafting_plan.coffee @@ -5,174 +5,140 @@ # All rights reserved. # -SimpleInventory = require './simple_inventory' +Inventory = require "./inventory" +{StringBuilder} = require "crafting-guide-common" ######################################################################################################################## module.exports = class CraftingPlan - constructor: (modPack, want, have, steps)-> - if not modPack? then throw new Error 'modPack is required' - if not want? then throw new Error 'want is required' - if not have? then throw new Error 'have is required' - if not steps? then throw new Error 'steps is required' + constructor: (attributes={})-> + @_id = _.uniqueId "crafting-plan-" + @_make = null + @_need = null + @have = attributes.have + @steps = attributes.steps + @want = attributes.want - @_have = have - @_made = null - @_modPack = modPack - @_need = null - @_rawScores = {} - @_scores = {} - @_steps = steps - @_tools = null - @_want = want + @_consolidateSteps() + @_computeResources() - @_numberSteps() - - # Public Methods ############################################################################### - - computeRequired: -> - @_need = new SimpleInventory modPack:@_modPack - @_need.addInventory @_want - - @_made = new SimpleInventory modPack:@_modPack - @_made.addInventory @_have - - @_tools = new SimpleInventory modPack:@_modPack - - for i in [@_steps.length-1..0] by -1 - step = @_steps[i] - step.multiplier = 0 - recipe = step.recipe - - for stack in step.recipe.output - continue if recipe.isPassThroughFor stack.itemSlug - qualifiedSlug = @_modPack.qualifySlug stack.itemSlug - - while @_need.quantityOf(qualifiedSlug) > 0 - @_executeStep step - - if step.multiplier > 0 - for stack in step.recipe.output - qualifiedSlug = @_modPack.qualifySlug stack.itemSlug - continue unless recipe.isPassThroughFor stack.itemSlug - continue if @_made.hasAtLeast qualifiedSlug, 1 - continue if @_need.hasAtLeast qualifiedSlug, 1 - continue if @_tools.hasAtLeast qualifiedSlug, 1 - @_need.add qualifiedSlug, 1 - @_tools.add qualifiedSlug, 1 - - @_made.addInventory @_want - @_made.addInventory @_tools - @_pruneEmptySteps() - @_numberSteps() - - hasRawScore: (name)-> - return @_rawScores[name]? - - getRawScore: (name)-> - return @_rawScores[name] - - setRawScore: (name, rawScore)-> - @_rawScores[name] = rawScore - - hasScore: (name)-> - return @_scores[name]? - - getScore: (name)-> - return @_scores[name] - - setScore: (name, score)-> - @_scores[name] = score - - # Property Methods ############################################################################# + # Properties ################################################################################### Object.defineProperties @prototype, - have: - get: -> @_have - length: - get: -> @steps.length - made: - get: -> @_made - need: - get: -> @_need - steps: - get: -> @_steps - want: - get: -> @_want + + have: # an Inventory specifying what the player already has + get: -> return @_have + set: (have)-> + have ?= new Inventory + if @_have is have then return + if @_have? then throw new Error "have cannot be reassigned" + @_have = new Inventory have + + id: # a string uniquely specifying this crafting plan + get: -> return @_id + set: -> throw new Error "id is not assignabled" + + make: # an Inventory specifying what the results of executing the plan will be + get: -> return @_make + set: -> throw new Error "make cannot be assigned" + + need: # an Inventory specifying what the player will need to gather before executing the plan + get: -> return @_need + set: -> throw new Error "need cannot be assigned" + + want: # an Inventory specifying what the player wants to make + get: -> return @_want + set: (want)-> + if not want? then throw new Error "want is required" + if want.isEmpty then throw new Error "want cannot be empty" + if @_want is want then return + if @_want? then throw new Error "want cannot be reassigned" + @_want = new Inventory want + + steps: # an array of CraftingPlanSteps detailing the steps of the plan + get: -> return @_steps + set: (steps)-> + steps ?= [] + if steps is @_steps then return + if @_steps? then throw new Error "steps cannot be reassigned" + @_steps = steps # Object Overrides ############################################################################# - toString: -> - result = ["To Make:"] - @_want.each (stack)-> - result.push " #{stack}" + toString: (options={})-> + options.full ?= false - result.push "When you already have:" - @_have.each (stack)-> - result.push " #{stack}" + if options.full + b = new StringBuilder + b.line "CraftingPlan<", @_id, ">" + b.indent() + b.line "have: ", @_have.toString full:true + b.line "want: ", @_want.toString full:true + b.line "need: ", @_need.toString full:true + b.line "steps:" + b.indent() + b.loop @_steps, delimiter:"\n", onEach:(b, step)-> + b.push step.count, " × ", step.recipe.toString(full:true) + b.line() + b.outdent() + b.line "make: ", @_make.toString full:true + b.outdent() - result.push "Start with:" - if @_need? - @_need.each (stack)-> - result.push " #{stack}" - - result.push "Use these recipes:" - for step in @_steps - result.push " #{step}" - - result.push "To produce:" - if @_made? - @_made.each (stack)-> - result.push " #{stack}" - - if _.keys(@_rawScores).length > 0 - result.push "Scores:" - for criteria, score of @_rawScores - if @_scores[criteria]? - result.push " #{criteria}: #{score} (#{@_scores[criteria]})" - else - result.push " #{criteria}: #{score}" - - return result.join '\n' + return b.toString() + else + return "CraftingPlan:#{@_make.toString(full:true)}<#{@_id}>" # Private Methods ############################################################################## - _executeStep: (step)-> - step.multiplier += 1 - recipe = step.recipe + _computeResources: -> + need = new Inventory @_want + make = new Inventory @_have + steps = @_steps[..].reverse() - for stack in recipe.input - qualifiedSlug = @_modPack.qualifySlug stack.itemSlug + for step in steps + step.count = 0 - available = @_made.quantityOf qualifiedSlug - required = recipe.getQuantityRequired stack.itemSlug - consumed = Math.min required, available - deficit = required - consumed + for productStack in step.recipe.allProducts + continue unless need.contains productStack.item + productCount = Math.ceil need.getQuantity(productStack.item) / productStack.quantity + step.count = Math.max step.count, productCount - @_made.remove qualifiedSlug, consumed - @_need.add qualifiedSlug, deficit + continue unless step.count > 0 - for stack in recipe.output - qualifiedSlug = @_modPack.qualifySlug stack.itemSlug + for itemId, item of step.recipe.inputs + amountNeeded = step.count * step.recipe.computeQuantityRequired item + amountAvailable = make.getQuantity item + amountUsed = Math.min amountAvailable, amountNeeded + amountMissing = amountNeeded - amountUsed - deficit = @_need.quantityOf qualifiedSlug - created = recipe.getQuantityProduced stack.itemSlug - replenished = Math.min deficit, created - surplus = created - replenished + make.remove item, amountUsed + need.add item, amountMissing - @_made.add qualifiedSlug, surplus - @_need.remove qualifiedSlug, replenished + for productStack in step.recipe.allProducts + amountCreated = step.count * productStack.quantity + amountNeeded = need.getQuantity productStack.item + amountFulfilled = Math.min amountNeeded, amountCreated + amountSurplus = amountCreated - amountFulfilled - _numberSteps: -> - for step, i in @_steps - step.number = i + 1 + need.remove productStack.item, amountFulfilled + make.add productStack.item, amountSurplus - _pruneEmptySteps: -> - index = 0 - while index < @_steps.length - step = @_steps[index] - if step.multiplier is 0 - @_steps.splice index, 1 - else - index++ + make.merge @_want + + @_make = make + @_need = need + + _consolidateSteps: -> + steps = [] + stepsByRecipeId = {} + + for step in @_steps + priorStep = stepsByRecipeId[step.recipe.id] + if priorStep? + priorStep.count += step.count + else if step.count > 0 + steps.push step + stepsByRecipeId[step.recipe.id] = step + + @_steps = steps diff --git a/src/client/models/crafting/crafting_plan.test.coffee b/src/client/models/crafting/crafting_plan.test.coffee index d1e696ff8..83249514e 100644 --- a/src/client/models/crafting/crafting_plan.test.coffee +++ b/src/client/models/crafting/crafting_plan.test.coffee @@ -5,119 +5,95 @@ # All rights reserved. # -CraftingPlan = require './crafting_plan' -fixtures = require './fixtures.test' -ItemSlug = require '../game/item_slug' +CraftingPlan = require './crafting_plan' +CraftingPlanStep = require './crafting_plan_step' +Inventory = require './inventory' +fixtures = require './fixtures' ######################################################################################################################## -describe 'crafting_plan.coffee', -> +describe "CraftingPlan", -> - it 'requires wanted item if gatherable', -> - plans = fixtures.makePlans [1, 'test__coal'] - plans.length.should.equal 1 + beforeEach -> + @mod = fixtures.createMod() + @want = new Inventory + @have = new Inventory - plan = plans[0] - plan.computeRequired() - plan.need.unparse().should.equal 'coal' - plan.made.unparse().should.equal 'coal' + describe "with a single step plan", -> - it 'can compute a single item with one single step plan', -> - plans = fixtures.makePlans [1, 'test__charcoal'] - plans.length.should.equal 1 + beforeEach -> + @oakPlank = fixtures.configureOakPlank @mod + @want.add @oakPlank, 11 - plan = plans[0] - plan.computeRequired() - plan.need.unparse().should.equal 'coal:8.oak_wood' - plan.made.unparse().should.equal '8.charcoal' - (s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal'] + @plan = new CraftingPlan want:@want, steps:[ + new CraftingPlanStep @oakPlank.firstRecipe + ] - 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 + it "correctly computes the step counts", -> + @plan.steps[0].count.should.equal 3 - plan = plans[0] - plan.computeRequired() - plan.need.unparse().should.equal '2.coal:16.oak_wood' - plan.made.unparse().should.equal '16.charcoal' - (s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal'] + it "correctly determines the inputs needed", -> + @plan.need.toString(full:true).should.equal "3 Oak Wood" - it 'can compute a single item with multiple plans', -> - plans = fixtures.makePlans [1, 'test__iron_ingot'] - plans.length.should.equal 2 + it "correctly computes the products created", -> + @plan.make.toString(full:true).should.equal "12 Oak Planks" - plan = plans[0] - plan.computeRequired() - plan.need.unparse().should.equal 'coal:8.iron_ore:8.oak_wood' - plan.made.unparse().should.equal '7.charcoal:8.iron_ingot' - (s.toString() for s in plan.steps).should.eql [ - '1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal' - '1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot' - ] + describe "with a multi-step plan", -> - plan = plans[1] - plan.computeRequired() - plan.need.unparse().should.equal 'coal:8.iron_ore' - plan.made.unparse().should.equal '8.iron_ingot' - (s.toString() for s in plan.steps).should.eql [ - '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot' - ] + beforeEach -> + @ironIngot = fixtures.configureIronIngot @mod + @ironSword = fixtures.configureIronSword @mod + @oakPlank = fixtures.configureOakPlank @mod + @stick = fixtures.configureStick @mod - it 'uses the correct amount of passthrough items', -> - plans = fixtures.makePlans [10, 'test__split_oak_wood'], [10, 'test__split_spruce_wood'] - plan = plans[2] - plan.computeRequired() + @want.add @ironSword, 20 - plan.need.unparse().should.equal 'coal:8.iron_ore:6.oak_wood:5.spruce_wood' - plan.made.unparse().should.equal '4.iron_ingot:maul:2.oak_planks:10.split_oak_wood:10.split_spruce_wood:stick' - (s.toString() for s in plan.steps).should.eql [ - '1x test__oak_wood>... .0. ...>>4 test__oak_planks' - '1x test__oak_planks>.0. .0. ...>>4 test__stick' - '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot' - '1x test__iron_ingot,test__stick>001 001 ..1>test__crafting_table>test__maul' - '5x test__spruce_wood,test__maul>.1. .0. ...>>2 test__split_spruce_wood,test__maul' - '5x test__oak_wood,test__maul>.1. .0. ...>>2 test__split_oak_wood,test__maul' - ] + @plan = new CraftingPlan want:@want, steps:[ + new CraftingPlanStep @oakPlank.firstRecipe + new CraftingPlanStep @stick.firstRecipe + new CraftingPlanStep @ironIngot.firstRecipe + new CraftingPlanStep @ironSword.firstRecipe + ] - it 'can compute multiple items with multiple plans', -> - plans = fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword'] - plans.length.should.equal 4 + it "correctly computes the step counts", -> + @plan.steps[0].count.should.equal 3 + @plan.steps[1].count.should.equal 5 + @plan.steps[2].count.should.equal 5 + @plan.steps[3].count.should.equal 20 - for plan in plans - plan.computeRequired() - plan.need.unparse().should.match /16.copper_ore.*8.iron_ore/ - plan.made.unparse().should.match /copper_block.*:iron_sword/ + it "correctly determines the inputs needed", -> + @plan.need.toString(full:true).should.equal "5 Coal, 40 Iron Ore, 3 Oak Wood" - plan = plans[0] - plan.need.unparse().should.match /3.coal.*9.oak_wood/ - plan.made.unparse().should.match /7.charcoal/ - "#{plan.steps[3]}".should.equal \ - '1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot' - "#{plan.steps[4]}".should.equal \ - '2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot' - plan.steps.length.should.equal 7 + it "correctly computes the products created", -> + @plan.make.toString(full:true).should.equal "20 Iron Sword, 2 Oak Planks" - plan = plans[1] - plan.need.unparse().should.match /3.coal.*:oak_wood/ - plan.made.unparse().should.not.match /charcoal/ - "#{plan.steps[2]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot' - "#{plan.steps[3]}".should.equal \ - '2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot' - plan.steps.length.should.equal 6 + describe "with a plan which recycles some items", -> - plan = plans[2] - plan.need.unparse().should.match /^coal.*9.oak_wood/ - plan.made.unparse().should.match /5.charcoal/ - "#{plan.steps[3]}".should.equal \ - '1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot' - "#{plan.steps[4]}".should.equal \ - '2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot' - plan.steps.length.should.equal 7 + beforeEach -> + @bucket = fixtures.configureBucket @mod + @cake = fixtures.configureCake @mod + @ironIngot = fixtures.configureIronIngot @mod + @milkBucket = fixtures.configureMilkBucket @mod + @sugar = fixtures.configureSugar @mod - plan = plans[3] - plan.need.unparse().should.match /2.coal.*9.oak_wood/ - plan.made.unparse().should.match /6.charcoal/ - "#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot' - "#{plan.steps[4]}".should.equal \ - '2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot' - plan.steps.length.should.equal 7 + @want.add @cake, 2 + + @plan = new CraftingPlan want:@want, steps:[ + new CraftingPlanStep @ironIngot.firstRecipe + new CraftingPlanStep @bucket.firstRecipe + new CraftingPlanStep @milkBucket.firstRecipe + new CraftingPlanStep @sugar.firstRecipe + new CraftingPlanStep @cake.firstRecipe + ] + + it "correctly computes the step counts", -> + @plan.steps.length.should.equal 3 + @plan.steps[0].count.should.equal 6 + @plan.steps[1].count.should.equal 4 + @plan.steps[2].count.should.equal 2 + + it "correctly determines the inputs needed", -> + @plan.need.toString(full:true).should.equal "2 Egg, 6 Milk, 4 Sugar Cane, 6 Wheat" + + it "correctly computes the products created", -> + @plan.make.toString(full:true).should.equal "2 Cake" diff --git a/src/client/models/crafting/crafting_plan_step.coffee b/src/client/models/crafting/crafting_plan_step.coffee new file mode 100644 index 000000000..2986e2b4e --- /dev/null +++ b/src/client/models/crafting/crafting_plan_step.coffee @@ -0,0 +1,39 @@ +# +# Crafting Guide - crafting_plan_step.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +######################################################################################################################## + +module.exports = class CraftingPlanStep + + constructor: (recipe, count=1)-> + @_id = _.uniqueId "crafting-plan-step-" + @recipe = recipe + @count = count + + # Properties ################################################################################### + + Object.defineProperties @prototype, + + recipe: + get: -> return @_recipe + set: (recipe)-> + if not recipe? then throw new Error 'recipe is required' + if @_recipe is recipe then return + if @_recipe? then throw new Error 'recipe cannot be reassigned' + @_recipe = recipe + + count: + get: -> return @_count + set: (count)-> + count = parseInt "#{count}" + count = if Number.isNaN(count) then 0 else Math.max 0, count + @_count = count + + # Object Overrides ############################################################################# + + toString: -> + return "CraftingPlanStep:#{@_recipe}×#{@_count}<#{@_id}>" diff --git a/src/client/models/crafting/crafting_step.coffee b/src/client/models/crafting/crafting_step.coffee deleted file mode 100644 index 2d1a2bd60..000000000 --- a/src/client/models/crafting/crafting_step.coffee +++ /dev/null @@ -1,73 +0,0 @@ -# -# Crafting Guide - crafting_step.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -SimpleInventory = require './simple_inventory' - -######################################################################################################################## - -module.exports = class CraftingStep - - constructor: (recipe, modPack, multiplier=0)-> - if not recipe? then throw new Error 'recipe is required' - if not modPack? then throw new Error 'modPack is required' - if multiplier < 0 then throw new Error 'multiplier must be at least 1' - - @number = null - - @_inventory = null - @_modPack = modPack - @_multiplier = multiplier - @_recipe = recipe - - # Public Methods ############################################################################### - - addToolsTo: (targetInventory)-> - for stack in @_recipe.tools - targetInventory.add stack.itemSlug, stack.quantity, insert:true - - completeInto: (targetInventory)-> - for stack in @_recipe.output - continue if @_recipe.isPassThroughFor stack.itemSlug - quantity = @_recipe.getQuantityProduced stack.itemSlug - targetInventory.add stack.itemSlug, quantity * @_multiplier - - # Property Methods ############################################################################# - - Object.defineProperties @prototype, - - inventory: - get: -> - if not @_inventory? - @_refreshInventory() - return @_inventory - - recipe: - get: -> @_recipe - - multiplier: - get: -> @_multiplier - set: (newMultiplier)-> - @_multiplier = newMultiplier - @_refreshInventory() if @_inventory? - - slug: - get: -> "#{@multiplier}x #{@_recipe.slug}" - - # Object Overrides ############################################################################# - - toString: -> - return @slug - - # Private Methods ############################################################################## - - _refreshInventory: -> - @_inventory = new SimpleInventory - for stack in @_recipe.input - qualifiedSlug = @_modPack.qualifySlug stack.itemSlug - required = @_recipe.getQuantityRequired(stack.itemSlug) - @_recipe.getQuantityProduced(stack.itemSlug) - required = if required > 0 then required * @multiplier else 1 - @_inventory.add qualifiedSlug, required diff --git a/src/client/models/crafting/craftsman.coffee b/src/client/models/crafting/craftsman.coffee deleted file mode 100644 index 9fd8419c9..000000000 --- a/src/client/models/crafting/craftsman.coffee +++ /dev/null @@ -1,150 +0,0 @@ -# -# Crafting Guide - craftsman.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -BaseModel = require '../base_model' -GraphBuilder = require './graph_builder' -Inventory = require '../game/inventory' -PlanBuilder = require './plan_builder' -PlanEvaluator = require './plan_evaluator' - -######################################################################################################################## - -module.exports = class Craftsman extends BaseModel - - @::ANALYZE_STEP_INCREMENT = 29 - - @::GRAPH_STEP_INCREMENT = 59 - - @::PLAN_STEP_INCREMENT = 39 - - @::STAGE = - EMPTY: 'empty' - READY: 'ready' - GRAPHING: 'examining recipes' - PLANNING: 'computing plans' - ANALYZING: 'analyzing plans' - COMPLETE: 'complete' - INVALID: 'invalid' - OUTDATED: 'outdated' - - constructor: (modPack)-> - if not modPack? then throw new Error 'modPack is required' - attributes = - stage: @STAGE.READY - stageCount: 0 - super attributes, {} - - @_modPack = modPack - - @_have = new Inventory modPack:@_modPack - @_have.on c.event.change, => @_resetStage() - - @_want = new Inventory modPack:@_modPack - @_want.on c.event.change, => @_resetStage() - - @on c.event.change + ':stage', => logger.info "Craftsman has started #{@stage}..." - @on 'scheduleNextWork', => @_scheduleNextWork() - @reset() - - # Public Methods ############################################################################### - - reset: -> - @_graphBuilder = null - @_planBuilder = null - @_planEvaluator = null - @_plans = null - - @_resetStage() - - work: -> - return if @_want.isEmpty - - if not @_graphBuilder? - @_want.localize() - @_have.localize() - logger.info -> "Craftsman starting to build #{@_want} from #{@_have}" - - @_graphBuilder = new GraphBuilder modPack:@_modPack, want:@_want, have:@_have - @stage = @STAGE.GRAPHING - @stageCount = 0 - else if not @_graphBuilder.complete - @_graphBuilder.expandGraph @GRAPH_STEP_INCREMENT - @stageCount = @_graphBuilder.stepCount - logger.verbose => "Craftsman is expanding graph: #{@stageCount} nodes" - else if not @_graphBuilder.rootNode.valid - @stage = @STAGE.INVALID - @stageCount = 0 - logger.warning => "Craftsman could not complete a crafting plan:\n#{@_graphBuilder.rootNode}" - else if not @_planBuilder? - @_graphBuilder.pruneInvalidNodes() - logger.debug => "Craftsman finished computing graph:\n#{@_graphBuilder.rootNode}" - - @_planBuilder = new PlanBuilder @_graphBuilder.rootNode, @_modPack, have:@_have, want:@_want - @stage = @STAGE.PLANNING - @stageCount = 0 - else if not @_planBuilder.complete - @_planBuilder.producePlans @PLAN_STEP_INCREMENT - @stageCount = @_planBuilder.plans.length - logger.verbose => "Craftsman is producing plans: #{@_planBuilder.plans.length} plans" - else if not @_planEvaluator? - logger.verbose => "Craftsman finished producing plans: #{@_planBuilder.plans.length} plans" - @_planEvaluator = new PlanEvaluator @_planBuilder.plans - @stage = @STAGE.ANALYZING - @stageCount = 0 - else if not @_planEvaluator.complete - @_planEvaluator.scorePlans @ANALYZE_STEP_INCREMENT - @stageCount = @_planEvaluator.lastScored - logger.verbose => "Craftsman is rating plans: #{@stageCount} plans" - else - @_plans = [ - @_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.FEWEST_STEPS - @_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.LEAST_MATERIALS - ] - @stage = @STAGE.COMPLETE - @stageCount = 0 - - logger.info => "Craftsman has finished with plans: #{(p.toString() for p in @_plans).join('\n\n')}" - - @trigger c.event.change, this - @trigger 'scheduleNextWork' - return @complete - - # Property Methods ############################################################################# - - Object.defineProperties @prototype, - - complete: - get: -> @stage in [@STAGE.COMPLETE, @STAGE.INVALID, @STAGE.OUTDATED] - - have: - get: -> @_have - - plan: - get: -> @_plans?[0] - - want: - get: -> @_want - - # Private Methods ############################################################################## - - _resetStage: -> - @stageCount = 0 - - if @want.isEmpty - if @stage isnt @STAGE.EMPTY - @stage = @STAGE.EMPTY - @reset() - else if not @_plans? - @stage = @STAGE.READY - else - @stage = @STAGE.OUTDATED - - _scheduleNextWork: -> - return if @want.isEmpty - return if @complete - - _.defer => @work() diff --git a/src/client/models/crafting/evaluation.coffee b/src/client/models/crafting/evaluation.coffee new file mode 100644 index 000000000..ea4cde295 --- /dev/null +++ b/src/client/models/crafting/evaluation.coffee @@ -0,0 +1,112 @@ +# +# Crafting Guide - evaluation.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +######################################################################################################################## + +module.exports = class Evaluation + + constructor: (attributes={})-> + @evaluator = attributes.evaluator + @item = attributes.item if attributes.item? + @recipe = attributes.recipe if attributes.recipe? + @baseScore = attributes.baseScore + + @_baseEvaluations = [] + @_includedTools = {} + @_toolScore = null + + # Properties ################################################################################### + + Object.defineProperties @prototype, + + baseEvaluations: + get: -> return @_baseEvaluations + set: -> throw new Error "baseEvaluations cannot be assigned" + + baseScore: + get: -> return @_baseScore + set: (baseScore)-> + baseScore = parseFloat "#{baseScore}" + baseScore = if Number.isNaN(baseScore) then null else baseScore + @_baseScore = baseScore + + evaluator: + get: -> return @_evaluator + set: (evaluator)-> + if not evaluator? then throw new Error "evaluator is required" + if @_evaluator is evaluator then return + if @_evaluator? then throw new Error "evaluator cannot be reassigned" + @_evaluator = evaluator + + includedTools: + get: -> return @_includedTools + set: -> throw new Error "includedTools cannot be assigned" + + item: + get: -> return @_item + set: (item)-> + if @_recipe? then throw new Error "this evaluation is for a recipe: cannot set an item" + if @_item is item then return + if not item? then throw new Error "item cannot be assigned null" + if @_item? then throw new Error "item cannot be reassigned" + @_item = item + + recipe: + get: -> return @_recipe + set: (recipe)-> + if @_item? then throw new Error "this evaluation is for an item: cannot set an recipe" + if @_recipe is recipe then return + if not recipe? then throw new Error "recipe cannot be assigned null" + if @_recipe? then throw new Error "recipe cannot be reassigned" + @_recipe = recipe + + toolScore: + get: -> @_computeToolScore() + set: -> throw new Error "toolScore cannot be assigned" + + # Public Methods ############################################################################### + + addBaseEvaluation: (evaluation)-> + @_baseEvaluations.push evaluation + + addIncludedTool: (item)-> + @_includedTools[item.id] = item + @_toolScore = null + + computeTotalScore: (quantity=1)-> + if @item? + multiplier = quantity + else if @recipe? + multiplier = Math.ceil 1.0 * quantity / @recipe.output.quantity + + return @baseScore * multiplier + @toolScore + + isToolIncluded: (item)-> + return true if @_includedTools[item.id]? + + for baseEvaluation in @_baseEvaluations + return true if baseEvaluation.isToolIncluded item + + return false + + # Object Overrides ############################################################################# + + toString: -> + obj = if @item? then @item else @recipe + return "#{@evaluator}=>#{obj}@#{@baseScore}" + + # Private Methods ############################################################################## + + _computeToolScore: -> + if not @_toolScore? + @_toolScore = 0 + for id, toolItem of @_includedTools + evaluation = @evaluator.evaluateItem toolItem + continue unless evaluation?.baseScore? + @_toolScore += evaluation.baseScore + + return @_toolScore diff --git a/src/client/models/crafting/evaluator.coffee b/src/client/models/crafting/evaluator.coffee new file mode 100644 index 000000000..079e56384 --- /dev/null +++ b/src/client/models/crafting/evaluator.coffee @@ -0,0 +1,102 @@ +# +# Crafting Guide - evaluator.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +Evaluation = require './evaluation' + +######################################################################################################################## + +module.exports = class Evaluator + + constructor: -> + @_id = _.uniqueId "evaluator-" + @_evaluations = {} + + # Property Methods ############################################################################# + + Object.defineProperties @prototype, + + id: + get: -> return @_id + set: -> throw new Error "id cannot be assigned" + + # Public Methods ############################################################################### + + evaluateItem: (item)-> + return null unless item? + + evaluation = @_evaluations[item.id] + if not evaluation? + evaluation = @_evaluations[item.id] = new Evaluation evaluator:this, item:item + + recipeEvaluation = @_findBestRecipeEvaluationFor item + if recipeEvaluation? + evaluation.addBaseEvaluation recipeEvaluation + evaluation.baseScore = recipeEvaluation.baseScore + else + @_computeGatherableItemScore item, evaluation + + return evaluation + + evaluateRecipe: (recipe)-> + return null unless recipe? + + evaluation = @_evaluations[recipe.id] + if not evaluation? + evaluation = @_evaluations[recipe.id] = new Evaluation evaluator:this, recipe:recipe + @_computeRecipeScore recipe, evaluation + + for id, toolItem of recipe.tools + evaluation.addIncludedTool toolItem + + for id, toolItem of @evaluateItem(toolItem).includedTools + evaluation.addIncludedTool toolItem + + + return evaluation + + getOrderedRecipes: (item, quantity=1)-> + recipes = (recipe for recipeId, recipe of item.recipes) + + recipes.sort (a, b)=> + scoreA = @evaluateRecipe(a).computeTotalScore quantity + scoreB = @evaluateRecipe(b).computeTotalScore quantity + + if scoreA isnt scoreB + return if scoreA < scoreB then -1 else +1 + + return 0 + + return recipes + + # Object Overrides ############################################################################# + + toString: -> + return "#{@constructor.name}<#{@id}>" + + # Overrideable Methods ######################################################################### + + _computeRecipeScore: (recipe, evaluation)-> + throw new Error "#{@constructor.name} must override _computeRecipeScore" + + _computeGatherableItemScore: (item, evaluation)-> + throw new Error "#{@constructor.name} must override _computeGatherableItemScore" + + # Private Methods ############################################################################## + + _findBestRecipeEvaluationFor: (item)-> + return null if item.isGatherable + result = null + + for recipeMap in [item.recipes, item.recipesAsExtra] + for id, recipe of recipeMap + evaluation = @evaluateRecipe recipe + continue unless evaluation?.baseScore? + + if not result? then result = evaluation + if evaluation.baseScore < result.baseScore then result = evaluation + + return result diff --git a/src/client/models/crafting/fixtures.coffee b/src/client/models/crafting/fixtures.coffee new file mode 100644 index 000000000..7b2751c35 --- /dev/null +++ b/src/client/models/crafting/fixtures.coffee @@ -0,0 +1,313 @@ +# +# Crafting Guide - fixtures.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +Item = require "./item" +Mod = require "./mod" +ModPack = require "./mod_pack" +Recipe = require "./recipe" +Stack = require "./stack" + +# Instance Creation Fixtures ########################################################################################### + +exports.createModPack = createModPack = (attributes={})-> + attributes.id ?= _.uniqueId "mod-pack-" + attributes.displayName ?= "Test ModPack" + return new ModPack attributes + +exports.createMod = createMod = (attributes={})-> + attributes.modPack ?= createModPack() + attributes.id ?= _.uniqueId "mod-" + attributes.displayName ?= "Test Mod" + return new Mod attributes + +exports.createItem = createItem = (attributes={})-> + attributes.mod ?= createMod() + attributes.id ?= _.uniqueId "item-" + attributes.displayName ?= _.uniqueId "Test Item " + return new Item attributes + +exports.createRecipe = createRecipe = (attributes={})-> + attributes.id ?= _.uniqueId "recipe-" + attributes.output ?= createItem() + return new Recipe attributes + +exports.createStack = createStack = (attributes={})-> + attributes.item ?= createItem() + attributes.quantity ?= 1 + return new Stack attributes + +# Item Configuration Fixtures ########################################################################################## + +exports.configureBucket = configureBucket = (mod)-> + bucket = mod.items["bucket"] + if not bucket? + ironIngot = configureIronIngot mod + bucket = createItem mod:mod, id:"bucket", displayName:"Bucket" + craftingTable = configureCraftingTable mod + + recipe = createRecipe output:createStack item:bucket + recipe.setInputAt 0, 0, createStack item:ironIngot + recipe.setInputAt 1, 1, createStack item:ironIngot + recipe.setInputAt 0, 2, createStack item:ironIngot + recipe.addTool craftingTable + + return bucket + +exports.configureCake = configureCake = (mod)-> + cake = mod.items["cake"] + if not cake? + bucket = configureBucket mod + cake = createItem mod:mod, id:"cake", displayName:"Cake" + craftingTable = configureCraftingTable mod + egg = configureEgg mod + milkBucket = configureMilkBucket mod + sugar = configureSugar mod + wheat = configureWheat mod + + recipe = createRecipe output:createStack item:cake + recipe.setInputAt 0, 0, createStack item:milkBucket + recipe.setInputAt 0, 1, createStack item:milkBucket + recipe.setInputAt 0, 2, createStack item:milkBucket + recipe.setInputAt 1, 0, createStack item:sugar + recipe.setInputAt 1, 1, createStack item:egg + recipe.setInputAt 1, 2, createStack item:sugar + recipe.setInputAt 2, 0, createStack item:wheat + recipe.setInputAt 2, 1, createStack item:wheat + recipe.setInputAt 2, 2, createStack item:wheat + recipe.addTool craftingTable + recipe.addExtra createStack item:bucket, quantity:3 + + return cake + +exports.configureCoal = configureCoal = (mod)-> + coal = mod.items["coal"] + if not coal? + coal = createItem mod:mod, id:"coal", displayName:"Coal", isGatherable:true + return coal + +exports.configureCobblestone = configureCobblestone = (mod)-> + cobblestone = mod.items["cobblestone"] + if not cobblestone? + cobblestone = createItem mod:mod, displayName:"Cobblestone", isGatherable:true + return cobblestone + +exports.configureCraftingTable = configureCraftingTable = (mod)-> + craftingTable = mod.items["crafting_table"] + if not craftingTable? + craftingTable = createItem mod:mod, id:"crafting_table", displayName:"Crafting Table" + oakPlanks = configureOakPlank mod + + recipe = createRecipe output:createStack item:craftingTable + recipe.setInputAt 0, 0, createStack item:oakPlanks + recipe.setInputAt 0, 1, createStack item:oakPlanks + recipe.setInputAt 1, 0, createStack item:oakPlanks + recipe.setInputAt 1, 1, createStack item:oakPlanks + + return craftingTable + +exports.configureEgg = configureEgg = (mod)-> + egg = mod.items["egg"] + if not egg? + egg = createItem mod:mod, id:"egg", displayName:"Egg", isGatherable:true + return egg + +exports.configureFurnace = configureFurnace = (mod)-> + furnace = mod.items["furnace"] + if not furnace? + cobblestone = configureCobblestone mod + craftingTable = configureCraftingTable mod + furnace = createItem mod:mod, displayName:"Furnace" + + recipe = createRecipe output:createStack item:furnace + recipe.setInputAt 0, 0, createStack item:cobblestone + recipe.setInputAt 0, 1, createStack item:cobblestone + recipe.setInputAt 0, 2, createStack item:cobblestone + recipe.setInputAt 1, 0, createStack item:cobblestone + recipe.setInputAt 1, 2, createStack item:cobblestone + recipe.setInputAt 2, 0, createStack item:cobblestone + recipe.setInputAt 2, 1, createStack item:cobblestone + recipe.setInputAt 2, 2, createStack item:cobblestone + recipe.addTool craftingTable + + return furnace + +exports.configureIronIngot = configureIronIngot = (mod)-> + ironIngot = mod.items["iron_ingot"] + if not ironIngot? + coal = configureCoal mod + furnace = configureFurnace mod + ironIngot = createItem mod:mod, id:"iron_ingot", displayName:"Iron Ingot" + ironOre = configureIronOre mod + + recipe = createRecipe output:createStack item:ironIngot, quantity:8 + recipe.setInputAt 0, 1, createStack item:ironOre, quantity:8 + recipe.setInputAt 2, 1, createStack item:coal + recipe.addTool furnace + + return ironIngot + +exports.configureIronBlock = configureIronBlock = (mod)-> + ironBlock = mod.items["iron_block"] + if not ironBlock? + craftingTable = configureCraftingTable mod + ironBlock = createItem mod:mod, id:"iron_block", displayName:"Iron Block" + ironIngot = configureIronIngot mod + + recipe = createRecipe output:createStack item:ironBlock + for row in [0..2] + for col in [0..2] + recipe.setInputAt row, col, createStack item:ironIngot + recipe.addTool craftingTable + + recipe = createRecipe output:createStack item:ironIngot, quantity:9 + recipe.setInputAt 1, 1, createStack item:ironBlock + + return ironBlock + +exports.configureIronSword = configureIronSword = (mod)-> + ironSword = mod.items["iron_sword"] + if not ironSword? + craftingTable = configureCraftingTable mod + ironIngot = configureIronIngot mod + ironSword = createItem mod:mod, id:"iron_sword", displayName:"Iron Sword" + stick = configureStick mod + + recipe = createRecipe output:createStack item:ironSword + recipe.setInputAt 0, 1, createStack item:ironIngot + recipe.setInputAt 1, 1, createStack item:ironIngot + recipe.setInputAt 2, 1, createStack item:stick + recipe.addTool craftingTable + + return ironSword + +exports.configureIronShovel = configureIronShovel = (mod)-> + ironShovel = mod.items["iron_shovel"] + if not ironShovel? + craftingTable = configureCraftingTable mod + ironIngot = configureIronIngot mod + ironShovel = createItem mod:mod, id:"iron_shovel", displayName:"Iron Shovel" + stick = configureStick mod + + recipe = createRecipe output:createStack item:ironShovel + recipe.setInputAt 0, 1, createStack item:ironIngot + recipe.setInputAt 1, 1, createStack item:stick + recipe.setInputAt 2, 1, createStack item:stick + recipe.addTool craftingTable + + return ironShovel + +exports.configureIronOre = configureIronOre = (mod)-> + ironOre = mod.items["iron_ore"] + if not ironOre? + ironOre = createItem mod:mod, id:"iron_ore", displayName:"Iron Ore", isGatherable:true + return ironOre + +exports.configureMilk = configureMilk = (mod)-> + milk = mod.items["milk"] + if not milk? + milk = createItem mod:mod, id:"milk", displayName:"Milk", isGatherable:true + return milk + +exports.configureMilkBucket = configureMilkBucket = (mod)-> + milkBucket = mod.items["milk_bucket"] + if not milkBucket? + bucket = configureBucket mod + milk = configureMilk mod + milkBucket = createItem mod:mod, id:"milkBucket", displayName:"Milk Bucket" + + recipe = createRecipe output:createStack item:milkBucket + recipe.setInputAt 0, 1, createStack item:milk + recipe.setInputAt 1, 1, createStack item:bucket + + return milkBucket + +exports.configureOakPlank = configureOakPlank = (mod)-> + oakPlanks = mod.items["oak_planks"] + if not oakPlanks? + oakPlanks = createItem mod:mod, id:"oak_planks", displayName:"Oak Planks" + oakWood = configureOakWood mod + + recipe = createRecipe output:createStack item:oakPlanks, quantity:4 + recipe.setInputAt 1, 1, createStack item:oakWood + + return oakPlanks + +exports.configureOakWood = configureOakWood = (mod)-> + oakWood = mod.items["oak_wood"] + if not oakWood? + oakWood = createItem mod:mod, id:"oak_wood", displayName:"Oak Wood", isGatherable:true + return oakWood + +exports.configureRedstoneDust = configureRedstoneDust = (mod)-> + redstoneDust = mod.items["redstone_dust"] + if not redstoneDust? + redstoneDust = createItem mod:mod, id:"redstone_dust", displayName:"Redstone Dust", isGatherable:true + return redstoneDust + +exports.configureStick = configureStick = (mod)-> + stick = mod.items["stick"] + if not stick? + oakPlanks = configureOakPlank mod + stick = createItem mod:mod, id:"stick", displayName:"Stick" + + recipe = createRecipe output:createStack item:stick, quantity:4 + recipe.setInputAt 0, 0, createStack item:oakPlanks + recipe.setInputAt 1, 0, createStack item:oakPlanks + + return stick + +exports.configureSugar = configureSugar = (mod)-> + sugar = mod.items["sugar"] + if not sugar? + sugar = createItem mod:mod, id:"sugar", displayName:"Sugar" + sugarCane = configureSugarCane mod + + recipe = createRecipe output:createStack item:sugar + recipe.setInputAt 1, 1, createStack item:sugarCane + + return sugar + +exports.configureSaw = configureSaw = (mod)-> + saw = mod.items["saw"] + if not saw? + craftingTable = configureCraftingTable mod + ironBlock = configureIronBlock mod + ironIngot = configureIronIngot mod + oakPlank = configureOakPlank mod + oakWood = configureOakWood mod + redstoneDust = configureRedstoneDust mod + saw = createItem mod:mod, id:"saw", displayName:"Saw" + + recipe = createRecipe output:createStack item:saw + recipe.setInputAt 0, 0, createStack item:oakPlank + recipe.setInputAt 0, 1, createStack item:ironIngot + recipe.setInputAt 0, 2, createStack item:oakPlank + recipe.setInputAt 1, 0, createStack item:oakPlank + recipe.setInputAt 1, 1, createStack item:ironBlock + recipe.setInputAt 1, 2, createStack item:oakPlank + recipe.setInputAt 2, 0, createStack item:oakPlank + recipe.setInputAt 2, 1, createStack item:redstoneDust + recipe.setInputAt 2, 2, createStack item:oakPlank + recipe.addTool craftingTable + + recipe = createRecipe output:createStack item:oakPlank, quantity:8 + recipe.setInputAt 1, 1, createStack item:oakWood + recipe.addTool saw + + return saw + +exports.configureSugarCane = configureSugarCane = (mod)-> + sugarCane = mod.items["sugar_cane"] + if not sugarCane? + sugarCane = createItem mod:mod, id:"sugar_cane", displayName:"Sugar Cane", isGatherable:true + return sugarCane + +exports.configureWheat = configureWheat = (mod)-> + wheat = mod.items["wheat"] + if not wheat? + wheat = createItem mod:mod, id:"wheat", displayName:"Wheat", isGatherable:true + return wheat diff --git a/src/client/models/crafting/fixtures.test.coffee b/src/client/models/crafting/fixtures.test.coffee deleted file mode 100644 index c151b35aa..000000000 --- a/src/client/models/crafting/fixtures.test.coffee +++ /dev/null @@ -1,189 +0,0 @@ -# -# Crafting Guide - fixtures.test.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -GraphBuilder = require './graph_builder' -Inventory = require '../game/inventory' -ItemSlug = require '../game/item_slug' -Mod = require '../game/mod' -ModPack = require '../game/mod_pack' -ModVersion = require '../game/mod_version' -PlanBuilder = require './plan_builder' - -######################################################################################################################## - -MOD_VERSION_FILE = - """ - schema: 1 - - item: Bed - recipe: - input: Oak Planks, Wool - pattern: ... 000 111 - - item: Charcoal - recipe: - input: 8 Oak Wood, Coal - pattern: .0. ... .1. - quantity: 8 - - item: Crafting Table - recipe: - input: Oak Planks - pattern: .00 .00 ... - - item: Coal - gatherable: yes - - item: Cobblestone - gatherable: yes - - item: Copper Block - recipe: - input: Copper Ingot - pattern: 000 000 000 - tools: Crafting Table - - item: Copper Ingot - recipe: - input: 8 Copper Ore, Coal - pattern: .0. ... .1. - quantity: 8 - tools: Furnace - recipe: - input: 8 Copper Ore, Charcoal - pattern: .0. ... .1. - quantity: 8 - tools: Furnace - recipe: - input: Copper Block - pattern: ... .0. ... - quantity: 9 - - item: Copper Ore - gatherable: yes - - item: Furnace - recipe: - input: Cobblestone - pattern: 000 0.0 000 - tools: Crafting Table - - item: Iron Ore - gatherable: yes - - item: Iron Ingot - 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 - recipe: - input: Iron Ingot, Stick - pattern: .0. .0. .1. - tools: Crafting Table - - item: Lever - recipe: - input: Stick, Cobblestone - pattern: .0. .1. ... - - item: Maul - recipe: - input: Iron Ingot, Stick - pattern: 001 001 ..1 - tools: Crafting Table - - item: Oak Planks - recipe: - input: Oak Wood - pattern: ... .0. ... - quantity: 4 - - item: Oak Wood - gatherable: yes - - item: Spruce Wood - gatherable: yes - - item: Stick - recipe: - input: Oak Planks - pattern: .0. .0. ... - quantity: 4 - - item: Split Oak Wood - recipe: - extras: Maul - input: Oak Wood, Maul - pattern: .1. .0. ... - quantity: 2 - - item: Split Spruce Wood - recipe: - extras: Maul - input: Spruce Wood, Maul - pattern: .1. .0. ... - quantity: 2 - - item: String - gatherable: yes - - item: Wool - gatherable: yes - recipe: - input: String - pattern: 00. 00. ... - """ - -######################################################################################################################## - -module.exports = fixtures = - - makeGraphBuilder: -> - return new GraphBuilder modPack:fixtures.makeModPack(), want:new Inventory - - makeModPack: -> - modPack = new ModPack - - mod = new Mod name:'Test', slug:'test' - modPack.addMod mod - - modVersion = new ModVersion modSlug:'test', version:'0.0' - modVersion.parse MOD_VERSION_FILE - mod.addModVersion modVersion - - return modPack - - makePlans: (stacks...)-> - modPack = fixtures.makeModPack() - - have = new Inventory - want = new Inventory - for stack in stacks - want.add ItemSlug.slugify(stack[1]), stack[0] - - graphBuilder = new GraphBuilder modPack:modPack, want:want - graphBuilder.expandGraph() - - planBuilder = new PlanBuilder graphBuilder.rootNode, modPack, want:want, have:have - return planBuilder.producePlans() - - makeTree: (itemSlug, quantity=1)-> - want = new Inventory - want.add ItemSlug.slugify itemSlug - - builder = new GraphBuilder modPack:fixtures.makeModPack(), want:want - builder.expandGraph() - - return builder.rootNode diff --git a/src/client/models/crafting/graph_builder.coffee b/src/client/models/crafting/graph_builder.coffee deleted file mode 100644 index fc8b0ce69..000000000 --- a/src/client/models/crafting/graph_builder.coffee +++ /dev/null @@ -1,72 +0,0 @@ -# -# Crafting Guide - graph_builder.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -Inventory = require '../game/inventory' -InventoryNode = require './inventory_node' - -######################################################################################################################## - -module.exports = class GraphBuilder - - constructor: (options={})-> - if not options.modPack? then throw new Error 'options.modPack is required' - if not options.want? then throw new Error 'options.want is required' - - @_maximumGraphSize = c.limits.maximumGraphSize - @_modPack = options.modPack - @_stepCount = 0 - @_want = options.want - - @_rootNode = new InventoryNode modPack:@_modPack, inventory:@_want - @_queue = [@_rootNode] - - # Public Methods ############################################################################### - - expandGraph: (steps=null)-> - maxSteps = if steps? then @_stepCount + steps else Number.MAX_VALUE - @_queue ?= [] - - while true - break if @_queue.length is 0 - break if @_stepCount >= maxSteps - - node = @_queue.shift() - break unless node? - - node.expand @_queue - @_stepCount += 1 - - pruneInvalidNodes: -> - @_rootNode.pruneInvalidChildren() - - reset: -> - - # Property Methods ############################################################################# - - Object.defineProperties @prototype, - - complete: - get: -> - return true if @_stepCount > @_maximumGraphSize - return false unless @_rootNode? - return false unless @_queue? - return false unless @_queue.length is 0 - return true - - rootNode: - get: -> @_rootNode - - stepCount: - get: -> @_stepCount - - want: - get: -> @_want - - # Object Overrides ############################################################################ - - toString: (indent='')-> - "Build Tree\n#{@_rootNode.toString(indent + ' ')}" diff --git a/src/client/models/crafting/graph_builder.test.coffee b/src/client/models/crafting/graph_builder.test.coffee deleted file mode 100644 index 241c73ca8..000000000 --- a/src/client/models/crafting/graph_builder.test.coffee +++ /dev/null @@ -1,77 +0,0 @@ -# -# Crafting Guide - graph_builder.test.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -fixtures = require './fixtures.test' -ItemSlug = require '../game/item_slug' - -######################################################################################################################## - -builder = null - -######################################################################################################################## - -describe 'GraphBuilder.coffee', -> - - beforeEach -> - builder = fixtures.makeGraphBuilder() - - describe 'expand', -> - - it 'can work a few steps at a time', -> - builder.want.add ItemSlug.slugify 'test__iron_sword' - builder.expandGraph 9 - - builder.rootNode.depth.should.equal 6 - builder.rootNode.size.should.equal 13 - builder.complete.should.be.false - - builder.expandGraph 9 - - builder.rootNode.depth.should.equal 8 - builder.rootNode.size.should.equal 18 - builder.complete.should.be.true - - it 'works properly with an empty inventory', -> - builder.expandGraph 100 - - builder.rootNode.depth.should.equal 1 - builder.rootNode.size.should.equal 1 - builder.complete.should.be.true - - describe 'can build a tree for', -> - - runSingleItemTreeBuildingTest = (slug, depth, size)-> - builder.want.add ItemSlug.slugify slug - builder.expandGraph 100 - - builder.rootNode.depth.should.equal depth - builder.rootNode.size.should.equal size - builder.complete.should.be.true - - it 'a gatherable item', -> - runSingleItemTreeBuildingTest 'test__oak_wood', 2, 2 - - it 'a single-step item', -> - runSingleItemTreeBuildingTest 'test__crafting_table', 6, 6 - - it 'an item with multiple inputs', -> - runSingleItemTreeBuildingTest 'test__lever', 8, 9 - - it 'an item with multiple recipes', -> - runSingleItemTreeBuildingTest 'test__iron_ingot', 6, 11 - - it 'an item with multiple inputs and multiple recipes', -> - runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18 - - it 'an item with one recursive recipe', -> - runSingleItemTreeBuildingTest 'test__copper_ingot', 6, 15 - - it 'an item which gatherable and craftable', -> - runSingleItemTreeBuildingTest 'test__wool', 4, 4 - - it 'an item which requires a gatherable and craftable item', -> - runSingleItemTreeBuildingTest 'test__bed', 6, 7 diff --git a/src/client/models/crafting/inventory.coffee b/src/client/models/crafting/inventory.coffee new file mode 100644 index 000000000..4eadaa7ba --- /dev/null +++ b/src/client/models/crafting/inventory.coffee @@ -0,0 +1,90 @@ +# +# Crafting Guide - inventory.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +Stack = require './stack' + +######################################################################################################################## + +module.exports = class Inventory + + constructor: (inventory=null)-> + @_id = _.uniqueId "inventory-" + @_stacks = {} + + if inventory? then @merge inventory + + # Properties ################################################################################### + + Object.defineProperties @prototype, + + isEmpty: + get: -> (id for id, stack of @_stacks).length is 0 + + stacks: + get: -> return @_stacks + set: -> throw new Error "stacks cannot be replaced" + + # Public Methods ############################################################################### + + add: (item, quantity)-> + return unless item? + return if quantity is 0 + + existingStack = @_stacks[item.id] + if existingStack? + if existingStack.quantity + quantity < 0 then throw new Error "cannot have a negative quantity" + existingStack.quantity += quantity + else + if quantity < 0 then throw new Error "cannot have a negative quantity" + @_stacks[item.id] = new Stack item:item, quantity:quantity + + if @_stacks[item.id].quantity is 0 + delete @_stacks[item.id] + + clear: -> + @_stacks = {} + + contains: (item)-> + return @_stacks[item.id]? + + getQuantity: (item)-> + existingStack = @_stacks[item.id] + return 0 unless existingStack? + return existingStack.quantity + + merge: (inventory)-> + for id, stack of inventory.stacks + @add stack.item, stack.quantity + + remove: (item, quantity)-> + @add item, -1 * quantity + + # Object Overrides ############################################################################# + + toString: (options={})-> + options.full ?= false + + if options.full + result = [] + needsDelimiter = false + + stackList = (stack for itemId, stack of @_stacks) + stackList.sort (a, b)-> + if a.item.displayName isnt b.item.displayName + return if a.item.displayName < b.item.displayName then -1 else +1 + return 0 + + for stack in stackList + if needsDelimiter then result.push ", " + needsDelimiter = true + + result.push stack.quantity + result.push " " + result.push stack.item.displayName + return result.join "" + else + return "Inventory<#{@_id}>@#{(id for id, item of @_stacks).length}" diff --git a/src/client/models/crafting/inventory_node.coffee b/src/client/models/crafting/inventory_node.coffee deleted file mode 100644 index 8f5555e32..000000000 --- a/src/client/models/crafting/inventory_node.coffee +++ /dev/null @@ -1,55 +0,0 @@ -# -# Crafting Guide - inventory_node.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -CraftingNode = require './crafting_node' -ItemNode = require './item_node' - -######################################################################################################################## - -module.exports = class InventoryNode extends CraftingNode - - @::ENTER_METHOD = 'onEnterInventoryNode' - @::LEAVE_METHOD = 'onLeaveInventoryNode' - @::TYPE = CraftingNode::TYPES.INVENTORY - - constructor: (options={})-> - if not options.inventory? then throw new Error 'options.inventory is required' - super options - - @inventory = options.inventory - - # CraftingNode Overrides ####################################################################### - - _createChildren: (result=[])-> - @inventory.each (stack)=> - item = @modPack.findItem stack.itemSlug - if not item? then throw new Error "Could not find an item for slug: #{stack.itemSlug}" - result.push new ItemNode modPack:@modPack, item:item, ignoreGatherable:true - return result - - _checkCompleteness: -> - for child in @children - return false unless child.complete - return true - - _checkValidity: -> - for child in @children - return false unless child.valid - return true - - # Object Overrides ############################################################################# - - toString: (options={})-> - options.indent ?= '' - options.recursive ?= true - - parts = ["#{options.indent}#{@completeText} #{@validText} InventoryNode for #{@inventory}"] - nextIndent = options.indent + ' ' - if options.recursive - for child in @children - parts.push child.toString indent:nextIndent - return parts.join '\n' diff --git a/src/client/models/crafting/item.coffee b/src/client/models/crafting/item.coffee new file mode 100644 index 000000000..beb259cfe --- /dev/null +++ b/src/client/models/crafting/item.coffee @@ -0,0 +1,93 @@ +# +# Crafting Guide - item.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +######################################################################################################################## + +module.exports = class Item + + constructor: (attributes={})-> + @id = attributes.id + @displayName = attributes.displayName + @isGatherable = attributes.isGatherable + @mod = attributes.mod + + @_hasPrimaryRecipe = false + @_recipesAsPrimary = {} + @_recipesAsExtra = {} + + # Property Methods ############################################################################# + + Object.defineProperties @prototype, + + displayName: + get: -> return @_displayName + set: (displayName)-> + if not displayName? then throw new Error "displayName is required" + @_displayName = displayName + + firstRecipe: + get: -> return recipe for id, recipe of @recipes + set: -> throw new Error "firstRecipe cannot be assigned" + + id: + get: -> return @_id + set: (id)-> + if not id? then throw new Error "id is required" + return if @_id is id + if @_id? then throw new Error "id cannot be reassigned" + @_id = id + + isGatherable: + get: -> + return true if @_isGatherable is true + return false if (id for id, recipe of @_recipesAsPrimary).length > 0 + return false if (id for id, recipe of @_recipesAsExtra).length > 0 + return true + set: (isGatherable)-> + @_isGatherable = null unless isGatherable? + @_isGatherable = !!isGatherable + + mod: + get: -> return @_mod + set: (mod)-> + if not mod? then throw new Error "mod is required" + if @_mod is mod then return + if @_mod? then throw new Error "mod cannot be reassigned" + @_mod = mod + @_mod.addItem this + + modPack: + get: -> return @_mod.modPack + set: -> throw new Error "modPack cannot be replaced" + + recipes: + get: -> return if @_hasPrimaryRecipe then @_recipesAsPrimary else @_recipesAsExtra + set: -> throw new Error "recipes cannot be assigned" + + recipesAsPrimary: + get: -> return @_recipesAsPrimary + set: -> throw new Error "recipes cannot be assigned" + + recipesAsExtra: + get: -> return @_recipesAsExtra + set: -> throw new Error "recipesAsExtra cannot be assigned" + + # Public Recipes ############################################################################### + + addRecipe: (recipe)-> + if recipe.output.item is this + @_recipesAsPrimary[recipe.id] = recipe + @_hasPrimaryRecipe = true + else if recipe.extras[this.id] is this + @_recipesAsExtra[recipe.id] = recipe + else + throw new Error "recipe<#{recipe.id}> does not produce this item<#{@id}>" + + # Object Overrides ############################################################################# + + toString: -> + return "Item:#{@displayName}<#{@id}>" diff --git a/src/client/models/crafting/item_node.coffee b/src/client/models/crafting/item_node.coffee deleted file mode 100644 index b58f44f53..000000000 --- a/src/client/models/crafting/item_node.coffee +++ /dev/null @@ -1,108 +0,0 @@ -# -# Crafting Guide - item_node.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -CraftingNode = require './crafting_node' -RecipeNode = require './recipe_node' - -######################################################################################################################## - -module.exports = class ItemNode extends CraftingNode - - @::ENTER_METHOD = 'onEnterItemNode' - @::LEAVE_METHOD = 'onLeaveItemNode' - @::TYPE = CraftingNode::TYPES.ITEM - - constructor: (options={})-> - if not options.item? then throw new Error 'options.item is required' - super options - - @item = options.item - @_ignoreGatherable = options.ignoreGatherable ?= false - @_recipes = null - - # Property Methods ############################################################################# - - getRecipes: -> - if not @_recipes? - @_recipes = @modPack.findRecipes @item.slug, forCrafting:true, onlyPrimary:true - if not @_recipes - @_recipes = @modPack.findRecipes @item.slug, forCrafting:true - - return @_recipes or [] - - isGatherable: -> - if not @_ignoreGatherable - return true if @item.isGatherable - return true if @getRecipes().length is 0 - return false - - Object.defineProperties @prototype, - gatherable: { get:@prototype.isGatherable } - recipes: { get:@prototype.getRecipes } - - # CraftingNode Overrides ####################################################################### - - _createChildren: (result=[])-> - recipes = @getRecipes() - return [] if @gatherable - - for recipe in recipes - child = new RecipeNode modPack:@modPack, recipe:recipe - child.parent = this - if child.valid - result.push child - - return result - - _checkCompleteness: -> - return true if @gatherable - return false unless @children? - - for child in @children - return true if child.complete - return false - - _checkValidity: -> - return false if @_isRepeatedItem() - return true unless @children.length > 0 - - for child in @children - return true if child.valid - return false - - _pruneInvalidChildren: -> - index = 0 - while index < @children.length - child = @children[index] - if child.valid - child.pruneInvalidChildren() - index++ - else - @removeChild index - - # Object Overrides ############################################################################# - - toString: (options={})-> - options.indent ?= '' - options.recursive ?= true - - parts = ["#{options.indent}#{@completeText} #{@validText} ItemNode for #{@item.name}"] - nextIndent = options.indent + ' ' - if options.recursive - for child in @children - parts.push child.toString indent:nextIndent - return parts.join '\n' - - # Private Methods ############################################################################## - - _isRepeatedItem: -> - nextParent = @parent - while nextParent? - return true if nextParent.item is @item - nextParent = nextParent.parent - - return false diff --git a/src/client/models/crafting/mod.coffee b/src/client/models/crafting/mod.coffee new file mode 100644 index 000000000..b4e013b86 --- /dev/null +++ b/src/client/models/crafting/mod.coffee @@ -0,0 +1,62 @@ +# +# Crafting Guide - mod.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +######################################################################################################################## + +module.exports = class Mod + + constructor: (attributes={})-> + @id = attributes.id + @displayName = attributes.displayName + @modPack = attributes.modPack + + @_items = {} + + # Properties ################################################################################### + + Object.defineProperties @prototype, + + displayName: + get: -> return @_displayName + set: (displayName)-> + if not displayName? then throw new Error "displayName is required" + return if @_displayName is displayName + @_displayName = displayName + + id: + get: -> return @_id + set: (id)-> + if not id? then throw new Error "id is required" + return if @_id is id + if @_id? then throw new Error "id cannot be reassigned" + @_id = id + + items: + get: -> return @_items + set: -> throw new Error "items cannot be replaced" + + modPack: + get: -> return @_modPack + set: (modPack)-> + if not modPack? then throw new Error "modPack is required" + if @_modPack is modPack then return + if @_modPack? then throw new Error "modPack cannot be reassigned" + @_modPack = modPack + @_modPack.addMod this + + # Public Methods ############################################################################### + + addItem: (item)-> + if not item? then return + if @_items[item.id] is item then return + @_items[item.id] = item + item.mod = this + + # Object Overrides ############################################################################# + + toString: -> + return "Mod:#{@displayName}<#{@id}>" diff --git a/src/client/models/crafting/mod_pack.coffee b/src/client/models/crafting/mod_pack.coffee new file mode 100644 index 000000000..2272fef3c --- /dev/null +++ b/src/client/models/crafting/mod_pack.coffee @@ -0,0 +1,52 @@ +# +# Crafting Guide - mod_pack.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +######################################################################################################################## + +module.exports = class ModPack + + constructor: (attributes={})-> + @id = attributes.id + @displayName = attributes.displayName + + @_mods = {} + @_oreDict = {} + + # Property Methods ############################################################################# + + Object.defineProperties @prototype, + + displayName: + get: -> return @_displayName + set: (displayName)-> + if not displayName? then throw new Error "displayName is required" + if @_displayName is displayName then return + @_displayName = displayName + + id: + get: -> return @_id + set: (id)-> + if not id? then throw new Error "id is required" + if @_id is id then return + if @_id? then throw new Error "id cannot be reassigned" + + mods: + get: -> return @_mods + set: -> throw new Error "mods cannot be replaced" + + # Public Methods ############################################################################### + + addMod: (mod)-> + if not mod? then return + if @_mods[mod.id] is mod then return + @_mods[mod.id] = mod + mod.modPack = this + + # Object Overrides ############################################################################# + + toString: -> + return "ModPack:#{@displayName}<#{@id}>" diff --git a/src/client/models/crafting/plan_builder.coffee b/src/client/models/crafting/plan_builder.coffee index a8687acc9..47ad57257 100644 --- a/src/client/models/crafting/plan_builder.coffee +++ b/src/client/models/crafting/plan_builder.coffee @@ -5,123 +5,88 @@ # All rights reserved. # -CraftingNode = require './crafting_node' -CraftingPlan = require './crafting_plan' -CraftingStep = require './crafting_step' -Inventory = require '../game/inventory' +CraftingPlan = require "./crafting_plan" +CraftingPlanStep = require "./crafting_plan_step" ######################################################################################################################## module.exports = class PlanBuilder - constructor: (rootNode, modPack, options={})-> - if not rootNode? then throw new Error 'rootNode is required' - if not modPack? then throw new Error 'modPack is required' + constructor: (evaluator)-> + @evaluator = evaluator - @want = options.want - @have = options.have - - @_choiceNodes = [] - @_complete = false - @_maxPlanCount = c.limits.maximumPlanCount - @_modPack = modPack - @_plans = [] - @_rootNode = rootNode - - @_isolateChoiceNodes() - - # Public Methods ############################################################################### - - producePlans: (maxPlans=null)-> - maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE - - if @plans.length >= @_maxPlanCount - @_complete = true - - while not @complete and (@plans.length < maxPlans) - plan = @_captureCurrentPlan() - if plan? - @plans.push plan - - @_incrementChoiceNodes() - - return @_plans - - # Property Methods ############################################################################# + # Properties ################################################################################### Object.defineProperties @prototype, - complete: - get: -> @_complete + evaluator: + get: -> return @_evaluator + set: (evaluator)-> + if not evaluator? then throw new Error "evaluator is required" + if @_evaluator is evaluator then return + if @_evaluator? then throw new Error "evaluator cannot be reassigned" + @_evaluator = evaluator - have: - get: -> @_have - set: (have)-> @_have = have or new Inventory + # Public Methods ############################################################################### - maxPlanCount: - get: -> @_maxPlanCount - set: (value)-> @_maxPlanCount = value + createPlan: (want, have)-> + @_alreadyMaking = {} + @_recipesInUse = {} - plans: - get: -> @_plans + stepList = [] + for itemId, stack of want.stacks + steps = @_findStepsForItem stack.item, stack.quantity + return null unless steps? - want: - get: -> @_want - set: (want)-> @_want = want or new Inventory + stepList.push steps + + plan = new CraftingPlan want:want, have:have, steps:_.flatten(stepList) + return plan # Private Methods ############################################################################## - _captureCurrentPlan: -> - toVisit = [@_rootNode] - stepNodes = [] + _findStepsForItem: (item, quantity=1)-> + steps = null - while toVisit.length > 0 - node = toVisit.shift() + if not @_alreadyMaking[item.id] + @_alreadyMaking[item.id] = true - if node.TYPE is CraftingNode::TYPES.INVENTORY - toVisit.push(c) for c in node.children - 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 - stepNodes.push node - toVisit.push(c) for c in node.children + recipes = @_evaluator.getOrderedRecipes item, quantity + if recipes.length is 0 + steps = [] + else + for recipe in recipes + continue if @_recipesInUse[recipe.id]? - steps = [] - seenRecipes = {} - index = stepNodes.length - 1 - while index >= 0 - node = stepNodes[index] - index -= 1 - return null unless node.valid and node.complete + steps = @_findStepsForRecipe recipe, quantity + break if steps? - recipeSlug = node.recipe.slug - continue if seenRecipes[recipeSlug]? + delete @_alreadyMaking[item.id] - seenRecipes[recipeSlug] = true - steps.push new CraftingStep node.recipe, @_modPack + return steps - plan = new CraftingPlan @_modPack, @_want, @_have, steps - return plan + _findStepsForRecipe: (recipe, quantity=1)-> + steps = null - _incrementChoiceNodes: -> - if @_choiceNodes.length is 0 - @_complete = true - return + if not @_recipesInUse[recipe.id] + @_recipesInUse[recipe.id] = true - index = @_choiceNodes.length - 1 - while true - if index is -1 - @_complete = true - return + invalidRecipe = false + for itemId, item of recipe.inputs + inputSteps = @_findStepsForItem item, quantity * recipe.computeQuantityRequired(item) + if not inputSteps? + invalidRecipe = true + break - node = @_choiceNodes[index] - node.rotateChildren() + steps ?= [] + for step in inputSteps + steps.push step - return unless node.rotations % node.children.length is 0 - index -= 1 + if invalidRecipe + steps = null + else + steps.push new CraftingPlanStep recipe - _isolateChoiceNodes: -> - @_rootNode.acceptVisitor - onEnterItemNode: (node)=> - if node.children.length > 1 - @_choiceNodes.push node + delete @_recipesInUse[recipe.id] + + return steps diff --git a/src/client/models/crafting/plan_builder.test.coffee b/src/client/models/crafting/plan_builder.test.coffee index f81a97cd5..bb2224457 100644 --- a/src/client/models/crafting/plan_builder.test.coffee +++ b/src/client/models/crafting/plan_builder.test.coffee @@ -5,39 +5,154 @@ # All rights reserved. # -fixtures = require './fixtures.test' -PlanBuilder = require './plan_builder' +fixtures = require './fixtures' +Inventory = require './inventory' +PlanBuilder = require './plan_builder' +ResourcesEvaluator = require './resources_evaluator' ######################################################################################################################## -describe 'plan_builder.coffee', -> +describe.only "PlanBuilder", -> - printPlan = (plan)-> - return '' unless plan? - return ((s.recipe.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > ' + beforeEach -> + @planner = new PlanBuilder new ResourcesEvaluator + @mod = fixtures.createMod() + @want = new Inventory + @have = new Inventory - it 'generates an empty plan for a gatherable item', -> - plans = fixtures.makePlans [1, 'test__oak_wood'] + describe "for a gatherable item, creates a plan that", -> - plans.length.should.equal 1 - plans[0].length.should.equal 0 + beforeEach -> + @oakWood = fixtures.configureOakWood @mod + @want.add @oakWood, 2 + @plan = @planner.createPlan @want - it 'can find a multi-step plan', -> - plans = fixtures.makePlans [1, 'test__lever'] + it "has no steps at all", -> + @plan.steps.length.should.equal 0 - printPlan(plans[0]).should.equal '4 test__oak_planks > 4 test__stick > test__lever' - plans.length.should.equal 1 + it "demands the item itself as the input", -> + @plan.need.toString(full:true).should.equal "2 Oak Wood" - it 'can find multiple plans', -> - plans = fixtures.makePlans [1, 'test__iron_ingot'] + it "produces the item as the result", -> + @plan.make.toString(full:true).should.equal "2 Oak Wood" - 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 + describe "for a single item with a one-step recipe, it creates a plan that", -> - it 'ignores invalid plans', -> - plans = fixtures.makePlans [1, 'test__copper_block'] + beforeEach -> + @oakPlank = fixtures.configureOakPlank @mod + @want.add @oakPlank, 63 + @plan = @planner.createPlan @want - 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 + it "has the recipe as the only step", -> + @plan.steps.length.should.equal 1 + @plan.steps[0].recipe.output.item.displayName.should.equal "Oak Planks" + + it "demands the right inputs", -> + @plan.need.toString(full:true).should.equal "16 Oak Wood" + + it "produces the right results", -> + @plan.make.toString(full:true).should.equal "64 Oak Planks" + + describe "for a single item with a complex recipe, it creates a plan that", -> + + beforeEach -> + @ironSword = fixtures.configureIronSword @mod + @want.add @ironSword, 2 + @plan = @planner.createPlan @want + + it "has the right steps", -> + @plan.steps.length.should.equal 4 + @plan.steps[0].recipe.output.item.displayName.should.equal "Iron Ingot" + @plan.steps[1].recipe.output.item.displayName.should.equal "Oak Planks" + @plan.steps[2].recipe.output.item.displayName.should.equal "Stick" + @plan.steps[3].recipe.output.item.displayName.should.equal "Iron Sword" + + it "demands the right inputs", -> + @plan.need.toString(full:true).should.equal "1 Coal, 8 Iron Ore, 1 Oak Wood" + + it "produces the right results", -> + @plan.make.toString(full:true).should.equal "4 Iron Ingot, 2 Iron Sword, 2 Oak Planks, 2 Stick" + + describe "for multiple items with complex recipes, it creates a plan that", -> + + beforeEach -> + @ironSword = fixtures.configureIronSword @mod + @ironShovel = fixtures.configureIronShovel @mod + @want.add @ironSword, 3 + @want.add @ironShovel, 3 + @plan = @planner.createPlan @want + + it "has consolidated the common steps", -> + (step.recipe.output.item.displayName for step in @plan.steps).should.eql [ + "Iron Ingot", "Oak Planks", "Stick", "Iron Sword", "Iron Shovel" + ] + + it "requires the correct repetitions for each step", -> + (step.count for step in @plan.steps).should.eql [ 2, 2, 3, 3, 3 ] + + it "demands the right inputs", -> + @plan.need.toString(full:true).should.equal "2 Coal, 16 Iron Ore, 2 Oak Wood" + + it "produces the right results", -> + @plan.make.toString(full:true).should.equal( + "7 Iron Ingot, 3 Iron Shovel, 3 Iron Sword, 2 Oak Planks, 3 Stick" + ) + + describe "for an item which has a recursive recipe, it creates a plan that", -> + + beforeEach -> + @ironBlock = fixtures.configureIronBlock @mod + @want.add @ironBlock, 4 + @plan = @planner.createPlan @want + + it "avoids the recursive options", -> + (step.recipe.output.item.displayName for step in @plan.steps).should.eql [ + "Iron Ingot", "Iron Block" + ] + + it "requires the correct repetitions for each step", -> + (step.count for step in @plan.steps).should.eql [ 5, 4 ] + + it "demands the right inputs", -> + @plan.need.toString(full:true).should.equal "5 Coal, 40 Iron Ore" + + it "produces the right results", -> + @plan.make.toString(full:true).should.equal( + "4 Iron Block, 4 Iron Ingot" + ) + + describe "when making enormous quantities of something simple", -> + + beforeEach -> + @oakPlank = fixtures.configureOakPlank @mod + @want.add @oakPlank, Number.MAX_VALUE / 2 + @start = Date.now() + @plan = @planner.createPlan @want + @duration = Date.now() - @start + + it "doesn't take forever to create the plan", -> + @duration.should.be.lessThan 10 + + describe "when making an item which can be made with and without a tool, for a", -> + + beforeEach -> + @oakPlank = fixtures.configureOakPlank @mod + @saw = fixtures.configureSaw @mod + + describe "small batch, the plan", -> + + beforeEach -> + @want.add @oakPlank, 2 + @plan = @planner.createPlan @want + + it "avoids the expensive tool", -> + (step.recipe.needsTools for step in @plan.steps).should.eql [ false ] + + describe "large batch, the plan", -> + + beforeEach -> + @want.add @oakPlank, 2048 + @plan = @planner.createPlan @want + + it "uses the tool for efficiency", -> + (step.recipe.needsTools for step in @plan.steps).should.eql [ true ] diff --git a/src/client/models/crafting/plan_evaluator.coffee b/src/client/models/crafting/plan_evaluator.coffee deleted file mode 100644 index 0676976b1..000000000 --- a/src/client/models/crafting/plan_evaluator.coffee +++ /dev/null @@ -1,98 +0,0 @@ -# -# Crafting Guide - plan_evaluator.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -######################################################################################################################## - -module.exports = class PlanEvaluator - - @::CRITERIA = - FEWEST_STEPS: 'fewest steps' - LEAST_MATERIALS: 'least materials' - - constructor: (plans)-> - if not plans? then throw new Error 'plans is required' - - @_plans = plans - @_lastScored = -1 - @_normalized = false - - # Public Methods ############################################################################### - - findBestPlan: (mainCriteria)-> - criteriaList = [mainCriteria].concat (criteria for key, criteria of @CRITERIA when criteria isnt mainCriteria) - @_plans.sort (a, b)-> - for criteria in criteriaList - scoreA = a.getScore criteria - scoreB = b.getScore criteria - - if scoreA isnt scoreB - return if scoreA > scoreB then -1 else +1 - - return 0 - - return @_plans[0] - - scorePlans: (count)-> - count ?= @_plans.length - return if @complete - - maxPlanIndex = Math.min @_lastScored + count, @_plans.length - 1 - for i in [(@_lastScored + 1)..maxPlanIndex] - @_plans[i].computeRequired() - @_scorePlan @_plans[i] - @_lastScored = i - - if @complete then @_normalizeScores() - - return @complete - - # Property Methods ############################################################################# - - Object.defineProperties @prototype, - - complete: - get: -> @_lastScored is @_plans.length - 1 - - lastScored: - get: -> @_lastScored - - # Private Methods ############################################################################## - - _computeFewestStepsScore: (plan)-> - total = 0 - for step in plan.steps - total += step.multiplier - plan.setRawScore @CRITERIA.FEWEST_STEPS, total - - _computeLeastMaterialsScore: (plan)-> - total = 0 - plan.need.each (stack)-> - total += stack.quantity - plan.setRawScore @CRITERIA.LEAST_MATERIALS, total - - _normalizeScores: -> - for key, criteria of @CRITERIA - maxScore = 0 - minScore = Number.MAX_VALUE - - for plan in @_plans - continue unless plan.hasRawScore criteria - maxScore = Math.max maxScore, plan.getRawScore(criteria) - minScore = Math.min minScore, plan.getRawScore(criteria) - - adjustedMaxScore = maxScore - minScore - for plan in @_plans - continue unless plan.hasRawScore criteria - if adjustedMaxScore > 0 - adjustedRawScore = plan.getRawScore(criteria) - minScore - plan.setScore criteria, 1.0 - (1.0 * adjustedRawScore / adjustedMaxScore) - else - plan.setScore criteria, 1.0 - - _scorePlan: (plan)-> - @_computeFewestStepsScore plan - @_computeLeastMaterialsScore plan diff --git a/src/client/models/crafting/plan_evaluator.test.coffee b/src/client/models/crafting/plan_evaluator.test.coffee deleted file mode 100644 index 70f87b91d..000000000 --- a/src/client/models/crafting/plan_evaluator.test.coffee +++ /dev/null @@ -1,80 +0,0 @@ -# -# Crafting Guide - plan_evaluator.test.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -CraftingPlan = require './crafting_plan' -fixtures = require './fixtures.test' -Inventory = require '../game/inventory' -PlanEvaluator = require './plan_evaluator' - -######################################################################################################################## - -criteria = evaluator = planA = planB = planC = plans = wanted = null - -######################################################################################################################## - -describe 'plan_evaluator.coffee', -> - - beforeEach -> - criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS - - describe 'findBestPlan', -> - - it 'can find the shortest of two options', -> - evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block'] - evaluator.scorePlans() - - bestPlan = evaluator.findBestPlan criteria - bestPlan.getScore(criteria).should.equal 1.0 - - it 'can find the shortest of many options', -> - evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword'] - evaluator.scorePlans() - - bestPlan = evaluator.findBestPlan criteria - bestPlan.getScore(criteria).should.equal 1.0 - - it 'can break ties using the non-primary criteria', -> - evaluator = new PlanEvaluator fixtures.makePlans [2, 'test__iron_ingot'], [1, 'test__charcoal'] - evaluator.scorePlans() - - bestPlan = evaluator.findBestPlan criteria - bestPlan.getScore(criteria).should.equal 1.0 - - (p.getRawScore('fewest steps') for p in evaluator._plans).should.eql [2, 2] - (p.getRawScore('least materials') for p in evaluator._plans).should.eql [17, 18] - - describe '_normalizeScores', -> - - beforeEach -> - modPack = fixtures.makeModPack() - wanted = new Inventory modPack:modPack - have = new Inventory modPack:modPack - planA = new CraftingPlan modPack, wanted, have, [] - planB = new CraftingPlan modPack, wanted, have, [] - planC = new CraftingPlan modPack, wanted, have, [] - plans = [planA, planB, planC] - evaluator = new PlanEvaluator plans - criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS - - it 'does not set scores when none have been evaluated', -> - evaluator._normalizeScores() - (plan.hasRawScore(criteria) for plan in plans).should.eql [false, false, false] - - it 'assigns a 1.0 when all plans equal', -> - for plan in plans - plan.setRawScore criteria, 5 - - evaluator._normalizeScores() - (plan.getScore(criteria) for plan in plans).should.eql [1.0, 1.0, 1.0] - - it 'computes correct scores from raw scores', -> - planA.setRawScore criteria, 7 - planB.setRawScore criteria, 8 - planC.setRawScore criteria, 9 - - evaluator._normalizeScores() - (plan.getScore(criteria) for plan in plans).should.eql [1.0, 0.5, 0.0] diff --git a/src/client/models/crafting/recipe.coffee b/src/client/models/crafting/recipe.coffee new file mode 100644 index 000000000..5c1cbe6fa --- /dev/null +++ b/src/client/models/crafting/recipe.coffee @@ -0,0 +1,148 @@ +# +# Crafting Guide - recipe.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +{StringBuilder} = require 'crafting-guide-common' + +######################################################################################################################## + +module.exports = class Recipe + + constructor: (attributes={})-> + @id = attributes.id + @height = attributes.height + @output = attributes.output + @width = attributes.width + + @_extras = {} + @_inputs = {} + @_inputGrid = [] + @_tools = {} + + # Properties ################################################################################### + + Object.defineProperties @prototype, + + allProducts: # an array of Stacks starting the the primary output of this recipe + get: -> return [].concat @output, (stack for id, stack of @extras) + set: -> throw new Error "allProducts cannot be assigned" + + extras: # a hash of item id to Stack of all the non-primary outputs of this recipe + get: -> return @_extras + set: -> throw new Error "extras cannot be replaced" + + id: # a string which uniquely identifies this recipe + get: -> return @_id + set: (id)-> + if not id? then throw new Error "id is required" + if @_id is id then return + if @_id? then throw new Error "id cannot be reassigned" + @_id = id + + height: # an integer specifying the number of rows needed by this recipe + get: -> return @_height + set: (height)-> + height = parseInt "#{height}" + height = if Number.isNaN(height) then 0 else Math.max(0, height) + @_height = height + + inputs: # a hash of item id to Item containing all the inputs to this recipe + get: -> return @_inputs + set: -> throw new Error "inputs cannot be replaced" + + needsTools: # a boolean indicating whether this recipe requires a tool + get: -> return (id for id, toolItem of @_tools).length > 0 + + output: # a Stack specifying the primary output of this recipe + get: -> return @_output + set: (output)-> + if not output? then throw new Error "output is required" + if @_output is output then return + if @_output? then throw new Error "output cannot be reassigned" + @_output = output + @_output.item.addRecipe this + + modPack: # the ModPack to which this recipe belongs + get: -> return @_output.modPack + set: -> throw new Error "modPack cannot be replaced" + + tools: # a hash of item id to Item of all the tools required for this recipe + get: -> return @_tools + set: -> throw new Error "tools cannot be assigned" + + width: # an integer specifying the number of columns needed by this recipe + get: -> return @_width + set: (width)-> + width = parseInt "#{width}" + width = if Number.isNaN(width) then 0 else Math.max(0, width) + @_width = width + + # Public Methods ############################################################################### + + addExtra: (stack)-> + return unless stack + @_extras[stack.item.id] = stack + + addTool: (item)-> + return unless item + @_tools[item.id] = item + + computeQuantityRequired: (item)-> + result = 0 + + for row in [0...@height] + for col in [0...@width] + stack = @_inputGrid[row]?[col] + continue unless stack? + continue unless stack.item.id is item.id + result += stack.quantity + + return result + + computeQuantityProduced: (item)-> + result = 0 + + if @_output.item.id is item.id + result += @_output.quantity + + for itemId, stack of @_extras + continue unless itemId is item.id + result += stack.quantity + + return result + + getInputAt: (row, col)-> + return @_inputGrid[row]?[col] + + setInputAt: (row, col, stack)-> + @_height = Math.max @_height, row + 1 + @_width = Math.max @_width, col + 1 + + @_inputGrid[row] ?= [] + @_inputGrid[row][col] = stack + @_inputs[stack.item.id] = stack.item + + # Object Overrides ############################################################################# + + toString: (options={})-> + options.full ?= false + + if options.full + b = new StringBuilder + b.loop (item for id, item of @inputs), delimiter:" + ", onEach:(b, item)=> + b.push @computeQuantityRequired(item), " ", item.displayName + b.push " =" + b.onlyIf @needsTools, (b)=> + b.push "(" + b.loop (toolItem for id, toolItem of @tools), onEach:(b, toolItem)-> b.push toolItem.displayName + b.push ")" + b.push "=> " + b.loop @allProducts, delimiter:" + ", onEach:(b, stack)=> + b.push stack.quantity, " ", stack.item.displayName + + return b.toString() + else + return "Recipe:#{@output}<#{@id}>" diff --git a/src/client/models/crafting/recipe_node.coffee b/src/client/models/crafting/recipe_node.coffee deleted file mode 100644 index b262a4141..000000000 --- a/src/client/models/crafting/recipe_node.coffee +++ /dev/null @@ -1,87 +0,0 @@ -# -# Crafting Guide - recipe_node.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -CraftingNode = require './crafting_node' -Item = require '../game/item' -# ItemNode = require './item_node' # don't include here, causes a cycle -ItemSlug = require '../game/item_slug' - -######################################################################################################################## - -module.exports = class RecipeNode extends CraftingNode - - @::ENTER_METHOD = 'onEnterRecipeNode' - @::LEAVE_METHOD = 'onLeaveRecipeNode' - @::TYPE = CraftingNode::TYPES.RECIPE - - constructor: (options={})-> - if not options.recipe? then throw new Error 'options.recipe is required' - super options - - @recipe = options.recipe - - # CraftingNode Overrides ####################################################################### - - _createChildren: (result=[])-> - ItemNode = require './item_node' # include here to avoid a cycle - - for stack in @recipe.input - item = @modPack.findItem stack.itemSlug - if not item? - name = @modPack.findName stack.itemSlug - item = new Item name:name, slug:stack.itemSlug, gatherable:true - result.push new ItemNode modPack:@modPack, item:item - return result - - _checkCompleteness: -> - for child in @children - return false unless child.complete - return true - - _checkValidity: -> - return false if @_isRepeatedRecipe() - return false if @_requiresToolBeingMade() - - for child in @children - return false unless child.valid - - return true - - # Private Methods ############################################################################## - - _isRepeatedRecipe: -> - nextParent = @parent - while nextParent? - return true if nextParent.recipe is @recipe - nextParent = nextParent.parent - - return false - - _requiresToolBeingMade: -> - for toolStack in @recipe.tools - toolSlug = toolStack.itemSlug - - nextParent = @parent - while nextParent? - if ItemSlug.equal toolSlug, nextParent.item?.slug - return true - nextParent = nextParent.parent - - return false - - # Object Overrides ############################################################################ - - toString: (options={})-> - options.indent ?= '' - options.recursive ?= true - - parts = ["#{options.indent}#{@completeText} #{@validText} RecipeNode for #{@recipe.slug}"] - nextIndent = options.indent + ' ' - if options.recursive - for child in @children - parts.push child.toString indent:nextIndent - return parts.join '\n' diff --git a/src/client/models/crafting/resources_evaluator.coffee b/src/client/models/crafting/resources_evaluator.coffee new file mode 100644 index 000000000..51c5b6655 --- /dev/null +++ b/src/client/models/crafting/resources_evaluator.coffee @@ -0,0 +1,45 @@ +# +# Crafting Guide - resources_evaluator.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +Evaluator = require './evaluator' + +######################################################################################################################## + +module.exports = class ResourcesEvaluator extends Evaluator + + # Evaluator Overrides ########################################################################## + + _computeRecipeScore: (recipe, evaluation)-> + evaluation.baseScore = 0 + + for row in [0...recipe.height] + for col in [0...recipe.width] + stack = recipe.getInputAt row, col + continue unless stack? + + inputEvaluation = @evaluateItem stack.item + if inputEvaluation?.baseScore? + evaluation.baseScore += inputEvaluation.baseScore * stack.quantity + evaluation.addBaseEvaluation inputEvaluation + else + evaluation.baseScore = null + logger.outdent() + return + + for id, extraStack of recipe.extras + extraEvaluation = @evaluateItem extraStack.item + continue unless extraEvaluation?.baseScore? + evaluation.baseScore -= extraStack.quantity * extraEvaluation.baseScore + + for id, toolItem of recipe.tools + toolEvaluation = @evaluateItem toolItem + evaluation.addBaseEvaluation toolEvaluation + + evaluation.baseScore = evaluation.baseScore / recipe.output.quantity + + _computeGatherableItemScore: (item, evaluation)-> + evaluation.baseScore = 1 diff --git a/src/client/models/crafting/resources_evaluator.test.coffee b/src/client/models/crafting/resources_evaluator.test.coffee new file mode 100644 index 000000000..29b716fee --- /dev/null +++ b/src/client/models/crafting/resources_evaluator.test.coffee @@ -0,0 +1,125 @@ +# +# Crafting Guide - resources_evaluator.test.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +ResourcesEvaluator = require './resources_evaluator' +fixtures = require './fixtures' + +######################################################################################################################## + +describe "ResourcesEvaluator", -> + + beforeEach -> + @evaluator = new ResourcesEvaluator + @mod = fixtures.createMod() + + describe 'evaluating a gatherable item', -> + + beforeEach -> + @cobblestone = fixtures.configureCobblestone @mod + @evaluation = @evaluator.evaluateItem @cobblestone + + it 'should return a score of 1 for each item', -> + # 1 cobblestone + @evaluation.computeTotalScore().should.equal 1 + + describe 'evaluating a first-level recipe', -> + + beforeEach -> + @craftingTable = fixtures.configureCraftingTable @mod + @evaluation = @evaluator.evaluateItem @craftingTable + + it 'should return a score which is the sum of all resources', -> + # 1 oak wood => 4 planks (1/4) + # 4 oak planks => 1 crafting table (4 * 1/4 = 1) + @evaluation.computeTotalScore().should.equal 1 + + describe 'evaluating a recipe which requires a tool', -> + + beforeEach -> + @furnace = fixtures.configureFurnace @mod + @evaluation = @evaluator.evaluateItem @furnace + + it 'includes the cost of the tool', -> + # 1 oak wood => 4 planks (1/4) + # 4 oak planks => 1 crafting table (4 * 1/4 = 1) + # 8 cobblestone =(crafting table)=> 1 furnace (8 + 1 = 9) + @evaluation.computeTotalScore().should.equal 9 + + describe 'evaluating a recipe where a tool requires a tool', -> + + beforeEach -> + @ironIngot = fixtures.configureIronIngot @mod + @evaluation = @evaluator.evaluateItem @ironIngot + + it 'should include both tools in the score', -> + # raw ingredients: + # 8 iron ore + 1 coal =(furnace)=> 8 iron ingots ((8 + 1) / 8 = 1.125) + # tools: + # 8 cobblestone =(crafting table)=> 1 furnace (8) + # 4 oak planks => 1 crafting table (4 * 1/4 = 1) + # 1 oak wood => 4 planks (1/4) + @evaluation.computeTotalScore().should.equal 10.125 + + describe 'evaluating a recipe which requires a tool multiple times', -> + + beforeEach -> + @ironSword = fixtures.configureIronSword @mod + @evaluation = @evaluator.evaluateItem @ironSword + + it 'should include the tool only once', -> + # raw ingredients: + # 1 oak wood ==> 4 planks (0.25) + # 2 oak planks ==> 4 sticks (0.125) + # 8 iron ore + 1 coal =(furnace)=> 8 iron ingots (1.125) + # 2 iron ingot + 1 stick =(crafting table)=> 1 iron sword (2.375) + # + # tools: + # 8 cobblestone =(crafting table)=> 1 furnace (8) + # + # 1 oak wood => 4 planks (0.25) + # 4 oak planks => 1 crafting table (1) + # + @evaluation.computeTotalScore().should.equal 11.375 + + describe 'evaluating a recipe which has a recursive recipe', -> + + beforeEach -> + @ironBlock = fixtures.configureIronBlock @mod + @evaluation = @evaluator.evaluateItem @ironBlock + + it 'should avoid the recursive recipes', -> + # raw ingredients: + # 8 iron ore + 1 coal =(furnace)=> 8 iron ingots ((8 + 1) / 8 = 1.125) + # 9 iron ingot =(crafting table)=> 1 iron block (10.125) + # + # tools: + # 8 cobblestone =(crafting table)=> 1 furnace (8) + # 4 oak planks => 1 crafting table (4 * 1/4 = 1) + # 1 oak wood => 4 planks (1/4) + @evaluation.computeTotalScore().should.equal 19.125 + + describe 'evaluating a recipe with multiple ouputs', -> + + beforeEach -> + @cake = fixtures.configureCake @mod + @evaluation = @evaluator.evaluateItem @cake + + it 'should discount the extra outputs', -> + # raw ingredients: + # 8 iron ore + 1 coal =(furnace)=> 8 iron ingot (1.125) + # 3 iron ingot =(crafting table)=> 1 bucket (3.375) + # 1 milk + 1 bucket ==> 1 milk bucket (4.375) + # 1 sugar cane ==> 1 sugar (1) + # 3 wheat + 1 egg + 2 sugar + 3 milk bucket =(crafting table)=> 1 cake, 3 buckets (9) + # + # furnace: + # 8 cobblestone =(crafting table)=> 1 furnace (8) + # + # crafting table + # 1 oak wood ==> 4 oak planks (0.25) + # 4 planks ==> 1 crafting table (1) + @evaluation.computeTotalScore().should.equal 18 diff --git a/src/client/models/crafting/simple_inventory.coffee b/src/client/models/crafting/simple_inventory.coffee deleted file mode 100644 index 8c4704d78..000000000 --- a/src/client/models/crafting/simple_inventory.coffee +++ /dev/null @@ -1,212 +0,0 @@ -# -# Crafting Guide - simple_inventory.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -ItemSlug = require '../game/item_slug' -SimpleStack = require './simple_stack' - -######################################################################################################################## - -module.exports = class SimpleInventory - - constructor: (attributes={}, options={})-> - @clear() - - if options.modPack? - @modPack = options.modPack - - if options.clone? - @addInventory options.clone - - # Class Methods ################################################################################ - - @Delimiters = - Item: '.' - Stack: ':' - - # Public Methods ############################################################################### - - add: (itemSlug, quantity=1)-> - return this unless quantity > 0 - - @_add itemSlug, quantity - return this - - addInventory: (inventory)-> - inventory.each (stack)=> @_add stack.itemSlug, stack.quantity - return this - - clear: (options={})-> - @_stacks = {} - @_itemSlugs = [] - - clone: -> - inventory = new SimpleInventory - inventory.addInventory this - return inventory - - each: (callback)-> - for itemSlug in @_itemSlugs - callback @_stacks[itemSlug] - - getSlugs: -> - return @_itemSlugs[..] - - hasAtLeast: (itemSlug, quantity=1)-> - if quantity is 0 then return true - - stack = @_stacks[itemSlug] - return false unless stack? - return stack.quantity >= quantity - - localize: -> - if not @modPack? then throw new Error 'localize requires @modPack' - - changed = false - newSlugs = [] - newStacks = [] - for itemSlug in @_itemSlugs - stack = @_stacks[itemSlug] - continue unless stack? - - qualifiedSlug = if itemSlug.isQualified then itemSlug else null - if not qualifiedSlug? - qualifiedSlug = @modPack.findItem(itemSlug)?.slug - changed = qualifiedSlug? - - if qualifiedSlug? - newSlugs.push qualifiedSlug - newStacks.push new SimpleStack itemSlug:qualifiedSlug, quantity:stack.quantity - else - newSlugs.push itemSlug - newStacks.push stack - - if changed - @_itemSlugs = newSlugs - @_stacks = {} - for stack in newStacks - @_stacks[stack.itemSlug] = stack - - @_sort() - - pop: -> - itemSlug = @_itemSlugs.pop() - return null unless itemSlug? - - stack = @_stacks[itemSlug] - delete @_stacks[itemSlug] - - return stack - - quantityOf: (itemSlug)-> - stack = @_stacks[itemSlug] - return stack.quantity if stack - return 0 - - remove: (itemSlug, quantity=null)-> - stack = @_stacks[itemSlug] - return this unless stack? - - quantity ?= stack.quantity - return this unless quantity > 0 - - if stack.quantity < quantity - throw new Error "cannot remove #{quantity}: only #{stack.quantity} #{itemSlug} in this inventory" - - stack.quantity -= quantity - if stack.quantity is 0 - for currentItemSlug, index in @_itemSlugs - if ItemSlug.equal itemSlug, currentItemSlug - @_itemSlugs.splice index, 1 - break - - return this - - # Parsing Methods ############################################################################## - - parse: (data)-> - return this if not data? or data.length is 0 - - stacks = data.split SimpleInventory.Delimiters.Stack - for stackText in stacks - stackParts = stackText.split SimpleInventory.Delimiters.Item - if stackParts.length is 2 - quantity = parseInt stackParts[0], 10 - itemSlug = ItemSlug.slugify stackParts[1] - else if stackParts.length is 1 - quantity = 1 - itemSlug = ItemSlug.slugify stackParts[0] - else - throw new Error "expected #{stackText} to have 0 or 1 parts" - - if itemSlug.qualified.length > 0 - @add itemSlug, quantity - - return this - - unparse: (options={})-> - parts = [] - @each (stack)=> - slugText = stack.itemSlug.item - if @modPack? - item = @modPack.findItem ItemSlug.slugify slugText - if item? and item.slug.qualified isnt stack.itemSlug.qualified - slugText = stack.itemSlug.qualified - - if stack.quantity is 1 - parts.push slugText - else - parts.push "#{stack.quantity}#{SimpleInventory.Delimiters.Item}#{slugText}" - - return parts.join SimpleInventory.Delimiters.Stack - - # Property Methods ############################################################################# - - getIsEmpty: -> - return @_itemSlugs.length is 0 - - getTotalQuantity: -> - total = 0 - @each (stack)-> - total += stack.quantity - return total - - Object.defineProperties @prototype, - isEmpty: { get:@prototype.getIsEmpty } - totalQuantity: { get:@prototype.getTotalQuantity } - - # Object Overrides ############################################################################# - - toString: -> - result = [@constructor.name, " (", @cid, ") {items: ["] - - needsDelimiter = false - @each (stack)-> - if needsDelimiter then result.push ', ' - result.push stack.toString() - needsDelimiter = true - result.push ']' - - result.push '}' - return result.join '' - - # Private Methods ############################################################################## - - _add: (itemSlug, quantity=1)-> - return unless itemSlug? - return unless quantity > 0 - - stack = @_stacks[itemSlug] - if not stack? - stack = new SimpleStack itemSlug:itemSlug, quantity:quantity - @_stacks[itemSlug] = stack - @_itemSlugs.push itemSlug - @_sort() - else - stack.quantity += quantity - - _sort: -> - @_itemSlugs.sort (a, b)-> ItemSlug.compare a, b diff --git a/src/client/models/crafting/simple_stack.coffee b/src/client/models/crafting/simple_stack.coffee deleted file mode 100644 index 361eedb4b..000000000 --- a/src/client/models/crafting/simple_stack.coffee +++ /dev/null @@ -1,33 +0,0 @@ -# -# Crafting Guide - simple_stack.coffee -# -# Copyright © 2014-2016 by Redwood Labs -# All rights reserved. -# - -ItemSlug = require '../game/item_slug' - -######################################################################################################################## - -module.exports = class Stack - - constructor: (attributes={}, options={})-> - if not attributes.itemSlug? then throw new Error 'attributes.itemSlug is required' - attributes.quantity ?= 1 - - @itemSlug = attributes.itemSlug - @quantity = attributes.quantity - - # Class Methods ################################################################################ - - @compare: (a, b)-> - if a? and not b? then return -1 - if not a? and b? then return +1 - if a.quantity isnt b.quantity - return if a.quantity > b.quantity then -1 else +1 - return ItemSlug.compare a.itemSlug, b.itemSlug - - # Object Overrides ############################################################################# - - toString: -> - return "#{@quantity} #{@itemSlug}" diff --git a/src/client/models/crafting/stack.coffee b/src/client/models/crafting/stack.coffee new file mode 100644 index 000000000..aa8541cfc --- /dev/null +++ b/src/client/models/crafting/stack.coffee @@ -0,0 +1,42 @@ +# +# Crafting Guide - stack.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +######################################################################################################################## + +module.exports = class Stack + + constructor: (attributes={})-> + @item = attributes.item + @quantity = attributes.quantity + + # Properties ################################################################################### + + Object.defineProperties @prototype, + + item: + get: -> return @_item + set: (item)-> + if not item? then throw new Error "item is required" + if @_item is item then return + if @_item? then throw new Error "item cannot be reassigned" + @_item = item + + modPack: + get: -> return @_item.modPack + set: -> throw new Error "modPack cannot be replaced" + + quantity: + get: -> return @_quantity + set: (quantity)-> + quantity = parseInt "#{quantity}" + quantity = if Number.isNaN(quantity) then 0 else Math.max(0, quantity) + @_quantity = quantity + + # Object Overrides ############################################################################# + + toString: -> + return "Stack:#{@item}×#{@quantity}" \ No newline at end of file diff --git a/src/client/models/crafting/steps_evaluator.coffee b/src/client/models/crafting/steps_evaluator.coffee new file mode 100644 index 000000000..3e938cb69 --- /dev/null +++ b/src/client/models/crafting/steps_evaluator.coffee @@ -0,0 +1,43 @@ +# +# Crafting Guide - steps_evaluator.coffee +# +# Copyright © 2014-2016 by Redwood Labs +# All rights reserved. +# + +Evaluator = require './evaluator' + +######################################################################################################################## + +module.exports = class StepsEvaluator extends Evaluator + + # Evaluator Overrides ########################################################################## + + _computeRecipeScore: (recipe, evaluation)-> + evaluation.score = 0 + + for id, stack of recipe.inputs + inputEvaluation = @evaluateItem stack.item + if not inputEvaluation?.score? + evaluation.score = null + return + + evaluation.addBaseEvaluation inputEvaluation + evaluation.score = Math.min evaluation.score, inputEvaluation.score + 1 + + for id, item of recipe.tools + continue if evaluation.isToolIncluded item + + toolEvaluation = @evaluateItem stack.item + if not toolEvaluation?.score? + evaluation.score = null + return + + evaluation.addBaseEvaluation evaluation + evaluation.addIncludedTool item + evaluation.score += toolEvaluation.score + + return result + + _computeGatherableItemScore: (item, evaluation)-> + evaluation.score = 0 diff --git a/src/client/models/game/mod_version.coffee b/src/client/models/game/mod_version.coffee index 466361538..7d5268406 100644 --- a/src/client/models/game/mod_version.coffee +++ b/src/client/models/game/mod_version.coffee @@ -157,7 +157,7 @@ module.exports = class ModVersion extends BaseModel for recipe in primaryRecipes result.push recipe - if not options.onlyPrimary + if not options.onlyPrimary and result.length is 0 for recipe in otherRecipes result.push recipe diff --git a/src/client/models/game/mod_version.test.coffee b/src/client/models/game/mod_version.test.coffee index cba6d85d5..93a1d7305 100644 --- a/src/client/models/game/mod_version.test.coffee +++ b/src/client/models/game/mod_version.test.coffee @@ -90,12 +90,17 @@ describe 'mod_version.coffee', -> recipe:; input: Cake Slice; pattern: 000 000 000; onlyIf: item Cake Slice item: Bucket recipe:; input: Iron Ingot; pattern: ... 0.0 .0. + recipe:; input: Copper Ingot; pattern: ... 0.0 .0.; extras: Copper Nugget """ it 'finds all recipes which list item as output', -> recipes = modVersion.findRecipes ItemSlug.slugify('test__bucket') - (r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'cake', 'cake'] + (r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'bucket'] it 'skip recipes whose conditions are not met', -> recipes = modVersion.findRecipes ItemSlug.slugify('test__cake') recipes.length.should.equal 2 + + it 'finds recipes for items which are only ever extras', -> + recipes = modVersion.findRecipes ItemSlug.slugify('test__copper_nugget') + recipes.length.should.equal 1