mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-09-02 12:23:08 +02:00
Merge branch 'main' into default-behaviors
This commit is contained in:
commit
7e80efb5b4
12 changed files with 169 additions and 48 deletions
35
README.md
35
README.md
|
@ -1,20 +1,41 @@
|
|||
# Mindcraft
|
||||
|
||||
Crafting minds for Minecraft with AI!
|
||||
Crafting minds for Minecraft with ChatGPT and Mineflayer
|
||||
|
||||
#### ‼️Warning‼️
|
||||
|
||||
This project allows an AI model to write/execute code on your computer that may be insecure, dangerous, and vulnerable to injection attacks on public servers. Code writing is disabled by default, you can enable it by setting `allow_insecure_coding` to `true` in `settings.json`. Enable only on local or private servers, **never** on public servers. Ye be warned.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [OpenAI API Subscription](https://openai.com/blog/openai-api)
|
||||
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-bedrock-edition-pc) (at most v1.20.2)
|
||||
- [Node.js](https://nodejs.org/) (at least v14)
|
||||
|
||||
## Installation
|
||||
|
||||
Install Node.js >= 14 from [nodejs.org](https://nodejs.org/)
|
||||
Add `OPENAI_API_KEY` (and optionally `OPENAI_ORG_ID`) to your environment variables
|
||||
|
||||
Install node modules with `npm install`
|
||||
Clone/Download this repository
|
||||
|
||||
## Usage
|
||||
Run `npm install`
|
||||
|
||||
Start minecraft server on localhost port `55916`
|
||||
## Run
|
||||
|
||||
Add `OPENAI_API_KEY` (and optionally `OPENAI_ORG_ID`) to your environment variables.
|
||||
Start a minecraft world and open it to LAN on localhost port `55916`
|
||||
|
||||
run `node main.js`
|
||||
Run `node main.js`
|
||||
|
||||
You can configure details in `settings.json`. Here is an example settings for connecting to a non-local server:
|
||||
```
|
||||
{
|
||||
"minecraft_version": "1.20.1",
|
||||
"host": "111.222.333.444",
|
||||
"port": 55920,
|
||||
"auth": "microsoft",
|
||||
"allow_insecure_coding": false
|
||||
}
|
||||
```
|
||||
|
||||
## Patches
|
||||
|
||||
|
|
7
settings.json
Normal file
7
settings.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"minecraft_version": "1.20.1",
|
||||
"host": "localhost",
|
||||
"port": 55916,
|
||||
"auth": "offline",
|
||||
"allow_insecure_coding": true
|
||||
}
|
|
@ -14,16 +14,18 @@ export class Agent {
|
|||
this.history = new History(this);
|
||||
this.coder = new Coder(this);
|
||||
|
||||
console.log('Loading examples...');
|
||||
|
||||
this.history.load(profile);
|
||||
await this.examples.load('./src/examples.json');
|
||||
await this.coder.load();
|
||||
|
||||
console.log('Logging in...');
|
||||
this.bot = initBot(name);
|
||||
|
||||
initModes(this);
|
||||
|
||||
this.bot.on('login', async () => {
|
||||
|
||||
console.log(`${this.name} logged in.`);
|
||||
this.coder.clear();
|
||||
|
||||
|
|
|
@ -228,13 +228,17 @@ export class Coder {
|
|||
|
||||
async stop() {
|
||||
if (!this.executing) return;
|
||||
const start = Date.now();
|
||||
while (this.executing) {
|
||||
this.agent.bot.interrupt_code = true;
|
||||
this.agent.bot.collectBlock.cancelTask();
|
||||
this.agent.bot.pathfinder.stop();
|
||||
this.agent.bot.pvp.stop();
|
||||
console.log('waiting for code to finish executing... interrupt:', this.agent.bot.interrupt_code);
|
||||
console.log('waiting for code to finish executing...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
if (Date.now() - start > 10 * 1000) {
|
||||
process.exit(1); // force exit program after 10 seconds of failing to stop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as skills from '../library/skills.js';
|
||||
|
||||
import settings from '../../settings.js';
|
||||
|
||||
function wrapExecution(func, timeout=-1, resume_name=null) {
|
||||
return async function (agent, ...args) {
|
||||
|
@ -24,6 +24,8 @@ export const actionsList = [
|
|||
name: '!newAction',
|
||||
description: 'Perform new and unknown custom behaviors that are not available as a command by writing code.',
|
||||
perform: async function (agent) {
|
||||
if (!settings.allow_insecure_coding)
|
||||
return 'Agent is not allowed to write code.';
|
||||
await agent.coder.generateCode(agent.history);
|
||||
}
|
||||
},
|
||||
|
@ -86,6 +88,14 @@ export const actionsList = [
|
|||
await skills.followPlayer(agent.bot, player_name);
|
||||
}, -1, 'followPlayer')
|
||||
},
|
||||
{
|
||||
name: '!moveAway',
|
||||
description: 'Move away from the current location in any direction by a given distance. Ex: !moveAway(2)',
|
||||
params: {'distance': '(number) The distance to move away.'},
|
||||
perform: wrapExecution(async (agent, distance) => {
|
||||
await skills.moveAway(agent.bot, distance);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '!givePlayer',
|
||||
description: 'Give the specified item to the given player. Ex: !givePlayer("steve", "stone_pickaxe", 1)',
|
||||
|
@ -155,5 +165,12 @@ export const actionsList = [
|
|||
perform: wrapExecution(async (agent) => {
|
||||
await skills.goToBed(agent.bot);
|
||||
})
|
||||
},
|
||||
{
|
||||
name: '!stay',
|
||||
description: 'Stay in the current location no matter what. Pauses all modes.',
|
||||
perform: wrapExecution(async (agent) => {
|
||||
await skills.stay(agent.bot);
|
||||
})
|
||||
}
|
||||
];
|
||||
|
|
|
@ -273,12 +273,12 @@ export async function attackEntity(bot, entity, kill=true) {
|
|||
}
|
||||
}
|
||||
log(bot, `Successfully killed ${entity.name}.`);
|
||||
await pickupNearbyItem(bot);
|
||||
await pickupNearbyItems(bot);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function defendSelf(bot, range=8) {
|
||||
export async function defendSelf(bot, range=9) {
|
||||
/**
|
||||
* Defend yourself from all nearby hostile mobs until there are no more.
|
||||
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||
|
@ -336,24 +336,27 @@ export async function collectBlock(bot, blockType, num=1) {
|
|||
blocktypes.push('deepslate_'+blockType);
|
||||
|
||||
let collected = 0;
|
||||
const blocks = world.getNearestBlocks(bot, blocktypes, 64, num);
|
||||
if (blocks.length === 0) {
|
||||
log(bot, `Could not find any ${blockType} to collect.`);
|
||||
return false;
|
||||
}
|
||||
const first_block = blocks[0];
|
||||
await bot.tool.equipForBlock(first_block);
|
||||
const itemId = bot.heldItem ? bot.heldItem.type : null
|
||||
if (!first_block.canHarvest(itemId)) {
|
||||
log(bot, `Don't have right tools to harvest ${blockType}.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let block of blocks) {
|
||||
for (let i=0; i<num; i++) {
|
||||
const blocks = world.getNearestBlocks(bot, blocktypes, 64, 1);
|
||||
if (blocks.length === 0) {
|
||||
if (collected === 0)
|
||||
log(bot, `No ${blockType} nearby to collect.`);
|
||||
else
|
||||
log(bot, `No more ${blockType} nearby to collect.`);
|
||||
break;
|
||||
}
|
||||
const block = blocks[0];
|
||||
await bot.tool.equipForBlock(block);
|
||||
const itemId = bot.heldItem ? bot.heldItem.type : null
|
||||
if (!block.canHarvest(itemId)) {
|
||||
log(bot, `Don't have right tools to harvest ${blockType}.`);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await bot.collectBlock.collect(block);
|
||||
collected++;
|
||||
autoLight(bot);
|
||||
await autoLight(bot);
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'NoChests') {
|
||||
|
@ -379,7 +382,7 @@ export async function pickupNearbyItems(bot) {
|
|||
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||
* @returns {Promise<boolean>} true if the items were picked up, false otherwise.
|
||||
* @example
|
||||
* await skills.pickupNearbyItem(bot);
|
||||
* await skills.pickupNearbyItems(bot);
|
||||
**/
|
||||
const distance = 8;
|
||||
const getNearestItem = bot => bot.nearestEntity(entity => entity.name === 'item' && bot.entity.position.distanceTo(entity.position) < distance);
|
||||
|
@ -413,9 +416,24 @@ export async function breakBlockAt(bot, x, y, z) {
|
|||
* let position = world.getPosition(bot);
|
||||
* await skills.breakBlockAt(bot, position.x, position.y - 1, position.x);
|
||||
**/
|
||||
let current = bot.blockAt(Vec3(x, y, z));
|
||||
if (current.name != 'air')
|
||||
await bot.dig(current, true);
|
||||
let block = bot.blockAt(Vec3(x, y, z));
|
||||
if (block.name !== 'air' && block.name !== 'water' && block.name !== 'lava') {
|
||||
await bot.tool.equipForBlock(block);
|
||||
const itemId = bot.heldItem ? bot.heldItem.type : null
|
||||
if (!block.canHarvest(itemId)) {
|
||||
log(bot, `Don't have right tools to break ${block.name}.`);
|
||||
return false;
|
||||
}
|
||||
if (bot.entity.position.distanceTo(block.position) > 4.5) {
|
||||
let pos = block.position;
|
||||
let movements = new pf.Movements(bot);
|
||||
movements.canPlaceOn = false;
|
||||
movements.allow1by1towers = false;
|
||||
bot.pathfinder.setMovements();
|
||||
await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4));
|
||||
}
|
||||
await bot.dig(block, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -663,7 +681,45 @@ export async function followPlayer(bot, username) {
|
|||
log(bot, `You are now actively following player ${username}.`);
|
||||
|
||||
while (!bot.interrupt_code) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export async function moveAway(bot, distance) {
|
||||
/**
|
||||
* Move away from current position in any direction.
|
||||
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||
* @param {number} distance, the distance to move away.
|
||||
* @returns {Promise<boolean>} true if the bot moved away, false otherwise.
|
||||
* @example
|
||||
* await skills.moveAway(bot, 8);
|
||||
**/
|
||||
const pos = bot.entity.position;
|
||||
let goal = new pf.goals.GoalNear(pos.x, pos.y, pos.z, distance);
|
||||
let inverted_goal = new pf.goals.GoalInvert(goal);
|
||||
bot.pathfinder.setMovements(new pf.Movements(bot));
|
||||
await bot.pathfinder.goto(inverted_goal);
|
||||
let new_pos = bot.entity.position;
|
||||
log(bot, `Moved away from nearest entity to ${new_pos}.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function stay(bot) {
|
||||
/**
|
||||
* Stay in the current position until interrupted. Disables all modes.
|
||||
* @param {MinecraftBot} bot, reference to the minecraft bot.
|
||||
* @returns {Promise<boolean>} true if the bot stayed, false otherwise.
|
||||
* @example
|
||||
* await skills.stay(bot);
|
||||
**/
|
||||
bot.modes.pause('self_defense');
|
||||
bot.modes.pause('hunting');
|
||||
bot.modes.pause('torch_placing');
|
||||
bot.modes.pause('item_collecting');
|
||||
while (!bot.interrupt_code) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import pf from 'mineflayer-pathfinder';
|
||||
import { getAllBlockIds } from '../../utils/mcdata.js';
|
||||
import { getAllBlockIds, getAllBiomes } from '../../utils/mcdata.js';
|
||||
|
||||
|
||||
export function getNearestFreeSpace(bot, size=1, distance=8) {
|
||||
|
@ -271,5 +271,5 @@ export function getBiomeName(bot) {
|
|||
* let biome = world.getBiomeName(bot);
|
||||
**/
|
||||
const biomeId = bot.world.getBiome(bot.entity.position);
|
||||
return mcdata.biomes[biomeId].name;
|
||||
return getAllBiomes()[biomeId].name;
|
||||
}
|
||||
|
|
|
@ -55,22 +55,25 @@ const modes = [
|
|||
active: false,
|
||||
|
||||
wait: 2, // number of seconds to wait after noticing an item to pick it up
|
||||
noticedAt: -1,
|
||||
prev_item: null,
|
||||
noticed_at: -1,
|
||||
update: async function (agent) {
|
||||
let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8);
|
||||
if (item && await world.isClearPath(agent.bot, item)) {
|
||||
if (this.noticedAt === -1) {
|
||||
this.noticedAt = Date.now();
|
||||
if (item && item !== this.prev_item && await world.isClearPath(agent.bot, item)) {
|
||||
if (this.noticed_at === -1) {
|
||||
this.noticed_at = Date.now();
|
||||
}
|
||||
if (Date.now() - this.noticedAt > this.wait * 1000) {
|
||||
if (Date.now() - this.noticed_at > this.wait * 1000) {
|
||||
agent.bot.chat(`Picking up ${item.name}!`);
|
||||
this.prev_item = item;
|
||||
execute(this, agent, async () => {
|
||||
await skills.pickupNearbyItems(agent.bot);
|
||||
});
|
||||
this.noticed_at = -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.noticedAt = -1;
|
||||
this.noticed_at = -1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -33,6 +33,6 @@
|
|||
],
|
||||
[
|
||||
{"role": "user", "content": "brug: build a dirt house"},
|
||||
{"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n// place a torch in the middle of the room\nawait skills.placeBlock(bot, 'torch', startX + width / 2, startY + height / 2, startZ + 1);```"}
|
||||
{"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n```"}
|
||||
]
|
||||
]
|
3
src/settings.js
Normal file
3
src/settings.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { readFileSync } from 'fs';
|
||||
const settings = JSON.parse(readFileSync('./settings.json', 'utf8'));
|
||||
export default settings;
|
|
@ -7,11 +7,17 @@ if (process.env.OPENAI_ORG_ID) {
|
|||
organization: process.env.OPENAI_ORG_ID,
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
};
|
||||
} else {
|
||||
}
|
||||
else if (process.env.OPENAI_API_KEY) {
|
||||
openAiConfig = {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
};
|
||||
}
|
||||
else {
|
||||
console.error('OpenAI API key missing! Make sure you set OPENAI_API_KEY and OPENAI_ORG_ID (optional) environment variables.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const openai = new OpenAIApi(openAiConfig);
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import minecraftData from 'minecraft-data';
|
||||
import settings from '../settings.js';
|
||||
import { createBot } from 'mineflayer';
|
||||
import { pathfinder } from 'mineflayer-pathfinder';
|
||||
import { plugin as pvp } from 'mineflayer-pvp';
|
||||
|
@ -7,7 +8,7 @@ import { plugin as autoEat } from 'mineflayer-auto-eat';
|
|||
import plugin from 'mineflayer-armor-manager';
|
||||
const armorManager = plugin;
|
||||
|
||||
const mc_version = '1.20.1'
|
||||
const mc_version = settings.minecraft_version;
|
||||
const mcdata = minecraftData(mc_version);
|
||||
|
||||
|
||||
|
@ -15,12 +16,9 @@ export function initBot(username) {
|
|||
let bot = createBot({
|
||||
username: username,
|
||||
|
||||
host: 'localhost',
|
||||
port: 55916,
|
||||
|
||||
// host: '000.111.222.333',
|
||||
// port: 55920,
|
||||
// auth: 'microsoft',
|
||||
host: settings.host,
|
||||
port: settings.port,
|
||||
auth: settings.auth,
|
||||
|
||||
version: mc_version,
|
||||
});
|
||||
|
@ -97,3 +95,7 @@ export function getAllBlockIds(ignore) {
|
|||
}
|
||||
return blockIds;
|
||||
}
|
||||
|
||||
export function getAllBiomes() {
|
||||
return mcdata.biomes;
|
||||
}
|
Loading…
Add table
Reference in a new issue