Select adjacent crops first

This commit is contained in:
Tanner
2026-11-04 15:21:59 +00:00
parent f45a1b6b82
commit 563aa53eec
3 changed files with 133 additions and 5 deletions
+1
View File
@@ -280,4 +280,5 @@ class JobStates(LegacyJobStates):
self.g.path = [] self.g.path = []
self.g.look_at = None self.g.look_at = None
self.gather_crop_states.state = self.gather_crop_states.idle self.gather_crop_states.state = self.gather_crop_states.idle
self.gather_crop_states.active_section_anchor = None
self.state = self.idle self.state = self.idle
+67 -5
View File
@@ -23,6 +23,61 @@ class GatherCropStates:
def init(self): def init(self):
self.state = self.find_new_crop 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): def find_new_crop(self):
print('Finding new crop...') print('Finding new crop...')
w = self.g.world w = self.g.world
@@ -36,17 +91,23 @@ class GatherCropStates:
] ]
candidates = w.find_blocks_indexed(p, mature_crops, 50) 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 is None:
if crop not in self.bad_crops:
break
else: # for
print('No good crops left; retrying after a short wait.') print('No good crops left; retrying after a short wait.')
self.retry_after = time.monotonic() + 15.0 self.retry_after = time.monotonic() + 15.0
self.state = self.wait_for_crops self.state = self.wait_for_crops
return return
print('Found crop:', crop) print('Found crop:', crop,
'(same-species section size={})'.format(section_size))
self.crop = crop self.crop = crop
self.type_id = w.block_at(*crop) self.type_id = w.block_at(*crop)
self.state = self.nav_to_crop self.state = self.nav_to_crop
@@ -178,6 +239,7 @@ class GatherCropStates:
self.target_seed = None self.target_seed = None
self.seed_deadline = None self.seed_deadline = None
self.seed_wait_logged = False self.seed_wait_logged = False
self.active_section_anchor = None
self.retry_after = 0.0 self.retry_after = 0.0
self.bad_crops = [] self.bad_crops = []
+65
View File
@@ -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()