dougbot/test-craft.js
roberts 195ef2d860 Major overhaul: combat fix, smart brain, Ollama stability, crafting
COMBAT:
- Fixed attack serialization: proper heldItemToNotch() for empty hand
- Attack now looks at target before swinging (server validates aim)
- Player position includes eye height in attack packet
- Click position calculated relative to target entity

INTELLIGENCE (complete rewrite):
- NeedsSystem: Sims-like needs (safety, hunger, social, shelter, boredom)
  that decay over time and drive behavior priorities
- GoalManager: Long-term goals broken into steps (gather_wood,
  explore_area, find_food, check_container, go_home)
- SpatialMemory: Remembers locations of containers, crafting tables,
  interesting blocks, and home position
- DailyRoutine: Morning/day/evening/night phases with trait-influenced
  activity selection
- Brain now WAITS for tasks to complete instead of piling on new ones
- Goal-based decision making replaces random wander

OLLAMA:
- Pre-warm model on deploy (loads into GPU before first chat)
- Keep-alive pings every 2 minutes (prevents model unload)
- Adaptive timeouts: 60s cold, 15s warm, 90s retry
- Auto-retry on timeout failure

CRAFTING:
- Quantity parsing ("craft 5 sticks", "craft a wooden pickaxe")
- Number words supported ("craft three planks")
- Smarter item name extraction with preposition stopping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:07:23 -05:00

275 lines
8.8 KiB
JavaScript

/**
* Test script for DougBot crafting — connects to BDS and tests crafting sticks.
*
* Usage:
* node --experimental-strip-types --disable-warning=ExperimentalWarning test-craft.js
*
* Or with full path:
* /Users/roberts/.local/share/fnm/node-versions/v22.22.2/installation/bin/node \
* --experimental-strip-types --disable-warning=ExperimentalWarning test-craft.js
*/
// --- Patch prismarine-physics for missing bedrock attributes ---
const origPhysicsPath = require.resolve('prismarine-physics');
const origPhysics = require('prismarine-physics');
const { Physics: OrigPhysics, PlayerState } = origPhysics;
function PatchedPhysics(mcData, world) {
if (!mcData.attributesByName || !mcData.attributesByName.movementSpeed) {
const attrs = [
{ name: 'movementSpeed', resource: 'minecraft:movement', min: 0, max: 3.4028235E38, default: 0.1 },
{ name: 'followRange', resource: 'minecraft:follow_range', min: 0, max: 2048, default: 16 },
{ name: 'knockbackResistance', resource: 'minecraft:knockback_resistance', min: 0, max: 1, default: 0 },
{ name: 'attackDamage', resource: 'minecraft:attack_damage', min: 0, max: 3.4028235E38, default: 1 },
{ name: 'armor', resource: 'minecraft:armor', min: 0, max: 30, default: 0 },
{ name: 'armorToughness', resource: 'minecraft:armor_toughness', min: 0, max: 20, default: 0 },
{ name: 'attackSpeed', resource: 'minecraft:attack_speed', min: 0, max: 1024, default: 4 },
{ name: 'luck', resource: 'minecraft:luck', min: -1024, max: 1024, default: 0 },
{ name: 'maxHealth', resource: 'minecraft:health', min: 0, max: 1024, default: 20 },
];
mcData.attributesArray = attrs;
mcData.attributes = {};
mcData.attributesByName = {};
for (const attr of attrs) {
mcData.attributes[attr.resource] = attr;
mcData.attributesByName[attr.name] = attr;
}
}
return OrigPhysics(mcData, world);
}
require.cache[origPhysicsPath] = {
id: origPhysicsPath,
exports: { Physics: PatchedPhysics, PlayerState },
loaded: true,
};
// --- Imports ---
const path = require('path');
const mineflayer = require(path.join(__dirname, 'bridge', 'lib', 'mineflayer'));
const { pathfinder: pathfinderPlugin, Movements, goals } = require('mineflayer-pathfinder');
const { GoalNear } = goals;
// --- Config ---
const HOST = process.env.BDS_HOST || '192.168.1.90';
const PORT = parseInt(process.env.BDS_PORT || '19140');
const USERNAME = process.env.BDS_USER || 'CraftTest';
function log(msg) {
console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`);
}
log(`Connecting to ${HOST}:${PORT} as ${USERNAME}...`);
const bot = mineflayer.createBot({
host: HOST,
port: PORT,
username: USERNAME,
version: 'bedrock_1.21.130',
bedrockProtocolVersion: '26.10',
auth: 'offline',
offline: true,
raknetBackend: 'jsp-raknet',
});
bot.loadPlugin(pathfinderPlugin);
let testPhase = 'waiting';
bot.once('spawn', async () => {
log('Spawned! Setting up pathfinder...');
const movements = new Movements(bot);
movements.canDig = false;
bot.pathfinder.setMovements(movements);
const pos = bot.entity.position;
log(`Position: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`);
// Wait a moment for recipes to load
await sleep(3000);
// --- Run test sequence ---
try {
await testResolveNames();
await testInventoryCheck();
await testCraftSticks();
log('=== ALL TESTS COMPLETE ===');
} catch (e) {
log(`TEST FAILED: ${e.message}`);
console.error(e);
}
// Disconnect after tests
await sleep(2000);
log('Disconnecting...');
bot.quit();
setTimeout(() => process.exit(0), 1000);
});
bot.on('error', (err) => {
log(`ERROR: ${err.message}`);
});
bot.on('kicked', (reason) => {
log(`KICKED: ${reason}`);
process.exit(1);
});
// --- Test: Name Resolution ---
async function testResolveNames() {
log('--- TEST: Item Name Resolution ---');
const mcData = require('minecraft-data')(bot.version);
const testCases = [
['sticks', 'stick'],
['stick', 'stick'],
['planks', 'planks'],
['oak_planks', 'oak_planks'],
['wooden_pickaxe', 'wooden_pickaxe'],
['wood_pickaxe', 'wooden_pickaxe'],
['wooden pickaxe', 'wooden_pickaxe'],
['crafting_table', 'crafting_table'],
['workbench', 'crafting_table'],
['torches', 'torch'],
['iron', 'iron_ingot'],
['cobble', 'cobblestone'],
['stone_sword', 'stone_sword'],
['diamond_pickaxe', 'diamond_pickaxe'],
];
// Inline version of resolveItemName for testing
const ITEM_ALIASES = {
'sticks': 'stick', 'planks': 'planks', 'torches': 'torch', 'logs': 'oak_log',
'wood': 'oak_log', 'wood_planks': 'planks', 'plank': 'planks',
'log': 'oak_log', 'cobble': 'cobblestone',
'wood_pickaxe': 'wooden_pickaxe', 'wood_sword': 'wooden_sword',
'wood_axe': 'wooden_axe', 'wood_shovel': 'wooden_shovel', 'wood_hoe': 'wooden_hoe',
'workbench': 'crafting_table', 'bench': 'crafting_table',
'iron': 'iron_ingot', 'gold': 'gold_ingot',
};
function resolveItemName(rawName) {
let name = rawName.toLowerCase().trim().replace(/\s+/g, '_');
if (ITEM_ALIASES[name]) name = ITEM_ALIASES[name];
let item = mcData.itemsByName[name];
if (item) return item.name;
if (name.endsWith('s') && !name.endsWith('ss')) {
item = mcData.itemsByName[name.slice(0, -1)];
if (item) return item.name;
}
if (name.startsWith('wooden_')) {
item = mcData.itemsByName[name.replace('wooden_', 'wood_')];
if (item) return item.name;
}
if (name.startsWith('wood_')) {
item = mcData.itemsByName[name.replace('wood_', 'wooden_')];
if (item) return item.name;
}
for (const [n] of Object.entries(mcData.itemsByName)) {
if (n === name || n.includes(name) || name.includes(n)) return n;
}
return null;
}
let passed = 0;
for (const [input, expected] of testCases) {
const result = resolveItemName(input);
const ok = result === expected;
log(` ${ok ? 'PASS' : 'FAIL'}: "${input}" → "${result}" (expected "${expected}")`);
if (ok) passed++;
}
log(` Results: ${passed}/${testCases.length} passed`);
}
// --- Test: Inventory Check ---
async function testInventoryCheck() {
log('--- TEST: Inventory Contents ---');
const items = bot.inventory.items();
if (items.length === 0) {
log(' Inventory is EMPTY. Give CraftTest some planks to test crafting.');
log(' Use: /give CraftTest oak_planks 4');
} else {
for (const item of items) {
log(` ${item.count}x ${item.name} (id=${item.type}, slot=${item.slot})`);
}
}
}
// --- Test: Craft Sticks ---
async function testCraftSticks() {
log('--- TEST: Craft Sticks ---');
const mcData = require('minecraft-data')(bot.version);
// Check if we have planks
const planks = bot.inventory.items().find(i => {
const n = i.name.replace('minecraft:', '');
return n === 'planks' || n.endsWith('_planks');
});
if (!planks) {
log(' SKIP: No planks in inventory. Give CraftTest planks first.');
log(' Hint: /give CraftTest oak_planks 4');
return;
}
log(` Have ${planks.count}x ${planks.name}`);
// Look up stick item
const stickItem = mcData.itemsByName['stick'];
if (!stickItem) {
log(' FAIL: "stick" not found in minecraft-data!');
return;
}
log(` stick item id = ${stickItem.id}`);
// Check recipes
const recipesNoTable = bot.recipesFor(stickItem.id, null, null, undefined);
log(` Recipes without table: ${recipesNoTable.length}`);
// Find crafting table nearby
const craftingTable = bot.findBlock({
matching: (block) => {
const n = block.name.replace('minecraft:', '');
return n === 'crafting_table' || n === 'workbench';
},
maxDistance: 32,
});
if (craftingTable) {
log(` Found crafting table at ${craftingTable.position}`);
const recipesWithTable = bot.recipesFor(stickItem.id, null, null, craftingTable);
log(` Recipes with table: ${recipesWithTable.length}`);
} else {
log(' No crafting table found nearby');
}
// Try to craft (sticks are 2x2, should work without a table)
const recipes = recipesNoTable.length > 0 ? recipesNoTable :
(craftingTable ? bot.recipesFor(stickItem.id, null, null, craftingTable) : []);
if (recipes.length === 0) {
log(' FAIL: No recipes available for sticks');
return;
}
log(` Using recipe: ${JSON.stringify(recipes[0], null, 2).slice(0, 200)}`);
try {
const table = recipesNoTable.length > 0 ? undefined : craftingTable;
await bot.craft(recipes[0], 1, table);
log(' SUCCESS: Crafted sticks!');
// Verify sticks in inventory
const sticks = bot.inventory.items().find(i => i.name.replace('minecraft:', '') === 'stick');
if (sticks) {
log(` Verified: ${sticks.count}x stick in inventory`);
}
} catch (e) {
log(` FAIL: Craft error: ${e.message}`);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}