diff --git a/data.py b/data.py index 7b3fa4f..b40cd35 100644 --- a/data.py +++ b/data.py @@ -319,13 +319,31 @@ 'min_grad': 5, 'ground_height': 10, 'chunk_size': 16, - 'max_biome_size': 50, - 'biome_tree_weights': [0]*2 + [.05]*2 + [.2], - 'tall_grass_rate': .25, + 'min_biome': 16, + 'max_biome': 64, + 'biomes': { + 'plains': { + 'chance': .3, + 'trees': 0, + 'grass': .15 + }, + 'normal': { + 'chance': .3, + 'trees': .05, + 'grass': .1 + }, + 'forest': { + 'chance': .3, + 'trees': .2, + 'grass': 0 + } + }, + # TODO: Densities need tuning. 'ores': { 'coal': { 'char': 'x', 'vain_size': 4, + 'vain_density': .4, 'chance': 0.05, 'upper': 30, 'lower': 1 @@ -333,6 +351,7 @@ 'iron': { 'char': '+', 'vain_size': 3, + 'vain_density': .3, 'chance': 0.03, 'upper': 15, 'lower': 1 @@ -340,6 +359,7 @@ 'redstone': { 'char': ':', 'vain_size': 4, + 'vain_density': .6, 'chance': 0.03, 'upper': 7, 'lower': 1 @@ -347,13 +367,15 @@ 'gold': { 'char': '"', 'vain_size': 2, + 'vain_density': .3, 'chance': 0.02, 'upper': 10, 'lower': 1 }, 'diamond': { 'char': 'o', - 'vain_size': 1, + 'vain_size': 2, + 'vain_density': .5, 'chance': 0.01, 'upper': 5, 'lower': 1 @@ -361,12 +383,14 @@ 'emerald': { 'char': '.', 'vain_size': 1, + 'vain_density': 1, 'chance': 0.002, 'upper': 7, 'lower': 1 } }, - 'trees': ( + 'trees': ( # TODO: Preprocessing should be done on these, to give the data + # the terrain gen needs. ((0, 1, 1), (1, 1, 0), (0, 1, 1)), diff --git a/player.py b/player.py index 6eff8e3..6cc2fdd 100644 --- a/player.py +++ b/player.py @@ -24,16 +24,14 @@ def get_pos_delta(char, map_, x, y, jump): dy = 0 dx = 0 - is_solid = lambda block: terrain.is_solid(block) - # Calculate change in x pos for left and right movement - for test_char, dir_, func in (('a', -1, left_slice), ('d', 1, right_slice)): + for test_char, dir_, next_slice in (('a', -1, left_slice), ('d', 1, right_slice)): if ( char in test_char - and not is_solid( func[head_y] )): + and not terrain.is_solid( next_slice[head_y] )): - if is_solid( func[feet_y] ): - if ( not is_solid( func[above_y] ) - and not is_solid( player_slice[above_y] )): + if terrain.is_solid( next_slice[feet_y] ): + if ( not terrain.is_solid( next_slice[above_y] ) + and not terrain.is_solid( player_slice[above_y] )): dy = -1 dx = dir_ @@ -42,8 +40,8 @@ def get_pos_delta(char, map_, x, y, jump): # Jumps if up pressed, block below, no block above if ( char in 'w' and y > 1 - and not is_solid( player_slice[above_y] ) - and ( is_solid( player_slice[below_y] ) + and not terrain.is_solid( player_slice[above_y] ) + and ( terrain.is_solid( player_slice[below_y] ) or player_slice[feet_y] == '=' )): dy = -1 diff --git a/render.py b/render.py index 13b8ab8..951673d 100644 --- a/render.py +++ b/render.py @@ -2,7 +2,7 @@ import terrain from colors import * -from console import CLS, CLS_END, CLS_END_LN, REDRAW, POS_STR, supported_chars +from console import DEBUG, CLS, CLS_END, CLS_END_LN, REDRAW, POS_STR, supported_chars, log import data from data import world_gen @@ -35,7 +35,7 @@ def render_map(map_, objects, sun, lights, time, last_frame): # [2, '## ']] # Separates the pos and data - map_ = tuple(zip(*map_))[1] + world_positions, map_ = tuple(zip(*map_)) # Orientates the data map_ = zip(*map_) @@ -49,6 +49,10 @@ def render_map(map_, objects, sun, lights, time, last_frame): for x, pixel in enumerate(row): pixel_out = calc_pixel(x, y, pixel, objects, sun, lights, time) + + if DEBUG and y == 1 and world_positions[x] % world_gen['chunk_size'] == 0: + pixel_out = colorStr('*', bg=RED, fg=YELLOW) + this_frame[-1].append(pixel_out) try: diff --git a/saves.py b/saves.py index e394507..5ab3e45 100644 --- a/saves.py +++ b/saves.py @@ -17,14 +17,14 @@ } default_player = { - 'player_x': 0, + 'player_x': int(os.getenv('PYCRAFT_START_X') or 0), 'player_y': 1, 'inv': [] } SAVES_DIR = 'saves' CHUNK_EXT = '.chunk' -SLICE_SEP = '' +CHUNK_SIZE = world_gen['chunk_size'] * (world_gen['height'] + 1) save_path = lambda save, filename='': os.path.join(SAVES_DIR, save, filename) @@ -77,30 +77,6 @@ def load_meta(save): return meta -def load_chunk(save, chunk): - try: - map_ = parse_slices(get_chunk(save, chunk)) - except FileNotFoundError: - map_ = {} - - valid_map = {} - for pos, slice_ in map_.items(): - if pos in range(chunk, chunk + world_gen['chunk_size']): - - if not len(slice_) == world_gen['height']: - if len(slice_) < world_gen['height']: - # Extend slice height - slice_ = [' '] * (world_gen['height'] - len(slice_)) + slice_ - elif len(slice_) > world_gen['height']: - # Truncate slice height - slice_ = slice_[len(slice_) - world_gen['height']:] - - valid_map[pos] = slice_ - - save_map(save, map_) - return map_ - - def get_meta(save): with open(meta_path(save)) as f: data = json.load(f) @@ -108,18 +84,6 @@ def get_meta(save): return data -def get_chunk(save, chunk): - data = [] - - chunk_file = save_path(save, str(chunk) + CHUNK_EXT) - - if os.path.isfile(chunk_file): - with open(chunk_file) as f: - data = f.readlines() - - return data - - def check_meta(meta): # Create meta items if needed for key, default in default_meta.items(): @@ -134,54 +98,78 @@ def check_meta(meta): return meta -def parse_slices(data): - slices = {} +def save_meta(save, meta): + # Save meta file + with open(meta_path(save), 'w') as f: + json.dump(meta, f) - for line in data: - # Parses map file - key, slice_ = line.split(SLICE_SEP) - slice_ = list(slice_) - # Removes new line char if it exists - slices[key] = slice_ if not slice_[-1] == '\n' else slice_[:-1] +def chunk_file_name(save, chunk_n): + return save_path(save, str(chunk_n) + CHUNK_EXT) - return slices +def load_chunk(save, chunk_n): + map_ = {} + chunk_pos = chunk_n * world_gen['chunk_size'] -def save_meta(save, meta): - # Save meta file - with open(meta_path(save), 'w') as f: - json.dump(meta, f) + try: + with open(chunk_file_name(save, chunk_n)) as data: + for d_pos, slice_ in enumerate(data): + + # Truncate to correct size + slice_ = slice_[:world_gen['height']] + + height_error = world_gen['height'] - len(slice_) + if not height_error == 0: + # Extend slice height + slice_ = (' ' * height_error) + slice_ + + map_[str(chunk_pos + d_pos)] = list(slice_) + + except FileNotFoundError: + pass + + return map_ -def save_map(save, new_slices): +def save_chunk(save, chunk_n, chunk): + """ Updates slices within one chunk. """ + + filename = chunk_file_name(save, chunk_n) + if os.path.isfile(filename): + mode = 'r+' + else: + mode = 'w' + + with open(filename, mode) as file_: + + file_.truncate(CHUNK_SIZE) + for pos, slice_ in chunk.items(): + rel_pos = int(pos) % world_gen['chunk_size'] + + file_.seek(int(rel_pos) * (world_gen['height'] + 1)) + file_.write(''.join(slice_) + '\n') + + +def save_slices(save, new_slices): + """ Updates slices anywhere in the world. """ + # Group slices by chunk chunks = {} for pos, slice_ in new_slices.items(): + chunk_pos = chunk_num(pos) + try: - chunks[chunk_num(pos)].update({pos: slice_}) + chunks[chunk_pos].update({pos: slice_}) except KeyError: - chunks[chunk_num(pos)] = {pos: slice_} + chunks[chunk_pos] = {pos: slice_} log('saving slices', new_slices.keys()) log('saving chunks', chunks.keys()) # Update chunk files - for num, chunk in chunks.items(): - chunk_file = save_path(save, str(num) + CHUNK_EXT) - - # Update slices in chunk file with new slices - try: - with open(chunk_file) as f: - slices = parse_slices(f.readlines()) - except (OSError, IOError): - slices = {} - slices.update(chunk) - - # Write slices back to file - with open(chunk_file, 'w') as f: - for pos, slice_ in slices.items(): - f.write(str(pos) + SLICE_SEP + ''.join(slice_) + '\n') + for chunk_pos, chunk in chunks.items(): + save_chunk(save, chunk_pos, chunk) def set_blocks(map_, blocks): diff --git a/server.py b/server.py index de4411a..d36989c 100644 --- a/server.py +++ b/server.py @@ -8,8 +8,6 @@ from console import log -chunk_size = terrain.world_gen['chunk_size'] - SUN_TICK = radians(1/32) TPS = 10 # Ticks @@ -171,35 +169,26 @@ def __init__(self, save): def get_chunks(self, chunk_list): new_slices = {} - gen_slices = {} log('loading chunks', chunk_list) # Generates new terrain - for chunk_num in chunk_list: - chunk = saves.load_chunk(self._save, chunk_num) - for i in range(chunk_size): - pos = i + chunk_num * chunk_size - if not str(pos) in chunk: - slice_ = terrain.gen_slice(pos, self._meta) - chunk[str(pos)] = slice_ - gen_slices[str(pos)] = slice_ + for chunk_n in chunk_list: + + chunk = saves.load_chunk(self._save, chunk_n) + if not chunk: + chunk = terrain.gen_chunk(chunk_n, self._meta) + saves.save_chunk(self._save, chunk_n, chunk) new_slices.update(chunk) - log('generated slices', gen_slices.keys()) log('new slices', new_slices.keys()) - # Save generated terrain to file - if gen_slices: - log('saving slices', gen_slices.keys()) - saves.save_map(self._save, gen_slices) - self._map.update(new_slices) return new_slices def set_blocks(self, blocks): self._map, new_slices = saves.set_blocks(self._map, blocks) - saves.save_map(self._save, new_slices) + saves.save_slices(self._save, new_slices) return blocks def set_player(self, name, player): diff --git a/terrain.py b/terrain.py index 8ce7e8c..b82a124 100644 --- a/terrain.py +++ b/terrain.py @@ -1,16 +1,24 @@ import random +from collections import OrderedDict from math import ceil import render from data import world_gen +from console import log, DEBUG # Maximum width of half a tree MAX_HALF_TREE = int(len(max(world_gen['trees'], key=lambda tree: len(tree))) / 2) + +largest_ore = max(map(lambda ore: world_gen['ores'][ore]['vain_size'], world_gen['ores'])) +MAX_ORE_RANGE = (int((largest_ore - 1) / 2), (int(largest_ore / 2) + 1)) + EMPTY_SLICE = [' ' for y in range(world_gen['height'])] get_chunk_list = lambda slice_list: list(set(int(i) // world_gen['chunk_size'] for i in slice_list)) +MAX_HILL_RAD = world_gen['max_hill'] * world_gen['min_grad'] + blocks = render.blocks @@ -22,172 +30,370 @@ def move_map(map_, edges): return slices -def slice_height(pos, meta): - slice_height_ = world_gen['ground_height'] +def detect_edges(map_, edges): + slices = [] + for pos in range(*edges): + if not str(pos) in map_: + slices.append(pos) + + return slices + + +def spawn_hierarchy(tests): + # TODO: Use argument expansion for tests + return max(tests, key=lambda block: blocks[block]['hierarchy']) + + +def is_solid(block): + return blocks[block]['solid'] + + +def in_chunk(pos, chunk_pos): + return chunk_pos <= pos < chunk_pos + world_gen['chunk_size'] + + +class TerrainCache(OrderedDict): + """ Implements a Dict with a size limit. + Beyond which it replaces the oldest item. """ + + def __init__(self, *args, **kwds): + self._limit = kwds.pop("limit", None) + OrderedDict.__init__(self, *args, **kwds) + self._check_limit() + + def __setitem__(self, key, value): + OrderedDict.__setitem__(self, key, value) + self._check_limit() + + def _check_limit(self): + if self._limit is not None: + while len(self) > self._limit: + self.popitem(last=False) + + +# TODO: This probably shouldn't stay here... +features = None +def init_features(): + global features + cache_size = (world_gen['max_biome'] * 2) + world_gen['chunk_size'] + features = TerrainCache(limit=cache_size) + +init_features() + + +# # TODO: Use this for the other functions! +# def gen_features(generator, features, feature_group_name, chunk_pos, meta): +# """ Ensures the features within `range` exist in `features` """ + +# feature_cache = features[feature_group_name] + +# for x in range(chunk_pos - RAD, chunk_pos + world_gen['chunk_size'] + RAD): +# if feature_cache.get(str(chunk_pos)) is None: + +# # Init to empty, so 'no features' is cached. +# feature_cache[str(chunk_pos)] = {} + +# random.seed(str(meta['seed']) + str(chunk_pos) + feature_group_name) +# feature_cache[str(chunk_pos)]['biome'] = generator() + + +def gen_biome_features(features, chunk_pos, meta): + for x in range(chunk_pos - world_gen['max_biome'], chunk_pos + world_gen['chunk_size'] + world_gen['max_biome']): + + # TODO: Each of these `if` blocks should be abstracted into a function + # which just returns the `attrs` object. + + if features.get(str(x)) is None: + # Init to empty, so 'no features' is cached. + features[str(x)] = {} + + # If it is not None, it has all ready been generated. + if features[str(x)].get('biome') is None: + + random.seed(str(meta['seed']) + str(x) + 'biome') + if random.random() <= 0.05: + + # TODO: Move outside function + biomes_population = [] + for name, data in world_gen['biomes'].items(): + biomes_population.extend([name] * int(data['chance'] * 100)) + + attrs = {} + attrs['type'] = random.choice(sorted(biomes_population)) + attrs['radius'] = random.randint(world_gen['min_biome'], world_gen['max_biome']) + + features[str(x)]['biome'] = attrs + + +def gen_hill_features(features, chunk_pos, meta): + for x in range(chunk_pos - MAX_HILL_RAD, chunk_pos + world_gen['chunk_size'] + MAX_HILL_RAD): - # Check surrounding slices for a hill with min gradient - for x in range(pos - world_gen['max_hill'] * world_gen['min_grad'], - pos + world_gen['max_hill'] * world_gen['min_grad']): - # Set seed for random numbers based on position - random.seed(str(meta['seed']) + str(x) + 'hill') + # TODO: Each of these `if` blocks should be abstracted into a function + # which just returns the `attrs` object. - # Generate a hill with a 5% chance - if random.random() <= 0.05: + if features.get(str(x)) is None: + # Init to empty, so 'no features' is cached. + features[str(x)] = {} - # Get gradient for left, or right side of hill - gradient_l = random.randint(1, world_gen['min_grad']) - gradient_r = random.randint(1, world_gen['min_grad']) + # If it is not None, it has all ready been generated. + if features[str(x)].get('hill') is None: - gradient = gradient_r if x < pos else gradient_l + random.seed(str(meta['seed']) + str(x) + 'hill') + if random.random() <= 0.05: - # Height is distance from hill with gradient - d_hill_height = abs(pos-x) / gradient + attrs = {} + attrs['gradient_l'] = random.randint(1, world_gen['min_grad']) + attrs['gradient_r'] = random.randint(1, world_gen['min_grad']) + attrs['height'] = random.randint(0, world_gen['max_hill']) - # Cut off anything that would not be a part of the hill assuming - # flat ground. - if d_hill_height < world_gen['max_hill']: + features[str(x)]['hill'] = attrs - hill_height = (world_gen['ground_height'] + - random.randint(0, world_gen['max_hill']) - d_hill_height) - # Make top of hill flat - hill_height -= 1 if pos == x else 0 - slice_height_ = max(slice_height_, hill_height) +def gen_tree_features(features, ground_heights, slices_biome, chunk_pos, meta): + for x in range(chunk_pos - MAX_HALF_TREE, chunk_pos + world_gen['chunk_size'] + MAX_HALF_TREE): - return int(slice_height_) + # TODO: Each of these `if` blocks should be abstracted into a function + # which just returns the `attrs` object. + if features.get(str(x)) is None: + # Init to empty, so 'no features' is cached. + features[str(x)] = {} -def add_tree(slice_, pos, meta): - for x in range(pos - MAX_HALF_TREE, pos + MAX_HALF_TREE + 1): - tree_chance = biome(x, meta) + # If it is not None, it has all ready been generated. + if features[str(x)].get('tree') is None: - # Set seed for random numbers based on position - random.seed(str(meta['seed']) + str(x) + 'tree') + biome_data = world_gen['biomes'][slices_biome[str(x)][0]] + tree_chance = biome_data['trees'] - # Generate a tree with a chance dependent on the biome - if random.random() <= tree_chance: - tree = random.choice(world_gen['trees']) + random.seed(str(meta['seed']) + str(x) + 'tree') + if random.random() <= tree_chance: - # Get height above ground - air_height = world_gen['height'] - slice_height(x, meta) + attrs = {} + attrs['type'] = random.randint(0, len(world_gen['trees'])-1) - # Centre tree slice (contains trunk) - center_leaves = tree[int(len(tree)/2)] - trunk_depth = next(i for i, leaf in enumerate(center_leaves[::-1]) - if leaf) - tree_height = random.randint(2, air_height - - len(center_leaves) + trunk_depth) + # Centre tree slice (contains trunk) + # TODO: This calculation could be done on start-up, and stored + # with each tree type. + tree = world_gen['trees'][attrs['type']] + center_leaves = tree[int(len(tree) / 2)] + attrs['trunk_depth'] = next(i for i, leaf in enumerate(center_leaves[::-1]) if leaf) - # Find leaves of current tree - for i, leaf_slice in enumerate(tree): - leaf_pos = x + (i - int(len(tree) / 2)) - if leaf_pos == pos: - leaf_height = air_height - tree_height - (len(leaf_slice) - trunk_depth) + # Get space above ground + air_height = world_gen['height'] - ground_heights[str(x)] + attrs['height'] = random.randint(2, air_height - len(center_leaves) + attrs['trunk_depth']) - # Add leaves to slice - for j, leaf in enumerate(leaf_slice): - if leaf: - sy = leaf_height + j - slice_[sy] = spawn_hierarchy(('@', slice_[sy])) + features[str(x)]['tree'] = attrs - if x == pos: - # Add trunk to slice - for i in range(air_height - tree_height, - air_height): - slice_[i] = spawn_hierarchy(('|', slice_[i])) - return slice_ +def gen_ore_features(features, ground_heights, slices_biome, chunk_pos, meta): + for x in range(chunk_pos - MAX_ORE_RANGE[0], chunk_pos + world_gen['chunk_size'] + MAX_ORE_RANGE[1]): + # TODO: Each of these `if` blocks should be abstracted into a function + # which just returns the `attrs` object. -def biome(pos, meta): - biome_type = [] + if features.get(str(x)) is None: + # Init to empty, so 'no features' is cached. + features[str(x)] = {} - # Check surrounding slices for a biome marker - for biome_x in range(pos - int(world_gen['max_biome_size'] / 2), - pos + int(world_gen['max_biome_size'] / 2)): - # Set seed for random numbers based on position - random.seed(str(meta['seed']) + str(biome_x) + 'biome') + # Ores + # NOTE: Ores seem to be the way to model the generalisation of the + # rest of the features after + for name, ore in world_gen['ores'].items(): + feature_name = name + '_ore_root' - # Generate a biome marker with a 5% chance - if random.random() <= .05: - biome_type.append(random.choice(world_gen['biome_tree_weights'])) + # If it is not None, it has all ready been generated. + if features[str(x)].get(feature_name) is None: - # If not plains or forest, it's normal - return max(set(biome_type), key=biome_type.count) if biome_type else .05 + random.seed(str(meta['seed']) + str(x) + feature_name) + if random.random() <= ore['chance']: + attrs = {} + attrs['root_height'] = world_gen['height'] - random.randint( + ore['lower'], + min(ore['upper'], (ground_heights[str(x)] - 1)) # -1 for grass. + ) -def add_ores(slice_, pos, meta, slice_height_): - for ore in world_gen['ores'].values(): - for x in range(pos - int(ore['vain_size'] / 2), - pos + ceil(ore['vain_size'] / 2)): - # Set seed for random numbers based on position and ore - random.seed(str(meta['seed']) + str(x) + ore['char']) + # Generates ore at random position around root ore + pot_vain_blocks = ore['vain_size'] ** 2 - # Gernerate a ore with a probability - if random.random() <= ore['chance']: - root_ore_height = random.randint(ore['lower'], ore['upper']) + # Describes the shape of the vain, + # top to bottom, left to right. + attrs['vain_shape'] = [b / 100 for b in random.sample(range(0, 100), pot_vain_blocks)] - # Generates ore at random position around root ore - random.seed(str(meta['seed']) + str(pos) + ore['char']) - ore_height = (root_ore_height + - random.randint(-int(ore['vain_size'] / 2), - ceil(ore['vain_size'] / 2))) + features[str(x)][feature_name] = attrs + +def gen_grass_features(features, ground_heights, slices_biome, chunk_pos, meta): + for x in range(chunk_pos, chunk_pos + world_gen['chunk_size']): + + # TODO: Each of these `if` blocks should be abstracted into a function + # which just returns the `attrs` object. + + if features.get(str(x)) is None: + # Init to empty, so 'no features' is cached. + features[str(x)] = {} + + # If it is not None, it has all ready been generated. + if features[str(x)].get('grass') is None: + + biome_data = world_gen['biomes'][slices_biome[str(x)][0]] + grass_chance = biome_data['grass'] + + random.seed(str(meta['seed']) + str(x) + 'grass') + if random.random() <= grass_chance: + + attrs = {} + attrs['y'] = ground_heights[str(x)] + + features[str(x)]['grass'] = attrs + + +def build_tree(chunk, chunk_pos, x, tree_feature, ground_heights): + """ Adds a tree feature at x to the chunk. """ + + # Add trunk + if in_chunk(x, chunk_pos): + air_height = world_gen['height'] - ground_heights[str(x)] + for trunk_y in range(air_height - tree_feature['height'], air_height - (bool(DEBUG) * 3)): + chunk[str(x)][trunk_y] = spawn_hierarchy(('|', chunk[str(x)][trunk_y])) + + # Add leaves + leaves = world_gen['trees'][tree_feature['type']] + half_leaves = int(len(leaves) / 2) + + for leaf_dx, leaf_slice in enumerate(leaves): + leaf_x = x + (leaf_dx - half_leaves) + + if in_chunk(leaf_x, chunk_pos): + air_height = world_gen['height'] - ground_heights[str(x)] + leaf_height = air_height - tree_feature['height'] - len(leaf_slice) + tree_feature['trunk_depth'] + + for leaf_dy, leaf in enumerate(leaf_slice): + if (bool(DEBUG) and leaf_dy == 0) or (not bool(DEBUG) and leaf): + leaf_y = leaf_height + leaf_dy + chunk[str(leaf_x)][leaf_y] = spawn_hierarchy(('@', chunk[str(leaf_x)][leaf_y])) + + +def build_grass(chunk, chunk_pos, x, grass_feature, ground_heights): + """ Adds a grass feature at x to the chunk. """ + + if in_chunk(x, chunk_pos): + grass_y = world_gen['height'] - ground_heights[str(x)] - 1 + chunk[str(x)][grass_y] = spawn_hierarchy(('v', chunk[str(x)][grass_y])) + + +def build_ore(chunk, chunk_pos, x, ore_feature, ore, ground_heights): + """ Adds an ore feature at x to the chunk. """ + + for block_pos in range(ore['vain_size'] ** 2): + if ore_feature['vain_shape'][block_pos] < ore['vain_density']: + + # Centre on root ore + block_dx = (block_pos % ore['vain_size']) - int((ore['vain_size'] - 1) / 2) + block_dy = int(block_pos / ore['vain_size']) - int((ore['vain_size'] - 1) / 2) + + block_x = block_dx + x + block_y = block_dy + ore_feature['root_height'] + + if in_chunk(block_x, chunk_pos): # Won't allow ore above surface - if ore['lower'] < ore_height < min(ore['upper'], slice_height_): - sy = world_gen['height'] - ore_height - slice_[sy] = spawn_hierarchy((ore['char'], slice_[sy])) + if world_gen['height'] > block_y > world_gen['height'] - ground_heights[str(block_x)]: + chunk[str(block_x)][block_y] = spawn_hierarchy((ore['char'], chunk[str(block_x)][block_y])) - return slice_ +def gen_chunk(chunk_n, meta): + chunk_pos = chunk_n * world_gen['chunk_size'] -def add_tall_grass(slice_, pos, meta, slice_height_): - # Set seed for random numbers based on position and grass - random.seed(str(meta['seed']) + str(pos) + 'grass') + # TODO: Allow more than one feature per x in features? - # Gernerate a grass with a probability - if random.random() <= world_gen['tall_grass_rate']: - sy = world_gen['height'] - slice_height_ - 1 - slice_[sy] = spawn_hierarchy(('v', slice_[sy])) + # First generate all the features we will need + # for all the slice is in this chunk - return slice_ + gen_biome_features(features, chunk_pos, meta) + gen_hill_features(features, chunk_pos, meta) + # Generate hill heights and biomes map for the tree and ore generation. + ground_heights = {str(x): world_gen['ground_height'] for x in range(chunk_pos - MAX_HILL_RAD, chunk_pos + world_gen['chunk_size'] + MAX_HILL_RAD)} + # Store feature_x with the value for calculating precedence. + slices_biome = {str(x): ('normal', None) for x in range(chunk_pos - world_gen['max_biome'], chunk_pos + world_gen['chunk_size'] + world_gen['max_biome'])} -def gen_slice(pos, meta): - slice_height_ = slice_height(pos, meta) + for feature_x, slice_features in features.items(): + feature_x = int(feature_x) - # Form slice of sky, grass, stone, bedrock - slice_ = ( - [' '] * (world_gen['height'] - slice_height_) + - ['-'] + - ['#'] * (slice_height_ - 2) + # 2 for grass and bedrock - ['_'] - ) + for feature_name, feature in slice_features.items(): - # TODO: Combine loops in each of these functions? - slice_ = add_tree(slice_, pos, meta) - slice_ = add_ores(slice_, pos, meta, slice_height_) - slice_ = add_tall_grass(slice_, pos, meta, slice_height_) + if feature_name == 'hill': - return slice_ + for d_x in range(-feature['height'] * feature['gradient_l'], + feature['height'] * feature['gradient_r']): + x = feature_x + d_x + gradient = feature['gradient_l'] if d_x < 0 else feature['gradient_r'] + hill_height = int(feature['height'] - (abs(d_x) / gradient)) -def detect_edges(map_, edges): - slices = [] - for pos in range(*edges): - if not str(pos) in map_: - slices.append(pos) + if d_x == 0: + hill_height -= 1 - return slices + ground_height = world_gen['ground_height'] + hill_height + old_height = ground_heights.get(str(x), 0) + ground_heights[str(x)] = max(ground_height, old_height) -def spawn_hierarchy(tests): - return max(tests, key=lambda block: blocks[block]['hierarchy']) + elif feature_name == 'biome': + for d_x in range(-feature['radius'], feature['radius']): + x = feature_x + d_x -def is_solid(block): - return blocks[block]['solid'] + if str(x) in slices_biome: + previous_slice_biome_feature_x = slices_biome[str(x)][1] + + if (previous_slice_biome_feature_x is None or + previous_slice_biome_feature_x < feature_x): + slices_biome[str(x)] = (feature['type'], feature_x) + + chunk = {} + for x in range(chunk_pos, chunk_pos + world_gen['chunk_size']): + chunk[str(x)] = ( + [' '] * (world_gen['height'] - ground_heights[str(x)]) + + ['-'] + + ['#'] * (ground_heights[str(x)] - 2) + # 2 for grass and bedrock + ['_'] + ) + + int_x = list(map(int, ground_heights.keys())) + log('chunk', chunk_pos, m=1) + log('max', max(int_x), m=1) + log('min', min(int_x), m=1) + log('gh diff', set(range(chunk_pos - MAX_HILL_RAD, chunk_pos + world_gen['chunk_size'] + MAX_HILL_RAD)) - set(int_x), m=1, trunc=False) + log('slices_biome', list(filter(lambda slice_: (int(slice_[0])%16 == 0) or (int(slice_[0])+1)%16 == 0, sorted(slices_biome.items()))), m=1, trunc=False) + + gen_tree_features(features, ground_heights, slices_biome, chunk_pos, meta) + gen_ore_features(features, ground_heights, slices_biome, chunk_pos, meta) + gen_grass_features(features, ground_heights, slices_biome, chunk_pos, meta) + + log('chunk_pos', chunk_pos, m=1) + tree_features = list(filter(lambda f: f[1].get('tree'), features.items())) + log('trees in cache\n', [str(f[0]) for f in tree_features], m=1, trunc=0) + log('trees in range', [str(f[0]) for f in tree_features if (chunk_pos <= int(f[0]) < chunk_pos + world_gen['chunk_size'])], m=1, trunc=0) + + # Insert trees and ores + for feature_x, slice_features in features.items(): + feature_x = int(feature_x) + + for feature_name, feature in slice_features.items(): + + if feature_name == 'tree': + build_tree(chunk, chunk_pos, feature_x, feature, ground_heights) + + elif feature_name == 'grass': + build_grass(chunk, chunk_pos, feature_x, feature, ground_heights) + + else: + for name, ore in world_gen['ores'].items(): + ore_name = name + '_ore_root' + if feature_name == ore_name: + build_ore(chunk, chunk_pos, feature_x, feature, ore, ground_heights) + break -def ground_height(slice_): - return next(i for i, block in enumerate(slice_) if blocks[block]['solid']) + return chunk diff --git a/tester.py b/tester.py index b3343ca..9cbd2c1 100644 --- a/tester.py +++ b/tester.py @@ -2,6 +2,7 @@ def main(): + pycraft.log('\n\n', m=1) save = pycraft.saves.new_save({'name': 'test', 'seed': 'This is a test!'}) pycraft.game(pycraft.server_interface.LocalInterface('tester', save, 0))