diff --git a/src/coffee/constants.coffee b/src/coffee/constants.coffee index a3ee1524f..77f554ed5 100644 --- a/src/coffee/constants.coffee +++ b/src/coffee/constants.coffee @@ -94,6 +94,10 @@ GitHub.file.itemDescription = {} GitHub.file.itemDescription.fileName = _.template "item.cg" GitHub.file.itemDescription.path = _.template "/data/<%= modSlug %>/items/<%= itemSlug %>" +exports.Limits = Limits = {} +Limits.maximumGraphSize = 10000 +Limits.maximumPlanCount = 10000 + exports.Login = Login = {} Login.authorizeUrl = _.template "https://github.com/login/oauth/authorize" + "?client_id=<%= clientId %>&scope=public_repo&state=<%= state %>" diff --git a/src/coffee/controllers/craft_page_controller.coffee b/src/coffee/controllers/craft_page_controller.coffee index 83f6f4042..91a01c050 100644 --- a/src/coffee/controllers/craft_page_controller.coffee +++ b/src/coffee/controllers/craft_page_controller.coffee @@ -32,45 +32,39 @@ module.exports = class CraftPageController extends PageController @modPack = options.modPack @storage = options.storage - @model.plan.on Event.change, => @tryRefresh() + @model.craftsman.on Event.change, => @tryRefresh() # Event Methods ################################################################################ - onCraftTool: (itemSlug)-> - # TODO: implement this - onHaveInventoryChanged: -> - @storage.store 'crafting-plan:have', @model.plan.have.unparse() + @storage.store 'crafting-plan:have', @model.craftsman.have.unparse() onMoveNeedToHave: (itemSlug)-> - quantity = @model.plan.need.quantityOf itemSlug - @model.plan.have.add itemSlug, quantity + quantity = @model.craftsman.need.quantityOf itemSlug + @model.craftsman.have.add itemSlug, quantity @onHaveInventoryChanged() onRemoveFromHaveInventory: (itemSlug)-> - @model.plan.have.remove itemSlug + @model.craftsman.have.remove itemSlug @onHaveInventoryChanged() onRemoveFromWant: (itemSlug)-> - @model.plan.want.remove itemSlug + @model.craftsman.want.remove itemSlug @onWantInventoryChange() - onStopUsingTool: (itemSlug)-> - # TODO: implement this - onStepComplete: (stepController)-> step = stepController.model 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() onWantInventoryChange: -> - text = @model.plan.want.unparse() + text = @model.craftsman.want.unparse() url = Url.crafting inventoryText:text router.navigate url - if @model.plan.want.isEmpty - @model.plan.have.clear() + if @model.craftsman.want.isEmpty + @model.craftsman.have.clear() @onHaveInventoryChanged() # PageController Overrides ##################################################################### @@ -89,24 +83,15 @@ module.exports = class CraftPageController extends PageController @wantInventoryController = @addChild InventoryController, '.want .view__inventory', imageLoader: @imageLoader isAcceptable: (item)=> item.isCraftable - model: @model.plan.want + model: @model.craftsman.want modPack: @modPack firstButtonType: 'remove' @wantInventoryController.on Event.button.first, (c, s)=> @onRemoveFromWant(s) @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', imageLoader: @imageLoader - model: @model.plan.have + model: @model.craftsman.have modPack: @modPack firstButtonType: 'down' @haveInventoryController.on Event.button.first, (c, s)=> @@ -116,7 +101,7 @@ module.exports = class CraftPageController extends PageController @needInventoryController = @addChild InventoryController, '.need .view__inventory', editable: false imageLoader: @imageLoader - model: @model.plan.need + model: null modPack: @modPack firstButtonType: 'up' @needInventoryController.on Event.button.first, (c, s)=> @onMoveNeedToHave(s) @@ -131,11 +116,12 @@ module.exports = class CraftPageController extends PageController super onWillRender: -> - @model.plan.have.clear() - @model.plan.have.parse @storage.load('crafting-plan:have') + @model.craftsman.have.clear() + @model.craftsman.have.parse @storage.load('crafting-plan:have') super refresh: -> + @needInventoryController.model = @model.craftsman.plan?.need @_refreshSectionVisibility() @_refreshSteps() @adsenseController.fillAdPositions() @@ -150,10 +136,10 @@ module.exports = class CraftPageController extends PageController # Private Methods ################################################################################ _isStepCompletable: (controller)-> - return not @model.plan.want.hasAtLeast controller.model.outputItemSlug + return not @model.craftsman.want.hasAtLeast controller.model.outputItemSlug _refreshSectionVisibility: -> - if @model.plan.want.isEmpty + if @model.craftsman.want.isEmpty for $el in [@$toolsSection, @$ingredientsSection, @$stepsSection] @hide $el @show @$instructionsSection @@ -163,10 +149,11 @@ module.exports = class CraftPageController extends PageController @hide @$instructionsSection _refreshSteps: -> + steps = @model.craftsman.plan?.steps or [] @_stepControllers ?= [] index = 0 - for step in @model.plan.steps + for step in steps controller = @_stepControllers[index] if not controller? controller = new StepController diff --git a/src/coffee/controllers/crafting_table_controller.coffee b/src/coffee/controllers/crafting_table_controller.coffee deleted file mode 100644 index 1d042424c..000000000 --- a/src/coffee/controllers/crafting_table_controller.coffee +++ /dev/null @@ -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 = $('

') - @$('.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' diff --git a/src/coffee/controllers/inventory_controller.coffee b/src/coffee/controllers/inventory_controller.coffee index 8ba2d299f..1536129dd 100644 --- a/src/coffee/controllers/inventory_controller.coffee +++ b/src/coffee/controllers/inventory_controller.coffee @@ -33,7 +33,6 @@ module.exports = class InventoryController 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' @imageLoader = options.imageLoader @@ -57,6 +56,8 @@ module.exports = class InventoryController extends BaseController # Event Methods ################################################################################ onClearButtonClicked: -> + return unless @model? + @model.clear() @trigger Event.clear, this @trigger Event.change, this @@ -65,6 +66,8 @@ module.exports = class InventoryController extends BaseController @trigger Event.button.first, this, stackController?.model?.itemSlug onItemChosen: (itemSlug)-> + return unless @model? + @model.add itemSlug, 1 @trigger Event.add, this, itemSlug @trigger Event.change, this @@ -95,11 +98,12 @@ module.exports = class InventoryController extends BaseController @$icon.attr 'src', @icon @$title.html @title - @$clearButton.disabled = @model.isEmpty @_refreshStacks() - if @model.isEmpty + @$clearButton.disabled = @model?.isEmpty + + if not @model or @model.isEmpty @show @$emptyPlaceholder else @hide @$emptyPlaceholder @@ -120,27 +124,28 @@ module.exports = class InventoryController extends BaseController @_stackControllers ?= [] index = 0 - @model.each (stack)=> - controller = @_stackControllers[index] - if not controller? - controller = new StackController - editable: @editable - firstButtonType: @firstButtonType - imageLoader: @imageLoader - model: stack - modPack: @modPack - secondButtonType: @secondButtonType - shouldEnableButton: @shouldEnableButton - controller.on Event.change, => @trigger Event.change, this - controller.on Event.button.first, (c)=> @onFirstButtonClicked(c) - controller.on Event.button.second, (c)=> @onSecondButtonClicked(c) - controller.render() + if @model? + @model.each (stack)=> + controller = @_stackControllers[index] + if not controller? + controller = new StackController + editable: @editable + firstButtonType: @firstButtonType + imageLoader: @imageLoader + model: stack + modPack: @modPack + secondButtonType: @secondButtonType + shouldEnableButton: @shouldEnableButton + controller.on Event.change, => @trigger Event.change, this + controller.on Event.button.first, (c)=> @onFirstButtonClicked(c) + controller.on Event.button.second, (c)=> @onSecondButtonClicked(c) + controller.render() - @_stackControllers.push controller - @$itemContainer.append controller.$el - else - controller.model = stack - index += 1 + @_stackControllers.push controller + @$itemContainer.append controller.$el + else + controller.model = stack + index += 1 while @_stackControllers.length > index @_stackControllers.pop().remove() diff --git a/src/coffee/controllers/step_controller.coffee b/src/coffee/controllers/step_controller.coffee index 709f2836f..0d7eb45f0 100644 --- a/src/coffee/controllers/step_controller.coffee +++ b/src/coffee/controllers/step_controller.coffee @@ -5,11 +5,11 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### +_ = require 'underscore' BaseController = require './base_controller' +{Event} = require '../constants' InventoryController = require './inventory_controller' MinimalRecipeController = require './minimal_recipe_controller' -_ = require 'underscore' -{Event} = require '../constants' ######################################################################################################################## @@ -56,7 +56,7 @@ module.exports = class StepController extends BaseController return super refresh: -> - itemDisplay = @modPack.findItemDisplay @model.outputItemSlug + itemDisplay = @modPack.findItemDisplay @model.recipe.output[0].itemSlug @$header.html "#{@model.number}. #{itemDisplay.itemName}" @inventoryController.model = @model.inventory diff --git a/src/coffee/models/craft_page.coffee b/src/coffee/models/craft_page.coffee index 403e58825..f43c31a2d 100644 --- a/src/coffee/models/craft_page.coffee +++ b/src/coffee/models/craft_page.coffee @@ -5,34 +5,32 @@ Copyright (c) 2014-2015 by Redwood Labs All rights reserved. ### -BaseModel = require './base_model' -CraftingPlan = require './crafting_plan' -CraftingTable = require './crafting_table' -{Event} = require '../constants' -Inventory = require './inventory' -ModPack = require './mod_pack' +BaseModel = require './base_model' +Craftsman = require './crafting/craftsman' +{Event} = require '../constants' +Inventory = require './inventory' +ModPack = require './mod_pack' ######################################################################################################################## module.exports = class CraftPage extends BaseModel constructor: (attributes={}, options={})-> - attributes.modPack ?= new ModPack - attributes.params ?= null - attributes.plan ?= new CraftingPlan modPack:attributes.modPack - attributes.table ?= new CraftingTable plan:attributes.plan + if not attributes.modPack then throw new Error 'attributes.modPack is required' + attributes.params ?= null + attributes.craftsman ?= new Craftsman attributes.modPack super attributes, options @modPack.on Event.change, => @_consumeParams() @on Event.change + ':params', => @_consumeParams() - @plan.on Event.change, => @trigger Event.change, this + @craftsman.on Event.change, => @trigger Event.change, this # Private Methods ############################################################################## _consumeParams: -> return unless @params? - @plan.want.clear() + @craftsman.want.clear() if not @params.inventoryText? @params = null else @@ -42,7 +40,7 @@ module.exports = class CraftPage extends BaseModel inventory.each (stack)=> item = @modPack.findItem stack.itemSlug, enableAsNeeded:true return unless item? and item.isCraftable - @plan.want.add stack.itemSlug, stack.quantity + @craftsman.want.add stack.itemSlug, stack.quantity inventory.remove stack.itemSlug if inventory.isEmpty then @params = null diff --git a/src/coffee/models/crafting/crafting_plan.coffee b/src/coffee/models/crafting/crafting_plan.coffee index cd4dc6c58..55af8b3eb 100644 --- a/src/coffee/models/crafting/crafting_plan.coffee +++ b/src/coffee/models/crafting/crafting_plan.coffee @@ -5,46 +5,90 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### -Inventory = require '../inventory' +_ = require '../../underscore_mixins' +SimpleInventory = require '../simple_inventory' ######################################################################################################################## 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 wanted? then throw new Error 'wanted is required' + if not want? then throw new Error 'want is required' - @_produced = new Inventory - @_required = new Inventory - @_steps = steps - @_wanted = wanted + @_made = null + @_modPack = modPack + @_need = null + @_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 ############################################################################# Object.defineProperties @prototype, length: get: -> @steps.length - produced: - get: -> @_produced - required: - get: -> @_required + made: + get: -> @_made + need: + get: -> @_need steps: get: -> @_steps - wanted: - get: -> @_wanted + want: + get: -> @_want # Object Overrides ############################################################################# toString: -> result = ["To Make:"] - @_wanted.each (stack)-> + @_want.each (stack)-> result.push " #{stack}" result.push "Start with:" - @_required.each (stack)-> + @_need.each (stack)-> result.push " #{stack}" result.push "Use these recipes:" @@ -52,44 +96,47 @@ module.exports = class CraftingPlan result.push " #{step}" result.push "To produce:" - @_produced.each (stack)-> + @_made.each (stack)-> result.push " #{stack}" + if _.keys(@_rawScores).length > 0 + result.push "Scores:" + for criteria, score of @_rawScores + if @_scores[criteria]? + result.push " #{criteria}: #{score} (#{@_scores[criteria]})" + else + result.push " #{criteria}: #{score}" + return result.join '\n' - # Private Methods ############################################################################## - _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)-> - step.repeat += 1 + step.multiplier += 1 recipe = step.recipe for stack in recipe.input - available = @_produced.quantityOf stack.itemSlug + qualifiedSlug = @_modPack.qualifySlug stack.itemSlug + + available = @_made.quantityOf qualifiedSlug required = recipe.getQuantityRequired stack.itemSlug consumed = Math.min required, available deficit = required - consumed - @_produced.remove stack.itemSlug, consumed - @_required.add stack.itemSlug, deficit + @_made.remove qualifiedSlug, consumed + @_need.add qualifiedSlug, deficit for stack in recipe.output - deficit = @_required.quantityOf stack.itemSlug + qualifiedSlug = @_modPack.qualifySlug stack.itemSlug + + deficit = @_need.quantityOf qualifiedSlug created = recipe.getQuantityProduced stack.itemSlug replenished = Math.min deficit, created surplus = created - replenished - @_produced.add stack.itemSlug, surplus - @_required.remove stack.itemSlug, replenished + @_made.add qualifiedSlug, surplus + @_need.remove qualifiedSlug, replenished + + _numberSteps: -> + for step, i in @_steps + step.number = i + 1 diff --git a/src/coffee/models/crafting/crafting_step.coffee b/src/coffee/models/crafting/crafting_step.coffee index 75ee5866c..81cbca4b9 100644 --- a/src/coffee/models/crafting/crafting_step.coffee +++ b/src/coffee/models/crafting/crafting_step.coffee @@ -5,25 +5,55 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### +SimpleInventory = require '../simple_inventory' + ######################################################################################################################## module.exports = class CraftingStep - constructor: (recipe, repeat=0)-> + constructor: (recipe, modPack, multiplier=0)-> 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 - @repeat = repeat + @number = null + + @_inventory = null + @_modPack = modPack + @_multiplier = multiplier + @_recipe = recipe # 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 "#{@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 diff --git a/src/coffee/models/crafting/craftsman.coffee b/src/coffee/models/crafting/craftsman.coffee new file mode 100644 index 000000000..16934d3ab --- /dev/null +++ b/src/coffee/models/crafting/craftsman.coffee @@ -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() diff --git a/src/coffee/models/crafting/graph_builder.coffee b/src/coffee/models/crafting/graph_builder.coffee index e859549e7..844463881 100644 --- a/src/coffee/models/crafting/graph_builder.coffee +++ b/src/coffee/models/crafting/graph_builder.coffee @@ -7,6 +7,7 @@ All rights reserved. Inventory = require '../inventory' InventoryNode = require './inventory_node' +{Limits} = require '../../constants' ######################################################################################################################## @@ -14,12 +15,15 @@ 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' - @modPack = options.modPack - @_wanted = options.wanted ?= new Inventory + @_maximumGraphSize = Limits.maximumGraphSize + @_modPack = options.modPack + @_stepCount = 0 + @_want = options.want - @_wanted.on 'change', => @reset() - @reset() + @_rootNode = new InventoryNode modPack:@_modPack, inventory:@_want + @_queue = [@_rootNode] # Public Methods ############################################################################### @@ -38,9 +42,6 @@ module.exports = class GraphBuilder @_stepCount += 1 reset: -> - @_rootNode = new InventoryNode modPack:@modPack, inventory:@wanted - @_queue = [@_rootNode] - @_stepCount = 0 # Property Methods ############################################################################# @@ -48,6 +49,7 @@ module.exports = class GraphBuilder complete: get: -> + return true if @_stepCount > @_maximumGraphSize return false unless @_rootNode? return false unless @_queue? return false unless @_queue.length is 0 @@ -59,8 +61,8 @@ module.exports = class GraphBuilder stepCount: get: -> @_stepCount - wanted: - get: -> @_wanted + want: + get: -> @_want # Object Overrides ############################################################################ diff --git a/src/coffee/models/crafting/item_node.coffee b/src/coffee/models/crafting/item_node.coffee index 51e54d26f..fe5972d82 100644 --- a/src/coffee/models/crafting/item_node.coffee +++ b/src/coffee/models/crafting/item_node.coffee @@ -17,7 +17,8 @@ module.exports = class ItemNode extends CraftingNode @::TYPE = CraftingNode::TYPES.ITEM 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 @item = options.item @@ -32,7 +33,7 @@ module.exports = class ItemNode extends CraftingNode isGatherable: -> return true if @item.isGatherable - return true unless @getRecipes().length > 0 + return true if @getRecipes().length is 0 return false Object.defineProperties @prototype, @@ -43,7 +44,7 @@ module.exports = class ItemNode extends CraftingNode _createChildren: (result=[])-> recipes = @getRecipes() - return [] unless recipes.length > 0 + return [] if @gatherable for recipe in recipes child = new RecipeNode modPack:@modPack, recipe:recipe @@ -62,7 +63,9 @@ module.exports = class ItemNode extends CraftingNode return false _checkValidity: -> + return false if @_isRepeatedItem() return true unless @children.length > 0 + for child in @children return true if child.valid return false @@ -79,3 +82,13 @@ module.exports = class ItemNode extends CraftingNode 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 diff --git a/src/coffee/models/crafting/plan_builder.coffee b/src/coffee/models/crafting/plan_builder.coffee index 83648dde3..ceab31ea9 100644 --- a/src/coffee/models/crafting/plan_builder.coffee +++ b/src/coffee/models/crafting/plan_builder.coffee @@ -9,20 +9,23 @@ CraftingNode = require './crafting_node' CraftingPlan = require './crafting_plan' CraftingStep = require './crafting_step' Inventory = require '../inventory' +{Limits} = require '../../constants' ######################################################################################################################## module.exports = class PlanBuilder - constructor: (rootNode, options={})-> + constructor: (rootNode, modPack, options={})-> if not rootNode? then throw new Error 'rootNode is required' - @wanted = options.wanted + @want = options.want - @_choiceNodes = [] - @_complete = false - @_plans = [] - @_rootNode = rootNode + @_choiceNodes = [] + @_complete = false + @_maxPlanCount = Limits.maximumPlanCount + @_modPack = modPack + @_plans = [] + @_rootNode = rootNode @_isolateChoiceNodes() @@ -31,6 +34,9 @@ module.exports = class PlanBuilder 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? @@ -47,12 +53,16 @@ module.exports = class PlanBuilder complete: get: -> @_complete + maxPlanCount: + get: -> @_maxPlanCount + set: (value)-> @_maxPlanCount = value + plans: get: -> @_plans - wanted: - get: -> @_wanted - set: (wanted)-> @_wanted = wanted or new Inventory + want: + get: -> @_want + set: (want)-> @_want = want or new Inventory # Private Methods ############################################################################## @@ -83,9 +93,9 @@ module.exports = class PlanBuilder continue if seenRecipes[recipeSlug]? 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 _incrementChoiceNodes: -> diff --git a/src/coffee/models/crafting/plan_evaluator.coffee b/src/coffee/models/crafting/plan_evaluator.coffee new file mode 100644 index 000000000..26bf97b32 --- /dev/null +++ b/src/coffee/models/crafting/plan_evaluator.coffee @@ -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 diff --git a/src/coffee/models/crafting_table.coffee b/src/coffee/models/crafting_table.coffee deleted file mode 100644 index 46099bd27..000000000 --- a/src/coffee/models/crafting_table.coffee +++ /dev/null @@ -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} }" diff --git a/src/coffee/models/inventory.coffee b/src/coffee/models/inventory.coffee index 918b5ba8c..e87c26276 100644 --- a/src/coffee/models/inventory.coffee +++ b/src/coffee/models/inventory.coffee @@ -21,6 +21,9 @@ module.exports = class Inventory extends BaseModel attributes.modPack ?= null @clear() + if options.clone? + @addInventory options.clone + # Class Methods ################################################################################ @Delimiters = @@ -100,8 +103,8 @@ module.exports = class Inventory extends BaseModel quantityOf: (itemSlug)-> stack = @_stacks[itemSlug] - return 0 unless stack? - return stack.quantity + return stack.quantity if stack + return 0 remove: (itemSlug, quantity=null)-> stack = @_stacks[itemSlug] @@ -172,7 +175,6 @@ module.exports = class Inventory extends BaseModel total += stack.quantity return total - Object.defineProperties @prototype, isEmpty: { get:@prototype.getIsEmpty } totalQuantity: { get:@prototype.getTotalQuantity } diff --git a/src/coffee/models/item.coffee b/src/coffee/models/item.coffee index 590adb456..34cc38dc7 100644 --- a/src/coffee/models/item.coffee +++ b/src/coffee/models/item.coffee @@ -21,15 +21,16 @@ module.exports = class Item extends BaseModel constructor: (attributes={}, options={})-> if not attributes.name? then throw new Error 'attributes.name is required' - attributes.description ?= null - attributes.group ?= Item.Group.Other - attributes.isGatherable ?= false - attributes.modVersion ?= null - attributes.officialUrl ?= null - attributes.slug ?= ItemSlug.slugify attributes.name - attributes.videos ?= [] + attributes.description ?= null + attributes.group ?= Item.Group.Other + attributes.ignoreDuringCrafting ?= false + attributes.isGatherable ?= false + attributes.modVersion ?= null + attributes.officialUrl ?= null + attributes.slug ?= ItemSlug.slugify attributes.name + attributes.videos ?= [] - options.logEvents ?= false + options.logEvents ?= false super attributes, options @on Event.change + ':modVersion', => diff --git a/src/coffee/models/item_page.coffee b/src/coffee/models/item_page.coffee index c60333654..b8b51d951 100644 --- a/src/coffee/models/item_page.coffee +++ b/src/coffee/models/item_page.coffee @@ -5,11 +5,10 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### -BaseModel = require './base_model' -CraftingPlan = require './crafting_plan' -_ = require 'underscore' -{Event} = require '../constants' -{Url} = require '../constants' +_ = require 'underscore' +BaseModel = require './base_model' +{Event} = require '../constants' +{Url} = require '../constants' ######################################################################################################################## @@ -57,4 +56,4 @@ module.exports = class ItemPage extends BaseModel result = _.values result return null unless result.length > 0 - return result.sort (a, b)-> a.compareTo b \ No newline at end of file + return result.sort (a, b)-> a.compareTo b diff --git a/src/coffee/models/mod_pack.coffee b/src/coffee/models/mod_pack.coffee index 18875d22d..b4a8ac388 100644 --- a/src/coffee/models/mod_pack.coffee +++ b/src/coffee/models/mod_pack.coffee @@ -20,24 +20,35 @@ module.exports = class ModPack extends BaseModel super attributes, options @_mods = [] + @_cache = {} + + @on Event.change, => @_cache = {} # Public Methods ############################################################################### findItem: (itemSlug, options={})-> options.includeDisabled ?= false + key = "#{itemSlug}-#{options.includeDisabled}" + @_cache.itemBySlug ?= {} + item = @_cache.itemBySlug[key] + return item if item? + if itemSlug.isQualified mod = @getMod itemSlug.mod if mod? item = mod.findItem itemSlug, options - return item if item? - for mod in @_mods - continue unless mod.enabled or options.includeDisabled - item = mod.findItem itemSlug, options - return item if item? + if not item? + for mod in @_mods + continue unless mod.enabled or options.includeDisabled + item = mod.findItem itemSlug, options + break if item? - return null + if item? + @_cache.itemBySlug[key] = item + + return item findItemByName: (name, options={})-> options.enableAsNeeded ?= false @@ -82,10 +93,16 @@ module.exports = class ModPack extends BaseModel return null - findRecipes: (itemSlug, result=[], options={})-> + findRecipes: (itemSlug, options={})-> options.alwaysFromOwningMod ?= false return null unless itemSlug? + key = "#{itemSlug}-#{options.alwaysFromOwningMod}" + @_cache.recipesBySlug ?= {} + result = @_cache.recipesBySlug[key] + return result if result? + + result = [] for mod in @_mods if not mod.enabled owningMod = itemSlug.isQualified and (itemSlug.mod is mod.slug) @@ -93,8 +110,16 @@ module.exports = class ModPack extends BaseModel mod.findRecipes itemSlug, result, options + @_cache.recipesBySlug[key] = result 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 ############################################################################# addMod: (mod)-> diff --git a/src/coffee/models/parser_versions/mod_version_parser_v1.coffee b/src/coffee/models/parser_versions/mod_version_parser_v1.coffee index 678ba87c3..facb74f92 100644 --- a/src/coffee/models/parser_versions/mod_version_parser_v1.coffee +++ b/src/coffee/models/parser_versions/mod_version_parser_v1.coffee @@ -63,6 +63,13 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase @_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...)-> 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"' @@ -148,11 +155,16 @@ module.exports = class ModVersionParserV1 extends CommandParserVersionBase _buildItem: (modVersion, itemData)-> @_lineNumber = itemData.line - itemData.gatherable ?= false - itemData.recipes ?= [] + itemData.gatherable ?= false + itemData.ignoreDuringCrafting ?= false + itemData.recipes ?= [] 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 itemData.slug = item.slug else diff --git a/src/coffee/models/recipe.coffee b/src/coffee/models/recipe.coffee index 40b79e4fa..680dbf8a0 100644 --- a/src/coffee/models/recipe.coffee +++ b/src/coffee/models/recipe.coffee @@ -35,7 +35,10 @@ module.exports = class Recipe extends BaseModel options.logEvents ?= false super attributes, options + @_computeQuantities attributes.pattern + @on Event.change + ':modVersion', => @_slug = null + @on Event.change + ':pattern', => @_patternCache = null # Class Methods ################################################################################ @@ -77,24 +80,22 @@ module.exports = class Recipe extends BaseModel getQuantityRequired: (itemSlug)-> total = 0 - for i in [0...@pattern.length] - stack = @getStackAtSlot i - continue unless stack? - + for stack, index in @input if ItemSlug.equal stack.itemSlug, itemSlug - total += stack.quantity + total += @_quantities[index] * stack.quantity + return total hasAllTools: (modPack)-> modPack ?= @modVersion?.mod?.modPack - return true unless modPack + return true unless modPack? for stack in @tools return false unless modPack.findItem stack.itemSlug return true isConditionSatisfied: (modPack)-> - return true unless @condition + return true unless @condition? modPack ?= @modVersion?.mod?.modPack result = false @@ -145,36 +146,38 @@ module.exports = class Recipe extends BaseModel # 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, - 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 ############################################################################# @@ -215,6 +218,22 @@ module.exports = class Recipe extends BaseModel # 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)-> return unless pattern? diff --git a/src/coffee/models/recipe_step.coffee b/src/coffee/models/recipe_step.coffee deleted file mode 100644 index 28b65b7c9..000000000 --- a/src/coffee/models/recipe_step.coffee +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/coffee/models/simple_inventory.coffee b/src/coffee/models/simple_inventory.coffee new file mode 100644 index 000000000..d809920f2 --- /dev/null +++ b/src/coffee/models/simple_inventory.coffee @@ -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 diff --git a/src/coffee/models/step.coffee b/src/coffee/models/step.coffee deleted file mode 100644 index 71a5e80c6..000000000 --- a/src/coffee/models/step.coffee +++ /dev/null @@ -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 diff --git a/test/crafting/crafting_plan.test.coffee b/test/crafting/crafting_plan.test.coffee index 7271a6287..22e08d6ef 100644 --- a/test/crafting/crafting_plan.test.coffee +++ b/test/crafting/crafting_plan.test.coffee @@ -18,44 +18,49 @@ describe 'crafting_plan.coffee', -> plans.length.should.equal 1 plan = plans[0] - plan.required.unparse().should.equal 'coal' - plan.produced.unparse().should.equal 'coal' + 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', -> plans = fixtures.makePlans [1, 'test__charcoal'] plans.length.should.equal 1 plan = plans[0] - plan.required.unparse().should.equal 'coal:8.oak_wood' - plan.produced.unparse().should.equal '8.charcoal' - (s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>>8 test__charcoal'] + plan.computeRequired() + plan.need.unparse().should.equal 'coal:8.oak_wood' + plan.made.unparse().should.equal '8.charcoal' + (s.toString() for s in plan.steps).should.eql ['1x 8 test__oak_wood,test__coal>.0. ... .1.>>8 test__charcoal'] it 'can compute a large quantity of a single item with one single step plan', -> plans = fixtures.makePlans [15, 'test__charcoal'] plans.length.should.equal 1 plan = plans[0] - plan.required.unparse().should.equal '2.coal:16.oak_wood' - plan.produced.unparse().should.equal '16.charcoal' - (s.toString() for s in plan.steps).should.eql ['2x 8 test__oak_wood,test__coal>>8 test__charcoal'] + 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.required.unparse().should.equal 'coal:8.iron_ore:8.oak_wood' - plan.produced.unparse().should.equal '7.charcoal:8.iron_ingot' + 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>>8 test__charcoal' - '1x 8 test__iron_ore,test__charcoal>test__furnace>8 test__iron_ingot' + '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] - plan.required.unparse().should.equal 'coal:8.iron_ore' - plan.produced.unparse().should.equal '8.iron_ingot' + plan.computeRequired() + plan.need.unparse().should.equal 'coal:8.iron_ore' + plan.made.unparse().should.equal '8.iron_ingot' (s.toString() for s in plan.steps).should.eql [ - '1x 8 test__iron_ore,test__coal>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', -> @@ -63,33 +68,40 @@ describe 'crafting_plan.coffee', -> plans.length.should.equal 4 for plan in plans - plan.required.unparse().should.match /16.copper_ore.*8.iron_ore/ - plan.produced.unparse().should.match /copper_block.*:iron_sword/ + plan.computeRequired() + plan.need.unparse().should.match /16.copper_ore.*8.iron_ore/ + plan.made.unparse().should.match /copper_block.*:iron_sword/ plan = plans[0] - plan.required.unparse().should.match /3.coal.*9.oak_wood/ - plan.produced.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[4]}".should.equal '2x 8 test__copper_ore,test__coal>test__furnace>8 test__copper_ingot' + plan.need.unparse().should.match /3.coal.*9.oak_wood/ + plan.made.unparse().should.match /7.charcoal/ + "#{plan.steps[3]}".should.equal \ + '1x 8 test__iron_ore,test__charcoal>.0. ... .1.>test__furnace>8 test__iron_ingot' + "#{plan.steps[4]}".should.equal \ + '2x 8 test__copper_ore,test__coal>.0. ... .1.>test__furnace>8 test__copper_ingot' plan.steps.length.should.equal 7 plan = plans[1] - plan.required.unparse().should.match /3.coal.*:oak_wood/ - plan.produced.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[3]}".should.equal '2x 8 test__copper_ore,test__coal>test__furnace>8 test__copper_ingot' + 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.required.unparse().should.match /^coal.*9.oak_wood/ - plan.produced.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[4]}".should.equal '2x 8 test__copper_ore,test__charcoal>test__furnace>8 test__copper_ingot' + 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.required.unparse().should.match /2.coal.*9.oak_wood/ - plan.produced.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[4]}".should.equal '2x 8 test__copper_ore,test__charcoal>test__furnace>8 test__copper_ingot' + 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 diff --git a/test/crafting/fixtures.coffee b/test/crafting/fixtures.coffee index c1e025d45..018849745 100644 --- a/test/crafting/fixtures.coffee +++ b/test/crafting/fixtures.coffee @@ -6,6 +6,7 @@ All rights reserved. ### GraphBuilder = require '../../src/coffee/models/crafting/graph_builder' +Inventory = require '../../src/coffee/models/inventory' ItemSlug = require '../../src/coffee/models/item_slug' Mod = require '../../src/coffee/models/mod' ModPack = require '../../src/coffee/models/mod_pack' @@ -30,8 +31,10 @@ MOD_VERSION_FILE = pattern: .00 .00 ... item: Coal + gatherable: yes item: Cobblestone + gatherable: yes item: Copper Block recipe: @@ -56,6 +59,7 @@ MOD_VERSION_FILE = quantity: 9 item: Copper Ore + gatherable: yes item: Furnace recipe: @@ -64,6 +68,7 @@ MOD_VERSION_FILE = tools: Crafting Table item: Iron Ore + gatherable: yes item: Iron Ingot recipe: @@ -95,12 +100,22 @@ MOD_VERSION_FILE = quantity: 4 item: Oak Wood + gatherable: yes item: Stick recipe: input: Oak Planks pattern: .0. .0. ... 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 = makeGraphBuilder: -> - return new GraphBuilder modPack:fixtures.makeModPack() + return new GraphBuilder modPack:fixtures.makeModPack(), want:new Inventory makeModPack: -> modPack = new ModPack @@ -125,17 +140,21 @@ module.exports = fixtures = makePlans: (stacks...)-> modPack = fixtures.makeModPack() - graphBuilder = fixtures.makeGraphBuilder() + want = new Inventory 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() - planBuilder = new PlanBuilder graphBuilder.rootNode, wanted:graphBuilder.wanted + planBuilder = new PlanBuilder graphBuilder.rootNode, modPack, want:graphBuilder.want return planBuilder.producePlans() makeTree: (itemSlug, quantity=1)-> - builder = fixtures.makeGraphBuilder() - builder.wanted.add ItemSlug.slugify itemSlug + want = new Inventory + want.add ItemSlug.slugify itemSlug + + builder = new GraphBuilder modPack:fixtures.makeModPack(), want:want builder.expandGraph() return builder.rootNode diff --git a/test/crafting/graph_builder.test.coffee b/test/crafting/graph_builder.test.coffee index 254bb7ee8..9d93e8e49 100644 --- a/test/crafting/graph_builder.test.coffee +++ b/test/crafting/graph_builder.test.coffee @@ -22,7 +22,7 @@ describe 'GraphBuilder.coffee', -> describe 'expand', -> 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.rootNode.depth.should.equal 6 @@ -45,7 +45,7 @@ describe 'GraphBuilder.coffee', -> describe 'can build a tree for', -> runSingleItemTreeBuildingTest = (slug, depth, size)-> - builder.wanted.add ItemSlug.slugify slug + builder.want.add ItemSlug.slugify slug builder.expandGraph 100 builder.rootNode.depth.should.equal depth @@ -68,4 +68,7 @@ describe 'GraphBuilder.coffee', -> runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18 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 diff --git a/test/crafting/plan_evaluator.test.coffee b/test/crafting/plan_evaluator.test.coffee new file mode 100644 index 000000000..09975f1a6 --- /dev/null +++ b/test/crafting/plan_evaluator.test.coffee @@ -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] diff --git a/test/mod_version.test.coffee b/test/mod_version.test.coffee index a5dd36b61..46346bcd3 100644 --- a/test/mod_version.test.coffee +++ b/test/mod_version.test.coffee @@ -97,5 +97,5 @@ describe 'mod_version.coffee', -> (r.output[0].itemSlug.item for r in recipes).sort().should.eql ['bucket', 'cake', 'cake'] 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