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