<file_sep># 2D Procedural Terrain

Procedural generation algorithms that can be used for terrain generation. Plan is to save generated maps (grids) into files
that can be read by C# code on Unity to generate terrain according to this maps.

Why not to make the algorithms on C# directly? a) I've already done it, you can check the repo, so there's nothing new for me there.
b) I'm trying to explore on how to communicate Python and C# scripts to work together, and this is a good way to do that.
<file_sep>

def if_assign(condition, if_true, if_false):
	"""Method for conditional assignments.
	If condition is true returns if_true,
	else returns if_false. Saves lines and ugly if-else use."""

	return if_true if condition else if_false
<file_sep>

class _ProceduralGenerationError(Exception):
	"""Base class for all procedural generation errors."""

	def __init__(self):
		pass


class InvalidRandomnessFactor (_ProceduralGenerationError):

	def __init__(self, factor):
		_ProceduralGenerationError.__init__(self)
		self.factor = factor

	def __str__(self):
		str = """
		The value {} is invalid as a randomness factor. Choose a value between 0 and 100 (inclusive)
		to determine how probable it is for a random variation to happen.""".format(self.factor)

		return str<file_sep>import random
import enum
import matplotlib.pyplot as plt
import json
from misc import if_assign
from exceptions import InvalidRandomnessFactor


class Symbols (enum.Enum):
	Void = ' '
	Floor = 'T'
	Chest = 'x'
	Water = 's'


class YVector (enum.Enum):
	Up = 1
	Down = -1


class Algorithms(enum.Enum):
	RandomWalkTop = enum.auto()


class Map:
	"""This class defines a grid through abstract information,
	i.e. the coordinates that form it and a default value of false
	on each coordinate."""

	def __init__(self, width, height, empty=False):
		self.width = width
		self.height = height
		self.base = self.generate(empty)
		self.elevations = self.generate(empty)

	def generate(self, empty):
		"""Generate the grid, a set of points with true or false values
		assigned to them."""
		grid = {}

		for x in range(self.width):
			for y in range(self.height):
				grid[(x, y)] = if_assign(empty is False, 1, 0)

		return grid

	def __str__(self):

		coordinates = list(filter(lambda x: self.base[x] == 1, self.base.keys()))
		coordinates = sorted(coordinates, key=lambda cor: cor[0])

		elv = list(filter(lambda x: self.elevations[x] == 1, self.elevations.keys()))
		elv = sorted(elv, key=lambda cor: cor[0])

		base_x = list(map(lambda x: x[0], coordinates))
		base_y = list(map(lambda y: y[1], coordinates))

		elv_x = list(map(lambda x: x[0], elv))
		elv_y = list(map(lambda y: y[1], elv))

		plt.plot(base_x, base_y)
		plt.plot(elv_x, elv_y, 'g-')
		plt.show()

		return ''


class ProceduralGeneration:
	"""This class takes a Map, i.e. a set of
	coordinates with default values assigned to each point,
	and pseudo-randomly assigns true values to some points of the grid.
	Points that equal 1 will be drawn, while points that equal 0 are empty."""

	def __init__(self, width, height, randomness=50):
		if randomness < 0 or randomness > 100:
			raise InvalidRandomnessFactor(randomness)
		self.width = width
		self.height = height
		self.starting_height = random.randint(0, int(self.height) / 2)
		self.randomness = randomness
		self.map = Map(width, height, True)

	def modify_height (self, cur_height, chance = 50):
		"""Given a current height value, decide whether to
		change it given a certain chance."""

		if random.randint(0, 100) <= chance:
			cur_height += YVector.Up.value
			return cur_height

		cur_height += YVector.Down.value
		return cur_height

	def random_terrain(self):
		"""Generate a completely random terrain.
		Each point of the grid randomly receives an empty or filled value,
		with completely random fluctuations on the height of the terrain."""

		grid = self.map.base
		cur_height = self.starting_height

		for x in range(self.width):
			choice = random.randint(0, 100)
			if choice <= self.randomness:
				dir = random.randint(0, 2)
				if dir == 0 and cur_height - 1 >= 0:
					cur_height -= 1
				elif dir == 1 and cur_height + 1 <= self.height:
					cur_height += 1
			grid[(x, cur_height)] = 1

		self.map.base = grid

	def random_walk_top(self, x_interval):
		"""Generate a pseudo-random terrain.
		A set of continuous intervals on the x direction will be drawn,
		with height variations between them."""

		grid = self.map.base
		cur_height = self.starting_height
		can_change_height = False
		interval_length = 0

		for x in range(self.width):
			interval_length += 1
			if interval_length == x_interval:
				can_change_height = True
			if can_change_height and random.randint(0, 100) <= self.randomness:
				cur_height = self.modify_height(cur_height)
				can_change_height = False
				interval_length = 0
			grid[(x, cur_height)] = 1

		self.map.base = grid

	def random_elevations(self, width, interval, altitude, amplitude, random_factor):
		"""Creates random elevated pieces of terrain. Each piece is separated
		from the other by a horizontal distance of interval; its horizontal size is
		width and its vertical size is amplitude. Altitude is how many units away
		from the floor will the pieces be placed."""

		if random.randint(0, 100) <= random_factor:
			amplitude += random.randint(-3, 3)
		if random.randint(0, 100) <= random_factor:
			altitude += random.randint(-3, 3)
		if random.randint(0, 100) <= random_factor:
			width += random.randint(-3, 3)
		if random.randint(0, 100) <= random_factor:
			interval += random.randint(-3, 3)

		grid = self.map.elevations
		y_pos = self.starting_height
		finding_pos = False
		y_displacement = 0
		last_x = 0

		# For every tile on the x axis,
		for x in range(self.map.width):

			# check if we have move interval tiles to the right.
			if x >= interval + last_x:
				# If we did, start looking for a proper elevation position.
				finding_pos = True

			if finding_pos is True:

				# Moving upwards, loop through every y point,
				# from start point of terrain to end.
				for y in range(y_pos + 1, self.map.height):

					# If this point is occupied or filled, move another x to the right.
					if self.map.base[(x, y)] is 1:
						y_displacement = 0
						break

					# If the point isn't occupied, and we are at the
					# proper altitude, generate an elevation there.
					if y_displacement >= altitude:
						# Generate elevation.
						for i in range(0, width):
							x_pos = x + i
							y_pos = if_assign(random.randint(0, 100) <= random_factor, y + random.randint(-1, 1), y)
							grid[(x_pos, y_pos)] = 1
							for a in range(0, amplitude):
								grid[(x_pos, y_pos + a)] = 1

						last_x = x + width
						y_displacement, x_displacement = 0, 0
						finding_pos = False
						break

					# If none of the conditions have been met,
					# and we are not at the desired altitude yet,
					# add 1 to the y displacement.
					if y_displacement < altitude:
						y_displacement += 1

					# Start filling

		self.map.elevations = grid

	def pseudo_random(self, algorithm, x_interval):

		if algorithm is Algorithms.RandomWalkTop:
			self.random_walk_top(x_interval)


class GridDataManager:
	"""Class working as a placeholder for static methods
	involving saving and loading generated map data."""

	@staticmethod
	def save_coordinates (map):
		"""Dump grid's points and values into a data file."""

		data = {'points': list(map.base.keys()),
				'values': list(map.base.values())}

		with open('data.txt', 'w') as outfile:
			json.dump(data, outfile)


PG = ProceduralGeneration(100, 100, 50)
PG.pseudo_random(Algorithms.RandomWalkTop, 5)
PG.random_elevations(10, 5, 20, 5, 50)
GridDataManager.save_coordinates(PG.map)
print(PG.map)
