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
+3 -3
View File
@@ -132,7 +132,7 @@ module.exports = (grunt)->
options: options:
bail: true bail: true
color: true color: true
reporter: 'dot' reporter: 'list'
require: [ require: [
'coffee-script/register' 'coffee-script/register'
'./src/test_helper.coffee' './src/test_helper.coffee'
@@ -188,7 +188,7 @@ module.exports = (grunt)->
tasks: ['sass'] tasks: ['sass']
test: test:
files: ['./src/**/*.coffee', './src/**/*.js', './test/**/*.coffee'] files: ['./src/**/*.coffee', './src/**/*.js', './test/**/*.coffee']
tasks: ['test'] tasks: ['script:clear', 'test']
# Compound Tasks ################################################################################################### # Compound Tasks ###################################################################################################
@@ -277,7 +277,7 @@ module.exports = (grunt)->
grunt.registerTask 'script:clear', "clear the current terminal buffer", -> grunt.registerTask 'script:clear', "clear the current terminal buffer", ->
done = this.async() done = this.async()
grunt.util.spawn cmd:'clear', opts:{stdio:'inherit'}, (error)-> done(error) grunt.util.spawn cmd:'./scripts/clear_buffer', opts:{stdio:'inherit'}, (error)-> done(error)
grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", -> grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", ->
done = this.async() done = this.async()
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
clear && printf '\e[3J'
@@ -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. # All rights reserved.
# #
SimpleInventory = require './simple_inventory' Inventory = require "./inventory"
{StringBuilder} = require "crafting-guide-common"
######################################################################################################################## ########################################################################################################################
module.exports = class CraftingPlan module.exports = class CraftingPlan
constructor: (modPack, want, have, steps)-> constructor: (attributes={})->
if not modPack? then throw new Error 'modPack is required' @_id = _.uniqueId "crafting-plan-"
if not want? then throw new Error 'want is required' @_make = null
if not have? then throw new Error 'have is required' @_need = null
if not steps? then throw new Error 'steps is required' @have = attributes.have
@steps = attributes.steps
@want = attributes.want
@_have = have @_consolidateSteps()
@_made = null @_computeResources()
@_modPack = modPack
@_need = null
@_rawScores = {}
@_scores = {}
@_steps = steps
@_tools = null
@_want = want
@_numberSteps() # Properties ###################################################################################
# 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 #############################################################################
Object.defineProperties @prototype, Object.defineProperties @prototype,
have:
get: -> @_have have: # an Inventory specifying what the player already has
length: get: -> return @_have
get: -> @steps.length set: (have)->
made: have ?= new Inventory
get: -> @_made if @_have is have then return
need: if @_have? then throw new Error "have cannot be reassigned"
get: -> @_need @_have = new Inventory have
steps:
get: -> @_steps id: # a string uniquely specifying this crafting plan
want: get: -> return @_id
get: -> @_want 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 ############################################################################# # Object Overrides #############################################################################
toString: -> toString: (options={})->
result = ["To Make:"] options.full ?= false
@_want.each (stack)->
result.push " #{stack}"
result.push "When you already have:" if options.full
@_have.each (stack)-> b = new StringBuilder
result.push " #{stack}" 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:" return b.toString()
if @_need? else
@_need.each (stack)-> return "CraftingPlan:#{@_make.toString(full:true)}<#{@_id}>"
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'
# Private Methods ############################################################################## # Private Methods ##############################################################################
_executeStep: (step)-> _computeResources: ->
step.multiplier += 1 need = new Inventory @_want
recipe = step.recipe make = new Inventory @_have
steps = @_steps[..].reverse()
for stack in recipe.input for step in steps
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug step.count = 0
available = @_made.quantityOf qualifiedSlug for productStack in step.recipe.allProducts
required = recipe.getQuantityRequired stack.itemSlug continue unless need.contains productStack.item
consumed = Math.min required, available productCount = Math.ceil need.getQuantity(productStack.item) / productStack.quantity
deficit = required - consumed step.count = Math.max step.count, productCount
@_made.remove qualifiedSlug, consumed continue unless step.count > 0
@_need.add qualifiedSlug, deficit
for stack in recipe.output for itemId, item of step.recipe.inputs
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug amountNeeded = step.count * step.recipe.computeQuantityRequired item
amountAvailable = make.getQuantity item
amountUsed = Math.min amountAvailable, amountNeeded
amountMissing = amountNeeded - amountUsed
deficit = @_need.quantityOf qualifiedSlug make.remove item, amountUsed
created = recipe.getQuantityProduced stack.itemSlug need.add item, amountMissing
replenished = Math.min deficit, created
surplus = created - replenished
@_made.add qualifiedSlug, surplus for productStack in step.recipe.allProducts
@_need.remove qualifiedSlug, replenished amountCreated = step.count * productStack.quantity
amountNeeded = need.getQuantity productStack.item
amountFulfilled = Math.min amountNeeded, amountCreated
amountSurplus = amountCreated - amountFulfilled
_numberSteps: -> need.remove productStack.item, amountFulfilled
for step, i in @_steps make.add productStack.item, amountSurplus
step.number = i + 1
_pruneEmptySteps: -> make.merge @_want
index = 0
while index < @_steps.length @_make = make
step = @_steps[index] @_need = need
if step.multiplier is 0
@_steps.splice index, 1 _consolidateSteps: ->
else steps = []
index++ 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. # All rights reserved.
# #
CraftingPlan = require './crafting_plan' CraftingPlan = require './crafting_plan'
fixtures = require './fixtures.test' CraftingPlanStep = require './crafting_plan_step'
ItemSlug = require '../game/item_slug' Inventory = require './inventory'
fixtures = require './fixtures'
######################################################################################################################## ########################################################################################################################
describe 'crafting_plan.coffee', -> describe "CraftingPlan", ->
it 'requires wanted item if gatherable', -> beforeEach ->
plans = fixtures.makePlans [1, 'test__coal'] @mod = fixtures.createMod()
plans.length.should.equal 1 @want = new Inventory
@have = new Inventory
plan = plans[0] describe "with a single step plan", ->
plan.computeRequired()
plan.need.unparse().should.equal 'coal'
plan.made.unparse().should.equal 'coal'
it 'can compute a single item with one single step plan', -> beforeEach ->
plans = fixtures.makePlans [1, 'test__charcoal'] @oakPlank = fixtures.configureOakPlank @mod
plans.length.should.equal 1 @want.add @oakPlank, 11
plan = plans[0] @plan = new CraftingPlan want:@want, steps:[
plan.computeRequired() new CraftingPlanStep @oakPlank.firstRecipe
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']
it 'can compute a large quantity of a single item with one single step plan', -> it "correctly computes the step counts", ->
plans = fixtures.makePlans [15, 'test__charcoal'] @plan.steps[0].count.should.equal 3
plans.length.should.equal 1
plan = plans[0] it "correctly determines the inputs needed", ->
plan.computeRequired() @plan.need.toString(full:true).should.equal "3 Oak Wood"
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 'can compute a single item with multiple plans', -> it "correctly computes the products created", ->
plans = fixtures.makePlans [1, 'test__iron_ingot'] @plan.make.toString(full:true).should.equal "12 Oak Planks"
plans.length.should.equal 2
plan = plans[0] describe "with a multi-step plan", ->
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'
]
plan = plans[1] beforeEach ->
plan.computeRequired() @ironIngot = fixtures.configureIronIngot @mod
plan.need.unparse().should.equal 'coal:8.iron_ore' @ironSword = fixtures.configureIronSword @mod
plan.made.unparse().should.equal '8.iron_ingot' @oakPlank = fixtures.configureOakPlank @mod
(s.toString() for s in plan.steps).should.eql [ @stick = fixtures.configureStick @mod
'1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
]
it 'uses the correct amount of passthrough items', -> @want.add @ironSword, 20
plans = fixtures.makePlans [10, 'test__split_oak_wood'], [10, 'test__split_spruce_wood']
plan = plans[2]
plan.computeRequired()
plan.need.unparse().should.equal 'coal:8.iron_ore:6.oak_wood:5.spruce_wood' @plan = new CraftingPlan want:@want, steps:[
plan.made.unparse().should.equal '4.iron_ingot:maul:2.oak_planks:10.split_oak_wood:10.split_spruce_wood:stick' new CraftingPlanStep @oakPlank.firstRecipe
(s.toString() for s in plan.steps).should.eql [ new CraftingPlanStep @stick.firstRecipe
'1x test__oak_wood>... .0. ...>>4 test__oak_planks' new CraftingPlanStep @ironIngot.firstRecipe
'1x test__oak_planks>.0. .0. ...>>4 test__stick' new CraftingPlanStep @ironSword.firstRecipe
'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'
]
it 'can compute multiple items with multiple plans', -> it "correctly computes the step counts", ->
plans = fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword'] @plan.steps[0].count.should.equal 3
plans.length.should.equal 4 @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 it "correctly determines the inputs needed", ->
plan.computeRequired() @plan.need.toString(full:true).should.equal "5 Coal, 40 Iron Ore, 3 Oak Wood"
plan.need.unparse().should.match /16.copper_ore.*8.iron_ore/
plan.made.unparse().should.match /copper_block.*:iron_sword/
plan = plans[0] it "correctly computes the products created", ->
plan.need.unparse().should.match /3.coal.*9.oak_wood/ @plan.make.toString(full:true).should.equal "20 Iron Sword, 2 Oak Planks"
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
plan = plans[1] describe "with a plan which recycles some items", ->
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
plan = plans[2] beforeEach ->
plan.need.unparse().should.match /^coal.*9.oak_wood/ @bucket = fixtures.configureBucket @mod
plan.made.unparse().should.match /5.charcoal/ @cake = fixtures.configureCake @mod
"#{plan.steps[3]}".should.equal \ @ironIngot = fixtures.configureIronIngot @mod
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot' @milkBucket = fixtures.configureMilkBucket @mod
"#{plan.steps[4]}".should.equal \ @sugar = fixtures.configureSugar @mod
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot'
plan.steps.length.should.equal 7
plan = plans[3] @want.add @cake, 2
plan.need.unparse().should.match /2.coal.*9.oak_wood/
plan.made.unparse().should.match /6.charcoal/ @plan = new CraftingPlan want:@want, steps:[
"#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot' new CraftingPlanStep @ironIngot.firstRecipe
"#{plan.steps[4]}".should.equal \ new CraftingPlanStep @bucket.firstRecipe
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot' new CraftingPlanStep @milkBucket.firstRecipe
plan.steps.length.should.equal 7 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. # All rights reserved.
# #
CraftingNode = require './crafting_node' CraftingPlan = require "./crafting_plan"
CraftingPlan = require './crafting_plan' CraftingPlanStep = require "./crafting_plan_step"
CraftingStep = require './crafting_step'
Inventory = require '../game/inventory'
######################################################################################################################## ########################################################################################################################
module.exports = class PlanBuilder module.exports = class PlanBuilder
constructor: (rootNode, modPack, options={})-> constructor: (evaluator)->
if not rootNode? then throw new Error 'rootNode is required' @evaluator = evaluator
if not modPack? then throw new Error 'modPack is required'
@want = options.want # Properties ###################################################################################
@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 #############################################################################
Object.defineProperties @prototype, Object.defineProperties @prototype,
complete: evaluator:
get: -> @_complete 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: # Public Methods ###############################################################################
get: -> @_have
set: (have)-> @_have = have or new Inventory
maxPlanCount: createPlan: (want, have)->
get: -> @_maxPlanCount @_alreadyMaking = {}
set: (value)-> @_maxPlanCount = value @_recipesInUse = {}
plans: stepList = []
get: -> @_plans for itemId, stack of want.stacks
steps = @_findStepsForItem stack.item, stack.quantity
return null unless steps?
want: stepList.push steps
get: -> @_want
set: (want)-> @_want = want or new Inventory plan = new CraftingPlan want:want, have:have, steps:_.flatten(stepList)
return plan
# Private Methods ############################################################################## # Private Methods ##############################################################################
_captureCurrentPlan: -> _findStepsForItem: (item, quantity=1)->
toVisit = [@_rootNode] steps = null
stepNodes = []
while toVisit.length > 0 if not @_alreadyMaking[item.id]
node = toVisit.shift() @_alreadyMaking[item.id] = true
if node.TYPE is CraftingNode::TYPES.INVENTORY recipes = @_evaluator.getOrderedRecipes item, quantity
toVisit.push(c) for c in node.children if recipes.length is 0
else if node.TYPE is CraftingNode::TYPES.ITEM steps = []
toVisit.push node.children[0] if node.children.length > 0 else
else if node.TYPE is CraftingNode::TYPES.RECIPE for recipe in recipes
stepNodes.push node continue if @_recipesInUse[recipe.id]?
toVisit.push(c) for c in node.children
steps = [] steps = @_findStepsForRecipe recipe, quantity
seenRecipes = {} break if steps?
index = stepNodes.length - 1
while index >= 0
node = stepNodes[index]
index -= 1
return null unless node.valid and node.complete
recipeSlug = node.recipe.slug delete @_alreadyMaking[item.id]
continue if seenRecipes[recipeSlug]?
seenRecipes[recipeSlug] = true return steps
steps.push new CraftingStep node.recipe, @_modPack
plan = new CraftingPlan @_modPack, @_want, @_have, steps _findStepsForRecipe: (recipe, quantity=1)->
return plan steps = null
_incrementChoiceNodes: -> if not @_recipesInUse[recipe.id]
if @_choiceNodes.length is 0 @_recipesInUse[recipe.id] = true
@_complete = true
return
index = @_choiceNodes.length - 1 invalidRecipe = false
while true for itemId, item of recipe.inputs
if index is -1 inputSteps = @_findStepsForItem item, quantity * recipe.computeQuantityRequired(item)
@_complete = true if not inputSteps?
return invalidRecipe = true
break
node = @_choiceNodes[index] steps ?= []
node.rotateChildren() for step in inputSteps
steps.push step
return unless node.rotations % node.children.length is 0 if invalidRecipe
index -= 1 steps = null
else
steps.push new CraftingPlanStep recipe
_isolateChoiceNodes: -> delete @_recipesInUse[recipe.id]
@_rootNode.acceptVisitor
onEnterItemNode: (node)=> return steps
if node.children.length > 1
@_choiceNodes.push node
@@ -5,39 +5,154 @@
# All rights reserved. # All rights reserved.
# #
fixtures = require './fixtures.test' fixtures = require './fixtures'
PlanBuilder = require './plan_builder' Inventory = require './inventory'
PlanBuilder = require './plan_builder'
ResourcesEvaluator = require './resources_evaluator'
######################################################################################################################## ########################################################################################################################
describe 'plan_builder.coffee', -> describe.only "PlanBuilder", ->
printPlan = (plan)-> beforeEach ->
return '' unless plan? @planner = new PlanBuilder new ResourcesEvaluator
return ((s.recipe.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > ' @mod = fixtures.createMod()
@want = new Inventory
@have = new Inventory
it 'generates an empty plan for a gatherable item', -> describe "for a gatherable item, creates a plan that", ->
plans = fixtures.makePlans [1, 'test__oak_wood']
plans.length.should.equal 1 beforeEach ->
plans[0].length.should.equal 0 @oakWood = fixtures.configureOakWood @mod
@want.add @oakWood, 2
@plan = @planner.createPlan @want
it 'can find a multi-step plan', -> it "has no steps at all", ->
plans = fixtures.makePlans [1, 'test__lever'] @plan.steps.length.should.equal 0
printPlan(plans[0]).should.equal '4 test__oak_planks > 4 test__stick > test__lever' it "demands the item itself as the input", ->
plans.length.should.equal 1 @plan.need.toString(full:true).should.equal "2 Oak Wood"
it 'can find multiple plans', -> it "produces the item as the result", ->
plans = fixtures.makePlans [1, 'test__iron_ingot'] @plan.make.toString(full:true).should.equal "2 Oak Wood"
printPlan(plans[0]).should.equal '8 test__charcoal > 8 test__iron_ingot' describe "for a single item with a one-step recipe, it creates a plan that", ->
printPlan(plans[1]).should.equal '8 test__iron_ingot'
plans.length.should.equal 2
it 'ignores invalid plans', -> beforeEach ->
plans = fixtures.makePlans [1, 'test__copper_block'] @oakPlank = fixtures.configureOakPlank @mod
@want.add @oakPlank, 63
@plan = @planner.createPlan @want
printPlan(plans[0]).should.equal '8 test__copper_ingot > test__copper_block' it "has the recipe as the only step", ->
printPlan(plans[1]).should.equal '8 test__charcoal > 8 test__copper_ingot > test__copper_block' @plan.steps.length.should.equal 1
plans.length.should.equal 2 @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 for recipe in primaryRecipes
result.push recipe result.push recipe
if not options.onlyPrimary if not options.onlyPrimary and result.length is 0
for recipe in otherRecipes for recipe in otherRecipes
result.push recipe result.push recipe
@@ -90,12 +90,17 @@ describe 'mod_version.coffee', ->
recipe:; input: Cake Slice; pattern: 000 000 000; onlyIf: item Cake Slice recipe:; input: Cake Slice; pattern: 000 000 000; onlyIf: item Cake Slice
item: Bucket item: Bucket
recipe:; input: Iron Ingot; pattern: ... 0.0 .0. 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', -> it 'finds all recipes which list item as output', ->
recipes = modVersion.findRecipes ItemSlug.slugify('test__bucket') 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', -> it 'skip recipes whose conditions are not met', ->
recipes = modVersion.findRecipes ItemSlug.slugify('test__cake') recipes = modVersion.findRecipes ItemSlug.slugify('test__cake')
recipes.length.should.equal 2 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