diff --git a/server/bin/gen_hills b/server/bin/gen_hills new file mode 100755 index 0000000..1d11b06 --- /dev/null +++ b/server/bin/gen_hills @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +import argparse +import json +import numpy as np + + +parser = argparse.ArgumentParser() +parser.add_argument("--seed", default=1, type=int, help="Random seed.") +parser.add_argument("--threshold", default=0.4, type=float, help="Higher threshold = less hills.") +parser.add_argument("--safezone", default=3.0, type=float, help="Size of hill-free area around home bases.") +parser.add_argument("--size", default=(30, 30), type=int, nargs=2, help="Width and height for team.") +parser.add_argument("--res", default=(10, 10), type=int, nargs=2, help="Higher res = more smaller hills. Must divide size.") + + +def main(): + np.random.seed(args.seed) + # base noise profile + noise = generate_perlin_noise_2d( + args.size, + args.res, + tileable=(True, True), + ) + + # make it less likely for hills to be close to home bases + home_distance_tile = np.fromfunction(distance_decay, args.size, dtype=np.float32) + home_distance = np.sum([ + home_distance_tile, + np.flip(home_distance_tile, axis=0), + np.flip(home_distance_tile, axis=1), + np.flip(home_distance_tile, axis=(0,1)), + ], axis=0) + + hills = np.where(noise - home_distance > args.threshold, "x", ".") + rows = ["".join(row) for row in hills] + config = { + "width_per_team": args.size[0], + "height_per_team": args.size[1], + "hills": rows + } + print(json.dumps(config, indent=4)) + + +def distance_decay(x, y): + return np.exp(-np.sqrt(x*x + y*y) / args.safezone) + + +# Source: +# https://github.com/pvigier/perlin-numpy/blob/master/perlin_numpy/perlin2d.py + +def interpolant(t): + return t*t*t*(t*(t*6 - 15) + 10) + + +def generate_perlin_noise_2d( + shape, res, tileable=(False, False), interpolant=interpolant +): + """Generate a 2D numpy array of perlin noise. + Args: + shape: The shape of the generated array (tuple of two ints). + This must be a multple of res. + res: The number of periods of noise to generate along each + axis (tuple of two ints). Note shape must be a multiple of + res. + tileable: If the noise should be tileable along each axis + (tuple of two bools). Defaults to (False, False). + interpolant: The interpolation function, defaults to + t*t*t*(t*(t*6 - 15) + 10). + Returns: + A numpy array of shape shape with the generated noise. + Raises: + ValueError: If shape is not a multiple of res. + """ + delta = (res[0] / shape[0], res[1] / shape[1]) + d = (shape[0] // res[0], shape[1] // res[1]) + grid = np.mgrid[0:res[0]:delta[0], 0:res[1]:delta[1]]\ + .transpose(1, 2, 0) % 1 + # Gradients + angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1) + gradients = np.dstack((np.cos(angles), np.sin(angles))) + if tileable[0]: + gradients[-1, :] = gradients[0, :] + if tileable[1]: + gradients[:, -1] = gradients[:, 0] + gradients = gradients.repeat(d[0], 0).repeat(d[1], 1) + g00 = gradients[:-d[0], :-d[1]] + g10 = gradients[d[0]:, :-d[1]] + g01 = gradients[:-d[0], d[1]:] + g11 = gradients[d[0]:, d[1]:] + # Ramps + n00 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1])) * g00, 2) + n10 = np.sum(np.dstack((grid[:, :, 0]-1, grid[:, :, 1])) * g10, 2) + n01 = np.sum(np.dstack((grid[:, :, 0], grid[:, :, 1]-1)) * g01, 2) + n11 = np.sum(np.dstack((grid[:, :, 0]-1, grid[:, :, 1]-1)) * g11, 2) + # Interpolation + t = interpolant(grid) + n0 = n00*(1-t[:, :, 0]) + t[:, :, 0]*n10 + n1 = n01*(1-t[:, :, 0]) + t[:, :, 0]*n11 + return np.sqrt(2)*((1-t[:, :, 1])*n0 + t[:, :, 1]*n1) + + +if __name__ == '__main__': + args = parser.parse_args([] if "__file__" not in globals() else None) + main() diff --git a/server/constraints.txt b/server/constraints.txt index e5bacea..6b2a165 100644 --- a/server/constraints.txt +++ b/server/constraints.txt @@ -16,6 +16,7 @@ lxml==4.9.1 MarkupSafe==2.1.1 mypy==0.971 mypy-extensions==0.4.3 +numpy==1.23.3 packaging==21.3 pikepdf==5.6.1 Pillow==9.2.0 diff --git a/server/setup.py b/server/setup.py index 537f88d..e785681 100644 --- a/server/setup.py +++ b/server/setup.py @@ -27,6 +27,7 @@ setuptools.setup( 'dateutils', 'flask_bootstrap', 'flask_sqlalchemy', + 'numpy', 'pikepdf', 'pillow', 'psycopg2',