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>
157 lines
6.1 KiB
C#
157 lines
6.1 KiB
C#
namespace MrGameEng.Genetics;
|
|
|
|
/// <summary>
|
|
/// An individual's managed genome: a variable-composition map from gene id to the
|
|
/// <see cref="Allele"/> pair it carries. Because composition is open, two organisms need not share
|
|
/// the same gene set and a genome can gain "foreign" genes — the basis for arbitrary hybrids. The
|
|
/// genome is generated from a set of <see cref="GeneDef"/>s, bred meiotically with mutation, and
|
|
/// expressed into phenotype values; all randomness flows through a caller-owned seeded
|
|
/// <see cref="Random"/> so the simulation stays deterministic.
|
|
/// </summary>
|
|
public sealed class Genome
|
|
{
|
|
private readonly Dictionary<string, Allele> _alleles;
|
|
|
|
/// <summary>Creates an empty genome.</summary>
|
|
public Genome() => _alleles = new Dictionary<string, Allele>(StringComparer.Ordinal);
|
|
|
|
/// <summary>Creates a genome from an existing allele map (copied).</summary>
|
|
public Genome(IReadOnlyDictionary<string, Allele> alleles) =>
|
|
_alleles = new Dictionary<string, Allele>(alleles, StringComparer.Ordinal);
|
|
|
|
/// <summary>The carried genes and their allele pairs.</summary>
|
|
public IReadOnlyDictionary<string, Allele> Alleles => _alleles;
|
|
|
|
/// <summary>Whether the genome carries the gene <paramref name="geneId"/>.</summary>
|
|
public bool Has(string geneId) => _alleles.ContainsKey(geneId);
|
|
|
|
/// <summary>Gets or sets the allele pair for <paramref name="geneId"/>.</summary>
|
|
public Allele this[string geneId]
|
|
{
|
|
get => _alleles[geneId];
|
|
set => _alleles[geneId] = value;
|
|
}
|
|
|
|
/// <summary>Removes a gene from the genome; returns whether it was present.</summary>
|
|
public bool Remove(string geneId) => _alleles.Remove(geneId);
|
|
|
|
/// <summary>
|
|
/// Expresses the gene's phenotype value: the mean of the alleles for a numeric gene, the
|
|
/// dominant (lower) allele for a discrete one. Throws if the genome does not carry the gene.
|
|
/// </summary>
|
|
public float Express(GeneDef gene)
|
|
{
|
|
if (!_alleles.TryGetValue(gene.DefName, out var allele))
|
|
{
|
|
throw new KeyNotFoundException($"Genome does not carry gene '{gene.DefName}'.");
|
|
}
|
|
|
|
return gene.Kind == GeneKind.Numeric ? allele.Mean : allele.Dominant;
|
|
}
|
|
|
|
/// <summary>Builds the allele map for serialization (a copy).</summary>
|
|
public Dictionary<string, Allele> ToDictionary() => new(_alleles, StringComparer.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Generates a fresh genome carrying every gene in <paramref name="genes"/>, each allele drawn
|
|
/// independently around the gene's default with its spread (numeric) or from its variant
|
|
/// distribution (discrete).
|
|
/// </summary>
|
|
public static Genome Generate(IEnumerable<GeneDef> genes, Random random)
|
|
{
|
|
var genome = new Genome();
|
|
foreach (var gene in genes)
|
|
{
|
|
genome._alleles[gene.DefName] = new Allele(
|
|
GenerateAllele(gene, random),
|
|
GenerateAllele(gene, random)
|
|
);
|
|
}
|
|
|
|
return genome;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Breeds a child genome from two parents (meiosis): the child carries every gene either parent
|
|
/// has. For a gene both carry, one allele is drawn from each parent; for a gene only one parent
|
|
/// 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,
|
|
float? mutationChance = null
|
|
)
|
|
{
|
|
var child = new Genome();
|
|
foreach (var geneId in UnionKeys(a, b))
|
|
{
|
|
if (!registry.TryGetValue(geneId, out var gene))
|
|
{
|
|
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], 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], chance, random),
|
|
Meiosis(gene, parent[geneId], chance, random)
|
|
);
|
|
}
|
|
}
|
|
|
|
return child;
|
|
}
|
|
|
|
// One inherited allele: pick one of the parent slot's two alleles, then maybe mutate it.
|
|
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() >= mutationChance)
|
|
{
|
|
return inherited;
|
|
}
|
|
|
|
if (gene.Kind == GeneKind.Discrete)
|
|
{
|
|
return GeneSampling.Variant(gene, null, random);
|
|
}
|
|
|
|
var shifted =
|
|
inherited
|
|
+ (random.NextSingle() * 2f - 1f) * gene.MutationMagnitude * MathF.Abs(inherited);
|
|
return Math.Clamp(shifted, gene.Min, gene.Max);
|
|
}
|
|
|
|
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)
|
|
{
|
|
var keys = new SortedSet<string>(StringComparer.Ordinal);
|
|
keys.UnionWith(a._alleles.Keys);
|
|
keys.UnionWith(b._alleles.Keys);
|
|
return keys;
|
|
}
|
|
}
|