diff --git a/src/coffee/models/crafting/crafting_plan.coffee b/src/coffee/models/crafting/crafting_plan.coffee index ca86baa92..1161cb909 100644 --- a/src/coffee/models/crafting/crafting_plan.coffee +++ b/src/coffee/models/crafting/crafting_plan.coffee @@ -5,8 +5,82 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### +Inventory = require '../inventory' + ######################################################################################################################## module.exports = class CraftingPlan - constructor: -> + constructor: (steps, wanted)-> + if not steps? then throw new Error 'steps is required' + if not wanted? then throw new Error 'wanted is required' + + @_produced = new Inventory + @_required = new Inventory + @_steps = steps + @_wanted = wanted + + @_computeRequired() + + # Property Methods ############################################################################# + + Object.defineProperties @prototype, + length: + get: -> @steps.length + produced: + get: -> @_produced + required: + get: -> @_required + steps: + get: -> @_steps + wanted: + get: -> @_wanted + + # Object Overrides ############################################################################# + + toString: -> + return "#{@constructor.name}{ + wanted:#{@wanted}, + required:#{@required}, + steps:[#{(step.toString() for step in @steps).join(',')}], + produced:#{@produced} + }" + + # Private Methods ############################################################################## + + _computeRequired: -> + @_required.addInventory @_wanted + + for i in [@_steps.length-1..0] by -1 + recipe = @_steps[i] + + for stack in recipe.output + while @_required.quantityOf(stack.itemSlug) > 0 + @_executeRecipe recipe + + @_produced.addInventory @_wanted + + _executeRecipe: (recipe)-> + #console.log "executing: #{recipe}" + for stack in recipe.input + @_use stack + for stack in recipe.output + @_produce stack + + _produce: (stack)-> + deficit = @_required.quantityOf stack.itemSlug + replenished = Math.min deficit, stack.quantity + surplus = stack.quantity - replenished + + #console.log "producing #{stack.itemSlug}, surplus: #{surplus}, replenished:#{replenished}" + @_produced.add stack.itemSlug, surplus + @_required.remove stack.itemSlug, replenished + + _use: (stack)-> + available = @_produced.quantityOf stack.itemSlug + consumed = Math.min stack.quantity, available + deficit = stack.quantity - consumed + + #console.log "using #{stack.itemSlug}, consumed: #{consumed}, deficit:#{deficit}" + @_produced.remove stack.itemSlug, consumed + @_required.add stack.itemSlug, deficit diff --git a/src/coffee/models/crafting/graph_builder.coffee b/src/coffee/models/crafting/graph_builder.coffee index 4d83efc55..02ef2b822 100644 --- a/src/coffee/models/crafting/graph_builder.coffee +++ b/src/coffee/models/crafting/graph_builder.coffee @@ -19,7 +19,6 @@ module.exports = class GraphBuilder @_wanted = options.wanted ?= new Inventory @_wanted.on 'change', => @reset() - @reset() # Public Methods ############################################################################### diff --git a/src/coffee/models/crafting/plan_builder.coffee b/src/coffee/models/crafting/plan_builder.coffee index 63383b9e3..185f2f424 100644 --- a/src/coffee/models/crafting/plan_builder.coffee +++ b/src/coffee/models/crafting/plan_builder.coffee @@ -6,14 +6,18 @@ All rights reserved. ### CraftingNode = require './crafting_node' +CraftingPlan = require './crafting_plan' +Inventory = require '../inventory' ######################################################################################################################## module.exports = class PlanBuilder - constructor: (rootNode)-> + constructor: (rootNode, options={})-> if not rootNode? then throw new Error 'rootNode is required' + @wanted = options.wanted + @_choiceNodes = [] @_complete = false @_plans = [] @@ -26,9 +30,9 @@ module.exports = class PlanBuilder producePlans: (maxPlans=null)-> maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE - while @plans.length < maxPlans and not @complete + while @plans.length is 0 or (@plans.length < maxPlans and not @complete) plan = @_captureCurrentPlan() - @plans.push plan if plan.length > 0 + @plans.push plan @_incrementChoiceNodes() return @_plans @@ -41,15 +45,22 @@ module.exports = class PlanBuilder getPlans: -> return @_plans + getWanted: -> + return @_wanted + + setWanted: (wanted)-> + @_wanted = wanted or new Inventory + Object.defineProperties @prototype, complete: { get:@prototype.isComplete } - plans: { get:@prototype.getPlans } + plans: { get:@prototype.getPlans } + wanted: { get:@prototype.getWanted, set:@prototype.setWanted } # Private Methods ############################################################################## _captureCurrentPlan: -> toVisit = [@_rootNode] - plan = [] + steps = [] while toVisit.length > 0 node = toVisit.shift() @@ -59,10 +70,12 @@ module.exports = class PlanBuilder else if node.TYPE is CraftingNode::TYPES.ITEM toVisit.push node.children[0] if node.children.length > 0 else if node.TYPE is CraftingNode::TYPES.RECIPE - plan.push node.recipe + steps.push node.recipe toVisit.push(c) for c in node.children - return plan.reverse() + steps.reverse() + plan = new CraftingPlan steps, @_wanted + return plan _incrementChoiceNodes: -> index = @_choiceNodes.length - 1 diff --git a/src/coffee/models/inventory.coffee b/src/coffee/models/inventory.coffee index f26184c39..918b5ba8c 100644 --- a/src/coffee/models/inventory.coffee +++ b/src/coffee/models/inventory.coffee @@ -30,6 +30,8 @@ module.exports = class Inventory extends BaseModel # Public Methods ############################################################################### add: (itemSlug, quantity=1)-> + return this unless quantity > 0 + @_add itemSlug, quantity @trigger Event.add, this, itemSlug, quantity @trigger Event.change, this @@ -102,12 +104,12 @@ module.exports = class Inventory extends BaseModel return stack.quantity remove: (itemSlug, quantity=null)-> - return if quantity is 0 - stack = @_stacks[itemSlug] - if not stack? then throw new Error "cannot remove #{itemSlug} since it is not in this inventory" + 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" @@ -115,7 +117,7 @@ module.exports = class Inventory extends BaseModel if stack.quantity is 0 @stopListening stack delete @_stacks[itemSlug] - @_itemSlugs = _(@_itemSlugs).without itemSlug + @_itemSlugs = (s for s in @_itemSlugs when not ItemSlug.equal(s, itemSlug)) @trigger Event.remove, this, itemSlug, quantity @trigger Event.change, this @@ -164,8 +166,16 @@ module.exports = class Inventory extends BaseModel getIsEmpty: -> return @_itemSlugs.length is 0 + getTotalQuantity: -> + total = 0 + @each (stack)-> + total += stack.quantity + return total + + Object.defineProperties @prototype, - isEmpty: { get:@prototype.getIsEmpty } + isEmpty: { get:@prototype.getIsEmpty } + totalQuantity: { get:@prototype.getTotalQuantity } # Object Overrides ############################################################################# @@ -186,7 +196,7 @@ module.exports = class Inventory extends BaseModel _add: (itemSlug, quantity=1)-> return unless itemSlug? - return if quantity is 0 + return unless quantity > 0 stack = @_stacks[itemSlug] if not stack? diff --git a/test/crafting/crafting_plan.test.coffee b/test/crafting/crafting_plan.test.coffee new file mode 100644 index 000000000..3c729eec1 --- /dev/null +++ b/test/crafting/crafting_plan.test.coffee @@ -0,0 +1,49 @@ +### +Crafting Guide - crafting_plan.test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +CraftingPlan = require '../../src/coffee/models/crafting/crafting_plan' +fixtures = require './fixtures' + +######################################################################################################################## + +describe 'crafting_plan.coffee', -> + + it 'requires wanted item if gatherable', -> + plans = fixtures.makePlans [1, 'test__coal'] + plans.length.should.equal 1 + + plan = plans[0] + plan.required.unparse().should.equal 'coal' + plan.produced.unparse().should.equal 'coal' + + it 'can compute a single item with a single step', -> + plans = fixtures.makePlans [1, 'test__charcoal'] + plans.length.should.equal 1 + + plan = plans[0] + plan.required.unparse().should.equal 'coal:8.oak_wood' + plan.produced.unparse().should.equal '8.charcoal' + + it 'can compute a large quantity of a single item with a single step', -> + plans = fixtures.makePlans [15, 'test__charcoal'] + plans.length.should.equal 1 + + plan = plans[0] + plan.required.unparse().should.equal '2.coal:16.oak_wood' + plan.produced.unparse().should.equal '16.charcoal' + + it 'can compute a single item with multiple steps', -> + plans = fixtures.makePlans [1, 'test__iron_ingot'] + plans.length.should.equal 2 + + plan = plans[0] + plan.required.unparse().should.equal 'coal:8.iron_ore:8.oak_wood' + plan.produced.unparse().should.equal '7.charcoal:iron_ingot' + + plan = plans[1] + plan.required.unparse().should.equal 'coal:8.iron_ore' + plan.produced.unparse().should.equal 'iron_ingot' diff --git a/test/crafting/fixtures.coffee b/test/crafting/fixtures.coffee index df7d34db5..8093685e3 100644 --- a/test/crafting/fixtures.coffee +++ b/test/crafting/fixtures.coffee @@ -10,6 +10,7 @@ ItemSlug = require '../../src/coffee/models/item_slug' Mod = require '../../src/coffee/models/mod' ModPack = require '../../src/coffee/models/mod_pack' ModVersion = require '../../src/coffee/models/mod_version' +PlanBuilder = require '../../src/coffee/models/crafting/plan_builder' ######################################################################################################################## @@ -21,6 +22,7 @@ MOD_VERSION_FILE = recipe: input: 8 Oak Wood, Coal pattern: .0. ... .1. + quantity: 8 item: Crafting Table recipe: @@ -100,6 +102,9 @@ MOD_VERSION_FILE = module.exports = fixtures = + makeGraphBuilder: -> + return new GraphBuilder modPack:fixtures.makeModPack() + makeModPack: -> modPack = new ModPack @@ -112,10 +117,18 @@ module.exports = fixtures = return modPack - makeGraphBuilder: -> - return new GraphBuilder modPack:fixtures.makeModPack() + makePlans: (stacks...)-> + modPack = fixtures.makeModPack() - makeTree: (itemSlug)-> + graphBuilder = fixtures.makeGraphBuilder() + for stack in stacks + graphBuilder.wanted.add ItemSlug.slugify(stack[1]), stack[0] + graphBuilder.expandGraph() + + planBuilder = new PlanBuilder graphBuilder.rootNode, wanted:graphBuilder.wanted + return planBuilder.producePlans() + + makeTree: (itemSlug, quantity=1)-> builder = fixtures.makeGraphBuilder() builder.wanted.add ItemSlug.slugify itemSlug builder.expandGraph() diff --git a/test/crafting/plan_builder.test.coffee b/test/crafting/plan_builder.test.coffee index fe1d09a67..8d3aca511 100644 --- a/test/crafting/plan_builder.test.coffee +++ b/test/crafting/plan_builder.test.coffee @@ -13,13 +13,14 @@ PlanBuilder = require '../../src/coffee/models/crafting/plan_builder' describe 'plan_builder.coffee', -> printPlan = (plan)-> - return ((s.slug.replace(/^.*>.*>/, '') for s in plan)).join ' > ' + return ((s.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > ' - it 'generates no plans for a gatherable item', -> + it 'generates an empty plan for a gatherable item', -> builder = new PlanBuilder fixtures.makeTree 'test__oak_wood' plans = builder.producePlans 100 - plans.length.should.equal 0 + plans.length.should.equal 1 + plans[0].length.should.equal 0 builder.complete.should.be.true it 'can find a multi-step plan', -> @@ -34,5 +35,5 @@ describe 'plan_builder.coffee', -> builder = new PlanBuilder fixtures.makeTree 'test__iron_ingot' plans = builder.producePlans 100 - printPlan(plans[0]).should.equal 'test__charcoal > test__iron_ingot' + printPlan(plans[0]).should.equal '8 test__charcoal > test__iron_ingot' printPlan(plans[1]).should.equal 'test__iron_ingot' diff --git a/test/crafting_plan.test.coffee b/test/crafting_plan.test.coffee deleted file mode 100644 index 32e955332..000000000 --- a/test/crafting_plan.test.coffee +++ /dev/null @@ -1,84 +0,0 @@ -### -Crafting Guide - crafting_plan.test.coffee - -Copyright (c) 2014-2015 by Redwood Labs -All rights reserved. -### - -CraftingPlan = require '../src/coffee/models/crafting_plan' -ItemSlug = require '../src/coffee/models/item_slug' -Mod = require '../src/coffee/models/mod' -ModPack = require '../src/coffee/models/mod_pack' -ModVersion = require '../src/coffee/models/mod_version' - -######################################################################################################################## - -modPack = plan = null - -######################################################################################################################## - -describe 'crafting_plan.coffee', -> - - beforeEach -> - mod = new Mod name:'Minecraft', slug:'minecraft' - mod.addModVersion new ModVersion modSlug:mod.slug, version:'1.7.10' - mod.activeModVersion.parse """ - schema:1 - - item:Oak Plank; recipe:; input:Oak Log; pattern:... .0. ...; quantity:4 - item:Stick; recipe:; input:Oak Plank; pattern:... .0. .0.; quantity:4 - item:Crafting Table; recipe:; input:Oak Plank; pattern:00. 00. ... - item:Furnace; recipe:; input:Cobblestone; pattern:000 0.0 000; tools:Crafting Table - item:Iron Ingot; recipe:; input:Iron Ore, furnace fuel; pattern:.0. ... .1.; tools:Furnace - item:Iron Sword; recipe:; input:Iron Ingot, Stick; pattern:.0. .0. .1.; tools:Crafting Table - """ - modPack = new ModPack - modPack.addMod mod - - plan = new CraftingPlan modPack:modPack, includingTools:false - - describe 'craft', -> - - describe 'under the simplest conditions', -> - - it 'can craft a single step recipe', -> - plan.want.add ItemSlug.slugify 'oak_plank' - plan.craft() - plan.need.unparse().should.equal 'oak_log' - plan.result.unparse().should.equal '4.oak_plank' - - it 'can craft a multi-step recipe', -> - plan.want.add ItemSlug.slugify 'crafting_table' - plan.craft() - plan.need.unparse().should.equal 'oak_log' - plan.result.unparse().should.equal 'crafting_table' - - it 'can craft a multi-step recipe using tools', -> - plan.want.add ItemSlug.slugify 'furnace' - plan.craft() - plan.need.unparse().should.equal '8.cobblestone' - plan.result.unparse().should.equal 'furnace' - - it 'can craft a multi-step recipe re-using tools', -> - plan.want.add ItemSlug.slugify 'iron_sword' - plan.craft() - plan.need.unparse().should.equal '2.furnace_fuel:2.iron_ore:oak_log' - plan.result.unparse().should.equal 'iron_sword:2.oak_plank:3.stick' - - describe 'with building tools', -> - - it 'can craft a multi-step recipe using tools', -> - plan.includingTools = true - plan.want.add ItemSlug.slugify 'furnace' - plan.craft() - plan.need.unparse().should.equal '8.cobblestone:oak_log' - plan.result.unparse().should.equal 'crafting_table:furnace' - - it 'can craft a multi-step recipe re-using tools', -> - plan.includingTools = true - plan.want.add ItemSlug.slugify 'iron_sword' - plan.craft() - - plan.need.unparse().should.eql '8.cobblestone:2.furnace_fuel:2.iron_ore:2.oak_log' - plan.result.unparse().should.equal 'crafting_table:furnace:' + - 'iron_sword:2.oak_plank:3.stick' diff --git a/test/inventory.test.coffee b/test/inventory.test.coffee index 2bef270b6..d699d1184 100644 --- a/test/inventory.test.coffee +++ b/test/inventory.test.coffee @@ -189,9 +189,12 @@ describe 'inventory.coffee', -> describe 'remove', -> - it 'throws when the item is absent', -> - expect(-> inventory.remove('chicken')).to.throw Error, - 'cannot remove chicken since it is not in this inventory' + it 'does nothing when the item is absent', -> + before = inventory.unparse() + inventory.remove 'foo' + after = inventory.unparse() + + before.should.equal after it 'throws when the item has insufficient quantity', -> expect(-> inventory.remove('wool', 10)).to.throw Error,