2025年8月14日木曜日

mineflayer でボットに簡単な建築をさせてみた

mineflayer でボットに簡単な建築をさせてみた

概要

前回までにボットを移動させることに成功しました
今回は簡単な建築に挑戦します
建築となると途端にコードが複雑になる気がします

環境

  • macOS 15.6
  • Minecraft 1.21.4
  • nodejs 22.15.1
  • mineflayer 4.31.0

建築の方針

建築するために必要な処理を管理します
今回は簡単な四角形の家を建築するだけです
がそれでもボットにまかせるとなると結構たいへんです
地面が平らでないと建築できないなどの条件があると大変なので今回はボットが立てれば建築可能を条件にします
つまり空中であろうとブロックが上にボットが立っていればそこから建築できるような仕組みにします

建築の流れは以下のとおりです

  1. ボットの足元に建築のベースとなるブロックを1つ設置する (垂直ジャンプ設置)
  2. ベースブロックを元に床を建設
  3. 床を設置したら床の中央に移動
  4. ベースブロックを元に壁を建設
  5. ベースブロックを元に屋根を建設

という流れにします
こうすることで例え平らな平地で無かろうと建築させることができます

lib/command/build/house.js

まずは建築に必要な各種処理を管理するメソッドを定義します
基本的には先程紹介した建築方針の流れを踏襲するメソッドを定義していきます

placeBlockFromBase がブロック1つ置くだけのメソッドなのですが次に接地するブロックの考慮などを入れているのでかなりのコード量になっています

buildBaseBlock もジャンプをし足元にブロックを1つ置くだけの処理なのですがジャンプ時に最高到達点にいないとブロックがおけないのでそれを考慮した作りになっているためかなりのコード量になっています

あとは床、壁、屋根を作成するメソッドをそれぞれ作成しています

普通人間がマイクラ内でブロックを設置する場合には必ず接地面が見えていないと設置できないのですがボットで建築する場合にそれを考慮するとかなり大変な処理が必要になるのでとりあえず今回は接地面が見えなくても設置できるようにしています

const { goals } = require("mineflayer-pathfinder");
const { GoalBlock, GoalNear } = goals;
const Vec3 = require("vec3");

// 建築用のベースブロックを配置する
async function buildBaseBlock(bot, blockName) {
  const basePos = bot.entity.position.floored(); // Vec3

  // インベントリからブロックを持つ
  const item = bot.inventory.items().find((i) => i.name.includes(blockName));
  if (!item) {
    bot.chat(`ブロック ${blockName} がありません`);
    return null;
  }
  await bot.equip(item, "hand");

  // 1マス下のブロックの情報を取得
  const belowPos = basePos.offset(0, -1, 0);
  const belowBlock = bot.blockAt(belowPos);
  // ジャンプ開始
  bot.setControlState("jump", true);

  // ジャンプの最高点到達まで待つ(垂直速度が0以下になるまで)
  await new Promise((resolve) => {
    function checkVelocity() {
      if (bot.entity.velocity.y <= 0) {
        bot.removeListener("move", checkVelocity);
        resolve();
      }
    }
    bot.on("move", checkVelocity);
    // 念のため5秒タイムアウト
    setTimeout(() => {
      bot.removeListener("move", checkVelocity);
      resolve();
    }, 5000);
  });

  try {
    // 最高点で設置
    await bot.placeBlock(belowBlock, { x: 0, y: 1, z: 0 });
    bot.chat("ジャンプしてベースブロックを積みました");
  } catch (err) {
    console.log(`ジャンプ設置失敗: ${err.message}`);
    bot.setControlState("jump", false);
    return null;
  }

  // ジャンプ終了
  bot.setControlState("jump", false);

  return Vec3(basePos.x, basePos.y, basePos.z);
}

// ベースブロックを元にブロックを設置します
async function placeBlockFromBase(bot, basePos, targetPos, blockName) {
  // ブロックを持つ
  const item = bot.inventory.items().find((i) => i.name.includes(blockName));
  if (!item) {
    bot.chat(`ブロック ${blockName} がありません`);
    return;
  }
  await bot.equip(item, "hand");

  // 置く場所の座標
  const targetBlock = bot.blockAt(targetPos);
  if (targetBlock && targetBlock.name !== "air") {
    // すでにブロックがある場合はスキップ
    return;
  }

  // 周囲の6方向を調べて参照ブロックを探す
  const directions = [
    { x: 1, y: 0, z: 0 },
    { x: -1, y: 0, z: 0 },
    { x: 0, y: 1, z: 0 },
    { x: 0, y: -1, z: 0 },
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: -1 },
  ];

  let refBlock = null;
  let faceVec = null;

  for (const dir of directions) {
    const neighborPos = targetPos.offset(dir.x, dir.y, dir.z);
    const block = bot.blockAt(neighborPos);
    if (block && block.name !== "air") {
      refBlock = block;
      // faceVec は参照ブロックから見た targetPos の方向
      faceVec = { x: -dir.x, y: -dir.y, z: -dir.z };
      break;
    }
  }

  // 周囲に参照ブロックがない → ベースブロックを利用
  if (!refBlock) {
    refBlock = bot.blockAt(basePos);
    if (!refBlock) {
      bot.chat("参照できるブロックがありません");
      return;
    }
    const dx = targetPos.x - basePos.x;
    const dy = targetPos.y - basePos.y;
    const dz = targetPos.z - basePos.z;

    // ベースブロックから見た方向に変換
    faceVec = { x: Math.sign(dx), y: Math.sign(dy), z: Math.sign(dz) };
  }

  try {
    await bot.placeBlock(refBlock, faceVec);
  } catch (err) {
    console.log(`ブロック設置失敗: ${err.message}`);
  }
}

// ベースブロックを基準に床を作成します
async function buildFloorFromBase(bot, basePos, blockName) {
  const size = 5;
  for (let dx = 0; dx < size; dx++) {
    for (let dz = 0; dz < size; dz++) {
      const target = basePos.offset(dx, 0, dz);
      await placeBlockFromBase(bot, basePos, target, blockName);
    }
  }
}

// ベースブロックを元に壁を作成します
async function buildWallsFromBase(
  bot,
  basePos,
  blockName,
  floorSize = 5,
  wallHeight = 3,
) {
  for (let y = 1; y <= wallHeight; y++) {
    // 壁の周囲のループ
    for (let i = 0; i < floorSize; i++) {
      // 4辺それぞれに壁を置く

      // x = 0 の面(南側)
      await placeBlockFromBase(
        bot,
        basePos,
        basePos.offset(0, y, i),
        blockName,
      );
      // x = floorSize-1 の面(北側)
      await placeBlockFromBase(
        bot,
        basePos,
        basePos.offset(floorSize - 1, y, i),
        blockName,
      );
      // z = 0 の面(西側)
      await placeBlockFromBase(
        bot,
        basePos,
        basePos.offset(i, y, 0),
        blockName,
      );
      // z = floorSize-1 の面(東側)
      await placeBlockFromBase(
        bot,
        basePos,
        basePos.offset(i, y, floorSize - 1),
        blockName,
      );
    }
  }
}

// ベースブロックを基準に屋根を作成します
async function buildRoofFlatFromBase(
  bot,
  basePos,
  blockName,
  floorSize = 5,
  wallHeight = 3,
) {
  if (typeof basePos.offset !== "function") {
    basePos = Vec3(basePos.x, basePos.y, basePos.z);
  }

  const roofY = basePos.y + wallHeight + 1;

  for (let dx = 0; dx < floorSize; dx++) {
    for (let dz = 0; dz < floorSize; dz++) {
      const roofPos = Vec3(basePos.x + dx, roofY, basePos.z + dz); // 絶対座標を明示
      await placeBlockFromBase(bot, basePos, roofPos, blockName);
      await bot.waitForTicks(2);
    }
  }
}

// 床の中央に移動します
async function moveBotToCenter(bot, basePos, floorSize) {
  const centerX = basePos.x + Math.floor(floorSize / 2);
  const centerZ = basePos.z + Math.floor(floorSize / 2);
  const centerY = basePos.y + 1; // 床の高さ

  try {
    await bot.pathfinder.goto(new GoalBlock(centerX, centerY, centerZ));
    bot.chat("部屋の中央に移動しました");
  } catch (err) {
    bot.chat("中央への移動に失敗しました: " + err.message);
  }
}

module.exports = {
  buildBaseBlock,
  placeBlockFromBase,
  buildFloorFromBase,
  buildWallsFromBase,
  buildRoofFlatFromBase,
  moveBotToCenter,
};

lib/command/commands.js

build_easy_house を実行するためのコマンドを追加します

const { goals, Movements } = require("mineflayer-pathfinder");
const { GoalBlock } = goals;
const {
  buildBaseBlock,
  buildFloorFromBase,
  buildWallsFromBase,
  buildRoofFlatFromBase,
  moveBotToCenter,
} = require("./build/house");

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));
}

// プレイヤーの目の前に移動するコマンド
function comeOnCmd(bot, username) {
  // 手持ちの足場ブロックを取得するヘルパーメソッド
  function getScaffoldBlocks(bot, mcData) {
    const usableItemIds = [
      mcData.itemsByName.dirt.id,
      mcData.itemsByName.cobblestone.id,
      mcData.itemsByName.sand.id,
    ];
    // console.log("usableItemIds:", usableItemIds);

    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));
}

// ボットのインベントリーの中身を確認するコマンド
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("屋根を作りました");
  }
}

module.exports = { moveCmd, comeOnCmd, inventoryCmd, buildEasyHouseCmd };

lib/command/parser.js

チャットに入力された build_easy_house をパースして判断できるメソッドを追加します

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";
  }

  getArgs() {
    return this.parts.slice(1);
  }
}

module.exports = CommandParser;

index.js

メイン部分に追記します

const mineflayer = require("mineflayer");
const { pathfinder, Movements } = require("mineflayer-pathfinder");
const CommandParser = require("./lib/command/parser");
const {
  moveCmd,
  comeOnCmd,
  inventoryCmd,
  buildEasyHouseCmd,
} = 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);
    }
  }

  sendChat(text) {
    this.bot.chat(text);
  }
}

const myBot = new MinecraftBot("localhost", 25565, "bot");

動作確認

  • npx node index.js

でボットを起動します
適当に場所にボットを移動したらチャット欄に build_easy_house を入力しましょう
以下のように建築してくれれば OK です

最後に

mineflayer でボットに簡単な建築をさせてみました
これまで移動やインベントリ処理を実装しましたが建築となるとかなりハードルがあがる印象です

実際に何度か試したのですがたまに以下の状況になります

  • ベースブロックを垂直ジャンプでおけない
  • 障害物がないのに壁、天井がおけない

基本的にはリトライするかまた別の位置に移動なおしてから再実行になります
建築が途中で止まったらそこから再開できるレジューム機能などがあると便利かなと思います

0 件のコメント:

コメントを投稿