Add new CraftingPlan class
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -19,7 +19,6 @@ module.exports = class GraphBuilder
|
||||
@_wanted = options.wanted ?= new Inventory
|
||||
|
||||
@_wanted.on 'change', => @reset()
|
||||
|
||||
@reset()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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'
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user