Download gemini_bot.js from Erinaldordso/Minecraft-Friends-AI-ZeroGPU: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/resolve/main/gemini_bot.js
- Command line
-
hf download hf://spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/gemini_bot.js
-
curl -L -o gemini_bot.js https://huggingface.co/spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/resolve/main/gemini_bot.js
12.2 kB
| ; | |
| // A second, independent Mineflayer player. Gemini CLI proposes only one | |
| // constrained game action; no generated JavaScript or shell command is run. | |
| const http = require("http"); | |
| const { execFile } = require("child_process"); | |
| const mineflayer = require("mineflayer"); | |
| const { pathfinder, Movements, goals: { GoalNear } } = require("mineflayer-pathfinder"); | |
| const mcData = require("minecraft-data"); | |
| const Vec3 = require("vec3"); | |
| const { DIRECTIONS, MINE_PALETTE, parseDecision, extractCliResponse, tokenUsage, makePrompt } = require("./gemini_bot_logic"); | |
| const USERNAME = "Gemini_Bot"; | |
| const ADMIN = "IffyBinkie6134"; | |
| const MODEL = "gemini-3.5-flash-lite"; | |
| const KEYS = [1, 2, 3, 4, 5, 6].map(i => process.env[`GEMINI_BOT_KEY_${i}`]).filter(Boolean); | |
| const CLI = process.env.GEMINI_BOT_CLI; | |
| const WORKSPACE = process.env.GEMINI_BOT_WORKSPACE; | |
| const PORT = Number(process.env.GEMINI_BOT_STATUS_PORT || "3001"); | |
| const state = { ready: false, busy: false, reconnect_count: 0, completed_actions: 0, | |
| failed_actions: 0, cli_calls: 0, last_action: null, last_error: null, last_elapsed_s: null, | |
| prompt_tokens: null, completion_tokens: null, models_used: [], model: MODEL, bot_username: USERNAME }; | |
| let bot = null; | |
| let reconnectTimer = null; | |
| let busy = false; | |
| let selectedKey = 0; | |
| let lastChat = 0; | |
| let lastDecision = 0; | |
| let closing = false; | |
| function event(kind, detail = {}) { console.log(JSON.stringify({ at: new Date().toISOString(), kind, ...detail })); } | |
| function observation() { | |
| const p = bot.entity.position; | |
| const nearby = new Set(); | |
| for (let dx = -4; dx <= 4; dx += 2) for (let dz = -4; dz <= 4; dz += 2) { | |
| for (let dy = -1; dy <= 2; dy++) { | |
| const block = bot.blockAt(p.floored().offset(dx, dy, dz)); | |
| if (block && block.name !== "air" && !block.name.endsWith("_air")) nearby.add(block.name); | |
| } | |
| } | |
| const players = {}; | |
| for (const name of [ADMIN, "Naldo_Bot"]) { | |
| const e = bot.players[name] && bot.players[name].entity; | |
| if (e) players[name] = { x: Math.round(e.position.x), y: Math.round(e.position.y), | |
| z: Math.round(e.position.z), distance: Math.round(e.position.distanceTo(p)) }; | |
| } | |
| // Avoid repeatedly digging a hole in the persistent terrain. A new mine | |
| // action becomes available after a distinct move, placement, or follow. | |
| const safeMineBlocks = state.last_action && state.last_action.action === "mine" ? [] : | |
| [...nearby].filter(name => MINE_PALETTE.has(name) && Boolean(safeMineTarget(name))); | |
| return { position: { x: Math.round(p.x), y: Math.round(p.y), z: Math.round(p.z) }, | |
| nearby_blocks: [...nearby].slice(0, 32), safe_mine_blocks: safeMineBlocks, | |
| safe_place_possible: Boolean(safePlaceSupport()), players, | |
| last_action: state.last_action, last_error: state.last_error }; | |
| } | |
| function callCli(prompt) { | |
| return new Promise((resolve, reject) => { | |
| if (!KEYS.length || !CLI) return reject(new Error("Gemini CLI credential or executable unavailable")); | |
| const start = Date.now(); | |
| const keySlot = selectedKey % KEYS.length; | |
| const env = { PATH: process.env.PATH || "/usr/bin:/bin", HOME: process.env.GEMINI_BOT_HOME, | |
| GEMINI_CLI_HOME: process.env.GEMINI_BOT_HOME, GEMINI_CLI_TRUST_WORKSPACE: "true", | |
| GEMINI_SYSTEM_MD: process.env.GEMINI_BOT_SYSTEM_MD, | |
| GEMINI_TELEMETRY_ENABLED: "false", GEMINI_TELEMETRY_LOG_PROMPTS: "false", | |
| LANG: "C.UTF-8", GEMINI_API_KEY: KEYS[keySlot] }; | |
| state.cli_calls++; | |
| execFile(CLI, ["--model", MODEL, "--output-format", "json", "--approval-mode", "plan", "--prompt", prompt], | |
| { cwd: WORKSPACE, env, timeout: 120000, maxBuffer: 512 * 1024 }, (err, stdout) => { | |
| state.last_elapsed_s = Math.round((Date.now() - start) / 100) / 10; | |
| if (err) { | |
| // stderr can contain credential-bearing diagnostic text; never log it. | |
| selectedKey = (selectedKey + 1) % KEYS.length; | |
| let code = String(err.code || "unknown"); | |
| try { | |
| const reported = JSON.parse(stdout).error; | |
| if (reported && Number.isInteger(reported.code)) code += ` API ${reported.code}`; | |
| } catch (_) { /* stderr and raw error messages remain private */ } | |
| return reject(new Error(`Gemini CLI exit ${code}`)); | |
| } | |
| try { | |
| const result = extractCliResponse(stdout); | |
| const usage = tokenUsage(result.stats); | |
| state.prompt_tokens = usage.prompt_tokens; | |
| state.completion_tokens = usage.completion_tokens; | |
| state.models_used = usage.models_used; | |
| resolve(result.decision); | |
| } catch (_) { reject(new Error("Gemini CLI response malformed")); } | |
| }); | |
| }); | |
| } | |
| async function travel(x, y, z, timeoutMs = 22000) { | |
| const start = bot.entity.position.clone(); | |
| const currentBot = bot; | |
| await new Promise((resolve, reject) => { | |
| let done = false; | |
| const finish = () => { | |
| if (done) return; | |
| done = true; | |
| clearTimeout(timer); | |
| currentBot.removeListener("goal_reached", finish); | |
| try { currentBot.pathfinder.setGoal(null); } catch (_) { /* disconnected */ } | |
| resolve(); | |
| }; | |
| const timer = setTimeout(finish, timeoutMs); | |
| currentBot.on("goal_reached", finish); | |
| try { currentBot.pathfinder.setGoal(new GoalNear(x, y, z, 2)); } | |
| catch (err) { clearTimeout(timer); currentBot.removeListener("goal_reached", finish); reject(err); } | |
| }); | |
| if (!currentBot.entity || currentBot.entity.position.distanceTo(start) < 2) | |
| throw new Error("movement not observed"); | |
| return Math.round(currentBot.entity.position.distanceTo(start)); | |
| } | |
| function awayFromPlayers(position, minDistance) { | |
| return [ADMIN, "Naldo_Bot"].every(name => { | |
| const e = bot.players[name] && bot.players[name].entity; | |
| return !e || e.position.distanceTo(position) >= minDistance; | |
| }); | |
| } | |
| function safeMineTarget(name) { | |
| const data = bot.registry.blocksByName[name]; | |
| if (!data || !bot.entity) return null; | |
| try { | |
| const positions = bot.findBlocks({ matching: data.id, maxDistance: 5, count: 24 }); | |
| for (const position of positions) { | |
| if (!awayFromPlayers(position, 3)) continue; | |
| const block = bot.blockAt(position); | |
| if (block && block.name === name) return block; | |
| } | |
| } catch (_) { /* unloaded chunk */ } | |
| return null; | |
| } | |
| function safePlaceSupport() { | |
| if (!bot.entity) return null; | |
| const p = bot.entity.position.floored(); | |
| for (const [dx, dz] of [[2, 0], [-2, 0], [0, 2], [0, -2], [3, 0], [-3, 0], [0, 3], [0, -3]]) { | |
| for (const dy of [-1, 0, -2]) { | |
| const base = bot.blockAt(p.offset(dx, dy, dz)); | |
| if (!base || !["grass_block", "dirt", "stone", "sand"].includes(base.name)) continue; | |
| const destination = bot.blockAt(base.position.offset(0, 1, 0)); | |
| if (destination && destination.name === "air" && awayFromPlayers(destination.position, 4)) return base; | |
| } | |
| } | |
| return null; | |
| } | |
| async function applyDecision(decision, observed) { | |
| if (!bot || !bot.entity) throw new Error("bot disconnected"); | |
| if (decision.action === "wait") return { action: "wait" }; | |
| if (decision.action === "say") { | |
| if (Date.now() - lastChat < 180000) throw new Error("chat rate limited"); | |
| bot.chat(decision.message); | |
| lastChat = Date.now(); | |
| return { action: "say" }; | |
| } | |
| if (decision.action === "explore") { | |
| const [dx, dz] = DIRECTIONS[decision.direction]; | |
| const p = bot.entity.position; | |
| const moved = await travel(Math.floor(p.x) + dx * decision.blocks, Math.floor(p.y), | |
| Math.floor(p.z) + dz * decision.blocks); | |
| return { action: "explore", direction: decision.direction, moved }; | |
| } | |
| if (decision.action === "follow") { | |
| const target = bot.players[decision.player] && bot.players[decision.player].entity; | |
| if (!target) throw new Error("follow target offline"); | |
| const p = target.position; | |
| const moved = await travel(Math.floor(p.x), Math.floor(p.y), Math.floor(p.z), 15000); | |
| return { action: "follow", moved }; | |
| } | |
| if (decision.action === "mine") { | |
| if (!observed.safe_mine_blocks.includes(decision.block)) throw new Error("block not safe in observation"); | |
| const target = safeMineTarget(decision.block); | |
| if (!target) throw new Error("no safe nearby block"); | |
| await bot.dig(target); | |
| const actual = bot.blockAt(target.position); | |
| if (actual && actual.name === decision.block) throw new Error("mined block unchanged"); | |
| return { action: "mine", block: decision.block, changed: true }; | |
| } | |
| if (decision.action === "place") { | |
| const datum = bot.registry.itemsByName[decision.block]; | |
| if (!datum || bot.game.gameMode !== "creative") throw new Error("Creative item unavailable"); | |
| const support = safePlaceSupport(); | |
| if (!support) throw new Error("no safe placement location"); | |
| const Item = require("prismarine-item")(bot.registry); | |
| await bot.creative.setInventorySlot(36, new Item(datum.id, 1)); | |
| await bot.equip(bot.inventory.slots[36] || datum.id, "hand"); | |
| const destination = support.position.offset(0, 1, 0); | |
| await bot.placeBlock(support, new Vec3(0, 1, 0)); | |
| const actual = bot.blockAt(destination); | |
| if (!actual || actual.name !== decision.block) throw new Error("placed block not observed"); | |
| return { action: "place", block: decision.block, changed: true }; | |
| } | |
| throw new Error("invalid action"); | |
| } | |
| async function decide(request = null) { | |
| if (busy || !state.ready || Date.now() - lastDecision < 12000) return; | |
| busy = state.busy = true; | |
| lastDecision = Date.now(); | |
| try { | |
| const activeBot = bot; | |
| const observed = observation(); | |
| let decision; | |
| for (let attempt = 0; attempt < Math.min(KEYS.length, 2); attempt++) { | |
| try { decision = await callCli(makePrompt(observed, request)); break; } | |
| catch (err) { | |
| if (!err.message.startsWith("Gemini CLI exit") || attempt + 1 >= Math.min(KEYS.length, 2)) throw err; | |
| } | |
| } | |
| if (bot !== activeBot || !state.ready) throw new Error("bot reconnected during decision"); | |
| const result = await applyDecision(decision, observed); | |
| state.last_action = result; | |
| state.last_error = null; | |
| state.completed_actions++; | |
| event("action", { result, elapsed_s: state.last_elapsed_s }); | |
| } catch (err) { | |
| state.failed_actions++; | |
| state.last_error = String(err.message).slice(0, 100); | |
| event("action_failed", { reason: state.last_error }); | |
| } finally { busy = state.busy = false; } | |
| } | |
| function connect() { | |
| if (closing) return; | |
| state.ready = false; | |
| const current = mineflayer.createBot({ host: "127.0.0.1", port: 25565, | |
| username: USERNAME, version: "1.19", auth: "offline" }); | |
| bot = current; | |
| current.loadPlugin(pathfinder); | |
| current.once("spawn", () => { | |
| if (bot !== current) return; | |
| const movements = new Movements(current, mcData(current.version)); | |
| current.pathfinder.setMovements(movements); | |
| state.ready = true; | |
| event("spawn", { username: USERNAME, position: observation().position }); | |
| setTimeout(() => decide(), 15000); | |
| }); | |
| current.on("chat", (name, message) => { | |
| if (name !== ADMIN || !/^\s*(?:@?Gemini_Bot)[,:\s]/i.test(message)) return; | |
| decide(message.replace(/^\s*@?Gemini_Bot[,:\s]*/i, "")); | |
| }); | |
| current.on("error", err => event("connection_error", { code: err.code || err.name || "unknown" })); | |
| current.on("kicked", () => event("kicked")); | |
| current.on("end", () => { | |
| if (bot !== current || closing) return; | |
| state.ready = false; | |
| state.reconnect_count++; | |
| event("disconnected", { reconnect_count: state.reconnect_count }); | |
| clearTimeout(reconnectTimer); | |
| reconnectTimer = setTimeout(connect, Math.min(30000, 3000 * state.reconnect_count)); | |
| }); | |
| } | |
| http.createServer((req, res) => { | |
| if (req.url !== "/health") { res.writeHead(404); return res.end(); } | |
| res.setHeader("Content-Type", "application/json"); | |
| let position = null; | |
| try { if (state.ready) position = observation().position; } catch (_) { /* reconnection in progress */ } | |
| res.end(JSON.stringify({ ...state, position })); | |
| }).listen(PORT, "127.0.0.1"); | |
| setInterval(() => decide(), 90000).unref(); | |
| process.on("SIGTERM", () => { closing = true; clearTimeout(reconnectTimer); if (bot) bot.quit(); process.exit(0); }); | |
| event("starting", { model: MODEL, key_count: KEYS.length }); | |
| connect(); | |