import collections import re import time import random from itertools import count from copy import copy from mosfet import utils from mosfet import path from mosfet.info import blocks from mosfet.info import mobs class World: def __init__(self, global_state): self.g = global_state def block_at(self, x, y, z): return self.g.chunks.get_block_at(x, y, z) def check_air_column(self, pos, distance): for i in range(distance): check = utils.padd(pos, (0, i, 0)) if self.block_at(*check) not in blocks.NON_SOLID_IDS: return False return True def find_blocks_3d(self, center, block_ids, distance=0, y_limit=0, thru_air=False): to_visit = collections.deque([(0, 0, 0)]) visited = set() while to_visit: cur = to_visit.pop() if cur in visited: continue if y_limit and abs(cur[1]) > y_limit: continue if distance and utils.hypot(*cur) > distance: continue check = utils.padd(center, cur) if not thru_air or self.block_at(*check) in blocks.NON_SOLID_IDS: for neighbor in utils.get_neighbors_3d(*cur): to_visit.appendleft(neighbor) visited.add(cur) if self.block_at(*check) in block_ids: yield check def find_blocks_indexed(self, center, block_ids, distance=0): index = [] for bid in block_ids: index.extend(self.g.chunks.index.get(bid, [])) result = [] for block in index: if self.block_at(*block) not in block_ids: continue if distance and utils.phyp(center, block) > distance: continue if block not in result: result.append(block) result.sort(key=lambda x: utils.phyp(center, x)) return result def find_blocks(self, center, distance, block_ids, limit=0): # search in a spiral from center to all blocks with ID result = [] for n in count(): offset = utils.spiral(n) check = utils.padd(center, offset) if self.block_at(*check) in block_ids: if utils.hypot(*offset) < distance: result.append(check) if limit and len(result) == limit: return result if offset[0] > distance: return result def find_trees(self, center, distance): found_trees = [] for log in self.find_blocks_3d(center, blocks.LOG_IDS, distance, 15): # crawl to the bottom log while self.block_at(*utils.padd(log, path.BLOCK_BELOW)) in blocks.LOG_IDS: log = utils.padd(log, path.BLOCK_BELOW) base = log if base in found_trees: continue # make sure we are on the ground if self.block_at(*utils.padd(base, path.BLOCK_BELOW)) in blocks.NON_SOLID_IDS: continue # crawl to the top log to count and check leaves log_count = 1 good_leaves = False while self.block_at(*utils.padd(log, path.BLOCK_ABOVE)) in blocks.LOG_IDS: log = utils.padd(log, path.BLOCK_ABOVE) log_count += 1 for offset in path.CHECK_DIRECTIONS: if self.block_at(*utils.padd(log, offset)) in blocks.LEAF_IDS: good_leaves = True # make sure it's a good tree if not good_leaves or log_count < 3: continue found_trees.append(base) yield base def find_tree_openings(self, tree): # returns coords in a cardinal direction where we can stand by tree maze_solver = path.Pathfinder(self.g) result = [] # TODO: make sure only non-solid and leaves between # make sure traversable too and non-avoid for distance in range(5): for direction in path.CHECK_DIRECTIONS: offset = utils.pmul(direction, distance+1) if maze_solver.check_traverse(tree, offset): result.append(utils.padd(tree, offset)) return result def path_to_place(self, start, place): maze_solver = path.Pathfinder(self.g) try: s = maze_solver.astar(start, place) return list(s) if s else None except path.AStarTimeout: return None 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 c = self.g.chunks above = utils.padd(place, path.BLOCK_ABOVE) below = utils.padd(place, path.BLOCK_BELOW) tmp = c.get_block_at(*place) tmp2 = c.get_block_at(*above) tmp3 = c.get_block_at(*below) c.set_block_at(*place, blocks.AIR) c.set_block_at(*above, blocks.AIR) c.set_block_at(*below, blocks.STONE) navpath = self.path_to_place(start, place) c.set_block_at(*place, tmp) c.set_block_at(*above, tmp2) c.set_block_at(*below, tmp3) return navpath def find_bed_areas(self, center, distance): """Yield nearby two-block bed footprints on solid, safe ground.""" # Only search loaded-neighborhood distance. The old 3-D flood fill # explored millions of air blocks and used overly broad 5x5 clearance. distance = min(distance, 32) headroom = path.BLOCK_ABOVE support = path.BLOCK_BELOW for offset in utils.search_2d(distance): if utils.hypot(offset[0], offset[2]) > distance: continue for dy in (0, 1, -1, 2, -2): area = (center[0] + offset[0], center[1] + dy, center[2] + offset[2]) if area == center: continue if self.block_at(*area) != blocks.AIR: continue if self.block_at(*utils.padd(area, headroom)) != blocks.AIR: continue support_block = self.block_at(*utils.padd(area, support)) if (support_block is None or support_block in blocks.NON_SOLID_IDS or support_block in blocks.AVOID_IDS): continue footprint_found = False for direction in path.CHECK_DIRECTIONS: head = utils.padd(area, direction) if (self.block_at(*head) != blocks.AIR or self.block_at(*utils.padd(head, headroom)) != blocks.AIR): continue head_support = self.block_at(*utils.padd(head, support)) if (head_support is None or head_support in blocks.NON_SOLID_IDS or head_support in blocks.AVOID_IDS): continue footprint_found = True break if not footprint_found: continue # Avoid placing a bed within range of nearby monsters. if self.find_monsters(area, 20): continue yield area def find_cache_areas(self, center, distance): """Yield nearby, supported air blocks suitable for placing a chest.""" center = tuple(center) for offset in utils.search_2d(distance): if utils.hypot(offset[0], offset[2]) > distance: continue for dy in (0, 1, -1, 2, -2): area = (center[0] + offset[0], center[1] + dy, center[2] + offset[2]) if area == center: continue if self.block_at(*area) != blocks.AIR: continue if self.block_at(*utils.padd(area, path.BLOCK_ABOVE)) != blocks.AIR: continue support = self.block_at(*utils.padd(area, path.BLOCK_BELOW)) if (support is None or support in blocks.NON_SOLID_IDS or support in blocks.AVOID_IDS): continue yield area def sand_adjacent_safe(self, sand): for direction in path.CHECK_DIRECTIONS: if self.block_at(*utils.padd(sand, direction)) in blocks.AVOID_IDS: return False return True def find_sand(self, center, distance, player): sand = [] sand.extend(self.find_blocks(center, distance, [blocks.SAND], 25)) safe_sand = [] for s in sand: # make sure it has solid below if self.block_at(*utils.padd(s, path.BLOCK_BELOW)) in blocks.NON_SOLID_IDS: continue # make sure it has solid two below - prevent hanging sand if self.block_at(*utils.padd(s, path.BLOCK_BELOW2)) in blocks.NON_SOLID_IDS: continue # and walkable air above if self.block_at(*utils.padd(s, path.BLOCK_ABOVE)) not in blocks.NON_SOLID_IDS: continue if not self.sand_adjacent_safe(s): continue safe_sand.append(s) safe_sand.sort(key=lambda x: utils.phyp(player, x)) return safe_sand def check_sand_slice(self, center): # checks if a 5x5x1 slice has sand in it for i in range(9): s = utils.padd(center, utils.spiral(i)) if self.block_at(*s) != blocks.SAND: continue # make sure it has solid below if self.block_at(*utils.padd(s, path.BLOCK_BELOW)) in blocks.NON_SOLID_IDS: continue # make sure it has solid two below - prevent hanging sand if self.block_at(*utils.padd(s, path.BLOCK_BELOW2)) in blocks.NON_SOLID_IDS: continue # and walkable air above if self.block_at(*utils.padd(s, path.BLOCK_ABOVE)) not in blocks.NON_SOLID_IDS: continue if not self.sand_adjacent_safe(s): continue return True return False def find_sand_slice(self, center, distance, y_limit=0, bad_slices=[], prev_layer=0): # returns the centre coord of the next 5x5x1 slice that still has # diggable sand in it. lower slices are only valid if there's an # adjacent slice farther at the same level. this should ensure an # upside down pyramid gets excavated so the edges are still climbable for v in count(prev_layer): peak = utils.padd(center, (0, 10-v, 0)) slices = [] layer = 0 for step in count(): offset = utils.spiral(step) layer = max(layer, *offset) offset = utils.pmul(offset, 3) check = utils.padd(peak, offset) check = utils.padd(check, (0, layer, 0)) if y_limit and check[1] - center[1] > y_limit: break if utils.phyp_king(center, check) > distance: break if self.check_sand_slice(check) and check not in bad_slices: slices.append(check) if len(slices): return v, slices[-1] elif v > 40: return None, None def find_bed_openings(self, area): # returns coords in a cardinal direction where we can stand by bed result = [] for direction in path.CHECK_DIRECTIONS: result.append(utils.padd(area, direction)) return result def check_bed_occupied(self, bed): """Return whether a loaded bed block currently has occupied=true.""" bid = self.g.chunks.get_block_at(*bed) occupied = blocks.PROPS.get(bid, {}).get('occupied') == 'true' print('Checking bed occupancy:', bed, '->', 'occupied' if occupied else 'free') return occupied def find_cache_openings(self, area): return self.find_bed_openings(area) def find_objects(self, object_ids): result = [] for eid, obj in copy(self.g.objects).items(): if obj.get('item_id', None) in object_ids: result.append(obj) return result def find_leaves(self, center, distance): for a in self.find_blocks_3d(center, blocks.LEAF_IDS, distance, 10): yield a def find_monsters(self, center, distance): # finds monsters within distance result = [] for eid, mob in copy(self.g.mobs).items(): if mob.type not in mobs.EVIL_IDS: continue pos = utils.pint((mob.x, mob.y, mob.z)) if utils.phyp(center, pos) > distance: continue result.append(mob) result.sort(key=lambda mob: utils.phyp(center, (mob.x, mob.y, mob.z))) return result def find_threats(self, center, distance): # finds monsters on the surface within distance monsters = self.find_monsters(center, distance) result = [] for mob in monsters: pos = utils.pint((mob.x, mob.y, mob.z)) # check distance number of blocks above, close enough? if not self.check_air_column(pos, distance): continue result.append(mob) result.sort(key=lambda mob: utils.phyp(center, (mob.x, mob.y, mob.z))) return result def find_villagers(self, center, distance): # finds villagers within distance result = [] for eid, mob in copy(self.g.mobs).items(): type_name = mobs.MOB_NAMES[mob.type] if type_name != 'villager' : continue pos = utils.pint((mob.x, mob.y, mob.z)) if utils.phyp(center, pos) > distance: continue result.append(mob) result.sort(key=lambda mob: utils.phyp(center, (mob.x, mob.y, mob.z))) return result def find_villager_openings(self, villager): # returns coords in a cardinal direction where we can stand by a villager maze_solver = path.Pathfinder(self.g) result = [] for distance in range(3): for direction in path.CHECK_DIRECTIONS: offset = utils.pmul(direction, distance+1) if not maze_solver.check_traverse(villager, offset): continue # check for line of sight for check in range(distance+1): offset2 = utils.pmul(direction, check+1) offset2 = utils.padd(offset2, path.BLOCK_ABOVE) check = utils.padd(villager, offset2) if self.block_at(*check) not in blocks.NON_SOLID_IDS: break else: # for result.append(utils.padd(villager, offset)) return result