diff --git a/mosfet/jobs/gather_crop.py b/mosfet/jobs/gather_crop.py index 43d651c..8813540 100644 --- a/mosfet/jobs/gather_crop.py +++ b/mosfet/jobs/gather_crop.py @@ -18,6 +18,7 @@ from mosfet.info import mobs class GatherCropStates: CROPS_PER_BATCH = 16 + SECTION_MATURITY_WEIGHT = 2.0 def idle(self): return None @@ -57,28 +58,50 @@ class GatherCropStates: 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 = [position for position in eligible - if component_for[position] == active_component] + active_candidates = candidates_by_component.get(active_component, []) if active_candidates: section_candidates = active_candidates + selected_component = active_component 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 - ] + # 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 - return crop, component_sizes[component_for[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...') @@ -111,8 +134,12 @@ class GatherCropStates: 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 size={})'.format(section_size)) + '(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 @@ -248,6 +275,7 @@ class GatherCropStates: 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 diff --git a/tests/test_crop_priority.py b/tests/test_crop_priority.py index 9d53b40..5fe68e7 100644 --- a/tests/test_crop_priority.py +++ b/tests/test_crop_priority.py @@ -43,6 +43,26 @@ class TestCropSectionPriority(unittest.TestCase): self.assertEqual(next_patch, carrot_a) self.assertEqual(next_patch_size, 4) + def test_farther_section_wins_when_much_more_mature(self): + near_patch = { + (3, 64, 0), (2, 64, 0), (1, 64, 0), (0, 64, 0), + (3, 64, 1), (2, 64, 1), (1, 64, 1), (0, 64, 1), + (3, 64, 2), (2, 64, 2), + } + near_mature = (3, 64, 0) + far_patch = { + (4, 64, 0), (5, 64, 0), (4, 64, 1), (5, 64, 1), + } + crop_types = {position: 'wheat' for position in near_patch} + crop_types.update({position: 'carrots' for position in far_patch}) + + crop, size = self.job._select_crop( + [near_mature, *far_patch], crop_types, (0, 64, 0)) + + self.assertEqual(crop, (4, 64, 0)) + self.assertEqual(size, len(far_patch)) + self.assertEqual(self.job.active_section_maturity, (4, 4)) + def test_unripe_same_species_blocks_keep_patch_connected(self): mature = (0, 64, 0) replanted = (1, 64, 0)