Initial project scaffold: dataset schema, 31 seed training examples, Mineflayer bot framework, and 7-phase roadmap
- IDEA.md: project scope (Minecraft ops AI assistant via qwen3-coder LoRA/SFT) - PLAN.md: complete roadmap with prior art analysis, architecture, phased plan, dev server docs - data/schema.json: training example JSON Schema with negative_output support - data/processed/seed_dataset.jsonl: 31 validated examples from repair code, prayer logs, session history - data/validate_dataset.py: schema validator with summary statistics - ingame/: Mineflayer bot framework (test_connect, spawn_bots, aware_bots with full event logging) - Directory structure for knowledge/, eval/, training/, agent/ (Phase 1.3+ work)
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* aware_bots.js -- Mineflayer bots that log everything they experience.
|
||||
*
|
||||
* Each bot tracks and logs:
|
||||
* - Position changes (teleports, movement)
|
||||
* - Health changes (damage, healing)
|
||||
* - Chat messages
|
||||
* - Effects applied/removed
|
||||
* - Block changes in their vicinity
|
||||
* - Items received
|
||||
* - Environmental state (underwater, on fire, in air)
|
||||
* - Deaths
|
||||
*
|
||||
* Usage: node aware_bots.js [count] [host] [port]
|
||||
* Defaults: 3 bots, 192.168.0.244:25568
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
|
||||
const count = parseInt(process.argv[2] || '3', 10);
|
||||
const host = process.argv[3] || '192.168.0.244';
|
||||
const port = parseInt(process.argv[4] || '25568', 10);
|
||||
|
||||
const bots = [];
|
||||
let connected = 0;
|
||||
|
||||
function ts() {
|
||||
return new Date().toISOString().slice(11, 19);
|
||||
}
|
||||
|
||||
function fmtPos(pos) {
|
||||
if (!pos) return '?,?,?';
|
||||
return `${pos.x.toFixed(1)},${pos.y.toFixed(1)},${pos.z.toFixed(1)}`;
|
||||
}
|
||||
|
||||
function spawnBot(index) {
|
||||
const name = `TrainBot_${index}`;
|
||||
console.log(`[${ts()}] [${name}] Connecting to ${host}:${port}...`);
|
||||
|
||||
const bot = mineflayer.createBot({
|
||||
host,
|
||||
port,
|
||||
username: name,
|
||||
auth: 'offline',
|
||||
version: '1.21.11',
|
||||
viewDistance: 'tiny',
|
||||
});
|
||||
|
||||
bot._name = name;
|
||||
bots.push(bot);
|
||||
|
||||
// --- State tracking ---
|
||||
let lastPos = null;
|
||||
let lastHealth = null;
|
||||
let lastFood = null;
|
||||
let lastOxygen = null;
|
||||
let isUnderwater = false;
|
||||
let isOnFire = false;
|
||||
|
||||
bot.on('login', () => {
|
||||
console.log(`[${ts()}] [${name}] Logged in`);
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
connected++;
|
||||
const pos = bot.entity.position;
|
||||
lastPos = { x: pos.x, y: pos.y, z: pos.z };
|
||||
lastHealth = bot.health;
|
||||
lastFood = bot.food;
|
||||
lastOxygen = bot.oxygenLevel;
|
||||
console.log(`[${ts()}] [${name}] SPAWN at (${fmtPos(pos)}) gamemode=${bot.game.gameMode} -- ${connected}/${count}`);
|
||||
|
||||
if (connected === count) {
|
||||
console.log(`[${ts()}] === All ${count} bots connected and aware ===`);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Position tracking (teleport detection) ---
|
||||
bot.on('forcedMove', () => {
|
||||
const pos = bot.entity.position;
|
||||
const oldStr = lastPos ? fmtPos(lastPos) : '?';
|
||||
console.log(`[${ts()}] [${name}] TELEPORTED from (${oldStr}) to (${fmtPos(pos)})`);
|
||||
lastPos = { x: pos.x, y: pos.y, z: pos.z };
|
||||
});
|
||||
|
||||
// --- Health tracking ---
|
||||
bot.on('health', () => {
|
||||
const hp = bot.health;
|
||||
const food = bot.food;
|
||||
const oxy = bot.oxygenLevel;
|
||||
|
||||
if (lastHealth !== null && hp !== lastHealth) {
|
||||
const delta = hp - lastHealth;
|
||||
const verb = delta < 0 ? 'DAMAGED' : 'HEALED';
|
||||
console.log(`[${ts()}] [${name}] ${verb} ${delta.toFixed(1)} (health: ${hp.toFixed(1)}/20)`);
|
||||
}
|
||||
|
||||
if (lastFood !== null && food !== lastFood) {
|
||||
console.log(`[${ts()}] [${name}] FOOD changed ${lastFood} -> ${food}`);
|
||||
}
|
||||
|
||||
if (lastOxygen !== null && oxy !== lastOxygen) {
|
||||
if (oxy < lastOxygen && !isUnderwater) {
|
||||
isUnderwater = true;
|
||||
console.log(`[${ts()}] [${name}] UNDERWATER (air: ${oxy})`);
|
||||
} else if (oxy > lastOxygen && isUnderwater) {
|
||||
isUnderwater = false;
|
||||
console.log(`[${ts()}] [${name}] SURFACED (air: ${oxy})`);
|
||||
}
|
||||
}
|
||||
|
||||
lastHealth = hp;
|
||||
lastFood = food;
|
||||
lastOxygen = oxy;
|
||||
});
|
||||
|
||||
// --- Chat ---
|
||||
bot.on('chat', (username, message) => {
|
||||
if (username === name) return;
|
||||
console.log(`[${ts()}] [${name}] CHAT <${username}> ${message}`);
|
||||
});
|
||||
|
||||
// --- Whisper ---
|
||||
bot.on('whisper', (username, message) => {
|
||||
console.log(`[${ts()}] [${name}] WHISPER from ${username}: ${message}`);
|
||||
});
|
||||
|
||||
// --- Death ---
|
||||
bot.on('death', () => {
|
||||
console.log(`[${ts()}] [${name}] DIED at (${fmtPos(lastPos)})`);
|
||||
});
|
||||
|
||||
bot.on('respawn', () => {
|
||||
const pos = bot.entity.position;
|
||||
console.log(`[${ts()}] [${name}] RESPAWNED at (${fmtPos(pos)})`);
|
||||
lastPos = { x: pos.x, y: pos.y, z: pos.z };
|
||||
});
|
||||
|
||||
// --- Effects ---
|
||||
bot.on('entityEffect', (entity, effect) => {
|
||||
if (entity === bot.entity) {
|
||||
console.log(`[${ts()}] [${name}] EFFECT APPLIED id=${effect.id} amplifier=${effect.amplifier} duration=${effect.duration}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('entityEffectEnd', (entity, effect) => {
|
||||
if (entity === bot.entity) {
|
||||
console.log(`[${ts()}] [${name}] EFFECT ENDED id=${effect.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Item collection ---
|
||||
bot.on('playerCollect', (collector, collected) => {
|
||||
if (collector === bot.entity) {
|
||||
console.log(`[${ts()}] [${name}] COLLECTED item entity ${collected.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// --- Fire detection ---
|
||||
bot.on('entityUpdate', (entity) => {
|
||||
if (entity === bot.entity) {
|
||||
const onFire = (entity.metadata && entity.metadata[0] && (entity.metadata[0] & 0x01)) ? true : false;
|
||||
if (onFire && !isOnFire) {
|
||||
isOnFire = true;
|
||||
console.log(`[${ts()}] [${name}] ON FIRE`);
|
||||
} else if (!onFire && isOnFire) {
|
||||
isOnFire = false;
|
||||
console.log(`[${ts()}] [${name}] FIRE OUT`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Periodic environment report ---
|
||||
let envInterval = setInterval(() => {
|
||||
if (!bot.entity) return;
|
||||
const pos = bot.entity.position;
|
||||
const blockBelow = bot.blockAt(pos.offset(0, -1, 0));
|
||||
const blockAt = bot.blockAt(pos);
|
||||
const blockAbove = bot.blockAt(pos.offset(0, 1, 0));
|
||||
|
||||
const env = [];
|
||||
if (blockAt && blockAt.name === 'water') env.push('in_water');
|
||||
if (blockAt && blockAt.name === 'lava') env.push('in_lava');
|
||||
if (blockBelow) env.push(`standing_on:${blockBelow.name}`);
|
||||
if (blockAbove && blockAbove.name === 'water') env.push('water_above');
|
||||
|
||||
// Only log if something interesting
|
||||
if (env.length > 0 && (env.includes('in_water') || env.includes('in_lava'))) {
|
||||
console.log(`[${ts()}] [${name}] ENV pos=(${fmtPos(pos)}) ${env.join(', ')} hp=${bot.health} air=${bot.oxygenLevel}`);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// --- Error/disconnect handling ---
|
||||
bot.on('error', (err) => {
|
||||
console.error(`[${ts()}] [${name}] ERROR: ${err.message}`);
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
let msg = reason;
|
||||
try { msg = JSON.stringify(reason); } catch (_) {}
|
||||
console.error(`[${ts()}] [${name}] KICKED: ${msg}`);
|
||||
connected = Math.max(0, connected - 1);
|
||||
clearInterval(envInterval);
|
||||
});
|
||||
|
||||
bot.on('end', (reason) => {
|
||||
console.log(`[${ts()}] [${name}] DISCONNECTED: ${reason}`);
|
||||
connected = Math.max(0, connected - 1);
|
||||
clearInterval(envInterval);
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
// Stagger connections
|
||||
for (let i = 0; i < count; i++) {
|
||||
setTimeout(() => spawnBot(i), i * 1500);
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log(`\n[${ts()}] Disconnecting all bots...`);
|
||||
for (const bot of bots) {
|
||||
try { bot.quit('Shutdown'); } catch (_) {}
|
||||
}
|
||||
setTimeout(() => process.exit(0), 2000);
|
||||
});
|
||||
|
||||
setInterval(() => {}, 60000);
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mc-ai-training-bots",
|
||||
"version": "0.1.0",
|
||||
"description": "Mineflayer bots for Minecraft AI model training and evaluation",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test-connect": "node test_connect.js",
|
||||
"spawn-bots": "node spawn_bots.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"mineflayer": "^4.23.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* spawn_bots.js -- Spawn multiple Mineflayer bots on the dev server.
|
||||
*
|
||||
* Each bot connects, reports its state, and stays connected for
|
||||
* external control (RCON commands, chat interaction, etc.).
|
||||
*
|
||||
* Usage: node spawn_bots.js [count] [host] [port]
|
||||
* Defaults: 3 bots, 192.168.0.244:25568
|
||||
*
|
||||
* Bots are named: TrainBot_0, TrainBot_1, TrainBot_2, ...
|
||||
* They join staggered (1s apart) to avoid connection floods.
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
|
||||
const count = parseInt(process.argv[2] || '3', 10);
|
||||
const host = process.argv[3] || '192.168.0.244';
|
||||
const port = parseInt(process.argv[4] || '25568', 10);
|
||||
|
||||
const bots = [];
|
||||
let connected = 0;
|
||||
|
||||
function spawnBot(index) {
|
||||
const name = `TrainBot_${index}`;
|
||||
console.log(`[${name}] Connecting to ${host}:${port}...`);
|
||||
|
||||
const bot = mineflayer.createBot({
|
||||
host,
|
||||
port,
|
||||
username: name,
|
||||
auth: 'offline',
|
||||
version: '1.21.11',
|
||||
});
|
||||
|
||||
bot._botIndex = index;
|
||||
bot._botName = name;
|
||||
bots.push(bot);
|
||||
|
||||
bot.on('login', () => {
|
||||
console.log(`[${name}] Logged in`);
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
connected++;
|
||||
const pos = bot.entity.position;
|
||||
console.log(`[${name}] Spawned at (${pos.x.toFixed(0)}, ${pos.y.toFixed(0)}, ${pos.z.toFixed(0)}) -- ${connected}/${count} connected`);
|
||||
|
||||
if (connected === count) {
|
||||
console.log(`\n=== All ${count} bots connected ===`);
|
||||
console.log('Bots are idle and ready for training commands.');
|
||||
console.log('Press Ctrl+C to disconnect all bots.\n');
|
||||
}
|
||||
});
|
||||
|
||||
bot.on('chat', (username, message) => {
|
||||
// Ignore own messages
|
||||
if (username === name) return;
|
||||
console.log(`[${name}] CHAT <${username}> ${message}`);
|
||||
});
|
||||
|
||||
bot.on('error', (err) => {
|
||||
console.error(`[${name}] ERROR: ${err.message}`);
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
console.error(`[${name}] KICKED: ${reason}`);
|
||||
connected = Math.max(0, connected - 1);
|
||||
});
|
||||
|
||||
bot.on('end', (reason) => {
|
||||
console.log(`[${name}] Disconnected: ${reason}`);
|
||||
connected = Math.max(0, connected - 1);
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
// Stagger connections
|
||||
for (let i = 0; i < count; i++) {
|
||||
setTimeout(() => spawnBot(i), i * 1500);
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nDisconnecting all bots...');
|
||||
for (const bot of bots) {
|
||||
try { bot.quit('Shutdown'); } catch (_) {}
|
||||
}
|
||||
setTimeout(() => process.exit(0), 2000);
|
||||
});
|
||||
|
||||
// Keep alive
|
||||
setInterval(() => {}, 60000);
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* test_connect.js -- Verify Mineflayer can connect to the dev Paper server.
|
||||
*
|
||||
* Usage: node test_connect.js [host] [port]
|
||||
* Defaults: 192.168.0.244 25568
|
||||
*/
|
||||
|
||||
const mineflayer = require('mineflayer');
|
||||
|
||||
const host = process.argv[2] || '192.168.0.244';
|
||||
const port = parseInt(process.argv[3] || '25568', 10);
|
||||
|
||||
console.log(`Connecting to ${host}:${port} as TestBot...`);
|
||||
|
||||
const bot = mineflayer.createBot({
|
||||
host,
|
||||
port,
|
||||
username: 'TestBot',
|
||||
auth: 'offline',
|
||||
version: '1.21.11',
|
||||
});
|
||||
|
||||
bot.on('login', () => {
|
||||
console.log(`[OK] Logged in as ${bot.username}`);
|
||||
});
|
||||
|
||||
bot.on('spawn', () => {
|
||||
const pos = bot.entity.position;
|
||||
console.log(`[OK] Spawned at x=${pos.x.toFixed(1)} y=${pos.y.toFixed(1)} z=${pos.z.toFixed(1)}`);
|
||||
console.log(`[OK] Gamemode: ${bot.game.gameMode}`);
|
||||
console.log(`[OK] Health: ${bot.health}, Food: ${bot.food}`);
|
||||
|
||||
// Look around and report nearby blocks
|
||||
const block = bot.blockAt(bot.entity.position.offset(0, -1, 0));
|
||||
console.log(`[OK] Block below: ${block ? block.name : 'unknown'}`);
|
||||
|
||||
// List nearby entities
|
||||
const entities = Object.values(bot.entities).filter(e => e !== bot.entity);
|
||||
console.log(`[OK] Nearby entities: ${entities.length}`);
|
||||
|
||||
console.log('\n=== Connection test passed ===');
|
||||
console.log('Disconnecting in 3s...');
|
||||
setTimeout(() => {
|
||||
bot.quit('Test complete');
|
||||
process.exit(0);
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
bot.on('chat', (username, message) => {
|
||||
console.log(`[CHAT] <${username}> ${message}`);
|
||||
});
|
||||
|
||||
bot.on('error', (err) => {
|
||||
console.error(`[ERROR] ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
bot.on('kicked', (reason) => {
|
||||
console.error(`[KICKED] ${reason}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
bot.on('end', (reason) => {
|
||||
console.log(`[END] ${reason}`);
|
||||
});
|
||||
|
||||
// Timeout if connection takes too long
|
||||
setTimeout(() => {
|
||||
console.error('[TIMEOUT] Connection took longer than 30s');
|
||||
process.exit(1);
|
||||
}, 30000);
|
||||
Reference in New Issue
Block a user