Initial take on crafting algo Mk.III
This commit is contained in:
+3
-3
@@ -132,7 +132,7 @@ module.exports = (grunt)->
|
||||
options:
|
||||
bail: true
|
||||
color: true
|
||||
reporter: 'dot'
|
||||
reporter: 'list'
|
||||
require: [
|
||||
'coffee-script/register'
|
||||
'./src/test_helper.coffee'
|
||||
@@ -188,7 +188,7 @@ module.exports = (grunt)->
|
||||
tasks: ['sass']
|
||||
test:
|
||||
files: ['./src/**/*.coffee', './src/**/*.js', './test/**/*.coffee']
|
||||
tasks: ['test']
|
||||
tasks: ['script:clear', 'test']
|
||||
|
||||
# Compound Tasks ###################################################################################################
|
||||
|
||||
@@ -277,7 +277,7 @@ module.exports = (grunt)->
|
||||
|
||||
grunt.registerTask 'script:clear', "clear the current terminal buffer", ->
|
||||
done = this.async()
|
||||
grunt.util.spawn cmd:'clear', opts:{stdio:'inherit'}, (error)-> done(error)
|
||||
grunt.util.spawn cmd:'./scripts/clear_buffer', opts:{stdio:'inherit'}, (error)-> done(error)
|
||||
|
||||
grunt.registerTask 'script:deploy:prod', "deploy code by copying to the production branch", ->
|
||||
done = this.async()
|
||||
|
||||
Executable
+3
@@ -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"
|
||||
]
|
||||
@@ -5,174 +5,140 @@
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
SimpleInventory = require './simple_inventory'
|
||||
Inventory = require "./inventory"
|
||||
{StringBuilder} = require "crafting-guide-common"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class CraftingPlan
|
||||
|
||||
constructor: (modPack, want, have, steps)->
|
||||
if not modPack? then throw new Error 'modPack is required'
|
||||
if not want? then throw new Error 'want is required'
|
||||
if not have? then throw new Error 'have is required'
|
||||
if not steps? then throw new Error 'steps is required'
|
||||
|
||||
@_have = have
|
||||
@_made = null
|
||||
@_modPack = modPack
|
||||
constructor: (attributes={})->
|
||||
@_id = _.uniqueId "crafting-plan-"
|
||||
@_make = null
|
||||
@_need = null
|
||||
@_rawScores = {}
|
||||
@_scores = {}
|
||||
@_steps = steps
|
||||
@_tools = null
|
||||
@_want = want
|
||||
@have = attributes.have
|
||||
@steps = attributes.steps
|
||||
@want = attributes.want
|
||||
|
||||
@_numberSteps()
|
||||
@_consolidateSteps()
|
||||
@_computeResources()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
computeRequired: ->
|
||||
@_need = new SimpleInventory modPack:@_modPack
|
||||
@_need.addInventory @_want
|
||||
|
||||
@_made = new SimpleInventory modPack:@_modPack
|
||||
@_made.addInventory @_have
|
||||
|
||||
@_tools = new SimpleInventory modPack:@_modPack
|
||||
|
||||
for i in [@_steps.length-1..0] by -1
|
||||
step = @_steps[i]
|
||||
step.multiplier = 0
|
||||
recipe = step.recipe
|
||||
|
||||
for stack in step.recipe.output
|
||||
continue if recipe.isPassThroughFor stack.itemSlug
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
|
||||
while @_need.quantityOf(qualifiedSlug) > 0
|
||||
@_executeStep step
|
||||
|
||||
if step.multiplier > 0
|
||||
for stack in step.recipe.output
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
continue unless recipe.isPassThroughFor stack.itemSlug
|
||||
continue if @_made.hasAtLeast qualifiedSlug, 1
|
||||
continue if @_need.hasAtLeast qualifiedSlug, 1
|
||||
continue if @_tools.hasAtLeast qualifiedSlug, 1
|
||||
@_need.add qualifiedSlug, 1
|
||||
@_tools.add qualifiedSlug, 1
|
||||
|
||||
@_made.addInventory @_want
|
||||
@_made.addInventory @_tools
|
||||
@_pruneEmptySteps()
|
||||
@_numberSteps()
|
||||
|
||||
hasRawScore: (name)->
|
||||
return @_rawScores[name]?
|
||||
|
||||
getRawScore: (name)->
|
||||
return @_rawScores[name]
|
||||
|
||||
setRawScore: (name, rawScore)->
|
||||
@_rawScores[name] = rawScore
|
||||
|
||||
hasScore: (name)->
|
||||
return @_scores[name]?
|
||||
|
||||
getScore: (name)->
|
||||
return @_scores[name]
|
||||
|
||||
setScore: (name, score)->
|
||||
@_scores[name] = score
|
||||
|
||||
# Property Methods #############################################################################
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
have:
|
||||
get: -> @_have
|
||||
length:
|
||||
get: -> @steps.length
|
||||
made:
|
||||
get: -> @_made
|
||||
need:
|
||||
get: -> @_need
|
||||
steps:
|
||||
get: -> @_steps
|
||||
want:
|
||||
get: -> @_want
|
||||
|
||||
have: # an Inventory specifying what the player already has
|
||||
get: -> return @_have
|
||||
set: (have)->
|
||||
have ?= new Inventory
|
||||
if @_have is have then return
|
||||
if @_have? then throw new Error "have cannot be reassigned"
|
||||
@_have = new Inventory have
|
||||
|
||||
id: # a string uniquely specifying this crafting plan
|
||||
get: -> return @_id
|
||||
set: -> throw new Error "id is not assignabled"
|
||||
|
||||
make: # an Inventory specifying what the results of executing the plan will be
|
||||
get: -> return @_make
|
||||
set: -> throw new Error "make cannot be assigned"
|
||||
|
||||
need: # an Inventory specifying what the player will need to gather before executing the plan
|
||||
get: -> return @_need
|
||||
set: -> throw new Error "need cannot be assigned"
|
||||
|
||||
want: # an Inventory specifying what the player wants to make
|
||||
get: -> return @_want
|
||||
set: (want)->
|
||||
if not want? then throw new Error "want is required"
|
||||
if want.isEmpty then throw new Error "want cannot be empty"
|
||||
if @_want is want then return
|
||||
if @_want? then throw new Error "want cannot be reassigned"
|
||||
@_want = new Inventory want
|
||||
|
||||
steps: # an array of CraftingPlanSteps detailing the steps of the plan
|
||||
get: -> return @_steps
|
||||
set: (steps)->
|
||||
steps ?= []
|
||||
if steps is @_steps then return
|
||||
if @_steps? then throw new Error "steps cannot be reassigned"
|
||||
@_steps = steps
|
||||
|
||||
# Object Overrides #############################################################################
|
||||
|
||||
toString: ->
|
||||
result = ["To Make:"]
|
||||
@_want.each (stack)->
|
||||
result.push " #{stack}"
|
||||
toString: (options={})->
|
||||
options.full ?= false
|
||||
|
||||
result.push "When you already have:"
|
||||
@_have.each (stack)->
|
||||
result.push " #{stack}"
|
||||
if options.full
|
||||
b = new StringBuilder
|
||||
b.line "CraftingPlan<", @_id, ">"
|
||||
b.indent()
|
||||
b.line "have: ", @_have.toString full:true
|
||||
b.line "want: ", @_want.toString full:true
|
||||
b.line "need: ", @_need.toString full:true
|
||||
b.line "steps:"
|
||||
b.indent()
|
||||
b.loop @_steps, delimiter:"\n", onEach:(b, step)->
|
||||
b.push step.count, " × ", step.recipe.toString(full:true)
|
||||
b.line()
|
||||
b.outdent()
|
||||
b.line "make: ", @_make.toString full:true
|
||||
b.outdent()
|
||||
|
||||
result.push "Start with:"
|
||||
if @_need?
|
||||
@_need.each (stack)->
|
||||
result.push " #{stack}"
|
||||
|
||||
result.push "Use these recipes:"
|
||||
for step in @_steps
|
||||
result.push " #{step}"
|
||||
|
||||
result.push "To produce:"
|
||||
if @_made?
|
||||
@_made.each (stack)->
|
||||
result.push " #{stack}"
|
||||
|
||||
if _.keys(@_rawScores).length > 0
|
||||
result.push "Scores:"
|
||||
for criteria, score of @_rawScores
|
||||
if @_scores[criteria]?
|
||||
result.push " #{criteria}: #{score} (#{@_scores[criteria]})"
|
||||
return b.toString()
|
||||
else
|
||||
result.push " #{criteria}: #{score}"
|
||||
|
||||
return result.join '\n'
|
||||
return "CraftingPlan:#{@_make.toString(full:true)}<#{@_id}>"
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_executeStep: (step)->
|
||||
step.multiplier += 1
|
||||
recipe = step.recipe
|
||||
_computeResources: ->
|
||||
need = new Inventory @_want
|
||||
make = new Inventory @_have
|
||||
steps = @_steps[..].reverse()
|
||||
|
||||
for stack in recipe.input
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
for step in steps
|
||||
step.count = 0
|
||||
|
||||
available = @_made.quantityOf qualifiedSlug
|
||||
required = recipe.getQuantityRequired stack.itemSlug
|
||||
consumed = Math.min required, available
|
||||
deficit = required - consumed
|
||||
for productStack in step.recipe.allProducts
|
||||
continue unless need.contains productStack.item
|
||||
productCount = Math.ceil need.getQuantity(productStack.item) / productStack.quantity
|
||||
step.count = Math.max step.count, productCount
|
||||
|
||||
@_made.remove qualifiedSlug, consumed
|
||||
@_need.add qualifiedSlug, deficit
|
||||
continue unless step.count > 0
|
||||
|
||||
for stack in recipe.output
|
||||
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
|
||||
for itemId, item of step.recipe.inputs
|
||||
amountNeeded = step.count * step.recipe.computeQuantityRequired item
|
||||
amountAvailable = make.getQuantity item
|
||||
amountUsed = Math.min amountAvailable, amountNeeded
|
||||
amountMissing = amountNeeded - amountUsed
|
||||
|
||||
deficit = @_need.quantityOf qualifiedSlug
|
||||
created = recipe.getQuantityProduced stack.itemSlug
|
||||
replenished = Math.min deficit, created
|
||||
surplus = created - replenished
|
||||
make.remove item, amountUsed
|
||||
need.add item, amountMissing
|
||||
|
||||
@_made.add qualifiedSlug, surplus
|
||||
@_need.remove qualifiedSlug, replenished
|
||||
for productStack in step.recipe.allProducts
|
||||
amountCreated = step.count * productStack.quantity
|
||||
amountNeeded = need.getQuantity productStack.item
|
||||
amountFulfilled = Math.min amountNeeded, amountCreated
|
||||
amountSurplus = amountCreated - amountFulfilled
|
||||
|
||||
_numberSteps: ->
|
||||
for step, i in @_steps
|
||||
step.number = i + 1
|
||||
need.remove productStack.item, amountFulfilled
|
||||
make.add productStack.item, amountSurplus
|
||||
|
||||
_pruneEmptySteps: ->
|
||||
index = 0
|
||||
while index < @_steps.length
|
||||
step = @_steps[index]
|
||||
if step.multiplier is 0
|
||||
@_steps.splice index, 1
|
||||
else
|
||||
index++
|
||||
make.merge @_want
|
||||
|
||||
@_make = make
|
||||
@_need = need
|
||||
|
||||
_consolidateSteps: ->
|
||||
steps = []
|
||||
stepsByRecipeId = {}
|
||||
|
||||
for step in @_steps
|
||||
priorStep = stepsByRecipeId[step.recipe.id]
|
||||
if priorStep?
|
||||
priorStep.count += step.count
|
||||
else if step.count > 0
|
||||
steps.push step
|
||||
stepsByRecipeId[step.recipe.id] = step
|
||||
|
||||
@_steps = steps
|
||||
|
||||
@@ -6,118 +6,94 @@
|
||||
#
|
||||
|
||||
CraftingPlan = require './crafting_plan'
|
||||
fixtures = require './fixtures.test'
|
||||
ItemSlug = require '../game/item_slug'
|
||||
CraftingPlanStep = require './crafting_plan_step'
|
||||
Inventory = require './inventory'
|
||||
fixtures = require './fixtures'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'crafting_plan.coffee', ->
|
||||
describe "CraftingPlan", ->
|
||||
|
||||
it 'requires wanted item if gatherable', ->
|
||||
plans = fixtures.makePlans [1, 'test__coal']
|
||||
plans.length.should.equal 1
|
||||
beforeEach ->
|
||||
@mod = fixtures.createMod()
|
||||
@want = new Inventory
|
||||
@have = new Inventory
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal'
|
||||
plan.made.unparse().should.equal 'coal'
|
||||
describe "with a single step plan", ->
|
||||
|
||||
it 'can compute a single item with one single step plan', ->
|
||||
plans = fixtures.makePlans [1, 'test__charcoal']
|
||||
plans.length.should.equal 1
|
||||
beforeEach ->
|
||||
@oakPlank = fixtures.configureOakPlank @mod
|
||||
@want.add @oakPlank, 11
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal:8.oak_wood'
|
||||
plan.made.unparse().should.equal '8.charcoal'
|
||||
(s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
|
||||
|
||||
it 'can compute a large quantity of a single item with one single step plan', ->
|
||||
plans = fixtures.makePlans [15, 'test__charcoal']
|
||||
plans.length.should.equal 1
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal '2.coal:16.oak_wood'
|
||||
plan.made.unparse().should.equal '16.charcoal'
|
||||
(s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
|
||||
|
||||
it 'can compute a single item with multiple plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__iron_ingot']
|
||||
plans.length.should.equal 2
|
||||
|
||||
plan = plans[0]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal:8.iron_ore:8.oak_wood'
|
||||
plan.made.unparse().should.equal '7.charcoal:8.iron_ingot'
|
||||
(s.toString() for s in plan.steps).should.eql [
|
||||
'1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal'
|
||||
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
@plan = new CraftingPlan want:@want, steps:[
|
||||
new CraftingPlanStep @oakPlank.firstRecipe
|
||||
]
|
||||
|
||||
plan = plans[1]
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.equal 'coal:8.iron_ore'
|
||||
plan.made.unparse().should.equal '8.iron_ingot'
|
||||
(s.toString() for s in plan.steps).should.eql [
|
||||
'1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
it "correctly computes the step counts", ->
|
||||
@plan.steps[0].count.should.equal 3
|
||||
|
||||
it "correctly determines the inputs needed", ->
|
||||
@plan.need.toString(full:true).should.equal "3 Oak Wood"
|
||||
|
||||
it "correctly computes the products created", ->
|
||||
@plan.make.toString(full:true).should.equal "12 Oak Planks"
|
||||
|
||||
describe "with a multi-step plan", ->
|
||||
|
||||
beforeEach ->
|
||||
@ironIngot = fixtures.configureIronIngot @mod
|
||||
@ironSword = fixtures.configureIronSword @mod
|
||||
@oakPlank = fixtures.configureOakPlank @mod
|
||||
@stick = fixtures.configureStick @mod
|
||||
|
||||
@want.add @ironSword, 20
|
||||
|
||||
@plan = new CraftingPlan want:@want, steps:[
|
||||
new CraftingPlanStep @oakPlank.firstRecipe
|
||||
new CraftingPlanStep @stick.firstRecipe
|
||||
new CraftingPlanStep @ironIngot.firstRecipe
|
||||
new CraftingPlanStep @ironSword.firstRecipe
|
||||
]
|
||||
|
||||
it 'uses the correct amount of passthrough items', ->
|
||||
plans = fixtures.makePlans [10, 'test__split_oak_wood'], [10, 'test__split_spruce_wood']
|
||||
plan = plans[2]
|
||||
plan.computeRequired()
|
||||
it "correctly computes the step counts", ->
|
||||
@plan.steps[0].count.should.equal 3
|
||||
@plan.steps[1].count.should.equal 5
|
||||
@plan.steps[2].count.should.equal 5
|
||||
@plan.steps[3].count.should.equal 20
|
||||
|
||||
plan.need.unparse().should.equal 'coal:8.iron_ore:6.oak_wood:5.spruce_wood'
|
||||
plan.made.unparse().should.equal '4.iron_ingot:maul:2.oak_planks:10.split_oak_wood:10.split_spruce_wood:stick'
|
||||
(s.toString() for s in plan.steps).should.eql [
|
||||
'1x test__oak_wood>... .0. ...>>4 test__oak_planks'
|
||||
'1x test__oak_planks>.0. .0. ...>>4 test__stick'
|
||||
'1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
'1x test__iron_ingot,test__stick>001 001 ..1>test__crafting_table>test__maul'
|
||||
'5x test__spruce_wood,test__maul>.1. .0. ...>>2 test__split_spruce_wood,test__maul'
|
||||
'5x test__oak_wood,test__maul>.1. .0. ...>>2 test__split_oak_wood,test__maul'
|
||||
it "correctly determines the inputs needed", ->
|
||||
@plan.need.toString(full:true).should.equal "5 Coal, 40 Iron Ore, 3 Oak Wood"
|
||||
|
||||
it "correctly computes the products created", ->
|
||||
@plan.make.toString(full:true).should.equal "20 Iron Sword, 2 Oak Planks"
|
||||
|
||||
describe "with a plan which recycles some items", ->
|
||||
|
||||
beforeEach ->
|
||||
@bucket = fixtures.configureBucket @mod
|
||||
@cake = fixtures.configureCake @mod
|
||||
@ironIngot = fixtures.configureIronIngot @mod
|
||||
@milkBucket = fixtures.configureMilkBucket @mod
|
||||
@sugar = fixtures.configureSugar @mod
|
||||
|
||||
@want.add @cake, 2
|
||||
|
||||
@plan = new CraftingPlan want:@want, steps:[
|
||||
new CraftingPlanStep @ironIngot.firstRecipe
|
||||
new CraftingPlanStep @bucket.firstRecipe
|
||||
new CraftingPlanStep @milkBucket.firstRecipe
|
||||
new CraftingPlanStep @sugar.firstRecipe
|
||||
new CraftingPlanStep @cake.firstRecipe
|
||||
]
|
||||
|
||||
it 'can compute multiple items with multiple plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword']
|
||||
plans.length.should.equal 4
|
||||
it "correctly computes the step counts", ->
|
||||
@plan.steps.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
|
||||
|
||||
for plan in plans
|
||||
plan.computeRequired()
|
||||
plan.need.unparse().should.match /16.copper_ore.*8.iron_ore/
|
||||
plan.made.unparse().should.match /copper_block.*:iron_sword/
|
||||
it "correctly determines the inputs needed", ->
|
||||
@plan.need.toString(full:true).should.equal "2 Egg, 6 Milk, 4 Sugar Cane, 6 Wheat"
|
||||
|
||||
plan = plans[0]
|
||||
plan.need.unparse().should.match /3.coal.*9.oak_wood/
|
||||
plan.made.unparse().should.match /7.charcoal/
|
||||
"#{plan.steps[3]}".should.equal \
|
||||
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[4]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 7
|
||||
|
||||
plan = plans[1]
|
||||
plan.need.unparse().should.match /3.coal.*:oak_wood/
|
||||
plan.made.unparse().should.not.match /charcoal/
|
||||
"#{plan.steps[2]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[3]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 6
|
||||
|
||||
plan = plans[2]
|
||||
plan.need.unparse().should.match /^coal.*9.oak_wood/
|
||||
plan.made.unparse().should.match /5.charcoal/
|
||||
"#{plan.steps[3]}".should.equal \
|
||||
'1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[4]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 7
|
||||
|
||||
plan = plans[3]
|
||||
plan.need.unparse().should.match /2.coal.*9.oak_wood/
|
||||
plan.made.unparse().should.match /6.charcoal/
|
||||
"#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
|
||||
"#{plan.steps[4]}".should.equal \
|
||||
'2x 8 test__copper_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__copper_ingot'
|
||||
plan.steps.length.should.equal 7
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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}>"
|
||||
@@ -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
|
||||
@@ -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}>"
|
||||
@@ -5,123 +5,88 @@
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingNode = require './crafting_node'
|
||||
CraftingPlan = require './crafting_plan'
|
||||
CraftingStep = require './crafting_step'
|
||||
Inventory = require '../game/inventory'
|
||||
CraftingPlan = require "./crafting_plan"
|
||||
CraftingPlanStep = require "./crafting_plan_step"
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class PlanBuilder
|
||||
|
||||
constructor: (rootNode, modPack, options={})->
|
||||
if not rootNode? then throw new Error 'rootNode is required'
|
||||
if not modPack? then throw new Error 'modPack is required'
|
||||
constructor: (evaluator)->
|
||||
@evaluator = evaluator
|
||||
|
||||
@want = options.want
|
||||
@have = options.have
|
||||
|
||||
@_choiceNodes = []
|
||||
@_complete = false
|
||||
@_maxPlanCount = c.limits.maximumPlanCount
|
||||
@_modPack = modPack
|
||||
@_plans = []
|
||||
@_rootNode = rootNode
|
||||
|
||||
@_isolateChoiceNodes()
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
producePlans: (maxPlans=null)->
|
||||
maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE
|
||||
|
||||
if @plans.length >= @_maxPlanCount
|
||||
@_complete = true
|
||||
|
||||
while not @complete and (@plans.length < maxPlans)
|
||||
plan = @_captureCurrentPlan()
|
||||
if plan?
|
||||
@plans.push plan
|
||||
|
||||
@_incrementChoiceNodes()
|
||||
|
||||
return @_plans
|
||||
|
||||
# Property Methods #############################################################################
|
||||
# Properties ###################################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
complete:
|
||||
get: -> @_complete
|
||||
evaluator:
|
||||
get: -> return @_evaluator
|
||||
set: (evaluator)->
|
||||
if not evaluator? then throw new Error "evaluator is required"
|
||||
if @_evaluator is evaluator then return
|
||||
if @_evaluator? then throw new Error "evaluator cannot be reassigned"
|
||||
@_evaluator = evaluator
|
||||
|
||||
have:
|
||||
get: -> @_have
|
||||
set: (have)-> @_have = have or new Inventory
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
maxPlanCount:
|
||||
get: -> @_maxPlanCount
|
||||
set: (value)-> @_maxPlanCount = value
|
||||
createPlan: (want, have)->
|
||||
@_alreadyMaking = {}
|
||||
@_recipesInUse = {}
|
||||
|
||||
plans:
|
||||
get: -> @_plans
|
||||
stepList = []
|
||||
for itemId, stack of want.stacks
|
||||
steps = @_findStepsForItem stack.item, stack.quantity
|
||||
return null unless steps?
|
||||
|
||||
want:
|
||||
get: -> @_want
|
||||
set: (want)-> @_want = want or new Inventory
|
||||
stepList.push steps
|
||||
|
||||
plan = new CraftingPlan want:want, have:have, steps:_.flatten(stepList)
|
||||
return plan
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_captureCurrentPlan: ->
|
||||
toVisit = [@_rootNode]
|
||||
stepNodes = []
|
||||
_findStepsForItem: (item, quantity=1)->
|
||||
steps = null
|
||||
|
||||
while toVisit.length > 0
|
||||
node = toVisit.shift()
|
||||
|
||||
if node.TYPE is CraftingNode::TYPES.INVENTORY
|
||||
toVisit.push(c) for c in node.children
|
||||
else if node.TYPE is CraftingNode::TYPES.ITEM
|
||||
toVisit.push node.children[0] if node.children.length > 0
|
||||
else if node.TYPE is CraftingNode::TYPES.RECIPE
|
||||
stepNodes.push node
|
||||
toVisit.push(c) for c in node.children
|
||||
if not @_alreadyMaking[item.id]
|
||||
@_alreadyMaking[item.id] = true
|
||||
|
||||
recipes = @_evaluator.getOrderedRecipes item, quantity
|
||||
if recipes.length is 0
|
||||
steps = []
|
||||
seenRecipes = {}
|
||||
index = stepNodes.length - 1
|
||||
while index >= 0
|
||||
node = stepNodes[index]
|
||||
index -= 1
|
||||
return null unless node.valid and node.complete
|
||||
else
|
||||
for recipe in recipes
|
||||
continue if @_recipesInUse[recipe.id]?
|
||||
|
||||
recipeSlug = node.recipe.slug
|
||||
continue if seenRecipes[recipeSlug]?
|
||||
steps = @_findStepsForRecipe recipe, quantity
|
||||
break if steps?
|
||||
|
||||
seenRecipes[recipeSlug] = true
|
||||
steps.push new CraftingStep node.recipe, @_modPack
|
||||
delete @_alreadyMaking[item.id]
|
||||
|
||||
plan = new CraftingPlan @_modPack, @_want, @_have, steps
|
||||
return plan
|
||||
return steps
|
||||
|
||||
_incrementChoiceNodes: ->
|
||||
if @_choiceNodes.length is 0
|
||||
@_complete = true
|
||||
return
|
||||
_findStepsForRecipe: (recipe, quantity=1)->
|
||||
steps = null
|
||||
|
||||
index = @_choiceNodes.length - 1
|
||||
while true
|
||||
if index is -1
|
||||
@_complete = true
|
||||
return
|
||||
if not @_recipesInUse[recipe.id]
|
||||
@_recipesInUse[recipe.id] = true
|
||||
|
||||
node = @_choiceNodes[index]
|
||||
node.rotateChildren()
|
||||
invalidRecipe = false
|
||||
for itemId, item of recipe.inputs
|
||||
inputSteps = @_findStepsForItem item, quantity * recipe.computeQuantityRequired(item)
|
||||
if not inputSteps?
|
||||
invalidRecipe = true
|
||||
break
|
||||
|
||||
return unless node.rotations % node.children.length is 0
|
||||
index -= 1
|
||||
steps ?= []
|
||||
for step in inputSteps
|
||||
steps.push step
|
||||
|
||||
_isolateChoiceNodes: ->
|
||||
@_rootNode.acceptVisitor
|
||||
onEnterItemNode: (node)=>
|
||||
if node.children.length > 1
|
||||
@_choiceNodes.push node
|
||||
if invalidRecipe
|
||||
steps = null
|
||||
else
|
||||
steps.push new CraftingPlanStep recipe
|
||||
|
||||
delete @_recipesInUse[recipe.id]
|
||||
|
||||
return steps
|
||||
|
||||
@@ -5,39 +5,154 @@
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
fixtures = require './fixtures.test'
|
||||
fixtures = require './fixtures'
|
||||
Inventory = require './inventory'
|
||||
PlanBuilder = require './plan_builder'
|
||||
ResourcesEvaluator = require './resources_evaluator'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'plan_builder.coffee', ->
|
||||
describe.only "PlanBuilder", ->
|
||||
|
||||
printPlan = (plan)->
|
||||
return '' unless plan?
|
||||
return ((s.recipe.slug.replace(/^.*>.*>/, '') for s in plan.steps;;)).join ' > '
|
||||
beforeEach ->
|
||||
@planner = new PlanBuilder new ResourcesEvaluator
|
||||
@mod = fixtures.createMod()
|
||||
@want = new Inventory
|
||||
@have = new Inventory
|
||||
|
||||
it 'generates an empty plan for a gatherable item', ->
|
||||
plans = fixtures.makePlans [1, 'test__oak_wood']
|
||||
describe "for a gatherable item, creates a plan that", ->
|
||||
|
||||
plans.length.should.equal 1
|
||||
plans[0].length.should.equal 0
|
||||
beforeEach ->
|
||||
@oakWood = fixtures.configureOakWood @mod
|
||||
@want.add @oakWood, 2
|
||||
@plan = @planner.createPlan @want
|
||||
|
||||
it 'can find a multi-step plan', ->
|
||||
plans = fixtures.makePlans [1, 'test__lever']
|
||||
it "has no steps at all", ->
|
||||
@plan.steps.length.should.equal 0
|
||||
|
||||
printPlan(plans[0]).should.equal '4 test__oak_planks > 4 test__stick > test__lever'
|
||||
plans.length.should.equal 1
|
||||
it "demands the item itself as the input", ->
|
||||
@plan.need.toString(full:true).should.equal "2 Oak Wood"
|
||||
|
||||
it 'can find multiple plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__iron_ingot']
|
||||
it "produces the item as the result", ->
|
||||
@plan.make.toString(full:true).should.equal "2 Oak Wood"
|
||||
|
||||
printPlan(plans[0]).should.equal '8 test__charcoal > 8 test__iron_ingot'
|
||||
printPlan(plans[1]).should.equal '8 test__iron_ingot'
|
||||
plans.length.should.equal 2
|
||||
describe "for a single item with a one-step recipe, it creates a plan that", ->
|
||||
|
||||
it 'ignores invalid plans', ->
|
||||
plans = fixtures.makePlans [1, 'test__copper_block']
|
||||
beforeEach ->
|
||||
@oakPlank = fixtures.configureOakPlank @mod
|
||||
@want.add @oakPlank, 63
|
||||
@plan = @planner.createPlan @want
|
||||
|
||||
printPlan(plans[0]).should.equal '8 test__copper_ingot > test__copper_block'
|
||||
printPlan(plans[1]).should.equal '8 test__charcoal > 8 test__copper_ingot > test__copper_block'
|
||||
plans.length.should.equal 2
|
||||
it "has the recipe as the only step", ->
|
||||
@plan.steps.length.should.equal 1
|
||||
@plan.steps[0].recipe.output.item.displayName.should.equal "Oak Planks"
|
||||
|
||||
it "demands the right inputs", ->
|
||||
@plan.need.toString(full:true).should.equal "16 Oak Wood"
|
||||
|
||||
it "produces the right results", ->
|
||||
@plan.make.toString(full:true).should.equal "64 Oak Planks"
|
||||
|
||||
describe "for a single item with a complex recipe, it creates a plan that", ->
|
||||
|
||||
beforeEach ->
|
||||
@ironSword = fixtures.configureIronSword @mod
|
||||
@want.add @ironSword, 2
|
||||
@plan = @planner.createPlan @want
|
||||
|
||||
it "has the right steps", ->
|
||||
@plan.steps.length.should.equal 4
|
||||
@plan.steps[0].recipe.output.item.displayName.should.equal "Iron Ingot"
|
||||
@plan.steps[1].recipe.output.item.displayName.should.equal "Oak Planks"
|
||||
@plan.steps[2].recipe.output.item.displayName.should.equal "Stick"
|
||||
@plan.steps[3].recipe.output.item.displayName.should.equal "Iron Sword"
|
||||
|
||||
it "demands the right inputs", ->
|
||||
@plan.need.toString(full:true).should.equal "1 Coal, 8 Iron Ore, 1 Oak Wood"
|
||||
|
||||
it "produces the right results", ->
|
||||
@plan.make.toString(full:true).should.equal "4 Iron Ingot, 2 Iron Sword, 2 Oak Planks, 2 Stick"
|
||||
|
||||
describe "for multiple items with complex recipes, it creates a plan that", ->
|
||||
|
||||
beforeEach ->
|
||||
@ironSword = fixtures.configureIronSword @mod
|
||||
@ironShovel = fixtures.configureIronShovel @mod
|
||||
@want.add @ironSword, 3
|
||||
@want.add @ironShovel, 3
|
||||
@plan = @planner.createPlan @want
|
||||
|
||||
it "has consolidated the common steps", ->
|
||||
(step.recipe.output.item.displayName for step in @plan.steps).should.eql [
|
||||
"Iron Ingot", "Oak Planks", "Stick", "Iron Sword", "Iron Shovel"
|
||||
]
|
||||
|
||||
it "requires the correct repetitions for each step", ->
|
||||
(step.count for step in @plan.steps).should.eql [ 2, 2, 3, 3, 3 ]
|
||||
|
||||
it "demands the right inputs", ->
|
||||
@plan.need.toString(full:true).should.equal "2 Coal, 16 Iron Ore, 2 Oak Wood"
|
||||
|
||||
it "produces the right results", ->
|
||||
@plan.make.toString(full:true).should.equal(
|
||||
"7 Iron Ingot, 3 Iron Shovel, 3 Iron Sword, 2 Oak Planks, 3 Stick"
|
||||
)
|
||||
|
||||
describe "for an item which has a recursive recipe, it creates a plan that", ->
|
||||
|
||||
beforeEach ->
|
||||
@ironBlock = fixtures.configureIronBlock @mod
|
||||
@want.add @ironBlock, 4
|
||||
@plan = @planner.createPlan @want
|
||||
|
||||
it "avoids the recursive options", ->
|
||||
(step.recipe.output.item.displayName for step in @plan.steps).should.eql [
|
||||
"Iron Ingot", "Iron Block"
|
||||
]
|
||||
|
||||
it "requires the correct repetitions for each step", ->
|
||||
(step.count for step in @plan.steps).should.eql [ 5, 4 ]
|
||||
|
||||
it "demands the right inputs", ->
|
||||
@plan.need.toString(full:true).should.equal "5 Coal, 40 Iron Ore"
|
||||
|
||||
it "produces the right results", ->
|
||||
@plan.make.toString(full:true).should.equal(
|
||||
"4 Iron Block, 4 Iron Ingot"
|
||||
)
|
||||
|
||||
describe "when making enormous quantities of something simple", ->
|
||||
|
||||
beforeEach ->
|
||||
@oakPlank = fixtures.configureOakPlank @mod
|
||||
@want.add @oakPlank, Number.MAX_VALUE / 2
|
||||
@start = Date.now()
|
||||
@plan = @planner.createPlan @want
|
||||
@duration = Date.now() - @start
|
||||
|
||||
it "doesn't take forever to create the plan", ->
|
||||
@duration.should.be.lessThan 10
|
||||
|
||||
describe "when making an item which can be made with and without a tool, for a", ->
|
||||
|
||||
beforeEach ->
|
||||
@oakPlank = fixtures.configureOakPlank @mod
|
||||
@saw = fixtures.configureSaw @mod
|
||||
|
||||
describe "small batch, the plan", ->
|
||||
|
||||
beforeEach ->
|
||||
@want.add @oakPlank, 2
|
||||
@plan = @planner.createPlan @want
|
||||
|
||||
it "avoids the expensive tool", ->
|
||||
(step.recipe.needsTools for step in @plan.steps).should.eql [ false ]
|
||||
|
||||
describe "large batch, the plan", ->
|
||||
|
||||
beforeEach ->
|
||||
@want.add @oakPlank, 2048
|
||||
@plan = @planner.createPlan @want
|
||||
|
||||
it "uses the tool for efficiency", ->
|
||||
(step.recipe.needsTools for step in @plan.steps).should.eql [ true ]
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - plan_evaluator.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
module.exports = class PlanEvaluator
|
||||
|
||||
@::CRITERIA =
|
||||
FEWEST_STEPS: 'fewest steps'
|
||||
LEAST_MATERIALS: 'least materials'
|
||||
|
||||
constructor: (plans)->
|
||||
if not plans? then throw new Error 'plans is required'
|
||||
|
||||
@_plans = plans
|
||||
@_lastScored = -1
|
||||
@_normalized = false
|
||||
|
||||
# Public Methods ###############################################################################
|
||||
|
||||
findBestPlan: (mainCriteria)->
|
||||
criteriaList = [mainCriteria].concat (criteria for key, criteria of @CRITERIA when criteria isnt mainCriteria)
|
||||
@_plans.sort (a, b)->
|
||||
for criteria in criteriaList
|
||||
scoreA = a.getScore criteria
|
||||
scoreB = b.getScore criteria
|
||||
|
||||
if scoreA isnt scoreB
|
||||
return if scoreA > scoreB then -1 else +1
|
||||
|
||||
return 0
|
||||
|
||||
return @_plans[0]
|
||||
|
||||
scorePlans: (count)->
|
||||
count ?= @_plans.length
|
||||
return if @complete
|
||||
|
||||
maxPlanIndex = Math.min @_lastScored + count, @_plans.length - 1
|
||||
for i in [(@_lastScored + 1)..maxPlanIndex]
|
||||
@_plans[i].computeRequired()
|
||||
@_scorePlan @_plans[i]
|
||||
@_lastScored = i
|
||||
|
||||
if @complete then @_normalizeScores()
|
||||
|
||||
return @complete
|
||||
|
||||
# Property Methods #############################################################################
|
||||
|
||||
Object.defineProperties @prototype,
|
||||
|
||||
complete:
|
||||
get: -> @_lastScored is @_plans.length - 1
|
||||
|
||||
lastScored:
|
||||
get: -> @_lastScored
|
||||
|
||||
# Private Methods ##############################################################################
|
||||
|
||||
_computeFewestStepsScore: (plan)->
|
||||
total = 0
|
||||
for step in plan.steps
|
||||
total += step.multiplier
|
||||
plan.setRawScore @CRITERIA.FEWEST_STEPS, total
|
||||
|
||||
_computeLeastMaterialsScore: (plan)->
|
||||
total = 0
|
||||
plan.need.each (stack)->
|
||||
total += stack.quantity
|
||||
plan.setRawScore @CRITERIA.LEAST_MATERIALS, total
|
||||
|
||||
_normalizeScores: ->
|
||||
for key, criteria of @CRITERIA
|
||||
maxScore = 0
|
||||
minScore = Number.MAX_VALUE
|
||||
|
||||
for plan in @_plans
|
||||
continue unless plan.hasRawScore criteria
|
||||
maxScore = Math.max maxScore, plan.getRawScore(criteria)
|
||||
minScore = Math.min minScore, plan.getRawScore(criteria)
|
||||
|
||||
adjustedMaxScore = maxScore - minScore
|
||||
for plan in @_plans
|
||||
continue unless plan.hasRawScore criteria
|
||||
if adjustedMaxScore > 0
|
||||
adjustedRawScore = plan.getRawScore(criteria) - minScore
|
||||
plan.setScore criteria, 1.0 - (1.0 * adjustedRawScore / adjustedMaxScore)
|
||||
else
|
||||
plan.setScore criteria, 1.0
|
||||
|
||||
_scorePlan: (plan)->
|
||||
@_computeFewestStepsScore plan
|
||||
@_computeLeastMaterialsScore plan
|
||||
@@ -1,80 +0,0 @@
|
||||
#
|
||||
# Crafting Guide - plan_evaluator.test.coffee
|
||||
#
|
||||
# Copyright © 2014-2016 by Redwood Labs
|
||||
# All rights reserved.
|
||||
#
|
||||
|
||||
CraftingPlan = require './crafting_plan'
|
||||
fixtures = require './fixtures.test'
|
||||
Inventory = require '../game/inventory'
|
||||
PlanEvaluator = require './plan_evaluator'
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
criteria = evaluator = planA = planB = planC = plans = wanted = null
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
describe 'plan_evaluator.coffee', ->
|
||||
|
||||
beforeEach ->
|
||||
criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS
|
||||
|
||||
describe 'findBestPlan', ->
|
||||
|
||||
it 'can find the shortest of two options', ->
|
||||
evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block']
|
||||
evaluator.scorePlans()
|
||||
|
||||
bestPlan = evaluator.findBestPlan criteria
|
||||
bestPlan.getScore(criteria).should.equal 1.0
|
||||
|
||||
it 'can find the shortest of many options', ->
|
||||
evaluator = new PlanEvaluator fixtures.makePlans [1, 'test__copper_block'], [1, 'test__iron_sword']
|
||||
evaluator.scorePlans()
|
||||
|
||||
bestPlan = evaluator.findBestPlan criteria
|
||||
bestPlan.getScore(criteria).should.equal 1.0
|
||||
|
||||
it 'can break ties using the non-primary criteria', ->
|
||||
evaluator = new PlanEvaluator fixtures.makePlans [2, 'test__iron_ingot'], [1, 'test__charcoal']
|
||||
evaluator.scorePlans()
|
||||
|
||||
bestPlan = evaluator.findBestPlan criteria
|
||||
bestPlan.getScore(criteria).should.equal 1.0
|
||||
|
||||
(p.getRawScore('fewest steps') for p in evaluator._plans).should.eql [2, 2]
|
||||
(p.getRawScore('least materials') for p in evaluator._plans).should.eql [17, 18]
|
||||
|
||||
describe '_normalizeScores', ->
|
||||
|
||||
beforeEach ->
|
||||
modPack = fixtures.makeModPack()
|
||||
wanted = new Inventory modPack:modPack
|
||||
have = new Inventory modPack:modPack
|
||||
planA = new CraftingPlan modPack, wanted, have, []
|
||||
planB = new CraftingPlan modPack, wanted, have, []
|
||||
planC = new CraftingPlan modPack, wanted, have, []
|
||||
plans = [planA, planB, planC]
|
||||
evaluator = new PlanEvaluator plans
|
||||
criteria = PlanEvaluator::CRITERIA.FEWEST_STEPS
|
||||
|
||||
it 'does not set scores when none have been evaluated', ->
|
||||
evaluator._normalizeScores()
|
||||
(plan.hasRawScore(criteria) for plan in plans).should.eql [false, false, false]
|
||||
|
||||
it 'assigns a 1.0 when all plans equal', ->
|
||||
for plan in plans
|
||||
plan.setRawScore criteria, 5
|
||||
|
||||
evaluator._normalizeScores()
|
||||
(plan.getScore(criteria) for plan in plans).should.eql [1.0, 1.0, 1.0]
|
||||
|
||||
it 'computes correct scores from raw scores', ->
|
||||
planA.setRawScore criteria, 7
|
||||
planB.setRawScore criteria, 8
|
||||
planC.setRawScore criteria, 9
|
||||
|
||||
evaluator._normalizeScores()
|
||||
(plan.getScore(criteria) for plan in plans).should.eql [1.0, 0.5, 0.0]
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -157,7 +157,7 @@ module.exports = class ModVersion extends BaseModel
|
||||
for recipe in primaryRecipes
|
||||
result.push recipe
|
||||
|
||||
if not options.onlyPrimary
|
||||
if not options.onlyPrimary and result.length is 0
|
||||
for recipe in otherRecipes
|
||||
result.push recipe
|
||||
|
||||
|
||||
@@ -90,12 +90,17 @@ describe 'mod_version.coffee', ->
|
||||
recipe:; input: Cake Slice; pattern: 000 000 000; onlyIf: item Cake Slice
|
||||
item: Bucket
|
||||
recipe:; input: Iron Ingot; pattern: ... 0.0 .0.
|
||||
recipe:; input: Copper Ingot; pattern: ... 0.0 .0.; extras: Copper Nugget
|
||||
"""
|
||||
|
||||
it 'finds all recipes which list item as output', ->
|
||||
recipes = modVersion.findRecipes ItemSlug.slugify('test__bucket')
|
||||
(r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'cake', 'cake']
|
||||
(r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'bucket']
|
||||
|
||||
it 'skip recipes whose conditions are not met', ->
|
||||
recipes = modVersion.findRecipes ItemSlug.slugify('test__cake')
|
||||
recipes.length.should.equal 2
|
||||
|
||||
it 'finds recipes for items which are only ever extras', ->
|
||||
recipes = modVersion.findRecipes ItemSlug.slugify('test__copper_nugget')
|
||||
recipes.length.should.equal 1
|
||||
|
||||
Reference in New Issue
Block a user