This commit is contained in:
Andrew Miner
2015-10-31 13:21:14 -07:00
parent 74e95e614b
commit 4e95ab998f
28 changed files with 948 additions and 471 deletions
+4
View File
@@ -94,6 +94,10 @@ GitHub.file.itemDescription = {}
GitHub.file.itemDescription.fileName = _.template "item.cg" GitHub.file.itemDescription.fileName = _.template "item.cg"
GitHub.file.itemDescription.path = _.template "/data/<%= modSlug %>/items/<%= itemSlug %>" GitHub.file.itemDescription.path = _.template "/data/<%= modSlug %>/items/<%= itemSlug %>"
exports.Limits = Limits = {}
Limits.maximumGraphSize = 10000
Limits.maximumPlanCount = 10000
exports.Login = Login = {} exports.Login = Login = {}
Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" + Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" +
"?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>" "?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>"
@@ -32,45 +32,39 @@ module.exports = class CraftPageController extends PageController
@modPack = options.modPack @modPack = options.modPack
@storage = options.storage @storage = options.storage
@model.plan.on Event.change, => @tryRefresh() @model.craftsman.on Event.change, => @tryRefresh()
# Event Methods ################################################################################ # Event Methods ################################################################################
onCraftTool: (itemSlug)->
# TODO: implement this
onHaveInventoryChanged: -> onHaveInventoryChanged: ->
@storage.store 'crafting-plan:have', @model.plan.have.unparse() @storage.store 'crafting-plan:have', @model.craftsman.have.unparse()
onMoveNeedToHave: (itemSlug)-> onMoveNeedToHave: (itemSlug)->
quantity = @model.plan.need.quantityOf itemSlug quantity = @model.craftsman.need.quantityOf itemSlug
@model.plan.have.add itemSlug, quantity @model.craftsman.have.add itemSlug, quantity
@onHaveInventoryChanged() @onHaveInventoryChanged()
onRemoveFromHaveInventory: (itemSlug)-> onRemoveFromHaveInventory: (itemSlug)->
@model.plan.have.remove itemSlug @model.craftsman.have.remove itemSlug
@onHaveInventoryChanged() @onHaveInventoryChanged()
onRemoveFromWant: (itemSlug)-> onRemoveFromWant: (itemSlug)->
@model.plan.want.remove itemSlug @model.craftsman.want.remove itemSlug
@onWantInventoryChange() @onWantInventoryChange()
onStopUsingTool: (itemSlug)->
# TODO: implement this
onStepComplete: (stepController)-> onStepComplete: (stepController)->
step = stepController.model step = stepController.model
for stack in step.recipe.output for stack in step.recipe.output
@model.plan.have.add stack.itemSlug, stack.quantity * step.multiplier @model.craftsman.have.add stack.itemSlug, stack.quantity * step.multiplier
@onHaveInventoryChanged() @onHaveInventoryChanged()
onWantInventoryChange: -> onWantInventoryChange: ->
text = @model.plan.want.unparse() text = @model.craftsman.want.unparse()
url = Url.crafting inventoryText:text url = Url.crafting inventoryText:text
router.navigate url router.navigate url
if @model.plan.want.isEmpty if @model.craftsman.want.isEmpty
@model.plan.have.clear() @model.craftsman.have.clear()
@onHaveInventoryChanged() @onHaveInventoryChanged()
# PageController Overrides ##################################################################### # PageController Overrides #####################################################################
@@ -89,24 +83,15 @@ module.exports = class CraftPageController extends PageController
@wantInventoryController = @addChild InventoryController, '.want .view__inventory', @wantInventoryController = @addChild InventoryController, '.want .view__inventory',
imageLoader: @imageLoader imageLoader: @imageLoader
isAcceptable: (item)=> item.isCraftable isAcceptable: (item)=> item.isCraftable
model: @model.plan.want model: @model.craftsman.want
modPack: @modPack modPack: @modPack
firstButtonType: 'remove' firstButtonType: 'remove'
@wantInventoryController.on Event.button.first, (c, s)=> @onRemoveFromWant(s) @wantInventoryController.on Event.button.first, (c, s)=> @onRemoveFromWant(s)
@wantInventoryController.on Event.change, (c)=> @onWantInventoryChange() @wantInventoryController.on Event.change, (c)=> @onWantInventoryChange()
# @toolsInUseController = @addChild InventoryController, '.tools .view__inventory',
# imageLoader: @imageLoader
# model: @model.plan.toolsInUse
# modPack: @modPack
# firstButtonType: 'down'
# secondButtonType: 'up'
# @toolsInUseController.on Event.button.first, (c, s)=> @onStopUsingTool(s)
# @toolsInUseController.of Event.button.second, (c, s)=> @onCraftTool(s)
@haveInventoryController = @addChild InventoryController, '.have .view__inventory', @haveInventoryController = @addChild InventoryController, '.have .view__inventory',
imageLoader: @imageLoader imageLoader: @imageLoader
model: @model.plan.have model: @model.craftsman.have
modPack: @modPack modPack: @modPack
firstButtonType: 'down' firstButtonType: 'down'
@haveInventoryController.on Event.button.first, (c, s)=> @haveInventoryController.on Event.button.first, (c, s)=>
@@ -116,7 +101,7 @@ module.exports = class CraftPageController extends PageController
@needInventoryController = @addChild InventoryController, '.need .view__inventory', @needInventoryController = @addChild InventoryController, '.need .view__inventory',
editable: false editable: false
imageLoader: @imageLoader imageLoader: @imageLoader
model: @model.plan.need model: null
modPack: @modPack modPack: @modPack
firstButtonType: 'up' firstButtonType: 'up'
@needInventoryController.on Event.button.first, (c, s)=> @onMoveNeedToHave(s) @needInventoryController.on Event.button.first, (c, s)=> @onMoveNeedToHave(s)
@@ -131,11 +116,12 @@ module.exports = class CraftPageController extends PageController
super super
onWillRender: -> onWillRender: ->
@model.plan.have.clear() @model.craftsman.have.clear()
@model.plan.have.parse @storage.load('crafting-plan:have') @model.craftsman.have.parse @storage.load('crafting-plan:have')
super super
refresh: -> refresh: ->
@needInventoryController.model = @model.craftsman.plan?.need
@_refreshSectionVisibility() @_refreshSectionVisibility()
@_refreshSteps() @_refreshSteps()
@adsenseController.fillAdPositions() @adsenseController.fillAdPositions()
@@ -150,10 +136,10 @@ module.exports = class CraftPageController extends PageController
# Private Methods ################################################################################ # Private Methods ################################################################################
_isStepCompletable: (controller)-> _isStepCompletable: (controller)->
return not @model.plan.want.hasAtLeast controller.model.outputItemSlug return not @model.craftsman.want.hasAtLeast controller.model.outputItemSlug
_refreshSectionVisibility: -> _refreshSectionVisibility: ->
if @model.plan.want.isEmpty if @model.craftsman.want.isEmpty
for $el in [@$toolsSection, @$ingredientsSection, @$stepsSection] for $el in [@$toolsSection, @$ingredientsSection, @$stepsSection]
@hide $el @hide $el
@show @$instructionsSection @show @$instructionsSection
@@ -163,10 +149,11 @@ module.exports = class CraftPageController extends PageController
@hide @$instructionsSection @hide @$instructionsSection
_refreshSteps: -> _refreshSteps: ->
steps = @model.craftsman.plan?.steps or []
@_stepControllers ?= [] @_stepControllers ?= []
index = 0 index = 0
for step in @model.plan.steps for step in steps
controller = @_stepControllers[index] controller = @_stepControllers[index]
if not controller? if not controller?
controller = new StepController controller = new StepController
@@ -1,95 +0,0 @@
###
Crafting Guide - crafting_table_controller.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseController = require './base_controller'
ImageLoader = require './image_loader'
MinimalRecipeController = require './minimal_recipe_controller'
_ = require 'underscore'
{Duration} = require '../constants'
{Event} = require '../constants'
########################################################################################################################
module.exports = class CraftingTableController extends BaseController
constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required'
options.templateName = 'crafting_table'
super options
@imageLoader = options.imageLoader
@modPack = options.modPack
# Event Methods ################################################################################
onNextClicked: ->
@model.stepIndex += 1
onPrevClicked: ->
@model.stepIndex -= 1
onReportProblem: ->
itemList = @model.plan.want.unparse()
toolsMessage = if @model.plan.includingTools then '(including tools)' else ''
message = "When I was on step #{@model.stepIndex + 1} of making:
\n\n#{itemList}#{toolsMessage}\n\nI noticed that...\n"
global.feedbackController.enterFeedback message
# BaseController Overrides #####################################################################
onDidRender: ->
@recipeController = @addChild MinimalRecipeController, '.view__minimal_recipe',
imageLoader: @imageLoader
modPack: @modPack
@$next = @$('.next')
@$prev = @$('.prev')
@$problemControl = @$('.problem')
@$title = @$('h2 p')
@$tool = @$('.tool p')
@$multiplier = $('<p class="multiplier"></p>')
@$('.output').append @$multiplier
@defaultTitle = @$title.html()
super
refresh: ->
@$prev.removeClass 'enabled'
@$next.removeClass 'enabled'
if @model.hasSteps
if @model.hasPrevStep then @$prev.addClass 'enabled'
if @model.hasNextStep then @$next.addClass 'enabled'
@$title.html "Step #{@model.stepIndex + 1} of #{@model.stepCount}"
else
@$title.html @defaultTitle
currentStep = @model.currentStep
@recipeController.model = currentStep?.recipe
if currentStep?.multiplier > 1
@$multiplier.html "×#{currentStep.multiplier}"
else
@$multiplier.html ''
if not (@model.hasSteps and global.feedbackController?)
@$problemControl.addClass 'hidden'
else
@$problemControl.removeClass 'hidden'
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click .next': 'onNextClicked'
'click .prev': 'onPrevClicked'
'click .problem a': 'onReportProblem'
@@ -33,7 +33,6 @@ module.exports = class InventoryController extends BaseController
constructor: (options={})-> constructor: (options={})->
if not options.imageLoader? then throw new Error 'options.imageLoader is required' if not options.imageLoader? then throw new Error 'options.imageLoader is required'
if not options.model? then throw new Error 'options.model is required'
if not options.modPack? then throw new Error 'options.modPack is required' if not options.modPack? then throw new Error 'options.modPack is required'
@imageLoader = options.imageLoader @imageLoader = options.imageLoader
@@ -57,6 +56,8 @@ module.exports = class InventoryController extends BaseController
# Event Methods ################################################################################ # Event Methods ################################################################################
onClearButtonClicked: -> onClearButtonClicked: ->
return unless @model?
@model.clear() @model.clear()
@trigger Event.clear, this @trigger Event.clear, this
@trigger Event.change, this @trigger Event.change, this
@@ -65,6 +66,8 @@ module.exports = class InventoryController extends BaseController
@trigger Event.button.first, this, stackController?.model?.itemSlug @trigger Event.button.first, this, stackController?.model?.itemSlug
onItemChosen: (itemSlug)-> onItemChosen: (itemSlug)->
return unless @model?
@model.add itemSlug, 1 @model.add itemSlug, 1
@trigger Event.add, this, itemSlug @trigger Event.add, this, itemSlug
@trigger Event.change, this @trigger Event.change, this
@@ -95,11 +98,12 @@ module.exports = class InventoryController extends BaseController
@$icon.attr 'src', @icon @$icon.attr 'src', @icon
@$title.html @title @$title.html @title
@$clearButton.disabled = @model.isEmpty
@_refreshStacks() @_refreshStacks()
if @model.isEmpty @$clearButton.disabled = @model?.isEmpty
if not @model or @model.isEmpty
@show @$emptyPlaceholder @show @$emptyPlaceholder
else else
@hide @$emptyPlaceholder @hide @$emptyPlaceholder
@@ -120,27 +124,28 @@ module.exports = class InventoryController extends BaseController
@_stackControllers ?= [] @_stackControllers ?= []
index = 0 index = 0
@model.each (stack)=> if @model?
controller = @_stackControllers[index] @model.each (stack)=>
if not controller? controller = @_stackControllers[index]
controller = new StackController if not controller?
editable: @editable controller = new StackController
firstButtonType: @firstButtonType editable: @editable
imageLoader: @imageLoader firstButtonType: @firstButtonType
model: stack imageLoader: @imageLoader
modPack: @modPack model: stack
secondButtonType: @secondButtonType modPack: @modPack
shouldEnableButton: @shouldEnableButton secondButtonType: @secondButtonType
controller.on Event.change, => @trigger Event.change, this shouldEnableButton: @shouldEnableButton
controller.on Event.button.first, (c)=> @onFirstButtonClicked(c) controller.on Event.change, => @trigger Event.change, this
controller.on Event.button.second, (c)=> @onSecondButtonClicked(c) controller.on Event.button.first, (c)=> @onFirstButtonClicked(c)
controller.render() controller.on Event.button.second, (c)=> @onSecondButtonClicked(c)
controller.render()
@_stackControllers.push controller @_stackControllers.push controller
@$itemContainer.append controller.$el @$itemContainer.append controller.$el
else else
controller.model = stack controller.model = stack
index += 1 index += 1
while @_stackControllers.length > index while @_stackControllers.length > index
@_stackControllers.pop().remove() @_stackControllers.pop().remove()
@@ -5,11 +5,11 @@ Copyright (c) 2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
_ = require 'underscore'
BaseController = require './base_controller' BaseController = require './base_controller'
{Event} = require '../constants'
InventoryController = require './inventory_controller' InventoryController = require './inventory_controller'
MinimalRecipeController = require './minimal_recipe_controller' MinimalRecipeController = require './minimal_recipe_controller'
_ = require 'underscore'
{Event} = require '../constants'
######################################################################################################################## ########################################################################################################################
@@ -56,7 +56,7 @@ module.exports = class StepController extends BaseController
return super return super
refresh: -> refresh: ->
itemDisplay = @modPack.findItemDisplay @model.outputItemSlug itemDisplay = @modPack.findItemDisplay @model.recipe.output[0].itemSlug
@$header.html "#{@model.number}. #{itemDisplay.itemName}" @$header.html "#{@model.number}. #{itemDisplay.itemName}"
@inventoryController.model = @model.inventory @inventoryController.model = @model.inventory
+11 -13
View File
@@ -5,34 +5,32 @@ Copyright (c) 2014-2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
BaseModel = require './base_model' BaseModel = require './base_model'
CraftingPlan = require './crafting_plan' Craftsman = require './crafting/craftsman'
CraftingTable = require './crafting_table' {Event} = require '../constants'
{Event} = require '../constants' Inventory = require './inventory'
Inventory = require './inventory' ModPack = require './mod_pack'
ModPack = require './mod_pack'
######################################################################################################################## ########################################################################################################################
module.exports = class CraftPage extends BaseModel module.exports = class CraftPage extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
attributes.modPack ?= new ModPack if not attributes.modPack then throw new Error 'attributes.modPack is required'
attributes.params ?= null attributes.params ?= null
attributes.plan ?= new CraftingPlan modPack:attributes.modPack attributes.craftsman ?= new Craftsman attributes.modPack
attributes.table ?= new CraftingTable plan:attributes.plan
super attributes, options super attributes, options
@modPack.on Event.change, => @_consumeParams() @modPack.on Event.change, => @_consumeParams()
@on Event.change + ':params', => @_consumeParams() @on Event.change + ':params', => @_consumeParams()
@plan.on Event.change, => @trigger Event.change, this @craftsman.on Event.change, => @trigger Event.change, this
# Private Methods ############################################################################## # Private Methods ##############################################################################
_consumeParams: -> _consumeParams: ->
return unless @params? return unless @params?
@plan.want.clear() @craftsman.want.clear()
if not @params.inventoryText? if not @params.inventoryText?
@params = null @params = null
else else
@@ -42,7 +40,7 @@ module.exports = class CraftPage extends BaseModel
inventory.each (stack)=> inventory.each (stack)=>
item = @modPack.findItem stack.itemSlug, enableAsNeeded:true item = @modPack.findItem stack.itemSlug, enableAsNeeded:true
return unless item? and item.isCraftable return unless item? and item.isCraftable
@plan.want.add stack.itemSlug, stack.quantity @craftsman.want.add stack.itemSlug, stack.quantity
inventory.remove stack.itemSlug inventory.remove stack.itemSlug
if inventory.isEmpty then @params = null if inventory.isEmpty then @params = null
+84 -37
View File
@@ -5,46 +5,90 @@ Copyright (c) 2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
Inventory = require '../inventory' _ = require '../../underscore_mixins'
SimpleInventory = require '../simple_inventory'
######################################################################################################################## ########################################################################################################################
module.exports = class CraftingPlan module.exports = class CraftingPlan
constructor: (steps, wanted)-> constructor: (steps, want, modPack)->
if not modPack? then throw new Error 'modPack is required'
if not steps? then throw new Error 'steps is required' if not steps? then throw new Error 'steps is required'
if not wanted? then throw new Error 'wanted is required' if not want? then throw new Error 'want is required'
@_produced = new Inventory @_made = null
@_required = new Inventory @_modPack = modPack
@_steps = steps @_need = null
@_wanted = wanted @_rawScores = {}
@_scores = {}
@_steps = steps
@_want = want
@_computeRequired() @_numberSteps()
# Public Methods ###############################################################################
computeRequired: ->
@_need = new SimpleInventory modPack:@_modPack
@_made = new SimpleInventory modPack:@_modPack
@_need.addInventory @_want
for i in [@_steps.length-1..0] by -1
step = @_steps[i]
step.multiplier = 0
for stack in step.recipe.output
if not stack?
throw new Error 'stack should not be null here'
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
while @_need.quantityOf(qualifiedSlug) > 0
@_executeStep step
@_made.addInventory @_want
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 ############################################################################# # Property Methods #############################################################################
Object.defineProperties @prototype, Object.defineProperties @prototype,
length: length:
get: -> @steps.length get: -> @steps.length
produced: made:
get: -> @_produced get: -> @_made
required: need:
get: -> @_required get: -> @_need
steps: steps:
get: -> @_steps get: -> @_steps
wanted: want:
get: -> @_wanted get: -> @_want
# Object Overrides ############################################################################# # Object Overrides #############################################################################
toString: -> toString: ->
result = ["To Make:"] result = ["To Make:"]
@_wanted.each (stack)-> @_want.each (stack)->
result.push " #{stack}" result.push " #{stack}"
result.push "Start with:" result.push "Start with:"
@_required.each (stack)-> @_need.each (stack)->
result.push " #{stack}" result.push " #{stack}"
result.push "Use these recipes:" result.push "Use these recipes:"
@@ -52,44 +96,47 @@ module.exports = class CraftingPlan
result.push " #{step}" result.push " #{step}"
result.push "To produce:" result.push "To produce:"
@_produced.each (stack)-> @_made.each (stack)->
result.push " #{stack}" result.push " #{stack}"
if _.keys(@_rawScores).length > 0
result.push "Scores:"
for criteria, score of @_rawScores
if @_scores[criteria]?
result.push " #{criteria}: #{score} (#{@_scores[criteria]})"
else
result.push " #{criteria}: #{score}"
return result.join '\n' return result.join '\n'
# Private Methods ############################################################################## # Private Methods ##############################################################################
_computeRequired: ->
@_required.addInventory @_wanted
for i in [@_steps.length-1..0] by -1
step = @_steps[i]
for stack in step.recipe.output
while @_required.quantityOf(stack.itemSlug) > 0
@_executeStep step
@_produced.addInventory @_wanted
_executeStep: (step)-> _executeStep: (step)->
step.repeat += 1 step.multiplier += 1
recipe = step.recipe recipe = step.recipe
for stack in recipe.input for stack in recipe.input
available = @_produced.quantityOf stack.itemSlug qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
available = @_made.quantityOf qualifiedSlug
required = recipe.getQuantityRequired stack.itemSlug required = recipe.getQuantityRequired stack.itemSlug
consumed = Math.min required, available consumed = Math.min required, available
deficit = required - consumed deficit = required - consumed
@_produced.remove stack.itemSlug, consumed @_made.remove qualifiedSlug, consumed
@_required.add stack.itemSlug, deficit @_need.add qualifiedSlug, deficit
for stack in recipe.output for stack in recipe.output
deficit = @_required.quantityOf stack.itemSlug qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
deficit = @_need.quantityOf qualifiedSlug
created = recipe.getQuantityProduced stack.itemSlug created = recipe.getQuantityProduced stack.itemSlug
replenished = Math.min deficit, created replenished = Math.min deficit, created
surplus = created - replenished surplus = created - replenished
@_produced.add stack.itemSlug, surplus @_made.add qualifiedSlug, surplus
@_required.remove stack.itemSlug, replenished @_need.remove qualifiedSlug, replenished
_numberSteps: ->
for step, i in @_steps
step.number = i + 1
@@ -5,25 +5,55 @@ Copyright (c) 2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
SimpleInventory = require '../simple_inventory'
######################################################################################################################## ########################################################################################################################
module.exports = class CraftingStep module.exports = class CraftingStep
constructor: (recipe, repeat=0)-> constructor: (recipe, modPack, multiplier=0)->
if not recipe? then throw new Error 'recipe is required' if not recipe? then throw new Error 'recipe is required'
if repeat < 0 then throw new Error 'repeat must be at least 1' if not modPack? then throw new Error 'modPack is required'
if multiplier < 0 then throw new Error 'multiplier must be at least 1'
@_recipe = recipe @number = null
@repeat = repeat
@_inventory = null
@_modPack = modPack
@_multiplier = multiplier
@_recipe = recipe
# Property Methods ############################################################################# # Property Methods #############################################################################
Object.defineProperties @prototype, Object.defineProperties @prototype,
inventory:
get: ->
if not @_inventory?
@_refreshInventory()
return @_inventory
recipe: recipe:
get: -> @_recipe get: -> @_recipe
multiplier:
get: -> @_multiplier
set: (newMultiplier)->
@_multiplier = newMultiplier
@_refreshInventory() if @_inventory?
slug:
get: -> "#{@multiplier}x #{@_recipe.slug}"
# Object Overrides ############################################################################# # Object Overrides #############################################################################
toString: -> toString: ->
return "#{@repeat}x #{@recipe.slug}" return @slug
# Private Methods ##############################################################################
_refreshInventory: ->
@_inventory = new SimpleInventory
for stack in @_recipe.input
qualifiedSlug = @_modPack.qualifySlug stack.itemSlug
@_inventory.add qualifiedSlug, @multiplier * @_recipe.getQuantityRequired stack.itemSlug
+140
View File
@@ -0,0 +1,140 @@
###
Crafting Guide - craftsman.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
_ = require 'underscore'
BaseModel = require '../base_model'
{Event} = require '../../constants'
GraphBuilder = require './graph_builder'
Inventory = require '../inventory'
PlanBuilder = require './plan_builder'
PlanEvaluator = require './plan_evaluator'
w = require 'when'
########################################################################################################################
module.exports = class Craftsman extends BaseModel
@::ANALYZE_STEP_INCREMENT = 100
@::GRAPH_STEP_INCREMENT = 10
@::PLAN_STEP_INCREMENT = 50
@::STAGE =
WAITING: 'waiting'
GRAPHING: 'examining recipes'
PLANNING: 'computing plans'
ANALYZING: 'analyzing plans'
COMPLETE: 'complete'
constructor: (modPack)->
if not modPack? then throw new Error 'modPack is required'
attributes =
paused: false
stage: @STAGE.WAITING
stageCount: 0
super attributes, {}
@_modPack = modPack
reset = _.throttle (=> @reset()), 100
@_have = new Inventory
@_have.on Event.change, reset
@_want = new Inventory
@_want.on Event.change, reset
@on Event.change + ':paused', reset
@on Event.change + ':stage', => logger.info "Craftsman has started #{@stage}..."
@reset()
# Public Methods ###############################################################################
work: ->
return if @_want.isEmpty
logger.verbose => "stage: #{@stage}(#{@stageCount}) working..."
if not @_graphBuilder?
want = new Inventory {modPack:@_modPack}, clone:@_want
want.localize()
have = new Inventory {modPack:@_modPack}, clone:@_have
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
else if not @_planBuilder?
logger.debug => "Craftsman finished computing graph:\n#{@_graphBuilder.rootNode}"
@_planBuilder = new PlanBuilder @_graphBuilder.rootNode, @_modPack, want:@_graphBuilder.want
@stage = @STAGE.PLANNING
@stageCount = 0
else if not @_planBuilder.complete
@_planBuilder.producePlans @PLAN_STEP_INCREMENT
@stageCount = @_planBuilder.plans.length
else if not @_planEvaluator?
@_planEvaluator = new PlanEvaluator @_planBuilder.plans
@stage = @STAGE.ANALYZING
@stageCount = 0
else if not @_planEvaluator.complete
@_planEvaluator.scorePlans @ANALYZE_STEP_INCREMENT
@stageCount = @_planEvaluator.lastScored
else
@_plans = [
@_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.FEWEST_STEPS
@_planEvaluator.findBestPlan PlanEvaluator::CRITERIA.LEAST_MATERIALS
]
@_plans[0].computeRequired()
logger.info => "Craftsman has finished with plans: #{(p.toString() for p in @_plans).join('\n')}"
@trigger Event.change + ':complete', this
@trigger Event.change, this
@_scheduleNextWork()
return @complete
reset: ->
@_graphBuilder = null
@_planBuilder = null
@_planEvaluator = null
@_plans = null
@stage = @STAGE.WAITING
@stageCount = 0
@_scheduleNextWork()
# Property Methods #############################################################################
Object.defineProperties @prototype,
complete:
get: -> @_plans?
have:
get: -> @_have
plan:
get: -> @_plans?[0]
want:
get: -> @_want
# Private Methods ##############################################################################
_scheduleNextWork: ->
return if @paused
return if @want.isEmpty
return if @complete
_.defer => @work()
@@ -7,6 +7,7 @@ All rights reserved.
Inventory = require '../inventory' Inventory = require '../inventory'
InventoryNode = require './inventory_node' InventoryNode = require './inventory_node'
{Limits} = require '../../constants'
######################################################################################################################## ########################################################################################################################
@@ -14,12 +15,15 @@ module.exports = class GraphBuilder
constructor: (options={})-> constructor: (options={})->
if not options.modPack? then throw new Error 'options.modPack is required' if not options.modPack? then throw new Error 'options.modPack is required'
if not options.want? then throw new Error 'options.want is required'
@modPack = options.modPack @_maximumGraphSize = Limits.maximumGraphSize
@_wanted = options.wanted ?= new Inventory @_modPack = options.modPack
@_stepCount = 0
@_want = options.want
@_wanted.on 'change', => @reset() @_rootNode = new InventoryNode modPack:@_modPack, inventory:@_want
@reset() @_queue = [@_rootNode]
# Public Methods ############################################################################### # Public Methods ###############################################################################
@@ -38,9 +42,6 @@ module.exports = class GraphBuilder
@_stepCount += 1 @_stepCount += 1
reset: -> reset: ->
@_rootNode = new InventoryNode modPack:@modPack, inventory:@wanted
@_queue = [@_rootNode]
@_stepCount = 0
# Property Methods ############################################################################# # Property Methods #############################################################################
@@ -48,6 +49,7 @@ module.exports = class GraphBuilder
complete: complete:
get: -> get: ->
return true if @_stepCount > @_maximumGraphSize
return false unless @_rootNode? return false unless @_rootNode?
return false unless @_queue? return false unless @_queue?
return false unless @_queue.length is 0 return false unless @_queue.length is 0
@@ -59,8 +61,8 @@ module.exports = class GraphBuilder
stepCount: stepCount:
get: -> @_stepCount get: -> @_stepCount
wanted: want:
get: -> @_wanted get: -> @_want
# Object Overrides ############################################################################ # Object Overrides ############################################################################
+16 -3
View File
@@ -17,7 +17,8 @@ module.exports = class ItemNode extends CraftingNode
@::TYPE = CraftingNode::TYPES.ITEM @::TYPE = CraftingNode::TYPES.ITEM
constructor: (options={})-> constructor: (options={})->
if not options.item? then throw new Error 'options.item is required' if not options.item?
throw new Error 'options.item is required'
super options super options
@item = options.item @item = options.item
@@ -32,7 +33,7 @@ module.exports = class ItemNode extends CraftingNode
isGatherable: -> isGatherable: ->
return true if @item.isGatherable return true if @item.isGatherable
return true unless @getRecipes().length > 0 return true if @getRecipes().length is 0
return false return false
Object.defineProperties @prototype, Object.defineProperties @prototype,
@@ -43,7 +44,7 @@ module.exports = class ItemNode extends CraftingNode
_createChildren: (result=[])-> _createChildren: (result=[])->
recipes = @getRecipes() recipes = @getRecipes()
return [] unless recipes.length > 0 return [] if @gatherable
for recipe in recipes for recipe in recipes
child = new RecipeNode modPack:@modPack, recipe:recipe child = new RecipeNode modPack:@modPack, recipe:recipe
@@ -62,7 +63,9 @@ module.exports = class ItemNode extends CraftingNode
return false return false
_checkValidity: -> _checkValidity: ->
return false if @_isRepeatedItem()
return true unless @children.length > 0 return true unless @children.length > 0
for child in @children for child in @children
return true if child.valid return true if child.valid
return false return false
@@ -79,3 +82,13 @@ module.exports = class ItemNode extends CraftingNode
for child in @children for child in @children
parts.push child.toString indent:nextIndent parts.push child.toString indent:nextIndent
return parts.join '\n' return parts.join '\n'
# Private Methods ##############################################################################
_isRepeatedItem: ->
nextParent = @parent
while nextParent?
return true if nextParent.item is @item
nextParent = nextParent.parent
return false
+21 -11
View File
@@ -9,20 +9,23 @@ CraftingNode = require './crafting_node'
CraftingPlan = require './crafting_plan' CraftingPlan = require './crafting_plan'
CraftingStep = require './crafting_step' CraftingStep = require './crafting_step'
Inventory = require '../inventory' Inventory = require '../inventory'
{Limits} = require '../../constants'
######################################################################################################################## ########################################################################################################################
module.exports = class PlanBuilder module.exports = class PlanBuilder
constructor: (rootNode, options={})-> constructor: (rootNode, modPack, options={})->
if not rootNode? then throw new Error 'rootNode is required' if not rootNode? then throw new Error 'rootNode is required'
@wanted = options.wanted @want = options.want
@_choiceNodes = [] @_choiceNodes = []
@_complete = false @_complete = false
@_plans = [] @_maxPlanCount = Limits.maximumPlanCount
@_rootNode = rootNode @_modPack = modPack
@_plans = []
@_rootNode = rootNode
@_isolateChoiceNodes() @_isolateChoiceNodes()
@@ -31,6 +34,9 @@ module.exports = class PlanBuilder
producePlans: (maxPlans=null)-> producePlans: (maxPlans=null)->
maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE
if @plans.length >= @_maxPlanCount
@_complete = true
while not @complete and (@plans.length < maxPlans) while not @complete and (@plans.length < maxPlans)
plan = @_captureCurrentPlan() plan = @_captureCurrentPlan()
if plan? if plan?
@@ -47,12 +53,16 @@ module.exports = class PlanBuilder
complete: complete:
get: -> @_complete get: -> @_complete
maxPlanCount:
get: -> @_maxPlanCount
set: (value)-> @_maxPlanCount = value
plans: plans:
get: -> @_plans get: -> @_plans
wanted: want:
get: -> @_wanted get: -> @_want
set: (wanted)-> @_wanted = wanted or new Inventory set: (want)-> @_want = want or new Inventory
# Private Methods ############################################################################## # Private Methods ##############################################################################
@@ -83,9 +93,9 @@ module.exports = class PlanBuilder
continue if seenRecipes[recipeSlug]? continue if seenRecipes[recipeSlug]?
seenRecipes[recipeSlug] = true seenRecipes[recipeSlug] = true
steps.push new CraftingStep node.recipe steps.push new CraftingStep node.recipe, @_modPack
plan = new CraftingPlan steps, @_wanted plan = new CraftingPlan steps, @_want, @_modPack
return plan return plan
_incrementChoiceNodes: -> _incrementChoiceNodes: ->
@@ -0,0 +1,98 @@
###
Crafting Guide - plan_evaluator.coffee
Copyright (c) 2015 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 = 0
@_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..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
-72
View File
@@ -1,72 +0,0 @@
###
Crafting Guide - crafting_table.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
{Event} = require '../constants'
########################################################################################################################
module.exports = class CraftingTable extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.plan? then throw new Error "attributes.plan is required"
super attributes, options
@plan.on 'change', => @reset()
@_stepIndex = 0
Object.defineProperties this, {
currentStep: { get:@getCurrentStep }
hasNextStep: { get:@hasNextStep }
hasPrevStep: { get:@hasPrevStep }
hasSteps: { get:@hasSteps }
stepCount: { get:@getStepCount }
stepIndex: { get:@getStepIndex, set:@setStepIndex }
}
# Public Methods ###############################################################################
reset: ->
@stepIndex = 0
return this
# Property Methods #############################################################################
hasNextStep: ->
return @_stepIndex + 1 < @plan.steps.length
hasPrevStep: ->
return @_stepIndex > 0
hasSteps: ->
return @plan.steps.length > 0
getCurrentStep: ->
return @plan.steps[@_stepIndex]
getStepIndex: ->
return @_stepIndex
setStepIndex: (newStepIndex)->
oldStepIndex = @_stepIndex
newStepIndex = Math.max 0, Math.min @plan.steps.length - 1, newStepIndex
@_stepIndex = newStepIndex
@trigger Event.change + ':stepIndex', this, oldStepIndex, newStepIndex
@trigger Event.change, this
return this
getStepCount: ->
return 0 unless @plan.steps?
return @plan.steps.length
# Object Overrides #############################################################################
toString: ->
return "#{@constructor.name} (#{@cid}) { plan:#{@plan}, step:#{@_stepIndex} }"
+5 -3
View File
@@ -21,6 +21,9 @@ module.exports = class Inventory extends BaseModel
attributes.modPack ?= null attributes.modPack ?= null
@clear() @clear()
if options.clone?
@addInventory options.clone
# Class Methods ################################################################################ # Class Methods ################################################################################
@Delimiters = @Delimiters =
@@ -100,8 +103,8 @@ module.exports = class Inventory extends BaseModel
quantityOf: (itemSlug)-> quantityOf: (itemSlug)->
stack = @_stacks[itemSlug] stack = @_stacks[itemSlug]
return 0 unless stack? return stack.quantity if stack
return stack.quantity return 0
remove: (itemSlug, quantity=null)-> remove: (itemSlug, quantity=null)->
stack = @_stacks[itemSlug] stack = @_stacks[itemSlug]
@@ -172,7 +175,6 @@ module.exports = class Inventory extends BaseModel
total += stack.quantity total += stack.quantity
return total return total
Object.defineProperties @prototype, Object.defineProperties @prototype,
isEmpty: { get:@prototype.getIsEmpty } isEmpty: { get:@prototype.getIsEmpty }
totalQuantity: { get:@prototype.getTotalQuantity } totalQuantity: { get:@prototype.getTotalQuantity }
+9 -8
View File
@@ -21,15 +21,16 @@ module.exports = class Item extends BaseModel
constructor: (attributes={}, options={})-> constructor: (attributes={}, options={})->
if not attributes.name? then throw new Error 'attributes.name is required' if not attributes.name? then throw new Error 'attributes.name is required'
attributes.description ?= null attributes.description ?= null
attributes.group ?= Item.Group.Other attributes.group ?= Item.Group.Other
attributes.isGatherable ?= false attributes.ignoreDuringCrafting ?= false
attributes.modVersion ?= null attributes.isGatherable ?= false
attributes.officialUrl ?= null attributes.modVersion ?= null
attributes.slug ?= ItemSlug.slugify attributes.name attributes.officialUrl ?= null
attributes.videos ?= [] attributes.slug ?= ItemSlug.slugify attributes.name
attributes.videos ?= []
options.logEvents ?= false options.logEvents ?= false
super attributes, options super attributes, options
@on Event.change + ':modVersion', => @on Event.change + ':modVersion', =>
+4 -5
View File
@@ -5,11 +5,10 @@ Copyright (c) 2015 by Redwood Labs
All rights reserved. All rights reserved.
### ###
BaseModel = require './base_model' _ = require 'underscore'
CraftingPlan = require './crafting_plan' BaseModel = require './base_model'
_ = require 'underscore' {Event} = require '../constants'
{Event} = require '../constants' {Url} = require '../constants'
{Url} = require '../constants'
######################################################################################################################## ########################################################################################################################
+32 -7
View File
@@ -20,24 +20,35 @@ module.exports = class ModPack extends BaseModel
super attributes, options super attributes, options
@_mods = [] @_mods = []
@_cache = {}
@on Event.change, => @_cache = {}
# Public Methods ############################################################################### # Public Methods ###############################################################################
findItem: (itemSlug, options={})-> findItem: (itemSlug, options={})->
options.includeDisabled ?= false options.includeDisabled ?= false
key = "#{itemSlug}-#{options.includeDisabled}"
@_cache.itemBySlug ?= {}
item = @_cache.itemBySlug[key]
return item if item?
if itemSlug.isQualified if itemSlug.isQualified
mod = @getMod itemSlug.mod mod = @getMod itemSlug.mod
if mod? if mod?
item = mod.findItem itemSlug, options item = mod.findItem itemSlug, options
return item if item?
for mod in @_mods if not item?
continue unless mod.enabled or options.includeDisabled for mod in @_mods
item = mod.findItem itemSlug, options continue unless mod.enabled or options.includeDisabled
return item if item? item = mod.findItem itemSlug, options
break if item?
return null if item?
@_cache.itemBySlug[key] = item
return item
findItemByName: (name, options={})-> findItemByName: (name, options={})->
options.enableAsNeeded ?= false options.enableAsNeeded ?= false
@@ -82,10 +93,16 @@ module.exports = class ModPack extends BaseModel
return null return null
findRecipes: (itemSlug, result=[], options={})-> findRecipes: (itemSlug, options={})->
options.alwaysFromOwningMod ?= false options.alwaysFromOwningMod ?= false
return null unless itemSlug? return null unless itemSlug?
key = "#{itemSlug}-#{options.alwaysFromOwningMod}"
@_cache.recipesBySlug ?= {}
result = @_cache.recipesBySlug[key]
return result if result?
result = []
for mod in @_mods for mod in @_mods
if not mod.enabled if not mod.enabled
owningMod = itemSlug.isQualified and (itemSlug.mod is mod.slug) owningMod = itemSlug.isQualified and (itemSlug.mod is mod.slug)
@@ -93,8 +110,16 @@ module.exports = class ModPack extends BaseModel
mod.findRecipes itemSlug, result, options mod.findRecipes itemSlug, result, options
@_cache.recipesBySlug[key] = result
return if result.length > 0 then result else null return if result.length > 0 then result else null
qualifySlug: (itemSlug)->
return itemSlug if itemSlug.isQualified
item = @findItem itemSlug
return item.slug if item?
return itemSlug
# Property Methods ############################################################################# # Property Methods #############################################################################
addMod: (mod)-> addMod: (mod)->
@@ -63,6 +63,13 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
@_recipeData = null @_recipeData = null
_command_ignoreDuringCrafting: (value)->
if not @_itemData? then throw new Error 'cannot declare "ignoreDuringCraft" before "item"'
if @_itemData.ignoreDuringCrafting? then throw new Error 'duplicate declaration of "ignoreDuringCraft"'
if not (value in ['yes', 'no']) then throw new Error 'ignoreDuringCraft must be either "yes" or "no"'
@_itemData.ignoreDuringCrafting = (value is 'yes')
_command_input: (inputNames...)-> _command_input: (inputNames...)->
if not @_recipeData? then throw new Error 'cannot declare "input" before "recipe"' if not @_recipeData? then throw new Error 'cannot declare "input" before "recipe"'
if @_recipeData.input.length isnt 0 then throw new Error 'duplicate declaration of "input"' if @_recipeData.input.length isnt 0 then throw new Error 'duplicate declaration of "input"'
@@ -148,11 +155,16 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase
_buildItem: (modVersion, itemData)-> _buildItem: (modVersion, itemData)->
@_lineNumber = itemData.line @_lineNumber = itemData.line
itemData.gatherable ?= false itemData.gatherable ?= false
itemData.recipes ?= [] itemData.ignoreDuringCrafting ?= false
itemData.recipes ?= []
if itemData.type is 'new' if itemData.type is 'new'
item = new Item name:itemData.name, isGatherable:itemData.gatherable, group:itemData.group item = new Item
name: itemData.name,
ignoreDuringCrafting: itemData.ignoreDuringCrafting,
isGatherable: itemData.gatherable,
group: itemData.group
modVersion.addItem item modVersion.addItem item
itemData.slug = item.slug itemData.slug = item.slug
else else
+55 -36
View File
@@ -35,7 +35,10 @@ module.exports = class Recipe extends BaseModel
options.logEvents ?= false options.logEvents ?= false
super attributes, options super attributes, options
@_computeQuantities attributes.pattern
@on Event.change + ':modVersion', => @_slug = null @on Event.change + ':modVersion', => @_slug = null
@on Event.change + ':pattern', => @_patternCache = null
# Class Methods ################################################################################ # Class Methods ################################################################################
@@ -77,24 +80,22 @@ module.exports = class Recipe extends BaseModel
getQuantityRequired: (itemSlug)-> getQuantityRequired: (itemSlug)->
total = 0 total = 0
for i in [0...@pattern.length] for stack, index in @input
stack = @getStackAtSlot i
continue unless stack?
if ItemSlug.equal stack.itemSlug, itemSlug if ItemSlug.equal stack.itemSlug, itemSlug
total += stack.quantity total += @_quantities[index] * stack.quantity
return total return total
hasAllTools: (modPack)-> hasAllTools: (modPack)->
modPack ?= @modVersion?.mod?.modPack modPack ?= @modVersion?.mod?.modPack
return true unless modPack return true unless modPack?
for stack in @tools for stack in @tools
return false unless modPack.findItem stack.itemSlug return false unless modPack.findItem stack.itemSlug
return true return true
isConditionSatisfied: (modPack)-> isConditionSatisfied: (modPack)->
return true unless @condition return true unless @condition?
modPack ?= @modVersion?.mod?.modPack modPack ?= @modVersion?.mod?.modPack
result = false result = false
@@ -145,36 +146,38 @@ module.exports = class Recipe extends BaseModel
# Property Methods ############################################################################# # Property Methods #############################################################################
getSlug: ->
if not @_slug?
builder = new StringBuilder
delimiterNeeded = false
for stack in @input
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
builder.push '>'
for stack in @tools
builder.push stack.itemSlug.qualified
builder.push '>'
delimiterNeeded = false
for stack in @output
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
@_slug = builder.toString()
return @_slug
Object.defineProperties @prototype, Object.defineProperties @prototype,
slug: {get:@prototype.getSlug}
slug:
get: ->
if not @_slug?
builder = new StringBuilder
delimiterNeeded = false
for stack in @input
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
builder.push '>'
builder.push @pattern
builder.push '>'
for stack in @tools
builder.push stack.itemSlug.qualified
builder.push '>'
delimiterNeeded = false
for stack in @output
if delimiterNeeded then builder.push ','
delimiterNeeded = true
if stack.quantity > 1 then builder.push stack.quantity, ' '
builder.push stack.itemSlug.qualified
@_slug = builder.toString()
return @_slug
# Object Overrides ############################################################################# # Object Overrides #############################################################################
@@ -215,6 +218,22 @@ module.exports = class Recipe extends BaseModel
# Private Methods ############################################################################## # Private Methods ##############################################################################
_computeQuantities: (pattern)->
quantityMap = {}
for c in pattern.split ''
continue if c is '.'
continue if c is ' '
if quantityMap[c]?
quantityMap[c] += 1
else
quantityMap[c] = 1
@_quantities = []
for i in [0...@input.length]
@_quantities.push quantityMap["#{i}"]
_parsePattern: (pattern)-> _parsePattern: (pattern)->
return unless pattern? return unless pattern?
-26
View File
@@ -1,26 +0,0 @@
###
Crafting Guide - recipe_step.coffee
Copyright (c) 2014 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
########################################################################################################################
module.exports = class RecipeStep extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.recipe? then throw new Error 'attributes.recipe is required'
attributes.multiple ?= 1
super attributes, options
@recipe.on 'change', =>
@trigger 'change:recipe'
@trigger 'change'
# Public Methods ###############################################################################
getItemAt: (index)->
return @recipe.getItemAt index
+200
View File
@@ -0,0 +1,200 @@
###
Crafting Guide - inventory.coffee
Copyright (c) 2014-2015 by Redwood Labs
All rights reserved.
###
ItemSlug = require './item_slug'
SimpleStack = require './simple_stack'
_ = require 'underscore'
########################################################################################################################
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 Inventory
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'
newSlugs = []
for itemSlug in @_itemSlugs
stack = @_stacks[itemSlug]
qualifiedSlug = @modPack.findItem(itemSlug)?.slug
if qualifiedSlug?
delete @_stacks[itemSlug]
newSlugs.push qualifiedSlug
@_stacks[qualifiedSlug] = stack
stack.itemSlug = qualifiedSlug
else
newSlugs.push itemSlug
@_itemSlugs = newSlugs
@_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
delete @_stacks[itemSlug]
@_itemSlugs = (s for s in @_itemSlugs when not ItemSlug.equal(s, itemSlug))
return this
# Parsing Methods ##############################################################################
parse: (data)->
return this if not data? or data.length is 0
stacks = data.split Inventory.Delimiters.Stack
for stackText in stacks
stackParts = stackText.split Inventory.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
-35
View File
@@ -1,35 +0,0 @@
###
Crafting Guide - step.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
BaseModel = require './base_model'
Inventory = require './inventory'
{Event} = require '../constants'
########################################################################################################################
module.exports = class Step extends BaseModel
constructor: (attributes={}, options={})->
if not attributes.outputItemSlug? then throw new Error 'attributes.outputItemSlug is required'
if not attributes.recipe? then throw new Error 'attributes.recipe is required'
attributes.inventory = new Inventory
attributes.number ?= 1
attributes.outputItemSlug ?= null
attributes.multiplier ?= 1
attributes.recipe ?= null
super attributes, options
@_computeInventory()
@on Event.change + ':multiplier', => @_computeInventory()
# Private Methods ##############################################################################
_computeInventory: ->
@inventory.clear()
for stack in @recipe.input
@inventory.add stack.itemSlug, stack.quantity * @multiplier
+45 -33
View File
@@ -18,44 +18,49 @@ describe 'crafting_plan.coffee', ->
plans.length.should.equal 1 plans.length.should.equal 1
plan = plans[0] plan = plans[0]
plan.required.unparse().should.equal 'coal' plan.computeRequired()
plan.produced.unparse().should.equal 'coal' plan.need.unparse().should.equal 'coal'
plan.made.unparse().should.equal 'coal'
it 'can compute a single item with one single step plan', -> it 'can compute a single item with one single step plan', ->
plans = fixtures.makePlans [1, 'test__charcoal'] plans = fixtures.makePlans [1, 'test__charcoal']
plans.length.should.equal 1 plans.length.should.equal 1
plan = plans[0] plan = plans[0]
plan.required.unparse().should.equal 'coal:8.oak_wood' plan.computeRequired()
plan.produced.unparse().should.equal '8.charcoal' plan.need.unparse().should.equal 'coal:8.oak_wood'
(s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>>8 test__charcoal'] plan.made.unparse().should.equal '8.charcoal'
(s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
it 'can compute a large quantity of a single item with one single step plan', -> it 'can compute a large quantity of a single item with one single step plan', ->
plans = fixtures.makePlans [15, 'test__charcoal'] plans = fixtures.makePlans [15, 'test__charcoal']
plans.length.should.equal 1 plans.length.should.equal 1
plan = plans[0] plan = plans[0]
plan.required.unparse().should.equal '2.coal:16.oak_wood' plan.computeRequired()
plan.produced.unparse().should.equal '16.charcoal' plan.need.unparse().should.equal '2.coal:16.oak_wood'
(s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>>8 test__charcoal'] plan.made.unparse().should.equal '16.charcoal'
(s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal']
it 'can compute a single item with multiple plans', -> it 'can compute a single item with multiple plans', ->
plans = fixtures.makePlans [1, 'test__iron_ingot'] plans = fixtures.makePlans [1, 'test__iron_ingot']
plans.length.should.equal 2 plans.length.should.equal 2
plan = plans[0] plan = plans[0]
plan.required.unparse().should.equal 'coal:8.iron_ore:8.oak_wood' plan.computeRequired()
plan.produced.unparse().should.equal '7.charcoal:8.iron_ingot' 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 [ (s.toString() for s in plan.steps).should.eql [
'1x 8 test__oak_wood,test__coal>>8 test__charcoal' '1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal'
'1x 8 test__iron_ore,test__charcoal>test__furnace>8 test__iron_ingot' '1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot'
] ]
plan = plans[1] plan = plans[1]
plan.required.unparse().should.equal 'coal:8.iron_ore' plan.computeRequired()
plan.produced.unparse().should.equal '8.iron_ingot' plan.need.unparse().should.equal 'coal:8.iron_ore'
plan.made.unparse().should.equal '8.iron_ingot'
(s.toString() for s in plan.steps).should.eql [ (s.toString() for s in plan.steps).should.eql [
'1x 8 test__iron_ore,test__coal>test__furnace>8 test__iron_ingot' '1x 8 test__iron_ore,test__coal>.0. ... .1.>test__furnace>8 test__iron_ingot'
] ]
it 'can compute multiple items with multiple plans', -> it 'can compute multiple items with multiple plans', ->
@@ -63,33 +68,40 @@ describe 'crafting_plan.coffee', ->
plans.length.should.equal 4 plans.length.should.equal 4
for plan in plans for plan in plans
plan.required.unparse().should.match /16.copper_ore.*8.iron_ore/ plan.computeRequired()
plan.produced.unparse().should.match /copper_block.*:iron_sword/ plan.need.unparse().should.match /16.copper_ore.*8.iron_ore/
plan.made.unparse().should.match /copper_block.*:iron_sword/
plan = plans[0] plan = plans[0]
plan.required.unparse().should.match /3.coal.*9.oak_wood/ plan.need.unparse().should.match /3.coal.*9.oak_wood/
plan.produced.unparse().should.match /7.charcoal/ plan.made.unparse().should.match /7.charcoal/
"#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__charcoal>test__furnace>8 test__iron_ingot' "#{plan.steps[3]}".should.equal \
"#{plan.steps[4]}".should.equal '2x 8 test__copper_ore,test__coal>test__furnace>8 test__copper_ingot' '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.steps.length.should.equal 7
plan = plans[1] plan = plans[1]
plan.required.unparse().should.match /3.coal.*:oak_wood/ plan.need.unparse().should.match /3.coal.*:oak_wood/
plan.produced.unparse().should.not.match /charcoal/ plan.made.unparse().should.not.match /charcoal/
"#{plan.steps[2]}".should.equal '1x 8 test__iron_ore,test__coal>test__furnace>8 test__iron_ingot' "#{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>test__furnace>8 test__copper_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.steps.length.should.equal 6
plan = plans[2] plan = plans[2]
plan.required.unparse().should.match /^coal.*9.oak_wood/ plan.need.unparse().should.match /^coal.*9.oak_wood/
plan.produced.unparse().should.match /5.charcoal/ plan.made.unparse().should.match /5.charcoal/
"#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__charcoal>test__furnace>8 test__iron_ingot' "#{plan.steps[3]}".should.equal \
"#{plan.steps[4]}".should.equal '2x 8 test__copper_ore,test__charcoal>test__furnace>8 test__copper_ingot' '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.steps.length.should.equal 7
plan = plans[3] plan = plans[3]
plan.required.unparse().should.match /2.coal.*9.oak_wood/ plan.need.unparse().should.match /2.coal.*9.oak_wood/
plan.produced.unparse().should.match /6.charcoal/ plan.made.unparse().should.match /6.charcoal/
"#{plan.steps[3]}".should.equal '1x 8 test__iron_ore,test__coal>test__furnace>8 test__iron_ingot' "#{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>test__furnace>8 test__copper_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.steps.length.should.equal 7
+25 -6
View File
@@ -6,6 +6,7 @@ All rights reserved.
### ###
GraphBuilder = require '../../src/coffee/models/crafting/graph_builder' GraphBuilder = require '../../src/coffee/models/crafting/graph_builder'
Inventory = require '../../src/coffee/models/inventory'
ItemSlug = require '../../src/coffee/models/item_slug' ItemSlug = require '../../src/coffee/models/item_slug'
Mod = require '../../src/coffee/models/mod' Mod = require '../../src/coffee/models/mod'
ModPack = require '../../src/coffee/models/mod_pack' ModPack = require '../../src/coffee/models/mod_pack'
@@ -30,8 +31,10 @@ MOD_VERSION_FILE =
pattern: .00 .00 ... pattern: .00 .00 ...
item: Coal item: Coal
gatherable: yes
item: Cobblestone item: Cobblestone
gatherable: yes
item: Copper Block item: Copper Block
recipe: recipe:
@@ -56,6 +59,7 @@ MOD_VERSION_FILE =
quantity: 9 quantity: 9
item: Copper Ore item: Copper Ore
gatherable: yes
item: Furnace item: Furnace
recipe: recipe:
@@ -64,6 +68,7 @@ MOD_VERSION_FILE =
tools: Crafting Table tools: Crafting Table
item: Iron Ore item: Iron Ore
gatherable: yes
item: Iron Ingot item: Iron Ingot
recipe: recipe:
@@ -95,12 +100,22 @@ MOD_VERSION_FILE =
quantity: 4 quantity: 4
item: Oak Wood item: Oak Wood
gatherable: yes
item: Stick item: Stick
recipe: recipe:
input: Oak Planks input: Oak Planks
pattern: .0. .0. ... pattern: .0. .0. ...
quantity: 4 quantity: 4
item: String
gatherable: yes
item: Wool
gatherable: yes
recipe:
input: String
pattern: 00. 00. ...
""" """
######################################################################################################################## ########################################################################################################################
@@ -108,7 +123,7 @@ MOD_VERSION_FILE =
module.exports = fixtures = module.exports = fixtures =
makeGraphBuilder: -> makeGraphBuilder: ->
return new GraphBuilder modPack:fixtures.makeModPack() return new GraphBuilder modPack:fixtures.makeModPack(), want:new Inventory
makeModPack: -> makeModPack: ->
modPack = new ModPack modPack = new ModPack
@@ -125,17 +140,21 @@ module.exports = fixtures =
makePlans: (stacks...)-> makePlans: (stacks...)->
modPack = fixtures.makeModPack() modPack = fixtures.makeModPack()
graphBuilder = fixtures.makeGraphBuilder() want = new Inventory
for stack in stacks for stack in stacks
graphBuilder.wanted.add ItemSlug.slugify(stack[1]), stack[0] want.add ItemSlug.slugify(stack[1]), stack[0]
graphBuilder = new GraphBuilder modPack:fixtures.makeModPack(), want:want
graphBuilder.expandGraph() graphBuilder.expandGraph()
planBuilder = new PlanBuilder graphBuilder.rootNode, wanted:graphBuilder.wanted planBuilder = new PlanBuilder graphBuilder.rootNode, modPack, want:graphBuilder.want
return planBuilder.producePlans() return planBuilder.producePlans()
makeTree: (itemSlug, quantity=1)-> makeTree: (itemSlug, quantity=1)->
builder = fixtures.makeGraphBuilder() want = new Inventory
builder.wanted.add ItemSlug.slugify itemSlug want.add ItemSlug.slugify itemSlug
builder = new GraphBuilder modPack:fixtures.makeModPack(), want:want
builder.expandGraph() builder.expandGraph()
return builder.rootNode return builder.rootNode
+6 -3
View File
@@ -22,7 +22,7 @@ describe 'GraphBuilder.coffee', ->
describe 'expand', -> describe 'expand', ->
it 'can work a few steps at a time', -> it 'can work a few steps at a time', ->
builder.wanted.add ItemSlug.slugify 'test__iron_sword' builder.want.add ItemSlug.slugify 'test__iron_sword'
builder.expandGraph 9 builder.expandGraph 9
builder.rootNode.depth.should.equal 6 builder.rootNode.depth.should.equal 6
@@ -45,7 +45,7 @@ describe 'GraphBuilder.coffee', ->
describe 'can build a tree for', -> describe 'can build a tree for', ->
runSingleItemTreeBuildingTest = (slug, depth, size)-> runSingleItemTreeBuildingTest = (slug, depth, size)->
builder.wanted.add ItemSlug.slugify slug builder.want.add ItemSlug.slugify slug
builder.expandGraph 100 builder.expandGraph 100
builder.rootNode.depth.should.equal depth builder.rootNode.depth.should.equal depth
@@ -68,4 +68,7 @@ describe 'GraphBuilder.coffee', ->
runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18 runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18
it 'an item with one recursive recipe', -> it 'an item with one recursive recipe', ->
runSingleItemTreeBuildingTest 'test__copper_ingot', 10, 24 runSingleItemTreeBuildingTest 'test__copper_ingot', 6, 15
it 'an item which gatherable and craftable', ->
runSingleItemTreeBuildingTest 'test__wool', 2, 2
+79
View File
@@ -0,0 +1,79 @@
###
Crafting Guide - plan_evaluator.test.coffee
Copyright (c) 2015 by Redwood Labs
All rights reserved.
###
CraftingPlan = require '../../src/coffee/models/crafting/crafting_plan'
fixtures = require './fixtures'
Inventory = require '../../src/coffee/models/inventory'
PlanEvaluator = require '../../src/coffee/models/crafting/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
planA = new CraftingPlan [], wanted, modPack
planB = new CraftingPlan [], wanted, modPack
planC = new CraftingPlan [], wanted, modPack
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]
+1 -1
View File
@@ -97,5 +97,5 @@ describe 'mod_version.coffee', ->
(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', 'cake', 'cake']
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