Apply sign work zones before candidate routing
This commit is contained in:
@@ -82,7 +82,11 @@ class LegacyJobStates:
|
||||
if m.state == m.idle:
|
||||
continue
|
||||
if m.state != m.done:
|
||||
self.active_machine = m
|
||||
try:
|
||||
m.run()
|
||||
finally:
|
||||
self.active_machine = None
|
||||
return
|
||||
# if we went through them all
|
||||
for m in machines:
|
||||
@@ -256,6 +260,7 @@ class LegacyJobStates:
|
||||
|
||||
def __init__(self, global_state):
|
||||
self.g = global_state
|
||||
self.active_machine = None
|
||||
|
||||
self.init_machines()
|
||||
self.state = self.idle
|
||||
@@ -281,6 +286,7 @@ class JobStates(LegacyJobStates):
|
||||
self.eat_food_states = eat_food.EatFoodStates(global_state)
|
||||
self.check_threats_states = check_threats.CheckThreatsStates(
|
||||
global_state)
|
||||
self.active_machine = None
|
||||
self.state = self.idle
|
||||
|
||||
def farm_wood(self):
|
||||
@@ -356,4 +362,5 @@ class JobStates(LegacyJobStates):
|
||||
self.check_threats_states.external_safety = False
|
||||
self.gather_crop_states.active_section_anchor = None
|
||||
self.gather_crop_states.completed_crops = 0
|
||||
self.active_machine = None
|
||||
self.state = self.idle
|
||||
|
||||
@@ -92,10 +92,12 @@ class GatherCropStates:
|
||||
blocks.MATURE_BEETROOT_ID,
|
||||
]
|
||||
|
||||
candidates = w.find_blocks_indexed(p, mature_crops, 50)
|
||||
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.find_blocks_indexed(p, crop_ids, 50)
|
||||
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
|
||||
|
||||
@@ -61,12 +61,13 @@ class GatherSandStates:
|
||||
head = utils.padd(p, path.BLOCK_ABOVE)
|
||||
|
||||
for sand in w.find_sand(self.slice, 2, p):
|
||||
if sand not in self.bad_sand:
|
||||
if (sand not in self.bad_sand and
|
||||
w.work_position_allowed(sand)):
|
||||
print('Found sand:', sand)
|
||||
break
|
||||
else: # for
|
||||
print('No good sands left, aborting.')
|
||||
self.state = self.cleanup
|
||||
print('No in-bounds sand left in this slice; trying another.')
|
||||
self.state = self.find_new_slice
|
||||
return
|
||||
|
||||
self.sand = sand
|
||||
|
||||
@@ -30,7 +30,8 @@ class GatherWartStates:
|
||||
|
||||
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:
|
||||
if (wart not in self.bad_warts and
|
||||
w.work_position_allowed(wart)):
|
||||
break
|
||||
else: # for
|
||||
print('No good warts left, aborting.')
|
||||
|
||||
@@ -30,10 +30,11 @@ class GatherWoodStates:
|
||||
w = self.g.world
|
||||
p = utils.pint(self.g.pos)
|
||||
|
||||
for tree in w.find_trees(p, 100):
|
||||
print('Found tree:', tree)
|
||||
trees = w.filter_work_positions(w.find_trees(p, 100))
|
||||
for tree in trees:
|
||||
if tree in self.bad_trees:
|
||||
continue
|
||||
print('Found tree:', tree)
|
||||
tree_type = blocks.BLOCKS[w.block_at(*tree)].replace('_log', '')
|
||||
if items.get_sapling_id(tree_type) is None:
|
||||
print('No plantable sapling for', tree_type, '- skipping')
|
||||
@@ -53,7 +54,10 @@ class GatherWoodStates:
|
||||
|
||||
def find_openings(self):
|
||||
w = self.g.world
|
||||
self.openings = w.find_tree_openings(self.tree)
|
||||
self.openings = [
|
||||
opening for opening in w.find_tree_openings(self.tree)
|
||||
if w.work_position_allowed(opening)
|
||||
]
|
||||
self.state = self.choose_opening
|
||||
|
||||
def choose_opening(self):
|
||||
|
||||
@@ -44,6 +44,8 @@ class GrabSaplingStates:
|
||||
|
||||
if s.entity_id in self.eid_blacklist:
|
||||
continue
|
||||
if not w.work_position_allowed(s_pos):
|
||||
continue
|
||||
|
||||
# skip if the sapling is floating
|
||||
if self.g.chunks.get_block_at(*check) in blocks.LEAF_IDS | {0}:
|
||||
|
||||
@@ -80,6 +80,8 @@ class PlantTreeStates:
|
||||
p = utils.pint(self.g.pos)
|
||||
|
||||
for opening in w.find_tree_openings(p)[::-1]:
|
||||
if not w.work_position_allowed(opening):
|
||||
continue
|
||||
print('trying sapling opening', opening)
|
||||
navpath = w.path_to_place(p, opening)
|
||||
if navpath:
|
||||
|
||||
+121
-2
@@ -76,6 +76,113 @@ class World:
|
||||
result.sort(key=lambda sign: utils.phyp(center, sign['position']))
|
||||
return result
|
||||
|
||||
def sign_policies(self, bot_name, distance=64):
|
||||
"""Parse sign policies addressed to this bot or its numbered group."""
|
||||
bot_name = str(bot_name).casefold()
|
||||
group_name = (bot_name[:-1] if len(bot_name) > 1 and
|
||||
bot_name[-1] in '123' else None)
|
||||
addresses = {bot_name}
|
||||
if group_name:
|
||||
addresses.add(group_name)
|
||||
|
||||
center = utils.pint(self.g.pos)
|
||||
policies = []
|
||||
active_sign_position = None
|
||||
for sign in self.find_signs(center, distance):
|
||||
lines = [line.strip() for line in sign['lines'] if line.strip()]
|
||||
if not lines:
|
||||
continue
|
||||
first = lines[0].split(maxsplit=1)
|
||||
address = first[0].rstrip(':,').casefold()
|
||||
if address not in addresses:
|
||||
continue
|
||||
directives = ([first[1]] if len(first) > 1 else []) + lines[1:]
|
||||
text = ' '.join(directives).casefold()
|
||||
sign_policies = []
|
||||
for match in re.finditer(
|
||||
r'\bstay\s+within\s*(\d+)(?:\s+(squares?|circle))?\b',
|
||||
text):
|
||||
shape = match.group(2) or 'circle'
|
||||
if shape == 'squares':
|
||||
shape = 'square'
|
||||
sign_policies.append({
|
||||
'position': sign['position'],
|
||||
'distance': int(match.group(1)),
|
||||
'shape': shape,
|
||||
'side': sign['side'],
|
||||
})
|
||||
if not sign_policies:
|
||||
continue
|
||||
if active_sign_position is None:
|
||||
# The nearest addressed policy sign overrides farther signs.
|
||||
active_sign_position = sign['position']
|
||||
if sign['position'] == active_sign_position:
|
||||
policies.extend(sign_policies)
|
||||
return policies
|
||||
|
||||
def _active_work_policies(self):
|
||||
job = self.g.get('job')
|
||||
machine = getattr(job, 'active_machine', None) if job else None
|
||||
if machine is None:
|
||||
return []
|
||||
support_machines = (
|
||||
'grab_supplies_states', 'sleep_with_bed_states',
|
||||
'eat_food_states', 'cache_items_states',
|
||||
'check_threats_states',
|
||||
)
|
||||
if any(machine is getattr(job, name, None)
|
||||
for name in support_machines):
|
||||
return []
|
||||
return self.sign_policies(self.g.get('name', ''))
|
||||
|
||||
@staticmethod
|
||||
def _sign_policy_distance(position, policy):
|
||||
# Work zones are horizontal; crop height or terrain elevation does not
|
||||
# consume the sign's radius.
|
||||
dx = abs(position[0] - policy['position'][0])
|
||||
dz = abs(position[2] - policy['position'][2])
|
||||
if policy['shape'] == 'square':
|
||||
return max(dx, dz)
|
||||
return utils.hypot(dx, dz)
|
||||
|
||||
@classmethod
|
||||
def _inside_sign_policy(cls, position, policy):
|
||||
return cls._sign_policy_distance(position, policy) <= policy['distance']
|
||||
|
||||
def work_position_allowed(self, position):
|
||||
"""Whether a work target satisfies all currently active sign rules."""
|
||||
return all(self._inside_sign_policy(position, policy)
|
||||
for policy in self._active_work_policies())
|
||||
|
||||
def filter_work_positions(self, positions):
|
||||
"""Filter a candidate set once against the active sign policies."""
|
||||
policies = self._active_work_policies()
|
||||
positions = list(positions)
|
||||
if not policies:
|
||||
return positions
|
||||
return [position for position in positions
|
||||
if all(self._inside_sign_policy(position, policy)
|
||||
for policy in policies)]
|
||||
|
||||
@classmethod
|
||||
def _path_allowed_by_sign(cls, points, policy):
|
||||
distances = [cls._sign_policy_distance(point, policy)
|
||||
for point in points]
|
||||
radius = policy['distance']
|
||||
if distances[0] <= radius:
|
||||
return all(distance <= radius for distance in distances)
|
||||
|
||||
# If the bot starts just outside a newly read boundary, let it return
|
||||
# without allowing it to travel farther away from that sign.
|
||||
start_distance = distances[0]
|
||||
entered = False
|
||||
for distance in distances[1:]:
|
||||
if distance > start_distance or (entered and distance > radius):
|
||||
return False
|
||||
if distance <= radius:
|
||||
entered = True
|
||||
return entered
|
||||
|
||||
def check_air_column(self, pos, distance):
|
||||
for i in range(distance):
|
||||
check = utils.padd(pos, (0, i, 0))
|
||||
@@ -194,11 +301,23 @@ class World:
|
||||
maze_solver = path.Pathfinder(self.g)
|
||||
|
||||
try:
|
||||
s = maze_solver.astar(start, place)
|
||||
return list(s) if s else None
|
||||
solution = maze_solver.astar(start, place)
|
||||
if not solution:
|
||||
return None
|
||||
navpath = list(solution)
|
||||
except path.AStarTimeout:
|
||||
return None
|
||||
|
||||
policies = self._active_work_policies()
|
||||
points = [tuple(start)] + [tuple(point) for point in navpath]
|
||||
for policy in policies:
|
||||
if not self._path_allowed_by_sign(points, policy):
|
||||
print('[sign policy] rejected work path outside {}-block {} '
|
||||
'around {}'.format(policy['distance'], policy['shape'],
|
||||
policy['position']), flush=True)
|
||||
return None
|
||||
return navpath
|
||||
|
||||
def path_to_place_faked(self, start, place):
|
||||
# same as above, but adds a fake block below and air before pathfinding
|
||||
# so that the pathfinder can actually make it to the block
|
||||
|
||||
@@ -36,6 +36,14 @@ class GatherWoodPortTests(unittest.TestCase):
|
||||
def find_trees(self, _position, _distance):
|
||||
return [(0, 64, 0), (3, 64, 0)]
|
||||
|
||||
@staticmethod
|
||||
def work_position_allowed(_position):
|
||||
return True
|
||||
|
||||
def filter_work_positions(self, positions):
|
||||
return [position for position in positions
|
||||
if self.work_position_allowed(position)]
|
||||
|
||||
def block_at(self, x, y, z):
|
||||
return (blocks.MCD_BLOCKS['crimson_stem']['minStateId']
|
||||
if x == 0 else
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import unittest
|
||||
from munch import Munch
|
||||
|
||||
from mosfet.info import blocks
|
||||
from mosfet.jobs.gather_crop import GatherCropStates
|
||||
from mosfet.jobs.gather_wood import GatherWoodStates
|
||||
|
||||
|
||||
class SignWorkTargetTests(unittest.TestCase):
|
||||
def test_crop_search_filters_out_of_bounds_candidates_before_navigation(self):
|
||||
outside = (10, 64, 0)
|
||||
mature_wheat = blocks.MATURE_WHEAT_ID
|
||||
crop_state = blocks.MCD_BLOCKS['wheat']['minStateId']
|
||||
|
||||
class FakeWorld:
|
||||
def find_blocks_indexed(self, _center, _block_ids, _distance):
|
||||
return [outside]
|
||||
|
||||
@staticmethod
|
||||
def work_position_allowed(position):
|
||||
return position != outside
|
||||
|
||||
def filter_work_positions(self, positions):
|
||||
return [position for position in positions
|
||||
if self.work_position_allowed(position)]
|
||||
|
||||
@staticmethod
|
||||
def block_at(*_position):
|
||||
return crop_state
|
||||
|
||||
state = GatherCropStates(Munch(world=FakeWorld(), pos=(0, 64, 0)))
|
||||
state.find_new_crop()
|
||||
|
||||
self.assertIs(state.state.__func__, state.wait_for_crops.__func__)
|
||||
self.assertEqual(state.bad_crops, [])
|
||||
|
||||
def test_tree_search_skips_out_of_bounds_tree_without_blacklisting_it(self):
|
||||
outside = (10, 64, 0)
|
||||
inside = (2, 64, 0)
|
||||
oak_log = blocks.MCD_BLOCKS['oak_log']['minStateId']
|
||||
|
||||
class FakeWorld:
|
||||
@staticmethod
|
||||
def find_trees(_center, _distance):
|
||||
return [outside, inside]
|
||||
|
||||
@staticmethod
|
||||
def work_position_allowed(position):
|
||||
return position != outside
|
||||
|
||||
def filter_work_positions(self, positions):
|
||||
return [position for position in positions
|
||||
if self.work_position_allowed(position)]
|
||||
|
||||
@staticmethod
|
||||
def block_at(*_position):
|
||||
return oak_log
|
||||
|
||||
state = GatherWoodStates(Munch(world=FakeWorld(), pos=(0, 64, 0)))
|
||||
state.find_new_tree()
|
||||
|
||||
self.assertEqual(state.tree, inside)
|
||||
self.assertEqual(state.type, 'oak')
|
||||
self.assertEqual(state.bad_trees, [])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from munch import Munch
|
||||
|
||||
from mosfet.commands import Commands
|
||||
@@ -44,6 +45,88 @@ class SignReadingTests(unittest.TestCase):
|
||||
signs = World(Munch(chunks=chunks)).find_signs((4, 65, -2), 1)
|
||||
self.assertEqual(signs[0]['lines'], ('Farm', '', '', ''))
|
||||
|
||||
def test_group_and_individual_sign_addresses(self):
|
||||
group_world = World(Munch(chunks=Munch(block_entities={
|
||||
(0, 64, 0): {'data': {'front_text': {'messages': [
|
||||
'mosfet', 'stay within25', 'square', '',
|
||||
]}}},
|
||||
}), pos=(0, 64, 0)))
|
||||
for bot_name in ('mosfet1', 'mosfet2', 'mosfet3'):
|
||||
self.assertEqual([p['shape'] for p in
|
||||
group_world.sign_policies(bot_name)], ['square'])
|
||||
self.assertEqual(group_world.sign_policies('mosfet4'), [])
|
||||
|
||||
individual_world = World(Munch(chunks=Munch(block_entities={
|
||||
(1, 64, 0): {'data': {'front_text': {'messages': [
|
||||
'mosfet2', 'stay within 10 circle', '', '',
|
||||
]}}},
|
||||
}), pos=(1, 64, 0)))
|
||||
self.assertEqual([p['shape'] for p in
|
||||
individual_world.sign_policies('mosfet2')], ['circle'])
|
||||
self.assertEqual(individual_world.sign_policies('mosfet1'), [])
|
||||
|
||||
def test_nearest_sign_policy_overrides_farther_matching_sign(self):
|
||||
chunks = Munch(block_entities={
|
||||
(-138, 71, -268): {'data': {'front_text': {'messages': [
|
||||
'mosfet1', 'stay within 25 square', '', '',
|
||||
]}}},
|
||||
(-158, 71, -248): {'data': {'front_text': {'messages': [
|
||||
'mosfet1', 'stay within 6 squares', '', '',
|
||||
]}}},
|
||||
})
|
||||
world = World(Munch(chunks=chunks, pos=(-158, 71, -248)))
|
||||
self.assertEqual(world.sign_policies('mosfet1'), [{
|
||||
'position': (-158, 71, -248), 'distance': 6,
|
||||
'shape': 'square', 'side': 'front',
|
||||
}])
|
||||
|
||||
def test_work_paths_obey_sign_boundary_but_cache_paths_do_not(self):
|
||||
sign_data = {'front_text': {'messages': [
|
||||
'mosfet1', 'stay within 25 square', '', '',
|
||||
]}}
|
||||
chunks = Munch(block_entities={(0, 64, 0): {'data': sign_data}})
|
||||
work_machine = object()
|
||||
cache_machine = object()
|
||||
job = Munch(active_machine=work_machine,
|
||||
cache_items_states=cache_machine)
|
||||
world = World(Munch(chunks=chunks, job=job, name='mosfet1',
|
||||
pos=(0, 64, 0)))
|
||||
|
||||
class FixedPathfinder:
|
||||
@staticmethod
|
||||
def astar(_start, _place):
|
||||
return [(0, 64, 0), (20, 64, 0), (30, 64, 0)]
|
||||
|
||||
with patch('mosfet.world.path.Pathfinder', return_value=FixedPathfinder()):
|
||||
self.assertIsNone(world.path_to_place((0, 64, 0), (30, 64, 0)))
|
||||
job.active_machine = cache_machine
|
||||
self.assertEqual(world.path_to_place(
|
||||
(0, 64, 0), (30, 64, 0)),
|
||||
[(0, 64, 0), (20, 64, 0), (30, 64, 0)])
|
||||
|
||||
policy = {'position': (0, 64, 0), 'distance': 25, 'shape': 'square'}
|
||||
self.assertTrue(World._path_allowed_by_sign(
|
||||
[(30, 64, 0), (27, 64, 0), (24, 64, 0)], policy))
|
||||
self.assertFalse(World._path_allowed_by_sign(
|
||||
[(30, 64, 0), (32, 64, 0), (24, 64, 0)], policy))
|
||||
|
||||
six_square = {'position': (-158, 71, -248),
|
||||
'distance': 6, 'shape': 'square'}
|
||||
six_circle = dict(six_square, shape='circle')
|
||||
crop = (-162, 68, -247)
|
||||
self.assertEqual(World._sign_policy_distance(crop, six_square), 4)
|
||||
self.assertAlmostEqual(World._sign_policy_distance(crop, six_circle),
|
||||
(17 ** 0.5))
|
||||
|
||||
four_square = {'position': (0, 64, 0),
|
||||
'distance': 4, 'shape': 'square'}
|
||||
self.assertEqual(World._sign_policy_distance((4, 70, 4), four_square), 4)
|
||||
self.assertTrue(World._inside_sign_policy((4, 70, 4), four_square))
|
||||
self.assertFalse(World._inside_sign_policy((5, 64, 4), four_square))
|
||||
same_xz_far_y = (-158, -20, -248)
|
||||
self.assertEqual(
|
||||
World._sign_policy_distance(same_xz_far_y, six_square), 0)
|
||||
|
||||
def test_signs_command_replies_with_nearby_text(self):
|
||||
class FakeChat:
|
||||
def __init__(self):
|
||||
|
||||
Reference in New Issue
Block a user