Initial take on crafting algo Mk.III

This commit is contained in:
Andrew Miner
2016-09-25 13:14:42 -07:00
parent c0472b268d
commit 7377559f7b
35 changed files with 1660 additions and 1776 deletions
@@ -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()
@@ -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"
]
+112 -146
View File
@@ -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
@@ -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"
@@ -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}>"
@@ -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
-150
View File
@@ -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()
@@ -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
+102
View File
@@ -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
+313
View File
@@ -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
@@ -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
@@ -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 + ' ')}"
@@ -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
@@ -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}"
@@ -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'
+93
View File
@@ -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}>"
-108
View File
@@ -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
+62
View File
@@ -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}>"
@@ -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}>"
+58 -93
View File
@@ -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
@@ -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 ]
@@ -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
@@ -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]
+148
View File
@@ -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}>"
@@ -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'
@@ -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
@@ -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
@@ -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
@@ -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}"
+42
View File
@@ -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}"
@@ -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
+1 -1
View File
@@ -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
@@ -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