diff --git a/src/coffee/models/crafting/crafting_node.coffee b/src/coffee/models/crafting/crafting_node.coffee new file mode 100644 index 000000000..260317803 --- /dev/null +++ b/src/coffee/models/crafting/crafting_node.coffee @@ -0,0 +1,99 @@ +### +Crafting Guide - crafting_node.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +######################################################################################################################## + +module.exports = class CraftingNode + + constructor: (options={})-> + if not options.modPack? then throw new Error 'options.modPack is required' + + @modPack = options.modPack + + @_children = [] + @_complete = null + @_valid = null + + # Public Methods ############################################################################### + + expand: (queue=[])-> + return queue if @children.length > 0 + + for child in @_createChildren() + child.parent = this + @_children.push child + queue.push child + return queue + + getNode: (path)-> + return null unless _.isArray(path) and path.length > 0 + + child = @children[path[0]] + return null unless child? + + return child.getPath path[1..] + + # Property Methods ############################################################################# + + getChildren: -> + return @_children + + isComplete: -> + if not @_complete? + @_complete = @_checkCompleteness() + return @_complete + + getCompleteText: -> + return if @complete then "✓" else "✗" + + getDepth: -> + maxDepth = 1 + for child in @children + maxDepth = Math.max maxDepth, child.getDepth() + 1 + return maxDepth + + getSize: -> + size = 1 + for child in @children + size += child.size + return size + + getNodeType: -> + return @_nodeType + + isValid: -> + return @_valid if @_valid? + + valid = @_checkValidity() + @_valid = false if not valid + + return valid + + Object.defineProperties @prototype, + children: { get:@prototype.getChildren } + complete: { get:@prototype.isComplete } + completeText: { get:@prototype.getCompleteText } + depth: { get:@prototype.getDepth } + size: { get:@prototype.getSize } + + + # Virtual Methods ############################################################################## + + # Subclasses must override this method to create such children as are appropriate for that kind of node. The new + # nodes should be appended to the given array. + _createChildren: (result)-> + throw new Error "#{@constructor.name} must override the _createChildren method" + + # Subclasses must override this method to indicate whether the node has at least one "complete" subtree rooted with + # itself. Completeness may be defined differently by different subclasses. + _checkCompleteness: -> + throw new Error "#{@constructor.name} must override the _checkCompletness method" + + # Subclasses must override this method to determine whether the subtree represented by a node should still be + # explored for having a useful crafting plan. + _checkValidity: -> + throw new Error "#{@constructor.name} must override the _checkValidity method" diff --git a/src/coffee/models/crafting/graph_builder.coffee b/src/coffee/models/crafting/graph_builder.coffee new file mode 100644 index 000000000..4d83efc55 --- /dev/null +++ b/src/coffee/models/crafting/graph_builder.coffee @@ -0,0 +1,72 @@ +### +Crafting Guide - graph_builder.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +Inventory = require '../inventory' +InventoryNode = require './inventory_node' + +######################################################################################################################## + +module.exports = class GraphBuilder + + constructor: (options={})-> + if not options.modPack? then throw new Error 'options.modPack is required' + + @modPack = options.modPack + @_wanted = options.wanted ?= new Inventory + + @_wanted.on 'change', => @reset() + + @reset() + + # Public Methods ############################################################################### + + expandGraph: (steps=null)-> + maxSteps = if steps? then @_stepCount + steps else Number.MAX_VALUE + @_queue ?= [] + + while true + break if @_queue.length is 0 + break if @_stepCount >= maxSteps + + node = @_queue.shift() + break unless node? + + node.expand @_queue + @_stepCount += 1 + + reset: -> + @_rootNode = new InventoryNode modPack:@modPack, inventory:@wanted + @_queue = [@_rootNode] + @_stepCount = 0 + + # Property Methods ############################################################################# + + isComplete: -> + return false unless @_rootNode? + return false unless @_queue? + return false unless @_queue.length is 0 + return true + + getRootNode: -> + return @_rootNode + + getStepCount: -> + return @_stepCount + + getWanted: -> + return @_wanted + + Object.defineProperties @prototype, + complete: { get:@prototype.isComplete } + rootNode: { get:@prototype.getRootNode } + stepCount: { get:@prototype.getStepCount } + wanted: { get:@prototype.getWanted } + + # Object Overrides ############################################################################ + + toString: (indent='')-> + "Build Tree\n#{@_rootNode.toString(indent + ' ')}" diff --git a/src/coffee/models/crafting/inventory_node.coffee b/src/coffee/models/crafting/inventory_node.coffee new file mode 100644 index 000000000..fa7614836 --- /dev/null +++ b/src/coffee/models/crafting/inventory_node.coffee @@ -0,0 +1,47 @@ +### +Crafting Guide - crafting_plan_node.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +CraftingNode = require './crafting_node' +ItemNode = require './item_node' + +######################################################################################################################## + +module.exports = class InventoryNode extends CraftingNode + + constructor: (options={})-> + if not options.inventory? then throw new Error 'options.inventory is required' + super options + + @inventory = options.inventory + + # CraftingNode Overrides ####################################################################### + + _createChildren: (result=[])-> + @inventory.each (stack)=> + item = @modPack.findItem stack.itemSlug + if not item? then throw new Error "Could not find an item for slug: #{stack.itemSlug}" + result.push new ItemNode modPack:@modPack, item:item + return result + + _checkCompleteness: -> + for child in @children + return false unless child.isComplete + return true + + _checkValidity: -> + for child in @children + return false unless child.isValid + + # Object Overrides ############################################################################# + + toString: (indent='')-> + completeText = if @complete then 'complete' else 'incomplete' + parts = ["#{indent}#{@completeText} InventoryNode for #{@inventory}"] + nextIndent = indent + ' ' + for child in @children + parts.push child.toString nextIndent + return parts.join '\n' diff --git a/src/coffee/models/crafting/item_node.coffee b/src/coffee/models/crafting/item_node.coffee new file mode 100644 index 000000000..4c6ee324e --- /dev/null +++ b/src/coffee/models/crafting/item_node.coffee @@ -0,0 +1,69 @@ +### +Crafting Guide - item_node.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +CraftingNode = require './crafting_node' +RecipeNode = require './recipe_node' + +######################################################################################################################## + +module.exports = class ItemNode extends CraftingNode + + constructor: (options={})-> + if not options.item? then throw new Error 'options.item is required' + super options + + @item = options.item + @_recipes = null + + # Property Methods ############################################################################# + + getRecipes: -> + if not @_recipes? + @_recipes = @modPack.findRecipes @item.slug + return @_recipes or [] + + isGatherable: -> + return true if @item.isGatherable + return true unless @getRecipes().length > 0 + return false + + Object.defineProperties @prototype, + gatherable: { get:@prototype.isGatherable } + recipes: { get:@prototype.getRecipes } + + # CraftingNode Overrides ####################################################################### + + _createChildren: (result=[])-> + recipes = @getRecipes() + return [] unless recipes.length > 0 + + for recipe in recipes + result.push new RecipeNode modPack:@modPack, recipe:recipe + + return result + + _checkCompleteness: -> + return true if @gatherable + return false unless @children? + + for child in @children + return true if child.isComplete + return false + + _checkValidity: -> + for child in @children + return true if child.isValid + + # Object Overrides ############################################################################# + + toString: (indent)-> + completeText = if @complete then 'complete' else 'incomplete' + parts = ["#{indent}#{@completeText} ItemNode for #{@item.name}"] + nextIndent = indent + ' ' + for child in @children + parts.push child.toString nextIndent + return parts.join '\n' diff --git a/src/coffee/models/crafting/recipe_node.coffee b/src/coffee/models/crafting/recipe_node.coffee new file mode 100644 index 000000000..4fad67cc4 --- /dev/null +++ b/src/coffee/models/crafting/recipe_node.coffee @@ -0,0 +1,61 @@ +### +Crafting Guide - recipe_node.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +CraftingNode = require './crafting_node' +# ItemNode = require './item_node' # don't include here, causes a cycle + +######################################################################################################################## + +module.exports = class RecipeNode extends CraftingNode + + constructor: (options={})-> + if not options.recipe? then throw new Error 'options.recipe is required' + super options + + @recipe = options.recipe + + # CraftingNode Overrides ####################################################################### + + _createChildren: (result=[])-> + ItemNode = require './item_node' # include here to avoid a cycle + + for stack in @recipe.input + item = @modPack.findItem stack.itemSlug + result.push new ItemNode modPack:@modPack, item:item + return result + + _checkCompleteness: -> + for child in @children + return false unless child.isComplete + return true + + _checkValidity: -> + for child in @children + return false unless child.isValid + + return false if @_isRepeatedRecipe() + return true + + # Private Methods ############################################################################## + + _isRepeatedRecipe: -> + nextParent = @parent + while nextParent? + return true if nextParent.recipe is @recipe + nextParent = nextParent.parent + + return false + + # Object Overrides ############################################################################ + + toString: (indent)-> + completeText = if @complete then 'complete' else 'incomplete' + parts = ["#{indent}#{@completeText} RecipeNode for #{@recipe.slug}"] + nextIndent = indent + ' ' + for child in @children + parts.push child.toString nextIndent + return parts.join '\n' diff --git a/src/coffee/models/inventory.coffee b/src/coffee/models/inventory.coffee index 8aca9b788..f26184c39 100644 --- a/src/coffee/models/inventory.coffee +++ b/src/coffee/models/inventory.coffee @@ -170,7 +170,7 @@ module.exports = class Inventory extends BaseModel # Object Overrides ############################################################################# toString: -> - result = [@constructor.name, " (", @cid, ") { items: ["] + result = [@constructor.name, " (", @cid, ") {items: ["] needsDelimiter = false @each (stack)-> diff --git a/test/crafting/graph_builder.test.coffee b/test/crafting/graph_builder.test.coffee new file mode 100644 index 000000000..aaeb0e633 --- /dev/null +++ b/test/crafting/graph_builder.test.coffee @@ -0,0 +1,142 @@ +### +Crafting Guide - crafting_node.test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +GraphBuilder = require '../../src/coffee/models/crafting/graph_builder' +ItemSlug = require '../../src/coffee/models/item_slug' +Mod = require '../../src/coffee/models/mod' +ModPack = require '../../src/coffee/models/mod_pack' +ModVersion = require '../../src/coffee/models/mod_version' + +######################################################################################################################## + +SAMPLE_MOD_VERSION_TEXT = """ + schema: 1 + + item: Charcoal + recipe: + input: 8 Oak Wood, Coal + pattern: .0. ... .1. + + item: Crafting Table + recipe: + input: Oak Planks + pattern: .00 .00 ... + + item: Coal + + item: Cobblestone + + item: Furnace + recipe: + input: Cobblestone + pattern: 000 0.0 000 + tools: Crafting Table + + item: Iron Ore + + item: Iron Ingot + recipe: + input: 8 Iron Ore, Charcoal + pattern: .0. ... .1. + tools: Furnace + recipe: + input: 8 Iron Ore, Coal + pattern: .0. ... .1. + tools: Furnace + + item: Iron Sword + recipe: + input: Iron Ingot, Stick + pattern: .0. .0. .1. + tools: Crafting Table + + item: Lever + recipe: + input: Stick, Cobblestone + pattern: .0. .1. ... + + item: Oak Planks + recipe: + input: Oak Wood + pattern: ... .0. ... + quantity: 4 + + item: Oak Wood + + item: Stick + recipe: + input: Oak Planks + pattern: .0. .0. ... + quantity: 4 +""" + +builder = mod = modPack = modVersion = null + +######################################################################################################################## + +describe.only 'GraphBuilder.coffee', -> + + beforeEach -> + modPack = new ModPack + + mod = new Mod name:'Test', slug:'test' + modPack.addMod mod + + modVersion = new ModVersion modSlug:'test', version:'0.0' + modVersion.parse SAMPLE_MOD_VERSION_TEXT + mod.addModVersion modVersion + + builder = new GraphBuilder modPack:modPack + + describe 'expand', -> + + it 'can work a few steps at a time', -> + builder.wanted.add ItemSlug.slugify 'test__iron_sword' + builder.expandGraph 9 + + builder.rootNode.depth.should.equal 6 + builder.rootNode.size.should.equal 13 + builder.complete.should.be.false + + builder.expandGraph 9 + + builder.rootNode.depth.should.equal 8 + builder.rootNode.size.should.equal 18 + builder.complete.should.be.true + + it 'works properly with an empty inventory', -> + builder.expandGraph 100 + + builder.rootNode.depth.should.equal 1 + builder.rootNode.size.should.equal 1 + builder.complete.should.be.true + + describe 'can build a tree for', -> + + runSingleItemTreeBuildingTest = (slug, depth, size)-> + builder.wanted.add ItemSlug.slugify slug + builder.expandGraph 100 + + builder.rootNode.depth.should.equal depth + builder.rootNode.size.should.equal size + builder.complete.should.be.true + + it 'a gatherable item', -> + runSingleItemTreeBuildingTest 'test__oak_wood', 2, 2 + + it 'a single-step item', -> + runSingleItemTreeBuildingTest 'test__crafting_table', 6, 6 + + it 'an item with multiple inputs', -> + runSingleItemTreeBuildingTest 'test__lever', 8, 9 + + it 'can make a tree for an item with multiple recipes', -> + runSingleItemTreeBuildingTest 'test__iron_ingot', 6, 11 + + it 'can make a tree for an item with multiple inputs and multiple recipes', -> + runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18 + logger.debug builder.toString() diff --git a/test/test_helper.coffee b/test/test_helper.coffee index d4b7bbd07..744bc949a 100644 --- a/test/test_helper.coffee +++ b/test/test_helper.coffee @@ -21,7 +21,7 @@ global.util = require 'util' global.w = require 'when' {Logger} = require 'crafting-guide-common' -global.logger = new Logger level:Logger.FATAL +global.logger = new Logger level:Logger.DEBUG require '../src/coffee/polyfill' require '../src/coffee/underscore_mixins'