From 6ea97ac3aaecd2266718b05641b4d828e453eec7 Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Sat, 19 Sep 2015 09:31:14 -0700 Subject: [PATCH] Add plan builder and tests --- .../models/crafting/crafting_node.coffee | 35 ++++++-- .../models/crafting/crafting_plan.coffee | 12 +++ .../models/crafting/inventory_node.coffee | 2 + src/coffee/models/crafting/item_node.coffee | 3 + .../models/crafting/plan_builder.coffee | 84 +++++++++++++++++++ src/coffee/models/crafting/recipe_node.coffee | 1 + test/crafting/fixtures.coffee | 21 +++++ test/crafting/graph_builder.test.coffee | 12 +-- test/crafting/plan_builder.test.coffee | 38 +++++++++ 9 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 src/coffee/models/crafting/crafting_plan.coffee create mode 100644 src/coffee/models/crafting/plan_builder.coffee create mode 100644 test/crafting/plan_builder.test.coffee diff --git a/src/coffee/models/crafting/crafting_node.coffee b/src/coffee/models/crafting/crafting_node.coffee index 26fd3ace6..bf070fde8 100644 --- a/src/coffee/models/crafting/crafting_node.coffee +++ b/src/coffee/models/crafting/crafting_node.coffee @@ -5,18 +5,29 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### +_ = require 'underscore' + ######################################################################################################################## module.exports = class CraftingNode + @::TYPES = + INVENTORY: 0 + ITEM: 1 + RECIPE: 2 + + @::TYPE = null + constructor: (options={})-> if not options.modPack? then throw new Error 'options.modPack is required' @modPack = options.modPack - @_children = [] - @_complete = null - @_valid = null + @_children = [] + @_complete = null + @_id = _.uniqueId 'node_' + @_rotations = 0 + @_valid = null # Public Methods ############################################################################### @@ -25,8 +36,9 @@ module.exports = class CraftingNode for child in @_createChildren() child.parent = this - @_children.push child - queue.push child + if child.valid + @_children.push child + queue.push child return queue acceptVisitor: (visitor)-> @@ -47,6 +59,10 @@ module.exports = class CraftingNode leave = visitor['onLeaveOtherNode'] if leave? then leave.call visitor, this + rotateChildren: -> + @_children.push @_children.shift() + @_rotations += 1 + # Property Methods ############################################################################# getChildren: -> @@ -66,6 +82,12 @@ module.exports = class CraftingNode maxDepth = Math.max maxDepth, child.getDepth() + 1 return maxDepth + getId: -> + return @_id + + getRotations: -> + return @_rotations + getSize: -> size = 1 for child in @children @@ -85,7 +107,10 @@ module.exports = class CraftingNode complete: { get:@prototype.isComplete } completeText: { get:@prototype.getCompleteText } depth: { get:@prototype.getDepth } + id: { get:@prototype.getId } + rotations: { get:@prototype.getRotations } size: { get:@prototype.getSize } + valid: { get:@prototype.isValid } # Virtual Methods ############################################################################## diff --git a/src/coffee/models/crafting/crafting_plan.coffee b/src/coffee/models/crafting/crafting_plan.coffee new file mode 100644 index 000000000..ca86baa92 --- /dev/null +++ b/src/coffee/models/crafting/crafting_plan.coffee @@ -0,0 +1,12 @@ +### +Crafting Guide - crafting_plan.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +######################################################################################################################## + +module.exports = class CraftingPlan + + constructor: -> diff --git a/src/coffee/models/crafting/inventory_node.coffee b/src/coffee/models/crafting/inventory_node.coffee index a29607f43..e0a00102d 100644 --- a/src/coffee/models/crafting/inventory_node.coffee +++ b/src/coffee/models/crafting/inventory_node.coffee @@ -14,6 +14,7 @@ module.exports = class InventoryNode extends CraftingNode @::ENTER_METHOD = 'onEnterInventoryNode' @::LEAVE_METHOD = 'onLeaveInventoryNode' + @::TYPE = CraftingNode::TYPES.INVENTORY constructor: (options={})-> if not options.inventory? then throw new Error 'options.inventory is required' @@ -38,6 +39,7 @@ module.exports = class InventoryNode extends CraftingNode _checkValidity: -> for child in @children return false unless child.isValid + return true # Object Overrides ############################################################################# diff --git a/src/coffee/models/crafting/item_node.coffee b/src/coffee/models/crafting/item_node.coffee index b886fcaa3..0065ef683 100644 --- a/src/coffee/models/crafting/item_node.coffee +++ b/src/coffee/models/crafting/item_node.coffee @@ -14,6 +14,7 @@ module.exports = class ItemNode extends CraftingNode @::ENTER_METHOD = 'onEnterItemNode' @::LEAVE_METHOD = 'onLeaveItemNode' + @::TYPE = CraftingNode::TYPES.ITEM constructor: (options={})-> if not options.item? then throw new Error 'options.item is required' @@ -58,8 +59,10 @@ module.exports = class ItemNode extends CraftingNode return false _checkValidity: -> + return true unless @children.length > 0 for child in @children return true if child.isValid + return false # Object Overrides ############################################################################# diff --git a/src/coffee/models/crafting/plan_builder.coffee b/src/coffee/models/crafting/plan_builder.coffee new file mode 100644 index 000000000..63383b9e3 --- /dev/null +++ b/src/coffee/models/crafting/plan_builder.coffee @@ -0,0 +1,84 @@ +### +Crafting Plan - plan_builder.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +CraftingNode = require './crafting_node' + +######################################################################################################################## + +module.exports = class PlanBuilder + + constructor: (rootNode)-> + if not rootNode? then throw new Error 'rootNode is required' + + @_choiceNodes = [] + @_complete = false + @_plans = [] + @_rootNode = rootNode + + @_isolateChoiceNodes() + + # Public Methods ############################################################################### + + producePlans: (maxPlans=null)-> + maxPlans = if maxPlans then @plans.length + maxPlans else Number.MAX_VALUE + + while @plans.length < maxPlans and not @complete + plan = @_captureCurrentPlan() + @plans.push plan if plan.length > 0 + @_incrementChoiceNodes() + + return @_plans + + # Property Methods ############################################################################# + + isComplete: -> + return @_complete + + getPlans: -> + return @_plans + + Object.defineProperties @prototype, + complete: { get:@prototype.isComplete } + plans: { get:@prototype.getPlans } + + # Private Methods ############################################################################## + + _captureCurrentPlan: -> + toVisit = [@_rootNode] + plan = [] + + while toVisit.length > 0 + node = toVisit.shift() + + if node.TYPE is CraftingNode::TYPES.INVENTORY + toVisit.push(c) for c in node.children + else if node.TYPE is CraftingNode::TYPES.ITEM + toVisit.push node.children[0] if node.children.length > 0 + else if node.TYPE is CraftingNode::TYPES.RECIPE + plan.push node.recipe + toVisit.push(c) for c in node.children + + return plan.reverse() + + _incrementChoiceNodes: -> + index = @_choiceNodes.length - 1 + while true + if index is -1 + @_complete = true + return + + node = @_choiceNodes[index] + node.rotateChildren() + + return unless node.rotations % node.children.length is 0 + index -= 1 + + _isolateChoiceNodes: -> + @_rootNode.acceptVisitor + onEnterItemNode: (node)=> + if node.children.length > 1 + @_choiceNodes.push node diff --git a/src/coffee/models/crafting/recipe_node.coffee b/src/coffee/models/crafting/recipe_node.coffee index 5eaa50d73..1bf1cce9c 100644 --- a/src/coffee/models/crafting/recipe_node.coffee +++ b/src/coffee/models/crafting/recipe_node.coffee @@ -14,6 +14,7 @@ module.exports = class RecipeNode extends CraftingNode @::ENTER_METHOD = 'onEnterRecipeNode' @::LEAVE_METHOD = 'onLeaveRecipeNode' + @::TYPE = CraftingNode::TYPES.RECIPE constructor: (options={})-> if not options.recipe? then throw new Error 'options.recipe is required' diff --git a/test/crafting/fixtures.coffee b/test/crafting/fixtures.coffee index 5f7fefc72..df7d34db5 100644 --- a/test/crafting/fixtures.coffee +++ b/test/crafting/fixtures.coffee @@ -31,6 +31,27 @@ MOD_VERSION_FILE = item: Cobblestone + item: Copper Block + recipe: + input: Copper Ingot + pattern: 000 000 000 + tools: Crafting Table + + item: Copper Ingot + recipe: + input: Copper Ore, Coal + pattern: .0. ... .1. + tools: Furnace + recipe: + input: Copper Ore, Charcoal + pattern: .0. ... .1. + tools: Furnace + recipe: + input: Copper Block + pattern: ... .0. ... + + item: Copper Ore + item: Furnace recipe: input: Cobblestone diff --git a/test/crafting/graph_builder.test.coffee b/test/crafting/graph_builder.test.coffee index bd5dd1918..254bb7ee8 100644 --- a/test/crafting/graph_builder.test.coffee +++ b/test/crafting/graph_builder.test.coffee @@ -5,8 +5,8 @@ Copyright (c) 2015 by Redwood Labs All rights reserved. ### -fixtures = require './fixtures' -ItemSlug = require '../../src/coffee/models/item_slug' +fixtures = require './fixtures' +ItemSlug = require '../../src/coffee/models/item_slug' ######################################################################################################################## @@ -61,9 +61,11 @@ describe 'GraphBuilder.coffee', -> it 'an item with multiple inputs', -> runSingleItemTreeBuildingTest 'test__lever', 8, 9 - it 'can make a tree for an item with multiple recipes', -> + it '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', -> + it 'an item with multiple inputs and multiple recipes', -> runSingleItemTreeBuildingTest 'test__iron_sword', 8, 18 - logger.debug builder.toString() + + it 'an item with one recursive recipe', -> + runSingleItemTreeBuildingTest 'test__copper_ingot', 10, 24 diff --git a/test/crafting/plan_builder.test.coffee b/test/crafting/plan_builder.test.coffee new file mode 100644 index 000000000..fe1d09a67 --- /dev/null +++ b/test/crafting/plan_builder.test.coffee @@ -0,0 +1,38 @@ +### +Crafting Guide - plan_builder.test.coffee + +Copyright (c) 2015 by Redwood Labs +All rights reserved. +### + +fixtures = require './fixtures' +PlanBuilder = require '../../src/coffee/models/crafting/plan_builder' + +######################################################################################################################## + +describe 'plan_builder.coffee', -> + + printPlan = (plan)-> + return ((s.slug.replace(/^.*>.*>/, '') for s in plan)).join ' > ' + + it 'generates no plans for a gatherable item', -> + builder = new PlanBuilder fixtures.makeTree 'test__oak_wood' + plans = builder.producePlans 100 + + plans.length.should.equal 0 + builder.complete.should.be.true + + it 'can find a multi-step plan', -> + builder = new PlanBuilder fixtures.makeTree 'test__lever' + plans = builder.producePlans 100 + + printPlan(plans[0]).should.equal '4 test__oak_planks > 4 test__stick > test__lever' + plans.length.should.equal 1 + builder.complete.should.be.true + + it 'can find multiple plans', -> + builder = new PlanBuilder fixtures.makeTree 'test__iron_ingot' + plans = builder.producePlans 100 + + printPlan(plans[0]).should.equal 'test__charcoal > test__iron_ingot' + printPlan(plans[1]).should.equal 'test__iron_ingot'