Index: framspy/FramsticksLib.py
===================================================================
--- framspy/FramsticksLib.py	(revision 1204)
+++ framspy/FramsticksLib.py	(revision 1218)
@@ -59,9 +59,7 @@
 		frams.Simulator.expdef = "standard-eval"  # this expdef (or fully compatible) must be used by EVALUATION_SETTINGS_FILE
 		if sim_settings_files is not None:
-			self.EVALUATION_SETTINGS_FILE = sim_settings_files.split(";")  # overwrite defaults
+			self.EVALUATION_SETTINGS_FILE = sim_settings_files.split(";")  # override defaults. str becomes list
 		print('Basic tests OK. Using settings:', self.EVALUATION_SETTINGS_FILE)
 		print()
-		if not isinstance(self.EVALUATION_SETTINGS_FILE, list):  # ensure settings file(s) are provided as a list
-			raise ValueError("Evaluation settings file(s) '%s' should be provided as a list" % self.EVALUATION_SETTINGS_FILE)
 
 		for simfile in self.EVALUATION_SETTINGS_FILE:
@@ -112,5 +110,5 @@
 			if ec.error_count._value() > 0:
 				print(ec.messages)  # if errors occurred, output all caught messages for debugging
-				raise RuntimeError("[ERROR] %d error(s) and %d warning(s) while evaluating %d genotype(s)" % (ec.error_count._value(), ec.warning_count._value() - ec.error_count._value(), len(genotype_list)))  # make errors fatal; by default they stop the simulation anyway so let's not use potentially incorrect or partial results and fix the cause first.
+				raise RuntimeError("[ERROR] %d error(s) and %d warning(s) while evaluating %d genotype(s)" % (ec.error_count._value(), ec.warning_count._value(), len(genotype_list)))  # make errors fatal; by default they stop the simulation anyway so let's not use potentially incorrect or partial results and fix the cause first.
 
 		results = []
@@ -198,4 +196,56 @@
 
 
+	def getRandomGenotype(self, initial_genotype: str, parts_min: int, parts_max: int, neurons_min: int, neurons_max: int, iter_max: int, return_even_if_failed: bool):
+		"""
+		Some algorithms require a "random solution". To this end, this method generates a random framstick genotype.
+
+		:param initial_genotype: if not a specific genotype (which could facilitate greater variability of returned genotypes), try `getSimplest(format)`.
+		:param iter_max: how many mutations can be used to generate a random genotype that fullfills target numbers of parts and neurons.
+		:param return_even_if_failed: if the target numbers of parts and neurons was not achieved, return the closest genotype that was found? Set it to False first to experimentally adjust `iter_max` so that in most calls this function returns a genotype with target numbers of parts and neurons, and then you can set this parameter to True if target numbers of parts and neurons are not absolutely required.
+		:returns: a valid genotype or None if failed and `return_even_if_failed` is False.
+		"""
+
+
+		def estimate_diff(g: str):
+			if not self.isValidCreature([g])[0]:
+				return None, None
+			m = frams.Model.newFromString(g)
+			numparts = m.numparts._value()
+			numneurons = m.numneurons._value()
+			diff_parts = abs(target_parts - numparts)
+			diff_neurons = abs(target_neurons - numneurons)
+			in_target_range = (parts_min <= numparts <= parts_max) and (neurons_min <= numneurons <= neurons_max)  # less demanding than precisely reaching target_parts and target_neurons
+			return diff_parts + diff_neurons, in_target_range
+
+
+		# try to find a genotype that matches the number of parts and neurons randomly selected from the provided min..max range
+		# (even if we fail to match this precise target, the goal will be achieved if the found genotype manages to be within min..max ranges for parts and neurons)
+		target_parts = np.random.default_rng().integers(parts_min, parts_max + 1)
+		target_neurons = np.random.default_rng().integers(neurons_min, neurons_max + 1)
+
+		if not self.isValidCreature([initial_genotype])[0]:
+			raise ValueError("Initial genotype '%s' is invalid" % initial_genotype)
+
+		g = initial_genotype
+		for i in range(iter_max // 2):  # a sequence of iter_max/2 undirected mutations starting from initial_genotype
+			g_new = self.mutate([g])[0]
+			if self.isValidCreature([g_new])[0]:  # valid mutation
+				g = g_new
+
+		best_diff, best_in_target_range = estimate_diff(g)
+		for i in range(iter_max // 2):  # a sequence of iter_max/2 mutations, only accepting those which approach target numbers of parts and neurons
+			g_new = self.mutate([g])[0]
+			diff, in_target_range = estimate_diff(g_new)
+			if diff is not None and diff <= best_diff:  # valid mutation and better or as good as current
+				g = g_new
+				best_diff = diff
+				best_in_target_range = in_target_range
+		# print(diff, best_diff) # print progress approaching target numbers of parts and neurons
+
+		if best_in_target_range or return_even_if_failed:
+			return g  # best found so far (closest to target numbers of parts and neurons)
+		return None
+
+
 	def isValid(self, genotype_list: List[str]) -> List[bool]:
 		"""
@@ -206,4 +256,33 @@
 		for g in genotype_list:
 			valid.append(frams.Geno.newFromString(g).is_valid._int() == 1)
+		if len(genotype_list) != len(valid):
+			raise RuntimeError("Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid)))
+		return valid
+
+
+	def isValidCreature(self, genotype_list: List[str]) -> List[bool]:
+		"""
+		:returns: validity of the genotype when revived. Apart from genetic validity, this includes detecting problems that may arise when building a Creature from Genotype, such as multiple muscles of the same type in the same location in body, e.g. 'X[@][@]'.
+		"""
+
+		# Genetic validity and simulator validity are two separate properties (in particular, genetic validity check is implemented by the author of a given genetic format and operators).
+		# Thus, the subset of genotypes valid genetically and valid in simulation may be overlapping.
+		# For example, 'X[]' or 'Xr' are considered invalid by the genetic checker, but the f1->f0 converter will ignore meaningless genes and produce a valid f0 genotype.
+		# On the other hand, 'X[@][@]' or 'X[|][|]' are valid genetically, but not possible to simulate.
+		# For simplicity of usage (so that one does not need to check both properties separately using both functions), let's make one validity a subset of the other.
+		# The genetic check in the first lines of the "for" loop makes this function at least as demanding as isValid().
+
+		assert isinstance(genotype_list, list)  # because in python, str has similar capabilities as list and here it would pretend to work too, so to avoid any ambiguity
+
+		pop = frams.Populations[0]  # assuming rules from population #0 (self-colision settings are population-dependent and can influence creature build success/failure)
+
+		valid = []
+		for g in genotype_list:
+			if frams.Geno.newFromString(g).is_valid._int() != 1:
+				valid.append(False)  # invalid according to genetic check
+			else:
+				can_add = pop.canAdd(g, 1, 1)  # First "1" means to treat warnings during build as build failures - this allows detecting problems when building Creature from Genotype. Second "1" means mute emitted errors, warnings, messages. Returns 1 (ok, could add) or 0 (there were some problems building Creature from Genotype)
+				valid.append(can_add._int() == 1)
+
 		if len(genotype_list) != len(valid):
 			raise RuntimeError("Tested %d genotypes, received %d validity values" % (len(genotype_list), len(valid)))
@@ -253,2 +332,4 @@
 	print('\tPerformance of Offspring:', framsLib.evaluate([offspring]))
 	print('\tValidity (genetic) of Parent1, Parent 2, and Offspring:', framsLib.isValid([parent1, parent2, offspring]))
+	print('\tValidity (simulation) of Parent1, Parent 2, and Offspring:', framsLib.isValidCreature([parent1, parent2, offspring]))
+	print('\tRandom genotype:', framsLib.getRandomGenotype(simplest, 2, 6, 2, 4, 100, True))
