diff --git a/build_terrain.py b/build_terrain.py new file mode 100644 index 0000000..c2eec1c --- /dev/null +++ b/build_terrain.py @@ -0,0 +1,126 @@ +from data import world_gen +from terrain import in_chunk, spawn_hierarchy +from console import log, DEBUG + +import terrain_gen, terrain + + +def ground_height_feature(features, x): + ground_height = world_gen['ground_height'] + + slice_features = features.get(x) + if slice_features and slice_features.get('ground_height'): + ground_height = slice_features['ground_height'] + + return ground_height + + +def build_tree(chunk, chunk_pos, x, tree_feature, features): + """ Adds a tree feature at x to the chunk. """ + + # Add trunk + if in_chunk(x, chunk_pos): + air_height = world_gen['height'] - ground_height_feature(features, x) + for trunk_y in range(air_height - tree_feature['height'], air_height - (bool(DEBUG) * 3)): + chunk[x][trunk_y] = spawn_hierarchy(('|', chunk[x][trunk_y])) + + # Add leaves + leaves = world_gen['trees'][tree_feature['type']]['leaves'] + 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_height_feature(features, 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[leaf_x][leaf_y] = spawn_hierarchy(('@', chunk[leaf_x][leaf_y])) + + +def build_grass(chunk, chunk_pos, x, grass_feature, features): + """ Adds a grass feature at x to the chunk. """ + + if in_chunk(x, chunk_pos): + grass_y = world_gen['height'] - ground_height_feature(features, x) - 1 + chunk[x][grass_y] = spawn_hierarchy(('v', chunk[x][grass_y])) + + +def build_ore(chunk, chunk_pos, x, ore_feature, ore, features): + """ 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 not in_chunk(block_x, chunk_pos): + continue + + if not world_gen['height'] > block_y > world_gen['height'] - ground_height_feature(features, block_x): + continue + + if chunk[block_x][block_y] is ' ': + continue + + chunk[block_x][block_y] = spawn_hierarchy((ore['char'], chunk[block_x][block_y])) + + +def build_cave(chunk, chunk_pos, x, cave_feature, features): + """ Adds caves at x to the chunk. """ + + for (world_x, y) in cave_feature: + if in_chunk(world_x, chunk_pos): + chunk[world_x][int(y)] = ' ' + + +def build_chunk(chunk_n, meta): + chunk_pos = chunk_n * world_gen['chunk_size'] + + chunk_features = terrain_gen.gen_chunk_features(chunk_n, meta) + + chunk_ground_heights = {} + chunk = {} + for x in range(chunk_pos, chunk_pos + world_gen['chunk_size']): + slice_features = chunk_features.get(x) + + chunk_ground_heights[x] = ground_height_feature(chunk_features, x) + + chunk[x] = ( + [' '] * (world_gen['height'] - chunk_ground_heights[x]) + + ['-'] + + ['#'] * (chunk_ground_heights[x] - 2) + # 2 for grass and bedrock + ['_'] + ) + + for x, slice_features in chunk_features.items(): + x = int(x) + + for feature_name, feature in slice_features.items(): + + if feature_name == 'tree': + build_tree(chunk, chunk_pos, x, feature, chunk_features) + + elif feature_name == 'grass': + build_grass(chunk, chunk_pos, x, feature, chunk_features) + + elif feature_name == 'cave': + build_cave(chunk, chunk_pos, x, feature, chunk_features) + + else: + for name, ore in world_gen['ores'].items(): + ore_name = name + '_ore_root' + + if feature_name == ore_name: + build_ore(chunk, chunk_pos, x, feature, ore, chunk_features) + break + + return chunk, chunk_ground_heights diff --git a/colours.py b/colours.py index f72ef06..df7c7fe 100644 --- a/colours.py +++ b/colours.py @@ -80,7 +80,7 @@ def round_to_palette(*colour): def lightness(colour): - return 0.2126 * colour[0] + 0.7152 * colour[1] + 0.0722 * colour[2]; + return 0.2126 * colour[0] + 0.7152 * colour[1] + 0.0722 * colour[2] def grey(value): diff --git a/data.py b/data.py index c60b9c4..e644ffb 100644 --- a/data.py +++ b/data.py @@ -1,4 +1,4 @@ -from colours import * +from colours import DARK, BOLD, DARK_GRAY, YELLOW, RED, WHITE from console import supported_chars @@ -470,6 +470,8 @@ 'chance': 1, 'max_height': 8, 'min_height': 3, + 'mean_height': 4, + 'max_std_dev': 2, 'leaves': ((0, 1, 1), (1, 1, 0), (0, 1, 1)) @@ -478,6 +480,8 @@ 'chance': 1, 'max_height': 12, 'min_height': 6, + 'mean_height': 7, + 'max_std_dev': 3, 'leaves': ((1, 1, 0, 0, 0, 1, 1), (0, 1, 1, 0, 1, 1, 0), (0, 0, 1, 1, 0, 0, 0), @@ -488,6 +492,8 @@ 'chance': .7, 'max_height': 10, 'min_height': 4, + 'mean_height': 8, + 'max_std_dev': 4, 'leaves': ((0, 0, 0, 0, 1), (0, 0, 1, 1, 0), (0, 1, 0, 0, 0), @@ -503,6 +509,8 @@ 'chance': 1, 'max_height': 8, 'min_height': 4, + 'mean_height': 5, + 'max_std_dev': 2, 'leaves': ((0, 0, 1), (1, 0, 0), (0, 1, 0)) @@ -511,6 +519,8 @@ 'chance': .9, 'max_height': 16, 'min_height': 6, + 'mean_height': 7, + 'max_std_dev': 4, 'leaves': ((0, 0, 0, 1, 0, 1), (0, 1, 1, 1, 1, 0), (1, 1, 0, 0, 0, 0), @@ -521,6 +531,8 @@ 'chance': .8, 'max_height': 22, 'min_height': 8, + 'mean_height': 7, + 'max_std_dev': 3, 'leaves': ((0,0,1,1,0,1,1,0), (0,1,1,1,1,1,1,1), (1,1,1,1,1,1,1,1), @@ -533,6 +545,8 @@ 'chance': 1, 'max_height': 32, 'min_height': 12, + 'mean_height': 10, + 'max_std_dev': 4, 'leaves': ((0, 0, 0, 0, 0, 1, 1, 1, 0), (0, 0, 0, 0, 1, 1, 1, 1, 0), (0, 0, 0, 1, 1, 1, 1, 1, 0), diff --git a/events.py b/events.py index aacc5b2..51915ad 100644 --- a/events.py +++ b/events.py @@ -40,4 +40,4 @@ def boom(server, x, y): server.splash_damage(x, y, radius*2, blast_strength/3) - return new_blocks \ No newline at end of file + return new_blocks diff --git a/gravity.py b/gravity.py new file mode 100644 index 0000000..3d46884 --- /dev/null +++ b/gravity.py @@ -0,0 +1,52 @@ +from data import world_gen +from terrain import is_solid + + +def apply_gravity(map_, edges): + start_pos = (sum(edges) / 2, + world_gen['height'] - 1) + + new_blocks = {} + connected_to_ground = explore_map(map_, edges, start_pos, set()) + + for x, slice_ in map_.items(): + if x not in range(*edges): + continue + for y in range(len(slice_)-3, -1, -1): + block = slice_[y] + if (is_solid(block) and (x, y) not in connected_to_ground): + new_blocks.setdefault(x, {}) + new_blocks[x][y] = ' ' + new_blocks[x][y+1] = block + + return new_blocks + + +def explore_map(map_, edges, start_pos, connected_to_ground): + blocks_to_explore = set([start_pos]) + visted_blocks = set() + + while blocks_to_explore: + current_pos = blocks_to_explore.pop() + visted_blocks.add(current_pos) + + if (current_pos[1] >= 0 and current_pos[1] < world_gen['height'] and + current_pos not in connected_to_ground and + current_pos[0] in range(edges[0]-1, edges[1]+1)): + + try: + current_block = map_[current_pos[0]][current_pos[1]] + except (IndexError, KeyError): + current_block = None + + if (current_block is not None and + is_solid(current_block)) or current_pos[0] in (edges[0]-1, edges[1]): + + connected_to_ground.add(current_pos) + for (dx, dy) in ((x, y) for x in (-1, 0, 1) for y in (-1, 0, 1)): + pos = (current_pos[0] + dx, current_pos[1] + dy) + + if pos not in visted_blocks: + blocks_to_explore.add(pos) + + return connected_to_ground diff --git a/items.py b/items.py index 2d7f0bf..4bd1498 100644 --- a/items.py +++ b/items.py @@ -66,4 +66,4 @@ def items_to_render_objects(items, x, offset): objects.append(object_) - return objects \ No newline at end of file + return objects diff --git a/main.py b/main.py index 8fce31d..013a5f7 100644 --- a/main.py +++ b/main.py @@ -11,7 +11,7 @@ from items import items_to_render_objects from events import process_events -import saves, ui, terrain, player, render, render_interface, server_interface, data +import saves, ui, terrain, gravity, player, render, render_interface, server_interface, data def main(): @@ -80,7 +80,6 @@ def setdown(): def game(server, settings, benchmarks): dt = 0 # Tick - df = 0 # Frame dc = 0 # Cursor ds = 0 # Selector dpos = False @@ -216,7 +215,7 @@ def game(server, settings, benchmarks): server.redraw = True if settings.get('gravity'): - blocks = terrain.apply_gravity(server.map_, extended_edges) + blocks = gravity.apply_gravity(server.map_, extended_edges) if blocks: server.set_blocks(blocks) ## Crafting @@ -286,14 +285,14 @@ def game(server, settings, benchmarks): events += new_events new_blocks = {} - for i in range(int(dt)): + for _ in range(int(dt)): new_blocks.update(process_events(events, server)) if new_blocks: server.set_blocks(new_blocks) # If no block below, kill player try: - block = server.map_[x][y+1] + _ = server.map_[x][y+1] except IndexError: alive = False diff --git a/mobs.py b/mobs.py index 74607ab..cde2343 100644 --- a/mobs.py +++ b/mobs.py @@ -30,7 +30,7 @@ def update(mobs, players, map_, last_tick): new_items = {} for mob_id, mob in mobs.items(): - mx, my, x_vel = mob['x'], mob['y'], mob['x_vel'] + mx, my = mob['x'], mob['y'] if mob['health'] <= 0: removed_mobs.append(mob_id) @@ -64,7 +64,7 @@ def update(mobs, players, map_, last_tick): def spawn(mobs, players, map_, x_start_range, y_start_range, x_end_range, y_end_range): - log("spawning", x_start_range, x_end_range, m='mobs'); + log('spawning', x_start_range, x_end_range, m='mobs') n_mobs_to_spawn = random.randint(0, 5) if random.random() < mob_rate else 0 new_mobs = {} diff --git a/network.py b/network.py index e428e86..33918ca 100644 --- a/network.py +++ b/network.py @@ -53,7 +53,7 @@ def receive(sock): d = bytes() try: - for i in range(length // bufsize): + for _ in range(length // bufsize): d += sock.recv(bufsize) time.sleep(0.001) d += sock.recv(length % bufsize) @@ -107,7 +107,7 @@ def start(data_handler, port): HOST, PORT = '0.0.0.0', int(port) server = ThreadedTCPServer((HOST, PORT), requestHandlerFactory(data_handler)) - ip, port = server.server_address + _, port = server.server_address # Start a thread with the server -- that thread will then start one # more thread for each request diff --git a/pathfinding.py b/pathfinding.py index f8c39d5..8440ed4 100644 --- a/pathfinding.py +++ b/pathfinding.py @@ -33,4 +33,4 @@ def pathfind_towards_delta(entity, delta, map_): updated = True - return updated, kill_entity \ No newline at end of file + return updated, kill_entity diff --git a/perlin.py b/perlin.py new file mode 100644 index 0000000..988f38a --- /dev/null +++ b/perlin.py @@ -0,0 +1,152 @@ +repeat = 100 + +# Hash lookup table as defined by Ken Perlin. This is a randomly arranged array of all numbers from 0-255 inclusive. +permutation = [ + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, + 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, + 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, + 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, + 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, + 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, + 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, + 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, + 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, + 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, + 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, + 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, + 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, + 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 +] + +# Doubled permutation to avoid overflow +p = [ permutation[x%256] for x in range(0, 512) ] + +# Fade function as defined by Ken Perlin. This eases coordinate values +# so that they will ease towards integral values. This ends up smoothing +# the final output. +# 6t^5 - 15t^4 + 10t^3 +def fade(t): + return t * t * t * (t * (t * 6 - 15) + 10) + +def inc(num): + num += 1 + if repeat > 0: + num %= repeat + return num + +def grad(hash, x, y, z): + return [x+y, -x+y, x-y, -x-y, x+z, -x+z, x-z, -x-z, y+z, -y+z, y-z, -y-z, y+x, -y+z, y-x, -y-z][hash & 0xF] + +# Linear Interpolate +def lerp(a, b, x): + return a + x * (b - a) + +def perlin3(x, y, z): + if repeat > 0: + # If we have any repeat on, change the coordinates to their "local" repetitions + x %= repeat + y %= repeat + z %= repeat + + # Calculate the "unit cube" that the point asked will be located in + # The left bound is ( |_x_|,|_y_|,|_z_| ) and the right bound is that + # plus 1. Next we calculate the location (from 0.0 to 1.0) in that cube. + xi = int(x) & 255 + yi = int(y) & 255 + zi = int(z) & 255 + xf = x - int(x) + yf = y - int(y) + zf = z - int(z) + + u = fade(xf) + v = fade(yf) + w = fade(zf) + + aaa = p[p[p[ xi ]+ yi ]+ zi ] + aba = p[p[p[ xi ]+inc(yi)]+ zi ] + aab = p[p[p[ xi ]+ yi ]+inc(zi)] + abb = p[p[p[ xi ]+inc(yi)]+inc(zi)] + baa = p[p[p[inc(xi)]+ yi ]+ zi ] + bba = p[p[p[inc(xi)]+inc(yi)]+ zi ] + bab = p[p[p[inc(xi)]+ yi ]+inc(zi)] + bbb = p[p[p[inc(xi)]+inc(yi)]+inc(zi)] + + # The gradient function calculates the dot product between a pseudorandom + # gradient vector and the vector from the input coordinate to the 8 + # surrounding points in its unit cube. + x1 = lerp(grad(aaa, xf, yf, zf), + grad(baa, xf-1, yf, zf), + u) + # This is all then lerped together as a sort of weighted average based on the faded(u,v,w) + # values we made earlier. + x2 = lerp(grad(aba, xf, yf-1, zf), + grad(bba, xf-1, yf-1, zf), + u) + y1 = lerp(x1, x2, v) + + x1 = lerp(grad(aab, xf, yf, zf-1), + grad(bab, xf-1, yf, zf-1), + u) + x2 = lerp(grad(abb, xf, yf-1, zf-1), + grad(bbb, xf-1, yf-1, zf-1), + u) + y2 = lerp(x1, x2, v) + + # For convenience we bind the result to 0 - 1 (theoretical min/max before is [-1, 1]) + return (lerp(y1, y2, w)+1)/2 + +def perlin2(x, y): + if repeat > 0: + # If we have any repeat on, change the coordinates to their "local" repetitions + x %= repeat + y %= repeat + + # Calculate the "unit cube" that the point asked will be located in + # The left bound is ( |_x_|,|_y_|,|_z_| ) and the right bound is that + # plus 1. Next we calculate the location (from 0.0 to 1.0) in that cube. + xi = int(x) & 255 + yi = int(y) & 255 + xf = x - int(x) + yf = y - int(y) + + u = fade(xf) + v = fade(yf) + + aa = p[p[ xi ]+ yi ] + ab = p[p[ xi ]+inc(yi)] + ba = p[p[inc(xi)]+ yi ] + bb = p[p[inc(xi)]+inc(yi)] + + # The gradient function calculates the dot product between a pseudorandom + # gradient vector and the vector from the input coordinate to the 8 + # surrounding points in its unit cube. + x1 = lerp(grad(aa, xf, yf, 0), + grad(ba, xf-1, yf, 0), + u) + # This is all then lerped together as a sort of weighted average based on the faded(u,v,0) + # values we made earlier. + x2 = lerp(grad(ab, xf, yf-1, 0), + grad(bb, xf-1, yf-1, 0), + u) + y1 = lerp(x1, x2, v) + + # theoretical min/max is [-1, 1]) + return y1 + +def OctavePerlin3(x, y, z, octaves, persistence): + total = 0 + frequency = 1 + amplitude = 1 + # Used for normalizing result to 0.0 - 1.0 + maxValue = 0 + for i in range(0, octaves): + total += perlin3(x * frequency, y * frequency, z * frequency) * amplitude + + maxValue += amplitude + + amplitude *= persistence + frequency *= 2 + + return total/maxValue diff --git a/player.py b/player.py index 14028c0..ac0da3a 100644 --- a/player.py +++ b/player.py @@ -1,4 +1,4 @@ -from colours import * +from colours import WHITE, RED from render import blocks from terrain import is_solid, world_gen from events import boom @@ -26,7 +26,7 @@ def get_pos_delta_on_input(inp, map_, x, y, jump, flight): dx = -1 * ('a' in inp) + 1 * ('d' in inp) - dx, dy = get_pos_delta(dx, x, y, map_) + dx, dy = get_pos_delta(dx, x, y, map_, flight) # Jumps if up pressed, block below, no block above if ( 'w' in inp and y > 1 @@ -38,26 +38,28 @@ def get_pos_delta_on_input(inp, map_, x, y, jump, flight): dy = -1 jump = 6 - if (flight and 's' in inp and head_y < world_gen['height'] - and (not is_solid(player_slice[below_y]))): + if (flight and 's' in inp and head_y < world_gen['height']): dy = 1 return dx, dy, jump -def get_pos_delta(dx, x, y, map_): +def get_pos_delta(dx, x, y, map_, flight=False): player_slice = map_[x] feet_y = y head_y = y - 1 - below_y = y + 1 above_y = y - 2 dy = 0 next_slice = map_[x + dx] checked_dx = 0 + + if flight: + return dx, dy + if not is_solid(next_slice[head_y]): if is_solid( next_slice[feet_y] ): if ( not is_solid( next_slice[above_y] ) @@ -196,8 +198,6 @@ def create_render_object_health_colour_effect(health, render_object): dead_colour = render_object.get('dead_colour') if dead_colour is not None: - live_colour = render_object.get('colour', render_object['model'][0][0]) - render_object['effect_colour'] = dead_colour render_object['effect_strength'] = 1 - (health / MAX_PLAYER_HEALTH) diff --git a/render.py b/render.py index cff66a2..f3d28bf 100644 --- a/render.py +++ b/render.py @@ -1,7 +1,7 @@ from math import pi, cos, sin, sqrt, modf, radians -from colours import * -from console import * +from colours import colour_str, uncolour_str, rgb, round_to_palette, lightness, bold, RED, CYAN, BLUE +from console import supported_chars, DEBUG, POS_STR, CLS_END_LN from data import lighting, world_gen, blocks, timings from terrain import is_solid @@ -68,6 +68,9 @@ def render_map(map_, slice_heights, edges, edges_y, objects, bk_objects, sky_col fg, bg, char, style = calc_pixel(x, y, world_x, world_y, edges[0], map_, slice_heights, pixel, objects, bk_objects, sky_colour, day, lights, settings.get('fancy_lights')) + if DEBUG and world_x % world_gen['chunk_size'] == 0: + bg = RED + if settings.get('terminal_output'): pixel = colour_str( char, @@ -101,7 +104,6 @@ def obj_pixel(x, y, objects): if object_: model = object_['model'] - width = len(model) height = len(model[0]) dx = x - object_['x'] @@ -237,7 +239,7 @@ def get_light_colour(x, y, world_x, map_, slice_heights, lights, colour_behind, else: - light = CYAN if any(map(lambda l: lit(world_x, x, y, l) < 1, lights)) else hsv_to_rgb(colour_behind) + light = CYAN if any(map(lambda l: lit(world_x + x, y, l) < 1, lights)) else hsv_to_rgb(colour_behind) return light diff --git a/render_interface.py b/render_interface.py index 8c938c9..9d11482 100644 --- a/render_interface.py +++ b/render_interface.py @@ -55,4 +55,4 @@ def get_light_level(*args): log('Not implemented: Python get_light_level function', m='warning') log('{}'.format(result), m='lighting') - return result \ No newline at end of file + return result diff --git a/server.py b/server.py index 56739b6..c45f2c5 100644 --- a/server.py +++ b/server.py @@ -2,7 +2,7 @@ from math import radians, floor, ceil from threading import Thread -import terrain, saves, network, mobs, items, render_interface +import build_terrain, saves, network, mobs, items, render_interface from colours import colour_str, TERM_YELLOW from console import log @@ -246,7 +246,7 @@ def get_chunks(self, chunk_list): chunk, chunk_slice_heights = saves.load_chunk(self._save, chunk_n) if not chunk: - chunk, chunk_slice_heights = terrain.gen_chunk(chunk_n, self._meta) + chunk, chunk_slice_heights = build_terrain.build_chunk(chunk_n, self._meta) saves.save_chunk(self._save, chunk_n, chunk, chunk_slice_heights) new_slices.update(chunk) @@ -322,7 +322,7 @@ def spawn_mobs(self, n_mob_spawn_cycles, bk_objects, sky_colour, day, lights): render_interface.create_lighting_buffer(width, height, x_start, y_start, self._map, self._slice_heights, bk_objects, sky_colour, day, lights) - for i in range(n_mob_spawn_cycles): + for _ in range(n_mob_spawn_cycles): mobs.spawn(self._meta['mobs'], self._meta['players'], self._map, x_start, y_start, x_end, y_end) def update_items(self): diff --git a/server_interface.py b/server_interface.py index 39b6938..f413ca7 100644 --- a/server_interface.py +++ b/server_interface.py @@ -337,15 +337,13 @@ def _event_logout(self, error=None): self.game = False def _event_error(self, error): - self.error = 'Error from server: ' + error['event'] + ': ' + event['message'] + self.error = 'Error from server: ' + error['event'] + ': ' + error['message'] log(self.error) self.game = False # Main loop methods: def get_chunks(self, chunk_list): - slices_its_loading = ((chunk_num + chunk * chunk_size) for chunk in chunk_list for chunk_num in range(chunk_size)) - self.handle(self._send('get_chunks', [chunk_list])) self.view_change = True diff --git a/terrain.py b/terrain.py index 5d75ba7..2c27e4c 100644 --- a/terrain.py +++ b/terrain.py @@ -1,22 +1,11 @@ -import random -from collections import OrderedDict -from math import ceil, cos, sin, radians, atan2 - from data import world_gen, blocks -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'] +def get_chunk_list(slice_list): + return list(set(int(i) // world_gen['chunk_size'] for i in slice_list)) def move_map(map_, edges): @@ -36,56 +25,6 @@ def detect_edges(map_, edges): return slices -def apply_gravity(map_, edges): - start_pos = (sum(edges) / 2, - world_gen['height'] - 1) - - new_blocks = {} - connected_to_ground = explore_map(map_, edges, start_pos, set()) - - for x, slice_ in map_.items(): - if x not in range(*edges): - continue - for y in range(len(slice_)-3, -1, -1): - block = slice_[y] - if (is_solid(block) and (x, y) not in connected_to_ground): - new_blocks.setdefault(x, {}) - new_blocks[x][y] = ' ' - new_blocks[x][y+1] = block - - return new_blocks - - -def explore_map(map_, edges, start_pos, connected_to_ground): - blocks_to_explore = set([start_pos]) - visted_blocks = set() - - while blocks_to_explore: - current_pos = blocks_to_explore.pop() - visted_blocks.add(current_pos) - - if (current_pos[1] >= 0 and current_pos[1] < world_gen['height'] and - current_pos not in connected_to_ground and - current_pos[0] in range(edges[0]-1, edges[1]+1)): - - try: - current_block = map_[current_pos[0]][current_pos[1]] - except (IndexError, KeyError): - current_block = None - - if (current_block is not None and - is_solid(current_block)) or current_pos[0] in (edges[0]-1, edges[1]): - - connected_to_ground.add(current_pos) - for (dx, dy) in ((x, y) for x in (-1, 0, 1) for y in (-1, 0, 1)): - pos = (current_pos[0] + dx, current_pos[1] + dy) - - if pos not in visted_blocks: - blocks_to_explore.add(pos) - - return connected_to_ground - - def spawn_hierarchy(tests): # TODO: Use argument expansion for tests return max(tests, key=lambda block: blocks[block]['hierarchy']) @@ -97,443 +36,3 @@ def is_solid(block): 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'] * 4) + 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(chunk_pos) is None: - -# # Init to empty, so 'no features' is cached. -# feature_cache[chunk_pos] = {} - -# random.seed(str(meta['seed']) + str(chunk_pos) + feature_group_name) -# feature_cache[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(x) is None: - # Init to empty, so 'no features' is cached. - features[x] = {} - - # If it is not None, it has all ready been generated. - if features[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[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): - - # TODO: Each of these `if` blocks should be abstracted into a function - # which just returns the `attrs` object. - - if features.get(x) is None: - # Init to empty, so 'no features' is cached. - features[x] = {} - - # If it is not None, it has all ready been generated. - if features[x].get('hill') is None: - - random.seed(str(meta['seed']) + str(x) + 'hill') - if random.random() <= 0.05: - - 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']) - - features[x]['hill'] = attrs - - -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): - - # TODO: Each of these `if` blocks should be abstracted into a function - # which just returns the `attrs` object. - - if features.get(x) is None: - # Init to empty, so 'no features' is cached. - features[x] = {} - - # If it is not None, it has all ready been generated. - if features[x].get('tree') is None: - - biome_data = world_gen['biomes'][slices_biome[x][0]] - boime_tree_chance = biome_data['trees'] - - random.seed(str(meta['seed']) + str(x) + 'tree') - type_ = random.randint(0, len(world_gen['trees'])-1) - tree_data = world_gen['trees'][type_] - - tree_chance = boime_tree_chance * tree_data['chance'] - - if random.random() <= tree_chance: - - attrs = {} - attrs['type'] = type_ - - leaves = tree_data['leaves'] - - # Centre tree slice (contains trunk) - # TODO: This calculation could be done on start-up, and stored - # with each tree type. - center_leaves = leaves[int(len(leaves) / 2)] - if 1 in center_leaves: - attrs['trunk_depth'] = center_leaves[::-1].index(1) - else: - attrs['trunk_depth'] = len(center_leaves) - - # Get space above ground - air_height = world_gen['height'] - ground_heights[x] - tree_height = air_height - (len(center_leaves) - attrs['trunk_depth']) - tree_height = min(tree_height, tree_data['min_height']) - - attrs['height'] = random.randint(tree_data['min_height'], max(tree_height, 2)) - - features[x]['tree'] = attrs - - -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. - - if features.get(x) is None: - # Init to empty, so 'no features' is cached. - features[x] = {} - - # Ores - # NOTE: Ores seem to be the way to model the generalization of the - # rest of the features after - for name, ore in world_gen['ores'].items(): - feature_name = name + '_ore_root' - - # If it is not None, it has all ready been generated. - if features[x].get(feature_name) is None: - - random.seed(str(meta['seed']) + str(x) + feature_name) - if random.random() <= ore['chance']: - - upper = int(world_gen['height'] * ore['upper']) - lower = int(world_gen['height'] * ore['lower']) - - attrs = {} - attrs['root_height'] = world_gen['height'] - random.randint( - lower, min(upper, (ground_heights[x] - 1)) # -1 for grass. - ) - - # Generates ore at random position around root ore - pot_vain_blocks = ore['vain_size'] ** 2 - - # 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)] - - features[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(x) is None: - # Init to empty, so 'no features' is cached. - features[x] = {} - - # If it is not None, it has all ready been generated. - if features[x].get('grass') is None: - - biome_data = world_gen['biomes'][slices_biome[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[x] - - features[x]['grass'] = attrs - - -def gen_cave_features(features, ground_heights, slices_biome, chunk_pos, meta): - - cave_y_res = 2 # Double the y resolution of the CA to correct for aspect ratio - ca_iterations = 6 - - air_points = set() - air_points_x_min = chunk_pos - ca_iterations - air_points_x_max = chunk_pos + world_gen['chunk_size'] + ca_iterations - - for x in range(air_points_x_min, air_points_x_max): - - # TODO: Each of these `if` blocks should be abstracted into a function - # which just returns the `attrs` object. - - if features.get(x) is None: - # Init to empty, so 'no features' is cached. - features[x] = {} - - # If it is not None, it has all ready been generated. - if features[x].get('cave_initial_air_points') is None: - random.seed(str(meta['seed']) + str(x) + 'cave') - - # Generate air points for this slice - slice_air_points = set() - for y in range(cave_y_res * (ground_heights[x] - 2)): - world_y = world_gen['height'] - (y/cave_y_res) - 2 - - if random.random() < world_gen['cave_chance']: - slice_air_points.add((x, world_y)) - - if slice_air_points: - features[x]['cave_initial_air_points'] = slice_air_points - - # Store slice air points in our local collection of air points for CA generation - if features[x].get('cave_initial_air_points'): - air_points = air_points.union(features[x]['cave_initial_air_points']) - - if features[chunk_pos].get('cave') is None: - # Perform cellular automata - for i in range(ca_iterations): - new_air_points = set() - - for x in range(air_points_x_min, air_points_x_max): - for y in range(cave_y_res * (ground_heights[x] - 2)): - world_y = world_gen['height'] - (y/cave_y_res) - 2 - - n_neighbours = 0 - for dx in (-1, 0, 1): - for dy in (-(1/cave_y_res), 0, (1/cave_y_res)): - if (x + dx, world_y + dy) in air_points: - n_neighbours += 1 - - if n_neighbours < 5: - new_air_points.add((x, world_y)) - - air_points = new_air_points - - features[chunk_pos]['cave'] = air_points - - - -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[x] - for trunk_y in range(air_height - tree_feature['height'], air_height - (bool(DEBUG) * 3)): - chunk[x][trunk_y] = spawn_hierarchy(('|', chunk[x][trunk_y])) - - # Add leaves - leaves = world_gen['trees'][tree_feature['type']]['leaves'] - 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[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[leaf_x][leaf_y] = spawn_hierarchy(('@', chunk[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[x] - 1 - chunk[x][grass_y] = spawn_hierarchy(('v', chunk[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 not in_chunk(block_x, chunk_pos): - continue - - if not world_gen['height'] > block_y > world_gen['height'] - ground_heights[block_x]: - continue - - if chunk[block_x][block_y] is ' ': - continue - - chunk[block_x][block_y] = spawn_hierarchy((ore['char'], chunk[block_x][block_y])) - - -def build_cave(chunk, chunk_pos, x, cave_feature, ground_heights): - """ Adds caves at x to the chunk. """ - - for (world_x, y) in cave_feature: - if in_chunk(world_x, chunk_pos): - chunk[world_x][int(y)] = ' ' - - -def gen_chunk(chunk_n, meta): - chunk_pos = chunk_n * world_gen['chunk_size'] - - # TODO: Allow more than one feature per x in features? - - # First generate all the features we will need - # for all the slice is in this chunk - - 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 = {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 = {x: ('normal', None) for x in range(chunk_pos - world_gen['max_biome'], chunk_pos + world_gen['chunk_size'] + world_gen['max_biome'])} - - for feature_x, slice_features in features.items(): - feature_x = int(feature_x) - - for feature_name, feature in slice_features.items(): - - if feature_name == 'hill': - - 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)) - - if d_x == 0: - hill_height -= 1 - - ground_height = world_gen['ground_height'] + hill_height - - old_height = ground_heights.get(x, 0) - ground_heights[x] = max(ground_height, old_height) - - elif feature_name == 'biome': - - for d_x in range(-feature['radius'], feature['radius']): - x = feature_x + d_x - - if x in slices_biome: - previous_slice_biome_feature_x = slices_biome[x][1] - - if (previous_slice_biome_feature_x is None or - previous_slice_biome_feature_x < feature_x): - slices_biome[x] = (feature['type'], feature_x) - - chunk = {} - for x in range(chunk_pos, chunk_pos + world_gen['chunk_size']): - chunk[x] = ( - [' '] * (world_gen['height'] - ground_heights[x]) + - ['-'] + - ['#'] * (ground_heights[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_cave_features(features, ground_heights, slices_biome, chunk_pos, meta) - 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) - - elif feature_name == 'cave': - build_cave(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 - - return chunk, {x: s for x, s in ground_heights.items() if x in range(chunk_pos, chunk_pos+world_gen['chunk_size'])} diff --git a/terrain_gen.py b/terrain_gen.py new file mode 100644 index 0000000..ea88cc9 --- /dev/null +++ b/terrain_gen.py @@ -0,0 +1,349 @@ +import random, math +from collections import OrderedDict +from math import ceil, cos, sin, radians, atan2 + +from data import world_gen, blocks +from console import log, DEBUG +from perlin import perlin2 + + +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)) + +MAX_HILL_RAD = world_gen['max_hill'] * world_gen['min_grad'] +MAX_HILL_CHUNKS = ceil(MAX_HILL_RAD / world_gen['chunk_size']) + +MAX_BIOME_RAD = world_gen['max_biome'] +MAX_BIOME_CHUNKS = ceil(MAX_BIOME_RAD / world_gen['chunk_size']) + +flatten = lambda l: [item for sublist in l for item in sublist] +biomes_population = sorted(flatten([name] * int(data['chance'] * 100) for name, data in world_gen['biomes'].items())) + + +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) + + +FEATURES = TerrainCache(limit=(max(MAX_HILL_RAD, MAX_BIOME_RAD) * 4) + world_gen['chunk_size']) + + +def gen_biome_chunk_features(chunk_pos, chunk_features_in_range, seed): + chunk_biomes = [] + for x in range(chunk_pos, chunk_pos + world_gen['chunk_size']): + + if random.random() <= 0.05: + + slice_biome = {} + slice_biome['x'] = x + slice_biome['type'] = random.choice(biomes_population) + slice_biome['radius'] = random.randint(world_gen['min_biome'], world_gen['max_biome']) + + # features[x]['biome'] = slice_biome + + chunk_biomes.append(slice_biome) + + return chunk_biomes + + +def gen_hill_chunk_features(chunk_pos, chunk_features_in_range, seed): + chunk_hills = [] + for x in range(chunk_pos, chunk_pos + world_gen['chunk_size']): + + if random.random() <= 0.05: + + slice_hill = {} + slice_hill['x'] = x + slice_hill['gradient_l'] = random.randint(1, world_gen['min_grad']) + slice_hill['gradient_r'] = random.randint(1, world_gen['min_grad']) + slice_hill['height'] = random.randint(0, world_gen['max_hill']) + + chunk_hills.append(slice_hill) + + return chunk_hills + + +def gen_biome_slice_features(x, chunk_features_in_range, slice_features, seed): + + slice_biome = ('normal', None) + slice_biome_distance_to_centre = float('inf') + + if chunk_features_in_range.get('biomes') is not None: + for biome in chunk_features_in_range.get('biomes'): + if (biome['x'] - biome['radius'] <= x and + biome['x'] + biome['radius'] > x and + abs(biome['x'] - x) < slice_biome_distance_to_centre): + + slice_biome_distance_to_centre = abs(biome['x'] - x) + slice_biome = (biome['type'], biome['x']) + + return slice_biome + + +def gen_ground_height_slice_features(x, chunk_features_in_range, slice_features, seed): + + ground_height = world_gen['ground_height'] + + hills_in_range = (hill for chunk_pos, fs in chunk_features_in_range.items() if fs.get('hills') for hill in fs['hills']) + + for hill in hills_in_range: + hill_dist = x - hill['x'] + + if hill_dist == 0: + gradient = 1 + elif hill_dist > 0: + gradient = hill['gradient_r'] + else: + gradient = hill['gradient_l'] + + hill_height = int(hill['height'] - (abs(hill_dist) / gradient)) + new_ground_height = world_gen['ground_height'] + hill_height + + ground_height = max(ground_height, new_ground_height) + + return ground_height + + +def gen_ground_height_slice_features_perlin(x, chunk_features_in_range, slice_features, seed): + + ground_height = world_gen['ground_height'] + ground_height += int(30*perlin2(x/100, 0)) + ground_height += int(round(2.5*perlin2(x/4, 0))) + + return min(max(1, ground_height), world_gen['height']) + + +def gen_tree_features(x, chunk_features_in_range, slice_features, seed): + + biome = slice_features.get('slice_biome') + ground_height = slice_features.get('ground_height') + + biome_data = world_gen['biomes'][biome[0]] + boime_tree_chance = biome_data['trees'] + + tree_type = random.randint(0, len(world_gen['trees'])-1) + tree_data = world_gen['trees'][tree_type] + + tree_chance = boime_tree_chance * tree_data['chance'] + + if random.random() <= tree_chance: + + tree_feature = {} + tree_feature['type'] = tree_type + + leaves = tree_data['leaves'] + + # Centre tree slice (contains trunk) + # TODO: This calculation could be done on start-up, and stored + # with each tree type. + center_leaves = leaves[int(len(leaves) / 2)] + if 1 in center_leaves: + tree_feature['trunk_depth'] = center_leaves[::-1].index(1) + else: + tree_feature['trunk_depth'] = len(center_leaves) + + # Get space above ground + air_height = world_gen['height'] - ground_height + tree_height = air_height - (len(center_leaves) - tree_feature['trunk_depth']) + tree_height = min(tree_height, tree_data['min_height']) + + # Gausian tree height distrubution + min_, max_ = 2, air_height - (len(center_leaves) - tree_feature['trunk_depth']) + mean_height = (min_ + tree_data['mean_height']) + max_std_dev = tree_data['max_std_dev'] + tree_feature['height'] = int(min(max(min_, random.gauss(mean_height, max_std_dev)), max_)) + + return tree_feature + + +def gen_ore_features(x, chunk_features_in_range, slice_features, seed): + + ground_height = slice_features.get('ground_height') + + ore_features = {} + + for ore_name, ore in world_gen['ores'].items(): + + random.seed(seed + ore_name) + if random.random() <= ore['chance']: + + upper = int(world_gen['height'] * ore['upper']) + lower = int(world_gen['height'] * ore['lower']) + + ore_feature = {} + ore_feature['root_height'] = world_gen['height'] - random.randint( + lower, min(upper, (ground_height - 1)) # -1 for grass. + ) + + # Generates ore at random position around root ore + pot_vain_blocks = ore['vain_size'] ** 2 + + # Describes the shape of the vain, + # top to bottom, left to right. + ore_feature['vain_shape'] = [b / 100 for b in random.sample(range(0, 100), pot_vain_blocks)] + + ore_features[ore_name] = ore_feature + + return ore_features + + +def gen_grass_features(x, chunk_features_in_range, slice_features, seed): + + biome = slice_features.get('slice_biome') + ground_height = slice_features.get('ground_height') + + biome_data = world_gen['biomes'][biome[0]] + grass_chance = biome_data['grass'] + + if random.random() <= grass_chance: + + grass_feature = {} + grass_feature['y'] = ground_height + + return grass_feature + + +def gen_cave_features(features, chunk_pos, meta): + + cave_y_res = 2 # Double the y resolution of the CA to correct for aspect ratio + ca_iterations = 6 + + air_points = set() + air_points_x_min = chunk_pos - ca_iterations + air_points_x_max = chunk_pos + world_gen['chunk_size'] + ca_iterations + + for x in range(air_points_x_min, air_points_x_max): + + # TODO: Each of these `if` blocks should be abstracted into a function + # which just returns the `attrs` object. + + if features.get(x) is None: + # Init to empty, so 'no features' is cached. + features[x] = {} + + # If it is not None, it has all ready been generated. + if features[x].get('cave_initial_air_points') is None: + random.seed(str(meta['seed']) + str(x) + 'cave') + + # Generate air points for this slice + slice_air_points = set() + for y in range(cave_y_res * (features[x]['ground_height'] - 2)): + world_y = world_gen['height'] - (y/cave_y_res) - 2 + + if random.random() < world_gen['cave_chance']: + slice_air_points.add((x, world_y)) + + if slice_air_points: + features[x]['cave_initial_air_points'] = slice_air_points + + # Store slice air points in our local collection of air points for CA generation + if features[x].get('cave_initial_air_points'): + air_points = air_points.union(features[x]['cave_initial_air_points']) + + if features[chunk_pos].get('cave') is None: + # Perform cellular automata + for _ in range(ca_iterations): + new_air_points = set() + + for x in range(air_points_x_min, air_points_x_max): + for y in range(cave_y_res * (features[x]['ground_height'] - 2)): + world_y = world_gen['height'] - (y/cave_y_res) - 2 + + n_neighbours = 0 + for dx in (-1, 0, 1): + for dy in (-(1/cave_y_res), 0, (1/cave_y_res)): + if (x + dx, world_y + dy) in air_points: + n_neighbours += 1 + + if n_neighbours < 5: + new_air_points.add((x, world_y)) + + air_points = new_air_points + + features[chunk_pos]['cave'] = air_points + + +def generate_slice_features(features, chunk_pos, meta, feature_generator, feature_name, feature_buffer): + + log('generate_slice_features range', chunk_pos - feature_buffer[0], chunk_pos + world_gen['chunk_size'] + feature_buffer[1], m=1) + + # Generate dictionary of chunk-features (features on chunk positions) within the feature_buffer range. + feature_buffer_chunk = (ceil(feature_buffer[0] / world_gen['chunk_size']) * world_gen['chunk_size'], + ceil(feature_buffer[1] / world_gen['chunk_size']) * world_gen['chunk_size']) + feature_buffer_range = range(chunk_pos - feature_buffer_chunk[0], + chunk_pos + world_gen['chunk_size'] + feature_buffer_chunk[1]) + chunk_features_in_range = {chunk: features[chunk] for chunk in feature_buffer_range if features.get(chunk)} + + for x in range(chunk_pos - feature_buffer[0], chunk_pos + world_gen['chunk_size'] + feature_buffer[1]): + + if features.get(x) is None: + # Init to empty, so 'no features' is cached. + features[x] = {} + + # If it is not None, it has all ready been generated. + if features[x].get(feature_name) is None: + + seed = str(meta['seed']) + str(x) + feature_name + random.seed(seed) + + feature = feature_generator(x, chunk_features_in_range, features[x], seed) + if feature is not None: + features[x][feature_name] = feature + + +def generate_chunk_features(features, chunk_n, meta, feature_generator, feature_name, feature_buffer): + for chunk_n_gen in range(chunk_n - feature_buffer[0], chunk_n + feature_buffer[1] + 1): + chunk_pos = chunk_n_gen * world_gen['chunk_size'] + + if features.get(chunk_pos) is None: + # Init to empty, so 'no features' is cached. + features[chunk_pos] = {} + + # If it is not None, it has all ready been generated. + if features[chunk_pos].get(feature_name) is None: + + seed = str(meta['seed']) + str(chunk_pos) + feature_name + random.seed(seed) + + feature = feature_generator(chunk_pos, features[chunk_pos], seed) + if feature is not None: + features[chunk_pos][feature_name] = feature + + +def gen_chunk_features(chunk_n, meta): + log('', m=1) + + chunk_pos = chunk_n * world_gen['chunk_size'] + log('chunk', chunk_pos, m=1) + + generate_chunk_features(FEATURES, chunk_n, meta, gen_hill_chunk_features, 'hills', (MAX_HILL_CHUNKS, MAX_HILL_CHUNKS)) + generate_slice_features(FEATURES, chunk_pos, meta, gen_ground_height_slice_features, 'ground_height', (MAX_HILL_RAD, MAX_HILL_RAD)) + + log('missing ground heights', set(range(chunk_pos - MAX_HILL_RAD, chunk_pos + world_gen['chunk_size'] + MAX_HILL_RAD)) - set(pos for pos, fs in FEATURES.items() if fs.get('ground_height')), m=1, trunc=False) + + generate_chunk_features(FEATURES, chunk_n, meta, gen_biome_chunk_features, 'biomes', (MAX_BIOME_CHUNKS, MAX_BIOME_CHUNKS)) + generate_slice_features(FEATURES, chunk_pos, meta, gen_biome_slice_features, 'slice_biome', (MAX_BIOME_RAD, MAX_BIOME_RAD)) + + gen_cave_features(FEATURES, chunk_pos, meta) + + generate_slice_features(FEATURES, chunk_pos, meta, gen_tree_features, 'tree', (MAX_HALF_TREE, MAX_HALF_TREE)) + generate_slice_features(FEATURES, chunk_pos, meta, gen_grass_features, 'grass', (0, 0)) + generate_slice_features(FEATURES, chunk_pos, meta, gen_ore_features, 'ores', MAX_ORE_RANGE) + + return FEATURES diff --git a/translate_data.py b/translate_data.py index ed6da4b..68124af 100644 --- a/translate_data.py +++ b/translate_data.py @@ -72,4 +72,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/ui.py b/ui.py index c8f9e08..0fe36dc 100644 --- a/ui.py +++ b/ui.py @@ -1,7 +1,6 @@ from nbinput import BlockingInput, UP, DOWN, RIGHT, LEFT from console import CLS, REDRAW, WIDTH, HEIGHT, SHOW_CUR, HIDE_CUR -from colours import * -from console import * +from colours import colour_str, TERM_YELLOW, TERM_RED, BOLD from data import help_data import saves