Merge branch 'main' into default-behaviors

This commit is contained in:
MaxRobinsonTheGreat 2024-02-04 19:53:09 -06:00
commit 7e80efb5b4
12 changed files with 169 additions and 48 deletions

View file

@ -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
View file

@ -0,0 +1,7 @@
{
"minecraft_version": "1.20.1",
"host": "localhost",
"port": 55916,
"auth": "offline",
"allow_insecure_coding": true
}

View file

@ -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();

View file

@ -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
}
}
}

View file

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

View file

@ -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);
for (let i=0; i<num; i++) {
const blocks = world.getNearestBlocks(bot, blocktypes, 64, 1);
if (blocks.length === 0) {
log(bot, `Could not find any ${blockType} to collect.`);
return false;
if (collected === 0)
log(bot, `No ${blockType} nearby to collect.`);
else
log(bot, `No more ${blockType} nearby to collect.`);
break;
}
const first_block = blocks[0];
await bot.tool.equipForBlock(first_block);
const block = blocks[0];
await bot.tool.equipForBlock(block);
const itemId = bot.heldItem ? bot.heldItem.type : null
if (!first_block.canHarvest(itemId)) {
if (!block.canHarvest(itemId)) {
log(bot, `Don't have right tools to harvest ${blockType}.`);
return false;
}
for (let block of blocks) {
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;
}

View file

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

View file

@ -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;
}
}
},

View file

@ -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
View file

@ -0,0 +1,3 @@
import { readFileSync } from 'fs';
const settings = JSON.parse(readFileSync('./settings.json', 'utf8'));
export default settings;

View file

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

View file

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