Files
minecraft-bot/mosfet/jobs/gather_crop.py
T

284 lines
10 KiB
Python

import re
import time
import importlib
import random
from itertools import count
from math import floor
from minecraft.networking.types import BlockFace
from mosfet.protocol.managers import ChunkNotLoadedException
from mosfet import utils
from mosfet import path
from mosfet.info import blocks
from mosfet.info import items
from mosfet.info import mcdata
from mosfet.info import mobs
class GatherCropStates:
CROPS_PER_BATCH = 16
SECTION_MATURITY_WEIGHT = 2.0
def idle(self):
return None
def init(self):
self.state = self.find_new_crop
def _select_crop(self, candidates, crop_types, center):
"""Stay with a connected same-species patch, nearest patch first."""
adjacent_offsets = ((1, 0, 0), (-1, 0, 0),
(0, 0, 1), (0, 0, -1))
remaining = set(crop_types)
component_for = {}
component_sizes = {}
component_id = 0
while remaining:
seed = remaining.pop()
species = crop_types[seed]
stack = [seed]
component = [seed]
component_for[seed] = component_id
while stack:
x, y, z = stack.pop()
for dx, dy, dz in adjacent_offsets:
adjacent = (x + dx, y + dy, z + dz)
if (adjacent in remaining and
crop_types[adjacent] == species):
remaining.remove(adjacent)
component_for[adjacent] = component_id
component.append(adjacent)
stack.append(adjacent)
component_sizes[component_id] = len(component)
component_id += 1
eligible = [position for position in candidates
if position not in self.bad_crops and
position in component_for]
if not eligible:
self.active_section_maturity = (0, 0)
return None, 0
mature_by_component = {}
candidates_by_component = {}
for position in eligible:
component = component_for[position]
mature_by_component[component] = (
mature_by_component.get(component, 0) + 1)
candidates_by_component.setdefault(component, []).append(position)
active_component = component_for.get(self.active_section_anchor)
active_candidates = candidates_by_component.get(active_component, [])
if active_candidates:
section_candidates = active_candidates
selected_component = active_component
else:
# Favor a nearby section if maturity is similar, but allow a
# farther, substantially more-mature section to win. Once chosen,
# keep working that connected component until it has no mature
# crops left, matching the existing patch-stability behavior.
ranked_sections = []
for component, positions in candidates_by_component.items():
nearest_crop = min(positions, key=lambda position: (
utils.phyp(center, position), position))
distance = utils.phyp(center, nearest_crop)
maturity = (mature_by_component[component] /
component_sizes[component])
score = distance / (
1.0 + self.SECTION_MATURITY_WEIGHT * maturity)
ranked_sections.append((
score, distance, -mature_by_component[component],
nearest_crop, component,
))
selected_component = min(ranked_sections)[-1]
section_candidates = candidates_by_component[selected_component]
crop = min(section_candidates, key=lambda position: (
utils.phyp(center, position), position))
section_size = component_sizes[selected_component]
self.active_section_anchor = crop
self.active_section_maturity = (
mature_by_component[selected_component], section_size)
return crop, section_size
def find_new_crop(self):
print('Finding new crop...')
w = self.g.world
p = utils.pint(self.g.pos)
mature_crops = [
blocks.MATURE_WHEAT_ID,
blocks.MATURE_POTATO_ID,
blocks.MATURE_CARROT_ID,
blocks.MATURE_BEETROOT_ID,
]
candidates = w.filter_work_positions(
w.find_blocks_indexed(p, mature_crops, 50))
crop_ids = (blocks.WHEAT_IDS | blocks.POTATO_IDS |
blocks.CARROT_IDS | blocks.BEETROOT_IDS)
crop_positions = w.filter_work_positions(
w.find_blocks_indexed(p, crop_ids, 50))
crop_types = {
position: blocks.BLOCKS.get(w.block_at(*position))
for position in crop_positions
}
crop, section_size = self._select_crop(candidates, crop_types, p)
if crop is None:
print('No good crops left; running maintenance after {} harvests'.format(
self.completed_crops))
self.completed_crops = 0
self.state = self.cleanup
return
mature_count, total_count = self.active_section_maturity
maturity_percent = (100 * mature_count / total_count
if total_count else 0)
print('Found crop:', crop,
'(same-species section: {}/{} mature, {:.0f}%)'.format(
mature_count, total_count, maturity_percent))
self.crop = crop
self.type_id = w.block_at(*crop)
self.state = self.nav_to_crop
def nav_to_crop(self):
w = self.g.world
p = utils.pint(self.g.pos)
navpath = w.path_to_place(p, self.crop)
if navpath:
print('Going to crop', self.crop)
self.g.path = navpath
self.g.look_at = self.crop
self.state = self.going_to_crop
else:
print('Cant get to it, blacklisting')
time.sleep(0.1)
self.bad_crops.append(self.crop)
self.state = self.find_new_crop
def going_to_crop(self):
if utils.pint(self.g.pos) == self.crop:
print('At the crop')
self.state = self.break_crop
def break_crop(self):
print('Requesting crop break at {}: block={}, mode={}, held={}'.format(
self.crop, self.g.world.block_at(*self.crop),
self.g.get('game_mode'), self.g.get('holding')))
if self.g.game.break_block(self.crop):
print('Started breaking crop {} (state {})'.format(
self.crop, self.type_id))
self.break_deadline = time.time() + 5.0
self.state = self.wait_break
else:
print('Break request skipped; current block is',
self.g.world.block_at(*self.crop))
self.bad_crops.append(self.crop)
self.state = self.find_new_crop
def wait_break(self):
current_state = self.g.world.block_at(*self.crop)
if current_state != self.type_id:
print('Crop break confirmed at {}: {} -> {}'.format(
self.crop, self.type_id, current_state))
self.seed_deadline = time.monotonic() + 8.0
self.seed_wait_logged = False
self.state = self.select_seed
elif time.time() > self.break_deadline:
print('Crop break timed out at {}: expected state {}, server still has {}'.format(
self.crop, self.type_id, current_state))
print('Crop did not break; blacklisting', self.crop)
self.bad_crops.append(self.crop)
self.state = self.find_new_crop
def select_seed(self):
p = utils.pint(self.g.pos)
crop_seeds = {
blocks.MATURE_WHEAT_ID: items.WHEAT_SEEDS_ID,
blocks.MATURE_POTATO_ID: items.POTATO_ID,
blocks.MATURE_CARROT_ID: items.CARROT_ID,
blocks.MATURE_BEETROOT_ID: items.BEETROOT_SEEDS_ID,
}
self.target_seed = crop_seeds[self.type_id]
if self.g.game.select_item([self.target_seed]):
self.seed_deadline = None
self.state = self.wait_select
else:
if (self.seed_deadline is not None and
time.monotonic() > self.seed_deadline):
print('No seed collected for crop; skipping', self.crop)
self.bad_crops.append(self.crop)
self.seed_deadline = None
self.state = self.find_new_crop
elif not getattr(self, 'seed_wait_logged', False):
print('Waiting for crop seed {} in inventory/hotbar'.format(
self.target_seed))
self.seed_wait_logged = True
return
def wait_select(self):
if self.target_seed != self.g.holding:
return
else:
self.state = self.place_crop
def place_crop(self):
p = utils.padd(self.crop, path.BLOCK_BELOW)
self.place_deadline = time.time() + 3.0
self.g.game.place_block(p, BlockFace.TOP)
print('Placed crop')
self.state = self.wait_place
def wait_place(self):
w = self.g.world
current_state = w.block_at(*self.crop)
crop_name = blocks.BLOCKS.get(self.type_id)
replanted = (current_state not in (None, blocks.AIR) and
blocks.BLOCKS.get(current_state) == crop_name and
current_state != self.type_id)
if replanted:
self.completed_crops += 1
if self.completed_crops >= self.CROPS_PER_BATCH:
print('Completed {} crop harvests; running maintenance'.format(
self.completed_crops))
self.completed_crops = 0
self.state = self.cleanup
else:
self.state = self.find_new_crop
elif time.time() > getattr(self, 'place_deadline', 0):
print('Crop did not replant; blacklisting', self.crop)
self.bad_crops.append(self.crop)
self.state = self.find_new_crop
def cleanup(self):
self.g.look_at = None
self.state = self.done
def done(self):
# never gets ran, placeholder
return None
def __init__(self, global_state):
self.g = global_state
self.state = self.idle
self.crop = None
self.type_id = None
self.target_seed = None
self.seed_deadline = None
self.seed_wait_logged = False
self.active_section_anchor = None
self.active_section_maturity = (0, 0)
self.bad_crops = []
self.completed_crops = 0
def run(self):
self.state()