diff --git a/mosfet/job.py b/mosfet/job.py index e3f9dab..dde2870 100644 --- a/mosfet/job.py +++ b/mosfet/job.py @@ -280,4 +280,5 @@ class JobStates(LegacyJobStates): self.g.path = [] self.g.look_at = None self.gather_crop_states.state = self.gather_crop_states.idle + self.gather_crop_states.active_section_anchor = None self.state = self.idle diff --git a/mosfet/jobs/gather_crop.py b/mosfet/jobs/gather_crop.py index 1c61a4c..3f7d220 100644 --- a/mosfet/jobs/gather_crop.py +++ b/mosfet/jobs/gather_crop.py @@ -23,6 +23,61 @@ class GatherCropStates: 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: + return None, 0 + + active_component = component_for.get(self.active_section_anchor) + active_candidates = [position for position in eligible + if component_for[position] == active_component] + if active_candidates: + section_candidates = active_candidates + else: + # Begin with the nearest available patch. Once selected, keep its + # connected component active until no mature crops remain there. + first_crop = min(eligible, key=lambda position: ( + utils.phyp(center, position), position)) + selected_component = component_for[first_crop] + section_candidates = [ + position for position in eligible + if component_for[position] == selected_component + ] + + crop = min(section_candidates, key=lambda position: ( + utils.phyp(center, position), position)) + self.active_section_anchor = crop + return crop, component_sizes[component_for[crop]] + def find_new_crop(self): print('Finding new crop...') w = self.g.world @@ -36,17 +91,23 @@ class GatherCropStates: ] candidates = 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.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) - for crop in candidates: - if crop not in self.bad_crops: - break - else: # for + if crop is None: print('No good crops left; retrying after a short wait.') self.retry_after = time.monotonic() + 15.0 self.state = self.wait_for_crops return - print('Found crop:', crop) + print('Found crop:', crop, + '(same-species section size={})'.format(section_size)) self.crop = crop self.type_id = w.block_at(*crop) self.state = self.nav_to_crop @@ -178,6 +239,7 @@ class GatherCropStates: self.target_seed = None self.seed_deadline = None self.seed_wait_logged = False + self.active_section_anchor = None self.retry_after = 0.0 self.bad_crops = [] diff --git a/tests/test_crop_priority.py b/tests/test_crop_priority.py new file mode 100644 index 0000000..9d53b40 --- /dev/null +++ b/tests/test_crop_priority.py @@ -0,0 +1,65 @@ +import unittest + +from munch import Munch + +from mosfet.jobs.gather_crop import GatherCropStates + + +class TestCropSectionPriority(unittest.TestCase): + def setUp(self): + self.job = GatherCropStates(Munch()) + self.job.bad_crops = [] + + def test_stays_in_active_patch_before_switching(self): + wheat_a = (0, 64, 0) + wheat_b = (1, 64, 0) + carrot_a = (10, 64, 0) + crop_types = { + wheat_a: 'wheat', + wheat_b: 'wheat', + carrot_a: 'carrots', + (10, 64, 1): 'carrots', + (10, 64, -1): 'carrots', + (11, 64, 0): 'carrots', + } + + first, first_size = self.job._select_crop( + [wheat_a, carrot_a], crop_types, (0, 64, 0)) + self.assertEqual(first, wheat_a) + self.assertEqual(first_size, 2) + self.job.init() + self.assertEqual(self.job.active_section_anchor, wheat_a) + + # The carrot patch is now closer, but the bot should finish the wheat + # patch it already selected first. + next_crop, next_size = self.job._select_crop( + [wheat_b, carrot_a], crop_types, (10, 64, 0)) + self.assertEqual(next_crop, wheat_b) + self.assertEqual(next_size, 2) + + # Once the active patch has no mature candidates, move to the next one. + next_patch, next_patch_size = self.job._select_crop( + [carrot_a], crop_types, (10, 64, 0)) + self.assertEqual(next_patch, carrot_a) + self.assertEqual(next_patch_size, 4) + + def test_unripe_same_species_blocks_keep_patch_connected(self): + mature = (0, 64, 0) + replanted = (1, 64, 0) + isolated = (4, 64, 0) + crop_types = { + mature: 'wheat', + replanted: 'wheat', + isolated: 'wheat', + } + + crop, size = self.job._select_crop([mature, isolated], crop_types, + (0, 64, 0)) + + self.assertEqual(crop, mature) + self.assertEqual(size, 2) + self.assertEqual(self.job.active_section_anchor, mature) + + +if __name__ == '__main__': + unittest.main()