Merge pull request #431 from kolbytn/crafting_plan

Crafting plan
This commit is contained in:
Max Robinson 2025-02-04 16:27:11 -06:00 committed by GitHub
commit 845a9334b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 202 additions and 11 deletions

View file

@ -178,6 +178,42 @@ export const queryList = [
return "Saved place names: " + agent.memory_bank.getKeys(); return "Saved place names: " + agent.memory_bank.getKeys();
} }
}, },
{
name: '!getCraftingPlan',
description: "Provides a comprehensive crafting plan for a specified item. This includes a breakdown of required ingredients, the exact quantities needed, and an analysis of missing ingredients or extra items needed based on the bot's current inventory.",
params: {
targetItem: {
type: 'string',
description: 'The item that we are trying to craft'
},
quantity: {
type: 'int',
description: 'The quantity of the item that we are trying to craft',
optional: true,
domain: [1, Infinity, '[)'], // Quantity must be at least 1,
default: 1
}
},
perform: function (agent, targetItem, quantity = 1) {
let bot = agent.bot;
// Fetch the bot's inventory
const curr_inventory = world.getInventoryCounts(bot);
const target_item = targetItem;
let existingCount = curr_inventory[target_item] || 0;
let prefixMessage = '';
if (existingCount > 0) {
curr_inventory[target_item] -= existingCount;
prefixMessage = `You already have ${existingCount} ${target_item} in your inventory. If you need to craft more,\n`;
}
// Generate crafting plan
let craftingPlan = mc.getDetailedCraftingPlan(target_item, quantity, curr_inventory);
craftingPlan = prefixMessage + craftingPlan;
console.log(craftingPlan);
return pad(craftingPlan);
},
},
{ {
name: '!help', name: '!help',
description: 'Lists all available commands and their descriptions.', description: 'Lists all available commands and their descriptions.',

View file

@ -79,7 +79,7 @@ export async function craftRecipe(bot, itemName, num=1) {
} }
} }
if (!recipes || recipes.length === 0) { if (!recipes || recipes.length === 0) {
log(bot, `You do not have the resources to craft a ${itemName}. It requires: ${Object.entries(mc.getItemCraftingRecipes(itemName)[0]).map(([key, value]) => `${key}: ${value}`).join(', ')}.`); log(bot, `You do not have the resources to craft a ${itemName}. It requires: ${Object.entries(mc.getItemCraftingRecipes(itemName)[0][0]).map(([key, value]) => `${key}: ${value}`).join(', ')}.`);
if (placedTable) { if (placedTable) {
await collectBlock(bot, 'crafting_table', 1); await collectBlock(bot, 'crafting_table', 1);
} }

View file

@ -204,7 +204,7 @@ class ItemWrapper {
} }
createChildren() { createChildren() {
let recipes = mc.getItemCraftingRecipes(this.name); let recipes = mc.getItemCraftingRecipes(this.name).map(([recipe, craftedCount]) => recipe);
if (recipes) { if (recipes) {
for (let recipe of recipes) { for (let recipe of recipes) {
let includes_blacklisted = false; let includes_blacklisted = false;

View file

@ -109,11 +109,11 @@ export class Task {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
if (this.data.agent_count > 1) { if (this.data.agent_count > 1) {
var initial_inventory = this.data.initial_inventory[this.agent.count_id.toString()]; let initial_inventory = this.data.initial_inventory[this.agent.count_id.toString()];
console.log("Initial inventory:", initial_inventory); console.log("Initial inventory:", initial_inventory);
} else if (this.data) { } else if (this.data) {
console.log("Initial inventory:", this.data.initial_inventory); console.log("Initial inventory:", this.data.initial_inventory);
var initial_inventory = this.data.initial_inventory; let initial_inventory = this.data.initial_inventory;
} }
if ("initial_inventory" in this.data) { if ("initial_inventory" in this.data) {

View file

@ -190,7 +190,10 @@ export function getItemCraftingRecipes(itemName) {
recipe[ingredientName] = 0; recipe[ingredientName] = 0;
recipe[ingredientName]++; recipe[ingredientName]++;
} }
recipes.push(recipe); recipes.push([
recipe,
{craftedCount : r.result.count}
]);
} }
return recipes; return recipes;
@ -327,4 +330,156 @@ export function calculateLimitingResource(availableItems, requiredItems, discret
} }
if(discrete) num = Math.floor(num); if(discrete) num = Math.floor(num);
return {num, limitingResource} return {num, limitingResource}
}
let loopingItems = new Set();
export function initializeLoopingItems() {
loopingItems = new Set(['coal',
'wheat',
'diamond',
'emerald',
'raw_iron',
'raw_gold',
'redstone',
'blue_wool',
'packed_mud',
'raw_copper',
'iron_ingot',
'dried_kelp',
'gold_ingot',
'slime_ball',
'black_wool',
'quartz_slab',
'copper_ingot',
'lapis_lazuli',
'honey_bottle',
'rib_armor_trim_smithing_template',
'eye_armor_trim_smithing_template',
'vex_armor_trim_smithing_template',
'dune_armor_trim_smithing_template',
'host_armor_trim_smithing_template',
'tide_armor_trim_smithing_template',
'wild_armor_trim_smithing_template',
'ward_armor_trim_smithing_template',
'coast_armor_trim_smithing_template',
'spire_armor_trim_smithing_template',
'snout_armor_trim_smithing_template',
'shaper_armor_trim_smithing_template',
'netherite_upgrade_smithing_template',
'raiser_armor_trim_smithing_template',
'sentry_armor_trim_smithing_template',
'silence_armor_trim_smithing_template',
'wayfinder_armor_trim_smithing_template']);
}
/**
* Gets a detailed plan for crafting an item considering current inventory
*/
export function getDetailedCraftingPlan(targetItem, count = 1, current_inventory = {}) {
initializeLoopingItems();
if (!targetItem || count <= 0 || !getItemId(targetItem)) {
return "Invalid input. Please provide a valid item name and positive count.";
}
if (isBaseItem(targetItem)) {
const available = current_inventory[targetItem] || 0;
if (available >= count) return "You have all required items already in your inventory!";
return `${targetItem} is a base item, you need to find ${count - available} more in the world`;
}
const inventory = { ...current_inventory };
const leftovers = {};
const plan = craftItem(targetItem, count, inventory, leftovers);
return formatPlan(plan);
}
function isBaseItem(item) {
return loopingItems.has(item) || getItemCraftingRecipes(item) === null;
}
function craftItem(item, count, inventory, leftovers, crafted = { required: {}, steps: [], leftovers: {} }) {
// Check available inventory and leftovers first
const availableInv = inventory[item] || 0;
const availableLeft = leftovers[item] || 0;
const totalAvailable = availableInv + availableLeft;
if (totalAvailable >= count) {
// Use leftovers first, then inventory
const useFromLeft = Math.min(availableLeft, count);
leftovers[item] = availableLeft - useFromLeft;
const remainingNeeded = count - useFromLeft;
if (remainingNeeded > 0) {
inventory[item] = availableInv - remainingNeeded;
}
return crafted;
}
// Use whatever is available
const stillNeeded = count - totalAvailable;
if (availableLeft > 0) leftovers[item] = 0;
if (availableInv > 0) inventory[item] = 0;
if (isBaseItem(item)) {
crafted.required[item] = (crafted.required[item] || 0) + stillNeeded;
return crafted;
}
const recipe = getItemCraftingRecipes(item)?.[0];
if (!recipe) {
crafted.required[item] = stillNeeded;
return crafted;
}
const [ingredients, result] = recipe;
const craftedPerRecipe = result.craftedCount;
const batchCount = Math.ceil(stillNeeded / craftedPerRecipe);
const totalProduced = batchCount * craftedPerRecipe;
// Add excess to leftovers
if (totalProduced > stillNeeded) {
leftovers[item] = (leftovers[item] || 0) + (totalProduced - stillNeeded);
}
// Process each ingredient
for (const [ingredientName, ingredientCount] of Object.entries(ingredients)) {
const totalIngredientNeeded = ingredientCount * batchCount;
craftItem(ingredientName, totalIngredientNeeded, inventory, leftovers, crafted);
}
// Add crafting step
const stepIngredients = Object.entries(ingredients)
.map(([name, amount]) => `${amount * batchCount} ${name}`)
.join(' + ');
crafted.steps.push(`Craft ${stepIngredients} -> ${totalProduced} ${item}`);
return crafted;
}
function formatPlan({ required, steps, leftovers }) {
const lines = [];
if (Object.keys(required).length > 0) {
lines.push('You are missing the following items:');
Object.entries(required).forEach(([item, count]) =>
lines.push(`- ${count} ${item}`));
lines.push('\nOnce you have these items, here\'s your crafting plan:');
} else {
lines.push('You have all items required to craft this item!');
lines.push('Here\'s your crafting plan:');
}
lines.push('');
lines.push(...steps);
if (Object.keys(leftovers).length > 0) {
lines.push('\nYou will have leftover:');
Object.entries(leftovers).forEach(([item, count]) =>
lines.push(`- ${count} ${item}`));
}
return lines.join('\n');
} }

View file

@ -26,9 +26,9 @@
</div> </div>
<script> <script>
function updateLayout() { function updateLayout() {
var width = window.innerWidth; let width = window.innerWidth;
var height = window.innerHeight; let height = window.innerHeight;
var iframes = document.querySelectorAll('.iframe-wrapper'); let iframes = document.querySelectorAll('.iframe-wrapper');
if (width > height) { if (width > height) {
iframes.forEach(function(iframe) { iframes.forEach(function(iframe) {
iframe.style.width = '50%'; iframe.style.width = '50%';
@ -43,10 +43,10 @@
} }
window.addEventListener('resize', updateLayout); window.addEventListener('resize', updateLayout);
window.addEventListener('load', updateLayout); window.addEventListener('load', updateLayout);
var iframes = document.querySelectorAll('.iframe-wrapper'); let iframes = document.querySelectorAll('.iframe-wrapper');
iframes.forEach(function(iframe) { iframes.forEach(function(iframe) {
var port = iframe.getAttribute('data-port'); let port = iframe.getAttribute('data-port');
var loaded = false; let loaded = false;
function checkServer() { function checkServer() {
fetch('http://localhost:' + port, { method: 'HEAD' }) fetch('http://localhost:' + port, { method: 'HEAD' })
.then(function(response) { .then(function(response) {