diff --git a/mosfet/bot.py b/mosfet/bot.py index e917e09..41dd2bb 100644 --- a/mosfet/bot.py +++ b/mosfet/bot.py @@ -62,7 +62,8 @@ FARMING_PITCH = 45.0 def pitch_lock_for_job(job_state): state = getattr(job_state, 'state', None) if job_state else None - return (FARMING_PITCH if getattr(state, '__name__', None) == 'farm_crop' + return (FARMING_PITCH + if getattr(state, '__name__', None) in ('farm_crop', 'farm_wart') else None) diff --git a/mosfet/commands.py b/mosfet/commands.py index 2535546..ea34c85 100644 --- a/mosfet/commands.py +++ b/mosfet/commands.py @@ -73,7 +73,7 @@ class Commands: command in unsupported or (command == 'farm' and not data.lower().startswith('crop') and - data.lower().strip() != 'wood')): + data.lower().strip() not in ('wood', 'wart'))): reply = 'not migrated to Minecraft 26.3 yet' print('Reply:', reply) self._send_reply(reply, private, reply_target) diff --git a/mosfet/info/blocks.py b/mosfet/info/blocks.py index 8694ef3..fba64ea 100644 --- a/mosfet/info/blocks.py +++ b/mosfet/info/blocks.py @@ -78,7 +78,7 @@ SAPLINGS = sorted(name for name in MCD_BLOCKS if name.endswith('_sapling') or name.endswith('_propagule')) BEDS = sorted(name for name in MCD_BLOCKS if name.endswith('_bed')) INDEXED = ['chest', 'trapped_chest', 'emerald_block', 'barrel'] + BEDS + [ - 'wheat', 'potatoes', 'carrots', 'beetroots', + 'wheat', 'potatoes', 'carrots', 'beetroots', 'nether_wart', ] + LOGS LOG_IDS = get_set(LOGS) diff --git a/mosfet/job.py b/mosfet/job.py index 9b10de3..29a795e 100644 --- a/mosfet/job.py +++ b/mosfet/job.py @@ -270,11 +270,12 @@ class LegacyJobStates: class JobStates(LegacyJobStates): - """26.3 facade for migrated gather-wood and crop workflows.""" + """26.3 facade for migrated gathering and farming workflows.""" def __init__(self, global_state): self.g = global_state self.gather_wood_states = gather_wood.GatherWoodStates(global_state) self.gather_crop_states = gather_crop.GatherCropStates(global_state) + self.gather_wart_states = gather_wart.GatherWartStates(global_state) self.grab_supplies_states = grab_supplies.GrabSuppliesStates( global_state) self.clear_leaves_states = clear_leaves.ClearLeavesStates(global_state) @@ -308,6 +309,17 @@ class JobStates(LegacyJobStates): self.cache_items_states, ] + def farm_wart(self): + self.sleep_with_bed_states.silent = True + self.cache_items_states.silent = True + items.set_wanted({items.NETHERWART_ID}) + return [ + self.gather_wart_states, + self.sleep_with_bed_states, + self.eat_food_states, + self.cache_items_states, + ] + def farm_crop(self): self.cache_items_states.silent = True self.sleep_with_bed_states.silent = True diff --git a/mosfet/jobs/gather_wart.py b/mosfet/jobs/gather_wart.py index 4405ced..cd70c0c 100644 --- a/mosfet/jobs/gather_wart.py +++ b/mosfet/jobs/gather_wart.py @@ -1,52 +1,52 @@ -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 import utils from mosfet.info import blocks from mosfet.info import items -from mosfet.info import mcdata -from mosfet.info import mobs + class GatherWartStates: + BREAK_TIMEOUT = 5.0 + ITEM_TIMEOUT = 8.0 + PLACE_TIMEOUT = 3.0 + def idle(self): return None def init(self): self.state = self.find_new_wart + def _blacklist_wart(self): + if self.wart is not None and self.wart not in self.bad_warts: + self.bad_warts.append(self.wart) + def find_new_wart(self): print('Finding new wart...') - w = self.g.world - p = utils.pint(self.g.pos) - + world = self.g.world + center = utils.pint(self.g.pos) mature_wart = max(blocks.NETHERWART_IDS) - for wart in w.find_blocks_3d(p, [mature_wart], 50, 20, True): - if (wart not in self.bad_warts and - w.work_position_allowed(wart)): - break - else: # for + candidates = world.find_blocks_indexed( + center, {mature_wart}, 50) + candidates = world.filter_work_positions(candidates) + wart = next((position for position in candidates + if position not in self.bad_warts), None) + if wart is None: print('No good warts left, aborting.') self.state = self.cleanup return print('Found wart:', wart) self.wart = wart + self.type_id = mature_wart self.state = self.nav_to_wart def nav_to_wart(self): - w = self.g.world - p = utils.pint(self.g.pos) - - navpath = w.path_to_place(p, self.wart) + world = self.g.world + position = utils.pint(self.g.pos) + navpath = world.path_to_place(position, self.wart) if navpath: print('Going to wart', self.wart) @@ -55,8 +55,7 @@ class GatherWartStates: self.state = self.going_to_wart else: print('Cant get to it, blacklisting') - time.sleep(0.1) - self.bad_warts.append(wart) + self._blacklist_wart() self.state = self.find_new_wart def going_to_wart(self): @@ -65,63 +64,84 @@ class GatherWartStates: self.state = self.break_wart def break_wart(self): - self.g.game.break_block(self.wart) - self.wait_time = 0.5 - self.state = self.wait + current = self.g.world.block_at(*self.wart) + if current != self.type_id: + print('Wart changed before break; searching again') + self._blacklist_wart() + self.state = self.find_new_wart + return - def wait(self): - # wait for the item - if self.wait_time > 0: - self.wait_time -= utils.TICK + if self.g.game.break_block(self.wart): + self.break_deadline = time.monotonic() + self.BREAK_TIMEOUT + self.state = self.wait_break else: + print('Break request skipped; blacklisting', self.wart) + self._blacklist_wart() + self.state = self.find_new_wart + + def wait_break(self): + current = self.g.world.block_at(*self.wart) + if current != self.type_id: + print('Wart break confirmed at {}: {} -> {}'.format( + self.wart, self.type_id, current)) + self.item_deadline = time.monotonic() + self.ITEM_TIMEOUT + self.item_wait_logged = False self.state = self.select_wart + elif time.monotonic() > self.break_deadline: + print('Wart did not break; blacklisting', self.wart) + self._blacklist_wart() + self.state = self.find_new_wart def select_wart(self): - p = utils.pint(self.g.pos) - if self.g.game.select_item([items.NETHERWART_ID]): self.state = self.wait_select - self.wait_time = 0.5 - else: - print('Aborting planting, no wart') - self.state = self.cleanup + return + + if time.monotonic() > self.item_deadline: + print('No nether wart collected; skipping', self.wart) + self._blacklist_wart() + self.state = self.find_new_wart + elif not self.item_wait_logged: + print('Waiting for nether wart in inventory/hotbar') + self.item_wait_logged = True def wait_select(self): - # wait a bit to select - if self.wait_time > 0: - self.wait_time -= utils.TICK - else: + if self.g.get('holding') == items.NETHERWART_ID: self.state = self.place_wart def place_wart(self): - p = utils.pint(self.g.pos) - self.g.game.place_block(p, BlockFace.TOP) - print('Placed wart') + soul_sand = utils.padd(self.wart, path.BLOCK_BELOW) + self.g.game.place_block(soul_sand, BlockFace.TOP) + self.place_deadline = time.monotonic() + self.PLACE_TIMEOUT self.state = self.wait_place - self.wait_time = 0.5 def wait_place(self): - # wait a bit for chunk data to update - if self.wait_time > 0: - self.wait_time -= utils.TICK - else: + current = self.g.world.block_at(*self.wart) + if current in blocks.NETHERWART_IDS and current != self.type_id: + print('Nether wart replanted at', self.wart) self.state = self.cleanup + elif time.monotonic() > self.place_deadline: + print('Nether wart did not replant; blacklisting', self.wart) + self._blacklist_wart() + self.state = self.find_new_wart 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.wart = None + self.type_id = None self.bad_warts = [] - self.wait_time = 0 + self.break_deadline = None + self.item_deadline = None + self.place_deadline = None + self.item_wait_logged = False def run(self): self.state() diff --git a/tests/test_farming_pitch.py b/tests/test_farming_pitch.py index bed7be8..67858c5 100644 --- a/tests/test_farming_pitch.py +++ b/tests/test_farming_pitch.py @@ -6,15 +6,20 @@ from mosfet.bot import FARMING_PITCH, pitch_lock_for_job, update_pitch class TestFarmingPitch(unittest.TestCase): - def test_only_crop_farming_locks_pitch(self): + def test_crop_and_wart_farming_lock_pitch(self): def farm_crop(): pass + def farm_wart(): + pass + def sleep(): pass self.assertEqual( pitch_lock_for_job(Munch(state=farm_crop)), FARMING_PITCH) + self.assertEqual( + pitch_lock_for_job(Munch(state=farm_wart)), FARMING_PITCH) self.assertIsNone(pitch_lock_for_job(Munch(state=sleep))) self.assertIsNone(pitch_lock_for_job(None)) diff --git a/tests/test_gather_wart.py b/tests/test_gather_wart.py new file mode 100644 index 0000000..214c685 --- /dev/null +++ b/tests/test_gather_wart.py @@ -0,0 +1,142 @@ +import unittest +from munch import Munch + +from mosfet.commands import Commands +from mosfet.info import blocks, items +from mosfet.job import JobStates +from mosfet.jobs.gather_wart import GatherWartStates + + +class GatherWartTests(unittest.TestCase): + def test_nether_wart_states_are_indexed_for_patch_search(self): + self.assertTrue(blocks.NETHERWART_IDS.issubset(blocks.INDEXED_IDS)) + + def test_farm_wart_command_is_enabled_and_keeps_job_sequence(self): + old_wanted = items.WANTED_ITEMS + job = JobStates(Munch()) + + class FakeChat: + def __init__(self): + self.sent = [] + + def set_handler(self, _handler): + pass + + def send(self, message): + self.sent.append(message) + + chat = FakeChat() + g = Munch(name='mosfet1', chat=chat, job=job, + inv={0: Munch(item_id=next(iter(items.BED_IDS)))}) + try: + Commands(g).handle_chat(('CHAT', 'tester', + 'tester: 1farm wart')) + self.assertEqual(chat.sent, ['ok']) + self.assertIs(job.state.__func__, job.farm_wart.__func__) + self.assertIs(job.farm_wart()[0], job.gather_wart_states) + self.assertIn(items.NETHERWART_ID, items.WANTED_ITEMS) + finally: + items.WANTED_ITEMS = old_wanted + + def test_finds_only_indexed_mature_wart_allowed_by_work_signs(self): + position = (5, 64, 8) + mature = max(blocks.NETHERWART_IDS) + + class FakeWorld: + def __init__(self): + self.search = None + self.filter_input = None + + def find_blocks_indexed(self, center, block_ids, distance): + self.search = (center, block_ids, distance) + return [position] + + def filter_work_positions(self, positions): + self.filter_input = list(positions) + return list(positions) + + world = FakeWorld() + state = GatherWartStates(Munch(world=world, pos=(1.5, 64, 1.5))) + + state.find_new_wart() + + self.assertEqual(world.search, ((1, 64, 1), {mature}, 50)) + self.assertEqual(world.filter_input, [position]) + self.assertEqual(state.wart, position) + self.assertIs(state.state.__func__, state.nav_to_wart.__func__) + + def test_harvests_confirms_replant_on_soul_sand_below_wart(self): + position = (5, 64, 8) + mature = max(blocks.NETHERWART_IDS) + unripe = min(blocks.NETHERWART_IDS) + + class FakeWorld: + def __init__(self): + self.state_id = mature + + def block_at(self, *_position): + return self.state_id + + class FakeGame: + def __init__(self, global_state): + self.g = global_state + self.break_calls = [] + self.place_calls = [] + + def break_block(self, target): + self.break_calls.append(target) + return True + + def select_item(self, item_ids): + self.g.holding = items.NETHERWART_ID + return items.NETHERWART_ID in item_ids + + def place_block(self, target, face): + self.place_calls.append((target, face)) + + world = FakeWorld() + g = Munch(world=world, pos=position, holding=0, look_at=None) + game = FakeGame(g) + g.game = game + state = GatherWartStates(g) + state.wart = position + state.type_id = mature + + state.break_wart() + self.assertIs(state.state.__func__, state.wait_break.__func__) + self.assertEqual(game.break_calls, [position]) + + world.state_id = blocks.AIR + state.wait_break() + self.assertIs(state.state.__func__, state.select_wart.__func__) + state.select_wart() + state.wait_select() + self.assertIs(state.state.__func__, state.place_wart.__func__) + + state.place_wart() + support = (position[0], position[1] - 1, position[2]) + self.assertEqual(game.place_calls, [(support, 1)]) + world.state_id = unripe + state.wait_place() + self.assertIs(state.state.__func__, state.cleanup.__func__) + + def test_unreachable_wart_is_blacklisted_without_undefined_variable(self): + position = (5, 64, 8) + + class FakeWorld: + @staticmethod + def path_to_place(_start, _target): + return None + + state = GatherWartStates(Munch( + world=FakeWorld(), pos=(1, 64, 1), path=[])) + state.wart = position + + state.nav_to_wart() + + self.assertIn(position, state.bad_warts) + self.assertIs(state.state.__func__, state.find_new_wart.__func__) + + +if __name__ == '__main__': + unittest.main()