diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7275bb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv/ diff --git a/.replit b/.replit new file mode 100644 index 0000000..6e544a6 --- /dev/null +++ b/.replit @@ -0,0 +1,105 @@ +# The command that runs the program. If the interpreter field is set, it will have priority and this run command will do nothing +run = "python3 main.py" + +# The primary language of the repl. There can be others, though! +language = "python3" +entrypoint = "main.py" +# A list of globs that specify which files and directories should +# be hidden in the workspace. +hidden = ["venv", ".config", "**/__pycache__", "**/.mypy_cache", "**/*.pyc"] + +# Specifies which nix channel to use when building the environment. +[nix] +channel = "stable-21_11" + +# The command to start the interpreter. +[interpreter] + [interpreter.command] + args = [ + "stderred", + "--", + "prybar-python3", + "-q", + "--ps1", + "\u0001\u001b[33m\u0002\u0001\u001b[00m\u0002 ", + "-i", + ] + env = { LD_LIBRARY_PATH = "$PYTHON_LD_LIBRARY_PATH" } + +[env] +VIRTUAL_ENV = "/home/runner/${REPL_SLUG}/venv" +PATH = "${VIRTUAL_ENV}/bin" +PYTHONPATH = "${VIRTUAL_ENV}/lib/python3.8/site-packages" +REPLIT_POETRY_PYPI_REPOSITORY = "https://package-proxy.replit.com/pypi/" +MPLBACKEND = "TkAgg" +POETRY_CACHE_DIR = "${HOME}/${REPL_SLUG}/.cache/pypoetry" + +# Enable unit tests. This is only supported for a few languages. +[unitTest] +language = "python3" + +# Add a debugger! +[debugger] +support = true + + # How to start the debugger. + [debugger.interactive] + transport = "localhost:0" + startCommand = ["dap-python", "main.py"] + + # How to communicate with the debugger. + [debugger.interactive.integratedAdapter] + dapTcpAddress = "localhost:0" + + # How to tell the debugger to start a debugging session. + [debugger.interactive.initializeMessage] + command = "initialize" + type = "request" + + [debugger.interactive.initializeMessage.arguments] + adapterID = "debugpy" + clientID = "replit" + clientName = "replit.com" + columnsStartAt1 = true + linesStartAt1 = true + locale = "en-us" + pathFormat = "path" + supportsInvalidatedEvent = true + supportsProgressReporting = true + supportsRunInTerminalRequest = true + supportsVariablePaging = true + supportsVariableType = true + + # How to tell the debugger to start the debuggee application. + [debugger.interactive.launchMessage] + command = "attach" + type = "request" + + [debugger.interactive.launchMessage.arguments] + logging = {} + +# Configures the packager. +[packager] +language = "python3" +ignoredPackages = ["unit_tests"] + + [packager.features] + enabledForHosting = false + # Enable searching packages from the sidebar. + packageSearch = true + # Enable guessing what packages are needed from the code. + guessImports = true + +# These are the files that need to be preserved when this +# language template is used as the base language template +# for Python repos imported from GitHub +[gitHubImport] +requiredFiles = [".replit", "replit.nix", ".config", "venv"] + +[languages] + +[languages.python3] +pattern = "**/*.py" + +[languages.python3.languageServer] +start = "pylsp" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..42afd1b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Regression Games, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dff34ff --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Python Bot (Regression Games) + +This template is a starting point for our experimental Python language support in Regression Games. Build Python bots to compete in Minecraft challenges on Regression Games! + +* See the [start.py](#start.py) file for starting code + +## Requirements + +To make a valid bot, you must: + +* Have a file called `start.py` +* Have a function with signature `configure_bot(bot)` + +## Known Limitations + +Python bots on Regression Games work by integrating into our JavaScript bots. This means that the Python calls to the bot are complete via calls to a Node/JavaScript backend. There are some known limitations to the current setup. + +* The bot may be slower than JavaScript bots +* There is limited support for code written in separate files + +Please see this note for more limitations: https://regressiongg.notion.site/Python-Common-Errors-34ea3ed2e5de4cd29529c49638a92a42 + +_Please provide us with feedback and suggestions for which limitations are blockers, and any other thoughts you may have!_ \ No newline at end of file diff --git a/regression_games.py b/regression_games.py new file mode 100644 index 0000000..192183e --- /dev/null +++ b/regression_games.py @@ -0,0 +1,22 @@ +from rg_javascript import require, On + +RG_BOT_VERSION = '1.10.0' +RG_CTF_UTILS_VERSION = '1.0.5' +MINEFLAYER_VERSION = '4.5.1' + +# Because of how we load these JS modules to be used in Python, we +# define everything here to abstract away the "JS"-ness of it +# TODO: We can attach types here for easier development +mineflayer_pathfinder = require('mineflayer-pathfinder') +mineflayer = require('mineflayer', MINEFLAYER_VERSION) +rg_match_info = require('rg-match-info') +Vec3 = require('vec3').Vec3 +RGBot = require('rg-bot', RG_BOT_VERSION).RGBot +FindResult = require('rg-bot', RG_BOT_VERSION).FindResult +RGCTFUtils = require('rg-ctf-utils', RG_CTF_UTILS_VERSION).RGCTFUtils +CTFEvent = require('rg-ctf-utils', RG_CTF_UTILS_VERSION).CTFEvent +armorManager = require('mineflayer-armor-manager') +Item = require('prismarine-item').Item +Entity = require('prismarine-entity').Entity +goals = mineflayer_pathfinder.goals +RGEventHandler = On diff --git a/replit.nix b/replit.nix new file mode 100644 index 0000000..bb4b987 --- /dev/null +++ b/replit.nix @@ -0,0 +1,18 @@ +{ pkgs }: { + deps = [ + pkgs.python38Full + ]; + env = { + PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + # Needed for pandas / numpy + pkgs.stdenv.cc.cc.lib + pkgs.zlib + # Needed for pygame + pkgs.glib + # Needed for matplotlib + pkgs.xorg.libX11 + ]; + PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; + LANG = "en_US.UTF-8"; + }; +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b27d72a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +rg_javascript==1!1.0.4 \ No newline at end of file diff --git a/start.py b/start.py new file mode 100644 index 0000000..70e6112 --- /dev/null +++ b/start.py @@ -0,0 +1,193 @@ +""" +An example of a bot that uses a main loop to make decisions +on almost every tick of the game. +""" + +import logging +import json +import traceback + +logging.basicConfig(level=logging.NOTSET) + +import os, sys +sys.path.append(os.path.dirname(__file__)) + + +def configure_bot(bot): + """ + configure_bot is called by Regression games - this is where you configure + how your bot behaves + """ + + from regression_games import RGBot, RGCTFUtils, armorManager, RGEventHandler, Vec3, Entity + from utilities import get_unbreakable_blocks, name_for_item, nearest_teammates, throttle_runtime + from strategy import handle_attack_flag_carrier, handle_attack_nearby_opponent, handle_bot_idle_position, handle_looting_items, handle_low_health, handle_placing_blocks, handle_scoring_flag, handle_collecting_flag + + + # Disable rg-bot debug logging. You can enable this to see more details about rg-bot api calls + bot.setDebug(False) + + # Allow parkour so that our bots pathfinding will jump short walls and optimize their path for sprint jumps. + bot.allowParkour(True) + + # We recommend disabling this on as you can't dig the CTF map. Turning this on can lead pathfinding to get stuck. + bot.allowDigWhilePathing(False) + + # Setup the rg-ctf-utils with debug logging + rg_ctf_utils = RGCTFUtils(bot) + rg_ctf_utils.setDebug(True) + + # Load the armor-manager plugin (https://github.com/PrismarineJS/MineflayerArmorManager) + bot.mineflayer().loadPlugin(armorManager) + + # default to true in-case we miss the start + match_in_progress = True + + # Information about the unbreakable block types + unbreakable = get_unbreakable_blocks(bot) + + @RGEventHandler(bot, 'match_ended') + def match_ended(self, match_info, *args): + if match_info: + players = [p for p in match_info.players if p.username == bot.username()] + player = players[0] if players else None + if player: + points = player.metadata.score + captures = player.metadata.flagCaptures + print(f'The match has ended - I had {captures} captures and scored {points} points') + match_in_progress = False + + @RGEventHandler(bot, 'match_started') + def match_started(self, match_info, *args): + print("The match has started") + match_in_progress = True + + # Part of using a main loop is being careful not to leave it running at the wrong time. + # It is very easy to end up with 2 loops running by accident. + # Here we track the mainLoop instance count and update on key events. + main_loop_instance_tracker = 0 + + + @RGEventHandler(bot, 'playerLeft') + def player_left(self, player, *args): + if (player.username == bot.username()): + print("I have left the match") + main_loop_instance_tracker += 1 + + @RGEventHandler(bot, 'end') + def end(self, *args): + print("I have disconnected") + main_loop_instance_tracker += 1 + + @RGEventHandler(bot, 'kicked') + def kicked(self, *args): + print("I have been kicked") + main_loop_instance_tracker += 1 + + @RGEventHandler(bot, 'death') + def death(self, *args): + print("I have died") + main_loop_instance_tracker += 1 + try: + # Try to stop any goal currently going on + bot.mineflayer().pathfinder.setGoal(None) + bot.mineflayer().pathfinder.stop() + except Exception: + pass + + # Take a look at the spawn event handler at the end + def main_loop(): + current_main_loop_instance = main_loop_instance_tracker + is_active_function = lambda: match_in_progress and current_main_loop_instance == main_loop_instance_tracker + while is_active_function(): + + try: + # always throttle the runtime first to make sure we don't execute too frequently and waste CPU + throttle_runtime(bot) + + if not bot.matchInfo(): + print("Match info not available yet, waiting") + continue + + #find out which team I'm on + my_team_name: str = bot.getMyTeam() + other_team_names: str = [t for t in bot.matchInfo().teams if t.name != my_team_name] + other_team_name = other_team_names[0].name if other_team_names else None + + # get my current position and log information about my state + my_position: Vec3 = bot.position() + print(f'My team: {my_team_name}, my position: {bot.vecToString(my_position)}, my inventory: ${json.dumps([name_for_item(item) for item in bot.getAllInventoryItems()])}') + + # find any opponents in range + opponent_names: list[str] = list(bot.getOpponentUsernames()) + print(f'Found the following opponents: {opponent_names}') + print(opponent_names if opponent_names else ['...']) + opponents_results = bot.findEntities({ + # opNames can be empty in practice mode where there is no other team + # if we don't pass some array to match, then this will return all entities instead + 'entityNames': opponent_names if opponent_names else ['...'], + 'attackable': True, + 'maxCount': 3, + 'maxDistance': 33, # Bots can only see ~30 +/1 blocks, so no need to search far + # override the default value function here as we aren't using this value in the sortValueFunction + 'entityValueFunction': lambda entity_name: 0, + # just sort them by distance for now... We'll filter them by decision point later + 'sortValueFunction': lambda distance, entity_value, health, defense, toughness: distance + }) + opponents: list[Entity] = [o.result for o in opponents_results] + + # find any teammates in range + teammates: list[Entity] = nearest_teammates(bot, 33, True) + + # equip my best armor + bot.mineflayer().armorManager.equipAll() + + # Only take 1 action per main loop pass. There are exceptions, but this is best practice as the + # game server can only process so many actions per tick + did_something: bool = False + + if not did_something: + # Check if I'm low on health + did_something = handle_low_health(bot, rg_ctf_utils, opponents, teammates) + + if not did_something: + # if someone has the flag, hunt down player with flag if it isn't a team-mate + did_something = handle_attack_flag_carrier(bot, rg_ctf_utils, opponents, teammates) + + if not did_something: + # do I need to attack a nearby opponent + did_something = handle_attack_nearby_opponent(bot, rg_ctf_utils, opponents, teammates) + + if not did_something: + # if I have the flag, go score + did_something = handle_scoring_flag(bot, rg_ctf_utils, opponents, teammates) + + if not did_something: + # go pickup the loose flag + did_something = handle_collecting_flag(bot, rg_ctf_utils, opponents, teammates) + + if not did_something: + # If no-one within N blocks, place blocks + did_something = handle_placing_blocks(bot, rg_ctf_utils, opponents, teammates) + + if not did_something: + # see if we can find some items to loot + did_something = handle_looting_items(bot, rg_ctf_utils, opponents, teammates) + + if not did_something: + # we had nothing to do ... move towards the middle + did_something = handle_bot_idle_position(bot, rg_ctf_utils, opponents, teammates) + except Exception as exc: + # if we get anything other than a pathfinding change error, log it so that we can fix our bot + if 'GoalChanged' not in str(exc) or 'PathStopped' not in str(exc): + print("An exception occurred while running this turn of logic") + print(traceback.format_exc()) + # wait 1 seconds before looping again to avoid tight loops on errors + bot.wait(20) + + print(f'Ended loop that ran for instance {main_loop_instance_tracker} of the bot') + + @RGEventHandler(bot, 'spawn') + def on_spawn(self, *args): + bot.chat('I have come to win Capture The Flag with my main loop.') + main_loop() diff --git a/strategy.py b/strategy.py new file mode 100644 index 0000000..50e7713 --- /dev/null +++ b/strategy.py @@ -0,0 +1,164 @@ +""" +A set of strategies for our bot +""" +import math +from typing import List +from regression_games import RGBot, FindResult, Item, Entity, Vec3, RGCTFUtils +from utilities import move_toward_position, use_potion_of_type, get_potion_of_type, use_potion + +def handle_low_health(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + if bot.mineflayer().health <= 7: + #near death, see if I can use a potion to make the opponent die with me + nearby_opponents = [o for o in opponents if o.position.distanceSquared(bot.position()) <= 16] + near_opponent = nearby_opponents[0] if nearby_opponents else None + if near_opponent: + potion: Item = get_potion_of_type(bot, 'ninja') + if potion: + # look at their feet before throwing down a ninja potion + bot.mineflayer().lookAt(near_opponent.position.offset(0, -1, 0)) # TODO: WAS AWAIT + return use_potion(bot, potion) # TODO: WAS AWAIT + elif bot.mineflayer().health <= 15: + # just need a top up + print('[Health] Need to use potion while my health is low') + return use_potion_of_type(bot, 'health') # TODO: WAS AWAIT + return False + +def handle_attack_flag_carrier(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + """ + find out if the flag is available + """ + flag_location: Vec3 = rg_ctf_utils.getFlagLocation() + if flag_location is None: + print(f'Checking {len(opponents)} opponents in range for flag carriers') + # see if one of these opponents is holding the flag + opponents_with_flag = [o for o in opponents if o.heldItem and rg_ctf_utils.FLAG_SUFFIX in o.heldItem.name] + opponent_with_flag = opponents_with_flag[0] if opponents_with_flag else None + + if opponent_with_flag: + print(f'Attacking flag carrier {opponent_with_flag.name} at position: ${bot.vecToString(opponent_with_flag.position)}') + use_potion_of_type(bot, 'movement') # run faster to get them + # TODO: Once I get in range of attack, should I use a combat potion ? should I equip a shield ? + bot.attackEntity(opponent_with_flag) + return True + return False + +def handle_attack_nearby_opponent(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + outnumbered = len(teammates) + 1 < len(opponents) + yolo = len(teammates) == 0 + + my_position = bot.position() + + # opportunistically kill any player in close range even if that means dropping the flag to do it + # within range 10 regular, 5 if I have the flag + the_opponents = [o for o in opponents if o.position.distanceSquared(my_position) <= (25 if rg_ctf_utils.hasFlag() else 100)] + + print(f'Checking {len(the_opponents)} opponents in range to murder') + if the_opponents: + first_opponent = the_opponents[0] + + # Attack if a teammate is nearby only, otherwise move toward team-mate + if not outnumbered or yolo: + print(f'Attacking opponent at position: {bot.vecToString(first_opponent.position)}') + # TODO: Once I get in range of attack, should I use a combat potion ? should I equip a shield ? + bot.attackEntity(first_opponent) + return True + else: + print('Outnumbered, running to nearest team-mate for help') + # TODO: Do I need to use potions ? un-equip my shield to run faster ? + move_toward_position(bot, teammates[0].position, 3) + return True + return False + +def handle_scoring_flag(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + if rg_ctf_utils.hasFlag(): + # TODO: Do I need to use potions ? un-equip my shield to run faster ? + print('I have the flag, running to score') + my_team_name = bot.getMyTeam() + my_score_location = rg_ctf_utils.BLUE_SCORE_LOCATION if my_team_name == 'BLUE' else rg_ctf_utils.RED_SCORE_LOCATION + move_toward_position(bot, my_score_location, 1) + return True + return False + +def handle_collecting_flag(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + flag_location: Vec3 = rg_ctf_utils.getFlagLocation() + if flag_location: + print(f'Moving toward the flag at {bot.vecToString(flag_location)}') + # TODO: Do I need to use potions ? un-equip my shield to run faster ? + move_toward_position(bot, flag_location, 1) + return True + return False + +placeable_block_display_names = ['Gravel', 'Grass Block', 'Dirt', 'Stripped Dark Oak Wood'] + +# bridge blockade +blue_block_placements = [Vec3(81,65,-387), Vec3(81, 66, -387), Vec3(81,65,-385), Vec3(81, 66, -385)] + +# bridge blockade +red_block_placements = [Vec3(111,65,-387), Vec3(111, 66, -387), Vec3(111,65,-385), Vec3(111, 66, -385)] + +def handle_placing_blocks(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + my_position = bot.position() + my_team_name = bot.getMyTeam() + + # only consider bots on the same y plane not those down in the tunnel, and within range 15 + the_opponents = [o for o in opponents if abs(o.position.y - my_position.y) < 5 and o.position.distanceSquared(my_position) < 225] + + print(f'Checking {len(the_opponents)} opponents in range before getting items or placing blocks') + if len(the_opponents) == 0: + # If I have blocks to place, go place blocks at strategic locations if they aren't already filled + block_in_inventory = [i for i in bot.getAllInventoryItems() if i.displayName in placeable_block_display_names] + + if block_in_inventory: + print(f'I have a "{block_in_inventory.displayName}" block to place') + block_placements = blue_block_placements if my_team_name == 'BLUE' else red_block_placements + for location in block_placements: + # if I'm within 20 blocks of a place to put blocks + block = bot.mineflayer().blockAt(location) + range_sq = location.distanceSquared(my_position) + print(f'Checking for block: {block and block.type} at range_sq: {range_sq}') + if range_sq <= 400: + if not block or block.type == 0: # air + print(f'Moving to place block "{block_in_inventory.displayName}" at: {location}') + move_toward_position(bot, location, 3) + # if I'm close, then place the block + if location.distanceSquared(my_position) < 15: + print(f'Placing block "{block_in_inventory.displayName}" at: {location}') + # TODO: RGBot.placeBlock should handle this for us once a defect is fixed + bot.mineflayer().equip(block_in_inventory, 'hand') + # place block on top face of the block under our target + bot.mineflayer().placeBlock(bot.mineflayer().blockAt(location.offset(0, -1, 0)), Vec3(0, 1, 0)) + return True + else: + print('No placeable blocks in inventory') + return False + +def handle_looting_items(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + my_position = bot.position() + items = bot.findItemsOnGround({ + 'maxDistance': 33, + 'maxCount': 5, + # prioritize items I don't have that are the closest + 'itemValueFunction': lambda block_name: 999999 if bot.inventoryContainsItem(block_name) else 1, + 'sortValueFunction': lambda distance, point_value: distance * point_value + }) + # TODO: Should I let my bots run down into the tunnel for better loot ? + # or keep them on the top only + items = [i.result for i in items if abs(i.result.position.y - my_position.y) < 5] + item = items[0] if items else None + + if item: + print(f'Going to collect item: {item.name} at: {bot.vecToString(item.position)}') + # TODO: Do I need to use potions ? un-equip my shield to run faster ? + move_toward_position(bot, item.position, 1) + return True + return False + +def handle_bot_idle_position(bot: RGBot, rg_ctf_utils: RGCTFUtils, opponents: List[Entity], teammates: List[Entity]) -> bool: + # TODO: Is this really the best place to move my bot towards ? + # Hint: This is where most of Macro game strategy gets implemented + # Do my bots spread out to key points looking for items or opponents ? + # Do my bots group up to control key areas of the map ? + # Do those areas of the map change dependent on where the flag currently is ? + print(f'Moving toward center point: ${bot.vecToString(rg_ctf_utils.FLAG_SPAWN)}') + move_toward_position(bot, rg_ctf_utils.FLAG_SPAWN, 1) + return True diff --git a/utilities.py b/utilities.py new file mode 100644 index 0000000..2892cbb --- /dev/null +++ b/utilities.py @@ -0,0 +1,235 @@ +""" +A set of utilities for Regression Games and Capture the Flag +""" +from regression_games import RGBot, Entity, Vec3, goals, Item +import time +from typing import Union, List +import json + + +def get_unbreakable_blocks(bot: RGBot) -> list: + """ + Returns a list of all unbreakable block types in the RG CTF mode. + :param bot: The bot being configured + """ + blocks_by_name = bot.mcData.blocksByName + return [ + # materials used for castles + blocks_by_name.stone_bricks.id, + blocks_by_name.stone_brick_slab.id, + blocks_by_name.stone_brick_stairs.id, + blocks_by_name.stone_brick_wall.id, + blocks_by_name.ladder.id, + blocks_by_name.cracked_stone_bricks.id, + blocks_by_name.white_carpet.id, + + # blue castle + blocks_by_name.blue_carpet.id, + blocks_by_name.light_blue_carpet.id, + blocks_by_name.blue_stained_glass_pane.id, + blocks_by_name.light_blue_stained_glass_pane.id, + blocks_by_name.soul_torch.id, + blocks_by_name.soul_wall_torch.id, + blocks_by_name.soul_lantern.id, + blocks_by_name.lapis_block.id, + blocks_by_name.blue_glazed_terracotta.id, + + # red castle + blocks_by_name.red_carpet.id, + blocks_by_name.pink_carpet.id, + blocks_by_name.red_stained_glass_pane.id, + blocks_by_name.pink_stained_glass_pane.id, + blocks_by_name.redstone_torch.id, + blocks_by_name.redstone_wall_torch.id, + blocks_by_name.lantern.id, + blocks_by_name.red_wool.id, + blocks_by_name.red_glazed_terracotta.id, + + # item spawns + flag barrier + blocks_by_name.polished_andesite.id, + blocks_by_name.polished_andesite_slab.id, + blocks_by_name.polished_andesite_stairs.id, + + # arena, obstacles, and underwater tunnel + blocks_by_name.snow_block.id, + blocks_by_name.snow.id, + blocks_by_name.glass.id, + blocks_by_name.glass_pane.id, + blocks_by_name.white_stained_glass_pane.id, + blocks_by_name.spruce_fence.id + ] + + +def nearest_teammates(bot: RGBot, max_distance=33, bots_only=True) -> List[Entity]: + """ + Finds any teammates I have within the maxDistance. Results are sorted by closest distance from my bot. + Note: The bot can only see ~30 +/- blocks. So you may have a team-mate at 40 blocks away but this API won't find them. + You could share information between bots via in game whispers if you want to share location information beyond the bot + sight range. + + Args: + bot: RGBot instance + max_distance: The maximum distance to look for a teammate. Don't set larger than 33. Defaults to 33 + bots_only: Whether to select only bots or also human players on the same team. Defaults to True. + + Returns: The list of teammates that are nearby + """ + match_info = bot.matchInfo() + if match_info: + bot_name = bot.username() + team_name = bot.teamForPlayer(bot_name) + print(f'Checking for any team-mates in range: {max_distance}') + if team_name: + teammates = [p for p in match_info.players if p.team == team_name and ( + not bots_only or p.isBot) and p.username is not bot_name] + if teammates: + my_position = bot.position() + entities = bot.findEntities({ + 'entityNames': [p.username for p in teammates], + 'attackable': True, + 'maxDistance': max_distance + }) + entities = [e.result for e in entities] + return sorted(entities, key=lambda t: t.position.distanceSquared(my_position)) + return [] + + +last_move_position: Vec3 = None + + +def move_toward_position(bot: RGBot, target_position: Vec3, reach: int = 1, should_wait: bool = False) -> bool: + """ + Handles movement from a main loop bot. It is important NOT to change the pathfinding target every loop iteration unless + it really needs to change. This function handles only updating the target when the destination has changed. + + Args: + bot: RGBot instance + target_position: The pathfinding destination position + reach: How many blocks away from the target position should I get before pathfinding stops. Defaults to 1. + should_wait: should I await pathfinding (true), or let it run asynchronously in the background (false). Defaults to False. + + Returns: True if the bot successfully moved toward the position + + """ + global last_move_position + is_moving = bot.mineflayer().pathfinder.isMoving() + if not last_move_position or not is_moving or target_position.distanceSquared(last_move_position) > reach**2: + print( + f'[Movement] Moving toward position: {bot.vecToString(target_position)}, isMoving: {is_moving}') + last_move_position = target_position + if should_wait: + bot.approachPosition( + target_position, {reach: reach}) + print( + f'[Movement] Reached target position: {bot.vecToString(target_position)}') + else: + # DO NOT AWAIT PATHING... WE'LL INTERRUPT IT LATER WITH A NEW TARGET IF NEED BE + # TODO: START THIS ON A NEW THREAD + bot.mineflayer().pathfinder.goto(goals.GoalNear(target_position.x, target_position.y, target_position.z, reach)) + print(f'[Movement] Reached target position: ${bot.vecToString(target_position)}') + last_move_position = None + return True + else: + print('[Movement] Not changing movement target because previous ~= new') + return False + + +last_run_time = -1 + + +def throttle_runtime(bot: RGBot): + """ + Used at the start of each main loop to throttle the runtime. Minecraft server runs at 20 ticks per second (50ms per tick). + Thus executing our bot main loop more frequently than every 50ms would re-process stale game state. + Executing more often that this would waste CPU and starve the other bots on our team, which share our limited CPU resources. + """ + compute_wait_time = 50 + + global last_run_time + + wait_time = (last_run_time + compute_wait_time) - int(time.time()*1000) + if wait_time > 0: + print(f'[Throttle] Waiting {wait_time} millis before next loop') + bot.wait(round(wait_time*20/1000)) # TODO: WAS AWAIT + + last_run_time = int(time.time()*1000) + + +# sort potions with the ones you want to use first near the front +MOVEMENT_POTIONS = ['Gotta Go Fast', 'Lava Swim'] +COMBAT_POTIONS = ['Increased Damage Potion'] +NINJA_POTIONS = ['Poison Cloud II', 'Poison Cloud'] +HEALTH_POTIONS = ['Totem of Undying', 'Healing Potion', 'Tincture of Life', + 'Tincture of Mending II', 'Tincture of Mending', 'Golden Apple'] + +POTION_TYPE = Union['movement', 'combat', 'ninja', 'health'] + +def get_potion_of_type(bot: RGBot, potion_type: POTION_TYPE) -> Union[str, None]: + """ + get the potion item from the bot's inventory of the specified type if it exists + """ + potions = [] + if potion_type == 'movement': + potions = MOVEMENT_POTIONS + elif potion_type == 'combat': + potions = COMBAT_POTIONS + elif potion_type == 'ninja': + potions = NINJA_POTIONS + elif potion_type == 'health': + potions = HEALTH_POTIONS + + if len(potions) > 0: + inventory_items = bot.getAllInventoryItems() + found_potions = [item for item in inventory_items if name_for_item(item) in potions] + return found_potions[0] if found_potions else None + return None + +def use_potion(bot: RGBot, potion: str) -> bool: + """ + hold and activate the given potion item from the bot's inventory + """ + if potion: + bot.holdItem(potion) # TODO: WAS AWAIT + print(f"[Potions] Using potion: {name_for_item(potion)}") + bot.mineflayer().activateItem(False) + return True + return False + +def use_potion_of_type(bot: RGBot, potion_type: POTION_TYPE) -> bool: + """ + hold and activate a potion item of the specified type from the bot's inventory + """ + potion = get_potion_of_type(bot, potion_type) + return use_potion(bot, potion) # TODO: WAS AWAIT + +def name_for_item(item: Item) -> str: + """ + This will get the CustomName or DisplayName or Name for an item in that preference Order. + This is important for potions, where the name and displayName for the item are not unique. + """ + if item.customName: + try: + j = json.loads(item.customName) + return j['extra'][0]['text'] + except: + pass + return item.displayName or item.name + + +def equip_shield(bot: RGBot) -> bool: + """ + Equip shield from inventory into off-hand if possible + """ + shields = [item for item in bot.getAllInventoryItems() if ('Shield' in item.displayName or 'shield' in item.name)] + if shields: + shield = shields[0] + print(f'[Shield] Equipping: {shield.displayName}') + bot.mineflayer().equip(shield, 'off-hand') # TODO: WAS AWAIT + return True + return False + +def unequip_off_hand(bot: RGBot): + """ + Un-equip off-hand item like a shield + """ + bot.mineflayer().unequip('off-hand') # TODO: WAS AWAIT \ No newline at end of file