module Seshbot::Packing::Recipe

Public Class Methods

_calculate_recipe_factor(recipe, items) click to toggle source

private

# File lib/seshbot/packing/recipe.rb, line 83
def _calculate_recipe_factor(recipe, items)
  recipe_inputs = recipe['inputs'].nil? ? [recipe] : recipe['inputs']

  recipe_input_factors = recipe_inputs.map do |inputs|
    input_fragment = inputs['input_fragment']
    input_quantity = inputs['input_quantity']

    item = items.find { |i| i.sku_fragment == input_fragment }
    raise "recipe contains items not present in order (#{input_fragment})" if item.nil?

    [inputs, item.quantity / input_quantity]
  end

  recipe_input_factors.map { |i,f| f }.min
end
_find_recipes(recipes, items, unpacking: false) click to toggle source
# File lib/seshbot/packing/recipe.rb, line 99
def _find_recipes(recipes, items, unpacking: false)
  recipes = _reverse_recipes(recipes) if unpacking
  recipes.select do |recipe_name, recipe_details|
    recipe_inputs = recipe_details['inputs'].nil? ? [recipe_details] : recipe_details['inputs']

    # recipe is a candidate if all inputs may be satisfied by available items
    recipe_inputs.all? do |inputs|
      items.any? { |i| inputs["input_fragment"] == i.sku_fragment && i.quantity >= inputs["input_quantity"] }
    end
  end
end
_reverse_recipes(recipes) click to toggle source
# File lib/seshbot/packing/recipe.rb, line 111
def _reverse_recipes(recipes)
  # first exclude recipes that are explicitly configured as not reversible
  reversible_recipes = recipes.select do |recipe_name, recipe_details|
    recipe_details['is_reversible'].nil? || recipe_details['is_reversible']
  end

  unique_reversible_output_fragments = reversible_recipes.values.map { |r| r['output_fragment'] }.sort.uniq

  # further refine by excluding recipes that are technically incapable of being reversed
  reversible_recipes = reversible_recipes.select do |recipe_name, recipe_details|
    # currently dont support multipe outputs, so cannot reverse recipes with multiple inputs
    is_composite = !recipe_details['inputs'].nil? && recipe_details['inputs'].length != 1
    # cannot reverse recipes where there are multipe ways of creating the same output
    is_ambiguous = !unique_reversible_output_fragments.include?(recipe_details['output_fragment'])

    !is_composite && !is_ambiguous
  end

  results = reversible_recipes.map do |recipe_name, recipe_details|
    # here we know that if 'inputs' is specified it will have exactly 1 input
    input_fragment = recipe_details["input_fragment"] || recipe_details["inputs"][0]["input_fragment"]
    input_quantity = recipe_details["input_quantity"] || recipe_details["inputs"][0]["input_quantity"]

    new_recipe_details = {
      "input_fragment" => recipe_details["output_fragment"],
      "input_quantity" => recipe_details["output_quantity"],
      "output_fragment" => input_fragment,
      "output_quantity" => input_quantity
    }
    [recipe_name, new_recipe_details]
  end
  results.to_h
end
apply_recipe(recipe, items) click to toggle source
# File lib/seshbot/packing/recipe.rb, line 51
def apply_recipe(recipe, items)
  factor = _calculate_recipe_factor(recipe, items)

  # shortcut - cannot apply recipe, not enough inputs
  return items if factor == 0

  results = items.dup

  # first remove inputs (add negative quantities)
  recipe_inputs = recipe['inputs'].nil? ? [recipe] : recipe['inputs']
  recipe_inputs.each do |inputs|
    results << LineItem.new(inputs['input_fragment'], -1 * inputs['input_quantity'] * factor)
  end

  # now add input
  results << LineItem.new(recipe['output_fragment'], recipe['output_quantity'] * factor)

  # consolidate
  results = LineItem.merge_line_items(results)
end
find_best_recipe(recipes_hash, items, unpacking: false) click to toggle source
# File lib/seshbot/packing/recipe.rb, line 5
def find_best_recipe(recipes_hash, items, unpacking: false)
  recipes = _find_recipes(recipes_hash, items, unpacking: unpacking)

  # we want the recipe that is 'best' (packaging into as few items as possible, or unpacaging into as many as possible)
  best_recipe = nil
  best_recipe_output_quantity = nil
  best_recipe_inputs_quantity = nil
  recipes.each do |recipe_name, recipe|
    recipe_inputs = recipe['inputs'].nil? ? [recipe] : recipe['inputs']

    recipe_factor = _calculate_recipe_factor(recipe, items)
    recipe_output_quantity = recipe_factor * recipe['output_quantity']
    recipe_inputs_quantity = recipe_factor * recipe_inputs.map { |r| r['input_quantity'] }.sum

    # is this recipe the 'best'? (if packaging, best is the one that creates fewest quantity)
    is_best = if best_recipe_output_quantity.nil?
      true
    else
      #
      # the below code uses output as the main feature, but if they are equal uses the number of inputs
      #

      output_equal = recipe_output_quantity == best_recipe_output_quantity

      # when packing, fewer outputs are better
      output_better = unpacking ?
          recipe_output_quantity > best_recipe_output_quantity :
          recipe_output_quantity < best_recipe_output_quantity

      # when packing, more inputs are better
      inputs_better = unpacking ?
          recipe_inputs_quantity < best_recipe_inputs_quantity :
          recipe_inputs_quantity > best_recipe_inputs_quantity

      output_equal ? inputs_better : output_better
    end

    if is_best
      best_recipe_output_quantity = recipe_output_quantity
      best_recipe_inputs_quantity = recipe_inputs_quantity
      best_recipe = recipe
    end
  end
  best_recipe
end
summarise(recipe) click to toggle source
# File lib/seshbot/packing/recipe.rb, line 72
def summarise(recipe)
  recipe_inputs = recipe['inputs'].nil? ? [recipe] : recipe['inputs']
  inputs = recipe_inputs.map { |i| "#{i['input_quantity']}x#{i['input_fragment']}" }.join(',')
  outputs = "#{recipe['output_quantity']}x#{recipe['output_fragment']}"
  "#{inputs} -> #{outputs}"
end