概要
前回 Mineflayer 上で JavaScript の eval を使って動的にコードを動かすことに成功しました
今度はその応用として AI (ollama) にコードを生成してもらい生成したコードをボットに実行してもらうようにしてみました
環境
- macOS 15.6
- Minecraft 1.21.4
- nodejs 22.15.1
- mineflayer 4.31.0
lib/command/commands.js
useAiCodeCmd を追加しています
node-fetch を使って ollama の API をコールします
コールしたあとでレスポンスはそのまま使わずに少し加工しています
サンプルでは gemma3 を使っていますがこのあたりのレスポンスの処理はモデルによって変わってくるので適宜変更してください
const { VM } = require("vm2");
const fetch = require("node-fetch");
const { goals, Movements } = require("mineflayer-pathfinder");
const { GoalBlock } = goals;
const {
buildBaseBlock,
buildFloorFromBase,
buildWallsFromBase,
buildRoofFlatFromBase,
moveBotToCenter,
} = require("./build/house");
const { tryRescue } = require("./dig/low_level_api");
const mcDataLoader = require("minecraft-data");
// 指定の座標に移動するコマンド
function moveCmd(bot, username, args) {
if (args.length < 3) {
bot.chat(`Usage: move <x> <y> <z>`);
return;
}
const [x, y, z] = args.map(Number);
if ([x, y, z].some(isNaN)) {
bot.chat("座標は数値で指定してください");
return;
}
bot.chat(`${username} の指示で (${x}, ${y}, ${z}) に移動します`);
bot.pathfinder.setGoal(new GoalBlock(x, y, z));
}
// プレイヤーの目の前に移動するコマンド(canDig + 自動救出版)
function comeOnCmd(bot, username) {
function getScaffoldBlocks(bot, mcData) {
const usableItemIds = [
mcData.itemsByName.dirt.id,
mcData.itemsByName.cobblestone.id,
mcData.itemsByName.sand.id,
];
return bot.inventory
.items()
.filter((item) => usableItemIds.includes(item.type))
.map((item) => item.type);
}
const player = bot.players[username]?.entity;
if (!player) {
bot.chat(`プレイヤー ${username} が見つかりません`);
return;
}
const mcData = mcDataLoader(bot.version);
const movements = new Movements(bot, mcData);
// 各種設定
movements.allowParkour = true;
movements.allow1by1towers = true;
movements.canDig = true;
movements.scafoldingBlocks = getScaffoldBlocks(bot, mcData);
bot.pathfinder.setMovements(movements);
// プレイヤーの目の前の座標
const pos = player.position.clone();
const dx = Math.round(Math.cos(player.yaw));
const dz = Math.round(Math.sin(player.yaw));
const targetPos = pos.offset(dx, 0, dz);
bot.chat(`${username} の所に行きます!`);
bot.pathfinder.setGoal(new GoalBlock(targetPos.x, targetPos.y, targetPos.z));
// 自動救出ループ
let lastPos = bot.entity.position.clone();
const checkInterval = setInterval(() => {
const dist = bot.entity.position.distanceTo(targetPos);
if (dist <= 1.5) {
bot.chat(`到着しました!(距離 ${dist.toFixed(2)})`);
clearInterval(checkInterval);
return;
}
if (bot.entity.position.distanceTo(lastPos) < 0.01) {
tryRescue(bot, targetPos);
}
lastPos = bot.entity.position.clone();
}, 1500);
}
// ボットのインベントリーの中身を確認するコマンド
function inventoryCmd(bot, username) {
const items = bot.inventory.items();
if (items.length === 0) {
bot.chat(`${username}、インベントリは空です。`);
return;
}
const itemList = items
.map((item) => `${item.count}x${item.name}(${item.type})`)
.join(", ");
bot.chat(`${username} の所持品: ${itemList}`);
}
// 四角形の家を建築するコマンド
async function buildEasyHouseCmd(bot, username) {
const baseBlockPos = await buildBaseBlock(bot, "dirt");
if (baseBlockPos) {
await buildFloorFromBase(bot, baseBlockPos, "dirt");
await moveBotToCenter(bot, baseBlockPos, 5);
bot.chat("床を作りました!");
await buildWallsFromBase(bot, baseBlockPos, "dirt", 5, 3);
bot.chat("壁を作りました");
await buildRoofFlatFromBase(bot, baseBlockPos, "dirt", 5, 3);
bot.chat("屋根を作りました");
}
}
// チャットから指定したボットへの命令を動的に実行する
function evalCmd(bot, username, code) {
// VM2 サンドボックス作成
const vm = new VM({
timeout: 1000, // 無限ループ防止
sandbox: {
bot, // bot API を利用可能にする
Vec3: require("vec3"), // 位置操作用
console: { log: (...args) => bot.chat(args.join(" ")) }, // console.log をチャットに出力
},
});
try {
const result = vm.run(new String(code)); // コード実行
if (result !== undefined) {
bot.chat(`実行結果: ${result}`);
}
} catch (err) {
bot.chat(`エラー: ${err.message}`);
}
}
// AI に問い合わせてコードを生成してもらいそれを eval する
async function useAiCodeCmd(bot, username, prompt) {
bot.chat(`AIにリクエスト中: "${prompt}"`);
try {
// 1. Ollama にリクエスト
const response = await fetch("http://localhost:11434/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "gemma3", // 使うモデル名(Ollamaにあるもの)
prompt: `
あなたはmineflayerのJavaScriptコードを書くアシスタントです。
ユーザーの指示に従い、mineflayer用のJavaScriptコードだけを出力してください。
bot オブジェクトは使えるので bot オブジェクトに対する操作をしてください。
説明文や余計な文章は書かず、コードのみを出力してください。
ユーザーの指示: ${prompt}
`,
stream: false,
}),
});
const data = await response.json();
let aiCode = data.response.trim();
// Markdown のコードブロック記法を削除
aiCode = aiCode
.replace(/```(?:javascript|js)?\n?/gi, "")
.replace(/```/g, "")
.trim();
console.log(aiCode);
bot.chat("AIコードを受信、実行します…");
// 2. サンドボックスで実行
const vm = new VM({
timeout: 2000,
sandbox: {
bot,
Vec3: require("vec3"),
console: { log: (...args) => bot.chat(args.join(" ")) },
},
});
try {
vm.run(aiCode);
bot.chat("AIコードの実行が完了しました");
} catch (err) {
bot.chat(`コード実行エラー: ${err.message}`);
}
} catch (err) {
bot.chat(`AI呼び出しエラー: ${err.message}`);
}
}
module.exports = {
moveCmd,
comeOnCmd,
inventoryCmd,
buildEasyHouseCmd,
evalCmd,
useAiCodeCmd,
};
lib/command/parser.js
isUseAiCodeCommand を追加します
class CommandParser {
constructor(message) {
this.message = message.trim();
this.parts = this.message.split(/\s+/);
}
isMoveCommand() {
return this.parts[0].toLowerCase() === "move";
}
isComeOnCommand() {
return this.parts[0].toLowerCase() === "comeon";
}
isInventoryCommand() {
return this.parts[0].toLowerCase() === "inv";
}
isBuildEasyHouseCommand() {
return this.parts[0].toLowerCase() === "build_easy_house";
}
isEvalCommand() {
return this.parts[0].toLowerCase() === "eval";
}
isUseAiCodeCommand() {
return this.parts[0].toLowerCase() === "use_ai_code";
}
getArgs() {
return this.parts.slice(1);
}
}
module.exports = CommandParser;
index.js
メインの処理を追記します
onChat の分岐処理を追記します
const mineflayer = require("mineflayer");
const { pathfinder, Movements } = require("mineflayer-pathfinder");
const CommandParser = require("./lib/command/parser");
const {
moveCmd,
comeOnCmd,
inventoryCmd,
buildEasyHouseCmd,
evalCmd,
useAiCodeCmd,
} = require("./lib/command/commands");
class MinecraftBot {
constructor(host, port, username) {
this.bot = mineflayer.createBot({
host,
port,
username,
version: false,
});
this.bot.loadPlugin(pathfinder);
this.registerEvents();
}
registerEvents() {
this.bot.once("spawn", () => this.onSpawn());
this.bot.on("chat", (username, message) => this.onChat(username, message));
this.bot.on("kicked", (reason) =>
console.error("キックされました:", reason),
);
this.bot.on("error", (err) => console.error("エラー:", err));
this.bot.on("end", () => console.log("Bot が切断されました"));
}
onSpawn() {
console.log("Bot がログインしました!");
this.bot.chat("こんにちは!Botクラスです!");
const defaultMovements = new Movements(this.bot, this.bot.registry);
this.bot.pathfinder.setMovements(defaultMovements);
}
onChat(username, message) {
if (username === this.bot.username) return;
console.log(`[${username}]: ${message}`);
const parser = new CommandParser(message);
if (parser.isMoveCommand()) {
moveCmd(this.bot, username, parser.getArgs());
} else if (parser.isComeOnCommand()) {
comeOnCmd(this.bot, username);
} else if (parser.isInventoryCommand()) {
inventoryCmd(this.bot, username);
} else if (parser.isBuildEasyHouseCommand()) {
buildEasyHouseCmd(this.bot, username);
} else if (parser.isEvalCommand()) {
evalCmd(this.bot, username, parser.getArgs());
} else if (parser.isUseAiCodeCommand()) {
useAiCodeCmd(this.bot, username, parser.getArgs());
}
}
}
const myBot = new MinecraftBot("localhost", 25565, "bot");
動作確認
- npx node index.js
チャットに「use_ai_code ハローとチャットして」と入力すると少し間を置いてからちゃんとチャットが表示されることが確認できます
最後に
Mineflayer と ollama を連携してボットを AI に動かしてもらうことに挑戦してみました
内部的には JavaScript を eval しているので eval で実行できないコードはそもそも実行できません
これ以上複雑なことをやろうとするならばやはりスクラッチですべて実装するかもしくは minecraft-mcp-server などを使うことになるかなと思います

0 件のコメント:
コメントを投稿