Genetics: GenomeTemplate (per-organism gene allotment) + breed override
CI / build-test (push) Successful in 1m17s

Adds the species/organism layer on top of the gene foundation.
GenomeTemplate carries which GeneDefs an individual has plus the
per-organism base value and spread its alleles are drawn around (and an
optional discrete-variant override), so the same shared GeneDef expresses
different centres for different species — Generate() draws an individual,
Registry() feeds breeding and trait computation.

Genome.Breed gains an optional mutationChance that overrides every gene's
fixed MutationChance, so a caller can drive mutation from an evolvable
trait. Allele sampling (numeric spread+clamp, weighted discrete pick) is
factored into a shared GeneSampling used by both Generate paths.

Covered by GenomeTemplateTests (per-species centres, registry-driven
breeding, mutation override on/off).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 23:51:02 +03:00
co-authored by Claude Opus 4.8
parent bc18db5df6
commit ead22517ee
4 changed files with 240 additions and 51 deletions
@@ -0,0 +1,56 @@
namespace MrGameEng.Genetics;
/// <summary>
/// Shared, deterministic allele sampling used by both <see cref="Genome.Generate"/> (gene defaults)
/// and <see cref="GenomeTemplate"/> (per-individual base overrides). Centralizes the numeric
/// spread+clamp and the weighted discrete pick so the two paths stay consistent.
/// </summary>
internal static class GeneSampling
{
/// <summary>A numeric allele drawn as <c>baseValue ± spread·|baseValue|</c>, clamped to the gene's range.</summary>
public static float Numeric(GeneDef gene, float baseValue, float spread, Random random)
{
var value = baseValue + (random.NextSingle() * 2f - 1f) * spread * MathF.Abs(baseValue);
return Math.Clamp(value, gene.Min, gene.Max);
}
/// <summary>
/// A discrete variant index in <c>[0, Variants)</c>, picked from <paramref name="weights"/> when
/// they match the variant count, otherwise the gene's own weights, otherwise uniformly.
/// </summary>
public static float Variant(GeneDef gene, float[]? weights, Random random)
{
var variants = Math.Max(1, gene.Variants);
var w =
weights is { Length: > 0 } && weights.Length == variants
? weights
: gene.VariantWeights;
if (w.Length != variants)
{
return random.Next(variants);
}
var total = 0f;
foreach (var value in w)
{
total += MathF.Max(0f, value);
}
if (total <= 0f)
{
return random.Next(variants);
}
var roll = random.NextSingle() * total;
for (var i = 0; i < variants; i++)
{
roll -= MathF.Max(0f, w[i]);
if (roll < 0f)
{
return i;
}
}
return variants - 1;
}
}
+17 -51
View File
@@ -77,12 +77,16 @@ public sealed class Genome
/// carries, it is inherited (from that parent, on both sides) with 50% probability. Every
/// inherited allele may then mutate per its <see cref="GeneDef"/>. <paramref name="registry"/>
/// supplies the def for each gene id; genes absent from it are skipped.
/// <paramref name="mutationChance"/>, when given, overrides every gene's
/// <see cref="GeneDef.MutationChance"/> — letting the caller drive mutation from an evolvable
/// trait rather than a fixed per-gene constant.
/// </summary>
public static Genome Breed(
Genome a,
Genome b,
IReadOnlyDictionary<string, GeneDef> registry,
Random random
Random random,
float? mutationChance = null
)
{
var child = new Genome();
@@ -93,21 +97,22 @@ public sealed class Genome
continue;
}
var chance = mutationChance ?? gene.MutationChance;
var inA = a.Has(geneId);
var inB = b.Has(geneId);
if (inA && inB)
{
child._alleles[geneId] = new Allele(
Meiosis(gene, a[geneId], random),
Meiosis(gene, b[geneId], random)
Meiosis(gene, a[geneId], chance, random),
Meiosis(gene, b[geneId], chance, random)
);
}
else if (random.NextSingle() < 0.5f)
{
var parent = inA ? a : b;
child._alleles[geneId] = new Allele(
Meiosis(gene, parent[geneId], random),
Meiosis(gene, parent[geneId], random)
Meiosis(gene, parent[geneId], chance, random),
Meiosis(gene, parent[geneId], chance, random)
);
}
}
@@ -116,17 +121,17 @@ public sealed class Genome
}
// One inherited allele: pick one of the parent slot's two alleles, then maybe mutate it.
private static float Meiosis(GeneDef gene, Allele parent, Random random)
private static float Meiosis(GeneDef gene, Allele parent, float mutationChance, Random random)
{
var inherited = random.NextSingle() < 0.5f ? parent.A : parent.B;
if (random.NextSingle() >= gene.MutationChance)
if (random.NextSingle() >= mutationChance)
{
return inherited;
}
if (gene.Kind == GeneKind.Discrete)
{
return PickVariant(gene, random);
return GeneSampling.Variant(gene, null, random);
}
var shifted =
@@ -135,49 +140,10 @@ public sealed class Genome
return Math.Clamp(shifted, gene.Min, gene.Max);
}
private static float GenerateAllele(GeneDef gene, Random random)
{
if (gene.Kind == GeneKind.Discrete)
{
return PickVariant(gene, random);
}
var value =
gene.Default + (random.NextSingle() * 2f - 1f) * gene.Spread * MathF.Abs(gene.Default);
return Math.Clamp(value, gene.Min, gene.Max);
}
private static float PickVariant(GeneDef gene, Random random)
{
var variants = Math.Max(1, gene.Variants);
if (gene.VariantWeights.Length != variants)
{
return random.Next(variants);
}
var total = 0f;
foreach (var w in gene.VariantWeights)
{
total += MathF.Max(0f, w);
}
if (total <= 0f)
{
return random.Next(variants);
}
var roll = random.NextSingle() * total;
for (var i = 0; i < variants; i++)
{
roll -= MathF.Max(0f, gene.VariantWeights[i]);
if (roll < 0f)
{
return i;
}
}
return variants - 1;
}
private static float GenerateAllele(GeneDef gene, Random random) =>
gene.Kind == GeneKind.Discrete
? GeneSampling.Variant(gene, null, random)
: GeneSampling.Numeric(gene, gene.Default, gene.Spread, random);
// Deterministic union of both parents' gene ids (ordered) so breeding is reproducible.
private static IEnumerable<string> UnionKeys(Genome a, Genome b)
@@ -0,0 +1,61 @@
namespace MrGameEng.Genetics;
/// <summary>
/// A species' (or any organism kind's) gene allotment: which <see cref="GeneDef"/>s an individual
/// carries and the per-organism base values its alleles are generated around. The same
/// <see cref="GeneDef"/> (e.g. "optimal light") is shared by every species, while the template
/// supplies the species-specific centre and spread — so an oak and grass differ in values, not in
/// machinery. <see cref="Generate"/> draws a fresh individual; <see cref="Registry"/> feeds breeding
/// and trait computation.
/// </summary>
public sealed class GenomeTemplate
{
/// <summary>
/// One gene in the allotment. <paramref name="Base"/>/<paramref name="Spread"/> centre a numeric
/// gene's alleles; <paramref name="VariantWeights"/> (optional) override a discrete gene's
/// variant distribution for this organism.
/// </summary>
public readonly record struct Entry(
GeneDef Gene,
float Base,
float Spread,
float[]? VariantWeights = null
);
private readonly List<Entry> _entries;
private readonly Dictionary<string, GeneDef> _registry;
/// <summary>Builds a template from its gene entries.</summary>
public GenomeTemplate(IEnumerable<Entry> entries)
{
_entries = entries.ToList();
_registry = new Dictionary<string, GeneDef>(StringComparer.Ordinal);
foreach (var entry in _entries)
{
_registry[entry.Gene.DefName] = entry.Gene;
}
}
/// <summary>The gene entries that make up the allotment.</summary>
public IReadOnlyList<Entry> Entries => _entries;
/// <summary>Gene id → def for every carried gene; pass to <see cref="Genome.Breed"/> and <see cref="Phenotype.Compute"/>.</summary>
public IReadOnlyDictionary<string, GeneDef> Registry => _registry;
/// <summary>Generates a fresh individual: two alleles per gene drawn around each entry's base/variant.</summary>
public Genome Generate(Random random)
{
var genome = new Genome();
foreach (var entry in _entries)
{
genome[entry.Gene.DefName] = new Allele(Draw(entry, random), Draw(entry, random));
}
return genome;
}
private static float Draw(Entry entry, Random random) =>
entry.Gene.Kind == GeneKind.Discrete
? GeneSampling.Variant(entry.Gene, entry.VariantWeights, random)
: GeneSampling.Numeric(entry.Gene, entry.Base, entry.Spread, random);
}