import collections import re import time import random from math import hypot 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 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): print('finding', block_ids) index = [] for bid in block_ids: index.extend(self.g.chunks.index.get(bid, [])) print('index', index) 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 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): bed_clearance = 9 # 5x5 area clear_distance = 2 for a in self.find_blocks_3d(center, [0], distance, 50): # check for air around the area if len(self.find_blocks(a, clear_distance, [0], bed_clearance)) < bed_clearance: continue # check for ground around the area if len(self.find_blocks(utils.padd(a, path.BLOCK_BELOW), clear_distance, blocks.NON_SOLID_IDS, bed_clearance)): continue # check for air above the area if len(self.find_blocks(utils.padd(a, path.BLOCK_ABOVE), clear_distance, [0], bed_clearance)) < bed_clearance: continue # ensure there's no monsters within 20 blocks # can't sleep if they are within 10, good to have a buffer if self.find_monsters(a, 20): continue yield a def find_cache_areas(self, center, distance): return self.find_bed_areas(center, distance) 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): # returns true if the bed is occupied by a player print('Checking bed occupancy:', bed) for player in self.g.players.values(): ppos = utils.pint((player.x, player.y, player.z)) if utils.phyp(bed, ppos) <= 1 and player.y - int(player.y) == 0.6875: print('Bed is occupied by:', player, self.g.player_names[player.player_uuid]) return True return False 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) 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) 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) 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