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>
275 lines
8.8 KiB
JavaScript
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));
|
|
}
|